feat: revive only whole RFC3339 timestamps (never custom field values or text that starts with a date), keep equal revived Dates across React Query refetches so unchanged data keeps its identity, type every revived response timestamp in the dashboard models as Date, reset the launch dialog only when a different campaign opens, show the build on the page error panel, and tag Cloudflare Pages dashboard and admin builds with their commit as the release

This commit is contained in:
Matthew Meszaros
2026-09-27 14:59:06 +02:00
parent 1dbbee61c6
commit ea5dd435d7
108 changed files with 406 additions and 270 deletions
+4 -1
View File
@@ -1,9 +1,12 @@
import { defineConfig } from "vite";
if (!process.env.VITE_SENTRY_RELEASE && process.env.CF_PAGES_COMMIT_SHA) process.env.VITE_SENTRY_RELEASE = process.env.CF_PAGES_COMMIT_SHA;import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
import tailwindcss from "@tailwindcss/vite";
import { sentryVitePlugin } from "@sentry/vite-plugin";
// A Cloudflare Pages build sets no release, so its events are tagged with the commit it built.
if (!process.env.VITE_SENTRY_RELEASE && process.env.CF_PAGES_COMMIT_SHA) process.env.VITE_SENTRY_RELEASE = process.env.CF_PAGES_COMMIT_SHA;
// Source maps, and nothing else, is what this section decides.
//
// Uploading them is never a required build step: a fork, a self-host build or a
@@ -545,7 +545,7 @@ function Legend({ color, label }: { color: string; label: string }) {
);
}
function fmtRelative(iso: string): string {
function fmtRelative(iso: string | Date): string {
try {
const d = new Date(iso);
const diff = Date.now() - d.getTime();
@@ -563,7 +563,7 @@ function fmtRelative(iso: string): string {
}
}
function fmtFull(iso: string): string {
function fmtFull(iso: string | Date): string {
try {
const d = new Date(iso);
return d.toLocaleString("en-US", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit" });
@@ -574,7 +574,7 @@ function fmtFull(iso: string): string {
// Compact HH:MM for the mobile activity log, where the full timestamp
// would crowd out the endpoint path.
function fmtTime(iso: string): string {
function fmtTime(iso: string | Date): string {
try {
const d = new Date(iso);
return d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false });
@@ -59,7 +59,7 @@ export function Sparkline({
}
export interface BucketLike {
bucket: string;
bucket: Date;
success: number;
client_errors: number;
server_errors: number;
@@ -90,7 +90,7 @@ export function StackedBars({
<div className="relative w-full" style={{ height }}>
<DitherColumns
data={buckets.map((b) => ({
key: b.bucket,
key: b.bucket.toISOString(),
parts: [b.success, b.client_errors, b.server_errors],
}))}
tones={["emerald", "amber", "rose"]}
@@ -107,7 +107,7 @@ export function StackedBars({
);
}
function labelTick(iso: string | undefined): string {
function labelTick(iso: Date | undefined): string {
if (!iso) return "";
try {
const d = new Date(iso);
+2 -2
View File
@@ -332,7 +332,7 @@ function Legend({ color, label }: { color: string; label: string }) {
);
}
function fmtRelative(iso: string): string {
function fmtRelative(iso: string | Date): string {
try {
const d = new Date(iso);
const diff = Date.now() - d.getTime();
@@ -350,7 +350,7 @@ function fmtRelative(iso: string): string {
}
}
function fmtFull(iso: string): string {
function fmtFull(iso: string | Date): string {
try {
return new Date(iso).toLocaleString("en-US", {
month: "short",
+1 -1
View File
@@ -412,7 +412,7 @@ function actorLabel(log: AuditLog): string {
return "System";
}
function fmt(d: string) {
function fmt(d: string | Date) {
if (!d) return "—";
try {
const dt = new Date(d);
+4 -3
View File
@@ -56,6 +56,7 @@ import { useLivePatch } from "@/hooks/useLivePatch";
import { cursorColor } from "@/hooks/useLiveCursors";
import ResourceViewers from "@/components/app/presence/ResourceViewers";
import type Deal from "@/lib/api/models/app/crm/Deal";
import type { DealWrite } from "@/lib/api/models/app/crm/Deal";
import type { Stage } from "@/lib/api/models/app/crm/Pipeline";
import type { AppError } from "@/lib/api/client/normalizeError";
import buildError from "@/lib/helper/buildError";
@@ -159,7 +160,7 @@ export default function DealsPage() {
async function moveDeal(dealId: string, newStageId: string) {
try {
await toast.promise(
updateDeal.mutateAsync({ id: dealId, data: { stage_id: newStageId } as Partial<Deal> }),
updateDeal.mutateAsync({ id: dealId, data: { stage_id: newStageId } as DealWrite }),
{ loading: "Moving…", success: "Moved", error: (e: AppError) => buildError(e) },
);
// Nudge teammates on the same board to update now (the audit refetch is
@@ -694,7 +695,7 @@ function DealDialog({
toast.error("Pick a stage");
return;
}
const data: Partial<Deal> = {
const data: DealWrite = {
pipeline_id: pipelineId,
stage_id: stageId,
name: name.trim(),
@@ -949,7 +950,7 @@ function formatMoney(n: number | undefined, currency = "USD") {
}
}
function fmtDate(d: string | undefined) {
function fmtDate(d: string | Date | undefined) {
if (!d) return "—";
try {
return new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric" });
+2 -2
View File
@@ -71,7 +71,7 @@ const STATUS_STYLE: Record<MeetingStatus, { label: string; cls: string }> = {
no_show: { label: "No-show", cls: "bg-red-50 text-red-700 border-red-200" },
};
function formatWhen(iso?: string): { date: string; time: string; rel: string } {
function formatWhen(iso?: string | Date): { date: string; time: string; rel: string } {
if (!iso) return { date: "No time set", time: "", rel: "" };
const d = new Date(iso);
if (isNaN(d.getTime())) return { date: "No time set", time: "", rel: "" };
@@ -87,7 +87,7 @@ function formatWhen(iso?: string): { date: string; time: string; rel: string } {
// --- Add to calendar (no API: a Google template link + a downloadable .ics) ---
function gcalStamp(iso: string): string {
function gcalStamp(iso: string | Date): string {
return new Date(iso).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
}
+7 -6
View File
@@ -80,6 +80,7 @@ import { useSuppressGlobalCursors } from "@/components/app/presence/GlobalCursor
import { useLivePatch } from "@/hooks/useLivePatch";
import ResourceViewers from "@/components/app/presence/ResourceViewers";
import type CRMTask from "@/lib/api/models/app/crm/CRMTask";
import type { CRMTaskWrite } from "@/lib/api/models/app/crm/CRMTask";
import type { CRMTaskPriority, CRMTaskStatus } from "@/lib/api/models/app/crm/CRMTask";
import type SearchTasks from "@/lib/api/models/app/crm/SearchTasks";
import type { TaskSortBy } from "@/lib/api/models/app/crm/SearchTasks";
@@ -146,7 +147,7 @@ const TONE = {
muted: { dot: "bg-slate-300", label: "text-slate-500" },
} as const;
function bucketize(due: string | undefined): Bucket {
function bucketize(due: string | Date | undefined): Bucket {
if (!due) return "no_due";
const d = new Date(due);
if (Number.isNaN(d.getTime())) return "no_due";
@@ -739,7 +740,7 @@ function FlatRow({
try {
await update.mutateAsync({
id: task.id,
data: { status: done ? "completed" : "pending" } as Partial<CRMTask>,
data: { status: done ? "completed" : "pending" } as CRMTaskWrite,
});
} catch (err) {
toast.error(buildError(err as AppError));
@@ -1012,7 +1013,7 @@ function GroupedRow({
e.stopPropagation();
const next: CRMTaskStatus = isDone ? "pending" : "completed";
try {
await update.mutateAsync({ id: task.id, data: { status: next } as Partial<CRMTask> });
await update.mutateAsync({ id: task.id, data: { status: next } as CRMTaskWrite });
} catch (err) {
toast.error(buildError(err as AppError));
}
@@ -1210,7 +1211,7 @@ function TaskTypeTag({
);
}
function DueCell({ due, overdue }: { due: string | undefined; overdue: boolean }) {
function DueCell({ due, overdue }: { due: string | Date | undefined; overdue: boolean }) {
if (!due) return <span className="text-slate-300 text-[11px]">—</span>;
if (overdue) {
return (
@@ -1823,7 +1824,7 @@ function TaskDialog({
toast.error("Title required");
return;
}
const data: Partial<CRMTask> = {
const data: CRMTaskWrite = {
title: title.trim(),
priority,
type,
@@ -2210,7 +2211,7 @@ function hasAnyFilter(f: SearchTasks): boolean {
);
}
function fmtDue(d: string) {
function fmtDue(d: string | Date) {
try {
const dt = new Date(d);
const now = new Date();
@@ -285,14 +285,14 @@ function describeReason(reason: string): string {
return map[reason] ?? reason.replace(/_/g, " ");
}
function formatReset(value: string | null | undefined): string {
function formatReset(value: string | Date | null | undefined): string {
if (!value) return "on renewal";
const d = new Date(value);
if (Number.isNaN(d.getTime())) return "on renewal";
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}
function formatWhen(value: string): string {
function formatWhen(value: string | Date): string {
const d = new Date(value);
if (Number.isNaN(d.getTime())) return "—";
return d.toLocaleDateString("en-US", {
+1 -1
View File
@@ -517,7 +517,7 @@ function describeRedemption(d: DiscountRedemption): string {
return d.type || "Discount";
}
function fmtDate(value?: string | null): string {
function fmtDate(value?: string | Date | null): string {
if (!value) return "—";
const d = new Date(value);
if (Number.isNaN(d.getTime())) return "—";
+1 -1
View File
@@ -488,7 +488,7 @@ function formatMoney(cents: number, currency: string): string {
}
}
function formatDate(value?: string): string {
function formatDate(value?: string | Date): string {
if (!value) return "—";
const d = new Date(value);
if (Number.isNaN(d.getTime())) return "—";
@@ -19,7 +19,7 @@ type Dialog = "enroll" | "regenerate" | "disable" | null;
// Below this many unused codes the row turns amber and asks for a new set.
const LOW_CODES = 3;
const fmt = (d: string) =>
const fmt = (d: string | Date) =>
new Date(d).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" });
export default function TwoFactorManager() {
+1 -1
View File
@@ -1166,7 +1166,7 @@ function DockBar({
// ── History rail ────────────────────────────────────────────────────
// historyBucket labels a session's recency group for the sidebar sections.
function historyBucket(iso: string): string {
function historyBucket(iso: string | Date): string {
const t = new Date(iso).getTime();
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
@@ -16,6 +16,7 @@ import { EyeIcon, InboxIcon, MailIcon, ReplyIcon } from "lucide-react";
import { EmptyBlock, SectionBar, Stat, StatStrip } from "@/components/layout/Page";
import { MultiTrend, type TrendSeries } from "@/components/ui/charts";
import { utcDay } from "@/lib/campaignPeriod";
import useDirectMail from "@/lib/api/hooks/app/analytics/useDirectMail";
import type DirectMailAnalytics from "@/lib/api/models/app/analytics/DirectMailAnalytics";
@@ -60,7 +61,7 @@ export default function DirectMailSection({ period }: { period: string }) {
{ key: "sent", label: "Sent", tone: "sky", values: (d?.daily_trend ?? []).map((x) => x.sent) },
{ key: "received", label: "Received", tone: "emerald", values: (d?.daily_trend ?? []).map((x) => x.received) },
];
const labels = (d?.daily_trend ?? []).map((x) => x.date);
const labels = (d?.daily_trend ?? []).map((x) => utcDay(x.date));
if (q.isError) {
return (
@@ -133,7 +133,8 @@ export default function LaunchCampaignDialog({
setRiskBlocked(false);
setWaiting(false);
}
}, [campaign]);
// eslint-disable-next-line react-hooks/exhaustive-deps -- keyed on the id: a launch changes the campaign itself
}, [campaign?.id]);
React.useEffect(() => {
return () => {
@@ -123,7 +123,7 @@ const LIMITED_BY: Record<MailboxPlan["limited_by"], string> = {
workspace_risk: "workspace posture",
};
function fmtTime(iso: string | undefined, tz: string): string {
function fmtTime(iso: string | Date | undefined, tz: string): string {
if (!iso) return "";
try {
return new Date(iso).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", timeZone: tz });
@@ -132,7 +132,7 @@ function fmtTime(iso: string | undefined, tz: string): string {
}
}
function fmtDay(iso: string | undefined, tz: string): string {
function fmtDay(iso: string | Date | undefined, tz: string): string {
if (!iso) return "";
const d = new Date(iso);
const today = new Date();
@@ -1219,7 +1219,7 @@ function StatCard({
);
}
function durationText(start: string, end: string): string {
function durationText(start: string | Date, end: string | Date): string {
const s = new Date(start).getTime();
const e = new Date(end).getTime();
if (Number.isNaN(s) || Number.isNaN(e)) return "—";
@@ -23,7 +23,7 @@ describe("verification provider attribution", () => {
verification_status: "invalid",
verification_source: "provider",
verification_provider: "millionverifier",
verification_requested_at: "2026-09-22T10:00:00Z",
verification_requested_at: new Date("2026-09-22T10:00:00Z"),
})).toBe("Undeliverable · verified with MillionVerifier · re-check queued");
});
+2 -2
View File
@@ -42,9 +42,9 @@ export interface ContactRow {
verification_sub_status?: string;
verification_source?: VerificationSource;
verification_provider?: string;
verification_checked_at?: string | null;
verification_checked_at?: Date | null;
verification_confidence?: number;
verification_requested_at?: string | null;
verification_requested_at?: Date | null;
mail_host?: string;
created_at: Date;
updated_at?: Date;
@@ -365,7 +365,7 @@ function LatestRow({
icon,
}: {
label: string;
ts?: string | null;
ts?: Date | string | null;
icon: React.ReactNode;
}) {
return (
+1 -1
View File
@@ -570,7 +570,7 @@ function money(n: number | undefined, currency = "USD") {
}
}
function fmtDate(d: string | undefined) {
function fmtDate(d: string | Date | undefined) {
if (!d) return "—";
try {
return new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric" });
@@ -25,7 +25,7 @@ const REASON_COPY: Record<SyncThrottleReason, string> = {
priority_daily: "the daily limit for replies was reached",
};
function relative(iso: string): string {
function relative(iso: string | Date): string {
const diff = Date.now() - new Date(iso).getTime();
const m = Math.round(diff / 60_000);
if (m < 1) return "just now";
@@ -35,7 +35,7 @@ function relative(iso: string): string {
return new Date(iso).toLocaleDateString();
}
function until(iso: string): string {
function until(iso: string | Date): string {
const d = new Date(iso);
const sameDay = d.toDateString() === new Date().toDateString();
return sameDay
@@ -61,7 +61,9 @@ import useUpdateDeal from "@/lib/api/hooks/app/crm/deals/useUpdateDeal";
import usePipelines from "@/lib/api/hooks/app/crm/pipelines/usePipelines";
import type { Stage } from "@/lib/api/models/app/crm/Pipeline";
import type Deal from "@/lib/api/models/app/crm/Deal";
import type { DealWrite } from "@/lib/api/models/app/crm/Deal";
import type CRMTask from "@/lib/api/models/app/crm/CRMTask";
import type { CRMTaskWrite } from "@/lib/api/models/app/crm/CRMTask";
import type { AppError } from "@/lib/api/client/normalizeError";
import buildError from "@/lib/helper/buildError";
@@ -369,7 +371,7 @@ function campaignLine(s: ContactCampaignState): { text: string; title?: string }
return { text, title: text };
}
function shortDate(iso: string): string {
function shortDate(iso: string | Date): string {
return new Date(iso).toLocaleString(undefined, {
month: "short",
day: "numeric",
@@ -424,7 +426,7 @@ function DealsSection({
toast.error("Deal name required");
return;
}
const data: Partial<Deal> = {
const data: DealWrite = {
pipeline_id: pipelineId,
stage_id: stageId,
name: name.trim(),
@@ -453,7 +455,7 @@ function DealsSection({
async function moveDeal(dealId: string, newStageId: string) {
try {
await toast.promise(
updateDeal.mutateAsync({ id: dealId, data: { stage_id: newStageId } as Partial<Deal> }),
updateDeal.mutateAsync({ id: dealId, data: { stage_id: newStageId } as DealWrite }),
{ loading: "Moving…", success: "Moved", error: (e: AppError) => buildError(e) },
);
} catch {
@@ -639,7 +641,7 @@ function TasksSection({
toast.error("Task title required");
return;
}
const data: Partial<CRMTask> = {
const data: CRMTaskWrite = {
title: title.trim(),
contact_id: contactId,
priority,
@@ -26,7 +26,7 @@ import type UniboxScheduledItem from "@/lib/api/models/app/unibox/UniboxSchedule
import { cn } from "@/lib/utils";
import ComposeButton from "@/components/app/unibox/compose/ComposeButton";
function formatWhen(iso: string): { absolute: string; relative: string } {
function formatWhen(iso: string | Date): { absolute: string; relative: string } {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return { absolute: "—", relative: "" };
+2 -2
View File
@@ -777,9 +777,9 @@ function IconAction({
// Friendly relative-or-absolute time used for scheduled cards.
// Examples: "in 12 min", "in 3 h", "tomorrow, 09:00", "Mar 5, 17:00".
function formatScheduled(iso: string): string {
function formatScheduled(iso: string | Date): string {
const d = new Date(iso);
if (!Number.isFinite(d.getTime())) return iso;
if (!Number.isFinite(d.getTime())) return String(iso);
const now = new Date();
const diffMs = d.getTime() - now.getTime();
const diffMin = Math.round(diffMs / 60_000);
@@ -15,7 +15,7 @@ function draftTitle(d: ComposeDraft): string {
return d.subject.trim() || d.to[0] || "(no subject)";
}
function formatWhen(iso: string): string {
function formatWhen(iso: string | Date): string {
const d = new Date(iso);
const now = new Date();
const sameDay =
@@ -30,7 +30,7 @@ interface ComposeHistoryPanelProps {
affinityLine?: string;
}
function formatWhen(iso: string): string {
function formatWhen(iso: string | Date): string {
const d = new Date(iso);
const now = new Date();
const sameYear = d.getFullYear() === now.getFullYear();
@@ -367,7 +367,7 @@ function ComposeWindowInner({
// Undo window: the send is queued a few seconds out. No
// "sent" toast; the header pill counts down and can cancel,
// reopening the composer from this seed.
const nowIso = new Date().toISOString();
const now = new Date();
addOutbox({
taskId: res.task_id,
scheduledAt: resolveSendAt(res.scheduled_at, user.undo_send_seconds || 30),
@@ -382,8 +382,8 @@ function ComposeWindowInner({
bcc,
subject: subject.trim(),
body: trimmedBody,
updated_at: nowIso,
created_at: nowIso,
updated_at: now,
created_at: now,
},
});
} else {
+1 -1
View File
@@ -321,7 +321,7 @@ function formatCredits(n: number): string {
}
// resetLabel turns next_reset_at into a short relative note for the panel.
function resetLabel(iso: string | null): string | null {
function resetLabel(iso: string | Date | null): string | null {
if (!iso) return null;
const days = Math.ceil((new Date(iso).getTime() - Date.now()) / 86_400_000);
if (days <= 0) return "Resets soon";
@@ -13,6 +13,7 @@ import React from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { AlertTriangleIcon, RefreshCcwIcon } from "lucide-react";
import { captureException } from "@/lib/observability";
import { SENTRY_RELEASE } from "@/lib/information";
interface State {
error: Error | null;
@@ -108,6 +109,7 @@ function BoundaryFallback({ error, info, reset }: { error: Error; info: React.Er
Stack
</summary>
<pre className="px-3 py-3 text-[10.5px] font-mono text-slate-700 leading-relaxed overflow-x-auto whitespace-pre-wrap border-t border-slate-200">
{SENTRY_RELEASE ? `Build: ${SENTRY_RELEASE}\n\n` : ""}
{error.stack || ""}
{info?.componentStack ? `\n\nComponent stack:${info.componentStack}` : ""}
</pre>
@@ -73,7 +73,7 @@ function bucketFor(d: Date): Bucket {
}
// Compact relative timestamp: "now", "2m", "3h", "Yesterday", then a short date.
function relTime(iso: string): string {
function relTime(iso: string | Date): string {
const d = new Date(iso);
const s = Math.max(0, Math.floor((Date.now() - d.getTime()) / 1000));
if (s < 60) return "now";
@@ -22,7 +22,7 @@ export interface CampaignEstimateResult {
remaining_today: number;
// Null when the pool has no capacity or the audience is empty.
sending_days: number | null;
estimated_finish_at: string | null;
estimated_finish_at: Date | null;
}
export default async function estimateCampaign(input: CampaignEstimateInput): Promise<CampaignEstimateResult> {
@@ -6,7 +6,7 @@ interface CampaignLogRow {
event_type: string;
message: string;
metadata?: Record<string, unknown> | null;
created_at: string;
created_at: Date;
}
export interface CampaignLogItem {
@@ -66,8 +66,9 @@ export interface ImportResult {
updated: number;
skipped: number;
failed: number;
started_at: string;
ended_at: string;
// A Date when revived through Request (a lead-sync run); a string from the direct import upload.
started_at: Date | string;
ended_at: Date | string;
errors?: ImportRowError[];
// Set when more rows failed than the API reports back; `errors` then holds
// the first slice of them and `failed` is the true count.
@@ -1,7 +1,8 @@
import type Deal from "@/lib/api/models/app/crm/Deal";
import type { DealWrite } from "@/lib/api/models/app/crm/Deal";
import Request from "../../../Request";
export default async function createDeal(data: Partial<Deal>): Promise<Deal> {
export default async function createDeal(data: DealWrite): Promise<Deal> {
return await Request<Deal>({
method: "POST",
url: `/crm/deals`,
@@ -1,7 +1,8 @@
import type Deal from "@/lib/api/models/app/crm/Deal";
import type { DealWrite } from "@/lib/api/models/app/crm/Deal";
import Request from "../../../Request";
export default async function updateDeal(id: string, data: Partial<Deal>): Promise<Deal> {
export default async function updateDeal(id: string, data: DealWrite): Promise<Deal> {
return await Request<Deal>({
method: "PATCH",
url: `/crm/deals/${id}`,
@@ -1,7 +1,8 @@
import type CRMTask from "@/lib/api/models/app/crm/CRMTask";
import type { CRMTaskWrite } from "@/lib/api/models/app/crm/CRMTask";
import Request from "../../../Request";
export default async function createCRMTask(data: Partial<CRMTask>): Promise<CRMTask> {
export default async function createCRMTask(data: CRMTaskWrite): Promise<CRMTask> {
return await Request<CRMTask>({
method: "POST",
url: `/crm/tasks`,
@@ -1,7 +1,8 @@
import type CRMTask from "@/lib/api/models/app/crm/CRMTask";
import type { CRMTaskWrite } from "@/lib/api/models/app/crm/CRMTask";
import Request from "../../../Request";
export default async function updateCRMTask(id: string, data: Partial<CRMTask>): Promise<CRMTask> {
export default async function updateCRMTask(id: string, data: CRMTaskWrite): Promise<CRMTask> {
return await Request<CRMTask>({
method: "PATCH",
url: `/crm/tasks/${id}`,
@@ -15,7 +15,7 @@ interface RawMembership {
avatar?: string;
avatar_url?: string | null;
plan?: string;
created_at: string;
created_at: Date;
};
}
@@ -19,8 +19,8 @@ export interface AIThreadDraft {
confidence: number
model: string
status: "pending" | "approved" | "discarded"
created_at: string
updated_at: string
created_at: Date
updated_at: Date
}
// listAgentDrafts returns the org's pending inbox-agent drafts, newest first.
@@ -12,8 +12,8 @@ export interface ComposeDraft {
bcc: string[];
subject: string;
body: string;
updated_at: string;
created_at: string;
updated_at: Date;
created_at: Date;
}
export interface ComposeDraftSaveInput {
@@ -14,7 +14,7 @@ export interface UniboxListRow {
to_addr: string[];
subject: string;
snippet: string;
internal_date: string;
internal_date: Date;
seen: boolean;
/** Messages in the conversation behind this row (1 = singleton). */
message_count: number;
@@ -10,7 +10,7 @@ export interface SnoozeRequest {
export interface SnoozeResponse {
id: string;
thread_id: string;
snoozed_until: string;
snoozed_until: Date;
}
export async function snoozeThread(req: SnoozeRequest): Promise<SnoozeResponse> {
+1 -1
View File
@@ -4,7 +4,7 @@ import Request from "../Request";
export interface TwoFactorStatus {
enabled: boolean;
/** When the authenticator was confirmed. Absent while 2FA is off. */
confirmed_at?: string;
confirmed_at?: Date;
recovery_codes_remaining: number;
recovery_codes_total: number;
}
@@ -1,12 +1,12 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type Deal from "@/lib/api/models/app/crm/Deal";
import type { DealWrite } from "@/lib/api/models/app/crm/Deal";
import createDeal from "@/lib/api/client/app/crm/deals/createDeal";
export default function useCreateDeal() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: Partial<Deal>) => createDeal(data),
mutationFn: (data: DealWrite) => createDeal(data),
onSuccess: (_result, variables) => {
// Broad prefix so the board list, the cross-pipeline search table,
// and the summary aggregates all refresh.
@@ -1,12 +1,12 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type Deal from "@/lib/api/models/app/crm/Deal";
import type { DealWrite } from "@/lib/api/models/app/crm/Deal";
import updateDeal from "@/lib/api/client/app/crm/deals/updateDeal";
export default function useUpdateDeal() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: Partial<Deal> }) => updateDeal(id, data),
mutationFn: ({ id, data }: { id: string; data: DealWrite }) => updateDeal(id, data),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["crm", "deals"],
@@ -1,5 +1,5 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type CRMTask from "@/lib/api/models/app/crm/CRMTask";
import type { CRMTaskWrite } from "@/lib/api/models/app/crm/CRMTask";
import createCRMTask from "@/lib/api/client/app/crm/tasks/createCRMTask";
import { useLivePatch } from "@/hooks/useLivePatch";
@@ -8,7 +8,7 @@ export default function useCreateCRMTask() {
const { pushPatch } = useLivePatch("crm_tasks");
return useMutation({
mutationFn: (data: Partial<CRMTask>) => createCRMTask(data),
mutationFn: (data: CRMTaskWrite) => createCRMTask(data),
onSuccess: () => {
// Broad prefix so the server-driven search list, the summary totals,
// and the sidebar overdue indicator all refresh on create.
@@ -1,5 +1,5 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type CRMTask from "@/lib/api/models/app/crm/CRMTask";
import type { CRMTaskWrite } from "@/lib/api/models/app/crm/CRMTask";
import updateCRMTask from "@/lib/api/client/app/crm/tasks/updateCRMTask";
import { useLivePatch } from "@/hooks/useLivePatch";
@@ -10,7 +10,7 @@ export default function useUpdateCRMTask() {
const { pushPatch } = useLivePatch("crm_tasks");
return useMutation({
mutationFn: ({ id, data }: { id: string; data: Partial<CRMTask> }) => updateCRMTask(id, data),
mutationFn: ({ id, data }: { id: string; data: CRMTaskWrite }) => updateCRMTask(id, data),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["crm", "tasks"],
@@ -107,10 +107,10 @@ export interface AdvisorFinding {
evidence?: Record<string, unknown>;
action?: AdvisorAction;
first_seen_at: string;
last_seen_at: string;
snoozed_until?: string;
applied_at?: string;
first_seen_at: Date;
last_seen_at: Date;
snoozed_until?: Date;
applied_at?: Date;
applied_result?: string;
}
@@ -129,7 +129,7 @@ export interface AdvisorSummary {
medium: number;
low: number;
surfaces: AdvisorSurfaceCount[];
last_run_at?: string;
last_run_at?: Date;
}
export interface AdvisorSettings {
@@ -142,7 +142,7 @@ export interface AdvisorSettings {
// it on.
autopilot: boolean;
autopilot_actor_id?: string;
updated_at: string;
updated_at: Date;
}
export interface AdvisorFindingsQuery {
+2 -2
View File
@@ -14,8 +14,8 @@ export interface AgentSession {
model?: string;
pending?: PendingAgentTool | null;
};
created_at: string;
updated_at: string;
created_at: Date;
updated_at: Date;
}
export interface PendingAgentTool {
@@ -17,7 +17,7 @@ export interface AccountError {
title: string;
message: string;
action_required?: string;
created_at: string;
created_at: Date;
}
export interface AccountDailyUsage {
@@ -31,8 +31,8 @@ export interface AccountDailyUsage {
export interface WarmupStatusInfo {
enabled: boolean;
paused: boolean;
paused_at?: string | null;
started_at: string;
paused_at?: Date | null;
started_at: Date;
current_volume: number;
target_volume: number;
max_volume: number;
@@ -58,7 +58,7 @@ export interface WarmupRampHold {
placements: number;
sends: number;
volume_cut: boolean;
resumes_at: string;
resumes_at: Date;
}
// Warmup-pool reputation for this mailbox. Folded into health.score and also
@@ -71,8 +71,8 @@ export interface WarmupHealthInfo {
/** @deprecated Always 0 since the warmup spam score was retired; read score and reason. */
spam_score: number;
reason?: string;
blocked_until?: string | null;
evaluated_at?: string | null;
blocked_until?: Date | null;
evaluated_at?: Date | null;
/** Distinct warmup partners over the last 7 days: mailboxes, their domains, and the workspaces behind them. */
partner_mailboxes_7d: number;
partner_domains_7d: number;
@@ -87,7 +87,7 @@ export default interface AccountStatus {
email: string;
provider: string;
status: string;
last_synced_at: string | null;
last_synced_at: Date | null;
health: AccountHealth;
errors: AccountError[];
daily_usage: AccountDailyUsage;
@@ -117,6 +117,6 @@ export type SendLifecycle = "active" | "resting" | "reserve";
// Why a mailbox is not being offered cold sends.
export interface SendLifecycleState {
state: SendLifecycle;
since?: string;
since?: Date;
reason?: string;
}
@@ -29,7 +29,7 @@ export interface RecentActivityItem {
campaign_name: string
contact_email: string
contact_id?: string
timestamp: string
timestamp: Date
link?: string
// Client, device and location of a person's open or click, when logged.
origin?: EngagementOrigin
@@ -64,8 +64,8 @@ export interface WarmupHostPlacement {
}
export default interface DeliverabilityDashboard {
from: string;
to: string;
from: Date;
to: Date;
events_total: number;
bounce_count: number;
complaint_count: number;
@@ -41,7 +41,7 @@ export interface DirectMailTracking {
}
export interface DirectMailDailyStats {
date: string;
date: Date;
sent: number;
received: number;
}
@@ -58,7 +58,7 @@ export interface DirectMailContact {
email: string;
sent: number;
received: number;
last_at: string;
last_at: Date;
}
export default interface DirectMailAnalytics {
+5 -5
View File
@@ -23,14 +23,14 @@ export default interface APIKey {
rate_limit_per_minute: number;
status: APIKeyStatus;
last_used_at?: string | null;
last_used_at?: Date | null;
last_request_ip?: string | null;
expires_at?: string | null;
revoked_at?: string | null;
expires_at?: Date | null;
revoked_at?: Date | null;
revoked_reason?: string | null;
created_at: string;
updated_at: string;
created_at: Date;
updated_at: Date;
}
// The `status` column only ever holds "active" or "revoked": expiry is applied
@@ -5,11 +5,11 @@ export interface APIKeyUsageSummary {
requests_24h: number;
errors_24h: number;
avg_latency_ms_24h: number;
last_call_at?: string | null;
last_call_at?: Date | null;
}
export interface APIKeyUsageBucket {
bucket: string;
bucket: Date;
total: number;
success: number;
client_errors: number;
@@ -27,8 +27,8 @@ export interface APIKeyEndpointStat {
export interface APIKeyAnalytics {
api_key_id: string;
from: string;
to: string;
from: Date;
to: Date;
interval: "minute" | "hour" | "day";
buckets: APIKeyUsageBucket[];
endpoints: APIKeyEndpointStat[];
@@ -45,7 +45,7 @@ export interface APIKeyUsageLog {
user_agent: string;
response_code: number;
response_time_ms: number;
created_at: string;
created_at: Date;
}
export interface APIKeyUsageLogsResult {
+2 -2
View File
@@ -82,7 +82,7 @@ export default interface AuditLog {
org_id: string;
user_id: string; // actor id (kept for backwards compatibility)
actor?: AuditActor;
action_date: string;
action_date: Date;
action: AuditAction;
entity_type: AuditEntityType;
entity_id?: string;
@@ -90,7 +90,7 @@ export default interface AuditLog {
user_agent: string;
changes?: Record<string, string>;
metadata?: Record<string, string>;
timestamp: string;
timestamp: Date;
}
export interface AuditLogsResult {
@@ -104,8 +104,8 @@ export interface Automation {
trigger_event: string;
filter?: Record<string, unknown>;
graph: AutomationGraph;
created_at: string;
updated_at: string;
created_at: Date;
updated_at: Date;
// Public POST path that fires this automation, set only when its trigger is
// the inbound webhook. Append to the API origin for the full URL.
inbound_url?: string;
@@ -139,8 +139,8 @@ export interface AutomationRun {
status: string; // running | success | error
node_results: AutomationNodeResult[];
error_detail?: string;
started_at: string;
finished_at?: string | null;
started_at: Date;
finished_at?: Date | null;
}
// Dry-run (test) response: the trace of the walk + the sample data used.
@@ -61,7 +61,7 @@ export default interface Campaign {
ramp_increment: number;
ramp_ceiling: number;
ramp_level: number;
ramp_level_date?: string | null;
ramp_level_date?: Date | null;
// Match the sending mailbox provider to the recipient's provider.
esp_match_mode: 'off' | 'prefer' | 'strict';
@@ -77,7 +77,7 @@ export default interface Campaign {
// Keep running for new leads: out of leads, the campaign waits (idle_since
// set) instead of finishing. Linking a segment turns it on.
continuous: boolean;
idle_since?: string | null;
idle_since?: Date | null;
// Auto-pause guardrails. Bounce and complaint rates are ceilings (pause at
// or above); the reply rate is a floor (pause below). A rate of 0 turns its
@@ -88,13 +88,13 @@ export default interface Campaign {
guardrail_reply_rate_min: number;
guardrail_min_sample: number;
guardrail_window_days: number;
guardrail_tripped_at?: string | null;
guardrail_tripped_at?: Date | null;
guardrail_reason?: string;
// Campaign-scoped tracking-domain override (honored only when verified).
tracking_domain: string;
tracking_domain_verified: boolean;
tracking_domain_verified_at?: string | null;
tracking_domain_verified_at?: Date | null;
// Automatic UTM tagging of every link. Empty source/medium/campaign keep
// the defaults (warmbly / email / the campaign name); utm_content is
@@ -120,7 +120,7 @@ export interface ScheduleInterval {
export interface CampaignSender {
email_account_id: string;
weight: number;
last_sent_at?: string | null;
last_sent_at?: Date | null;
enabled: boolean;
}
@@ -10,7 +10,7 @@ export default interface EmailImage {
width: number;
height: number;
url: string;
created_at: string;
created_at: Date;
}
// One keyset page of the library, newest first.
@@ -17,7 +17,7 @@ export interface PreflightReport {
score: number;
checks: PreflightCheckResult[];
recommendations: string[];
created_at: string;
created_at: Date;
}
// Only failures are worth a user's attention; a passing check is noise in a
@@ -43,11 +43,11 @@ export interface SendLimit {
export interface SendWindow {
sending_day: boolean;
open_now: boolean;
opens_at?: string;
closes_at?: string;
opens_at?: Date;
closes_at?: Date;
minutes_left: number;
starts_at?: string;
ends_at?: string;
starts_at?: Date;
ends_at?: Date;
}
export interface LeadSupply {
@@ -60,7 +60,7 @@ export interface LeadSupply {
waiting_on_sender: number;
new_leads_started_today: number;
max_new_leads_per_day: number;
next_due_at?: string;
next_due_at?: Date;
}
export interface ColdRampInfo {
@@ -81,7 +81,7 @@ export interface MailboxPlan {
sent_by_other_campaigns: number;
expected_remaining: number;
state: MailboxPlanState;
reopens_at?: string;
reopens_at?: Date;
health?: string;
min_gap_seconds: number;
graduation?: ColdRampInfo;
@@ -98,7 +98,7 @@ export default interface SendPlan {
status: string;
day: string;
timezone: string;
computed_at: string;
computed_at: Date;
configured_ceiling: number;
projected_today: number;
sent_today: number;
@@ -109,7 +109,7 @@ export default interface SendPlan {
leads: LeadSupply;
mailboxes: MailboxPlan[];
organization?: OrgAllowance;
next_wake_at?: string;
next_wake_at?: Date;
}
// What the workspace's mailboxes can send today between them, on the
@@ -13,6 +13,6 @@ export interface CLIAuthCode {
status: CLIAuthCodeStatus;
organization_id?: string;
api_key_id?: string;
expires_at: string;
created_at: string;
expires_at: Date;
created_at: Date;
}
@@ -24,8 +24,8 @@ export type LeadStatus =
// when an auto-reply parked it and "manual" when a member did; `until` absent
// means the hold has no end and only a resume lifts it.
export interface LeadHold {
since: string;
until?: string | null;
since: Date;
until?: Date | null;
reason?: string;
source: "manual" | "out_of_office" | string;
}
@@ -70,7 +70,7 @@ export interface ContactCampaignProgress {
clicked: number;
replied: number;
bounced: number;
last_activity_at?: string | null;
last_activity_at?: Date | null;
// Label of the step the lead is on now (latest step sent). Empty when the
// lead hasn't been contacted yet.
current_step?: string;
@@ -114,13 +114,13 @@ export default interface Contact {
verification_sub_status?: string;
verification_source?: VerificationSource;
verification_provider?: string;
verification_checked_at?: string | null;
verification_checked_at?: Date | null;
// How sure the platform is of the status, 0 to 100, scored from the last
// check plus what real mail to the address showed.
verification_confidence?: number;
// Set while a re-check a member asked for waits to run; the verdict
// above stands until it lands.
verification_requested_at?: string | null;
verification_requested_at?: Date | null;
is_catch_all?: boolean;
// Who hosts the contact's inbox, read from its domain's MX; "" until
@@ -10,12 +10,12 @@ export interface ContactCampaignStep {
position: number;
subject?: string;
sent_at?: string | null;
opened_at?: string | null;
clicked_at?: string | null;
replied_at?: string | null;
bounced_at?: string | null;
failed_at?: string | null;
sent_at?: Date | null;
opened_at?: Date | null;
clicked_at?: Date | null;
replied_at?: Date | null;
bounced_at?: Date | null;
failed_at?: Date | null;
attempts?: number;
in_flight?: boolean;
}
@@ -38,8 +38,8 @@ export interface ContactNextAction {
// Only when due now, and then it is the campaign chain's own next wakeup,
// not a slot reserved for this contact: leads queued ahead can still push
// this step to a later pass. Absent while the chain is being re-seeded.
scheduled_at?: string | null;
not_before?: string | null;
scheduled_at?: Date | null;
not_before?: Date | null;
constraint?: string;
}
@@ -62,7 +62,7 @@ export default interface ContactCampaignState {
current_step?: ContactCampaignStep | null;
last_action?: string;
last_action_at?: string | null;
last_action_at?: Date | null;
// The live per-lead hold: an out-of-office auto-reply parked the contact,
// or a member paused them. Absent when the lead is not held.
@@ -8,11 +8,11 @@ export interface ContactEngagement {
total_bounced: number;
total_complained: number;
last_sent_at?: string | null;
last_opened_at?: string | null;
last_clicked_at?: string | null;
last_replied_at?: string | null;
last_bounced_at?: string | null;
last_sent_at?: Date | null;
last_opened_at?: Date | null;
last_clicked_at?: Date | null;
last_replied_at?: Date | null;
last_bounced_at?: Date | null;
// How the contact reads your mail: each client and device a person's
// opens came from, most recent first.
@@ -27,7 +27,7 @@ export interface ContactReadingOrigin {
os?: string;
browser?: string;
opens: number;
last_opened_at: string;
last_opened_at: Date;
}
export interface ContactSuppression {
@@ -38,8 +38,8 @@ export interface ContactSuppression {
value: string;
reason: string;
source: "bounce" | "complaint" | "unsubscribe" | "manual" | "import" | string;
expires_at?: string | null;
created_at: string;
expires_at?: Date | null;
created_at: Date;
}
// Where a contact first came from. Mirrors the backend CHECK; "unknown" is
@@ -69,7 +69,7 @@ export type VerificationEvidenceKind =
export interface ContactVerificationEvidence {
kind: VerificationEvidenceKind;
detail?: string;
observed_at: string;
observed_at: Date;
}
export interface ContactVerificationDetail {
@@ -87,9 +87,9 @@ export interface ContactVerificationDetail {
provider_label?: string;
// What that check said, before real mail was weighed against it.
check_status: "" | "valid" | "risky" | "invalid" | "unknown";
checked_at?: string | null;
checked_at?: Date | null;
// Set while a re-check a member asked for waits to run.
requested_at?: string | null;
requested_at?: Date | null;
}
export default interface ContactDetail extends Contact {
@@ -100,5 +100,5 @@ export default interface ContactDetail extends Contact {
// First-touch attribution; never changes after creation.
source: ContactSource;
source_detail: string;
first_seen_at: string;
first_seen_at: Date;
}
@@ -27,7 +27,7 @@ export interface ContactPageHit {
id: string;
visitor_id: string;
session_key: string;
occurred_at: string;
occurred_at: Date;
url: string;
path: string;
title: string;
@@ -86,7 +86,7 @@ export interface EngagementOrigin {
export default interface ContactTimelineEvent {
type: ContactTimelineEventType;
at: string;
at: Date;
// Engagement classification (email_opened / email_clicked): true when an
// automated fetcher (mail privacy proxy, security gateway) did it rather
@@ -120,7 +120,7 @@ export default interface ContactTimelineEvent {
content?: string | null;
// Meeting events (meeting_booked / rescheduled / canceled).
scheduled_for?: string | null;
scheduled_for?: Date | null;
join_url?: string | null;
meeting_state?: string | null;
@@ -60,6 +60,6 @@ export interface ContactResearchRun {
credits_charged: number;
model_used: string;
tokens_used: number;
created_at: string;
updated_at: string;
created_at: Date;
updated_at: Date;
}
+7 -4
View File
@@ -11,16 +11,19 @@ export default interface CRMTask {
created_by: string;
title: string;
description?: string;
due_date?: string;
due_date?: Date;
priority: CRMTaskPriority;
// The task type's name (user-managed; empty = no type).
type: string;
status: CRMTaskStatus;
completed_at?: string;
created_at: string;
updated_at: string;
completed_at?: Date;
created_at: Date;
updated_at: Date;
}
// Create/update body: due_date goes out as the RFC3339 string the form builds.
export type CRMTaskWrite = Partial<Omit<CRMTask, "due_date">> & { due_date?: string };
export interface CRMTasksResult {
data: CRMTask[];
pagination: {
+8 -5
View File
@@ -10,17 +10,17 @@ export default interface Deal {
value?: number;
currency: string;
status: DealStatus;
expected_close_date?: string;
won_at?: string;
lost_at?: string;
expected_close_date?: Date;
won_at?: Date;
lost_at?: Date;
lost_reason?: string;
assigned_to?: string;
// Attribution: the campaign + sender mailbox that produced the originating
// reply. Nullable; editable. Lets won revenue trace back to outreach.
campaign_id?: string;
source_mailbox_id?: string;
created_at: string;
updated_at: string;
created_at: Date;
updated_at: Date;
// Optional joined fields the API may return
contact?: {
@@ -39,6 +39,9 @@ export default interface Deal {
campaign_name?: string;
}
// Create/update body: expected_close_date goes out as the RFC3339 string the form builds.
export type DealWrite = Partial<Omit<Deal, "expected_close_date">> & { expected_close_date?: string };
export interface DealsResult {
data: Deal[];
pagination: {
+4 -4
View File
@@ -5,8 +5,8 @@ export interface Stage {
color: string;
position: number;
deal_count?: number;
created_at: string;
updated_at: string;
created_at: Date;
updated_at: Date;
}
export default interface Pipeline {
@@ -15,6 +15,6 @@ export default interface Pipeline {
name: string;
position: number;
stages: Stage[];
created_at: string;
updated_at: string;
created_at: Date;
updated_at: Date;
}
+2 -2
View File
@@ -6,6 +6,6 @@ export default interface TaskType {
name: string;
color: string;
position: number;
created_at: string;
updated_at: string;
created_at: Date;
updated_at: Date;
}
@@ -22,7 +22,7 @@ export default interface SendIdentity {
/** Empty means the mailbox's own address. */
send_as_email: string;
identities: SendAsIdentity[];
synced_at?: string | null;
synced_at?: Date | null;
signature_source: "manual" | "provider";
signature_imported_at?: string | null;
signature_imported_at?: Date | null;
}
@@ -33,8 +33,8 @@ export default interface SendingBehavior {
timezone?: string;
created_at: string;
updated_at: string;
created_at: Date;
updated_at: Date;
}
export type SendingBehaviorPatch = Partial<Omit<SendingBehavior, "email_account_id" | "timezone" | "created_at" | "updated_at">>;
@@ -52,7 +52,7 @@ export interface DailyPlan {
lunch_end_minute: number | null;
gap_min_seconds: number;
gap_max_seconds: number;
created_at: string;
created_at: Date;
}
export interface DailyPlanView extends DailyPlan {
@@ -13,10 +13,10 @@ export type SyncThrottleReason =
export interface SyncState {
backfill_status: SyncBackfillStatus;
backfill_synced: number;
backfill_since?: string;
backfill_started_at?: string;
backfill_completed_at?: string;
throttled_until?: string;
backfill_since?: Date;
backfill_started_at?: Date;
backfill_completed_at?: Date;
throttled_until?: Date;
throttle_reason?: SyncThrottleReason | "";
// Live messages seen on the server but waiting on budget.
deferred: number;
@@ -24,7 +24,7 @@ export interface SyncState {
folders_skipped_cap?: number;
/** Folders left out because the mail server listed their name more than once. */
folders_skipped_conflict?: number;
last_synced_at?: string;
last_synced_at?: Date;
}
export interface SyncPolicy {
@@ -7,8 +7,8 @@ export default interface WarmupBanStatus {
blocked: boolean;
health_state: string;
reason: string;
blocked_at: string | null;
blocked_until: string | null;
blocked_at: Date | null;
blocked_until: Date | null;
can_appeal: boolean;
pending_appeal: boolean;
}
@@ -36,7 +36,7 @@ export interface FormIdentifiedVisitor {
contact_id: string;
name: string;
email: string;
last_seen: string;
last_seen: Date;
furthest_page: number;
completed: boolean;
/** Campaign whose email brought this contact here, when known. */
@@ -4,7 +4,7 @@
export default interface FormsDomainStatus {
forms_domain: string;
forms_domain_verified: boolean;
forms_domain_verified_at?: string;
forms_domain_verified_at?: Date;
/** The value to put in the CNAME: this install's forms host. */
cname_target: string;
/** verified | unset | no_target | not_found | wrong_target | lookup_error | pending */
@@ -30,7 +30,7 @@ export interface InboxTagRow {
answers: Record<string, unknown>;
model: string;
input_tokens: number;
created_at: string;
created_at: Date;
}
export default interface InboxTagReview {
@@ -121,7 +121,7 @@ export interface IntegrationFieldMapping {
transform: string;
static_value: string;
is_default: boolean;
created_at: string;
created_at: Date;
}
export interface IntegrationConnection {
@@ -136,15 +136,15 @@ export interface IntegrationConnection {
external_account_id?: string;
external_account_name?: string;
granted_scopes?: string[];
token_expires_at?: string | null;
token_expires_at?: Date | null;
health: IntegrationHealth;
health_detail?: string | null;
health_checked_at?: string | null;
last_synced_at?: string | null;
health_checked_at?: Date | null;
last_synced_at?: Date | null;
last_error?: string | null;
last_error_at?: string | null;
created_at: string;
updated_at: string;
last_error_at?: Date | null;
created_at: Date;
updated_at: Date;
/** Per-connection onboarding/capability snapshot (selected use-cases, picker
* selections, scheduling_url for meeting providers). */
@@ -187,8 +187,8 @@ export interface IntegrationEventSubscription {
action: IntegrationAction;
config: Record<string, unknown>;
enabled: boolean;
created_at: string;
updated_at: string;
created_at: Date;
updated_at: Date;
}
export interface IntegrationSyncRun {
@@ -199,8 +199,8 @@ export interface IntegrationSyncRun {
status: "running" | "success" | "error";
detail: string;
records_processed: number;
started_at: string;
finished_at?: string | null;
started_at: Date;
finished_at?: Date | null;
}
export interface IntegrationOAuthStartResponse {
@@ -226,8 +226,8 @@ export interface MeetingBooking {
invitee_name: string;
event_name: string;
event_type?: string;
scheduled_for?: string;
end_time?: string;
scheduled_for?: Date;
end_time?: Date;
join_url?: string;
location?: string;
cancel_url?: string;
@@ -236,8 +236,8 @@ export interface MeetingBooking {
contact_id?: string;
campaign_id?: string;
contact_name?: string;
created_at: string;
updated_at?: string;
created_at: Date;
updated_at?: Date;
}
export interface MeetingsSummary {
@@ -48,11 +48,11 @@ export interface LeadSyncSource {
subscribed_default: boolean;
label?: string;
status: LeadSyncStatus;
last_synced_at?: string;
last_synced_at?: Date;
last_result?: ImportResult;
last_error?: string;
created_at: string;
updated_at: string;
created_at: Date;
updated_at: Date;
}
// Result of POST /sources/:id/sync.
+2 -2
View File
@@ -13,8 +13,8 @@ export interface MCPServer {
enabled: boolean;
discovered_tools: MCPTool[];
last_error?: string;
created_at: string;
updated_at: string;
created_at: Date;
updated_at: Date;
}
export interface CreateMCPServer {
@@ -97,6 +97,6 @@ export interface AppNotification {
body?: string;
link?: string;
metadata?: Record<string, unknown>;
read_at?: string | null;
created_at: string;
read_at?: Date | null;
created_at: Date;
}
+4 -4
View File
@@ -24,8 +24,8 @@ export interface OAuthApplication {
// Bitmask of the API permissions this app may request (same bits as API keys).
scopes: number;
status: OAuthAppStatus;
created_at: string;
updated_at: string;
created_at: Date;
updated_at: Date;
}
// Returned once on create / secret rotation; client_secret is shown a single time.
@@ -68,8 +68,8 @@ export interface OAuthAuthorizedApp {
logo_url: string;
website_url: string;
scopes: number;
authorized_at: string;
last_used_at?: string;
authorized_at: Date;
last_used_at?: Date;
}
export interface OAuthAuthorizedAppsResult {
@@ -21,9 +21,9 @@ export default interface LimitIncreaseRequest {
reason: string;
status: LimitRequestStatus;
submitted_by: string;
submitted_at: string;
submitted_at: Date;
reviewed_by?: string | null;
reviewed_at?: string | null;
reviewed_at?: Date | null;
review_notes: string;
}
@@ -8,6 +8,6 @@ export default interface OrganizationRole {
color: string;
permissions: number;
member_count: number;
created_at: string;
updated_at: string;
created_at: Date;
updated_at: Date;
}
@@ -96,7 +96,7 @@ export interface CampaignSegmentLink {
contact_count: number;
lead_count: number;
held_out_count: number;
linked_at: string;
linked_at: Date;
}
// Operators per field kind, mirrored from the backend catalog.
+2 -2
View File
@@ -5,8 +5,8 @@ export interface AISkill {
description: string;
content: string;
enabled: boolean;
created_at: string;
updated_at: string;
created_at: Date;
updated_at: Date;
}
export interface CreateAISkill {
@@ -21,9 +21,9 @@ export interface CreditBalance {
// Lifetime purchased credits.
total_purchased: number;
// When the monthly pool was last reset.
monthly_reset_at: string;
monthly_reset_at: Date;
// When the monthly pool next resets (subscription period end), if any.
next_reset_at: string | null;
next_reset_at: Date | null;
packs: CreditPack[];
}
@@ -43,7 +43,7 @@ export interface CreditTransaction {
// what ran (campaign/step/contact, automation/node/run, thread, session).
actor_user_id?: string | null;
context?: CreditContext;
created_at: string;
created_at: Date;
}
export interface CreditTransactionsPage {
@@ -66,7 +66,7 @@ export interface AISpendSettings {
member_limit_weekly: number | null;
member_limit_monthly: number | null;
low_balance_threshold: number;
low_balance_notified_at?: string | null;
low_balance_notified_at?: Date | null;
auto_topup_enabled: boolean;
auto_topup_pack: string;
auto_topup_threshold: number;
@@ -9,8 +9,8 @@ export interface DiscountRedemption {
currency?: string | null;
trial_extension_days?: number | null;
status: string;
redeemed_at: string;
applied_at?: string | null;
redeemed_at: Date;
applied_at?: Date | null;
}
export interface DiscountRedemptionsResult {
@@ -24,8 +24,8 @@ export interface ReferralCode {
owner_user_id: string;
owner_org_id: string;
discount_code_id: string;
created_at: string;
updated_at: string;
created_at: Date;
updated_at: Date;
}
export type ReferralAttributionStatus = "pending" | "qualified" | "rewarded" | "void";
@@ -36,9 +36,9 @@ export interface ReferralAttribution {
reward_cents: number;
reward_currency: string;
invitee_org_id: string;
qualified_at?: string;
rewarded_at?: string;
created_at: string;
qualified_at?: Date;
rewarded_at?: Date;
created_at: Date;
}
// Positive amount = reward, negative = clawback.
@@ -48,7 +48,7 @@ export interface ReferralEarningsTransaction {
currency: string;
reason: string;
balance_after_cents: number;
created_at: string;
created_at: Date;
}
export interface ReferralPagination {
@@ -11,10 +11,10 @@ export default interface Suppression {
reason: string;
source: SuppressionSource | string;
campaign_id?: string | null;
expires_at?: string | null;
expires_at?: Date | null;
metadata?: Record<string, unknown>;
created_at: string;
updated_at: string;
created_at: Date;
updated_at: Date;
}
export interface SuppressionListResult {
+3 -3
View File
@@ -5,7 +5,7 @@ export interface TeamMember {
user_id: string;
email: string;
name: string;
added_at: string;
added_at: Date;
}
export default interface Team {
@@ -13,7 +13,7 @@ export default interface Team {
organization_id: string;
name: string;
color: string;
created_at: string;
updated_at: string;
created_at: Date;
updated_at: Date;
members: TeamMember[];
}
+2 -2
View File
@@ -17,7 +17,7 @@ export interface ComposeCandidate {
/** Prior messages exchanged with the recipient from this mailbox. */
history_messages: number;
/** RFC3339; omitted when this mailbox never contacted the recipient. */
last_contact_at?: string | null;
last_contact_at?: Date | null;
score: number;
/** Human-readable scoring signals ("has history", "low load today", ...). */
reasons: string[];
@@ -55,7 +55,7 @@ export interface ComposeSendInput {
export interface ComposeSendResponse {
task_id: string;
scheduled_at: string;
scheduled_at: Date;
send_mode: string;
account_id: string;
account_email: string;
@@ -60,7 +60,7 @@ export default interface UniboxOverview {
mailboxes: UniboxMailboxOverview[];
tags: UniboxTagOverview[];
categories: UniboxCategoryOverview[];
generated_at: string;
window_today_start: string;
window_week_start: string;
generated_at: Date;
window_today_start: Date;
window_week_start: Date;
}
@@ -3,8 +3,8 @@
export default interface UniboxScheduledItem {
task_id: string
scheduled_at: string
created_at: string
scheduled_at: Date
created_at: Date
account_id: string
account_email: string
@@ -12,7 +12,7 @@ export interface UniboxThreadMessage {
to_addr: string[]
subject: string
snippet: string
internal_date: string
internal_date: Date
seen: boolean
}
@@ -15,7 +15,7 @@ export interface ViewPreferences {
columns: string[];
// Null or absent means the list's default sort.
sort?: ViewSort | null;
updated_at?: string | null;
updated_at?: Date | null;
}
export interface ViewPreferencesEnvelope {
+12 -12
View File
@@ -18,18 +18,18 @@ export interface WebhookEndpoint {
description: string;
event_types: string[];
enabled: boolean;
last_success_at?: string;
last_failure_at?: string;
last_success_at?: Date;
last_failure_at?: Date;
last_failure_reason?: string;
consecutive_failures: number;
oauth_application_id?: string;
created_by?: string;
verified_at?: string;
verified_at?: Date;
ownership_confirmed: boolean;
auto_disabled_at?: string;
auto_disabled_at?: Date;
disabled_reason?: string;
created_at: string;
updated_at: string;
created_at: Date;
updated_at: Date;
}
// Returned once on create; secret is shown a single time and never again.
@@ -56,21 +56,21 @@ export interface WebhookDelivery {
status: WebhookDeliveryStatus;
attempt_count: number;
max_attempts: number;
next_attempt_at: string;
last_attempt_at?: string;
next_attempt_at: Date;
last_attempt_at?: Date;
response_status?: number;
response_body_excerpt?: string;
error_reason?: string;
created_at: string;
updated_at: string;
created_at: Date;
updated_at: Date;
}
// A high-volume event family that was throttled (dropped windows) for a day.
export interface WebhookEventDrop {
event_type: string;
day: string;
day: Date;
dropped_windows: number;
last_dropped_at: string;
last_dropped_at: Date;
}
// GET /webhooks
@@ -9,7 +9,7 @@ export interface WebsiteTrackingSettings {
location_precision: WebsiteLocationPrecision;
allowed_hosts: string[];
retention_days: number;
updated_at: string;
updated_at: Date;
// The deployment's tracking host; empty when the install has none.
tracking_host: string;
}

Some files were not shown because too many files have changed in this diff Show More