diff --git a/apps/console/biome.jsonc b/apps/console/biome.jsonc index 0b9f57e21..5e68022ff 100644 --- a/apps/console/biome.jsonc +++ b/apps/console/biome.jsonc @@ -5,6 +5,7 @@ "formatter": { "lineWidth": 120 }, + "linter": { "rules": { "correctness": { "noUnresolvedImports": "off" } } }, "files": { "ignoreUnknown": true, // Stock Ultracite + lineWidth 120 — no rule overrides beyond the diff --git a/apps/console/next-config-redirects.test.mjs b/apps/console/next-config-redirects.test.mjs index 338cf75eb..d14d54e07 100644 --- a/apps/console/next-config-redirects.test.mjs +++ b/apps/console/next-config-redirects.test.mjs @@ -6,6 +6,7 @@ import test from "node:test"; import nextConfig from "./next.config.mjs"; const redirects = await nextConfig.redirects(); +const TRAILING_SLASH_RE = /\/$/; function matchRule(rule, pathname) { const { source, destination } = rule; @@ -17,7 +18,7 @@ function matchRule(rule, pathname) { } if (pathname.startsWith(`${prefix}/`)) { const rest = pathname.slice(prefix.length + 1); - return destination.replace(":rest*", rest).replace(/\/$/, "") || "/"; + return destination.replace(":rest*", rest).replace(TRAILING_SLASH_RE, "") || "/"; } return null; } diff --git a/apps/console/next.config.mjs b/apps/console/next.config.mjs index ad1135f83..1a2a7150c 100644 --- a/apps/console/next.config.mjs +++ b/apps/console/next.config.mjs @@ -1,13 +1,13 @@ // Copyright The PDP-Connect Contributors // SPDX-License-Identifier: Apache-2.0 -import path from 'path'; -import { fileURLToPath } from 'url'; -import { collectAllowedDevOrigins } from './scripts/dev-origins.mjs'; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { collectAllowedDevOrigins } from "./scripts/dev-origins.mjs"; -const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const __dirname = fileURLToPath(new URL(".", import.meta.url)); -const allowedDevOrigins = process.env.NODE_ENV === 'production' ? [] : collectAllowedDevOrigins(); +const allowedDevOrigins = process.env.NODE_ENV === "production" ? [] : collectAllowedDevOrigins(); function parseBuildWorkers(value) { if (!value) { @@ -18,31 +18,11 @@ function parseBuildWorkers(value) { } const buildWorkers = parseBuildWorkers(process.env.PDPP_WEB_BUILD_WORKERS); -const manualUploadBodyLimit = '1024mb'; +const manualUploadBodyLimit = "1024mb"; /** @type {import('next').NextConfig} */ const nextConfig = { ...(allowedDevOrigins.length > 0 ? { allowedDevOrigins } : {}), - output: 'standalone', - outputFileTracingRoot: path.join(__dirname, '../..'), - outputFileTracingIncludes: { - '/well-known/skills/**': [ - '../../docs/agent-skills/**/*.md', - '../../openspec/README.md', - '../../pnpm-workspace.yaml', - ], - '/llms-full.txt': [ - '../../docs/agent-skills/**/*.md', - '../../openspec/README.md', - '../../pnpm-workspace.yaml', - ], - '/llms.txt': [ - '../../docs/agent-skills/**/*.md', - '../../openspec/README.md', - '../../pnpm-workspace.yaml', - ], - }, - reactStrictMode: true, experimental: { cpus: buildWorkers, proxyClientMaxBodySize: manualUploadBodyLimit, @@ -50,48 +30,60 @@ const nextConfig = { bodySizeLimit: manualUploadBodyLimit, }, }, - // Transpile the reference-implementation workspace package so Next can - // consume its TypeScript sources directly once shim pairs (.js + .d.ts) - // collapse into single .ts exports. Without this, Next's bundler would - // reject .ts entries from a node_modules-resolved workspace package. - transpilePackages: ['pdpp-reference-implementation', '@pdpp/brand', '@pdpp/brand-react', '@pdpp/operator-ui'], - async redirects() { + output: "standalone", + outputFileTracingIncludes: { + "/llms-full.txt": ["../../docs/agent-skills/**/*.md", "../../openspec/README.md", "../../pnpm-workspace.yaml"], + "/llms.txt": ["../../docs/agent-skills/**/*.md", "../../openspec/README.md", "../../pnpm-workspace.yaml"], + "/well-known/skills/**": [ + "../../docs/agent-skills/**/*.md", + "../../openspec/README.md", + "../../pnpm-workspace.yaml", + ], + }, + outputFileTracingRoot: path.join(__dirname, "../.."), + reactStrictMode: true, + redirects() { return [ { - source: '/favicon.ico', - destination: '/brand/pdpp-favicon.svg', + destination: "/brand/pdpp-favicon.svg", permanent: false, + source: "/favicon.ico", }, ]; }, - async rewrites() { + rewrites() { return [ { - source: '/.well-known/oauth-authorization-server', - destination: '/well-known/oauth-authorization-server', + destination: "/well-known/oauth-authorization-server", + source: "/.well-known/oauth-authorization-server", }, { - source: '/.well-known/oauth-protected-resource/:path*', - destination: '/well-known/oauth-protected-resource/:path*', + destination: "/well-known/oauth-protected-resource/:path*", + source: "/.well-known/oauth-protected-resource/:path*", }, { - source: '/.well-known/oauth-protected-resource', - destination: '/well-known/oauth-protected-resource', + destination: "/well-known/oauth-protected-resource", + source: "/.well-known/oauth-protected-resource", }, { - source: '/.well-known/skills/:path*', - destination: '/well-known/skills/:path*', + destination: "/well-known/skills/:path*", + source: "/.well-known/skills/:path*", }, { - source: '/.well-known/llms.txt', - destination: '/llms.txt', + destination: "/llms.txt", + source: "/.well-known/llms.txt", }, ]; }, + // Transpile the reference-implementation workspace package so Next can + // consume its TypeScript sources directly once shim pairs (.js + .d.ts) + // collapse into single .ts exports. Without this, Next's bundler would + // reject .ts entries from a node_modules-resolved workspace package. + transpilePackages: ["pdpp-reference-implementation", "@pdpp/brand", "@pdpp/brand-react", "@pdpp/operator-ui"], webpack(config) { config.resolve.alias = { ...config.resolve.alias, - '@': path.join(__dirname, 'src'), + "@": path.join(__dirname, "src"), }; return config; }, diff --git a/apps/console/postcss.config.mjs b/apps/console/postcss.config.mjs index 6e71b3344..95974dcd4 100644 --- a/apps/console/postcss.config.mjs +++ b/apps/console/postcss.config.mjs @@ -3,7 +3,7 @@ const config = { plugins: { - '@tailwindcss/postcss': {}, + "@tailwindcss/postcss": {}, }, }; diff --git a/apps/console/src/app/(console)/audit/[traceId]/page.tsx b/apps/console/src/app/(console)/audit/[traceId]/page.tsx index b54f89ae1..1413e8cb9 100644 --- a/apps/console/src/app/(console)/audit/[traceId]/page.tsx +++ b/apps/console/src/app/(console)/audit/[traceId]/page.tsx @@ -176,12 +176,12 @@ function groupTimeline(events: SpineEvent[]): TimelineNode[] { } const runLen = j - i; if (runLen >= PROGRESS_GROUP_THRESHOLD) { - nodes.push({ kind: "progress-group", events: events.slice(i, j), start: i }); + nodes.push({ events: events.slice(i, j), kind: "progress-group", start: i }); i = j; continue; } } - nodes.push({ kind: "event", event: current, index: i }); + nodes.push({ event: current, index: i, kind: "event" }); i += 1; } return nodes; @@ -230,6 +230,7 @@ function EventTableRow({ event, index }: { event: SpineEvent; index: number }) { padding: "var(--space-2)", }} > + {/** biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic */} {JSON.stringify(redactSecrets(event.data || {}), null, 2)} @@ -239,7 +240,7 @@ function EventTableRow({ event, index }: { event: SpineEvent; index: number }) { } function ProgressGroupTableRow({ events, startIndex }: { events: SpineEvent[]; startIndex: number }) { - const first = events[0]; + const [first] = events; const last = events.at(-1); if (!(first && last)) { return null; @@ -315,7 +316,7 @@ export default async function TraceDetailPage({ notFound(); } - const events = envelope.events; + const { events } = envelope; const first = events[0] ?? null; const actorLabel = first ? `${first.actor_type}/${first.actor_id}` : null; @@ -413,10 +414,10 @@ export default async function TraceDetailPage({ href={`/grants/${encodeURIComponent(id)}`} key={`grant:${id}`} style={{ + alignItems: "center", border: "1px solid var(--color-border)", color: "var(--color-foreground)", display: "inline-flex", - alignItems: "center", padding: "var(--space-1) var(--space-2-5)", }} > @@ -433,10 +434,10 @@ export default async function TraceDetailPage({ href={`/syncs/${encodeURIComponent(id)}`} key={`run:${id}`} style={{ + alignItems: "center", border: "1px solid var(--color-border)", color: "var(--color-foreground)", display: "inline-flex", - alignItems: "center", padding: "var(--space-1) var(--space-2-5)", }} > diff --git a/apps/console/src/app/(console)/audit/page.tsx b/apps/console/src/app/(console)/audit/page.tsx index f71edb6da..04ce22568 100644 --- a/apps/console/src/app/(console)/audit/page.tsx +++ b/apps/console/src/app/(console)/audit/page.tsx @@ -151,9 +151,9 @@ function TracesHeader() {

@@ -221,7 +221,7 @@ function TraceFilterBand({ params, hasFilters }: { params: Params; hasFilters: b Apply {hasFilters ? ( - + Reset ) : null} @@ -263,7 +263,7 @@ function TracesDesktopTable({ traces, params }: { traces: TraceSummary[]; params {traces.map((trace) => { const peeked = params.peek === trace.trace_id; - const peekHref = listHref(params, { peek: peeked ? undefined : trace.trace_id, cursor: undefined }); + const peekHref = listHref(params, { cursor: undefined, peek: peeked ? undefined : trace.trace_id }); const label = traceRowLabel(trace); const kinds = trace.kinds.slice(0, 3).join(", "); return ( @@ -298,6 +298,7 @@ function TraceSubjectLink({ href, label, traceId }: { href: string; label: strin {label} @@ -313,10 +313,10 @@ function TraceSubjectLink({ href, label, traceId }: { href: string; label: strin {traceId} @@ -361,6 +361,7 @@ function TraceMobileCard({ {label} {trace.trace_id}

{params.cursor ? ( - + ← First page ) : null} {result.has_more && result.next_cursor ? ( Next → @@ -470,18 +470,18 @@ function TracesEmptyState({ hasFilters }: { hasFilters: boolean }) { return (

@@ -489,9 +489,9 @@ function TracesEmptyState({ hasFilters }: { hasFilters: boolean }) {

@@ -511,19 +511,19 @@ interface PeekPanelProps { } function TracesPeekPanel({ traceId, envelope, cliCommand, listParams }: PeekPanelProps) { - const firstEvent = envelope.events[0]; + const [firstEvent] = envelope.events; const closeHref = listHref(listParams, { peek: undefined }); const detailHref = `/audit/${encodeURIComponent(traceId)}`; return ( -

+
Trace {traceId}
- +
@@ -532,19 +532,19 @@ function TracesPeekPanel({ traceId, envelope, cliCommand, listParams }: PeekPane {/* Summary meta */}
- + {envelope.events.length} events {firstEvent ? ( - + actor{" "} {firstEvent.actor_type}/{firstEvent.actor_id} @@ -559,19 +559,19 @@ function TracesPeekPanel({ traceId, envelope, cliCommand, listParams }: PeekPane
-
+
{event.event_type} @@ -584,8 +584,8 @@ function TracesPeekPanel({ traceId, envelope, cliCommand, listParams }: PeekPane {event.status} @@ -603,20 +603,20 @@ function TracesPeekPanel({ traceId, envelope, cliCommand, listParams }: PeekPane {/* CLI command */}

CLI diff --git a/apps/console/src/app/(console)/components/cli-copy-consistency.test.ts b/apps/console/src/app/(console)/components/cli-copy-consistency.test.ts index 073d35a38..f075d31be 100644 --- a/apps/console/src/app/(console)/components/cli-copy-consistency.test.ts +++ b/apps/console/src/app/(console)/components/cli-copy-consistency.test.ts @@ -61,24 +61,26 @@ const PEEK_NO_INSTALL_HOOK = /peek-cli-no-install/; const DETAIL_NO_INSTALL_HOOK = /cli-no-install-command/; test("no surfaced file advertises legacy bare pdpp run/grant/trace aliases", async () => { - for (const relPath of SURFACED_FILES) { - const src = await read(relPath); - assert.equal( - LEGACY_RUN_TIMELINE.test(src), - false, - `${relPath}: found legacy 'pdpp run timeline' - use 'pdpp ref run timeline'` - ); - assert.equal( - LEGACY_GRANT_TIMELINE.test(src), - false, - `${relPath}: found legacy 'pdpp grant timeline' - use 'pdpp ref grant timeline'` - ); - assert.equal( - LEGACY_TRACE_SHOW.test(src), - false, - `${relPath}: found legacy 'pdpp trace show' - use 'pdpp ref trace show'` - ); - } + await Promise.all( + SURFACED_FILES.map(async (relPath) => { + const src = await read(relPath); + assert.equal( + LEGACY_RUN_TIMELINE.test(src), + false, + `${relPath}: found legacy 'pdpp run timeline' - use 'pdpp ref run timeline'` + ); + assert.equal( + LEGACY_GRANT_TIMELINE.test(src), + false, + `${relPath}: found legacy 'pdpp grant timeline' - use 'pdpp ref grant timeline'` + ); + assert.equal( + LEGACY_TRACE_SHOW.test(src), + false, + `${relPath}: found legacy 'pdpp trace show' - use 'pdpp ref trace show'` + ); + }) + ); }); test("reference-implementation.md advertises canonical pdpp ref commands", async () => { diff --git a/apps/console/src/app/(console)/components/deployment-readiness-panel.tsx b/apps/console/src/app/(console)/components/deployment-readiness-panel.tsx index 90d435195..33b0b70cf 100644 --- a/apps/console/src/app/(console)/components/deployment-readiness-panel.tsx +++ b/apps/console/src/app/(console)/components/deployment-readiness-panel.tsx @@ -89,7 +89,7 @@ function useRefreshTokenAdvertisement(): RefreshTokenProbe { const grants = Array.isArray(body.grant_types_supported) ? body.grant_types_supported : []; const refreshTokenSupported = grants.some((g) => g === "refresh_token"); if (!cancelled) { - setProbe({ state: "loaded", refreshTokenSupported }); + setProbe({ refreshTokenSupported, state: "loaded" }); } } catch { if (!cancelled) { @@ -118,32 +118,32 @@ function verdictPresentation(verdict: Verdict): { label: string; body: string; t switch (verdict) { case "ready": return { - label: "Ready to share with Claude / ChatGPT", body: "Owner gate, origin, storage, embeddings, and refresh-token metadata all check out.", + label: "Ready to share with Claude / ChatGPT", toneClass: "border-[color:var(--success)]/30 bg-[color:var(--success-wash)] text-[color:var(--success)]", }; case "attention": return { - label: "Attention needed before sharing", body: "Some rows are usable but suboptimal. Read the hints below.", + label: "Attention needed before sharing", toneClass: "border-[color:var(--warning)]/30 bg-[color:var(--warning-wash)] text-[color:var(--warning)]", }; case "blocked": return { - label: "Not yet ready to share", body: "At least one row is in an error state. Fix it before handing the MCP URL to an agent.", + label: "Not yet ready to share", toneClass: "border-destructive/30 bg-destructive/5 text-destructive", }; case "unknown": return { - label: "Some checks still running", body: "Browser-side probes have not returned yet.", + label: "Some checks still running", toneClass: "border-border/80 bg-muted/40 text-foreground", }; default: return { - label: "Some checks still running", body: "Browser-side probes have not returned yet.", + label: "Some checks still running", toneClass: "border-border/80 bg-muted/40 text-foreground", }; } @@ -163,27 +163,27 @@ function ReadinessRowItem({ row }: { row: ReadinessRow }) { } const STATUS_TONE: Record = { - ok: "bg-[color:var(--success-wash)] text-[color:var(--success)]", - warn: "bg-[color:var(--warning-wash)] text-[color:var(--warning)]", error: "bg-destructive/10 text-destructive", info: "bg-muted text-muted-foreground", + ok: "bg-[color:var(--success-wash)] text-[color:var(--success)]", unknown: "bg-muted text-muted-foreground", + warn: "bg-[color:var(--warning-wash)] text-[color:var(--warning)]", }; const STATUS_LABEL: Record = { - ok: "ready", - warn: "attention", error: "blocked", info: "n/a", + ok: "ready", unknown: "checking", + warn: "attention", }; const STATUS_BADGE_TONE: Record = { - ok: "success", - warn: "warning", error: "danger", info: "neutral", + ok: "success", unknown: "neutral", + warn: "warning", }; function StatusChip({ status }: { status: ReadinessStatus }) { diff --git a/apps/console/src/app/(console)/components/deployment-readiness-rows.test.ts b/apps/console/src/app/(console)/components/deployment-readiness-rows.test.ts index d3a5417ce..c12815123 100644 --- a/apps/console/src/app/(console)/components/deployment-readiness-rows.test.ts +++ b/apps/console/src/app/(console)/components/deployment-readiness-rows.test.ts @@ -30,25 +30,25 @@ import { } from "./deployment-readiness-rows.ts"; const HEALTHY_DISK_HEADROOM: DiskHeadroomInputs = { - path: "/data", freeBytesOnDataFs: 20 * 1024 * 1024 * 1024, // 20 GiB - totalBytesOnDataFs: 100 * 1024 * 1024 * 1024, largestRelationBytes: null, largestRelationName: null, mountLabel: null, + path: "/data", + totalBytesOnDataFs: 100 * 1024 * 1024 * 1024, }; const baseInputs: ServerInputs = { - ownerPasswordProvenance: "redacted", - referenceOriginConfigured: "https://example.com", - embeddingBackendConfigured: true, + databasePath: "/data/pdpp.db", + diskHeadroom: [HEALTHY_DISK_HEADROOM], embeddingBackendAvailable: true, - embeddingModelCachePresent: true, + embeddingBackendConfigured: true, embeddingDownloadAllowed: true, + embeddingModelCachePresent: true, + ownerPasswordProvenance: "redacted", + referenceOriginConfigured: "https://example.com", vectorIndexKind: "sqlite-vec", vectorIndexState: "built", - databasePath: "/data/pdpp.db", - diskHeadroom: [HEALTHY_DISK_HEADROOM], }; const OWNER_PASSWORD_ENV_RE = /PDPP_OWNER_PASSWORD/; @@ -138,8 +138,8 @@ test("embeddingCacheRow is error when uncached and download disabled", () => { const row = embeddingCacheRow({ ...baseInputs, embeddingBackendAvailable: false, - embeddingModelCachePresent: false, embeddingDownloadAllowed: false, + embeddingModelCachePresent: false, }); assert.equal(row.status, "error"); }); @@ -148,8 +148,8 @@ test("embeddingCacheRow is warn while cache is warming", () => { const row = embeddingCacheRow({ ...baseInputs, embeddingBackendAvailable: false, - embeddingModelCachePresent: false, embeddingDownloadAllowed: true, + embeddingModelCachePresent: false, }); assert.equal(row.status, "warn"); }); @@ -167,11 +167,11 @@ test("refreshTokenRow is warn when well-known is unreachable", () => { }); test("refreshTokenRow is ok when refresh_token is advertised", () => { - assert.equal(refreshTokenRow({ state: "loaded", refreshTokenSupported: true }).status, "ok"); + assert.equal(refreshTokenRow({ refreshTokenSupported: true, state: "loaded" }).status, "ok"); }); test("refreshTokenRow is error when refresh_token is missing", () => { - const row = refreshTokenRow({ state: "loaded", refreshTokenSupported: false }); + const row = refreshTokenRow({ refreshTokenSupported: false, state: "loaded" }); assert.equal(row.status, "error"); assert.match(row.hint ?? "", DOCKER_COMPOSE_PULL_RE); }); @@ -179,31 +179,31 @@ test("refreshTokenRow is error when refresh_token is missing", () => { // ─── Overall verdict ──────────────────────────────────────────────────────── test("overallVerdict ready when every row is ok", () => { - const rows: ReadinessRow[] = [{ check: "x", status: "ok", detail: "" }]; + const rows: ReadinessRow[] = [{ check: "x", detail: "", status: "ok" }]; assert.equal(overallVerdict(rows), "ready"); }); test("overallVerdict blocked when any row is error", () => { const rows: ReadinessRow[] = [ - { check: "x", status: "ok", detail: "" }, - { check: "y", status: "error", detail: "" }, - { check: "z", status: "warn", detail: "" }, + { check: "x", detail: "", status: "ok" }, + { check: "y", detail: "", status: "error" }, + { check: "z", detail: "", status: "warn" }, ]; assert.equal(overallVerdict(rows), "blocked"); }); test("overallVerdict attention when any row is warn but none is error", () => { const rows: ReadinessRow[] = [ - { check: "x", status: "ok", detail: "" }, - { check: "y", status: "warn", detail: "" }, + { check: "x", detail: "", status: "ok" }, + { check: "y", detail: "", status: "warn" }, ]; assert.equal(overallVerdict(rows), "attention"); }); test("overallVerdict unknown when probes still loading and nothing worse", () => { const rows: ReadinessRow[] = [ - { check: "x", status: "ok", detail: "" }, - { check: "y", status: "unknown", detail: "" }, + { check: "x", detail: "", status: "ok" }, + { check: "y", detail: "", status: "unknown" }, ]; assert.equal(overallVerdict(rows), "unknown"); }); @@ -279,9 +279,9 @@ test("diskHeadroomRow includes workload hint when free < largestRelationBytes", makeEntry({ // 4 GiB free — below warn (5 GiB) and below largest relation (6 GiB). freeBytesOnDataFs: 4 * GiB, - totalBytesOnDataFs: 100 * GiB, largestRelationBytes: 6 * GiB, largestRelationName: "records", + totalBytesOnDataFs: 100 * GiB, }), ], }); @@ -297,9 +297,9 @@ test("diskHeadroomRow omits workload hint when free >= largestRelationBytes", () makeEntry({ // 4 GiB free — below warn but ABOVE largest relation (3 GiB). freeBytesOnDataFs: 4 * GiB, - totalBytesOnDataFs: 100 * GiB, largestRelationBytes: 3 * GiB, largestRelationName: "records", + totalBytesOnDataFs: 100 * GiB, }), ], }); @@ -313,9 +313,9 @@ test("diskHeadroomRow omits workload hint when largestRelationBytes is null (SQL diskHeadroom: [ makeEntry({ freeBytesOnDataFs: 4 * GiB, - totalBytesOnDataFs: 100 * GiB, largestRelationBytes: null, largestRelationName: null, + totalBytesOnDataFs: 100 * GiB, }), ], }); @@ -330,12 +330,12 @@ test("diskHeadroomRows returns one row per entry", () => { ...baseInputs, diskHeadroom: [ makeEntry({ freeBytesOnDataFs: 20 * GiB, mountLabel: "data" }), - makeEntry({ path: "/var/lib/postgresql/data", freeBytesOnDataFs: 8 * GiB, mountLabel: "postgres" }), + makeEntry({ freeBytesOnDataFs: 8 * GiB, mountLabel: "postgres", path: "/var/lib/postgresql/data" }), ], }); assert.equal(rows.length, 2); - const dataRow = rows[0]; - const postgresRow = rows[1]; + const [dataRow] = rows; + const [, postgresRow] = rows; assert.ok(dataRow); assert.ok(postgresRow); assert.match(dataRow.check, /data/); @@ -356,10 +356,10 @@ test("diskHeadroomRows: unmeasured postgres entry shows info, not a false green" diskHeadroom: [ makeEntry({ freeBytesOnDataFs: 20 * GiB, mountLabel: "data" }), makeEntry({ - path: "/var/lib/postgresql/data", freeBytesOnDataFs: null, - totalBytesOnDataFs: null, mountLabel: "postgres", + path: "/var/lib/postgresql/data", + totalBytesOnDataFs: null, }), ], }); diff --git a/apps/console/src/app/(console)/components/deployment-readiness-rows.ts b/apps/console/src/app/(console)/components/deployment-readiness-rows.ts index fdd679223..463fb32d6 100644 --- a/apps/console/src/app/(console)/components/deployment-readiness-rows.ts +++ b/apps/console/src/app/(console)/components/deployment-readiness-rows.ts @@ -68,23 +68,23 @@ export function extractReadinessInputs(report: DeploymentDiagnostics): ServerInp const largestRelation = report.database.top_relations?.[0] ?? null; const dhEntries = report.disk_headroom ?? []; return { - ownerPasswordProvenance: owner?.provenance ?? "absent", - referenceOriginConfigured: origin?.provenance === "present" ? origin.value : null, - embeddingBackendConfigured: report.semantic.backend.configured, - embeddingBackendAvailable: report.semantic.backend.available, - embeddingModelCachePresent: report.semantic.backend.model_cache_present, - embeddingDownloadAllowed: report.semantic.backend.download_allowed, - vectorIndexKind: report.semantic.index.kind, - vectorIndexState: report.semantic.index.state, databasePath: report.database.path, diskHeadroom: dhEntries.map((dh) => ({ - path: dh.path, freeBytesOnDataFs: dh.free_bytes, - totalBytesOnDataFs: dh.total_bytes, - mountLabel: dh.mount_label ?? null, largestRelationBytes: largestRelation?.bytes ?? null, largestRelationName: largestRelation?.name ?? null, + mountLabel: dh.mount_label ?? null, + path: dh.path, + totalBytesOnDataFs: dh.total_bytes, })), + embeddingBackendAvailable: report.semantic.backend.available, + embeddingBackendConfigured: report.semantic.backend.configured, + embeddingDownloadAllowed: report.semantic.backend.download_allowed, + embeddingModelCachePresent: report.semantic.backend.model_cache_present, + ownerPasswordProvenance: owner?.provenance ?? "absent", + referenceOriginConfigured: origin?.provenance === "present" ? origin.value : null, + vectorIndexKind: report.semantic.index.kind, + vectorIndexState: report.semantic.index.state, }; } @@ -92,15 +92,15 @@ export function ownerPasswordRow(inputs: ServerInputs): ReadinessRow { if (inputs.ownerPasswordProvenance === "redacted") { return { check: "Owner password gate", - status: "ok", detail: "PDPP_OWNER_PASSWORD is set; owner surfaces require sign-in.", + status: "ok", }; } return { check: "Owner password gate", - status: "error", detail: "PDPP_OWNER_PASSWORD is not set.", hint: "Set `PDPP_OWNER_PASSWORD` in your env and restart; otherwise `/owner`, `/device`, `/consent`, and `/` are reachable without auth.", + status: "error", }; } @@ -108,17 +108,17 @@ export function referenceOriginRow(inputs: ServerInputs, browserOrigin: string | if (!inputs.referenceOriginConfigured) { return { check: "Reference origin alignment", - status: "warn", detail: "PDPP_REFERENCE_ORIGIN is not set. The deployment will infer the origin from request headers, which is brittle behind proxies.", hint: "Set `PDPP_REFERENCE_ORIGIN` to the URL you are visiting (e.g. `https://-3002.proxy.runpod.net`). Mismatches break the MCP and OAuth callback flows.", + status: "warn", }; } if (browserOrigin === null) { return { check: "Reference origin alignment", - status: "unknown", detail: `PDPP_REFERENCE_ORIGIN=${inputs.referenceOriginConfigured}. Browser origin not yet observed.`, + status: "unknown", }; } const configured = stripTrailingSlash(inputs.referenceOriginConfigured); @@ -126,15 +126,15 @@ export function referenceOriginRow(inputs: ServerInputs, browserOrigin: string | if (configured === observed) { return { check: "Reference origin alignment", - status: "ok", detail: `Configured origin matches the browser origin (${observed}).`, + status: "ok", }; } return { check: "Reference origin alignment", - status: "warn", detail: `PDPP_REFERENCE_ORIGIN=${configured}; you are viewing this dashboard from ${observed}.`, hint: "Set `PDPP_REFERENCE_ORIGIN` to the URL you are visiting (e.g. `https://-3002.proxy.runpod.net`). Mismatches break the MCP and OAuth callback flows.", + status: "warn", }; } @@ -142,29 +142,29 @@ export function storageBackendRow(inputs: ServerInputs): ReadinessRow { if (inputs.vectorIndexKind === null && inputs.vectorIndexState === null) { return { check: "Storage backend", - status: "info", detail: `Database at ${inputs.databasePath}. No vector index configured yet.`, + status: "info", }; } if (inputs.vectorIndexState === "stale") { return { check: "Storage backend", - status: "warn", detail: `Database at ${inputs.databasePath}; vector index (${inputs.vectorIndexKind ?? "unknown"}) is stale.`, hint: "Storage backend reports unhealthy. See `docs/operator/selfhost-quickstart.md` for storage layout.", + status: "warn", }; } if (inputs.vectorIndexState === "building") { return { check: "Storage backend", - status: "info", detail: `Database at ${inputs.databasePath}; vector index (${inputs.vectorIndexKind ?? "unknown"}) is still building.`, + status: "info", }; } return { check: "Storage backend", - status: "ok", detail: `Database at ${inputs.databasePath}; vector index (${inputs.vectorIndexKind ?? "n/a"}) is built.`, + status: "ok", }; } @@ -172,30 +172,30 @@ export function embeddingCacheRow(inputs: ServerInputs): ReadinessRow { if (!inputs.embeddingBackendConfigured) { return { check: "Embedding cache", - status: "info", detail: "No semantic embedding backend configured. Lexical retrieval still works.", + status: "info", }; } if (inputs.embeddingModelCachePresent === true && inputs.embeddingBackendAvailable) { return { check: "Embedding cache", - status: "ok", detail: "Embedding model is cached and the backend is available.", + status: "ok", }; } if (inputs.embeddingModelCachePresent === false && inputs.embeddingDownloadAllowed === false) { return { check: "Embedding cache", - status: "error", detail: "Embedding model is not cached and download is disabled.", hint: "Embedding cache is still downloading or missing. Wait for first-boot download to finish, or set `PDPP_EMBEDDING_DOWNLOAD_ALLOWED=0` if you do not need semantic search yet.", + status: "error", }; } return { check: "Embedding cache", - status: "warn", detail: "Embedding model cache is still warming up or the backend is not yet ready.", hint: "Embedding cache is still downloading or missing. Wait for first-boot download to finish, or set `PDPP_EMBEDDING_DOWNLOAD_ALLOWED=0` if you do not need semantic search yet.", + status: "warn", }; } @@ -203,30 +203,30 @@ export function refreshTokenRow(probe: RefreshTokenProbe): ReadinessRow { if (probe.state === "loading") { return { check: "MCP refresh-token advertisement", - status: "unknown", detail: "Checking the authorization-server metadata…", + status: "unknown", }; } if (probe.state === "unreachable") { return { check: "MCP refresh-token advertisement", - status: "warn", detail: "Could not reach `/.well-known/oauth-authorization-server` from this origin.", hint: "If your `AS_ISSUER` is not co-located with the dashboard origin, this check may show `warn` even on a healthy deployment. Confirm `grant_types_supported` includes `refresh_token` on your AS metadata directly.", + status: "warn", }; } if (probe.refreshTokenSupported) { return { check: "MCP refresh-token advertisement", - status: "ok", detail: "Authorization-server metadata advertises `refresh_token`.", + status: "ok", }; } return { check: "MCP refresh-token advertisement", - status: "error", detail: "Authorization-server metadata does not advertise `refresh_token`.", hint: "Reference image is too old to advertise `refresh_token`. `docker compose pull` to the current image.", + status: "error", }; } @@ -266,8 +266,8 @@ function diskHeadroomEntryRow(dh: DiskHeadroomInputs): ReadinessRow { if (dh.freeBytesOnDataFs === null) { return { check: checkLabel, - status: "info", detail: `Disk headroom could not be measured${dh.path ? ` on ${dh.path}` : ""}.`, + status: "info", }; } const free = dh.freeBytesOnDataFs; @@ -275,23 +275,23 @@ function diskHeadroomEntryRow(dh: DiskHeadroomInputs): ReadinessRow { if (free < DISK_ERROR_BYTES) { return { check: checkLabel, - status: "error", detail: `Only ${formatBytes(free)} free${pathLabel}. A restart or image build will very likely fail with "No space left on device".${workloadSuffix(dh, free)}`, hint: "Run `docker builder prune` or `docker system prune` to reclaim build cache and stopped containers. Inspect Docker volumes manually before removing any volume data.", + status: "error", }; } if (free < DISK_WARN_BYTES) { return { check: checkLabel, - status: "warn", detail: `${formatBytes(free)} free${pathLabel}. Disk space is running low.${workloadSuffix(dh, free)}`, hint: "Consider running `docker system prune` to reclaim build cache before the next restart.", + status: "warn", }; } return { check: checkLabel, - status: "ok", detail: `${formatBytes(free)} free${pathLabel}.`, + status: "ok", }; } @@ -303,8 +303,8 @@ export function diskHeadroomRows(inputs: ServerInputs): ReadinessRow[] { return [ { check: "Disk headroom", - status: "info", detail: "Disk headroom could not be measured on this filesystem.", + status: "info", }, ]; } @@ -318,18 +318,18 @@ export function diskHeadroomRow(inputs: ServerInputs): ReadinessRow { if (inputs.diskHeadroom.length === 0) { return { check: "Disk headroom", - status: "info", detail: "Disk headroom could not be measured on this filesystem.", + status: "info", }; } - const first = inputs.diskHeadroom[0]; + const [first] = inputs.diskHeadroom; // Guarded by the length check above; TypeScript sees index access as possibly // undefined when noUncheckedIndexedAccess is set. if (!first) { return { check: "Disk headroom", - status: "info", detail: "Disk headroom could not be measured on this filesystem.", + status: "info", }; } return diskHeadroomEntryRow(first); diff --git a/apps/console/src/app/(console)/components/record-inspector.tsx b/apps/console/src/app/(console)/components/record-inspector.tsx index c76ce825d..379fddea7 100644 --- a/apps/console/src/app/(console)/components/record-inspector.tsx +++ b/apps/console/src/app/(console)/components/record-inspector.tsx @@ -80,7 +80,6 @@ function BlobAffordanceView({ return (

{isImage ? ( - // biome-ignore lint/performance/noImgElement: blob fetch_url is a grant-scoped RS URL, not a static asset Next can optimize. // biome-ignore lint/correctness/useImageSize: a remote record blob has no known intrinsic dimensions; the CSS box constrains it. {affordance.fieldName} ) : null} @@ -131,8 +130,11 @@ export function RecordInspector({ record, relationships, streamRecordsHref }: Re const totalDeclared = record.fields.length; const blob = blobAffordance(record); const blobMime = blob ? declaredBlobMime(body, blob.fieldName) : undefined; + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic const relatedLinks = relationships?.relatedLinks ?? []; + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic const parentBackLinks = relationships?.parentBackLinks ?? []; + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic const reverseChildListLinks = relationships?.reverseChildListLinks ?? []; const hasRelationships = relatedLinks.length > 0 || parentBackLinks.length > 0 || reverseChildListLinks.length > 0; diff --git a/apps/console/src/app/(console)/components/route-loading.invariants.test.ts b/apps/console/src/app/(console)/components/route-loading.invariants.test.ts index 384955c73..f2cd67ce7 100644 --- a/apps/console/src/app/(console)/components/route-loading.invariants.test.ts +++ b/apps/console/src/app/(console)/components/route-loading.invariants.test.ts @@ -64,11 +64,13 @@ test("every high-value sources/runs surface ships a route-level loading.tsx", () }); test("each loading.tsx renders inside the shared DashboardShell for stable chrome", async () => { - for (const rel of LOADING_FILES) { - const src = await readFile(`${DASHBOARD}${rel}`, "utf8"); - assert.match(src, DASHBOARD_SHELL_RE, `${rel} must reuse DashboardShell`); - assert.match(src, SHARED_SKELETON_IMPORT_RE, `${rel} must use the shared skeleton`); - } + await Promise.all( + LOADING_FILES.map(async (rel) => { + const src = await readFile(`${DASHBOARD}${rel}`, "utf8"); + assert.match(src, DASHBOARD_SHELL_RE, `${rel} must reuse DashboardShell`); + assert.match(src, SHARED_SKELETON_IMPORT_RE, `${rel} must use the shared skeleton`); + }) + ); }); test("the shared skeleton is lightweight and accessible", async () => { diff --git a/apps/console/src/app/(console)/components/segment-error.tsx b/apps/console/src/app/(console)/components/segment-error.tsx index bb290fbc1..73ec0ede5 100644 --- a/apps/console/src/app/(console)/components/segment-error.tsx +++ b/apps/console/src/app/(console)/components/segment-error.tsx @@ -54,10 +54,11 @@ export function SegmentError({

{title}

{description}

- - + {backLabel}
diff --git a/apps/console/src/app/(console)/components/shell.tsx b/apps/console/src/app/(console)/components/shell.tsx index 00bbbfa65..b5f8f2e56 100644 --- a/apps/console/src/app/(console)/components/shell.tsx +++ b/apps/console/src/app/(console)/components/shell.tsx @@ -6,7 +6,7 @@ import { MobileDrawer, MobileDrawerProvider, MobileDrawerTrigger } from "@pdpp/o import { dashboardRoutes, type Routes, sandboxRoutes } from "@pdpp/operator-ui/components/views/routes"; import Link from "next/link"; import { cache, type ReactNode } from "react"; -import DensityToggle from "@/components/density/density-toggle.tsx"; +import { DensityToggle } from "@/components/density/density-toggle.tsx"; import { PdppLogo } from "@/components/pdpp-logo.tsx"; import { ThemeToggle } from "@/components/theme/theme-toggle.tsx"; import { getAsInternalUrl, getOwnerLoginPath, getReferencePublicOrigin, getRsInternalUrl } from "../lib/owner-token.ts"; diff --git a/apps/console/src/app/(console)/components/source-setup-catalog.tsx b/apps/console/src/app/(console)/components/source-setup-catalog.tsx index e58f7079a..64ba80c6e 100644 --- a/apps/console/src/app/(console)/components/source-setup-catalog.tsx +++ b/apps/console/src/app/(console)/components/source-setup-catalog.tsx @@ -14,7 +14,7 @@ import { sourceSetupSecondaryAction, sourceSetupStatus, } from "../lib/source-setup-presentation.ts"; -import { formatTotalRecordsLabel } from "../sources/sources-view-model.ts"; +import { formatTotalRecordsLabel } from "../lib/total-records.ts"; export interface ExistingSourceSetupLink { connectionId: string; @@ -186,13 +186,13 @@ function ExistingSourceLinks({
Open in Explore Source details @@ -263,11 +263,11 @@ function SourceSetupCard({ {action ? ( <> Next - + {action.label} {secondaryAction ? ( - + {secondaryAction.label} ) : null} @@ -305,7 +305,7 @@ function ServerSetupSummary({ entries }: { entries: readonly ConnectorCatalogEnt

{entry.displayName}

{sourceMethodLine(entry, 0)}

- + Open server settings diff --git a/apps/console/src/app/(console)/components/views/standing-demo-data.ts b/apps/console/src/app/(console)/components/views/standing-demo-data.ts index c4d5142e6..38de770d2 100644 --- a/apps/console/src/app/(console)/components/views/standing-demo-data.ts +++ b/apps/console/src/app/(console)/components/views/standing-demo-data.ts @@ -32,169 +32,169 @@ function iso(daysAgo: number): string { } const BEARERS: OwnerIssuedClient[] = [ - { client_id: "cli_claude_desktop", client_name: "Claude Desktop", active_token_count: 1, created_at: iso(40) }, - { client_id: "cli_framework", client_name: "CLI on framework", active_token_count: 2, created_at: iso(120) }, + { active_token_count: 1, client_id: "cli_claude_desktop", client_name: "Claude Desktop", created_at: iso(40) }, + { active_token_count: 2, client_id: "cli_framework", client_name: "CLI on framework", created_at: iso(120) }, // Worst case: a real owner bearer with NO human name — the long machine // client_id IS the identity and must truncate without breaking the row. { + active_token_count: 1, client_id: "single-use-proof-1781473829100-7f3a9c2e-b04d-4e51-9a2f-6c8e1d0b5a73", client_name: null, - active_token_count: 1, created_at: iso(3), }, // Named client whose machine id is a full OAuth client URL — name stays // prominent, the long id rides beneath it, truncated. { + active_token_count: 4, client_id: "https://chatgpt.com/connector_platform_oauth_client/2f1a8b3c-9d4e-4f0a-8c7b-1e2d3f4a5b6c", client_name: "ChatGPT", - active_token_count: 4, created_at: iso(7), }, ]; const GRANTS: GrantSummary[] = [ { - object: "grant_summary", - grant_id: "grant_atlas", client_id: "Atlas Mortgage", connector_id: "plaid", - status: "active", - first_at: iso(30), - last_at: iso(0), event_count: 12, - kinds: ["pay_statements", "transactions"], failure: null, + first_at: iso(30), + grant_id: "grant_atlas", + kinds: ["pay_statements", "transactions"], + last_at: iso(0), + object: "grant_summary", + status: "active", }, { - object: "grant_summary", - grant_id: "grant_northstar", client_id: "Northstar HR", connector_id: "employment", - status: "issued", - first_at: iso(14), - last_at: iso(2), event_count: 4, - kinds: ["employment"], failure: null, + first_at: iso(14), + grant_id: "grant_northstar", + kinds: ["employment"], + last_at: iso(2), + object: "grant_summary", + status: "issued", }, ]; const TRACES: TraceSummary[] = [ { - object: "trace_summary", - trace_id: "trace_1", - status: "succeeded", actor_id: "Claude Desktop", actor_type: "client", client_id: "Claude Desktop", - grant_id: null, - run_id: null, - request_id: null, - first_at: iso(0), - last_at: iso(0), event_count: 412, - kinds: ["transactions"], failure: null, - }, - { + first_at: iso(0), + grant_id: null, + kinds: ["transactions"], + last_at: iso(0), object: "trace_summary", - trace_id: "trace_2", + request_id: null, + run_id: null, status: "succeeded", + trace_id: "trace_1", + }, + { actor_id: "Atlas Mortgage", actor_type: "client", client_id: "Atlas Mortgage", - grant_id: "grant_atlas", - run_id: null, - request_id: null, - first_at: iso(1), - last_at: iso(1), event_count: 38, - kinds: ["pay_statements"], failure: null, + first_at: iso(1), + grant_id: "grant_atlas", + kinds: ["pay_statements"], + last_at: iso(1), + object: "trace_summary", + request_id: null, + run_id: null, + status: "succeeded", + trace_id: "trace_2", }, { - object: "trace_summary", - trace_id: "trace_3", - status: "denied", actor_id: "Unknown app", actor_type: "client", client_id: "Unknown app", - grant_id: null, - run_id: null, - request_id: null, - first_at: iso(2), - last_at: iso(2), event_count: 0, - kinds: ["browsing"], failure: { event_type: "trace.denied", reason: "scope not granted" }, + first_at: iso(2), + grant_id: null, + kinds: ["browsing"], + last_at: iso(2), + object: "trace_summary", + request_id: null, + run_id: null, + status: "denied", + trace_id: "trace_3", }, ]; const FAILED_RUNS: RunSummary[] = [ { - object: "run_summary", - run_id: "run_meridian", - status: "failed", connector_id: "current_activity", - grant_id: null, - provider_id: null, - first_at: iso(2), - last_at: iso(2), event_count: 3, + failure_reason: "First Meridian's connection expired. Reconnect to resume syncing.", + first_at: iso(2), + grant_id: null, kinds: ["run.failed"], + last_at: iso(2), needs_input: false, - failure_reason: "First Meridian's connection expired. Reconnect to resume syncing.", + object: "run_summary", + provider_id: null, + run_id: "run_meridian", + status: "failed", }, ]; const PENDING: PendingApproval[] = [ { - object: "approval", approval_id: "appr_atlas", client_id: "Atlas Mortgage", created_at: iso(0), - kind: "consent", - user_code: "WXYZ-1234", grant_preview: { source: null, streams: [{ name: "pay_statements" }, { name: "employment" }, { name: "transactions" }], }, + kind: "consent", + object: "approval", + user_code: "WXYZ-1234", }, ]; const SUMMARY = { - object: "dataset_summary" as const, - record_count: 48_120, - connector_count: 10, - stream_count: 24, - total_retained_bytes: 1_073_741_824, blob_bytes: 0, - record_json_bytes: 0, - record_changes_json_bytes: 0, - earliest_record_time: iso(365), - latest_record_time: iso(0), + connector_count: 10, earliest_ingested_at: iso(365), + earliest_record_time: iso(365), latest_ingested_at: iso(0), + latest_record_time: iso(0), + object: "dataset_summary" as const, + projection: { computed_at: iso(0), state: "fresh" as const }, + record_changes_json_bytes: 0, + record_count: 48_120, + record_json_bytes: 0, + stream_count: 24, top_connectors: [], - projection: { state: "fresh" as const, computed_at: iso(0) }, + total_retained_bytes: 1_073_741_824, }; export function buildDemoInputs(scenario: DemoScenario, hrefs: StandingHrefs): StandingInputs { const base: StandingInputs = { - now: NOW, - hrefs, - summary: SUMMARY, - bearerClients: BEARERS, - grants: GRANTS, - traces: TRACES, - pendingApprovals: [], - failedTraces: [], - failedRuns: [], - sourceWork: EMPTY_SOURCE_WORK_GROUPS, advisoryOwnerActions: [], attentionConnections: [], + bearerClients: BEARERS, + failedRuns: [], + failedTraces: [], + grants: GRANTS, + hrefs, + now: NOW, overviewLoadIssues: [], + pendingApprovals: [], sourceIssues: [], + sourceWork: EMPTY_SOURCE_WORK_GROUPS, + summary: SUMMARY, + traces: TRACES, }; if (scenario === "decide") { return { ...base, pendingApprovals: PENDING }; @@ -202,17 +202,17 @@ export function buildDemoInputs(scenario: DemoScenario, hrefs: StandingHrefs): S if (scenario === "alarm") { return { ...base, - failedRuns: FAILED_RUNS, attentionConnections: [ { + actionLabel: "Check the collector", connectorKey: "claude-code", - routeId: "cin_demo_claude_code", deviceLocal: true, label: "Claude Code on workstation", + routeId: "cin_demo_claude_code", what: "Check the collector before this source can make progress.", - actionLabel: "Check the collector", }, ], + failedRuns: FAILED_RUNS, }; } return base; diff --git a/apps/console/src/app/(console)/components/views/standing-view-model.test.ts b/apps/console/src/app/(console)/components/views/standing-view-model.test.ts index d2e4b71a5..6e8e00198 100644 --- a/apps/console/src/app/(console)/components/views/standing-view-model.test.ts +++ b/apps/console/src/app/(console)/components/views/standing-view-model.test.ts @@ -33,18 +33,18 @@ import { } from "./standing-view-model.ts"; const HREFS: StandingHrefs = { - grants: "/grants", - grantPackages: "/grants/packages", - notifications: "/notifications", - runs: "/syncs", - sources: "/sources", - traces: "/audit", + connection: (id) => `/sources/${id}`, deployment: "/deployment", deploymentTokens: "/deployment/tokens", - connection: (id) => `/sources/${id}`, grant: (id) => `/grants/${id}`, + grantPackages: "/grants/packages", + grants: "/grants", + notifications: "/notifications", run: (id) => `/syncs/${id}`, + runs: "/syncs", + sources: "/sources", trace: (id) => `/audit/${id}`, + traces: "/audit", }; const NOW = new Date("2026-06-13T12:00:00Z"); @@ -78,35 +78,35 @@ const WILL_NOT_CLAIM_ALL_CLEAR_RE = /will not claim all-clear from partial data/ function baseInputs(over: Partial = {}): StandingInputs { return { - now: NOW, + advisoryOwnerActions: [], + attentionConnections: [], + bearerClients: [], + failedRuns: [], + failedTraces: [], + grants: [], hrefs: HREFS, + now: NOW, + overviewLoadIssues: [], + pendingApprovals: [], + sourceIssues: [], + sourceWork: EMPTY_SOURCE_WORK_GROUPS, summary: { - object: "dataset_summary", - record_count: 48_120, - connector_count: 10, - stream_count: 24, - total_retained_bytes: 0, blob_bytes: 0, - record_json_bytes: 0, - record_changes_json_bytes: 0, - earliest_record_time: null, - latest_record_time: null, + connector_count: 10, earliest_ingested_at: null, + earliest_record_time: null, latest_ingested_at: null, - top_connectors: [], + latest_record_time: null, + object: "dataset_summary", projection: { state: "fresh" }, + record_changes_json_bytes: 0, + record_count: 48_120, + record_json_bytes: 0, + stream_count: 24, + top_connectors: [], + total_retained_bytes: 0, }, - bearerClients: [], - grants: [], traces: [], - pendingApprovals: [], - failedTraces: [], - failedRuns: [], - sourceWork: EMPTY_SOURCE_WORK_GROUPS, - advisoryOwnerActions: [], - attentionConnections: [], - overviewLoadIssues: [], - sourceIssues: [], ...over, }; } @@ -141,18 +141,18 @@ test("grantEndorseStatus collapses the live vocab to active", () => { }); test("grantReads humanizes kinds, falls back to connector, then generic", () => { - const withKinds = { kinds: ["pay_statements", "transactions"], connector_id: "plaid" } as GrantSummary; + const withKinds = { connector_id: "plaid", kinds: ["pay_statements", "transactions"] } as GrantSummary; assert.equal(grantReads(withKinds), "reads only your pay and your spending"); - const onlyConnector = { kinds: ["read"], connector_id: "employment" } as GrantSummary; + const onlyConnector = { connector_id: "employment", kinds: ["read"] } as GrantSummary; assert.equal(grantReads(onlyConnector), "reads only your employment history"); - const nothing = { kinds: [], connector_id: null } as unknown as GrantSummary; + const nothing = { connector_id: null, kinds: [] } as unknown as GrantSummary; assert.equal(grantReads(nothing), "reads a scoped slice of your data"); }); test("grantReads humanizes protocol audit stream names instead of raw event ids", () => { const auditGrant = { - kinds: ["consent.approved", "token.issued", "query.received", "disclosure.served", "query.rejected"], connector_id: "pdpp", + kinds: ["consent.approved", "token.issued", "query.received", "disclosure.served", "query.rejected"], } as GrantSummary; assert.equal( @@ -175,12 +175,12 @@ test("relDay produces calm relative labels", () => { test("hero is DECIDE when an approval is pending", () => { const pending: PendingApproval = { - object: "approval", approval_id: "a1", client_id: "Atlas Mortgage", created_at: NOW.toISOString(), - kind: "consent", grant_preview: { streams: [{ name: "pay_statements" }, { name: "transactions" }] }, + kind: "consent", + object: "approval", }; const hero = computeHero(baseInputs({ pendingApprovals: [pending] })); assert.equal(hero.tone, "decide"); @@ -196,12 +196,12 @@ test("hero ALARM for a DEVICE-LOCAL recovery: CTA NAVIGATES (does not restate th baseInputs({ attentionConnections: [ { + actionLabel: "Recover local collector uploads", connectorKey: "claude-code", - routeId: "ci_laptop", deviceLocal: true, label: "laptop Claude Code", + routeId: "ci_laptop", what: "The local collector has saved records on its host that did not upload to this server.", - actionLabel: "Recover local collector uploads", }, ], }) @@ -224,12 +224,12 @@ test("hero ALARM for a DASHBOARD-ACTIONABLE recovery keeps its action verb", () baseInputs({ attentionConnections: [ { + actionLabel: "Reconnect", connectorKey: "chase", - routeId: "cin_chase", deviceLocal: false, label: "Chase - Personal", + routeId: "cin_chase", what: "Reconnect Chase.", - actionLabel: "Reconnect", }, ], }) @@ -242,20 +242,20 @@ test("hero ALARM with several attention connections → CTA routes to the syncs baseInputs({ attentionConnections: [ { + actionLabel: "Check the collector", connectorKey: "claude-code", - routeId: "ci_laptop", deviceLocal: true, label: "laptop Claude Code", + routeId: "ci_laptop", what: "Check the collector.", - actionLabel: "Check the collector", }, { + actionLabel: "Reconnect", connectorKey: "chase", - routeId: "cin_chase", deviceLocal: false, label: "Chase - Personal", + routeId: "cin_chase", what: "Reconnect Chase.", - actionLabel: "Reconnect", }, ], }) @@ -268,29 +268,29 @@ test("hero ALARM with several attention connections → CTA routes to the syncs test("failed syncs/traces alone do NOT drive the alarm — only the rendered-verdict attention set does", () => { // The old bug: a failed YNAB trace inflated the alarm while YNAB was healthy. - const run = { run_id: "r1", connector_id: "current_activity", failure_reason: "expired" } as RunSummary; - const trace = { trace_id: "t1", client_id: "ynab" } as TraceSummary; - const hero = computeHero(baseInputs({ failedRuns: [run], failedTraces: [trace], attentionConnections: [] })); + const run = { connector_id: "current_activity", failure_reason: "expired", run_id: "r1" } as RunSummary; + const trace = { client_id: "ynab", trace_id: "t1" } as TraceSummary; + const hero = computeHero(baseInputs({ attentionConnections: [], failedRuns: [run], failedTraces: [trace] })); assert.notEqual(hero.tone, "alarm"); }); test("decide wins over alarm", () => { const pending = { - object: "approval", approval_id: "a1", created_at: NOW.toISOString(), kind: "consent", + object: "approval", } as PendingApproval; const both = computeHero( baseInputs({ attentionConnections: [ { + actionLabel: "Reconnect", connectorKey: "chase", - routeId: "cin_chase", deviceLocal: false, label: "Chase - Personal", + routeId: "cin_chase", what: "x", - actionLabel: "Reconnect", }, ], pendingApprovals: [pending], @@ -343,18 +343,17 @@ function verdict( over: Partial> ): RefConnectorSummary["rendered_verdict"] { return { + annotations: [], channel: "attention", - pill: { label: "Can't collect", tone: "red" }, + detail: undefined, forward_statement: "Check the collector before this source can make progress.", + pill: { label: "Can't collect", tone: "red" }, required_actions: [ { affects: [], audience: "owner", cta: "Check the collector", kind: "add_info", - satisfied_when: { kind: "attention_resolved" }, - terminal: false, - urgency: "now", remediation: { cause: "dead_letter_backlog", commands: [], @@ -363,17 +362,18 @@ function verdict( summary: "The local collector has saved records on its host that did not upload to this server.", target: { identity_source: "source_instance_bindings", kind: "local_device" }, }, + satisfied_when: { kind: "attention_resolved" }, + terminal: false, + urgency: "now", }, ], - annotations: [], - detail: undefined, ...over, } as RefConnectorSummary["rendered_verdict"]; } test("attention truth: only attention-channel connections with an owner-satisfiable action count", () => { const connectors: RefConnectorSummary[] = [ - connector({ connector_id: "claude-code", connection_id: "cin_laptop", rendered_verdict: verdict({}) }), // ✓ attention + owner action + connector({ connection_id: "cin_laptop", connector_id: "claude-code", rendered_verdict: verdict({}) }), // ✓ attention + owner action connector({ connector_id: "calm-source", rendered_verdict: verdict({ channel: "calm" }) }), // ✗ calm connector({ connector_id: "ynab", rendered_verdict: null }), // ✗ no verdict (e.g. healthy) connector({ @@ -392,7 +392,7 @@ test("attention truth: only attention-channel connections with an owner-satisfia ], }), }), // ✗ attention but no owner-satisfiable action (S1 — code_fix is the maintainer's, not the owner's) - connector({ connector_id: "revoked", revoked_at: "2026-06-01T00:00:00Z", rendered_verdict: verdict({}) }), // ✗ revoked + connector({ connector_id: "revoked", rendered_verdict: verdict({}), revoked_at: "2026-06-01T00:00:00Z" }), // ✗ revoked ]; const attention = attentionConnectionsFromConnectors(connectors); assert.deepEqual( @@ -409,9 +409,9 @@ test("attention truth: only attention-channel connections with an owner-satisfia test("attention truth falls back to legacy blocked health when rendered verdict is absent", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "chase", - connection_id: "cin_chase", connection_health: legacyHealth("blocked"), + connection_id: "cin_chase", + connector_id: "chase", display_name: "Chase", rendered_verdict: null, }), @@ -430,13 +430,13 @@ test("attention truth falls back to legacy blocked health when rendered verdict test("source issues show non-owner material verdicts without alarming as owner attention", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "chase", connection_id: "cin_chase", + connector_id: "chase", display_name: "Chase", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Can't collect", tone: "red" }, forward_statement: "This connector needs a code fix before it can collect again.", + pill: { label: "Can't collect", tone: "red" }, required_actions: [ { affects: [], @@ -456,8 +456,8 @@ test("source issues show non-owner material verdicts without alarming as owner a }), connector({ connector_id: "revoked", - revoked_at: "2026-06-01T00:00:00Z", rendered_verdict: verdict({ pill: { label: "Can't collect", tone: "red" } }), + revoked_at: "2026-06-01T00:00:00Z", }), ]; @@ -479,13 +479,13 @@ test("source issues show non-owner material verdicts without alarming as owner a test("advisory owner actions surface non-urgent Amazon retry work without calm all-clear copy", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "amazon", connection_id: "cin_amazon", + connector_id: "amazon", display_name: "Amazon - Personal", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Degraded", tone: "amber" }, forward_statement: "Some order detail is still outstanding. Retry this source to collect the missing detail.", + pill: { label: "Degraded", tone: "amber" }, required_actions: [ { affects: ["orders"], @@ -525,13 +525,13 @@ test("advisory owner actions surface non-urgent Amazon retry work without calm a test("advisory owner actions surface Reddit refresh work in the home summary", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "reddit", connection_id: "cin_reddit", + connector_id: "reddit", display_name: "Reddit", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Healthy", tone: "green" }, forward_statement: "Run a refresh when you want the latest saved posts.", + pill: { label: "Healthy", tone: "green" }, required_actions: [ { affects: [], @@ -558,13 +558,13 @@ test("advisory owner actions surface Reddit refresh work in the home summary", ( test("source actionability groups live-shaped rows with scoped counts", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "chatgpt", connection_id: "cin_chatgpt", + connector_id: "chatgpt", display_name: "ChatGPT - personal", rendered_verdict: verdict({ channel: "attention", - pill: { label: "Can't collect", tone: "red" }, forward_statement: "Reconnect this account and collection resumes.", + pill: { label: "Can't collect", tone: "red" }, required_actions: [ { affects: [], @@ -579,13 +579,13 @@ test("source actionability groups live-shaped rows with scoped counts", () => { }), }), connector({ - connector_id: "usaa", connection_id: "cin_usaa", + connector_id: "usaa", display_name: "USAA - Personal", rendered_verdict: verdict({ channel: "attention", - pill: { label: "Can't collect", tone: "red" }, forward_statement: "Reconnect this account and collection resumes.", + pill: { label: "Can't collect", tone: "red" }, required_actions: [ { affects: [], @@ -600,19 +600,19 @@ test("source actionability groups live-shaped rows with scoped counts", () => { }), }), connector({ - connector_id: "claude-code", connection_id: "cin_claude", + connector_id: "claude-code", display_name: "Local Claude Code", rendered_verdict: verdict({}), }), connector({ - connector_id: "amazon", connection_id: "cin_amazon", + connector_id: "amazon", display_name: "Amazon - Personal", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Degraded", tone: "amber" }, forward_statement: "Run a refresh to bring this up to date.", + pill: { label: "Degraded", tone: "amber" }, required_actions: [ { affects: [], @@ -627,13 +627,13 @@ test("source actionability groups live-shaped rows with scoped counts", () => { }), }), connector({ - connector_id: "chase", connection_id: "cin_chase", + connector_id: "chase", display_name: "Chase - Personal", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Degraded", tone: "amber" }, forward_statement: "Latest collection completed with known coverage gaps.", + pill: { label: "Degraded", tone: "amber" }, required_actions: [ { affects: [], @@ -648,13 +648,13 @@ test("source actionability groups live-shaped rows with scoped counts", () => { }), }), connector({ - connector_id: "github", connection_id: "cin_github", + connector_id: "github", display_name: "GitHub - Personal", rendered_verdict: verdict({ channel: "calm", - pill: { label: "Not measured", tone: "grey" }, forward_statement: "Coverage has not been measured yet.", + pill: { label: "Not measured", tone: "grey" }, required_actions: [], }), }), @@ -694,8 +694,8 @@ const DASHBOARD_PASSIVE_RECOVERY_RE = /catching up|syncing details|safe to retry function deferredRecoveryVerdict(): RefConnectorSummary["rendered_verdict"] { return verdict({ channel: "calm", - pill: { label: "Degraded", tone: "amber" }, forward_statement: "The next run is expected to fill the remaining data.", + pill: { label: "Degraded", tone: "amber" }, required_actions: [ { affects: [], @@ -732,13 +732,13 @@ function inactiveRecoveryHealth(): RefConnectorSummary["connection_health"] { test("dashboard cross-surface: every source-work section count equals its rendered row count", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "chatgpt", connection_id: "cin_chatgpt", + connector_id: "chatgpt", display_name: "ChatGPT - personal", rendered_verdict: verdict({ channel: "attention", - pill: { label: "Can't collect", tone: "red" }, forward_statement: "Reconnect this account and collection resumes.", + pill: { label: "Can't collect", tone: "red" }, required_actions: [ { affects: [], @@ -753,21 +753,21 @@ test("dashboard cross-surface: every source-work section count equals its render }), }), connector({ - connector_id: "amazon", - connection_id: "cin_amazon", - display_name: "Amazon - Personal", // Durable, inactive recovery backlog → passive progress ("PDPP is working"). connection_health: inactiveRecoveryHealth(), + connection_id: "cin_amazon", + connector_id: "amazon", + display_name: "Amazon - Personal", rendered_verdict: deferredRecoveryVerdict(), }), connector({ - connector_id: "reddit", connection_id: "cin_reddit", + connector_id: "reddit", display_name: "Reddit - Personal", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Degraded", tone: "amber" }, forward_statement: "Run a refresh to bring this up to date.", + pill: { label: "Degraded", tone: "amber" }, required_actions: [ { affects: [], @@ -782,13 +782,13 @@ test("dashboard cross-surface: every source-work section count equals its render }), }), connector({ - connector_id: "chase", connection_id: "cin_chase", + connector_id: "chase", display_name: "Chase - Personal", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Can't collect", tone: "red" }, forward_statement: "Connector code needs a fix before this can collect again.", + pill: { label: "Can't collect", tone: "red" }, required_actions: [ { affects: [], @@ -831,10 +831,10 @@ test("dashboard cross-surface: every source-work section count equals its render test("dashboard cross-surface: an inactive queued recovery row is passive progress, never a Checking or degraded row", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "amazon", + connection_health: inactiveRecoveryHealth(), connection_id: "cin_amazon", + connector_id: "amazon", display_name: "Amazon - Personal", - connection_health: inactiveRecoveryHealth(), rendered_verdict: deferredRecoveryVerdict(), }), ]; @@ -850,7 +850,7 @@ test("dashboard cross-surface: an inactive queued recovery row is passive progre const allRows = data.sourceWorkSections.flatMap((section) => section.rows); assert.equal(allRows.length, 1); - const row = allRows[0]; + const [row] = allRows; assert.ok(row); assert.doesNotMatch(row.what, DASHBOARD_CHECKING_RE); assert.match(`${row.what} ${row.why ?? ""}`, DASHBOARD_PASSIVE_RECOVERY_RE); @@ -859,13 +859,13 @@ test("dashboard cross-surface: an inactive queued recovery row is passive progre test("reviewable degraded source appears once rather than as review plus source issue", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "amazon", connection_id: "cin_amazon", + connector_id: "amazon", display_name: "Amazon - Personal", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Degraded", tone: "amber" }, forward_statement: "Retry now to give the recoverable gap another run.", + pill: { label: "Degraded", tone: "amber" }, required_actions: [ { affects: [], @@ -894,13 +894,13 @@ test("reviewable degraded source appears once rather than as review plus source test("source actionability follows primary-action parity with push policy", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "mixed", connection_id: "cin_mixed", + connector_id: "mixed", display_name: "Mixed-action source", rendered_verdict: verdict({ channel: "attention", - pill: { label: "Can't collect", tone: "red" }, forward_statement: "Connector code needs a fix before this can collect again.", + pill: { label: "Can't collect", tone: "red" }, required_actions: [ { affects: [], @@ -935,13 +935,13 @@ test("source actionability follows primary-action parity with push policy", () = test("maintainer-only actions are not advisory owner actions", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "maintainer-only", connection_id: "cin_maintainer", + connector_id: "maintainer-only", display_name: "Maintainer-only source", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Degraded", tone: "amber" }, forward_statement: "This source needs a connector code fix before it can make progress.", + pill: { label: "Degraded", tone: "amber" }, required_actions: [ { affects: [], @@ -965,13 +965,13 @@ test("maintainer-only actions are not advisory owner actions", () => { test("source issues omit healthy advisory refresh hints", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "reddit", connection_id: "cin_reddit", + connector_id: "reddit", display_name: "Reddit - dondochaka", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Healthy", tone: "green" }, forward_statement: "Run a refresh to bring this up to date.", + pill: { label: "Healthy", tone: "green" }, required_actions: [ { affects: [], @@ -998,13 +998,13 @@ test("source issues omit healthy advisory refresh hints", () => { test("source issues surface attention verdicts that have no owner action, even with a green pill", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "maintainer-only", connection_id: "cin_maintainer", + connector_id: "maintainer-only", display_name: "Maintainer-only source", rendered_verdict: verdict({ channel: "attention", - pill: { label: "Healthy", tone: "green" }, forward_statement: "This source needs a maintainer action before it can make progress.", + pill: { label: "Healthy", tone: "green" }, required_actions: [ { affects: [], @@ -1033,9 +1033,9 @@ test("source issues surface attention verdicts that have no owner action, even w test("source issues fall back to legacy degraded health when rendered verdict is absent", () => { const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "usaa", - connection_id: "cin_usaa", connection_health: legacyHealth("degraded"), + connection_id: "cin_usaa", + connector_id: "usaa", display_name: "USAA - Personal", rendered_verdict: null, }), @@ -1064,13 +1064,13 @@ test("attention routeId targets the EXACT connection instance, not the connector // the CTA lands on the connection that actually needs the owner. const connectors: RefConnectorSummary[] = [ connector({ - connector_id: "claude-code", connection_id: "cin_simon", + connector_id: "claude-code", rendered_verdict: verdict({ channel: "calm" }), }), // healthy, first connector({ - connector_id: "claude-code", connection_id: "cin_laptop", + connector_id: "claude-code", connector_instance_id: "ci_laptop", rendered_verdict: verdict({}), }), // the attention one @@ -1091,8 +1091,8 @@ test("hero ALARMs on a stale projection even with no failures", () => { stale.summary = { ...stale.summary, projection: { - state: "stale", last_error: "bulk write on unknown connection", + state: "stale", }, } as StandingInputs["summary"]; const hero = computeHero(stale); @@ -1113,8 +1113,8 @@ test("hero uses owner-safe copy for failed projection details", () => { failed.summary = { ...failed.summary, projection: { - state: "failed", last_error: "SQL failed: bulk write on unknown connection", + state: "failed", }, } as StandingInputs["summary"]; const hero = computeHero(failed); @@ -1146,7 +1146,7 @@ test("hero ALARMs when dashboard inputs fail instead of claiming all-clear from test("hero is CALM with reassurance when all is well", () => { const clients: OwnerIssuedClient[] = [ - { client_id: "c1", client_name: "Claude Desktop", active_token_count: 1, created_at: NOW.toISOString() }, + { active_token_count: 1, client_id: "c1", client_name: "Claude Desktop", created_at: NOW.toISOString() }, ]; const hero = computeHero(baseInputs({ bearerClients: clients })); assert.equal(hero.tone, "calm"); @@ -1156,8 +1156,8 @@ test("hero is CALM with reassurance when all is well", () => { test("bearer section and hero count only active owner tokens", () => { const clients: OwnerIssuedClient[] = [ - { client_id: "inactive", client_name: "Old smoke client", active_token_count: 0, created_at: NOW.toISOString() }, - { client_id: "active", client_name: "Claude Desktop", active_token_count: 2, created_at: NOW.toISOString() }, + { active_token_count: 0, client_id: "inactive", client_name: "Old smoke client", created_at: NOW.toISOString() }, + { active_token_count: 2, client_id: "active", client_name: "Claude Desktop", created_at: NOW.toISOString() }, ]; const data = buildStandingData(baseInputs({ bearerClients: clients })); @@ -1169,7 +1169,7 @@ test("bearer section and hero count only active owner tokens", () => { test("hero says no owner token can act when all issued clients are inactive", () => { const clients: OwnerIssuedClient[] = [ - { client_id: "inactive", client_name: "Old smoke client", active_token_count: 0, created_at: NOW.toISOString() }, + { active_token_count: 0, client_id: "inactive", client_name: "Old smoke client", created_at: NOW.toISOString() }, ]; const data = buildStandingData(baseInputs({ bearerClients: clients })); @@ -1181,7 +1181,7 @@ test("hero says no owner token can act when all issued clients are inactive", () test("bearer row carries the raw created_at for the shared IcTimestamp, not a prebaked date string", () => { const clients: OwnerIssuedClient[] = [ - { client_id: "c1", client_name: "Claude Desktop", active_token_count: 1, created_at: "2026-06-01T00:00:00Z" }, + { active_token_count: 1, client_id: "c1", client_name: "Claude Desktop", created_at: "2026-06-01T00:00:00Z" }, ]; const data = buildStandingData(baseInputs({ bearerClients: clients })); @@ -1197,10 +1197,10 @@ test("bearer row carries the raw created_at for the shared IcTimestamp, not a pr test('single-token bearer labels created_at "issued"; multi-token bearer degrades to "first issued"', () => { const single: OwnerIssuedClient[] = [ - { client_id: "one", client_name: "Solo", active_token_count: 1, created_at: NOW.toISOString() }, + { active_token_count: 1, client_id: "one", client_name: "Solo", created_at: NOW.toISOString() }, ]; const multi: OwnerIssuedClient[] = [ - { client_id: "many", client_name: "Legacy", active_token_count: 3, created_at: NOW.toISOString() }, + { active_token_count: 3, client_id: "many", client_name: "Legacy", created_at: NOW.toISOString() }, ]; const singleData = buildStandingData(baseInputs({ bearerClients: single })); @@ -1216,9 +1216,9 @@ test('single-token bearer labels created_at "issued"; multi-token bearer degrade test("bearer block previews the most-recent few and reports the hidden overflow count", () => { const clients: OwnerIssuedClient[] = Array.from({ length: 7 }, (_, i) => ({ + active_token_count: 1, client_id: `c${i}`, client_name: `Client ${i}`, - active_token_count: 1, created_at: NOW.toISOString(), })); const data = buildStandingData(baseInputs({ bearerClients: clients })); @@ -1231,7 +1231,7 @@ test("bearer block previews the most-recent few and reports the hidden overflow test("bearer overflow is zero when active bearers fit within the preview cap", () => { const clients: OwnerIssuedClient[] = [ - { client_id: "c1", client_name: "Claude Desktop", active_token_count: 1, created_at: NOW.toISOString() }, + { active_token_count: 1, client_id: "c1", client_name: "Claude Desktop", created_at: NOW.toISOString() }, ]; const data = buildStandingData(baseInputs({ bearerClients: clients })); @@ -1244,43 +1244,43 @@ test("bearer overflow is zero when active bearers fit within the preview cap", ( test("grant packages are surfaced from loaded grants without a count endpoint", () => { const grants: GrantSummary[] = [ { - object: "grant_summary", - grant_id: "g1", client_id: "App A", connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T00:00:00Z", event_count: 5, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", grant_package_id: "pkg_alpha", + kinds: ["query.received"], + last_at: "2026-06-12T00:00:00Z", + object: "grant_summary", + status: "active", }, { - object: "grant_summary", - grant_id: "g2", client_id: "App A", connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T00:00:00Z", event_count: 3, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g2", grant_package_id: "pkg_alpha", + kinds: ["query.received"], + last_at: "2026-06-12T00:00:00Z", + object: "grant_summary", + status: "active", }, { - object: "grant_summary", - grant_id: "g3", client_id: "App B", connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T00:00:00Z", event_count: 1, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g3", grant_package_id: "pkg_beta", + kinds: ["query.received"], + last_at: "2026-06-12T00:00:00Z", + object: "grant_summary", + status: "active", }, ]; @@ -1300,20 +1300,20 @@ test("the authoritative grant-package count drives the overview badge when prese // The overview must trust the endpoint (exact), not the loaded-grants floor, // so packages not represented in the preview still surface. const grant: GrantSummary = { - object: "grant_summary", - grant_id: "g1", client_id: "App A", connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T00:00:00Z", event_count: 5, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", grant_package_id: "pkg_alpha", + kinds: ["query.received"], + last_at: "2026-06-12T00:00:00Z", + object: "grant_summary", + status: "active", }; - const data = buildStandingData(baseInputs({ grants: [grant], grantPackageCount: 4 })); + const data = buildStandingData(baseInputs({ grantPackageCount: 4, grants: [grant] })); assert.equal(data.grantPackages?.count, 4); assert.equal(data.grantPackages?.exact, true); assert.equal(data.grantPackages?.href, HREFS.grantPackages); @@ -1321,55 +1321,55 @@ test("the authoritative grant-package count drives the overview badge when prese test("an authoritative zero grant-package count collapses the badge even if a stale grant carries a package id", () => { const grant: GrantSummary = { - object: "grant_summary", - grant_id: "g1", client_id: "App A", connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T00:00:00Z", event_count: 5, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", grant_package_id: "pkg_alpha", + kinds: ["query.received"], + last_at: "2026-06-12T00:00:00Z", + object: "grant_summary", + status: "active", }; - const data = buildStandingData(baseInputs({ grants: [grant], grantPackageCount: 0 })); + const data = buildStandingData(baseInputs({ grantPackageCount: 0, grants: [grant] })); assert.equal(data.grantPackages, null); }); test("a null grant-package count falls back to the loaded-grants floor", () => { const grant: GrantSummary = { - object: "grant_summary", - grant_id: "g1", client_id: "App A", connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T00:00:00Z", event_count: 5, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", grant_package_id: "pkg_alpha", + kinds: ["query.received"], + last_at: "2026-06-12T00:00:00Z", + object: "grant_summary", + status: "active", }; - const data = buildStandingData(baseInputs({ grants: [grant], grantPackageCount: null })); + const data = buildStandingData(baseInputs({ grantPackageCount: null, grants: [grant] })); assert.equal(data.grantPackages?.count, 1); assert.equal(data.grantPackages?.exact, false); }); test("grant packages are absent when no loaded grant carries a package id", () => { const grant: GrantSummary = { - object: "grant_summary", - grant_id: "g1", client_id: "App A", connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T00:00:00Z", event_count: 5, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", + kinds: ["query.received"], + last_at: "2026-06-12T00:00:00Z", + object: "grant_summary", + status: "active", }; const data = buildStandingData(baseInputs({ grants: [grant] })); @@ -1378,17 +1378,17 @@ test("grant packages are absent when no loaded grant carries a package id", () = test("a fully-revoked package does not advertise from the overview", () => { const grant: GrantSummary = { - object: "grant_summary", - grant_id: "g1", client_id: "App A", connector_id: "pdpp", - status: "revoked", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T00:00:00Z", event_count: 5, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", grant_package_id: "pkg_dead", + kinds: ["query.received"], + last_at: "2026-06-12T00:00:00Z", + object: "grant_summary", + status: "revoked", }; const data = buildStandingData(baseInputs({ grants: [grant] })); @@ -1399,35 +1399,35 @@ test("a fully-revoked package does not advertise from the overview", () => { test("buildStandingData wires bearers, relationships, lately, attention", () => { const clients: OwnerIssuedClient[] = [ - { client_id: "c1", client_name: "Claude Desktop", active_token_count: 2, created_at: "2026-06-01T00:00:00Z" }, + { active_token_count: 2, client_id: "c1", client_name: "Claude Desktop", created_at: "2026-06-01T00:00:00Z" }, ]; const grant: GrantSummary = { - object: "grant_summary", - grant_id: "g1", client_id: "Atlas Mortgage", connector_id: "plaid", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T00:00:00Z", event_count: 5, - kinds: ["pay_statements"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", + kinds: ["pay_statements"], + last_at: "2026-06-12T00:00:00Z", + object: "grant_summary", + status: "active", }; const readTrace: TraceSummary = { - object: "trace_summary", - trace_id: "t1", - status: "succeeded", actor_id: "Claude Desktop", actor_type: "client", client_id: "Claude Desktop", - grant_id: null, - run_id: null, - request_id: null, - first_at: "2026-06-13T00:00:00Z", - last_at: "2026-06-13T00:00:00Z", event_count: 412, - kinds: ["transactions"], failure: null, + first_at: "2026-06-13T00:00:00Z", + grant_id: null, + kinds: ["transactions"], + last_at: "2026-06-13T00:00:00Z", + object: "trace_summary", + request_id: null, + run_id: null, + status: "succeeded", + trace_id: "t1", }; const data = buildStandingData(baseInputs({ bearerClients: clients, grants: [grant], traces: [readTrace] })); assert.equal(data.bearers.length, 1); @@ -1444,25 +1444,25 @@ test("buildStandingData wires bearers, relationships, lately, attention", () => test("lately uses trace client metadata instead of raw client ids", () => { const trace: TraceSummary = { - object: "trace_summary", - trace_id: "trc_named", - status: "succeeded", actor_id: "client", actor_type: "client", - client_id: "cli_named", client: { client_id: "cli_named", client_name: "Claude", registration_mode: "dynamic", }, - grant_id: null, - run_id: null, - request_id: null, - first_at: "2026-06-13T00:00:00Z", - last_at: "2026-06-13T00:00:00Z", + client_id: "cli_named", event_count: 3, - kinds: ["query.received"], failure: null, + first_at: "2026-06-13T00:00:00Z", + grant_id: null, + kinds: ["query.received"], + last_at: "2026-06-13T00:00:00Z", + object: "trace_summary", + request_id: null, + run_id: null, + status: "succeeded", + trace_id: "trc_named", }; const data = buildStandingData(baseInputs({ traces: [trace] })); @@ -1474,23 +1474,23 @@ test("lately uses trace client metadata instead of raw client ids", () => { test("lately humanizes live denial reason codes instead of rendering raw diagnostics", () => { const trace: TraceSummary = { - object: "trace_summary", - trace_id: "trc_orphaned", - status: "denied", actor_id: "slack", actor_type: "client", client_id: "slack", - grant_id: null, - run_id: "run_orphaned", - request_id: null, - first_at: "2026-06-13T00:00:00Z", - last_at: "2026-06-13T00:00:00Z", event_count: 1, - kinds: ["query.rejected"], failure: { event_type: "run.started", reason: "orphaned_started_run", }, + first_at: "2026-06-13T00:00:00Z", + grant_id: null, + kinds: ["query.rejected"], + last_at: "2026-06-13T00:00:00Z", + object: "trace_summary", + request_id: null, + run_id: "run_orphaned", + status: "denied", + trace_id: "trc_orphaned", }; const data = buildStandingData(baseInputs({ traces: [trace] })); @@ -1502,23 +1502,23 @@ test("lately humanizes live denial reason codes instead of rendering raw diagnos test("lately does not fall through to unknown snake-case denial reasons", () => { const trace: TraceSummary = { - object: "trace_summary", - trace_id: "trc_unknown_denial", - status: "denied", actor_id: "client", actor_type: "client", client_id: "client", - grant_id: null, - run_id: null, - request_id: null, - first_at: "2026-06-13T00:00:00Z", - last_at: "2026-06-13T00:00:00Z", event_count: 1, - kinds: ["query.rejected"], failure: { event_type: "query.rejected", reason: "new_internal_reason_code", }, + first_at: "2026-06-13T00:00:00Z", + grant_id: null, + kinds: ["query.rejected"], + last_at: "2026-06-13T00:00:00Z", + object: "trace_summary", + request_id: null, + run_id: null, + status: "denied", + trace_id: "trc_unknown_denial", }; const data = buildStandingData(baseInputs({ traces: [trace] })); @@ -1531,42 +1531,42 @@ test("lately does not fall through to unknown snake-case denial reasons", () => test("lately does not overclaim off-surface expired or credential scope failures", () => { const traces: TraceSummary[] = [ { - object: "trace_summary", - trace_id: "trc_state_expired", - status: "denied", actor_id: "state_client", actor_type: "client", client_id: "state_client", - grant_id: null, - run_id: null, - request_id: null, - first_at: "2026-06-13T00:00:00Z", - last_at: "2026-06-13T00:00:00Z", event_count: 1, - kinds: ["query.rejected"], failure: { event_type: "query.rejected", reason: "state_expired", }, - }, - { + first_at: "2026-06-13T00:00:00Z", + grant_id: null, + kinds: ["query.rejected"], + last_at: "2026-06-13T00:00:00Z", object: "trace_summary", - trace_id: "trc_credential_scope", + request_id: null, + run_id: null, status: "denied", + trace_id: "trc_state_expired", + }, + { actor_id: "credential_client", actor_type: "client", client_id: "credential_client", - grant_id: null, - run_id: null, - request_id: null, - first_at: "2026-06-13T00:00:01Z", - last_at: "2026-06-13T00:00:01Z", event_count: 1, - kinds: ["query.rejected"], failure: { event_type: "query.rejected", reason: "github_credential_insufficient_scope", }, + first_at: "2026-06-13T00:00:01Z", + grant_id: null, + kinds: ["query.rejected"], + last_at: "2026-06-13T00:00:01Z", + object: "trace_summary", + request_id: null, + run_id: null, + status: "denied", + trace_id: "trc_credential_scope", }, ]; @@ -1580,20 +1580,20 @@ test("lately does not overclaim off-surface expired or credential scope failures test("lately does not bold raw technical client ids when metadata is missing", () => { const trace: TraceSummary = { - object: "trace_summary", - trace_id: "trc_raw", - status: "succeeded", actor_id: "cli_raw", actor_type: "client", client_id: "cli_raw", - grant_id: null, - run_id: null, - request_id: null, - first_at: "2026-06-13T00:00:00Z", - last_at: "2026-06-13T00:00:00Z", event_count: 3, - kinds: ["query.received"], failure: null, + first_at: "2026-06-13T00:00:00Z", + grant_id: null, + kinds: ["query.received"], + last_at: "2026-06-13T00:00:00Z", + object: "trace_summary", + request_id: null, + run_id: null, + status: "succeeded", + trace_id: "trc_raw", }; const data = buildStandingData(baseInputs({ traces: [trace] })); @@ -1606,20 +1606,20 @@ test("lately does not bold raw technical client ids when metadata is missing", ( test("lately does not render bare opaque client ids as names", () => { const opaqueClientId = "d9f1c1bb7a5c4a6f9e8d7c6b5a4f3210"; const trace: TraceSummary = { - object: "trace_summary", - trace_id: "trc_opaque", - status: "succeeded", actor_id: opaqueClientId, actor_type: "unknown", client_id: opaqueClientId, - grant_id: null, - run_id: null, - request_id: null, - first_at: "2026-06-13T00:00:00Z", - last_at: "2026-06-13T00:00:00Z", event_count: 3, - kinds: ["query.received"], failure: null, + first_at: "2026-06-13T00:00:00Z", + grant_id: null, + kinds: ["query.received"], + last_at: "2026-06-13T00:00:00Z", + object: "trace_summary", + request_id: null, + run_id: null, + status: "succeeded", + trace_id: "trc_opaque", }; const data = buildStandingData(baseInputs({ traces: [trace] })); @@ -1633,37 +1633,37 @@ test("lately summarizes identical recent reads instead of repeating the same row const repeated = Array.from( { length: 5 }, (_, i): TraceSummary => ({ - object: "trace_summary", - trace_id: `trc_longview_${i}`, - status: "succeeded", actor_id: "client", actor_type: "client", - client_id: "cli_longview", client: { client_id: "cli_longview", client_name: "Longview CLI", registration_mode: "pre_registered_public", }, - grant_id: null, - run_id: null, - request_id: null, - first_at: "2026-06-13T00:00:00Z", - last_at: "2026-06-13T00:00:00Z", + client_id: "cli_longview", event_count: 3, - kinds: ["query.received"], failure: null, + first_at: "2026-06-13T00:00:00Z", + grant_id: null, + kinds: ["query.received"], + last_at: "2026-06-13T00:00:00Z", + object: "trace_summary", + request_id: null, + run_id: null, + status: "succeeded", + trace_id: `trc_longview_${i}`, }) ); - const baseRepeated = repeated[0]; + const [baseRepeated] = repeated; assert.ok(baseRepeated); const different: TraceSummary = { ...baseRepeated, - trace_id: "trc_controller", actor_id: "controller", actor_type: "runtime", - client_id: null, client: undefined, + client_id: null, event_count: 1, + trace_id: "trc_controller", }; const data = buildStandingData(baseInputs({ traces: [...repeated, different] })); @@ -1678,28 +1678,28 @@ test("lately summarizes identical recent reads instead of repeating the same row test("relationships summarize grants by client instead of repeating one row per grant", () => { const grants: GrantSummary[] = [ { - object: "grant_summary", - grant_id: "g1", client_id: "CLI agent", connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-10T00:00:00Z", event_count: 5, - kinds: ["token.issued", "query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", + kinds: ["token.issued", "query.received"], + last_at: "2026-06-10T00:00:00Z", + object: "grant_summary", + status: "active", }, { - object: "grant_summary", - grant_id: "g2", client_id: "CLI agent", connector_id: "pdpp", - status: "active", - first_at: "2026-06-02T00:00:00Z", - last_at: "2026-06-12T12:00:00Z", event_count: 7, - kinds: ["disclosure.served", "query.rejected"], failure: null, + first_at: "2026-06-02T00:00:00Z", + grant_id: "g2", + kinds: ["disclosure.served", "query.rejected"], + last_at: "2026-06-12T12:00:00Z", + object: "grant_summary", + status: "active", }, ]; @@ -1719,16 +1719,16 @@ test("relationships summarize grants by client instead of repeating one row per test("relationships use known client names without replacing the verified client id", () => { const grants: GrantSummary[] = [ { - object: "grant_summary", - grant_id: "g1", client_id: "cli_known", connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T12:00:00Z", event_count: 7, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", + kinds: ["query.received"], + last_at: "2026-06-12T12:00:00Z", + object: "grant_summary", + status: "active", }, ]; const clients: OwnerIssuedClient[] = [ @@ -1751,21 +1751,21 @@ test("relationships use known client names without replacing the verified client test("relationships prefer grant client metadata over owner-token labels", () => { const grants: GrantSummary[] = [ { - object: "grant_summary", - grant_id: "g1", - client_id: "cli_known", client: { client_id: "cli_known", client_name: "Claude Code", registration_mode: "dynamic", }, + client_id: "cli_known", connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T12:00:00Z", event_count: 7, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", + kinds: ["query.received"], + last_at: "2026-06-12T12:00:00Z", + object: "grant_summary", + status: "active", }, ]; const clients: OwnerIssuedClient[] = [ @@ -1788,16 +1788,16 @@ test("relationships do not render raw URL client ids as owner-facing names", () const urlClientId = "https://chatgpt.com/oauth/Dyp26IIu2iQg/client.json?token_endpoint_auth_method=none"; const grants: GrantSummary[] = [ { - object: "grant_summary", - grant_id: "g1", client_id: urlClientId, connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T12:00:00Z", event_count: 7, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", + kinds: ["query.received"], + last_at: "2026-06-12T12:00:00Z", + object: "grant_summary", + status: "active", }, ]; @@ -1813,16 +1813,16 @@ test("relationships do not render bare UUID or opaque client ids as owner-facing for (const clientId of ["0b643449-9516-45e0-b375-7feb9ecb7a58", "d9f1c1bb7a5c4a6f9e8d7c6b5a4f3210"]) { const grants: GrantSummary[] = [ { - object: "grant_summary", - grant_id: "g1", client_id: clientId, connector_id: "pdpp", - status: "active", - first_at: "2026-06-01T00:00:00Z", - last_at: "2026-06-12T12:00:00Z", event_count: 7, - kinds: ["query.received"], failure: null, + first_at: "2026-06-01T00:00:00Z", + grant_id: "g1", + kinds: ["query.received"], + last_at: "2026-06-12T12:00:00Z", + object: "grant_summary", + status: "active", }, ]; @@ -1837,16 +1837,16 @@ test("relationships do not render bare UUID or opaque client ids as owner-facing test("revoked grants are excluded from relationships", () => { const revoked: GrantSummary = { - object: "grant_summary", - grant_id: "g2", client_id: "Old App", connector_id: null, - status: "revoked", - first_at: "2026-05-01T00:00:00Z", - last_at: "2026-05-02T00:00:00Z", event_count: 0, - kinds: [], failure: null, + first_at: "2026-05-01T00:00:00Z", + grant_id: "g2", + kinds: [], + last_at: "2026-05-02T00:00:00Z", + object: "grant_summary", + status: "revoked", }; const data = buildStandingData(baseInputs({ grants: [revoked] })); assert.equal(data.relationships.length, 0); diff --git a/apps/console/src/app/(console)/components/views/standing-view-model.ts b/apps/console/src/app/(console)/components/views/standing-view-model.ts index f8deaa706..20e5b387f 100644 --- a/apps/console/src/app/(console)/components/views/standing-view-model.ts +++ b/apps/console/src/app/(console)/components/views/standing-view-model.ts @@ -41,31 +41,31 @@ import { // back to its own prettified name. We never invent a meaning we can't justify. const SCOPE_HUMAN: Record = { - pay_statements: "your pay", - paystubs: "your pay", - income: "your pay", - employment: "your employment history", - listening_history: "what you listen to", - watch_history: "what you watch", - transactions: "your spending", - current_activity: "your spending", - statements: "your statements", - tax_docs: "your tax documents", - tax_documents: "your tax documents", - browsing: "your browsing", browser_history: "your browsing", - messages: "your messages", + browsing: "your browsing", + "consent.approved": "grant decisions", conversations: "your conversations", + current_activity: "your spending", + "disclosure.served": "data disclosures", emails: "your email", - orders: "your orders", - purchases: "your purchases", + employment: "your employment history", health: "your health records", + income: "your pay", + listening_history: "what you listen to", location: "where you've been", - "consent.approved": "grant decisions", - "disclosure.served": "data disclosures", + messages: "your messages", + orders: "your orders", + pay_statements: "your pay", + paystubs: "your pay", + purchases: "your purchases", "query.received": "read requests", "query.rejected": "rejected reads", + statements: "your statements", + tax_docs: "your tax documents", + tax_documents: "your tax documents", "token.issued": "token activity", + transactions: "your spending", + watch_history: "what you watch", }; const READ_SUFFIX_RE = /\.read$/; @@ -100,15 +100,15 @@ export function joinHuman(parts: readonly string[]): string { } const STREAM_RECORD_NOUN: Record = { - pay_statements: "pay records", + current_activity: "transactions", + emails: "emails", employment: "employment records", listening_history: "listening records", - tax_docs: "tax records", - transactions: "transactions", - current_activity: "transactions", messages: "messages", - emails: "emails", orders: "orders", + pay_statements: "pay records", + tax_docs: "tax records", + transactions: "transactions", }; /** "transactions" / "pay records" — the plural noun for a stream of records. */ @@ -528,13 +528,13 @@ function toBearers(clients: OwnerIssuedClient[], hrefs: StandingHrefs): BearerVi const tokenWord = count === 1 ? "token" : "tokens"; return { clientId: c.client_id, - who: clientLabel(c.client_name, c.client_id), how: `owner token · ${count} active ${tokenWord}`, + issuedAt: c.created_at, // A client with more than one active token registered before the newest // token was issued, so `created_at` is a "first issued", not "issued". issuedLabel: count > 1 ? "first issued" : "issued", - issuedAt: c.created_at, revokeHref: hrefs.deploymentTokens, + who: clientLabel(c.client_name, c.client_id), }; }); } @@ -741,18 +741,18 @@ function toLately(traces: TraceSummary[], now: Date): LatelyView[] { let row: LatelyView; if (isDeny) { row = { + deny: true, id: tr.trace_id, + text: { rest: `tried to read — turned away, ${denyReason(tr.failure?.reason ?? null)}.`, who }, when: relDay(tr.last_at, now), - deny: true, - text: { who, rest: `tried to read — turned away, ${denyReason(tr.failure?.reason ?? null)}.` }, }; } else { const noun = tr.event_count === 1 ? "record" : "records"; row = { + deny: false, id: tr.trace_id, + text: { rest: `read ${fmtInt(tr.event_count)} ${noun}.`, who }, when: relDay(tr.last_at, now), - deny: false, - text: { who, rest: `read ${fmtInt(tr.event_count)} ${noun}.` }, }; } const key = `${row.deny ? "deny" : "read"}|${row.when}|${row.text.who}|${row.text.rest}`; @@ -829,19 +829,19 @@ export function advisoryOwnerActionsFromConnectors( function toAttention(attention: AttentionConnection[], hrefs: StandingHrefs): AttentionRowView[] { return attention.map((a) => ({ + href: hrefs.connection(a.routeId), id: `connection:${a.routeId}`, what: `${a.label} needs you`, why: a.what, - href: hrefs.connection(a.routeId), })); } function toSourceIssues(sourceIssues: SourceIssueConnection[], hrefs: StandingHrefs): AttentionRowView[] { return sourceIssues.map((issue) => ({ + href: hrefs.connection(issue.routeId), id: `source-issue:${issue.routeId}`, what: `${issue.label} ${issue.status}`, why: issue.what, - href: hrefs.connection(issue.routeId), })); } @@ -850,10 +850,10 @@ function toAdvisoryOwnerActions( hrefs: StandingHrefs ): AttentionRowView[] { return advisoryOwnerActions.map((action) => ({ + href: hrefs.connection(action.routeId), id: `advisory-owner-action:${action.routeId}`, what: `${action.label} has an action to review`, why: action.what, - href: hrefs.connection(action.routeId), })); } @@ -864,13 +864,13 @@ function toOverviewIssues(loadIssues: readonly string[], hrefs: StandingHrefs): const count = loadIssues.length; return [ { + href: hrefs.deployment, id: "overview:partial-data", what: "Overview could not check everything", why: count === 1 ? "One dashboard check did not load. Refresh this page; if it keeps happening, check deployment." : `${count} dashboard checks did not load. Refresh this page; if it keeps happening, check deployment.`, - href: hrefs.deployment, }, ]; } @@ -958,10 +958,10 @@ function sourceWorkRow(item: SourceWorkItem, hrefs: StandingHrefs): AttentionRow what = `${item.label} ${item.statusLabel}`; } return { + href: hrefs.connection(item.routeId), id: item.id, what, why: item.group === "working" || item.group === "notMeasured" ? item.statusLabel : item.what, - href: hrefs.connection(item.routeId), }; } @@ -977,52 +977,52 @@ function toSourceWorkSections( const sections: SourceWorkSectionView[] = []; if (groups.needsOwner.length > 0) { sections.push({ + countLabel: pluralSource(groups.needsOwner.length), id: "needsOwner", - title: SOURCE_WORK_GROUP_COPY.needsOwner.label, note: SOURCE_WORK_GROUP_COPY.needsOwner.note, - countLabel: pluralSource(groups.needsOwner.length), - tone: "owner", rows: groups.needsOwner.map((item) => sourceWorkRow(item, hrefs)), + title: SOURCE_WORK_GROUP_COPY.needsOwner.label, + tone: "owner", }); } if (groups.review.length > 0) { sections.push({ + countLabel: pluralSource(groups.review.length), id: "review", - title: SOURCE_WORK_GROUP_COPY.review.label, note: SOURCE_WORK_GROUP_COPY.review.note, - countLabel: pluralSource(groups.review.length), - tone: "review", rows: groups.review.map((item) => sourceWorkRow(item, hrefs)), + title: SOURCE_WORK_GROUP_COPY.review.label, + tone: "review", }); } if (groups.systemIssues.length > 0 || overviewIssues.length > 0) { sections.push({ + countLabel: pluralSource(groups.systemIssues.length + overviewIssues.length), id: "systemIssue", - title: SOURCE_WORK_GROUP_COPY.systemIssue.label, note: SOURCE_WORK_GROUP_COPY.systemIssue.note, - countLabel: pluralSource(groups.systemIssues.length + overviewIssues.length), - tone: "system", rows: [...groups.systemIssues.map((item) => sourceWorkRow(item, hrefs)), ...overviewIssues], + title: SOURCE_WORK_GROUP_COPY.systemIssue.label, + tone: "system", }); } if (groups.working.length > 0) { sections.push({ + countLabel: pluralSource(groups.working.length), id: "working", - title: SOURCE_WORK_GROUP_COPY.working.label, note: SOURCE_WORK_GROUP_COPY.working.note, - countLabel: pluralSource(groups.working.length), - tone: "muted", rows: groups.working.map((item) => sourceWorkRow(item, hrefs)), + title: SOURCE_WORK_GROUP_COPY.working.label, + tone: "muted", }); } if (groups.notMeasured.length > 0) { sections.push({ + countLabel: pluralSource(groups.notMeasured.length), id: "notMeasured", - title: SOURCE_WORK_GROUP_COPY.notMeasured.label, note: SOURCE_WORK_GROUP_COPY.notMeasured.note, - countLabel: pluralSource(groups.notMeasured.length), - tone: "muted", rows: groups.notMeasured.map((item) => sourceWorkRow(item, hrefs)), + title: SOURCE_WORK_GROUP_COPY.notMeasured.label, + tone: "muted", }); } return sections; @@ -1032,17 +1032,17 @@ function toSourceWorkSections( /** DECIDE — a request is waiting on the owner. */ function buildDecideHero(pending: PendingApproval[], hrefs: StandingHrefs): StandingHero { - const first = pending[0]; + const [first] = pending; const more = pending.length - 1; const who = first ? clientLabel(first.client_id ?? null, first.approval_id) : "An app"; const reads = first ? approvalReads(first) : "parts of your data"; const moreSub = `Nothing leaves until you say so — review each request one at a time. ${more} more after this one.`; return { - tone: "decide", + cta: { href: hrefs.grants, human: true, label: "Review the request" }, kicker: pending.length === 1 ? "A request is waiting on you" : `${pending.length} requests are waiting`, - line: { text: `${who} wants to read `, emphasis: reads, tail: "." }, + line: { emphasis: reads, tail: ".", text: `${who} wants to read ` }, sub: more > 0 ? moreSub : "Nothing leaves until you say so — approve it one piece at a time.", - cta: { label: "Review the request", href: hrefs.grants, human: true }, + tone: "decide", }; } @@ -1055,27 +1055,27 @@ function buildFailureHero(attention: AttentionConnection[], hrefs: StandingHrefs const [only] = attention; if (count === 1 && only) { return { - tone: "alarm", - kicker: "One thing needs you", - line: { text: `${only.label} `, emphasis: "needs you", tail: "." }, - sub: only.what, // A device-local recovery is not performed by clicking — the CTA only // navigates to where the commands are. Use a navigation label ("See what // to do") so the owner doesn't click expecting the dashboard to run it. // A dashboard-actionable verdict (reauth, refresh) keeps its action verb. cta: { - label: only.deviceLocal ? "See what to do" : only.actionLabel, href: hrefs.connection(only.routeId), human: true, + label: only.deviceLocal ? "See what to do" : only.actionLabel, }, + kicker: "One thing needs you", + line: { emphasis: "needs you", tail: ".", text: `${only.label} ` }, + sub: only.what, + tone: "alarm", }; } return { - tone: "alarm", + cta: { href: hrefs.runs, label: "See what needs you" }, kicker: `${count} things need you`, - line: { text: `${count} connections `, emphasis: "need a look", tail: "." }, + line: { emphasis: "need a look", tail: ".", text: `${count} connections ` }, sub: "Nothing you already have is lost — open each one to see what it needs.", - cta: { label: "See what needs you", href: hrefs.runs }, + tone: "alarm", }; } @@ -1084,32 +1084,32 @@ function buildStaleHero(summary: DatasetSummary | null, hrefs: StandingHrefs): S const projectionState = summary?.projection?.state; const isFailed = projectionState === "failed"; return { - tone: "alarm", + cta: { href: hrefs.deployment, label: "View status" }, kicker: isFailed ? "Totals update delayed" : "Totals updating", line: { - text: "Records ", emphasis: "are still available", tail: ".", + text: "Records ", }, sub: isFailed ? "Dashboard totals are using the last completed update." : "Dashboard totals may use the last completed update for a few minutes.", - cta: { label: "View status", href: hrefs.deployment }, + tone: "alarm", }; } function buildPartialDataHero(loadIssues: readonly string[], hrefs: StandingHrefs): StandingHero { const count = loadIssues.length; return { - tone: "alarm", + cta: { href: hrefs.deployment, label: "Check deployment" }, kicker: "Overview is incomplete", line: { - text: count === 1 ? "One dashboard check " : `${count} dashboard checks `, emphasis: "did not load", tail: ".", + text: count === 1 ? "One dashboard check " : `${count} dashboard checks `, }, sub: "Refresh this page; if it keeps happening, check deployment. This page will not claim all-clear from partial data.", - cta: { label: "Check deployment", href: hrefs.deployment }, + tone: "alarm", }; } @@ -1118,21 +1118,22 @@ function buildAdvisoryHero(actions: AdvisoryOwnerActionConnection[], hrefs: Stan if (actions.length === 1 && only) { // Lead with the CONCRETE action the owner can run ("Refresh now" / "Retry // now"), not the "ready for review" taxonomy phrasing. + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic const action = only.actionLabel ?? "Run the available action"; return { - tone: "decide", + cta: { href: hrefs.connection(only.routeId), human: true, label: action }, kicker: "One optional action is available", - line: { text: `${only.label}: `, emphasis: action, tail: "." }, + line: { emphasis: action, tail: ".", text: `${only.label}: ` }, sub: only.what, - cta: { label: action, href: hrefs.connection(only.routeId), human: true }, + tone: "decide", }; } return { - tone: "decide", + cta: { href: hrefs.sources, label: "See available actions" }, kicker: `${actions.length} optional actions are available`, - line: { text: "Refreshes and retries ", emphasis: "are available", tail: "." }, + line: { emphasis: "are available", tail: ".", text: "Refreshes and retries " }, sub: "Nothing is broken — run these refreshes and retries when you like. Records remain available.", - cta: { label: "See available actions", href: hrefs.sources }, + tone: "decide", }; } @@ -1143,6 +1144,7 @@ function buildCalmHero(input: StandingInputs): StandingHero { const activeTokenCount = activeOwnerTokenCount(activeClients); const liveGrants = input.grants.filter(isLiveGrant); const records = summary ? fmtInt(summary.record_count) : "0"; + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic const sources = summary?.connector_count ?? 0; const sourceWord = sources === 1 ? "source" : "sources"; const grantWord = liveGrants.length === 1 ? "app reads" : "apps read"; @@ -1151,10 +1153,10 @@ function buildCalmHero(input: StandingInputs): StandingHero { const withBearers = `${activeClients.length} ${clientWord} ${fmtInt(activeTokenCount)} active owner ${tokenWord} with full access. ${liveGrants.length} ${grantWord} only the slices you granted. Revoke any of them instantly.`; const noBearers = `No owner token can act as you yet. ${liveGrants.length} ${grantWord} only the slices you granted. Revoke any of them instantly.`; return { - tone: "calm", kicker: "Where you stand", - line: { text: `${records} records from ${sources} ${sourceWord} — `, emphasis: "all yours to read", tail: "." }, + line: { emphasis: "all yours to read", tail: ".", text: `${records} records from ${sources} ${sourceWord} — ` }, sub: activeTokenCount > 0 ? withBearers : noBearers, + tone: "calm", }; } @@ -1178,9 +1180,9 @@ export function computeHero(input: StandingInputs): StandingHero { sourceWork.needsOwner.map((item) => ({ actionLabel: item.actionLabel ?? "Review source", connectorKey: "", - routeId: item.routeId, deviceLocal: item.deviceLocal, label: item.label, + routeId: item.routeId, what: item.what, })), input.hrefs @@ -1215,15 +1217,15 @@ export function buildStandingData(input: StandingInputs): StandingData { const allBearers = toBearers(input.bearerClients, input.hrefs); const bearers = allBearers.slice(0, BEARER_PREVIEW_LIMIT); return { - hero: computeHero(input), + advisoryOwnerActions: toAdvisoryOwnerActions(input.advisoryOwnerActions, input.hrefs), + attention: toAttention(input.attentionConnections, input.hrefs), bearers, bearersOverflow: allBearers.length - bearers.length, grantPackages: toGrantPackages(input.grants, input.hrefs, input.grantPackageCount), - relationships: toRelationships(input.grants, input.hrefs, input.now, clientNamesById(input.bearerClients)), + hero: computeHero(input), lately: toLately(input.traces, input.now), - advisoryOwnerActions: toAdvisoryOwnerActions(input.advisoryOwnerActions, input.hrefs), - attention: toAttention(input.attentionConnections, input.hrefs), overviewIssues, + relationships: toRelationships(input.grants, input.hrefs, input.now, clientNamesById(input.bearerClients)), sourceIssues: toSourceIssues(input.sourceIssues, input.hrefs), sourceWorkSections: toSourceWorkSections(sourceWork, overviewIssues, input.hrefs), }; diff --git a/apps/console/src/app/(console)/components/warnings-banner.tsx b/apps/console/src/app/(console)/components/warnings-banner.tsx index 713b9790a..8322bda2c 100644 --- a/apps/console/src/app/(console)/components/warnings-banner.tsx +++ b/apps/console/src/app/(console)/components/warnings-banner.tsx @@ -18,6 +18,7 @@ function warningKey(warning: CanonicalReadWarning): string { * `deprecated_alias_used` plus a `count_downgraded`). */ export function WarningsBanner({ warnings }: { warnings: CanonicalReadWarning[] }) { + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic if (!warnings || warnings.length === 0) { return null; } diff --git a/apps/console/src/app/(console)/components/web-push-settings.tsx b/apps/console/src/app/(console)/components/web-push-settings.tsx index e70a77577..3da10d2c3 100644 --- a/apps/console/src/app/(console)/components/web-push-settings.tsx +++ b/apps/console/src/app/(console)/components/web-push-settings.tsx @@ -40,11 +40,11 @@ type DeviceAction = function deviceReducer(state: DeviceState, action: DeviceAction): DeviceState { switch (action.type) { case "supportDetected": - return { ...state, unavailable: action.unavailable, permission: action.permission, status: action.status }; + return { ...state, permission: action.permission, status: action.status, unavailable: action.unavailable }; case "swState": return action.status === undefined ? { ...state, swState: action.swState } - : { ...state, swState: action.swState, status: action.status }; + : { ...state, status: action.status, swState: action.swState }; case "subscribed": return { ...state, endpoint: action.endpoint, status: action.status }; case "disabled": @@ -177,91 +177,91 @@ function setupStepToneClass(state: DiagnosticState) { function secureContextRow(): DiagnosticRow { if (typeof window === "undefined") { - return { label: "Secure context (HTTPS/localhost)", state: "unknown", detail: "Server-rendered" }; + return { detail: "Server-rendered", label: "Secure context (HTTPS/localhost)", state: "unknown" }; } if (window.isSecureContext) { - return { label: "Secure context (HTTPS/localhost)", state: "ok", detail: window.location.origin }; + return { detail: window.location.origin, label: "Secure context (HTTPS/localhost)", state: "ok" }; } return { + detail: "Page is not served over a secure origin.", label: "Secure context (HTTPS/localhost)", state: "fail", - detail: "Page is not served over a secure origin.", }; } function featureRow(label: string, present: boolean, presentDetail: string, missingDetail: string): DiagnosticRow { - return present ? { label, state: "ok", detail: presentDetail } : { label, state: "fail", detail: missingDetail }; + return present ? { detail: presentDetail, label, state: "ok" } : { detail: missingDetail, label, state: "fail" }; } function vapidRow(config: WebPushConfig): DiagnosticRow { if (config.enabled && config.public_key) { return { + detail: "/_ref/web-push/config reports enabled", label: "Server VAPID keys configured", state: "ok", - detail: "/_ref/web-push/config reports enabled", }; } return { + detail: config.unavailable_reason || "Server VAPID keys are not configured.", label: "Server VAPID keys configured", state: "fail", - detail: config.unavailable_reason || "Server VAPID keys are not configured.", }; } function swRow(swState: "registered" | "absent" | "unknown" | "unsupported"): DiagnosticRow { const label = "Service worker registered"; if (swState === "registered") { - return { label, state: "ok", detail: "The PDPP service worker controls /." }; + return { detail: "The PDPP service worker controls /.", label, state: "ok" }; } if (swState === "absent") { - return { label, state: "warn", detail: "Not registered yet - use Enable this device." }; + return { detail: "Not registered yet - use Enable this device.", label, state: "warn" }; } if (swState === "unsupported") { - return { label, state: "fail", detail: "Browser lacks serviceWorker." }; + return { detail: "Browser lacks serviceWorker.", label, state: "fail" }; } - return { label, state: "unknown", detail: "Could not inspect registration." }; + return { detail: "Could not inspect registration.", label, state: "unknown" }; } function permissionRow(permission: NotificationPermission | "unknown"): DiagnosticRow { const label = "Notification permission granted"; if (permission === "granted") { - return { label, state: "ok", detail: 'Notification.permission === "granted"' }; + return { detail: 'Notification.permission === "granted"', label, state: "ok" }; } if (permission === "denied") { return { + detail: 'Notification.permission === "denied" - change browser/OS notification settings to opt in.', label, state: "fail", - detail: 'Notification.permission === "denied" - change browser/OS notification settings to opt in.', }; } if (permission === "default") { return { + detail: "Permission has not been requested on this device - use Enable this device.", label, state: "warn", - detail: "Permission has not been requested on this device - use Enable this device.", }; } - return { label, state: "unknown", detail: "Notification API not available." }; + return { detail: "Notification API not available.", label, state: "unknown" }; } function browserSubscriptionRow(endpoint: string | null, matchesThisBrowser: boolean): DiagnosticRow { const label = "Browser push subscription active"; if (!endpoint) { return { - label, - state: "warn", detail: "No active subscription on this device - use Enable this device to create one (installing the PWA alone does not subscribe).", + label, + state: "warn", }; } if (matchesThisBrowser) { - return { label, state: "ok", detail: "Browser endpoint is registered for this owner on the server." }; + return { detail: "Browser endpoint is registered for this owner on the server.", label, state: "ok" }; } return { - label, - state: "warn", detail: "Browser has a subscription but the server does not list it for this owner - use Enable this device to re-register.", + label, + state: "warn", }; } @@ -269,15 +269,15 @@ function deliveryHealthRow(lastSubscription: WebPushSubscriptionSummary | undefi const label = "Last delivery health"; if (lastSubscription?.last_failure_reason) { return { + detail: `Most recent failure: ${lastSubscription.last_failure_reason} (${lastSubscription.last_failure_at ?? "unknown time"}). Use Enable this device on the affected device to re-subscribe.`, label, state: "warn", - detail: `Most recent failure: ${lastSubscription.last_failure_reason} (${lastSubscription.last_failure_at ?? "unknown time"}). Use Enable this device on the affected device to re-subscribe.`, }; } if (lastSubscription?.last_success_at) { - return { label, state: "ok", detail: `Last success: ${lastSubscription.last_success_at}.` }; + return { detail: `Last success: ${lastSubscription.last_success_at}.`, label, state: "ok" }; } - return { label, state: "unknown", detail: "No delivery attempt recorded yet." }; + return { detail: "No delivery attempt recorded yet.", label, state: "unknown" }; } function deviceStatus({ @@ -295,43 +295,43 @@ function deviceStatus({ }): DeviceStatus { if (unavailable) { return { - title: "This browser cannot receive PDPP notifications.", detail: unavailable, + title: "This browser cannot receive PDPP notifications.", }; } if (permission === "unknown" && swState === "unknown") { return { - title: "Checking this device…", detail: "Inspecting browser permission and subscription state.", + title: "Checking this device…", }; } if (permission === "denied") { return { - title: "Notifications are blocked for this browser.", detail: "Change browser or OS notification settings, then return here and enable this device.", + title: "Notifications are blocked for this browser.", }; } if (matchesThisBrowser) { return { - title: "This device is subscribed.", detail: "Pending connector interactions can send browser notifications to this browser or installed app.", + title: "This device is subscribed.", }; } if (endpoint) { return { - title: "This browser has a local subscription, but the server does not recognize it.", detail: "Enable this device again to repair the server-side subscription.", + title: "This browser has a local subscription, but the server does not recognize it.", }; } if (permission === "granted") { return { - title: "Notifications are allowed, but this device is not subscribed.", detail: "Enable this device once so PDPP can send alerts here.", + title: "Notifications are allowed, but this device is not subscribed.", }; } return { - title: "This device is not subscribed yet.", detail: "Installing the PWA only adds the app icon. You still need to enable notifications from this device.", + title: "This device is not subscribed yet.", }; } @@ -352,26 +352,26 @@ function buildSetupSteps({ return [ { + detail: "This page configures notifications only for the browser or installed app you are using right now.", label: "Open the right device", state: "ok", - detail: "This page configures notifications only for the browser or installed app you are using right now.", }, { + detail: setupPermissionDetail(permission), label: "Allow notifications", state: setupPermissionState(permission, unavailable), - detail: setupPermissionDetail(permission), }, { + detail: setupSubscriptionDetail({ endpoint, matchesThisBrowser }), label: "Subscribe this device", state: setupSubscriptionState({ matchesThisBrowser, unavailable }), - detail: setupSubscriptionDetail({ endpoint, matchesThisBrowser }), }, { - label: "Send a test", - state: setupTestState({ matchesThisBrowser, testNotificationAccepted }), detail: testNotificationAccepted ? "The push provider accepted the test. Only the device can confirm whether it displayed." : "Use Send test notification after this device is subscribed.", + label: "Send a test", + state: setupTestState({ matchesThisBrowser, testNotificationAccepted }), }, ]; } @@ -493,12 +493,12 @@ function buildDiagnostics({ permissionRow(permission), browserSubscriptionRow(endpoint, matchesThisBrowser), { - label: "Server-tracked subscriptions for this owner", - state: subscriptions.length > 0 ? "ok" : "warn", detail: subscriptions.length > 0 ? `${subscriptions.length} saved across all of this owner's devices.` : "Server has no saved subscriptions yet for this owner.", + label: "Server-tracked subscriptions for this owner", + state: subscriptions.length > 0 ? "ok" : "warn", }, deliveryHealthRow(lastSubscription), ]; @@ -564,27 +564,33 @@ export function WebPushSettings({ async function refreshSubscriptionState() { if (!hasNavigatorFeature("serviceWorker")) { - dispatch({ type: "swState", swState: "unsupported" }); + dispatch({ swState: "unsupported", type: "swState" }); return; } try { const registration = await navigator.serviceWorker.getRegistration("/"); - await registration?.update().catch(() => undefined); - dispatch({ type: "swState", swState: registration ? "registered" : "absent" }); + if (registration) { + try { + await registration.update(); + } catch { + // A refresh failure is non-fatal; subscription state remains usable. + } + } + dispatch({ swState: registration ? "registered" : "absent", type: "swState" }); const existing = await registration?.pushManager.getSubscription(); - dispatch({ type: "endpoint", endpoint: existing?.endpoint ?? null }); + dispatch({ endpoint: existing?.endpoint ?? null, type: "endpoint" }); } catch { - dispatch({ type: "swState", swState: "unknown" }); + dispatch({ swState: "unknown", type: "swState" }); } } useEffect(() => { const reason = detectSupport(config); dispatch({ - type: "supportDetected", - unavailable: reason, permission: "Notification" in window ? Notification.permission : "unknown", status: reason ? "Unavailable in this browser" : `Permission: ${Notification.permission}`, + type: "supportDetected", + unavailable: reason, }); if (reason) { return; @@ -596,26 +602,32 @@ export function WebPushSettings({ if (cancelled) { return; } - dispatch({ type: "swState", swState: registration ? "registered" : "absent" }); - await registration?.update().catch(() => undefined); + dispatch({ swState: registration ? "registered" : "absent", type: "swState" }); + if (registration) { + try { + await registration.update(); + } catch { + // A refresh failure is non-fatal; subscription state remains usable. + } + } const existing = await registration?.pushManager.getSubscription(); if (cancelled) { return; } if (existing) { dispatch({ - type: "subscribed", endpoint: existing.endpoint, status: "Web Push is enabled for this browser.", + type: "subscribed", }); } }) .catch(() => { if (!cancelled) { dispatch({ - type: "swState", - swState: "unknown", status: "Could not inspect this browser's Web Push subscription.", + swState: "unknown", + type: "swState", }); } }); @@ -632,45 +644,45 @@ export function WebPushSettings({ setTestStatus(null); try { const registration = await navigator.serviceWorker.register("/pdpp-dashboard-sw.js"); - dispatch({ type: "swState", swState: "registered" }); + dispatch({ swState: "registered", type: "swState" }); const result = await Notification.requestPermission(); - dispatch({ type: "permission", permission: result }); + dispatch({ permission: result, type: "permission" }); if (result !== "granted") { dispatch({ - type: "status", status: result === "denied" ? "Permission denied. Enable notifications in browser settings." : "Permission was not granted.", + type: "status", }); return; } const subscription = (await registration.pushManager.getSubscription()) ?? (await registration.pushManager.subscribe({ - userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(config.public_key), + userVisibleOnly: true, })); const response = await fetch("/_ref/web-push/subscriptions", { - method: "POST", - credentials: "same-origin", - headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify({ - subscription: subscription.toJSON(), - platform: navigator.platform || null, device_label: "PDPP browser", + platform: navigator.platform || null, + subscription: subscription.toJSON(), }), + credentials: "same-origin", + headers: { Accept: "application/json", "Content-Type": "application/json" }, + method: "POST", }); if (!response.ok) { throw await webPushResponseError(response, "Subscription failed"); } dispatch({ - type: "subscribed", endpoint: subscription.endpoint, status: "Web Push is enabled for this browser.", + type: "subscribed", }); } catch (err) { - dispatch({ type: "status", status: err instanceof Error ? err.message : "Failed to enable Web Push." }); + dispatch({ status: err instanceof Error ? err.message : "Failed to enable Web Push.", type: "status" }); } finally { setBusy(false); } @@ -681,9 +693,9 @@ export function WebPushSettings({ setTestStatus("Sending test notification…"); try { const response = await fetch("/_ref/web-push/test", { - method: "POST", credentials: "same-origin", headers: { Accept: "application/json" }, + method: "POST", }); if (!response.ok) { throw await webPushResponseError(response, "Test notification failed"); @@ -721,18 +733,18 @@ export function WebPushSettings({ } if (targetEndpoint) { const response = await fetch("/_ref/web-push/subscriptions", { - method: "DELETE", - credentials: "same-origin", - headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify({ endpoint: targetEndpoint }), + credentials: "same-origin", + headers: { Accept: "application/json", "Content-Type": "application/json" }, + method: "DELETE", }); if (!response.ok) { throw await webPushResponseError(response, "Unsubscribe failed"); } } - dispatch({ type: "disabled", status: "Web Push is disabled for this browser." }); + dispatch({ status: "Web Push is disabled for this browser.", type: "disabled" }); } catch (err) { - dispatch({ type: "status", status: err instanceof Error ? err.message : "Failed to disable Web Push." }); + dispatch({ status: err instanceof Error ? err.message : "Failed to disable Web Push.", type: "status" }); } finally { setBusy(false); } @@ -744,19 +756,19 @@ export function WebPushSettings({ const caveat = "Mobile browsers may require opening the installed PDPP app before notifications can arrive. Each phone, tablet, and browser profile must be enabled separately."; - const lastSubscription = subscriptions[0]; + const [lastSubscription] = subscriptions; const matchesThisBrowser = endpoint ? subscriptions.some((s) => s.endpoint === endpoint && !s.revoked_at) : false; - const currentDeviceStatus = deviceStatus({ unavailable, permission, endpoint, matchesThisBrowser, swState }); - const setupSteps = buildSetupSteps({ unavailable, permission, endpoint, matchesThisBrowser, testStatus }); + const currentDeviceStatus = deviceStatus({ endpoint, matchesThisBrowser, permission, swState, unavailable }); + const setupSteps = buildSetupSteps({ endpoint, matchesThisBrowser, permission, testStatus, unavailable }); const diagnostics = buildDiagnostics({ config, - swState, - permission, endpoint, + lastSubscription, matchesThisBrowser, + permission, subscriptions, - lastSubscription, + swState, }); // Whether this browser is set up and healthy, so the demoted summary line can @@ -791,8 +803,9 @@ export function WebPushSettings({
{enabled || unavailable ? null : ( - + Open Syncs - + Back to Sources
diff --git a/apps/console/src/app/(console)/connect/browser-session/[connectorId]/launch/page.tsx b/apps/console/src/app/(console)/connect/browser-session/[connectorId]/launch/page.tsx index dd3f7f0aa..f93d947dc 100644 --- a/apps/console/src/app/(console)/connect/browser-session/[connectorId]/launch/page.tsx +++ b/apps/console/src/app/(console)/connect/browser-session/[connectorId]/launch/page.tsx @@ -48,7 +48,7 @@ export default async function BrowserSessionLaunchPage({ + Back to sources } diff --git a/apps/console/src/app/(console)/connect/browser-session/[connectorId]/launch/recovery-classification.ts b/apps/console/src/app/(console)/connect/browser-session/[connectorId]/launch/recovery-classification.ts index 55afb6651..0c6cc098c 100644 --- a/apps/console/src/app/(console)/connect/browser-session/[connectorId]/launch/recovery-classification.ts +++ b/apps/console/src/app/(console)/connect/browser-session/[connectorId]/launch/recovery-classification.ts @@ -3,7 +3,12 @@ import type { RunSummary } from "../../../../lib/ref-client.ts"; -const RECOVERABLE_BROWSER_RUN_STATUSES = new Set(["started", "in_progress", "starting_surface", "waiting_for_browser_surface"]); +const RECOVERABLE_BROWSER_RUN_STATUSES = new Set([ + "started", + "in_progress", + "starting_surface", + "waiting_for_browser_surface", +]); export function isRecoverableBrowserSessionRun(run: Pick): boolean { return RECOVERABLE_BROWSER_RUN_STATUSES.has(run.status); diff --git a/apps/console/src/app/(console)/connect/browser-session/[connectorId]/page.tsx b/apps/console/src/app/(console)/connect/browser-session/[connectorId]/page.tsx index bf8f43737..07829fa38 100644 --- a/apps/console/src/app/(console)/connect/browser-session/[connectorId]/page.tsx +++ b/apps/console/src/app/(console)/connect/browser-session/[connectorId]/page.tsx @@ -65,10 +65,10 @@ function UnavailableSetupCard({ displayName }: { displayName: string }) { existing source to reconnect it, or return to Add source to see what this dashboard can add now.

- + Open sources - + Add source
@@ -119,7 +119,7 @@ export default async function BrowserSessionConnectPage({
{selected ? "Selected" : "Use"} @@ -192,27 +192,27 @@ function SelectedIdentityCommands({ const cimd = buildCimdCommands(mcpUrl, selected.client_id); const entries: SetupEntry[] = [ { - title: "Claude Code", body: "Default discovery; use this unless the client asks for an explicit client_id.", label: "Claude Code default command", + title: "Claude Code", value: targets.claudeCodeCommand, }, { - title: "Claude Code + CIMD", body: "Pins this local client to the selected stable metadata URL.", label: "Claude Code CIMD command", + title: "Claude Code + CIMD", value: cimd.claudeCodeCimdCommand, }, { - title: "Codex", body: "Default discovery for the hosted MCP endpoint.", label: "Codex default command", + title: "Codex", value: targets.codexCommand, }, { - title: "Codex + CIMD", body: "Pins Codex to the selected stable metadata URL.", label: "Codex CIMD command", + title: "Codex + CIMD", value: cimd.codexCimdCommand, }, ]; @@ -277,35 +277,35 @@ export default async function ConnectPage({ searchParams }: { searchParams: Prom const notice = noticeText(params.notice); const primaryEntries: SetupEntry[] = [ { - title: "MCP URL", body: "Use this for ChatGPT, Claude.ai, and remote MCP clients. Browser clients use PKCE; sandboxed clients can use the advertised device-code flow.", label: "MCP server URL", + title: "MCP URL", value: targets.mcpUrl, }, { - title: "Claude Code", body: "Adds the remote Streamable HTTP MCP server.", label: "Claude Code command", + title: "Claude Code", value: targets.claudeCodeCommand, }, { - title: "Codex", body: "Adds the same remote MCP endpoint without a bearer-token env var.", label: "Codex command", + title: "Codex", value: targets.codexCommand, }, ]; const secondaryEntries: SetupEntry[] = [ { - title: "PDPP CLI", body: "For a shell agent that will use scoped REST reads instead of hosted MCP.", label: "PDPP CLI connect command", + title: "PDPP CLI", value: targets.pdppCliCommand, }, { - title: "Agent skill", body: "For agents that discover instructions before choosing MCP or CLI.", label: "agent-readable entrypoint URL", + title: "Agent skill", value: targets.agentEntrypoint, }, ]; @@ -314,7 +314,7 @@ export default async function ConnectPage({ searchParams }: { searchParams: Prom + Deployment readiness } diff --git a/apps/console/src/app/(console)/connect/static-secret-payload.test.ts b/apps/console/src/app/(console)/connect/static-secret-payload.test.ts index 6150b656b..f8fddd0ed 100644 --- a/apps/console/src/app/(console)/connect/static-secret-payload.test.ts +++ b/apps/console/src/app/(console)/connect/static-secret-payload.test.ts @@ -249,5 +249,5 @@ test("required bundled fields fail before capture instead of storing incomplete form ); - assert.deepEqual(payload, { ok: false, error: "Password is required." }); + assert.deepEqual(payload, { error: "Password is required.", ok: false }); }); diff --git a/apps/console/src/app/(console)/connect/static-secret/[connectorId]/actions.ts b/apps/console/src/app/(console)/connect/static-secret/[connectorId]/actions.ts index 86c131a1b..12bc434f5 100644 --- a/apps/console/src/app/(console)/connect/static-secret/[connectorId]/actions.ts +++ b/apps/console/src/app/(console)/connect/static-secret/[connectorId]/actions.ts @@ -74,6 +74,7 @@ function autoResumeRunId(capture: StaticSecretCaptureResult): string | null { function shouldStartRunAfterCapture(capture: StaticSecretCaptureResult): boolean { const autoResume = capture.auto_resume; + // biome-ignore lint/suspicious/noEqualsToNull: The API may omit auto_resume as well as return null. return autoResume == null || autoResume.status === "no_satisfied_action"; } @@ -133,6 +134,7 @@ export async function replaceStaticSecretCredentialAction(formData: FormData) { }); const runId = await runIdAfterCapture(connectionId, captured); revalidatePath("/sources"); + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic target = statusHref(connectionId, runId, captured.identity?.account_identity ?? null); } catch (err) { if (err instanceof StaticSecretValidationError) { @@ -200,6 +202,7 @@ export async function createStaticSecretConnectionAction(formData: FormData) { // Land on the durable setup-status surface, not a transient form notice. The // status page reads the connection's projected setup_state and, for a // synchronous-probe connector, surfaces the echoed account identity. + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic target = statusHref(draft.connection_id, runId, captured.identity?.account_identity ?? null); } catch (err) { if (err instanceof StaticSecretValidationError) { diff --git a/apps/console/src/app/(console)/connect/static-secret/[connectorId]/page.tsx b/apps/console/src/app/(console)/connect/static-secret/[connectorId]/page.tsx index 30b9a6547..e92f7fdd9 100644 --- a/apps/console/src/app/(console)/connect/static-secret/[connectorId]/page.tsx +++ b/apps/console/src/app/(console)/connect/static-secret/[connectorId]/page.tsx @@ -55,8 +55,8 @@ export default async function StaticSecretConnectPage({ }); const resolvedSearchParams = await searchParams; const pageParams: PageSearchParams = { - error: firstValue(resolvedSearchParams.error), connectionId: firstValue(resolvedSearchParams.connection_id), + error: firstValue(resolvedSearchParams.error), }; // Repair/update mode: a connection_id in the query means the owner is // replacing the credential on an existing connection, not creating a new one. @@ -86,7 +86,7 @@ export default async function StaticSecretConnectPage({ + {backLabel} } diff --git a/apps/console/src/app/(console)/connect/static-secret/[connectorId]/static-secret-payload.ts b/apps/console/src/app/(console)/connect/static-secret/[connectorId]/static-secret-payload.ts index e17a9b34a..29dc56126 100644 --- a/apps/console/src/app/(console)/connect/static-secret/[connectorId]/static-secret-payload.ts +++ b/apps/console/src/app/(console)/connect/static-secret/[connectorId]/static-secret-payload.ts @@ -52,7 +52,7 @@ export function buildStaticSecretPayload( ? bundledSecretPayload(setup, formData) : singleSecretPayload(setup, formData); if ("error" in result) { - return { ok: false, error: result.error }; + return { error: result.error, ok: false }; } return { ok: true, secret: result.secret }; } diff --git a/apps/console/src/app/(console)/connect/status/[connectionId]/page.tsx b/apps/console/src/app/(console)/connect/status/[connectionId]/page.tsx index 1bb9330a7..23a7b9ea4 100644 --- a/apps/console/src/app/(console)/connect/status/[connectionId]/page.tsx +++ b/apps/console/src/app/(console)/connect/status/[connectionId]/page.tsx @@ -60,32 +60,32 @@ function describeActiveConnectionState(status: ConnectionSetupStatus): StatusDes if (status.setup_kind === "static_secret" && runStartedAfterCredentialRotation(status)) { if (status.running) { return { - tone: "pending", - headline: "Credential saved", detail: "A sync is running now to verify the updated credential. Existing records remain available while it runs.", + headline: "Credential saved", + tone: "pending", }; } if (statusRunIsFailure(status)) { return { - tone: "failed", - headline: "Credential saved, sync failed", detail: "The updated credential was saved, but the verification sync failed. Re-enter it or open the run timeline for the exact failure.", + headline: "Credential saved, sync failed", + tone: "failed", }; } if (statusRunIsSuccess(status)) { return { - tone: "active", - headline: "Connection active", detail: "The updated credential was verified by a completed sync. Records are available.", + headline: "Connection active", + tone: "active", }; } } return { - tone: "active", - headline: "Connection active", detail: "Records are available for this account.", + headline: "Connection active", + tone: "active", }; } @@ -93,45 +93,46 @@ function describeImportState(status: ConnectionSetupStatus): StatusDescription { switch (status.setup_state) { case "active": return { - tone: "active", - headline: "Import complete", detail: status.import_receipt ? "Your import was validated and committed. Review the durable coverage receipt below." : "Your import was committed. This connector did not provide a validation preview for the setup receipt.", + headline: "Import complete", + tone: "active", }; case "first_sync_running": return { - tone: "pending", - headline: "Import running", detail: "The import file is captured and the import is in progress. This page updates as it finishes.", + headline: "Import running", + tone: "pending", }; case "first_sync_pending": return { - tone: "pending", - headline: "Import starting", detail: "The import file is captured and the import is queued. This page updates as it runs.", + headline: "Import starting", + tone: "pending", }; case "awaiting_credential": return { - tone: "pending", - headline: "File needed", detail: "This source is set up but no import file is captured yet.", + headline: "File needed", + tone: "pending", }; case "first_sync_failed": return { - tone: "failed", - headline: "Import failed", + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic detail: status.last_error?.remediation ?? "Start the import again.", + headline: "Import failed", + tone: "failed", }; case "paused": - return { tone: "pending", headline: "Connection paused", detail: "This connection is paused." }; + return { detail: "This connection is paused.", headline: "Connection paused", tone: "pending" }; case "revoked": - return { tone: "failed", headline: "Connection revoked", detail: "This connection has been revoked." }; + return { detail: "This connection has been revoked.", headline: "Connection revoked", tone: "failed" }; default: return { - tone: "pending", - headline: "Setting up", detail: "This connection is being set up. This page updates as the setup progresses.", + headline: "Setting up", + tone: "pending", }; } } @@ -142,38 +143,39 @@ function describeConnectionState(status: ConnectionSetupStatus): StatusDescripti return describeActiveConnectionState(status); case "first_sync_running": return { - tone: "pending", - headline: "First sync running", detail: "The provider credential is captured and the first sync is in progress. This page updates as it finishes.", + headline: "First sync running", + tone: "pending", }; case "first_sync_pending": return { - tone: "pending", - headline: "First sync starting", detail: "The provider credential is captured and the first sync is queued. This page updates as it runs.", + headline: "First sync starting", + tone: "pending", }; case "awaiting_credential": return { - tone: "pending", - headline: "Setup material needed", detail: "This connection is set up but no provider credential is captured yet.", + headline: "Setup material needed", + tone: "pending", }; case "first_sync_failed": return { - tone: "failed", - headline: "First sync failed", + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic detail: status.last_error?.remediation ?? "Start the first sync again.", + headline: "First sync failed", + tone: "failed", }; case "paused": - return { tone: "pending", headline: "Connection paused", detail: "This connection is paused." }; + return { detail: "This connection is paused.", headline: "Connection paused", tone: "pending" }; case "revoked": - return { tone: "failed", headline: "Connection revoked", detail: "This connection has been revoked." }; + return { detail: "This connection has been revoked.", headline: "Connection revoked", tone: "failed" }; default: return { - tone: "pending", - headline: "Setting up", detail: "This connection is being set up. This page updates as the setup progresses.", + headline: "Setting up", + tone: "pending", }; } } @@ -274,7 +276,7 @@ interface ReceiptRow { function receiptRows(receipt: ImportReceipt): readonly ReceiptRow[] { const baseRows: ReceiptRow[] = [ - { label: "Batch", value: receipt.batch_id, monospace: true }, + { label: "Batch", monospace: true, value: receipt.batch_id }, { label: "File", value: receipt.uploaded_file_name }, { label: "Receipt status", value: receipt.status }, { label: "Detected format", value: receipt.detected_format }, @@ -403,41 +405,41 @@ function importPhaseProgress(status: ConnectionSetupStatus): readonly ImportPhas const facts = importPhaseFacts(status); return [ { - label: "Received", detail: facts.fileReceived ? "PDPP captured the file for this import." : "Choose a file to start.", + label: "Received", state: facts.fileReceived ? "done" : "waiting", }, { - label: "Parsed", detail: facts.parsed ? "The connector parser produced safe validation facts." : "PDPP has not parsed this file yet.", + label: "Parsed", state: parsedPhaseState(facts), }, { - label: "Deduplicated", detail: facts.deduped ? "Duplicate and skipped counts are available." : "Duplicate checks run before records commit.", + label: "Deduplicated", state: dedupedPhaseState(facts), }, { - label: "Committed", detail: facts.committed ? "Accepted records or duplicate-only receipt facts are committed." : "Records are not committed yet.", + label: "Committed", state: committedPhaseState(facts), }, { - label: "Indexed", detail: facts.active ? "Committed records are available on owner surfaces." : "Indexing follows commit.", + label: "Indexed", state: indexedPhaseState(facts), }, { - label: "Health projected", detail: facts.active ? "Connection health and acquisition coverage include this batch." : "Coverage updates after commit.", + label: "Health projected", state: healthProjectedPhaseState(facts), }, ]; @@ -526,7 +528,7 @@ export default async function ConnectionSetupStatusPage({ + Back to Sources } @@ -582,26 +584,26 @@ export default async function ConnectionSetupStatusPage({
{described.tone === "active" ? ( <> - + Open in Explore - + Source details {status.setup_kind === "manual_upload" ? ( - + Import another file ) : null} ) : null} {described.tone === "failed" || status.setup_state === "awaiting_credential" ? ( - + {retryLabel(status)} ) : null} {described.tone === "pending" && status.setup_state !== "awaiting_credential" ? ( - + Refresh status ) : null} diff --git a/apps/console/src/app/(console)/deployment/page.tsx b/apps/console/src/app/(console)/deployment/page.tsx index 21e408889..bd50935f1 100644 --- a/apps/console/src/app/(console)/deployment/page.tsx +++ b/apps/console/src/app/(console)/deployment/page.tsx @@ -68,7 +68,7 @@ export default async function DeploymentPage() { + Tokens } diff --git a/apps/console/src/app/(console)/deployment/tokens/actions.ts b/apps/console/src/app/(console)/deployment/tokens/actions.ts index 20c74fd90..962d76685 100644 --- a/apps/console/src/app/(console)/deployment/tokens/actions.ts +++ b/apps/console/src/app/(console)/deployment/tokens/actions.ts @@ -187,8 +187,8 @@ export async function revokeOwnerTokenAction(formData: FormData) { const res = await fetch( url, await withOwnerSessionCookie({ - method: "DELETE", cache: "no-store", + method: "DELETE", }) ); if (res.status !== 204 && res.status !== 404) { diff --git a/apps/console/src/app/(console)/deployment/tokens/page.tsx b/apps/console/src/app/(console)/deployment/tokens/page.tsx index 4c094420b..75dcca01b 100644 --- a/apps/console/src/app/(console)/deployment/tokens/page.tsx +++ b/apps/console/src/app/(console)/deployment/tokens/page.tsx @@ -81,7 +81,7 @@ function OwnerAgentOnboardingCard({ entrypoint }: { entrypoint: string }) { pasted into chat or copied out of the dashboard.

- + Review deployment metadata
@@ -191,9 +191,9 @@ function Transcript({ flow }: { flow: FlowState }) { const approve = JSON.stringify({ user_code: flow.userCode }, null, 2); const exchange = JSON.stringify( { - grant_type: "urn:ietf:params:oauth:grant-type:device_code", - device_code: flow.deviceCode, client_id: flow.clientId, + device_code: flow.deviceCode, + grant_type: "urn:ietf:params:oauth:grant-type:device_code", }, null, 2 @@ -491,6 +491,7 @@ export default async function DeploymentTokensPage({ searchParams }: { searchPar const tokenDetailsByClient = new Map(); try { const resp = await listOwnerIssuedClients(); + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic tokens = resp.data ?? []; // Only clients with more than one active token get a per-token drilldown; // a single-token client is fully described by its row. @@ -499,6 +500,7 @@ export default async function DeploymentTokensPage({ searchParams }: { searchPar multiTokenClients.map(async (client) => { try { const detail = await listOwnerClientTokens(client.client_id); + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic tokenDetailsByClient.set(client.client_id, detail.data ?? []); } catch { // A per-client drilldown failure must not break the whole page; the @@ -522,11 +524,11 @@ export default async function DeploymentTokensPage({ searchParams }: { searchPar + Deployment overview } - breadcrumbs={[{ label: "Deployment", href: "/deployment" }, { label: "Tokens" }]} + breadcrumbs={[{ href: "/deployment", label: "Deployment" }, { label: "Tokens" }]} description="Set up trusted local owner automation without pasting bearer material. Manual owner bearers stay available below for debugging." title="Owner-agent access" /> @@ -545,6 +547,7 @@ export default async function DeploymentTokensPage({ searchParams }: { searchPar { assert.equal(formatLastError(null), "none"); assert.equal( - formatLastError({ message: "browser session expired", code: "session_expired" }), + formatLastError({ code: "session_expired", message: "browser session expired" }), "browser session expired" ); assert.equal(formatLastError({ code: "rate_limited" }), "rate_limited"); diff --git a/apps/console/src/app/(console)/device-exporters/render.ts b/apps/console/src/app/(console)/device-exporters/render.ts index 56c307fa0..3eb5bb19e 100644 --- a/apps/console/src/app/(console)/device-exporters/render.ts +++ b/apps/console/src/app/(console)/device-exporters/render.ts @@ -57,8 +57,8 @@ export function formatLastError(error: Record | null | undefine if (!error) { return "none"; } - const message = error.message; - const code = error.code; + const { message } = error; + const { code } = error; if (typeof message === "string" && message.trim()) { return message; } diff --git a/apps/console/src/app/(console)/error.tsx b/apps/console/src/app/(console)/error.tsx index 43612f23c..1f7751932 100644 --- a/apps/console/src/app/(console)/error.tsx +++ b/apps/console/src/app/(console)/error.tsx @@ -29,10 +29,11 @@ export default function DashboardError({ error, reset }: { error: Error & { dige sign back in if the problem persists.

diff --git a/apps/console/src/app/(console)/event-subscriptions/[subscriptionId]/page.tsx b/apps/console/src/app/(console)/event-subscriptions/[subscriptionId]/page.tsx index 2b58667bd..490f0a737 100644 --- a/apps/console/src/app/(console)/event-subscriptions/[subscriptionId]/page.tsx +++ b/apps/console/src/app/(console)/event-subscriptions/[subscriptionId]/page.tsx @@ -217,7 +217,7 @@ function RecentAttempts({ attempts }: { attempts: ClientEventSubscriptionAttempt {attempt.event_type} - {attempt.latency_ms == null ? "—" : `${attempt.latency_ms}ms`} + {attempt.latency_ms === null ? "—" : `${attempt.latency_ms}ms`} diff --git a/apps/console/src/app/(console)/event-subscriptions/disable-action.ts b/apps/console/src/app/(console)/event-subscriptions/disable-action.ts index 152d18b23..c20c6ada6 100644 --- a/apps/console/src/app/(console)/event-subscriptions/disable-action.ts +++ b/apps/console/src/app/(console)/event-subscriptions/disable-action.ts @@ -43,7 +43,7 @@ function validateReason(raw: string): { ok: true; value: string | null } | { ok: // (an `email-loop_suspected_…` reason that becomes `email-l…` is worse // than no reason at all). if (Buffer.byteLength(raw, "utf8") > MAX_REASON_BYTES) { - return { ok: false, message: reasonOverflowMessage() }; + return { message: reasonOverflowMessage(), ok: false }; } return { ok: true, value: raw }; } diff --git a/apps/console/src/app/(console)/event-subscriptions/page.tsx b/apps/console/src/app/(console)/event-subscriptions/page.tsx index 019504b37..f9a034413 100644 --- a/apps/console/src/app/(console)/event-subscriptions/page.tsx +++ b/apps/console/src/app/(console)/event-subscriptions/page.tsx @@ -582,7 +582,7 @@ function RecentAttempts({ attempts }: { attempts: ClientEventSubscriptionAttempt {attempt.event_type} - {attempt.latency_ms == null ? "—" : `${attempt.latency_ms}ms`} + {attempt.latency_ms === null ? "—" : `${attempt.latency_ms}ms`} diff --git a/apps/console/src/app/(console)/explore/actions.ts b/apps/console/src/app/(console)/explore/actions.ts index 1d1be8487..d5cc6d872 100644 --- a/apps/console/src/app/(console)/explore/actions.ts +++ b/apps/console/src/app/(console)/explore/actions.ts @@ -49,17 +49,17 @@ export async function loadExploreBuckets(req: ExploreBucketRequest): Promise
- - + Back to PDPP
diff --git a/apps/console/src/app/(console)/explore/explore-acceptance.test.ts b/apps/console/src/app/(console)/explore/explore-acceptance.test.ts index f04d9492b..137f5fba7 100644 --- a/apps/console/src/app/(console)/explore/explore-acceptance.test.ts +++ b/apps/console/src/app/(console)/explore/explore-acceptance.test.ts @@ -107,13 +107,13 @@ function makeHit(over: { connection_id?: string; }): SearchResultHit { return { - connector_id: over.connector_id, connection_id: over.connection_id, - stream: over.stream ?? "records", - record_key: over.record_key ?? "rec-1", + connector_id: over.connector_id, emitted_at: "2026-01-01T00:00:00Z", matched_fields: [], object: "search_result", + record_key: over.record_key ?? "rec-1", + stream: over.stream ?? "records", }; } @@ -139,11 +139,11 @@ function makeLexicalPage( function makeRecordsPage(ids: string[], opts?: { has_more?: boolean; next_cursor?: string }): RecordsPage { return { data: ids.map((id) => ({ + data: {}, + emitted_at: "2026-01-01T00:00:00Z", id, object: "record" as const, stream: "transactions", - emitted_at: "2026-01-01T00:00:00Z", - data: {}, })), has_more: opts?.has_more ?? false, ...(opts?.next_cursor ? { next_cursor: opts.next_cursor } : {}), @@ -157,19 +157,19 @@ function makeTimelinePage( ): ExploreTimelinePage { return { data: Array.from({ length: count }, (_, i) => ({ - object: "timeline_record" as const, connector_id: "ynab", connector_instance_id: `cin_ynab_stub_${i}`, - stream: "transactions", - record_key: `rec-${i}`, - emitted_at: `2026-01-0${i + 1}T00:00:00Z`, data: {}, + emitted_at: `2026-01-0${i + 1}T00:00:00Z`, + object: "timeline_record" as const, + record_key: `rec-${i}`, + stream: "transactions", })), has_more: opts.has_more, + new_since_snapshot: 0, next_cursor: opts.next_cursor ?? null, object: "list", snapshot_at: opts.snapshot_at ?? "2026-06-19T00:00:00Z", - new_since_snapshot: 0, }; } @@ -177,40 +177,40 @@ const notStubbed = () => Promise.reject(new Error("not stubbed")); function makeDataSource(overrides: Partial): DashboardDataSource { return { - kind: "sandbox" as const, aggregateRecordsByTime: notStubbed, - listExploreRecordBuckets: notStubbed, - isHybridRetrievalAdvertised: () => Promise.resolve(false), - isSemanticRetrievalAdvertised: () => Promise.resolve(false), - listConnectorSummaries: notStubbed, - listConnectorManifests: notStubbed, - searchRecordsLexical: notStubbed, - searchRecordsHybrid: notStubbed, - searchRecordsSemantic: notStubbed, - queryRecords: notStubbed, - getRecord: notStubbed, getConnectorOverview: notStubbed, - getStreamMetadata: notStubbed, - getTraceTimeline: notStubbed, - getGrantTimeline: notStubbed, - getRunTimeline: notStubbed, getDatasetSummary: notStubbed, getDeploymentDiagnostics: notStubbed, - listGrants: notStubbed, - listPendingApprovals: notStubbed, - listRuns: notStubbed, - listStreams: notStubbed, - listTraces: notStubbed, - refSearch: notStubbed, + getGrantTimeline: notStubbed, + getRecord: notStubbed, + getRunTimeline: notStubbed, + getStreamMetadata: notStubbed, + getTraceTimeline: notStubbed, + isHybridRetrievalAdvertised: () => Promise.resolve(false), + isSemanticRetrievalAdvertised: () => Promise.resolve(false), + kind: "sandbox" as const, + listConnectorManifests: notStubbed, + listConnectorSummaries: notStubbed, + listExploreRecordBuckets: notStubbed, listExploreTimeline: () => Promise.resolve({ - object: "list" as const, data: [], has_more: false, + new_since_snapshot: 0, next_cursor: null, + object: "list" as const, snapshot_at: new Date(0).toISOString(), - new_since_snapshot: 0, }), + listGrants: notStubbed, + listPendingApprovals: notStubbed, + listRuns: notStubbed, + listStreams: notStubbed, + listTraces: notStubbed, + queryRecords: notStubbed, + refSearch: notStubbed, + searchRecordsHybrid: notStubbed, + searchRecordsLexical: notStubbed, + searchRecordsSemantic: notStubbed, ...overrides, }; } @@ -333,7 +333,6 @@ test("P1 assembler: streamSeeAllLinks is non-empty when the time-range fan-out h // produce the ramps. const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); const ds = makeDataSource({ - listConnectorSummaries: async () => summaryListResponse([summary]), // The time-range fan-out requires a consent_time_field on the stream so // timeRangeStreamTargets includes it. The base makeManifest helper does not // set this field; provide an explicit manifest here with it declared. @@ -341,22 +340,23 @@ test("P1 assembler: streamSeeAllLinks is non-empty when the time-range fan-out h [ { connector_id: "ynab", - streams: [{ name: "transactions", consent_time_field: "date" }], + streams: [{ consent_time_field: "date", name: "transactions" }], }, ] satisfies ConnectorManifest[], - // Time-range fan-out: queryRecords returns has_more=true so the see-all ramp fires. - queryRecords: async () => - makeRecordsPage(["rec-1", "rec-2", "rec-3", "rec-4", "rec-5", "rec-6"], { has_more: true }), + listConnectorSummaries: async () => summaryListResponse([summary]), // The recent lens would call this; it is irrelevant here (time-range lens), // but provide a valid empty page rather than throwing. listExploreTimeline: async () => ({ - object: "list" as const, data: [], has_more: false, + new_since_snapshot: 0, next_cursor: null, + object: "list" as const, snapshot_at: new Date(0).toISOString(), - new_since_snapshot: 0, }), + // Time-range fan-out: queryRecords returns has_more=true so the see-all ramp fires. + queryRecords: async () => + makeRecordsPage(["rec-1", "rec-2", "rec-3", "rec-4", "rec-5", "rec-6"], { has_more: true }), }); const result = await assembleExplorerData( @@ -369,7 +369,7 @@ test("P1 assembler: streamSeeAllLinks is non-empty when the time-range fan-out h result.streamSeeAllLinks.length > 0, "streamSeeAllLinks must be non-empty when a time-range fan-out stream has has_more=true" ); - const link = result.streamSeeAllLinks[0]; + const [link] = result.streamSeeAllLinks; assert.equal(link?.connectionId, "ynab-1", "see-all link must carry the correct connectionId"); assert.equal(link?.stream, "transactions", "see-all link must carry the correct stream"); }); @@ -387,8 +387,8 @@ test("P3 invariant: day-group (rr-x-day) and burst-collapse (rr-x-burst) class n test("P3 Load-more cursor: assembler returns non-null nextCursor when real endpoint has_more=true", async () => { const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); const ds = makeDataSource({ - listConnectorSummaries: async () => summaryListResponse([summary]), listConnectorManifests: async () => [makeManifest("ynab", ["transactions"])], + listConnectorSummaries: async () => summaryListResponse([summary]), // Real endpoint: returns has_more=true with a real composite cursor listExploreTimeline: async () => makeTimelinePage(32, { has_more: true, next_cursor: "composite-cursor-p2" }), }); @@ -404,8 +404,8 @@ test("P3 Load-more cursor: assembler returns non-null nextCursor when real endpo test("P3 Load-more cursor: assembler returns null nextCursor when real endpoint has_more=false (end of feed)", async () => { const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); const ds = makeDataSource({ - listConnectorSummaries: async () => summaryListResponse([summary]), listConnectorManifests: async () => [makeManifest("ynab", ["transactions"])], + listConnectorSummaries: async () => summaryListResponse([summary]), listExploreTimeline: async () => makeTimelinePage(5, { has_more: false, snapshot_at: "2026-06-19T00:00:00Z" }), }); @@ -430,39 +430,39 @@ test("P3 Load-more trail: second page ACCUMULATES (page 1 stays, page 2 appended // Snapshot newer than every record so the page-1 filter keeps the whole snapshot. const PAGE1_SNAPSHOT = "2026-12-31T00:00:00Z"; const mkRecord = (page: string, i: number, dayOffset: number) => ({ - object: "timeline_record" as const, connector_id: "ynab", connector_instance_id: "ynab-1", - stream: "transactions", - record_key: `${page}-${i}`, - emitted_at: new Date(baseMs - dayOffset * dayMs).toISOString(), data: {}, + emitted_at: new Date(baseMs - dayOffset * dayMs).toISOString(), + object: "timeline_record" as const, + record_key: `${page}-${i}`, + stream: "transactions", }); const page1Records = Array.from({ length: 32 }, (_, i) => mkRecord("p1", i, i)); const page2Records = Array.from({ length: 3 }, (_, i) => mkRecord("p2", i, 100 + i)); const page1: ExploreTimelinePage = { - object: "list", data: page1Records, has_more: true, + new_since_snapshot: 0, next_cursor: "composite-cursor-p2", + object: "list", snapshot_at: PAGE1_SNAPSHOT, - new_since_snapshot: 0, }; const page2: ExploreTimelinePage = { - object: "list", data: page2Records, has_more: false, + new_since_snapshot: 0, next_cursor: null, + object: "list", snapshot_at: PAGE1_SNAPSHOT, - new_since_snapshot: 0, }; // Capture each fetch as `${rewind ? "rewind:" : "cursor:"}${cursor}` so we can // assert the fetch plan distinguishes a REWIND of the trail head (page 1) from a // plain fetch of the same cursor (page 2). const capturedFetches: string[] = []; const ds = makeDataSource({ - listConnectorSummaries: async () => summaryListResponse([summary]), listConnectorManifests: async () => [makeManifest("ynab", ["transactions"])], + listConnectorSummaries: async () => summaryListResponse([summary]), listExploreTimeline: (opts) => { const cursor = opts?.cursor ?? null; const rewind = Boolean(opts?.rewindToFirstPage); @@ -485,7 +485,7 @@ test("P3 Load-more trail: second page ACCUMULATES (page 1 stays, page 2 appended // Page 2: append the page-1 cursor to the trail (Load more), forwarding the anchor. const accumulated = await assembleExplorerData( - { cursors: "composite-cursor-p2", anchor: firstSnapshot ?? undefined }, + { anchor: firstSnapshot ?? undefined, cursors: "composite-cursor-p2" }, ds, "https://rs.test" ); @@ -503,7 +503,7 @@ test("P3 Load-more trail: second page ACCUMULATES (page 1 stays, page 2 appended ); // Non-increasing emitted_at across the concatenation; no duplicates. const times = accumulated.feed.map((e) => Date.parse(e.emittedAt)); - for (let i = 1; i < times.length; i++) { + for (let i = 1; i < times.length; i += 1) { assert.ok((times[i] ?? 0) <= (times[i - 1] ?? 0), "feed must stay non-increasing emitted_at"); } const ids = accumulated.feed.map((e) => `${e.connectionId} ${e.stream} ${e.recordId}`); @@ -549,28 +549,28 @@ test("P2 Option D invariant: escape links to search_sort=recent in the URL build test("P2 sort toggle: lexical Most-relevant and Most-recent return same hit count (same pool)", async () => { const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); - const hit1 = makeHit({ connector_id: "ynab", stream: "transactions", record_key: "r1", connection_id: "ynab-1" }); - const hit2 = makeHit({ connector_id: "ynab", stream: "transactions", record_key: "r2", connection_id: "ynab-1" }); + const hit1 = makeHit({ connection_id: "ynab-1", connector_id: "ynab", record_key: "r1", stream: "transactions" }); + const hit2 = makeHit({ connection_id: "ynab-1", connector_id: "ynab", record_key: "r2", stream: "transactions" }); // Both orderings use the same assembler data source with the same hits. // Most-relevant: lexical call returns the hits ranked by relevance. // Most-recent (single-stream): switches to queryRecords for the same stream. // The feed count must be the same (same pool, different ordering). const dsRelevance = makeDataSource({ - listConnectorSummaries: async () => summaryListResponse([summary]), - listConnectorManifests: async () => [makeManifest("ynab", ["transactions"])], isHybridRetrievalAdvertised: async () => false, + listConnectorManifests: async () => [makeManifest("ynab", ["transactions"])], + listConnectorSummaries: async () => summaryListResponse([summary]), searchRecordsLexical: async () => makeLexicalPage([hit1, hit2], { has_more: false }), }); const dsRecent = makeDataSource({ - listConnectorSummaries: async () => summaryListResponse([summary]), - listConnectorManifests: async () => [makeManifest("ynab", ["transactions"])], isHybridRetrievalAdvertised: async () => false, - // Most-recent detection path: lexical to find stream door - searchRecordsLexical: async () => makeLexicalPage([hit1, hit2], { has_more: false }), + listConnectorManifests: async () => [makeManifest("ynab", ["transactions"])], + listConnectorSummaries: async () => summaryListResponse([summary]), // Then queryRecords for the same 2 records in time order queryRecords: async () => makeRecordsPage(["r1", "r2"], { has_more: false }), + // Most-recent detection path: lexical to find stream door + searchRecordsLexical: async () => makeLexicalPage([hit1, hit2], { has_more: false }), }); const relevanceResult = await assembleExplorerData({ q: "rent" }, dsRelevance, "https://rs.test"); @@ -586,14 +586,14 @@ test("P2 sort toggle: lexical Most-relevant and Most-recent return same hit coun test("P2 lexical Load-more forwards cursor (assembler advances, does not re-fetch from top)", async () => { const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); - const hit1 = makeHit({ connector_id: "ynab", stream: "transactions", record_key: "p1", connection_id: "ynab-1" }); - const hit2 = makeHit({ connector_id: "ynab", stream: "transactions", record_key: "p2", connection_id: "ynab-1" }); + const hit1 = makeHit({ connection_id: "ynab-1", connector_id: "ynab", record_key: "p1", stream: "transactions" }); + const hit2 = makeHit({ connection_id: "ynab-1", connector_id: "ynab", record_key: "p2", stream: "transactions" }); const capturedCursors: (string | undefined)[] = []; const ds = makeDataSource({ - listConnectorSummaries: async () => summaryListResponse([summary]), - listConnectorManifests: async () => [makeManifest("ynab", ["transactions"])], isHybridRetrievalAdvertised: () => Promise.resolve(false), + listConnectorManifests: async () => [makeManifest("ynab", ["transactions"])], + listConnectorSummaries: async () => summaryListResponse([summary]), searchRecordsLexical: (_q, opts) => { capturedCursors.push(opts?.cursor); // Recall proven complete: keyword_pageable, so the cursor pages exhaustively. @@ -608,7 +608,7 @@ test("P2 lexical Load-more forwards cursor (assembler advances, does not re-fetc const page1 = await assembleExplorerData({ q: "coffee" }, ds, "https://rs.test"); assert.equal(page1.searchNextCursor, "lex-p2"); - const page2 = await assembleExplorerData({ q: "coffee", cursor: "lex-p2" }, ds, "https://rs.test"); + const page2 = await assembleExplorerData({ cursor: "lex-p2", q: "coffee" }, ds, "https://rs.test"); assert.equal(page2.searchNextCursor, null, "page 2 must be exhausted"); assert.equal(capturedCursors[0], undefined, "page 1: no cursor"); diff --git a/apps/console/src/app/(console)/explore/explore-canvas.tsx b/apps/console/src/app/(console)/explore/explore-canvas.tsx index 337c8cbcc..34a792e08 100644 --- a/apps/console/src/app/(console)/explore/explore-canvas.tsx +++ b/apps/console/src/app/(console)/explore/explore-canvas.tsx @@ -248,18 +248,18 @@ function dateNormalizedHref(explorePath: string, state: DateNormalizeState): str const dateNav = dateNavFromLift(dateLift.after, dateLift.before); return withOrderSuffix( buildHref(explorePath, { - query: dateLift.rest, connectionIds: state.selectedConnectionIds, excludeConnectionIds: state.excludeConnectionIds, - streams: state.selectedStreams, excludeStreams: state.excludeStreams, + // Preserve a peeked record from a shared `?q=after:X&peek=Y` link. + peek: state.peek, + query: dateLift.rest, + searchSort: state.searchSort === "recent" ? "recent" : undefined, // Only the endpoint the URL actually typed overrides the canonical window; the // other side is carried forward from the existing window (never stacks/clears). since: dateNav.since ?? state.since, + streams: state.selectedStreams, until: dateNav.until ?? state.until, - searchSort: state.searchSort === "recent" ? "recent" : undefined, - // Preserve a peeked record from a shared `?q=after:X&peek=Y` link. - peek: state.peek, }), state.order ); @@ -299,9 +299,9 @@ function composeFacetSelection( const nextStreams = unionIds(cur.streams, lift.includeStreams); return { connectionIds: nextInclude, - streams: nextStreams, excludeConnectionIds: unionIds(cur.excludeConnectionIds, liftedExclude).filter((id) => !nextInclude.includes(id)), excludeStreams: unionIds(cur.excludeStreams, lift.excludeStreams).filter((s) => !nextStreams.includes(s)), + streams: nextStreams, }; } @@ -541,19 +541,19 @@ function buildFilterChips(args: { for (const id of args.selectedConnectionIds) { const name = args.connections.find((c) => c.connectionId === id)?.displayName ?? id; out.push({ + canNegate: true, + clear: () => args.toggleConnection(id), id: `con:${id}`, label: name, - property: "source", - operator: "is", - value: name, - negated: false, - canNegate: true, negate: () => { // Toggle include → exclude: remove from include, add to exclude args.toggleConnection(id); args.toggleExcludeConnection(id); }, - clear: () => args.toggleConnection(id), + negated: false, + operator: "is", + property: "source", + value: name, }); } // EXCLUDE chips render the facet/operator exclusion as a visible chip in the SAME @@ -562,51 +562,51 @@ function buildFilterChips(args: { for (const id of args.excludeConnectionIds) { const name = args.connections.find((c) => c.connectionId === id)?.displayName ?? id; out.push({ + canNegate: true, + clear: () => args.toggleExcludeConnection(id), id: `xcon:${id}`, label: `not ${name}`, - property: "source", - operator: "is not", - value: name, - negated: true, - canNegate: true, negate: () => { // Toggle exclude → include: remove from exclude, add to include args.toggleExcludeConnection(id); args.toggleConnection(id); }, - clear: () => args.toggleExcludeConnection(id), + negated: true, + operator: "is not", + property: "source", + value: name, }); } for (const s of args.selectedStreams) { out.push({ + canNegate: true, + clear: () => args.toggleStream(s), id: `stream:${s}`, label: `stream: ${s}`, - property: "stream", - operator: "is", - value: s, - negated: false, - canNegate: true, negate: () => { args.toggleStream(s); args.toggleExcludeStream(s); }, - clear: () => args.toggleStream(s), + negated: false, + operator: "is", + property: "stream", + value: s, }); } for (const s of args.excludeStreams) { out.push({ + canNegate: true, + clear: () => args.toggleExcludeStream(s), id: `xstream:${s}`, label: `not stream: ${s}`, - property: "stream", - operator: "is not", - value: s, - negated: true, - canNegate: true, negate: () => { args.toggleExcludeStream(s); args.toggleStream(s); }, - clear: () => args.toggleExcludeStream(s), + negated: true, + operator: "is not", + property: "stream", + value: s, }); } // NB: the active DATE window is NOT pushed here. It is rendered ONCE, by the @@ -620,24 +620,24 @@ function buildFilterChips(args: { // raw shapes: "stream:messages", "-con:abc", "has:link", "before:2026-01-01", or a // bare word (free-text search). A leading "-" is the exclude operator. A bare word // (no colon) is a free-text search term → property "search". - const raw = tk.raw; + const { raw } = tk; const negated = raw.startsWith("-"); const body = negated ? raw.slice(1) : raw; const colon = body.indexOf(":"); const property = colon > 0 ? body.slice(0, colon) : "search"; const value = colon > 0 ? body.slice(colon + 1) : body; out.push({ + canNegate: false, + clear: () => args.navigate({ query: removeToken(args.query, tk.raw) }), id: `tok:${i}`, label: tk.label, - property, - operator: negated ? "is not" : "is", - value, - negated, - canNegate: false, negate: () => { // token chips do not support negation toggling (use -operator: in the query) }, - clear: () => args.navigate({ query: removeToken(args.query, tk.raw) }), + negated, + operator: negated ? "is not" : "is", + property, + value, }); }); return out; @@ -809,7 +809,7 @@ function QueryInput(props: QueryInputProps) { return; } // At this point kind is "has-image" | "has-link" | "date" — all handled by suggestionToken. - const kind = s.kind; + const { kind } = s; if (kind === "has-image" || kind === "has-link" || kind === "date") { props.onDraftChange(appendOperatorToken(props.draft, suggestionToken(kind))); } @@ -873,7 +873,9 @@ function QueryInput(props: QueryInputProps) { aria-expanded={menuOpen && suggestions.length > 0} aria-label="Search or filter" className="rr-x-search" + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onBlur={() => setMenuOpen(false)} + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onChange={(e: ChangeEvent) => { props.onDraftChange(e.target.value); setMenuOpen(true); @@ -881,6 +883,7 @@ function QueryInput(props: QueryInputProps) { // until the owner explicitly arrows into the menu (Enter-hijack fix, F3). setCursor(NO_HIGHLIGHT); }} + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onFocus={() => setMenuOpen(true)} onKeyDown={onKeyDown} placeholder="Search or filter…" @@ -891,6 +894,7 @@ function QueryInput(props: QueryInputProps) { @@ -923,6 +928,7 @@ function QueryInput(props: QueryInputProps) { aria-selected={i === cursor} className={["rr-x-typeahead__btn", i === cursor ? "is-active" : ""].filter(Boolean).join(" ")} id={`rr-x-typeahead-opt-${i}`} + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onMouseDown={(e) => { e.preventDefault(); pickSuggestion(s); @@ -1229,6 +1235,7 @@ function DateChip({ aria-expanded={open} aria-haspopup="dialog" className="rr-x-datechip__trigger" + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onClick={() => setOpen((v) => !v)} type="button" > @@ -1256,6 +1263,7 @@ function DateChip({ aria-checked={selected} className={["rr-x-datechip__preset", selected ? "is-on" : ""].filter(Boolean).join(" ")} key={preset.key} + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onClick={() => { onPreset(preset.key); setOpen(false); @@ -1275,6 +1283,7 @@ function DateChip({ instantly above. */} { e.preventDefault(); if (!(customEmpty || customInvalid)) { @@ -1288,6 +1297,7 @@ function DateChip({ From setFrom(e.target.value)} type="date" value={from} @@ -1299,6 +1309,7 @@ function DateChip({ className="rr-x-datechip__date" // Guard To < From at the input where the browser supports it. min={from || undefined} + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onChange={(e) => setTo(e.target.value)} type="date" value={to} @@ -1421,6 +1432,7 @@ function FeedControls({ @@ -1786,6 +1806,7 @@ function StreamFacets({ setFilter(e.target.value)} placeholder="Search sources & streams…" type="text" @@ -1923,15 +1944,13 @@ function FeedRow({ server's honest highlight, rendered as real elements — never dangerouslySetInnerHTML). Falls back to the plain excerpt. */} {entry.snippetSegments && entry.snippetSegments.length > 0 - ? entry.snippetSegments.map((seg, i) => + ? entry.snippetSegments.map((seg) => seg.marked ? ( - // biome-ignore lint/suspicious/noArrayIndexKey: ordered immutable segment list - + {seg.text} ) : ( - // biome-ignore lint/suspicious/noArrayIndexKey: ordered immutable segment list - {seg.text} + {seg.text} ) ) : matchExcerpt} @@ -2005,6 +2024,7 @@ function FeedRow({ data-feed-row data-selected={selected ? "true" : undefined} onClick={onSelect} + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onKeyDown={(e) => { // The keyboard contract is a PURE decision (resolveRowKeyAction): // ↑/↓ move selection, Enter peeks, Cmd/Ctrl-Enter opens the full @@ -2071,7 +2091,7 @@ function BurstRow({ recordsBasePath: string; selectedPeekParam: string | null; }) { - const rep = burst.entries[0]; + const [rep] = burst.entries; const loaded = burst.entries.length; const streamLabel = `${rep?.connectionDisplayName ?? rep?.connectorId ?? ""}${rep?.stream ? ` / ${rep.stream}` : ""}`; // SLVP preview-content-by-default (review-gated 2026-06-22): a burst is NEVER @@ -2099,9 +2119,12 @@ function BurstRow({ onMoveSelection(param, direction)} onClearSelection={onClearSelection} + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onOpenFull={() => onOpenRecord(entry)} + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onSelect={() => onSelectRecord(entry)} recordsBasePath={recordsBasePath} selected={param === selectedPeekParam} @@ -2473,6 +2496,7 @@ function FeedDays({ onMoveSelection={onMoveSelection} onOpenRecord={onOpenRecord} onSelectRecord={onSelectRecord} + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onToggle={() => onToggleBurst(burst.key)} recordsBasePath={recordsBasePath} selectedPeekParam={selectedPeekParam} @@ -2485,9 +2509,12 @@ function FeedDays({ onMoveSelection(param, direction)} onClearSelection={onClearSelection} + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onOpenFull={() => onOpenRecord(entry)} + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onSelect={() => onSelectRecord(entry)} recordsBasePath={recordsBasePath} selected={param === selectedPeekParam} @@ -2571,6 +2598,7 @@ function UpcomingSection({ @@ -2842,7 +2875,9 @@ function SaveViewAction({ onSave }: { onSave: (name: string) => void }) { autoFocus className="rr-x-views-tab__input" onBlur={commit} + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onChange={(e) => setDraftName(e.target.value)} + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); @@ -2881,6 +2916,7 @@ function SavedViewTab({ @@ -3872,6 +3911,7 @@ export function ExploreCanvas({ data, explorePath, order = "newest", peekRelatio isPending={isPending} onClearAll={clearAll} onClearSelection={clearSelection} + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onLoadMore={(cursor) => { // Recent merged-timeline lens ACCUMULATES: append the next_cursor to // the trail so prior pages stay visible (the "records above disappear" @@ -3884,6 +3924,7 @@ export function ExploreCanvas({ data, explorePath, order = "newest", peekRelatio navigate({ cursor: cursor ?? undefined }, "loadmore"); } }} + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onLoadMoreUpcoming={(cursor) => { // Walk the Upcoming (future) projection one page further: append the // upcoming_next_cursor to the `ucursors` trail so revealed future records diff --git a/apps/console/src/app/(console)/explore/explore-control-state.test.ts b/apps/console/src/app/(console)/explore/explore-control-state.test.ts index 8a25d0a72..da9d2571c 100644 --- a/apps/console/src/app/(console)/explore/explore-control-state.test.ts +++ b/apps/console/src/app/(console)/explore/explore-control-state.test.ts @@ -98,8 +98,8 @@ test("buildRecordDetailHref builds the record-detail route from clean path segme const href = buildRecordDetailHref("/sources", { connectionId: "cin_bc1efca69a1c386d610f0924", connectorId: "usaa", - stream: "transactions", recordId: "d495b98f5bfe6ce1ae10c465aeb607b5", + stream: "transactions", }); assert.equal(href, "/sources/cin_bc1efca69a1c386d610f0924/transactions/d495b98f5bfe6ce1ae10c465aeb607b5"); }); @@ -114,8 +114,8 @@ test("buildRecordDetailHref reproduce-the-bug: no query string, record key is th const href = buildRecordDetailHref("/sources", { connectionId: null, connectorId: "gmail", - stream: "messages", recordId, + stream: "messages", }); assert.ok(!href.includes("?"), `record detail href must not contain a query string: ${href}`); assert.ok(!href.includes("order="), `record detail href must not carry a sort order: ${href}`); @@ -128,8 +128,8 @@ test("buildRecordDetailHref falls back to connectorId when connection identity i const href = buildRecordDetailHref("/sources", { connectionId: null, connectorId: "github", - stream: "issues", recordId: "42", + stream: "issues", }); assert.equal(href, "/sources/github/issues/42"); }); @@ -152,7 +152,7 @@ test("resolveRowKeyAction: Cmd/Ctrl-Enter ESCALATES to the full record route (di action: "open-full", preventDefault: true, }); - assert.deepEqual(resolveRowKeyAction({ key: "Enter", ctrlKey: true }), { + assert.deepEqual(resolveRowKeyAction({ ctrlKey: true, key: "Enter" }), { action: "open-full", preventDefault: true, }); diff --git a/apps/console/src/app/(console)/explore/explore-control-state.ts b/apps/console/src/app/(console)/explore/explore-control-state.ts index b86fbd172..c69825f6f 100644 --- a/apps/console/src/app/(console)/explore/explore-control-state.ts +++ b/apps/console/src/app/(console)/explore/explore-control-state.ts @@ -3,7 +3,7 @@ export type ExploreRange = "today" | "7d" | "30d" | "all"; -const RANGE_DAYS: Record, number> = { today: 0, "7d": 6, "30d": 29 }; +const RANGE_DAYS: Record, number> = { "7d": 6, "30d": 29, today: 0 }; const DAY_MS = 86_400_000; /** ISO `since` for a relative range, or "" for "all". */ diff --git a/apps/console/src/app/(console)/explore/explore-date.test.ts b/apps/console/src/app/(console)/explore/explore-date.test.ts index 4ae8620ea..5a89ae85d 100644 --- a/apps/console/src/app/(console)/explore/explore-date.test.ts +++ b/apps/console/src/app/(console)/explore/explore-date.test.ts @@ -23,7 +23,7 @@ test("dateChipLabel: sliding presets read by their rolling name (end = now)", () test("dateChipLabel: growing (since-only, not a preset) → Since ", () => { // An anchored start that is not one of the rolling presets is still growing to now. - const since = resolveCustomRange("2026-06-12", "", DAY_TZ).since; + const { since } = resolveCustomRange("2026-06-12", "", DAY_TZ); assert.equal(dateChipLabel(since, "", NOW_MS), "Since Jun 12"); }); diff --git a/apps/console/src/app/(console)/explore/explore-date.ts b/apps/console/src/app/(console)/explore/explore-date.ts index 26c795cda..ff3d85414 100644 --- a/apps/console/src/app/(console)/explore/explore-date.ts +++ b/apps/console/src/app/(console)/explore/explore-date.ts @@ -64,7 +64,7 @@ function parseYmd(value: string): CalendarDay | null { if (!m) { return null; } - return { year: Number(m[1]), month: Number(m[2]), day: Number(m[3]) }; + return { day: Number(m[3]), month: Number(m[2]), year: Number(m[1]) }; } /** @@ -74,13 +74,13 @@ function parseYmd(value: string): CalendarDay | null { */ function calendarDayInZone(ms: number, timeZone: string): CalendarDay { const parts = new Intl.DateTimeFormat("en-CA", { + day: "2-digit", + month: "2-digit", timeZone, year: "numeric", - month: "2-digit", - day: "2-digit", }).formatToParts(new Date(ms)); const get = (type: string) => Number(parts.find((p) => p.type === type)?.value ?? "0"); - return { year: get("year"), month: get("month"), day: get("day") }; + return { day: get("day"), month: get("month"), year: get("year") }; } /** @@ -101,14 +101,14 @@ function startOfDayMs(day: CalendarDay, timeZone: string): number { /** The wall-clock instant (as if-UTC ms) that `timeZone` displays for epoch `ms`. */ function wallClockMsInZone(ms: number, timeZone: string): number { const parts = new Intl.DateTimeFormat("en-US", { - timeZone, - year: "numeric", - month: "2-digit", day: "2-digit", hour: "2-digit", + hourCycle: "h23", minute: "2-digit", + month: "2-digit", second: "2-digit", - hourCycle: "h23", + timeZone, + year: "numeric", }).formatToParts(new Date(ms)); const get = (type: string) => Number(parts.find((p) => p.type === type)?.value ?? "0"); return Date.UTC(get("year"), get("month") - 1, get("day"), get("hour"), get("minute"), get("second")); @@ -207,7 +207,7 @@ const MONTH_DAY_FMT_CACHE = new Map(); function monthDayFormatter(timeZone: string): Intl.DateTimeFormat { let fmt = MONTH_DAY_FMT_CACHE.get(timeZone); if (!fmt) { - fmt = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone }); + fmt = new Intl.DateTimeFormat("en-US", { day: "numeric", month: "short", timeZone }); MONTH_DAY_FMT_CACHE.set(timeZone, fmt); } return fmt; diff --git a/apps/console/src/app/(console)/explore/explore-facet-groups.test.ts b/apps/console/src/app/(console)/explore/explore-facet-groups.test.ts index 8c097ae2d..dd5297405 100644 --- a/apps/console/src/app/(console)/explore/explore-facet-groups.test.ts +++ b/apps/console/src/app/(console)/explore/explore-facet-groups.test.ts @@ -51,12 +51,12 @@ test("per-source loaded count = loaded feed rows for THAT (connection, stream), entry("conn_slack", "channels"), ]; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK, IMESSAGE], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: [], }); const slack = groups.find((g) => g.connectionId === "conn_slack"); @@ -81,12 +81,12 @@ test("per-source loaded count = loaded feed rows for THAT (connection, stream), test("source loadedTotal is the sum of its visible streams' loaded counts (same KIND, not a lifetime total)", () => { const feed = [entry("conn_slack", "messages"), entry("conn_slack", "messages"), entry("conn_slack", "channels")]; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: [], }); assert.equal(groups[0]?.loadedTotal, 3); }); @@ -96,12 +96,12 @@ test("0-in-window stream is hidden (no dead-end) UNLESS it is an active filter", const feed = [entry("conn_slack", "channels")]; const hidden = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: [], }); // messages (0 in window, not selected) must NOT appear — never a "0" dead-end. assert.equal( @@ -112,12 +112,12 @@ test("0-in-window stream is hidden (no dead-end) UNLESS it is an active filter", // But a SELECTED messages stays visible even at 0 — an active filter is never hidden. const withSelected = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: ["messages"], - excludeStreams: [], }); const sel = withSelected[0]?.streams.find((s) => s.stream === "messages"); assert.ok(sel, "a selected stream with 0 in-window rows is kept (no dropped filter)"); @@ -126,12 +126,12 @@ test("0-in-window stream is hidden (no dead-end) UNLESS it is an active filter", // Likewise an EXCLUDED stream at 0 stays visible. const withExcluded = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK], + excludeStreams: ["messages"], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: ["messages"], }); const ex = withExcluded[0]?.streams.find((s) => s.stream === "messages"); assert.ok(ex); @@ -142,12 +142,12 @@ test("a source with no visible streams is dropped entirely (no empty group)", () // iMessage owns messages/attachments but the loaded window has neither. const feed = [entry("conn_slack", "channels")]; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK, IMESSAGE], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: [], }); assert.equal( groups.find((g) => g.connectionId === "conn_imessage"), @@ -158,12 +158,12 @@ test("a source with no visible streams is dropped entirely (no empty group)", () test("when connections are selected, the rail scopes to those sources only", () => { const feed = [entry("conn_slack", "messages"), entry("conn_imessage", "messages")]; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK, IMESSAGE], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: ["conn_slack"], selectedStreams: [], - excludeStreams: [], }); assert.equal(groups.length, 1); assert.equal(groups[0]?.connectionId, "conn_slack"); @@ -179,12 +179,12 @@ test("the client predicate is honored: counts only loaded rows that pass (count= return keep; }; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK], + excludeStreams: [], + feed, passes: everyOther, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: [], }); assert.equal( groups[0]?.streams.find((s) => s.stream === "messages")?.loadedCount, @@ -200,12 +200,12 @@ test("rows with a null connectionId never contribute to a per-source count", () { ...entry("conn_slack", "messages"), connectionId: null }, ]; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: [], }); // Only the row with a concrete connectionId counts (1, not 2). assert.equal(groups[0]?.streams.find((s) => s.stream === "messages")?.loadedCount, 1); @@ -219,12 +219,12 @@ test("active filters pin to the top of a source group; the rest rank by loaded c entry("conn_slack", "messages"), ]; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: ["messages"], // selected, but fewer loaded rows - excludeStreams: [], }); // Selected `messages` pins first despite channels having more rows. assert.equal(groups[0]?.streams[0]?.stream, "messages"); @@ -235,12 +235,12 @@ test("active filters pin to the top of a source group; the rest rank by loaded c test("totalVisibleStreamFacets sums the rendered stream rows across groups", () => { const feed = [entry("conn_slack", "messages"), entry("conn_slack", "channels"), entry("conn_imessage", "messages")]; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK, IMESSAGE], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: [], }); // Slack: messages + channels (2); iMessage: messages (1) → 3 total. assert.equal(totalVisibleStreamFacets(groups), 3); @@ -254,12 +254,12 @@ test("filterSourceGroups matches source name (keep whole group) or stream names entry("conn_imessage", "attachments"), ]; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK, IMESSAGE], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: [], }); // Matching the SOURCE name keeps the whole group with all its streams. @@ -284,12 +284,12 @@ test("filterSourceGroups matches source name (keep whole group) or stream names test("filterSourceGroups recomputes the group total over the surviving streams", () => { const feed = [entry("conn_slack", "messages"), entry("conn_slack", "messages"), entry("conn_slack", "channels")]; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [SLACK], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: [], }); // Filtering to just `channels` makes the source total 1 (not the full 3). const filtered = filterSourceGroups(groups, "channels"); diff --git a/apps/console/src/app/(console)/explore/explore-facet-groups.ts b/apps/console/src/app/(console)/explore/explore-facet-groups.ts index 88575d8ef..c93f35f46 100644 Binary files a/apps/console/src/app/(console)/explore/explore-facet-groups.ts and b/apps/console/src/app/(console)/explore/explore-facet-groups.ts differ diff --git a/apps/console/src/app/(console)/explore/explore-facet-unification.test.ts b/apps/console/src/app/(console)/explore/explore-facet-unification.test.ts index 36bfa6799..3be5cd281 100644 --- a/apps/console/src/app/(console)/explore/explore-facet-unification.test.ts +++ b/apps/console/src/app/(console)/explore/explore-facet-unification.test.ts @@ -32,25 +32,25 @@ const STREAM_NOT_MESSAGES_RE = /stream!=messages/; test("clicking a SOURCE facet and typing con: compile to the IDENTICAL query (chip == operator)", () => { // Rail click → selectedConnectionIds: ["gmail"]. const viaFacet = buildCompiledQuery({ + limit: 50, + order: "newest", parsed: parseQuery(""), selectedConnectionIds: ["gmail"], selectedStreams: [], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); // Typed operator → con:gmail. const viaOperator = buildCompiledQuery({ + limit: 50, + order: "newest", parsed: parseQuery("con:gmail"), selectedConnectionIds: [], selectedStreams: [], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); assert.match(viaFacet, CONNECTION_GMAIL_RE); assert.match(viaOperator, CONNECTION_GMAIL_RE); @@ -59,24 +59,24 @@ test("clicking a SOURCE facet and typing con: compile to the IDENTICAL query (ch test("clicking a STREAM facet and typing stream: compile to the IDENTICAL query (chip == operator)", () => { const viaFacet = buildCompiledQuery({ + limit: 50, + order: "newest", parsed: parseQuery(""), selectedConnectionIds: [], selectedStreams: ["messages"], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); const viaOperator = buildCompiledQuery({ + limit: 50, + order: "newest", parsed: parseQuery("stream:messages"), selectedConnectionIds: [], selectedStreams: [], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); assert.match(viaFacet, STREAM_MESSAGES_RE); assert.match(viaOperator, STREAM_MESSAGES_RE); @@ -107,26 +107,26 @@ test("removing a con:/stream: token from the query deselects the rail (no lifted test("the 'is not' SOURCE toggle and -con: compile to the same exclusion (inversion == flipped operator)", () => { const viaFacet = buildCompiledQuery({ + excludedConnectionIds: ["gmail"], + excludedStreams: [], + limit: 50, + order: "newest", parsed: parseQuery(""), selectedConnectionIds: [], selectedStreams: [], - excludedConnectionIds: ["gmail"], - excludedStreams: [], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); const viaOperator = buildCompiledQuery({ + limit: 50, + order: "newest", parsed: parseQuery("-con:gmail"), selectedConnectionIds: [], selectedStreams: [], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); assert.match(viaFacet, CONNECTION_NOT_GMAIL_RE); assert.match(viaOperator, CONNECTION_NOT_GMAIL_RE); @@ -135,25 +135,25 @@ test("the 'is not' SOURCE toggle and -con: compile to the same exclusion (invers test("the 'is not' STREAM toggle and -stream: compile to the same exclusion", () => { const viaFacet = buildCompiledQuery({ + excludedStreams: ["messages"], + limit: 50, + order: "newest", parsed: parseQuery(""), selectedConnectionIds: [], selectedStreams: [], - excludedStreams: ["messages"], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); const viaOperator = buildCompiledQuery({ + limit: 50, + order: "newest", parsed: parseQuery("-stream:messages"), selectedConnectionIds: [], selectedStreams: [], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); assert.match(viaFacet, STREAM_NOT_MESSAGES_RE); assert.match(viaOperator, STREAM_NOT_MESSAGES_RE); @@ -207,12 +207,12 @@ test("a stream owned by two sources shows source A's loaded count under A and so entry("conn_b", "messages", "b2"), ]; const groups = computeSourceGroupedStreamFacets({ - feed, connections: [A, B], + excludeStreams: [], + feed, passes: ALL, selectedConnectionIds: [], selectedStreams: [], - excludeStreams: [], }); const a = groups.find((g) => g.connectionId === "conn_a"); const b = groups.find((g) => g.connectionId === "conn_b"); diff --git a/apps/console/src/app/(console)/explore/explore-feed-grouping.test.ts b/apps/console/src/app/(console)/explore/explore-feed-grouping.test.ts index 5d6a5a615..6610e9602 100644 --- a/apps/console/src/app/(console)/explore/explore-feed-grouping.test.ts +++ b/apps/console/src/app/(console)/explore/explore-feed-grouping.test.ts @@ -140,9 +140,9 @@ test("groupFeedWithBursts: burst key uses connectionId when present", () => { const n = BURST_THRESHOLD; const entries = Array.from({ length: n }, (_, i) => entry({ + connectionId: "cin_abc", displayAt: `${TODAY}T10:00:${String(i).padStart(2, "0")}Z`, recordId: `r${i}`, - connectionId: "cin_abc", stream: "messages", }) ); @@ -154,10 +154,10 @@ test("groupFeedWithBursts: burst key falls back to connectorId when connectionId const n = BURST_THRESHOLD; const entries = Array.from({ length: n }, (_, i) => entry({ - displayAt: `${TODAY}T10:00:${String(i).padStart(2, "0")}Z`, - recordId: `r${i}`, connectionId: null, connectorId: "whatsapp", + displayAt: `${TODAY}T10:00:${String(i).padStart(2, "0")}Z`, + recordId: `r${i}`, stream: "chats", }) ); @@ -170,16 +170,16 @@ test("groupFeedWithBursts: burst key falls back to connectorId when connectionId test("groupFeedWithBursts: burst partition is separated from singles in same day", () => { const burstEntries = Array.from({ length: BURST_THRESHOLD }, (_, i) => entry({ + connectionId: "cin_wa", displayAt: `${TODAY}T10:00:${String(i).padStart(2, "0")}Z`, recordId: `burst_${i}`, - connectionId: "cin_wa", stream: "messages", }) ); // Two singles from a different (connection, stream) partition const singleEntries = [ - entry({ displayAt: `${TODAY}T11:00:00Z`, recordId: "s1", connectionId: "cin_gmail", stream: "emails" }), - entry({ displayAt: `${TODAY}T11:01:00Z`, recordId: "s2", connectionId: "cin_gmail", stream: "emails" }), + entry({ connectionId: "cin_gmail", displayAt: `${TODAY}T11:00:00Z`, recordId: "s1", stream: "emails" }), + entry({ connectionId: "cin_gmail", displayAt: `${TODAY}T11:01:00Z`, recordId: "s2", stream: "emails" }), ]; const groups = groupFeedWithBursts([...burstEntries, ...singleEntries], NOW_MS); assert.equal(groups.length, 1); @@ -192,9 +192,9 @@ test("groupFeedWithBursts: two burst partitions in same day produce two burst gr const mkBurst = (stream: string, connId: string) => Array.from({ length: BURST_THRESHOLD }, (_, i) => entry({ + connectionId: connId, displayAt: `${TODAY}T10:00:${String(i).padStart(2, "0")}Z`, recordId: `${stream}_${i}`, - connectionId: connId, stream, }) ); @@ -207,14 +207,14 @@ test("groupFeedWithBursts: two burst partitions in same day produce two burst gr test("groupFeedWithBursts: bursts in one day, singles in another day", () => { const burstEntries = Array.from({ length: BURST_THRESHOLD }, (_, i) => entry({ + connectionId: "cin_wa", displayAt: `${TODAY}T10:00:${String(i).padStart(2, "0")}Z`, recordId: `b${i}`, - connectionId: "cin_wa", stream: "messages", }) ); const singles = [ - entry({ displayAt: `${YESTERDAY}T09:00:00Z`, recordId: "y1", connectionId: "cin_gmail", stream: "emails" }), + entry({ connectionId: "cin_gmail", displayAt: `${YESTERDAY}T09:00:00Z`, recordId: "y1", stream: "emails" }), ]; const groups = groupFeedWithBursts([...burstEntries, ...singles], NOW_MS); assert.equal(groups.length, 2); @@ -256,11 +256,11 @@ test("isFutureDay is true strictly after today, false for today/past/empty", () test("partitionFeedByTime splits future records into Upcoming, leaving today/past in the main feed", () => { const feed = [ // Future (YNAB-shaped budget months), arriving newest-first (farthest-out first) - entry({ displayAt: "2026-08-01T00:00:00Z", recordId: "aug", connectorId: "ynab", stream: "months" }), - entry({ displayAt: "2026-07-01T00:00:00Z", recordId: "jul", connectorId: "ynab", stream: "months" }), + entry({ connectorId: "ynab", displayAt: "2026-08-01T00:00:00Z", recordId: "aug", stream: "months" }), + entry({ connectorId: "ynab", displayAt: "2026-07-01T00:00:00Z", recordId: "jul", stream: "months" }), // Today + past - entry({ displayAt: `${TODAY}T14:00:00Z`, recordId: "t1", connectorId: "claude-code" }), - entry({ displayAt: `${YESTERDAY}T09:00:00Z`, recordId: "y1", connectorId: "gmail", stream: "emails" }), + entry({ connectorId: "claude-code", displayAt: `${TODAY}T14:00:00Z`, recordId: "t1" }), + entry({ connectorId: "gmail", displayAt: `${YESTERDAY}T09:00:00Z`, recordId: "y1", stream: "emails" }), ]; const { upcoming, upcomingCount, past } = partitionFeedByTime(feed, NOW_MS); @@ -311,7 +311,7 @@ test("groupFeedDaysNoBursts: a same-partition day over the burst threshold stays // (it is already a collapsed disclosure — a second "expand" inside it is the clunk). const day = `${TODAY}T10:00:00Z`; const feed = Array.from({ length: BURST_THRESHOLD + 54 }, (_, i) => - entry({ displayAt: day, recordId: `m${i}`, stream: "month_categories", connectionId: "cin_ynab" }) + entry({ connectionId: "cin_ynab", displayAt: day, recordId: `m${i}`, stream: "month_categories" }) ); // Control: the normal grouping DOES burst (proving the input is burst-eligible). @@ -384,7 +384,7 @@ test("8.2: a day-group that crosses BURST_THRESHOLD still yields visible content // load-more scenario that previously produced a zero-row collapsed burst. const day = `${TODAY}T10:00:00Z`; const feed = Array.from({ length: BURST_THRESHOLD + 86 }, (_, i) => - entry({ displayAt: day, recordId: `m${i}`, stream: "messages", connectionId: "cin_wa" }) + entry({ connectionId: "cin_wa", displayAt: day, recordId: `m${i}`, stream: "messages" }) ); const days = groupFeedWithBursts(feed, NOW_MS); assert.equal(days.length, 1, "one day group"); @@ -403,7 +403,7 @@ test("8.3: preview-reachability — the burst count label number == burst.entrie const day = `${TODAY}T10:00:00Z`; const loaded = BURST_THRESHOLD + 33; const feed = Array.from({ length: loaded }, (_, i) => - entry({ displayAt: day, recordId: `m${i}`, stream: "messages", connectionId: "cin_wa" }) + entry({ connectionId: "cin_wa", displayAt: day, recordId: `m${i}`, stream: "messages" }) ); const burst = groupFeedWithBursts(feed, NOW_MS)[0]?.bursts[0]; assert.ok(burst, "burst exists"); @@ -435,9 +435,9 @@ test("8.3: preview-reachability — the burst count label number == burst.entrie function burstAt(opts: { stream: string; connectionId: string; isos: string[] }): ExplorerFeedEntry[] { return opts.isos.map((iso, i) => entry({ + connectionId: opts.connectionId, displayAt: iso, recordId: `${opts.stream}-${i}`, - connectionId: opts.connectionId, stream: opts.stream, }) ); @@ -447,7 +447,7 @@ function burstAt(opts: { stream: string; connectionId: string; isos: string[] }) * burst's latest member matters for ordering. */ function burstNewestAt(stream: string, connectionId: string, newestIso: string): ExplorerFeedEntry[] { // 10 members at the same instant => latestAt = newestIso, crosses BURST_THRESHOLD. - return burstAt({ stream, connectionId, isos: Array.from({ length: BURST_THRESHOLD }, () => newestIso) }); + return burstAt({ connectionId, isos: Array.from({ length: BURST_THRESHOLD }, () => newestIso), stream }); } /** The `latestAt` of each unit, in render order. */ @@ -497,11 +497,11 @@ test("burst latestAt = NEWEST member even when burst members arrive shuffled (no "2026-06-19T14:33:00Z", "2026-06-19T14:40:00Z", ]; - const feed = burstAt({ stream: "messages", connectionId: "cin_shuffled", isos }); + const feed = burstAt({ connectionId: "cin_shuffled", isos, stream: "messages" }); const groups = groupFeedWithBursts(feed, NOW_MS); const units = groups[0]?.units ?? []; assert.equal(units.length, 1, "one burst unit"); - const unit = units[0]; + const [unit] = units; assert.ok(unit && unit.kind === "burst", "the unit is a burst"); // latestAt is the NEWEST member, not the first array member. assert.equal(unit.latestAt, "2026-06-19T14:41:00Z", "latestAt = newest member, not entries[0] of input"); @@ -520,7 +520,7 @@ test("burst order: a single interleaves with bursts by time (single at 25m betwe const feed = [ // Feed order deliberately not display order: the single arrives first, the older // burst before the newer one — the fix must still produce strict DESC. - entry({ displayAt: ago25, recordId: "single-25", connectionId: "cin_gmail", stream: "emails" }), + entry({ connectionId: "cin_gmail", displayAt: ago25, recordId: "single-25", stream: "emails" }), ...burstNewestAt("cc_messages", "cin_cc", ago31), // older burst ...burstNewestAt("codex_messages", "cin_codex", ago23), // newer burst ]; @@ -548,7 +548,7 @@ test("burst order: members within a burst stay newest-first (descending input pr "2026-06-19T14:33:00Z", "2026-06-19T14:32:00Z", ]; - const feed = burstAt({ stream: "messages", connectionId: "cin_wa", isos: isosDesc }); + const feed = burstAt({ connectionId: "cin_wa", isos: isosDesc, stream: "messages" }); const burst = groupFeedWithBursts(feed, NOW_MS)[0]?.units[0]; assert.ok(burst, "the over-threshold partition produced a unit"); if (burst.kind !== "burst") { @@ -568,7 +568,7 @@ test("burst order: equal-latestAt units keep deterministic ORIGINAL feed order ( // order they first appeared in the feed: burstX, single, burstY. const feed = [ ...burstNewestAt("x", "cin_x", sameIso), - entry({ displayAt: sameIso, recordId: "tie-single", connectionId: "cin_s", stream: "single" }), + entry({ connectionId: "cin_s", displayAt: sameIso, recordId: "tie-single", stream: "single" }), ...burstNewestAt("y", "cin_y", sameIso), ]; const units = groupFeedWithBursts(feed, NOW_MS)[0]?.units ?? []; @@ -589,9 +589,9 @@ test("burst order: an undated unit sorts AFTER all dated units within the day (n const ago20 = "2026-06-19T14:40:00Z"; const feed = [ // Undated single arrives FIRST in the feed but must sink to the bottom of the day. - { ...entry({ recordId: "undated", connectionId: "cin_u", stream: "misc" }), displayAt: "" } as ExplorerFeedEntry, + { ...entry({ connectionId: "cin_u", recordId: "undated", stream: "misc" }), displayAt: "" } as ExplorerFeedEntry, ...burstNewestAt("dated", "cin_d", ago20), - entry({ displayAt: ago20, recordId: "dated-single", connectionId: "cin_ds", stream: "emails" }), + entry({ connectionId: "cin_ds", displayAt: ago20, recordId: "dated-single", stream: "emails" }), ]; const groups = groupFeedWithBursts(feed, NOW_MS); // The undated "" day buckets separately from the 2026-06-19 day. Within the DATED @@ -606,12 +606,12 @@ test("burst order: an undated unit sorts AFTER all dated units within the day (n // Now force an undated unit to share a day with dated units (unparseable displayAt // that still slices to a same-day prefix is impossible, so construct directly). const mixed = [ - entry({ displayAt: ago20, recordId: "d1", connectionId: "cin_a", stream: "a" }), + entry({ connectionId: "cin_a", displayAt: ago20, recordId: "d1", stream: "a" }), { - ...entry({ recordId: "u1", connectionId: "cin_b", stream: "b" }), + ...entry({ connectionId: "cin_b", recordId: "u1", stream: "b" }), displayAt: "2026-06-19TBROKEN", } as ExplorerFeedEntry, - entry({ displayAt: "2026-06-19T14:41:00Z", recordId: "d2", connectionId: "cin_c", stream: "c" }), + entry({ connectionId: "cin_c", displayAt: "2026-06-19T14:41:00Z", recordId: "d2", stream: "c" }), ]; const mixedDay = groupFeedWithBursts(mixed, NOW_MS).find((g) => g.day === "2026-06-19"); assert.ok(mixedDay, "mixed same-day group exists"); @@ -628,9 +628,9 @@ test("burst order SCOPE GUARD: groupFeedDaysNoBursts (Upcoming path) stays FLAT // Upcoming arrives soonest-first (forward-chron). The units must MIRROR that input // order verbatim — never get newest-first re-sorted like the past feed. const feed = [ - entry({ displayAt: "2026-06-20T09:00:00Z", recordId: "soon", connectorId: "ynab", stream: "months" }), - entry({ displayAt: "2026-06-20T11:00:00Z", recordId: "later-same-day", connectorId: "ynab", stream: "months" }), - entry({ displayAt: "2026-07-01T09:00:00Z", recordId: "next-month", connectorId: "ynab", stream: "months" }), + entry({ connectorId: "ynab", displayAt: "2026-06-20T09:00:00Z", recordId: "soon", stream: "months" }), + entry({ connectorId: "ynab", displayAt: "2026-06-20T11:00:00Z", recordId: "later-same-day", stream: "months" }), + entry({ connectorId: "ynab", displayAt: "2026-07-01T09:00:00Z", recordId: "next-month", stream: "months" }), ]; const days = groupFeedDaysNoBursts(feed, NOW_MS); // Days stay in input (forward-chron) order, every unit a single, no bursts. @@ -660,17 +660,17 @@ test("burst order SCOPE GUARD: groupFeedDaysNoBursts (Upcoming path) stays FLAT test("burst order INVARIANT: scanning units top-to-bottom, latestAt is monotonic non-increasing (dated), undated last", () => { // A realistic mixed day: bursts + singles at scattered times, fed in non-display order. const feed = [ - entry({ displayAt: "2026-06-19T10:00:00Z", recordId: "s-10", connectionId: "cin_1a", stream: "a" }), + entry({ connectionId: "cin_1a", displayAt: "2026-06-19T10:00:00Z", recordId: "s-10", stream: "a" }), ...burstNewestAt("b1", "cin_b1", "2026-06-19T14:00:00Z"), - entry({ displayAt: "2026-06-19T12:30:00Z", recordId: "s-1230", connectionId: "cin_2a", stream: "b" }), + entry({ connectionId: "cin_2a", displayAt: "2026-06-19T12:30:00Z", recordId: "s-1230", stream: "b" }), ...burstNewestAt("b2", "cin_b2", "2026-06-19T08:15:00Z"), - { ...entry({ recordId: "u", connectionId: "cin_u", stream: "u" }), displayAt: "" } as ExplorerFeedEntry, + { ...entry({ connectionId: "cin_u", recordId: "u", stream: "u" }), displayAt: "" } as ExplorerFeedEntry, ...burstNewestAt("b3", "cin_b3", "2026-06-19T13:45:00Z"), ]; // Put the undated entry into the SAME day as the dated ones via a broken-but-prefixed // displayAt so it shares the 2026-06-19 bucket. feed[4] = { - ...entry({ recordId: "u", connectionId: "cin_u", stream: "u" }), + ...entry({ connectionId: "cin_u", recordId: "u", stream: "u" }), displayAt: "2026-06-19Tnonsense", } as ExplorerFeedEntry; const day = groupFeedWithBursts(feed, NOW_MS).find((g) => g.day === "2026-06-19"); diff --git a/apps/console/src/app/(console)/explore/explore-feed-grouping.ts b/apps/console/src/app/(console)/explore/explore-feed-grouping.ts index f70cfab19..944ec072c 100644 --- a/apps/console/src/app/(console)/explore/explore-feed-grouping.ts +++ b/apps/console/src/app/(console)/explore/explore-feed-grouping.ts @@ -16,11 +16,11 @@ import type { ExplorerFeedEntry } from "@pdpp/operator-ui/components/views/explo // ─── Day label ─────────────────────────────────────────────────────────────── const DAY_FMT = new Intl.DateTimeFormat("en-US", { - weekday: "long", - year: "numeric", - month: "long", day: "numeric", + month: "long", timeZone: "UTC", + weekday: "long", + year: "numeric", }); export const MS_PER_DAY = 86_400_000; @@ -46,10 +46,10 @@ export function dayLabel(day: string, nowMs: number = Date.now()): string { if (Number.isNaN(ms)) { return "Undated"; } - const todayKey = new Date(nowMs).toISOString().slice(0, 10); + const currentDayKey = new Date(nowMs).toISOString().slice(0, 10); const yesterdayKey = new Date(nowMs - MS_PER_DAY).toISOString().slice(0, 10); const tomorrowKey = new Date(nowMs + MS_PER_DAY).toISOString().slice(0, 10); - if (day === todayKey) { + if (day === currentDayKey) { return "Today"; } if (day === yesterdayKey) { @@ -167,7 +167,7 @@ function entryLatestAt(e: ExplorerFeedEntry): string | null { */ function sortEntriesNewestFirst(entries: ExplorerFeedEntry[]): ExplorerFeedEntry[] { return entries - .map((entry, index) => ({ entry, index, at: entryLatestAt(entry) })) + .map((entry, index) => ({ at: entryLatestAt(entry), entry, index })) .sort((a, b) => { if (a.at === b.at) { return a.index - b.index; @@ -193,7 +193,7 @@ function sortEntriesNewestFirst(entries: ExplorerFeedEntry[]): ExplorerFeedEntry */ function orderDayUnits(units: DayRenderUnit[]): DayRenderUnit[] { return units - .map((unit, index) => ({ unit, index })) + .map((unit, index) => ({ index, unit })) .sort((a, b) => { const aAt = a.unit.latestAt; const bAt = b.unit.latestAt; @@ -246,14 +246,14 @@ function splitDayBursts(day: string, entries: ExplorerFeedEntry[], nowMs: number existing.entries.push(e); } else { // First member of this burst — reserve the burst's slot in feed order. - const burst: BurstGroup = { key: pk, entries: [e], preview: [], expanded: false }; + const burst: BurstGroup = { entries: [e], expanded: false, key: pk, preview: [] }; burstByKey.set(pk, burst); bursts.push(burst); - orderedUnits.push({ kind: "burst", burst, latestAt: null }); + orderedUnits.push({ burst, kind: "burst", latestAt: null }); } } else { singles.push(e); - orderedUnits.push({ kind: "single", entry: e, latestAt: entryLatestAt(e) }); + orderedUnits.push({ entry: e, kind: "single", latestAt: entryLatestAt(e) }); } } @@ -271,7 +271,7 @@ function splitDayBursts(day: string, entries: ExplorerFeedEntry[], nowMs: number } } - return { day, label: dayLabel(day, nowMs), singles, bursts, units: orderDayUnits(orderedUnits) }; + return { bursts, day, label: dayLabel(day, nowMs), singles, units: orderDayUnits(orderedUnits) }; } /** Group entries into ordered day buckets (insertion order preserved → already-sorted feed). */ @@ -325,11 +325,11 @@ export function groupFeedDaysNoBursts( // design — do NOT newest-first re-sort it. Every entry is a flat single, and // `units` preserves the incoming order verbatim (no orderDayUnits call). const units: DayRenderUnit[] = entries.map((entry) => ({ - kind: "single", entry, + kind: "single", latestAt: entryLatestAt(entry), })); - days.push({ day, label: dayLabel(day, nowMs), singles: entries, bursts: [], units }); + days.push({ bursts: [], day, label: dayLabel(day, nowMs), singles: entries, units }); } return days; } @@ -376,5 +376,5 @@ export function partitionFeedByTime(feed: readonly ExplorerFeedEntry[], nowMs: n const upcoming = groupDays(upcomingSorted, nowMs); const past = groupDays(pastEntries, nowMs); - return { upcoming, upcomingCount: futureEntries.length, past }; + return { past, upcoming, upcomingCount: futureEntries.length }; } diff --git a/apps/console/src/app/(console)/explore/explore-frontend-review-hold-fixes.test.ts b/apps/console/src/app/(console)/explore/explore-frontend-review-hold-fixes.test.ts index aea04a616..2ed87c3c5 100644 --- a/apps/console/src/app/(console)/explore/explore-frontend-review-hold-fixes.test.ts +++ b/apps/console/src/app/(console)/explore/explore-frontend-review-hold-fixes.test.ts @@ -58,13 +58,13 @@ function makeTimelineRecord(over: { emitted_at?: string; }): ExploreTimelineRecord { return { - object: "timeline_record", connector_id: over.connector_id, connector_instance_id: over.connector_instance_id, - stream: over.stream ?? "records", - record_key: over.record_key ?? "rec-1", - emitted_at: over.emitted_at ?? "2026-01-01T00:00:00Z", data: { title: "test" }, + emitted_at: over.emitted_at ?? "2026-01-01T00:00:00Z", + object: "timeline_record", + record_key: over.record_key ?? "rec-1", + stream: over.stream ?? "records", }; } @@ -76,12 +76,12 @@ function makeTimelinePage( } ): ExploreTimelinePage { return { - object: "list", data: records, has_more: opts?.has_more ?? false, + new_since_snapshot: 0, next_cursor: opts?.next_cursor ?? null, + object: "list", snapshot_at: "2026-06-19T00:00:00Z", - new_since_snapshot: 0, }; } @@ -93,13 +93,13 @@ function makeHit(over: { emitted_at?: string; }): SearchResultHit { return { - connector_id: over.connector_id, connection_id: over.connection_id, - stream: over.stream ?? "records", - record_key: over.record_key ?? "rec-1", + connector_id: over.connector_id, emitted_at: over.emitted_at ?? "2026-01-01T00:00:00Z", matched_fields: [], object: "search_result", + record_key: over.record_key ?? "rec-1", + stream: over.stream ?? "records", }; } @@ -132,32 +132,32 @@ function notStubbed(name: string): Promise { function makeDataSource(overrides: Partial): DashboardDataSource { const stub: DashboardDataSource = { - kind: "sandbox" as const, aggregateRecordsByTime: () => notStubbed("aggregateRecordsByTime"), - listExploreRecordBuckets: () => notStubbed("listExploreRecordBuckets"), - isHybridRetrievalAdvertised: () => Promise.resolve(false), - isSemanticRetrievalAdvertised: () => Promise.resolve(false), - listConnectorSummaries: () => notStubbed("listConnectorSummaries"), - listConnectorManifests: () => notStubbed("listConnectorManifests"), - searchRecordsLexical: () => notStubbed("searchRecordsLexical"), - searchRecordsHybrid: () => notStubbed("searchRecordsHybrid"), - searchRecordsSemantic: () => notStubbed("searchRecordsSemantic"), - queryRecords: () => notStubbed("queryRecords"), - getRecord: () => notStubbed("getRecord"), getConnectorOverview: () => notStubbed("getConnectorOverview"), + getDatasetSummary: () => notStubbed("getDatasetSummary"), + getDeploymentDiagnostics: () => notStubbed("getDeploymentDiagnostics"), + getGrantTimeline: () => notStubbed("getGrantTimeline"), + getRecord: () => notStubbed("getRecord"), + getRunTimeline: () => notStubbed("getRunTimeline"), getStreamMetadata: () => notStubbed("getStreamMetadata"), getTraceTimeline: () => notStubbed("getTraceTimeline"), + isHybridRetrievalAdvertised: () => Promise.resolve(false), + isSemanticRetrievalAdvertised: () => Promise.resolve(false), + kind: "sandbox" as const, + listConnectorManifests: () => notStubbed("listConnectorManifests"), + listConnectorSummaries: () => notStubbed("listConnectorSummaries"), + listExploreRecordBuckets: () => notStubbed("listExploreRecordBuckets"), + listExploreTimeline: () => Promise.resolve(makeTimelinePage([])), listGrants: () => notStubbed("listGrants"), listPendingApprovals: () => notStubbed("listPendingApprovals"), listRuns: () => notStubbed("listRuns"), listStreams: () => notStubbed("listStreams"), listTraces: () => notStubbed("listTraces"), + queryRecords: () => notStubbed("queryRecords"), refSearch: () => notStubbed("refSearch"), - listExploreTimeline: () => Promise.resolve(makeTimelinePage([])), - getDatasetSummary: () => notStubbed("getDatasetSummary"), - getDeploymentDiagnostics: () => notStubbed("getDeploymentDiagnostics"), - getGrantTimeline: () => notStubbed("getGrantTimeline"), - getRunTimeline: () => notStubbed("getRunTimeline"), + searchRecordsHybrid: () => notStubbed("searchRecordsHybrid"), + searchRecordsLexical: () => notStubbed("searchRecordsLexical"), + searchRecordsSemantic: () => notStubbed("searchRecordsSemantic"), ...overrides, }; return stub; @@ -182,35 +182,35 @@ test("F1 (P1): recent lens filters by selected connection — YNAB records do no connection_id: "cin_amazon_1", connector_id: "amazon", connector_instance_id: "cin_amazon_1", - streams: ["orders"], display_name: "Amazon", + streams: ["orders"], }); const ynabSummary = makeSummary({ connection_id: "cin_ynab_1", connector_id: "ynab", connector_instance_id: "cin_ynab_1", - streams: ["transactions"], display_name: "YNAB", + streams: ["transactions"], }); // The endpoint returns records from BOTH connections (cross-source leak scenario). const amazonRecord = makeTimelineRecord({ connector_id: "amazon", connector_instance_id: "cin_amazon_1", - stream: "orders", record_key: "order-1", + stream: "orders", }); const ynabRecord = makeTimelineRecord({ connector_id: "ynab", connector_instance_id: "cin_ynab_1", - stream: "transactions", record_key: "txn-1", + stream: "transactions", }); const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([amazonSummary, ynabSummary])), listConnectorManifests: () => Promise.resolve([makeManifest("amazon", ["orders"]), makeManifest("ynab", ["transactions"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([amazonSummary, ynabSummary])), listExploreTimeline: (opts) => { assert.deepEqual( opts?.connectionIds, @@ -257,30 +257,23 @@ test("F2 (P0): single-stream Most-recent calls lexical (not queryRecords) — on connection_id: "cin_amazon_1", connector_id: "amazon", connector_instance_id: "cin_amazon_1", - streams: ["orders"], display_name: "Amazon", + streams: ["orders"], }); // Only one matching hit (connector+stream all same so streamDoor fires). const matchingHit = makeHit({ + connection_id: "cin_amazon_1", connector_id: "amazon", - stream: "orders", record_key: "matching-order", - connection_id: "cin_amazon_1", + stream: "orders", }); let queryRecordsCalled = false; let lexicalCalledWithStream = false; const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), listConnectorManifests: () => Promise.resolve([makeManifest("amazon", ["orders"])]), - // Lexical must be called for BOTH stream-door probe AND the display results. - searchRecordsLexical: (_q, opts) => { - if (opts?.streams?.includes("orders")) { - lexicalCalledWithStream = true; - } - return Promise.resolve(makeLexicalPage([matchingHit], { has_more: false })); - }, + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), // PRE-FIX: assembler called queryRecords here. If we stub it to NOT throw, // all records (matching or not) would be returned. We stub it to throw so // the pre-fix behavior is caught as a test failure. @@ -290,6 +283,13 @@ test("F2 (P0): single-stream Most-recent calls lexical (not queryRecords) — on new Error("queryRecords must not be called for Most-recent single-stream (F2 reproduce bug)") ); }, + // Lexical must be called for BOTH stream-door probe AND the display results. + searchRecordsLexical: (_q, opts) => { + if (opts?.streams?.includes("orders")) { + lexicalCalledWithStream = true; + } + return Promise.resolve(makeLexicalPage([matchingHit], { has_more: false })); + }, }); const result = await assembleExplorerData({ q: "hdmi cable", search_sort: "recent" }, ds, "https://rs.test"); @@ -327,26 +327,26 @@ test("F2 (P0): multi-stream Most-recent wires lexical cursor — searchHasMore r const summaryA = makeSummary({ connection_id: "cin_ynab_1", connector_id: "ynab", streams: ["transactions"] }); const summaryB = makeSummary({ connection_id: "cin_amazon_1", connector_id: "amazon", streams: ["orders"] }); const hitA = makeHit({ - connector_id: "ynab", - stream: "transactions", connection_id: "cin_ynab_1", + connector_id: "ynab", record_key: "ynab-rec", + stream: "transactions", }); const hitB = makeHit({ - connector_id: "amazon", - stream: "orders", connection_id: "cin_amazon_1", + connector_id: "amazon", record_key: "amz-rec", + stream: "orders", }); const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summaryA, summaryB])), listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"]), makeManifest("amazon", ["orders"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summaryA, summaryB])), + queryRecords: () => Promise.reject(new Error("queryRecords must not be called for multi-stream Most-recent")), // Lexical returns has_more=true with a cursor to simulate a deep result set. searchRecordsLexical: () => Promise.resolve(makeLexicalPage([hitA, hitB], { has_more: true, next_cursor: "lex-cursor-99" })), - queryRecords: () => Promise.reject(new Error("queryRecords must not be called for multi-stream Most-recent")), }); const result = await assembleExplorerData({ q: "payment", search_sort: "recent" }, ds, "https://rs.test"); @@ -391,28 +391,28 @@ test("F3 (P1): timelineRecordToEntry uses connector_instance_id to resolve corre connection_id: "cin_ynab_personal", connector_id: "ynab", connector_instance_id: "cin_ynab_personal", - streams: ["transactions"], display_name: "YNAB Personal", + streams: ["transactions"], }); const workSummary = makeSummary({ connection_id: "cin_ynab_work", connector_id: "ynab", connector_instance_id: "cin_ynab_work", - streams: ["transactions"], display_name: "YNAB Work", + streams: ["transactions"], }); // Record belongs to the WORK instance. const workRecord = makeTimelineRecord({ connector_id: "ynab", // type: "ynab" connector_instance_id: "cin_ynab_work", // instance: work - stream: "transactions", record_key: "work-txn-1", + stream: "transactions", }); const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([personalSummary, workSummary])), listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([personalSummary, workSummary])), // The endpoint returns a record whose connector_instance_id = "cin_ynab_work". listExploreTimeline: () => Promise.resolve(makeTimelinePage([workRecord])), }); @@ -420,7 +420,7 @@ test("F3 (P1): timelineRecordToEntry uses connector_instance_id to resolve corre const result = await assembleExplorerData({}, ds, "https://rs.test"); assert.equal(result.feed.length, 1); - const entry = result.feed[0]; + const [entry] = result.feed; assert.ok(entry, "Feed must have one entry"); // PRE-FIX: entry.connectionId === "cin_ynab_personal" (wrong — first type match wins). @@ -465,21 +465,21 @@ test("F3 (P1): connectionId falls back to connector_instance_id (not connector_i connection_id: "cin_amazon_1", connector_id: "amazon", connector_instance_id: "cin_amazon_1", - streams: ["orders"], display_name: "Amazon", + streams: ["orders"], }); // Record from "twitter" — no twitter summary exists in the owner's connections. const twitterRecord = makeTimelineRecord({ connector_id: "twitter", connector_instance_id: "cin_twitter_unknown", - stream: "tweets", record_key: "tweet-999", + stream: "tweets", }); const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([amazonSummary])), listConnectorManifests: () => Promise.resolve([makeManifest("amazon", ["orders"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([amazonSummary])), listExploreTimeline: () => Promise.resolve(makeTimelinePage([twitterRecord])), }); @@ -487,7 +487,7 @@ test("F3 (P1): connectionId falls back to connector_instance_id (not connector_i // The record still appears (we do not drop records whose connection is unknown). assert.equal(result.feed.length, 1); - const entry = result.feed[0]; + const [entry] = result.feed; assert.ok(entry, "Feed must have one entry"); // PRE-FIX: entry.connectionId === "twitter" (the connector_id TYPE was used as fallback, diff --git a/apps/console/src/app/(console)/explore/explore-grammar.test.ts b/apps/console/src/app/(console)/explore/explore-grammar.test.ts index 83e1b2ed1..5c35e12bc 100644 --- a/apps/console/src/app/(console)/explore/explore-grammar.test.ts +++ b/apps/console/src/app/(console)/explore/explore-grammar.test.ts @@ -84,27 +84,27 @@ const STREAM_NOT_BUDGET_RE = /stream!=budget_months/; test("CHIP == OPERATOR for exclusion: the facet 'is not' and -con: compile to the SAME query", () => { // Typing the operator -con:ynab: const viaOperator = buildCompiledQuery({ + limit: 50, + order: "newest", parsed: parseQuery("-con:ynab"), selectedConnectionIds: [], selectedStreams: [], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); // Clicking the "is not" chip for the same connection (excludedConnectionIds): const viaChip = buildCompiledQuery({ + excludedConnectionIds: ["ynab"], + excludedStreams: [], + limit: 50, + order: "newest", parsed: parseQuery(""), selectedConnectionIds: [], selectedStreams: [], - excludedConnectionIds: ["ynab"], - excludedStreams: [], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); // Both produce the identical exclusion param (chip == operator). assert.match(viaOperator, CONNECTION_NOT_YNAB_RE); @@ -157,25 +157,25 @@ test("liftDateTokens: last-write-wins on a repeated operator (no stacking)", () test("buildCompiledQuery renders excluded streams as stream!= (chip == -stream: operator)", () => { const viaOperator = buildCompiledQuery({ + limit: 50, + order: "newest", parsed: parseQuery("-stream:budget_months"), selectedConnectionIds: [], selectedStreams: [], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); const viaChip = buildCompiledQuery({ + excludedStreams: ["budget_months"], + limit: 50, + order: "newest", parsed: parseQuery(""), selectedConnectionIds: [], selectedStreams: [], - excludedStreams: ["budget_months"], serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); assert.match(viaOperator, STREAM_NOT_BUDGET_RE); assert.match(viaChip, STREAM_NOT_BUDGET_RE); @@ -202,6 +202,8 @@ test("hasClientSideTokens treats a declared exact-filterable field as server-sid test("buildCompiledQuery renders server params plainly and undeclared field tokens behind a comment", () => { const line = buildCompiledQuery({ + limit: 50, + order: "newest", parsed: parseQuery("con:gmail stream:messages has:image merchant:coffee coffee"), selectedConnectionIds: [], selectedStreams: [], @@ -209,8 +211,6 @@ test("buildCompiledQuery renders server params plainly and undeclared field toke serverFilterableFields: new Set(), since: "", until: "", - order: "newest", - limit: 50, }); assert.match(line, GET_RECORDS_RE); assert.match(line, CONNECTION_GMAIL_RE); @@ -228,6 +228,8 @@ test("buildCompiledQuery renders server params plainly and undeclared field toke test("buildCompiledQuery promotes a declared exact-filterable field:value to a server filter[] param", () => { const line = buildCompiledQuery({ + limit: 50, + order: "newest", parsed: parseQuery("merchant:coffee has:image"), selectedConnectionIds: [], selectedStreams: [], @@ -235,8 +237,6 @@ test("buildCompiledQuery promotes a declared exact-filterable field:value to a s serverFilterableFields: new Set(["merchant"]), since: "", until: "", - order: "newest", - limit: 50, }); // merchant:coffee renders as a real server param in the server section. const [serverPart] = line.split("# client-side:"); @@ -251,14 +251,14 @@ test("buildCompiledQuery promotes a declared exact-filterable field:value to a s test("buildCompiledQuery prefers facet-selected ids and maps before/after to since/until", () => { const line = buildCompiledQuery({ + limit: 50, + order: "oldest", parsed: parseQuery("before:2026-06-11 after:2026-06-10"), selectedConnectionIds: ["conn_abc"], selectedStreams: ["transactions"], serverFilterableFields: new Set(), since: "", until: "", - order: "oldest", - limit: 50, }); assert.match(line, CONNECTION_ABC_RE); assert.match(line, STREAM_TX_RE); diff --git a/apps/console/src/app/(console)/explore/explore-grammar.ts b/apps/console/src/app/(console)/explore/explore-grammar.ts index b71f767a1..afff4f3e3 100644 --- a/apps/console/src/app/(console)/explore/explore-grammar.ts +++ b/apps/console/src/app/(console)/explore/explore-grammar.ts @@ -105,84 +105,84 @@ function applyKvToken(out: ParsedQuery, tok: string, negated: boolean, key: stri const lower = value.toLowerCase(); if (negated && key === "con") { out.conNot = lower; - out.tokens.push({ raw: tok, label: `not in ${value}` }); + out.tokens.push({ label: `not in ${value}`, raw: tok }); return; } if (negated && key === "stream") { out.streamNot = lower; - out.tokens.push({ raw: tok, label: `not stream: ${value}` }); + out.tokens.push({ label: `not stream: ${value}`, raw: tok }); return; } switch (key) { case "con": out.con = lower; - out.tokens.push({ raw: tok, label: `in ${value}` }); + out.tokens.push({ label: `in ${value}`, raw: tok }); break; case "stream": out.stream = lower; - out.tokens.push({ raw: tok, label: `stream: ${value}` }); + out.tokens.push({ label: `stream: ${value}`, raw: tok }); break; case "role": out.role = lower; - out.tokens.push({ raw: tok, label: `role: ${value}` }); + out.tokens.push({ label: `role: ${value}`, raw: tok }); break; case "has": if (lower === "image") { out.hasImage = true; - out.tokens.push({ raw: tok, label: "has image" }); + out.tokens.push({ label: "has image", raw: tok }); } else if (lower === "link") { out.hasLink = true; - out.tokens.push({ raw: tok, label: "has link" }); + out.tokens.push({ label: "has link", raw: tok }); } else { out.fields.push({ key, value: lower }); - out.tokens.push({ raw: tok, label: `${key}: ${value}` }); + out.tokens.push({ label: `${key}: ${value}`, raw: tok }); } break; case "is": if (lower === "folded") { out.folded = true; - out.tokens.push({ raw: tok, label: "folded" }); + out.tokens.push({ label: "folded", raw: tok }); } else { out.fields.push({ key, value: lower }); - out.tokens.push({ raw: tok, label: `${key}: ${value}` }); + out.tokens.push({ label: `${key}: ${value}`, raw: tok }); } break; case "before": out.before = value; - out.tokens.push({ raw: tok, label: `before ${value}` }); + out.tokens.push({ label: `before ${value}`, raw: tok }); break; case "after": out.after = value; - out.tokens.push({ raw: tok, label: `after ${value}` }); + out.tokens.push({ label: `after ${value}`, raw: tok }); break; default: out.fields.push({ key, value: lower }); - out.tokens.push({ raw: tok, label: `${key}: ${value}` }); + out.tokens.push({ label: `${key}: ${value}`, raw: tok }); } } /** Parse a raw query string into structured tokens. Pure; never throws. */ export function parseQuery(input: string): ParsedQuery { const out: ParsedQuery = { - text: [], + after: null, + before: null, con: null, conNot: null, - stream: null, - streamNot: null, - role: null, + fields: [], + folded: false, hasImage: false, hasLink: false, - folded: false, - before: null, - after: null, - fields: [], + role: null, + stream: null, + streamNot: null, + text: [], tokens: [], }; for (const tok of input.trim().split(WHITESPACE_RE).filter(Boolean)) { const m = tok.match(KV_RE); if (!m) { out.text.push(tok.toLowerCase()); - out.tokens.push({ raw: tok, label: tok }); + out.tokens.push({ label: tok, raw: tok }); continue; } applyKvToken(out, tok, m[1] === "-", (m[2] ?? "").toLowerCase(), m[3] ?? ""); @@ -263,10 +263,10 @@ export interface QueryFacetLift { */ export function liftFacetTokens(query: string): QueryFacetLift { const lift: QueryFacetLift = { - includeConnections: [], excludeConnections: [], - includeStreams: [], excludeStreams: [], + includeConnections: [], + includeStreams: [], rest: "", }; const kept: string[] = []; @@ -405,7 +405,7 @@ function splitFieldFilters( clientFields.push(f); } } - return { serverFilters, clientFields }; + return { clientFields, serverFilters }; } /** @@ -457,18 +457,18 @@ export function buildCompiledQuery(input: CompiledQueryInput): string { // Connection/stream include + exclude scope (the chip toggle and the operator // render identically; exclusion is a first-class `!=` scope param). ...renderScopeParams({ - include: selectedConnectionIds, exclude: input.excludedConnectionIds ?? [], - tokenInclude: parsed.con, - tokenExclude: parsed.conNot, + include: selectedConnectionIds, param: "connection", + tokenExclude: parsed.conNot, + tokenInclude: parsed.con, }), ...renderScopeParams({ - include: selectedStreams, exclude: input.excludedStreams ?? [], - tokenInclude: parsed.stream, - tokenExclude: parsed.streamNot, + include: selectedStreams, param: "stream", + tokenExclude: parsed.streamNot, + tokenInclude: parsed.stream, }), ]; diff --git a/apps/console/src/app/(console)/explore/explore-navigation.test.ts b/apps/console/src/app/(console)/explore/explore-navigation.test.ts index bb5af4460..388a949d0 100644 --- a/apps/console/src/app/(console)/explore/explore-navigation.test.ts +++ b/apps/console/src/app/(console)/explore/explore-navigation.test.ts @@ -41,7 +41,6 @@ function accumulatingState(overrides: Partial = {}): NavigateStat cursorTrail: ["cursor-p2"], excludeConnectionIds: [], excludeStreams: [], - upcomingTrail: ["ucursor-u1"], order: "newest", query: "", searchSort: "relevance", @@ -49,6 +48,7 @@ function accumulatingState(overrides: Partial = {}): NavigateStat snapshotAnchor: "2026-06-20T00:00:00Z", streams: ["orders"], until: "", + upcomingTrail: ["ucursor-u1"], ...overrides, }; } diff --git a/apps/console/src/app/(console)/explore/explore-navigation.ts b/apps/console/src/app/(console)/explore/explore-navigation.ts index 73681a6df..283716d7d 100644 --- a/apps/console/src/app/(console)/explore/explore-navigation.ts +++ b/apps/console/src/app/(console)/explore/explore-navigation.ts @@ -264,23 +264,23 @@ export function buildNavigateHref(explorePath: string, state: NavigateState, opt // clearCursor resets it to the head. const nextUpcomingTrail = nextAccumulatingTrail(resetFeed, state.upcomingTrail, opts.appendUpcomingCursor); const href = buildHref(explorePath, { - query: opts.query ?? state.query, + // Anchor survives ONLY for non-feed-defining moves (Load-more, peek, order); + // forwardAnchor is already undefined when the feed is reset. + anchor: forwardAnchor, connectionIds: opts.connectionIds ?? state.connectionIds, - excludeConnectionIds: opts.excludeConnectionIds ?? state.excludeConnectionIds, - streams: opts.streams ?? state.streams, - excludeStreams: opts.excludeStreams ?? state.excludeStreams, - since: opts.since ?? state.since, - until: opts.until ?? state.until, - peek: opts.peek, - searchSort: opts.searchSort ?? (state.searchSort === "recent" ? "recent" : undefined), cursor: opts.clearCursor ? undefined : opts.cursor, // Trail is preserved ONLY for Load-more (appendCursor); every other navigation // leaves nextTrail undefined → the `cursors` param is dropped (reset to page 1). cursors: nextTrail, + excludeConnectionIds: opts.excludeConnectionIds ?? state.excludeConnectionIds, + excludeStreams: opts.excludeStreams ?? state.excludeStreams, + peek: opts.peek, + query: opts.query ?? state.query, + searchSort: opts.searchSort ?? (state.searchSort === "recent" ? "recent" : undefined), + since: opts.since ?? state.since, + streams: opts.streams ?? state.streams, + until: opts.until ?? state.until, upcomingCursors: nextUpcomingTrail, - // Anchor survives ONLY for non-feed-defining moves (Load-more, peek, order); - // forwardAnchor is already undefined when the feed is reset. - anchor: forwardAnchor, }); const nextOrder = opts.order ?? state.order; return nextOrder === "oldest" ? `${href}${href.includes("?") ? "&" : "?"}order=oldest` : href; diff --git a/apps/console/src/app/(console)/explore/explore-p2-search-sort.test.ts b/apps/console/src/app/(console)/explore/explore-p2-search-sort.test.ts index 8ed473758..978a1d0de 100644 --- a/apps/console/src/app/(console)/explore/explore-p2-search-sort.test.ts +++ b/apps/console/src/app/(console)/explore/explore-p2-search-sort.test.ts @@ -66,13 +66,13 @@ function makeHit(over: { emitted_at?: string; }): SearchResultHit { return { - connector_id: over.connector_id, connection_id: over.connection_id, - stream: over.stream ?? "records", - record_key: over.record_key ?? "rec-1", + connector_id: over.connector_id, emitted_at: over.emitted_at ?? "2026-01-01T00:00:00Z", matched_fields: [], object: "search_result", + record_key: over.record_key ?? "rec-1", + stream: over.stream ?? "records", }; } @@ -99,11 +99,11 @@ function makeLexicalPage( function makeRecordsPage(ids: string[], opts?: { has_more?: boolean; next_cursor?: string }): RecordsPage { return { data: ids.map((id) => ({ + data: {}, + emitted_at: "2026-01-01T00:00:00Z", id, object: "record" as const, stream: "transactions", - emitted_at: "2026-01-01T00:00:00Z", - data: {}, })), has_more: opts?.has_more ?? false, ...(opts?.next_cursor ? { next_cursor: opts.next_cursor } : {}), @@ -132,42 +132,42 @@ const notStubbed = (name: string) => () => Promise.reject(new Error(`${name} not function makeDataSource(overrides: Partial): DashboardDataSource { const stub: DashboardDataSource = { - kind: "sandbox" as const, aggregateRecordsByTime: notStubbed("aggregateRecordsByTime"), - listExploreRecordBuckets: notStubbed("listExploreRecordBuckets"), - isHybridRetrievalAdvertised: () => Promise.resolve(false), - isSemanticRetrievalAdvertised: () => Promise.resolve(false), - listConnectorSummaries: notStubbed("listConnectorSummaries"), - listConnectorManifests: notStubbed("listConnectorManifests"), - searchRecordsLexical: notStubbed("searchRecordsLexical"), - searchRecordsHybrid: notStubbed("searchRecordsHybrid"), - searchRecordsSemantic: notStubbed("searchRecordsSemantic"), - queryRecords: notStubbed("queryRecords"), - getRecord: notStubbed("getRecord"), getConnectorOverview: notStubbed("getConnectorOverview"), + getDatasetSummary: notStubbed("getDatasetSummary"), + getDeploymentDiagnostics: notStubbed("getDeploymentDiagnostics"), + getGrantTimeline: notStubbed("getGrantTimeline"), + getRecord: notStubbed("getRecord"), + getRunTimeline: notStubbed("getRunTimeline"), getStreamMetadata: notStubbed("getStreamMetadata"), getTraceTimeline: notStubbed("getTraceTimeline"), - listGrants: notStubbed("listGrants"), - listPendingApprovals: notStubbed("listPendingApprovals"), - listRuns: notStubbed("listRuns"), - listStreams: notStubbed("listStreams"), - listTraces: notStubbed("listTraces"), - refSearch: notStubbed("refSearch"), + isHybridRetrievalAdvertised: () => Promise.resolve(false), + isSemanticRetrievalAdvertised: () => Promise.resolve(false), + kind: "sandbox" as const, + listConnectorManifests: notStubbed("listConnectorManifests"), + listConnectorSummaries: notStubbed("listConnectorSummaries"), + listExploreRecordBuckets: notStubbed("listExploreRecordBuckets"), // The recent lens is the merged-timeline endpoint (single path; no fan-out // fallback). Return an empty page so the empty-query feed assembles cleanly. listExploreTimeline: () => Promise.resolve({ - object: "list" as const, data: [], has_more: false, + new_since_snapshot: 0, next_cursor: null, + object: "list" as const, snapshot_at: new Date(0).toISOString(), - new_since_snapshot: 0, }), - getDatasetSummary: notStubbed("getDatasetSummary"), - getDeploymentDiagnostics: notStubbed("getDeploymentDiagnostics"), - getGrantTimeline: notStubbed("getGrantTimeline"), - getRunTimeline: notStubbed("getRunTimeline"), + listGrants: notStubbed("listGrants"), + listPendingApprovals: notStubbed("listPendingApprovals"), + listRuns: notStubbed("listRuns"), + listStreams: notStubbed("listStreams"), + listTraces: notStubbed("listTraces"), + queryRecords: notStubbed("queryRecords"), + refSearch: notStubbed("refSearch"), + searchRecordsHybrid: notStubbed("searchRecordsHybrid"), + searchRecordsLexical: notStubbed("searchRecordsLexical"), + searchRecordsSemantic: notStubbed("searchRecordsSemantic"), ...overrides, }; return stub; @@ -177,11 +177,11 @@ function makeDataSource(overrides: Partial): DashboardDataS test("P2 lexical Most-relevant: has_more and next_cursor wired into searchHasMore + searchNextCursor", async () => { const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); - const hit = makeHit({ connector_id: "ynab", stream: "transactions", connection_id: "ynab-1" }); + const hit = makeHit({ connection_id: "ynab-1", connector_id: "ynab", stream: "transactions" }); const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), - listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), isHybridRetrievalAdvertised: () => Promise.resolve(false), + listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), // Recall proven complete: the lexical page is exhaustively pageable, so the // descriptor is keyword_pageable and the cursor trail is honest. searchRecordsLexical: async () => @@ -202,23 +202,23 @@ test("P2 lexical Most-relevant: has_more and next_cursor wired into searchHasMor test("P2 lexical Load-more: passing cursor= calls searchRecordsLexical with that cursor", async () => { const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); const hitP1 = makeHit({ + connection_id: "ynab-1", connector_id: "ynab", - stream: "transactions", record_key: "p1-rec", - connection_id: "ynab-1", + stream: "transactions", }); const hitP2 = makeHit({ + connection_id: "ynab-1", connector_id: "ynab", - stream: "transactions", record_key: "p2-rec", - connection_id: "ynab-1", + stream: "transactions", }); const capturedCursors: (string | undefined)[] = []; const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), - listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), isHybridRetrievalAdvertised: () => Promise.resolve(false), + listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), searchRecordsLexical: (_q, opts) => { capturedCursors.push(opts?.cursor); if (opts?.cursor === "cursor-page2") { @@ -237,7 +237,7 @@ test("P2 lexical Load-more: passing cursor= calls searchRecordsLexical with that assert.equal(page1.feed[0]?.recordId, "p1-rec"); // Page 2: forward the cursor from page 1 - const page2 = await assembleExplorerData({ q: "coffee", cursor: "cursor-page2" }, ds, "https://rs.test"); + const page2 = await assembleExplorerData({ cursor: "cursor-page2", q: "coffee" }, ds, "https://rs.test"); assert.equal(page2.searchHasMore, false); assert.equal(page2.searchNextCursor, null); assert.equal(page2.feed[0]?.recordId, "p2-rec"); @@ -256,29 +256,29 @@ test("P2 Most-recent single-stream: lexical is called (not queryRecords); return const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); // All hits from the same connector+stream so detectSingleStreamDoor fires. const hit = makeHit({ - connector_id: "ynab", - stream: "transactions", connection_id: "ynab-1", + connector_id: "ynab", record_key: "rec-last", + stream: "transactions", }); let queryRecordsCalled = false; const lexicalCalls: Array<{ streams?: string[]; cursor?: string }> = []; const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), - listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), isHybridRetrievalAdvertised: () => Promise.resolve(false), - // All lexical calls (stream-door probe + display fetch) go here. - searchRecordsLexical: (_q, opts) => { - lexicalCalls.push({ streams: opts?.streams, cursor: opts?.cursor }); - return Promise.resolve(makeLexicalPage([hit], { has_more: false })); - }, + listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), // queryRecords must NOT be called (F2 fix: was the display path, returned ALL records). queryRecords: () => { queryRecordsCalled = true; return Promise.reject(new Error("queryRecords must not be called for Most-recent single-stream (F2 fix)")); }, + // All lexical calls (stream-door probe + display fetch) go here. + searchRecordsLexical: (_q, opts) => { + lexicalCalls.push({ cursor: opts?.cursor, streams: opts?.streams }); + return Promise.resolve(makeLexicalPage([hit], { has_more: false })); + }, }); const result = await assembleExplorerData({ q: "rent", search_sort: "recent" }, ds, "https://rs.test"); @@ -305,19 +305,21 @@ test("P2 Most-recent single-stream: lexical is called (not queryRecords); return test("P2 Most-recent single-stream: second page (cursor forwarded to lexical) reaches last matching record", async () => { const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); - const hit = makeHit({ connector_id: "ynab", stream: "transactions", connection_id: "ynab-1", record_key: "rec-p1" }); + const hit = makeHit({ connection_id: "ynab-1", connector_id: "ynab", record_key: "rec-p1", stream: "transactions" }); const hitEnd = makeHit({ - connector_id: "ynab", - stream: "transactions", connection_id: "ynab-1", + connector_id: "ynab", record_key: "rec-end", + stream: "transactions", }); const lexicalCursors: (string | undefined)[] = []; const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), - listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), isHybridRetrievalAdvertised: () => Promise.resolve(false), + listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), + // queryRecords must NOT be called (F2 fix). + queryRecords: () => Promise.reject(new Error("queryRecords must not be called (F2 fix)")), searchRecordsLexical: (_q, opts) => { // Track the cursor arg passed to lexical (skip the probe call which has no cursor). if (opts?.streams) { @@ -329,8 +331,6 @@ test("P2 Most-recent single-stream: second page (cursor forwarded to lexical) re // Probe call (no streams) or page 1 display call returns hit with cursor. return Promise.resolve(makeLexicalPage([hit], { has_more: true, next_cursor: "lex-p2" })); }, - // queryRecords must NOT be called (F2 fix). - queryRecords: () => Promise.reject(new Error("queryRecords must not be called (F2 fix)")), }); // Page 1 of Most-recent @@ -340,7 +340,7 @@ test("P2 Most-recent single-stream: second page (cursor forwarded to lexical) re // Page 2: forward the lexical cursor (F2 fix: was a keyset cursor to queryRecords) const page2 = await assembleExplorerData( - { q: "rent", search_sort: "recent", cursor: "lex-p2" }, + { cursor: "lex-p2", q: "rent", search_sort: "recent" }, ds, "https://rs.test" ); @@ -360,13 +360,13 @@ test("P2 Most-recent single-stream: second page (cursor forwarded to lexical) re test("P2 hybrid Most-relevant: searchHasMore false and searchNextCursor null (no fake Load-more)", async () => { const summary = makeSummary({ connection_id: "gmail-1", connector_id: "gmail", streams: ["messages"] }); - const hit1 = makeHit({ connector_id: "gmail", stream: "messages", record_key: "msg-1", connection_id: "gmail-1" }); - const hit2 = makeHit({ connector_id: "gmail", stream: "messages", record_key: "msg-2", connection_id: "gmail-1" }); + const hit1 = makeHit({ connection_id: "gmail-1", connector_id: "gmail", record_key: "msg-1", stream: "messages" }); + const hit2 = makeHit({ connection_id: "gmail-1", connector_id: "gmail", record_key: "msg-2", stream: "messages" }); const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), - listConnectorManifests: () => Promise.resolve([makeManifest("gmail", ["messages"])]), isHybridRetrievalAdvertised: () => Promise.resolve(true), + listConnectorManifests: () => Promise.resolve([makeManifest("gmail", ["messages"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), searchRecordsHybrid: () => Promise.resolve(makeLexicalPage([hit1, hit2], { has_more: false })), // Hybrid mode should NOT call lexical searchRecordsLexical: () => Promise.reject(new Error("lexical must not be called when hybrid is used")), @@ -390,7 +390,7 @@ test("P2 hybrid Most-relevant: searchHasMore false and searchNextCursor null (no test("P2 hybrid + search_sort=recent: uses lexical hit to detect stream door, then lexical (not queryRecords)", async () => { const summary = makeSummary({ connection_id: "gmail-1", connector_id: "gmail", streams: ["messages"] }); - const hit = makeHit({ connector_id: "gmail", stream: "messages", connection_id: "gmail-1" }); + const hit = makeHit({ connection_id: "gmail-1", connector_id: "gmail", stream: "messages" }); let lexicalCallCount = 0; let lexicalStreamsArg: string[] | undefined; @@ -398,20 +398,20 @@ test("P2 hybrid + search_sort=recent: uses lexical hit to detect stream door, th // detect the stream door, then uses lexical again (with stream scope) for the // actual display results (F2 fix: was queryRecords without query). const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), - listConnectorManifests: () => Promise.resolve([makeManifest("gmail", ["messages"])]), isHybridRetrievalAdvertised: () => Promise.resolve(true), + listConnectorManifests: () => Promise.resolve([makeManifest("gmail", ["messages"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), + // queryRecords must NOT be called (F2 fix: it returned ALL records ignoring query) + queryRecords: () => Promise.reject(new Error("queryRecords must not be called in Most-recent mode (F2 fix)")), // Hybrid must NOT be called when search_sort=recent searchRecordsHybrid: () => Promise.reject(new Error("hybrid must not be called in Most-recent mode")), searchRecordsLexical: (_q, opts) => { - lexicalCallCount++; + lexicalCallCount += 1; if (opts?.streams) { lexicalStreamsArg = opts.streams; } return Promise.resolve(makeLexicalPage([hit], { has_more: false })); }, - // queryRecords must NOT be called (F2 fix: it returned ALL records ignoring query) - queryRecords: () => Promise.reject(new Error("queryRecords must not be called in Most-recent mode (F2 fix)")), }); const result = await assembleExplorerData({ q: "invoice", search_sort: "recent" }, ds, "https://rs.test"); @@ -440,29 +440,29 @@ test("P2 multi-stream Most-recent: returns matching records via lexical with wir const summaryA = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); const summaryB = makeSummary({ connection_id: "gmail-1", connector_id: "gmail", streams: ["messages"] }); const hitA = makeHit({ - connector_id: "ynab", - stream: "transactions", connection_id: "ynab-1", + connector_id: "ynab", record_key: "ynab-rec", + stream: "transactions", }); const hitB = makeHit({ - connector_id: "gmail", - stream: "messages", connection_id: "gmail-1", + connector_id: "gmail", record_key: "gmail-rec", + stream: "messages", }); const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summaryA, summaryB])), + isHybridRetrievalAdvertised: () => Promise.resolve(false), listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"]), makeManifest("gmail", ["messages"])]), - isHybridRetrievalAdvertised: () => Promise.resolve(false), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summaryA, summaryB])), + // queryRecords must NOT be called (multi-stream has no single-stream path) + queryRecords: () => Promise.reject(new Error("queryRecords must not be called for multi-stream Most-recent")), // Both the stream-door probe and the display fetch are lexical. // The display fetch carries has_more=true to verify the cursor is wired. searchRecordsLexical: () => Promise.resolve(makeLexicalPage([hitA, hitB], { has_more: true, next_cursor: "lex-cursor-42" })), - // queryRecords must NOT be called (multi-stream has no single-stream path) - queryRecords: () => Promise.reject(new Error("queryRecords must not be called for multi-stream Most-recent")), }); const result = await assembleExplorerData({ q: "payment", search_sort: "recent" }, ds, "https://rs.test"); @@ -493,16 +493,16 @@ test("P2 stream door: populated when all hits share one connector+stream", async const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", - streams: ["transactions"], display_name: "My YNAB", + streams: ["transactions"], }); - const hit1 = makeHit({ connector_id: "ynab", stream: "transactions", record_key: "r1", connection_id: "ynab-1" }); - const hit2 = makeHit({ connector_id: "ynab", stream: "transactions", record_key: "r2", connection_id: "ynab-1" }); + const hit1 = makeHit({ connection_id: "ynab-1", connector_id: "ynab", record_key: "r1", stream: "transactions" }); + const hit2 = makeHit({ connection_id: "ynab-1", connector_id: "ynab", record_key: "r2", stream: "transactions" }); const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), - listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), isHybridRetrievalAdvertised: () => Promise.resolve(false), + listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), searchRecordsLexical: () => Promise.resolve(makeLexicalPage([hit1, hit2], { has_more: false })), }); @@ -521,14 +521,14 @@ test("P2 stream door: populated when all hits share one connector+stream", async test("P2 stream door: null when hits span multiple connectors", async () => { const summaryA = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); const summaryB = makeSummary({ connection_id: "gmail-1", connector_id: "gmail", streams: ["messages"] }); - const hitA = makeHit({ connector_id: "ynab", stream: "transactions", connection_id: "ynab-1" }); - const hitB = makeHit({ connector_id: "gmail", stream: "messages", connection_id: "gmail-1" }); + const hitA = makeHit({ connection_id: "ynab-1", connector_id: "ynab", stream: "transactions" }); + const hitB = makeHit({ connection_id: "gmail-1", connector_id: "gmail", stream: "messages" }); const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summaryA, summaryB])), + isHybridRetrievalAdvertised: () => Promise.resolve(false), listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"]), makeManifest("gmail", ["messages"])]), - isHybridRetrievalAdvertised: () => Promise.resolve(false), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summaryA, summaryB])), searchRecordsLexical: () => Promise.resolve(makeLexicalPage([hitA, hitB], { has_more: false })), }); @@ -573,9 +573,9 @@ test("P2 searchSort defaults to 'relevance' on non-search (empty-query) feeds", const summary = makeSummary({ connection_id: "ynab-1", connector_id: "ynab", streams: ["transactions"] }); const ds = makeDataSource({ - listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), - listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), isHybridRetrievalAdvertised: () => Promise.resolve(false), + listConnectorManifests: () => Promise.resolve([makeManifest("ynab", ["transactions"])]), + listConnectorSummaries: () => Promise.resolve(summaryListResponse([summary])), queryRecords: () => Promise.resolve(makeRecordsPage(["rec-1"], { has_more: false })), }); diff --git a/apps/console/src/app/(console)/explore/explore-peek-relationships.test.ts b/apps/console/src/app/(console)/explore/explore-peek-relationships.test.ts index 1bb1cec69..0f11be033 100644 --- a/apps/console/src/app/(console)/explore/explore-peek-relationships.test.ts +++ b/apps/console/src/app/(console)/explore/explore-peek-relationships.test.ts @@ -15,8 +15,6 @@ function stubDataSource(over: { }): DashboardDataSource { const summaries = over.summaries ?? [{ connection_id: "conn_bank_chk", connector_id: "chase" }]; return { - listConnectorSummaries: () => Promise.resolve({ object: "list", data: summaries, has_more: false } as never), - listConnectorManifests: () => Promise.resolve(over.manifests ?? []), getStreamMetadata: (_connectorId: string, stream: string) => { const meta = over.streamMetadata?.[stream]; if (!meta) { @@ -24,6 +22,8 @@ function stubDataSource(over: { } return Promise.resolve(meta); }, + listConnectorManifests: () => Promise.resolve(over.manifests ?? []), + listConnectorSummaries: () => Promise.resolve({ data: summaries, has_more: false, object: "list" } as never), } as unknown as DashboardDataSource; } @@ -35,7 +35,7 @@ test("buildPeekRelationships resolves a child → parent back-link from a child- streams: [ { name: "transactions", - relationships: [{ name: "account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }], + relationships: [{ cardinality: "has_one", foreign_key: "account_id", name: "account", stream: "accounts" }], }, { name: "accounts" }, ], @@ -43,11 +43,11 @@ test("buildPeekRelationships resolves a child → parent back-link from a child- ]; const rels = await buildPeekRelationships( { - connectorId: "chase", connectionId: "conn_bank_chk", - stream: "transactions", + connectorId: "chase", + data: { account_id: "acct_checking_4417", amount: -640 }, recordId: "rec_tx_41203", - data: { amount: -640, account_id: "acct_checking_4417" }, + stream: "transactions", }, stubDataSource({ manifests }) ); @@ -65,25 +65,25 @@ test("buildPeekRelationships resolves a parent → child has_many link from expa // Inspecting a PARENT (accounts) record whose metadata declares has_many → transactions. const streamMetadata: Record = { accounts: { - name: "accounts", expand_capabilities: [ { + cardinality: "has_many", + child_parent_key_field: "account_id", name: "transactions", target_stream: "transactions", - child_parent_key_field: "account_id", - cardinality: "has_many", usable: true, }, ], + name: "accounts", }, }; const rels = await buildPeekRelationships( { - connectorId: "chase", connectionId: "conn_bank_chk", - stream: "accounts", - recordId: "acct_checking_4417", + connectorId: "chase", data: { name: "checking ····4417" }, + recordId: "acct_checking_4417", + stream: "accounts", }, stubDataSource({ streamMetadata }) ); @@ -99,14 +99,14 @@ test("buildPeekRelationships resolves a parent → child has_many link from expa test("buildPeekRelationships returns empty (never throws) when no declared edge exists", async () => { const rels = await buildPeekRelationships( { - connectorId: "gmail", connectionId: "conn_mail_work", - stream: "messages", + connectorId: "gmail", + data: { from: "dana@studio.example", subject: "hi" }, recordId: "rec_msg_8841", - data: { subject: "hi", from: "dana@studio.example" }, + stream: "messages", }, stubDataSource({ summaries: [{ connection_id: "conn_mail_work", connector_id: "gmail" }] }) ); - assert.deepEqual(rels, { relatedLinks: [], reverseChildListLinks: [], parentBackLinks: [] }); + assert.deepEqual(rels, { parentBackLinks: [], relatedLinks: [], reverseChildListLinks: [] }); assert.equal(hasPeekRelationships(rels), false); }); diff --git a/apps/console/src/app/(console)/explore/explore-peek-relationships.ts b/apps/console/src/app/(console)/explore/explore-peek-relationships.ts index 05ca2d45e..702c17977 100644 --- a/apps/console/src/app/(console)/explore/explore-peek-relationships.ts +++ b/apps/console/src/app/(console)/explore/explore-peek-relationships.ts @@ -88,7 +88,7 @@ export async function buildPeekRelationships( input: PeekRelationshipInput, dataSource: DashboardDataSource ): Promise { - const empty: PeekRelationships = { relatedLinks: [], reverseChildListLinks: [], parentBackLinks: [] }; + const empty: PeekRelationships = { parentBackLinks: [], relatedLinks: [], reverseChildListLinks: [] }; const { connectionId, connectorId, stream, recordId, data } = input; // Resolve the connection's instance id so metadata reads scope to the right @@ -121,8 +121,8 @@ export async function buildPeekRelationships( .getStreamMetadata(connectorId, parentStream, { connectorInstanceId }) .catch(() => null); return { - parentStream, expandCapabilities: Array.isArray(parentMeta?.expand_capabilities) ? parentMeta.expand_capabilities : [], + parentStream, }; }) ); @@ -142,7 +142,7 @@ export async function buildPeekRelationships( ); const reverseChildListLinks = reverseChildListLinksFromManifest( connectorStreams, - { connectionId, parentStream: stream, parentRecordKey: recordId }, + { connectionId, parentRecordKey: recordId, parentStream: stream }, forwardChildListKeys ); // Child → parent back-links from parent metadata + child's own declared has_one. @@ -150,5 +150,5 @@ export async function buildPeekRelationships( const childHasOneLinks = childHasOneBackLinksFromManifest(childManifestStream, data, { connectionId }); const parentBackLinks = mergeParentBackLinks(parentBackLinkFromMeta, childHasOneLinks); - return { relatedLinks, reverseChildListLinks, parentBackLinks }; + return { parentBackLinks, relatedLinks, reverseChildListLinks }; } diff --git a/apps/console/src/app/(console)/explore/explore-query-input.test.ts b/apps/console/src/app/(console)/explore/explore-query-input.test.ts index 75c01f054..d40d035b7 100644 --- a/apps/console/src/app/(console)/explore/explore-query-input.test.ts +++ b/apps/console/src/app/(console)/explore/explore-query-input.test.ts @@ -51,13 +51,13 @@ const SEARCH_FALLBACK_LABEL_RE = /Search: yn/; test("typeahead surfaces source/stream/has-image/has-link/date chips, each carrying its operator", () => { const suggestions = buildTypeaheadSuggestions({ - fragment: "", connections: CONNECTIONS, - streams: ["transactions"], - selectedConnectionIds: new Set(), - selectedStreams: new Set(), + fragment: "", hasImageActive: false, hasLinkActive: false, + selectedConnectionIds: new Set(), + selectedStreams: new Set(), + streams: ["transactions"], }); // A source chip is EQUIVALENT to the con: operator (recognition over recall). const ynab = suggestions.find((s) => s.value === "cin_ynab"); @@ -73,13 +73,13 @@ test("typeahead surfaces source/stream/has-image/has-link/date chips, each carry test("typeahead narrows by the trailing fragment (recognition, fuzzy)", () => { const suggestions = buildTypeaheadSuggestions({ - fragment: "yn", connections: CONNECTIONS, - streams: ["transactions"], - selectedConnectionIds: new Set(), - selectedStreams: new Set(), + fragment: "yn", hasImageActive: false, hasLinkActive: false, + selectedConnectionIds: new Set(), + selectedStreams: new Set(), + streams: ["transactions"], }); const sources = suggestions.filter((s) => s.kind === "source"); assert.equal(sources.length, 1, "only YNAB matches 'yn'"); @@ -88,13 +88,13 @@ test("typeahead narrows by the trailing fragment (recognition, fuzzy)", () => { test("typeahead excludes already-selected facets and already-active has:image (no noise)", () => { const suggestions = buildTypeaheadSuggestions({ - fragment: "", connections: CONNECTIONS, - streams: ["transactions"], - selectedConnectionIds: new Set(["cin_ynab"]), - selectedStreams: new Set(["transactions"]), + fragment: "", hasImageActive: true, hasLinkActive: false, + selectedConnectionIds: new Set(["cin_ynab"]), + selectedStreams: new Set(["transactions"]), + streams: ["transactions"], }); assert.ok(!suggestions.some((s) => s.value === "cin_ynab"), "selected source is not re-suggested"); assert.ok(!suggestions.some((s) => s.value === "transactions"), "selected stream is not re-suggested"); @@ -104,14 +104,14 @@ test("typeahead excludes already-selected facets and already-active has:image (n test("typeahead is bounded by `limit` (popover never unbounded)", () => { const many = Array.from({ length: 50 }, (_, i) => ({ connectionId: `c${i}`, displayName: `Conn ${i}` })); const suggestions = buildTypeaheadSuggestions({ - fragment: "", connections: many, - streams: [], - selectedConnectionIds: new Set(), - selectedStreams: new Set(), + fragment: "", hasImageActive: false, hasLinkActive: false, limit: 5, + selectedConnectionIds: new Set(), + selectedStreams: new Set(), + streams: [], }); assert.ok(suggestions.length <= 5, "the suggestion menu is capped"); }); @@ -129,13 +129,13 @@ test("appendOperatorToken replaces the trailing fragment with the operator token test("typeahead emits SOURCES section label on first source suggestion only", () => { const suggestions = buildTypeaheadSuggestions({ - fragment: "", connections: CONNECTIONS, - streams: ["transactions"], - selectedConnectionIds: new Set(), - selectedStreams: new Set(), + fragment: "", hasImageActive: false, hasLinkActive: false, + selectedConnectionIds: new Set(), + selectedStreams: new Set(), + streams: ["transactions"], }); const sources = suggestions.filter((s) => s.kind === "source"); assert.ok(sources.length >= 2, "both connections should appear"); @@ -145,13 +145,13 @@ test("typeahead emits SOURCES section label on first source suggestion only", () test("typeahead emits STREAMS section label on first stream suggestion", () => { const suggestions = buildTypeaheadSuggestions({ - fragment: "", connections: [], - streams: ["transactions", "budgets"], - selectedConnectionIds: new Set(), - selectedStreams: new Set(), + fragment: "", hasImageActive: false, hasLinkActive: false, + selectedConnectionIds: new Set(), + selectedStreams: new Set(), + streams: ["transactions", "budgets"], }); const streams = suggestions.filter((s) => s.kind === "stream"); assert.equal(streams[0]?.sectionLabel, "STREAMS"); @@ -160,18 +160,18 @@ test("typeahead emits STREAMS section label on first stream suggestion", () => { test("typeahead attaches honest record counts when connectionCounts / streamCounts are provided", () => { const suggestions = buildTypeaheadSuggestions({ - fragment: "", - connections: CONNECTIONS, connectionCounts: new Map([ ["cin_ynab", 42], ["cin_chase", 7], ]), - streams: ["transactions"], - streamCounts: new Map([["transactions", 100]]), - selectedConnectionIds: new Set(), - selectedStreams: new Set(), + connections: CONNECTIONS, + fragment: "", hasImageActive: false, hasLinkActive: false, + selectedConnectionIds: new Set(), + selectedStreams: new Set(), + streamCounts: new Map([["transactions", 100]]), + streams: ["transactions"], }); const ynab = suggestions.find((s) => s.value === "cin_ynab"); assert.equal(ynab?.count, 42, "source carries its loaded count"); @@ -181,13 +181,13 @@ test("typeahead attaches honest record counts when connectionCounts / streamCoun test("typeahead omits counts when not provided (undefined, not 0)", () => { const suggestions = buildTypeaheadSuggestions({ - fragment: "", connections: CONNECTIONS, - streams: ["transactions"], - selectedConnectionIds: new Set(), - selectedStreams: new Set(), + fragment: "", hasImageActive: false, hasLinkActive: false, + selectedConnectionIds: new Set(), + selectedStreams: new Set(), + streams: ["transactions"], }); const ynab = suggestions.find((s) => s.value === "cin_ynab"); assert.equal(ynab?.count, undefined, "no count when connectionCounts not provided"); @@ -195,13 +195,13 @@ test("typeahead omits counts when not provided (undefined, not 0)", () => { test("typeahead always appends SEARCH-fallback last when fragment is non-empty", () => { const suggestions = buildTypeaheadSuggestions({ - fragment: "yn", connections: CONNECTIONS, - streams: ["transactions"], - selectedConnectionIds: new Set(), - selectedStreams: new Set(), + fragment: "yn", hasImageActive: false, hasLinkActive: false, + selectedConnectionIds: new Set(), + selectedStreams: new Set(), + streams: ["transactions"], }); const last = suggestions.at(-1); assert.equal(last?.kind, "search", "last item is always SEARCH fallback"); @@ -211,13 +211,13 @@ test("typeahead always appends SEARCH-fallback last when fragment is non-empty", test("typeahead SEARCH-fallback is absent when fragment is empty", () => { const suggestions = buildTypeaheadSuggestions({ - fragment: "", connections: CONNECTIONS, - streams: ["transactions"], - selectedConnectionIds: new Set(), - selectedStreams: new Set(), + fragment: "", hasImageActive: false, hasLinkActive: false, + selectedConnectionIds: new Set(), + selectedStreams: new Set(), + streams: ["transactions"], }); assert.ok(!suggestions.some((s) => s.kind === "search"), "no SEARCH fallback when fragment is empty"); }); @@ -225,13 +225,13 @@ test("typeahead SEARCH-fallback is absent when fragment is empty", () => { test("typeahead SEARCH-fallback carries SEARCH section label when it is the only suggestion", () => { // Fragment that matches nothing — only the SEARCH-fallback survives. const suggestions = buildTypeaheadSuggestions({ - fragment: "xyzzy_no_match_ever", connections: [], - streams: [], - selectedConnectionIds: new Set(), - selectedStreams: new Set(), + fragment: "xyzzy_no_match_ever", hasImageActive: true, hasLinkActive: true, + selectedConnectionIds: new Set(), + selectedStreams: new Set(), + streams: [], }); assert.equal(suggestions.length, 1); assert.equal(suggestions[0]?.kind, "search"); diff --git a/apps/console/src/app/(console)/explore/explore-saved-views.test.ts b/apps/console/src/app/(console)/explore/explore-saved-views.test.ts index 24543f18a..37c267cdf 100644 --- a/apps/console/src/app/(console)/explore/explore-saved-views.test.ts +++ b/apps/console/src/app/(console)/explore/explore-saved-views.test.ts @@ -20,7 +20,7 @@ import { sameView, } from "./explore-saved-views.ts"; -const view = (id: string, name: string, href: string): SavedView => ({ id, name, href }); +const view = (id: string, name: string, href: string): SavedView => ({ href, id, name }); test("canonicalViewIdentity ignores volatile params (peek/cursor/anchor) and param order", () => { const a = "/explore?q=deploy&connection=con_1&peek=p1&cursor=c1&anchor=a1"; @@ -60,13 +60,13 @@ test("parseSavedViews drops malformed entries, never throws", () => { assert.deepEqual(parseSavedViews("not json"), []); assert.deepEqual(parseSavedViews(JSON.stringify({ not: "an array" })), []); const raw = JSON.stringify([ - { id: "1", name: "Finance", href: "/explore?q=x" }, - { id: "2", name: "", href: "/explore?q=y" }, // empty name → dropped - { id: "3", href: "/explore?q=z" }, // missing name → dropped - { name: "no id", href: "/x" }, // missing id → dropped + { href: "/explore?q=x", id: "1", name: "Finance" }, + { href: "/explore?q=y", id: "2", name: "" }, // empty name → dropped + { href: "/explore?q=z", id: "3" }, // missing name → dropped + { href: "/x", name: "no id" }, // missing id → dropped "garbage", ]); - assert.deepEqual(parseSavedViews(raw), [{ id: "1", name: "Finance", href: "/explore?q=x" }]); + assert.deepEqual(parseSavedViews(raw), [{ href: "/explore?q=x", id: "1", name: "Finance" }]); }); test("addSavedView never saves the All (no-filter) view as a tab", () => { diff --git a/apps/console/src/app/(console)/explore/explore-saved-views.ts b/apps/console/src/app/(console)/explore/explore-saved-views.ts index 369fecc51..6f5a51bb2 100644 --- a/apps/console/src/app/(console)/explore/explore-saved-views.ts +++ b/apps/console/src/app/(console)/explore/explore-saved-views.ts @@ -108,7 +108,7 @@ export function parseSavedViews(raw: string | null): SavedView[] { typeof (item as SavedView).href === "string" && (item as SavedView).name.trim().length > 0 ) { - out.push({ id: (item as SavedView).id, name: (item as SavedView).name, href: (item as SavedView).href }); + out.push({ href: (item as SavedView).href, id: (item as SavedView).id, name: (item as SavedView).name }); } } return out; diff --git a/apps/console/src/app/(console)/explore/explorer-url.test.ts b/apps/console/src/app/(console)/explore/explorer-url.test.ts index e331fa491..d4f071daf 100644 --- a/apps/console/src/app/(console)/explore/explorer-url.test.ts +++ b/apps/console/src/app/(console)/explore/explorer-url.test.ts @@ -22,8 +22,8 @@ const NO_CONNECTION_TOKEN = "~"; test("buildExplorerHref preserves repeated connection params (no collapse)", () => { const href = buildExplorerHref(dashboardRoutes, { - query: "payroll", connectionIds: ["gmail-personal", "gmail-work"], + query: "payroll", streams: ["messages"], }); const url = new URL(href, "https://example.test"); @@ -61,10 +61,10 @@ test("buildExplorerHref carries the date window when set", () => { test("buildExplorerHref preserves date window alongside chip + query state", () => { const href = buildExplorerHref(dashboardRoutes, { - query: "invoice", connectionIds: ["gmail-personal"], - streams: ["messages"], + query: "invoice", since: "2026-05-21", + streams: ["messages"], until: "2026-05-28", }); const url = new URL(href, "https://example.test"); @@ -82,10 +82,10 @@ test("buildExplorerHref omits empty date params", () => { test("explorerPeekParam round-trips a concrete connection_id when known", () => { const entry = { - connectorId: "gmail", connectionId: "conn-personal", - stream: "messages", + connectorId: "gmail", recordId: "ABC123", + stream: "messages", }; const raw = explorerPeekParam(entry); assert.equal(raw, "gmail::conn-personal::messages::ABC123"); @@ -94,7 +94,7 @@ test("explorerPeekParam round-trips a concrete connection_id when known", () => }); test("explorerPeekParam encodes the no-connection sentinel when unknown", () => { - const entry = { connectorId: "gmail", connectionId: null, stream: "messages", recordId: "ABC123" }; + const entry = { connectionId: null, connectorId: "gmail", recordId: "ABC123", stream: "messages" }; const raw = explorerPeekParam(entry); assert.equal(raw, `gmail::${NO_CONNECTION_TOKEN}::messages::ABC123`); const parsed = parseExplorerPeekParam(raw); @@ -105,16 +105,16 @@ test("explorerPeekParam keeps two same-connector connections distinct in the URL // Regression: previously the peek param was `connectorId::stream::recordId`, // so two Gmail connections viewing the same logical record id collided. const a = explorerPeekParam({ - connectorId: "gmail", connectionId: "conn-personal", - stream: "messages", + connectorId: "gmail", recordId: "ABC123", + stream: "messages", }); const b = explorerPeekParam({ - connectorId: "gmail", connectionId: "conn-work", - stream: "messages", + connectorId: "gmail", recordId: "ABC123", + stream: "messages", }); assert.notEqual(a, b); }); @@ -133,10 +133,10 @@ test("explorerPeekParam round-trips record ids containing the ':: ' separator", // contains `::`, so a record id like `thread::42` would parse as five // parts and be rejected, or worse, silently split mid-id. const entry = { - connectorId: "imap", connectionId: "conn-personal", - stream: "threads", + connectorId: "imap", recordId: "thread::42", + stream: "threads", }; const raw = explorerPeekParam(entry); assert.deepEqual(parseExplorerPeekParam(raw), entry); @@ -144,10 +144,10 @@ test("explorerPeekParam round-trips record ids containing the ':: ' separator", test("explorerPeekParam round-trips ids containing /, #, and spaces", () => { const entry = { - connectorId: "github", connectionId: "owner/repo#42", - stream: "issues/comments", + connectorId: "github", recordId: "comment id with spaces", + stream: "issues/comments", }; const raw = explorerPeekParam(entry); assert.deepEqual(parseExplorerPeekParam(raw), entry); @@ -155,10 +155,10 @@ test("explorerPeekParam round-trips ids containing /, #, and spaces", () => { test("explorerPeekParam round-trips a stream containing the separator", () => { const entry = { - connectorId: "custom", connectionId: "conn-a", - stream: "ns::events", + connectorId: "custom", recordId: "rec-1", + stream: "ns::events", }; const raw = explorerPeekParam(entry); assert.deepEqual(parseExplorerPeekParam(raw), entry); @@ -166,10 +166,10 @@ test("explorerPeekParam round-trips a stream containing the separator", () => { test("explorerPeekParam round-trips a connection id containing the separator", () => { const entry = { - connectorId: "gmail", connectionId: "tenant::user", - stream: "messages", + connectorId: "gmail", recordId: "ABC123", + stream: "messages", }; const raw = explorerPeekParam(entry); assert.deepEqual(parseExplorerPeekParam(raw), entry); @@ -177,14 +177,14 @@ test("explorerPeekParam round-trips a connection id containing the separator", ( function fakeEntry(displayAt: string, recordId: string): ExplorerFeedEntry { return { - connectorId: "gmail", - connectionId: "conn-personal", connectionDisplayName: "Personal Gmail", - stream: "messages", - recordId, - emittedAt: displayAt, + connectionId: "conn-personal", + connectorId: "gmail", displayAt, displayIsSemantic: false, + emittedAt: displayAt, + recordId, + stream: "messages", }; } diff --git a/apps/console/src/app/(console)/explore/over-time-chart.tsx b/apps/console/src/app/(console)/explore/over-time-chart.tsx index 988c7f671..f4e4f268a 100644 --- a/apps/console/src/app/(console)/explore/over-time-chart.tsx +++ b/apps/console/src/app/(console)/explore/over-time-chart.tsx @@ -133,7 +133,7 @@ export function OverTimeChart({ series, since, until, descriptorKind, onSelectRa return; } draggingRef.current = true; - setDrag({ start: i, end: i }); + setDrag({ end: i, start: i }); }, [brushable] ); @@ -141,7 +141,7 @@ export function OverTimeChart({ series, since, until, descriptorKind, onSelectRa const onPointerEnterBar = useCallback((i: number) => { setHovered(i); if (draggingRef.current) { - setDrag((d) => (d ? { ...d, end: i } : { start: i, end: i })); + setDrag((d) => (d ? { ...d, end: i } : { end: i, start: i })); } }, []); @@ -203,6 +203,7 @@ export function OverTimeChart({ series, since, until, descriptorKind, onSelectRa
{ setHovered(null); }} @@ -218,8 +219,11 @@ export function OverTimeChart({ series, since, until, descriptorKind, onSelectRa inDrag={dragLo !== null && dragHi !== null && i >= dragLo && i <= dragHi} key={bucket.key} maxCount={maxCount} + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onClick={() => onBarClick(i)} + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onPointerDown={() => onPointerDown(i)} + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onPointerEnter={() => onPointerEnterBar(i)} selected={selectedIndices.has(i)} /> diff --git a/apps/console/src/app/(console)/explore/page.invariants.test.ts b/apps/console/src/app/(console)/explore/page.invariants.test.ts index 5ae13fbf0..1cb106c19 100644 --- a/apps/console/src/app/(console)/explore/page.invariants.test.ts +++ b/apps/console/src/app/(console)/explore/page.invariants.test.ts @@ -60,7 +60,8 @@ const ROW_ACTION_LABEL_RE = /\{actionLabel\}< // as "ingested", and still renders a `precision="time"` Timestamp. Keep the // assertions loose enough to tolerate JSX reshaping. const ROW_TIME_CONTAINER_RE = //; -const ROW_TIME_QUALIFIER_RE = /entry\.displayIsSemantic \? null : ingested <\/span>/; +const ROW_TIME_QUALIFIER_RE = + /entry\.displayIsSemantic \? null : ingested <\/span>/; const ROW_TIME_TIMESTAMP_RE = //; const ROW_TIME_CSS_RE = /\.rr-x-row__time\s*\{/; // Future-dated records (e.g. YNAB future budget months) must NOT sit above today: @@ -781,11 +782,11 @@ test("Slice 5: every Explore animation reference sits inside a prefers-reduced-m let depth = 0; let i = idx + opener.length - 1; // start at the opening brace let end = css.length; - for (; i < css.length; i++) { + for (; i < css.length; i += 1) { if (css[i] === "{") { - depth++; + depth += 1; } else if (css[i] === "}") { - depth--; + depth -= 1; if (depth === 0) { end = i; break; @@ -803,7 +804,7 @@ test("Slice 5: every Explore animation reference sits inside a prefers-reduced-m // reset — so we only check keyframe-driven `animation:` here.) const animationDeclRe = /animation:\s*[^;]+;/g; for (const m of css.matchAll(animationDeclRe)) { - const decl = m[0]; + const [decl] = m; const referencesGated = GATED_KEYFRAMES.some((k) => decl.includes(k)); if (referencesGated) { assert.ok( diff --git a/apps/console/src/app/(console)/explore/page.tsx b/apps/console/src/app/(console)/explore/page.tsx index 08099ef84..055085892 100644 --- a/apps/console/src/app/(console)/explore/page.tsx +++ b/apps/console/src/app/(console)/explore/page.tsx @@ -131,11 +131,11 @@ export default async function RecordsExplorerPage({ if (data.peek && !data.peek.error && data.peek.connectionId) { peekRelationships = await buildPeekRelationships( { - connectorId: data.peek.connectorId, connectionId: data.peek.connectionId, - stream: data.peek.stream, - recordId: data.peek.recordId, + connectorId: data.peek.connectorId, data: parsePeekData(data.peek.bodyJson), + recordId: data.peek.recordId, + stream: data.peek.stream, }, liveDashboardDataSource ); diff --git a/apps/console/src/app/(console)/explore/peek-url.test.ts b/apps/console/src/app/(console)/explore/peek-url.test.ts index 314a861c1..a8e8a1e87 100644 --- a/apps/console/src/app/(console)/explore/peek-url.test.ts +++ b/apps/console/src/app/(console)/explore/peek-url.test.ts @@ -7,11 +7,11 @@ import { buildPeekReadUrl } from "@pdpp/operator-ui/explore/peek-read-url"; test("buildPeekReadUrl matches the canonical record-read URL shape", () => { const url = buildPeekReadUrl({ - rsBaseUrl: "http://rs.test", connectorId: "gmail", - stream: "messages", - recordId: "rec-1", connectorInstanceId: "conn-abc", + recordId: "rec-1", + rsBaseUrl: "http://rs.test", + stream: "messages", }); const parsed = new URL(url); assert.equal(parsed.pathname, "/v1/streams/messages/records/rec-1"); @@ -21,11 +21,11 @@ test("buildPeekReadUrl matches the canonical record-read URL shape", () => { test("buildPeekReadUrl encodes path-unsafe stream and record ids", () => { const url = buildPeekReadUrl({ - rsBaseUrl: "http://rs.test", connectorId: "github", - stream: "issues/comments", - recordId: "owner/repo#42", connectorInstanceId: null, + recordId: "owner/repo#42", + rsBaseUrl: "http://rs.test", + stream: "issues/comments", }); const parsed = new URL(url); assert.equal(parsed.pathname, "/v1/streams/issues%2Fcomments/records/owner%2Frepo%2342"); diff --git a/apps/console/src/app/(console)/explore/search-hit-attribution.test.ts b/apps/console/src/app/(console)/explore/search-hit-attribution.test.ts index e4c621cf7..64d9bca22 100644 --- a/apps/console/src/app/(console)/explore/search-hit-attribution.test.ts +++ b/apps/console/src/app/(console)/explore/search-hit-attribution.test.ts @@ -34,8 +34,8 @@ test("attributeSearchHit honors hit.connection_id when present", () => { ]; const result = attributeSearchHit( { - connector_id: "gmail", connection_id: "conn-work", + connector_id: "gmail", }, visible ); @@ -94,13 +94,13 @@ test("shouldIncludeSearchHit drops a hit whose concrete connection identity is n // is filtered out at the summaries layer and so is not in this set. const allowedConnectionIds = new Set(["conn-personal"]); const hit = { - connector_id: "gmail", connection_id: "conn-work", + connector_id: "gmail", }; assert.equal( shouldIncludeSearchHit(hit, { - allowedConnectors, allowedConnectionIds, + allowedConnectors, enforceConnectionFilter: true, }), false @@ -111,13 +111,13 @@ test("shouldIncludeSearchHit keeps a hit whose concrete connection identity matc const allowedConnectors = new Set(["gmail"]); const allowedConnectionIds = new Set(["conn-personal"]); const hit = { - connector_id: "gmail", connection_id: "conn-personal", + connector_id: "gmail", }; assert.equal( shouldIncludeSearchHit(hit, { - allowedConnectors, allowedConnectionIds, + allowedConnectors, enforceConnectionFilter: true, }), true @@ -133,8 +133,8 @@ test("shouldIncludeSearchHit honors the deprecated connector_instance_id alias w }; assert.equal( shouldIncludeSearchHit(hit, { - allowedConnectors, allowedConnectionIds, + allowedConnectors, enforceConnectionFilter: true, }), false @@ -150,8 +150,8 @@ test("shouldIncludeSearchHit falls through to connector-scope when the hit carri const hit = { connector_id: "gmail" }; assert.equal( shouldIncludeSearchHit(hit, { - allowedConnectors, allowedConnectionIds, + allowedConnectors, enforceConnectionFilter: true, }), true @@ -161,11 +161,11 @@ test("shouldIncludeSearchHit falls through to connector-scope when the hit carri test("shouldIncludeSearchHit drops hits whose connector type is not in the visible set", () => { const allowedConnectors = new Set(["gmail"]); const allowedConnectionIds = new Set(["conn-personal"]); - const hit = { connector_id: "github", connection_id: "conn-personal" }; + const hit = { connection_id: "conn-personal", connector_id: "github" }; assert.equal( shouldIncludeSearchHit(hit, { - allowedConnectors, allowedConnectionIds, + allowedConnectors, enforceConnectionFilter: true, }), false @@ -178,11 +178,11 @@ test("shouldIncludeSearchHit does not enforce connection-scope when no chips are // carry an unrecognized connection_id. const allowedConnectors = new Set(["gmail"]); const allowedConnectionIds = new Set(); - const hit = { connector_id: "gmail", connection_id: "conn-unknown" }; + const hit = { connection_id: "conn-unknown", connector_id: "gmail" }; assert.equal( shouldIncludeSearchHit(hit, { - allowedConnectors, allowedConnectionIds, + allowedConnectors, enforceConnectionFilter: false, }), true @@ -195,8 +195,8 @@ test("attributeSearchHit prefers server-provided display_name over summary fallb ]; const result = attributeSearchHit( { - connector_id: "gmail", connection_id: "conn-personal", + connector_id: "gmail", display_name: "Server Label", }, visible diff --git a/apps/console/src/app/(console)/grants/[grantId]/page.tsx b/apps/console/src/app/(console)/grants/[grantId]/page.tsx index c2b324c83..fc79fb873 100644 --- a/apps/console/src/app/(console)/grants/[grantId]/page.tsx +++ b/apps/console/src/app/(console)/grants/[grantId]/page.tsx @@ -77,7 +77,7 @@ export default async function GrantDetailPage({
} - breadcrumbs={[{ label: "Grants", href: "/grants" }, { label: "Grant" }]} + breadcrumbs={[{ href: "/grants", label: "Grants" }, { label: "Grant" }]} cliCommand={`pdpp ref grant timeline ${grantId}`} count={`${envelope.events.length} events${revoked ? " · revoked" : ""}`} envelope={envelope} diff --git a/apps/console/src/app/(console)/grants/page.tsx b/apps/console/src/app/(console)/grants/page.tsx index 386813d76..6f0ae94cf 100644 --- a/apps/console/src/app/(console)/grants/page.tsx +++ b/apps/console/src/app/(console)/grants/page.tsx @@ -80,13 +80,13 @@ function grantClientCaption(grant: GrantSummary): string | null { export default async function GrantsPage({ searchParams }: { searchParams: Promise }) { const params = await searchParams; const filters = { - cursor: params.cursor, - status: params.status, client_id: params.client_id, + cursor: params.cursor, + limit: 50, + q: params.q, source_id: params.source_id, source_kind: params.source_kind, - q: params.q, - limit: 50, + status: params.status, }; let result: ListResponse; @@ -96,7 +96,7 @@ export default async function GrantsPage({ searchParams }: { searchParams: Promi const demo = await import("./grants-demo-data.ts"); const demoData = demo.buildGrantsDemoData(); result = demoData.grants; - approvals = demoData.approvals; + ({ approvals: approvals } = demoData); } else { try { [result, approvals] = await Promise.all([listGrants(filters), listPendingApprovals()]); @@ -166,21 +166,21 @@ export default async function GrantsPage({ searchParams }: { searchParams: Promi emptyHint: "Grant artifacts appear after client/provider-connect consent flows issue or reject grants.", emptyTitle: "No grants yet", filters: { - query: { name: "q", placeholder: "id contains…", defaultValue: params.q ?? "" }, + query: { defaultValue: params.q ?? "", name: "q", placeholder: "id contains…" }, status: { - name: "status", defaultValue: params.status ?? "", + name: "status", options: [ - { value: "issued", label: "issued" }, - { value: "revoked", label: "revoked" }, - { value: "denied", label: "denied" }, - { value: "failed", label: "failed" }, - { value: "pending", label: "pending" }, + { label: "issued", value: "issued" }, + { label: "revoked", value: "revoked" }, + { label: "denied", value: "denied" }, + { label: "failed", value: "failed" }, + { label: "pending", value: "pending" }, ], }, }, headerActions: ( - + Grant request workspace ), diff --git a/apps/console/src/app/(console)/grants/pending-actions.ts b/apps/console/src/app/(console)/grants/pending-actions.ts index c0c694b6c..94508ca2b 100644 --- a/apps/console/src/app/(console)/grants/pending-actions.ts +++ b/apps/console/src/app/(console)/grants/pending-actions.ts @@ -32,8 +32,8 @@ export async function approvePendingApprovalAction(formData: FormData) { try { await approvePendingApproval({ - kind, approvalId, + kind, subjectId, }); } catch (err) { @@ -52,8 +52,8 @@ export async function denyPendingApprovalAction(formData: FormData) { try { await denyPendingApproval({ - kind, approvalId, + kind, subjectId, }); } catch (err) { diff --git a/apps/console/src/app/(console)/grants/request/actions.ts b/apps/console/src/app/(console)/grants/request/actions.ts index d43b4b1dc..f96fb109a 100644 --- a/apps/console/src/app/(console)/grants/request/actions.ts +++ b/apps/console/src/app/(console)/grants/request/actions.ts @@ -24,22 +24,22 @@ function asSourceKind(value: FormDataEntryValue | null): "connector" | "provider function readDraft(formData: FormData) { return { - initialAccessToken: asString(formData.get("initial_access_token")), + accessMode: asString(formData.get("access_mode")), clientId: asString(formData.get("client_id")), clientName: asString(formData.get("client_name")), clientUri: asString(formData.get("client_uri")), - redirectUri: asString(formData.get("redirect_uri")), - sourceKind: asSourceKind(formData.get("source_kind")), - sourceId: asString(formData.get("source_id")), + connectionId: asString(formData.get("connection_id")), + fields: asString(formData.get("fields")), + initialAccessToken: asString(formData.get("initial_access_token")), purposeCode: asString(formData.get("purpose_code")), purposeDescription: asString(formData.get("purpose_description")), - accessMode: asString(formData.get("access_mode")), + redirectUri: asString(formData.get("redirect_uri")), retention: asString(formData.get("retention")), + sourceId: asString(formData.get("source_id")), + sourceKind: asSourceKind(formData.get("source_kind")), streamName: asString(formData.get("stream_name")), - connectionId: asString(formData.get("connection_id")), - fields: asString(formData.get("fields")), - view: asString(formData.get("view")), subjectId: asString(formData.get("subject_id")), + view: asString(formData.get("view")), }; } diff --git a/apps/console/src/app/(console)/grants/request/connection-pin.test.ts b/apps/console/src/app/(console)/grants/request/connection-pin.test.ts index 54ddb3cb7..69b4e813f 100644 --- a/apps/console/src/app/(console)/grants/request/connection-pin.test.ts +++ b/apps/console/src/app/(console)/grants/request/connection-pin.test.ts @@ -67,34 +67,34 @@ function draft(overrides = {}) { test("buildConnectionPinOptions shows owner-set names verbatim and excludes other connectors", () => { const options = buildConnectionPinOptions({ id: "gmail", kind: "connector" }, [ - { connector_id: "gmail", connection_id: "cin_a", display_name: "Work Gmail", streams: ["messages"] }, - { connector_id: "gmail", connection_id: "cin_b", display_name: "Personal Gmail", streams: ["messages"] }, - { connector_id: "slack", connection_id: "cin_s", display_name: "Team Slack", streams: ["messages"] }, + { connection_id: "cin_a", connector_id: "gmail", display_name: "Work Gmail", streams: ["messages"] }, + { connection_id: "cin_b", connector_id: "gmail", display_name: "Personal Gmail", streams: ["messages"] }, + { connection_id: "cin_s", connector_id: "slack", display_name: "Team Slack", streams: ["messages"] }, ]); assert.deepEqual(options, [ - { value: "cin_a", label: "Work Gmail" }, - { value: "cin_b", label: "Personal Gmail" }, + { label: "Work Gmail", value: "cin_a" }, + { label: "Personal Gmail", value: "cin_b" }, ]); }); test("buildConnectionPinOptions filters to connections that expose the addressed stream", () => { const options = buildConnectionPinOptions({ id: "gmail", kind: "connector", streamName: "messages" }, [ - { connector_id: "gmail", connection_id: "cin_messages", display_name: "Messages Gmail", streams: ["messages"] }, - { connector_id: "gmail", connection_id: "cin_contacts", display_name: "Contacts Gmail", streams: ["contacts"] }, + { connection_id: "cin_messages", connector_id: "gmail", display_name: "Messages Gmail", streams: ["messages"] }, + { connection_id: "cin_contacts", connector_id: "gmail", display_name: "Contacts Gmail", streams: ["contacts"] }, ]); - assert.deepEqual(options, [{ value: "cin_messages", label: "Messages Gmail" }]); + assert.deepEqual(options, [{ label: "Messages Gmail", value: "cin_messages" }]); }); test("buildConnectionPinOptions derives an owner-meaningful default for never-renamed connections", () => { // A blank label and a bare-connector-type label are both fallbacks; they get // a stable `· account N` disambiguator instead of a placeholder/URL/id. const options = buildConnectionPinOptions({ id: "gmail", kind: "connector" }, [ - { connector_id: "gmail", connection_id: "cin_a", display_name: null, streams: ["messages"] }, - { connector_id: "gmail", connection_id: "cin_b", display_name: "gmail", streams: ["messages"] }, + { connection_id: "cin_a", connector_id: "gmail", display_name: null, streams: ["messages"] }, + { connection_id: "cin_b", connector_id: "gmail", display_name: "gmail", streams: ["messages"] }, ]); assert.deepEqual(options, [ - { value: "cin_a", label: "gmail · account 1" }, - { value: "cin_b", label: "gmail · account 2" }, + { label: "gmail · account 1", value: "cin_a" }, + { label: "gmail · account 2", value: "cin_b" }, ]); // No rendered label is ever the raw connection_id or a placeholder string. for (const opt of options) { @@ -105,14 +105,14 @@ test("buildConnectionPinOptions derives an owner-meaningful default for never-re test("buildConnectionPinOptions keeps a lone connection's label undisambiguated", () => { const options = buildConnectionPinOptions({ id: "gmail", kind: "connector" }, [ - { connector_id: "gmail", connection_id: "cin_a", display_name: "Work Gmail", streams: ["messages"] }, + { connection_id: "cin_a", connector_id: "gmail", display_name: "Work Gmail", streams: ["messages"] }, ]); - assert.deepEqual(options, [{ value: "cin_a", label: "Work Gmail" }]); + assert.deepEqual(options, [{ label: "Work Gmail", value: "cin_a" }]); }); test("buildConnectionPinOptions returns [] for provider-native and empty sources (no connection dimension)", () => { const summaries = [ - { connector_id: "gmail", connection_id: "cin_a", display_name: "Work Gmail", streams: ["messages"] }, + { connection_id: "cin_a", connector_id: "gmail", display_name: "Work Gmail", streams: ["messages"] }, ]; assert.deepEqual(buildConnectionPinOptions({ id: "gmail", kind: "provider_native" }, summaries), []); assert.deepEqual(buildConnectionPinOptions({ id: "", kind: "connector" }, summaries), []); diff --git a/apps/console/src/app/(console)/grants/request/page.tsx b/apps/console/src/app/(console)/grants/request/page.tsx index 93d6951ba..25669c042 100644 --- a/apps/console/src/app/(console)/grants/request/page.tsx +++ b/apps/console/src/app/(console)/grants/request/page.tsx @@ -35,10 +35,10 @@ type Examples = NonNullable function HeaderActions({ ownerLoginUrl }: { ownerLoginUrl: string }) { return ( <> - + Pending approvals - + Owner access @@ -72,8 +72,8 @@ function WorkspaceError({ message }: { message: string }) { } const ACCESS_MODE_OPTIONS = [ - { value: "single_use", label: "single_use" }, - { value: "ongoing", label: "ongoing" }, + { label: "single_use", value: "single_use" }, + { label: "ongoing", value: "ongoing" }, ]; function DraftFormFields({ connectionOptions, draft }: { connectionOptions: ConnectionPinOption[]; draft: Draft }) { @@ -331,6 +331,7 @@ function EquivalentsSection({ examples }: { examples: Examples }) { export default async function GrantRequestPage({ searchParams }: { searchParams: Promise }) { const params = await searchParams; const workspace = params.workspace ? getGrantRequestWorkspace(params.workspace) : null; + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic const draft = workspace?.draft ?? createDefaultGrantRequestDraft(); const examples = workspace ? await buildGrantRequestExamples(workspace) : null; const connectionOptions = await loadConnectionPinOptions(draft); @@ -341,7 +342,7 @@ export default async function GrantRequestPage({ searchParams }: { searchParams: } - breadcrumbs={[{ label: "Grants", href: "/grants" }, { label: "Grant request" }]} + breadcrumbs={[{ href: "/grants", label: "Grants" }, { label: "Grant request" }]} description="Register a public client, stage a real PAR request with PDPP authorization details, then drive the resulting consent through the public approval path." meta={} title="Grant request workspace" diff --git a/apps/console/src/app/(console)/layout.tsx b/apps/console/src/app/(console)/layout.tsx index c2393b29a..267b66c48 100644 --- a/apps/console/src/app/(console)/layout.tsx +++ b/apps/console/src/app/(console)/layout.tsx @@ -14,7 +14,7 @@ import { isDashboardEnabled } from "./lib/dashboard-flag.ts"; // metadata export is defense in depth for the case where the dashboard // layout does render to completion. export const metadata: Metadata = { - robots: { index: false, follow: false, nocache: true }, + robots: { follow: false, index: false, nocache: true }, }; // Auth gating intentionally lives in two layers, not here: diff --git a/apps/console/src/app/(console)/lib/cancel-run-result.ts b/apps/console/src/app/(console)/lib/cancel-run-result.ts index b51da1942..995315852 100644 --- a/apps/console/src/app/(console)/lib/cancel-run-result.ts +++ b/apps/console/src/app/(console)/lib/cancel-run-result.ts @@ -26,11 +26,11 @@ export function cancelRunErrorCode(body: unknown): string | null { if (!body || typeof body !== "object") { return null; } - const error = (body as { error?: unknown }).error; + const { error } = body as { error?: unknown }; if (!error || typeof error !== "object") { return null; } - const code = (error as { code?: unknown }).code; + const { code } = error as { code?: unknown }; return typeof code === "string" ? code : null; } diff --git a/apps/console/src/app/(console)/lib/collection-report.test.ts b/apps/console/src/app/(console)/lib/collection-report.test.ts index 7d2b795eb..160697689 100644 --- a/apps/console/src/app/(console)/lib/collection-report.test.ts +++ b/apps/console/src/app/(console)/lib/collection-report.test.ts @@ -52,15 +52,15 @@ const STRATEGY_NUMERATOR_COPY = /not the coverage numerator/i; function entry(overrides: Partial): RefCollectionReportEntry { return { - stream: "items", + checkpoint: "unknown", collected: 0, considered: "unknown", - covered: "unknown", - checkpoint: "unknown", coverage_condition: "unknown", + covered: "unknown", forward_disposition: "resumable", pending_detail_gaps: 0, skipped: null, + stream: "items", ...overrides, }; } @@ -71,8 +71,8 @@ test("indexCollectionReportByStream tolerates absence and indexes by stream name assert.equal(indexCollectionReportByStream([]).size, 0); const byStream = indexCollectionReportByStream([ - entry({ stream: "items", collected: 3 }), - entry({ stream: "other_items", collected: 1 }), + entry({ collected: 3, stream: "items" }), + entry({ collected: 1, stream: "other_items" }), ]); assert.equal(byStream.size, 2); assert.equal(byStream.get("items")?.collected, 3); @@ -81,8 +81,8 @@ test("indexCollectionReportByStream tolerates absence and indexes by stream name test("a duplicate stream name keeps the first entry", () => { const byStream = indexCollectionReportByStream([ - entry({ stream: "items", collected: 5 }), - entry({ stream: "items", collected: 9 }), + entry({ collected: 5, stream: "items" }), + entry({ collected: 9, stream: "items" }), ]); assert.equal(byStream.size, 1); assert.equal(byStream.get("items")?.collected, 5); @@ -92,9 +92,9 @@ test("collectionReportHasOpenGaps distinguishes clean completion from unresolved assert.equal( collectionReportHasOpenGaps([ entry({ - coverage_condition: "complete", - considered: 1, collected: 0, + considered: 1, + coverage_condition: "complete", covered: 1, forward_disposition: "owner_refresh_due", }), @@ -121,7 +121,7 @@ test("runStatusWithCollectionReportGaps promotes only clean success statuses whe assert.equal(runStatusWithCollectionReportGaps("failed", gapReport), "failed"); assert.equal( runStatusWithCollectionReportGaps("succeeded", [ - entry({ coverage_condition: "complete", considered: 1, collected: 0, covered: 1 }), + entry({ collected: 0, considered: 1, coverage_condition: "complete", covered: 1 }), ]), "succeeded" ); @@ -129,7 +129,7 @@ test("runStatusWithCollectionReportGaps promotes only clean success statuses whe test("THE HONESTY GATE: collected records with an unknown considered denominator never imply completeness", () => { const facts = formatStreamCollectionFacts( - entry({ stream: "items", collected: 42, considered: "unknown", coverage_condition: "unknown" }) + entry({ collected: 42, considered: "unknown", coverage_condition: "unknown", stream: "items" }) ); // The coverage chip stays unknown, never complete. assert.equal(facts.coverage.value, "unknown"); @@ -144,7 +144,7 @@ test("THE HONESTY GATE: collected records with an unknown considered denominator test("a known considered denominator renders collected / considered", () => { const facts = formatStreamCollectionFacts( - entry({ stream: "items", collected: 7, considered: 10, coverage_condition: "partial" }) + entry({ collected: 7, considered: 10, coverage_condition: "partial", stream: "items" }) ); assert.equal(facts.countsLabel, "7 / 10 collected"); assert.equal(facts.coverage.value, "partial"); @@ -153,13 +153,13 @@ test("a known considered denominator renders collected / considered", () => { test("strategy-backed complete streams do not render collected / considered as a partial-looking fraction", () => { const facts = formatStreamCollectionFacts( entry({ - stream: "pull_requests", checkpoint: "committed", collected: 9, considered: 52, coverage_condition: "complete", coverage_strategy: "checkpoint_window", forward_disposition: "complete", + stream: "pull_requests", }) ); assert.equal(facts.countsLabel, "checkpoint covered · 9 collected"); @@ -172,13 +172,13 @@ test("strategy-backed complete streams do not render collected / considered as a test("full-inventory complete streams name the inventory proof instead of implying missing records", () => { const facts = formatStreamCollectionFacts( entry({ - stream: "repositories", checkpoint: "committed", collected: 5, considered: 100, coverage_condition: "complete", coverage_strategy: "full_inventory", forward_disposition: "complete", + stream: "repositories", }) ); assert.equal(facts.countsLabel, "inventory covered · 5 collected"); @@ -190,13 +190,13 @@ test("full-inventory complete streams name the inventory proof instead of implyi test("zero-emission singleton proofs still show the proof instead of collection count unavailable", () => { const facts = formatStreamCollectionFacts( entry({ - stream: "user", checkpoint: "committed", collected: 0, considered: "unknown", coverage_condition: "complete", coverage_strategy: "singleton_presence", forward_disposition: "complete", + stream: "user", }) ); assert.equal(facts.countsLabel, "presence checked"); @@ -208,7 +208,7 @@ test("THE CLAMP: collected > considered never renders an impossible fraction (ph // render "3 / 2 collected" — an impossible tuple. The displayed numerator is // clamped to the denominator; the raw count is disclosed in the title. const facts = formatStreamCollectionFacts( - entry({ stream: "items", collected: 3, considered: 2, coverage_condition: "complete" }) + entry({ collected: 3, considered: 2, coverage_condition: "complete", stream: "items" }) ); assert.equal(facts.countsLabel, "2 / 2 collected"); assert.doesNotMatch(facts.countsLabel ?? "", IMPOSSIBLE_COLLECTED_FRACTION); @@ -219,7 +219,7 @@ test("THE CLAMP: collected > considered never renders an impossible fraction (ph test("THE CLAMP: covered > considered is clamped too, raw covered preserved in the title", () => { const facts = formatStreamCollectionFacts( - entry({ stream: "items", collected: 5, considered: 4, covered: 6, coverage_condition: "complete" }) + entry({ collected: 5, considered: 4, coverage_condition: "complete", covered: 6, stream: "items" }) ); assert.equal(facts.countsLabel, "4 / 4 covered · 5 collected"); assert.doesNotMatch(facts.countsLabel ?? "", IMPOSSIBLE_COVERED_FRACTION); @@ -228,7 +228,7 @@ test("THE CLAMP: covered > considered is clamped too, raw covered preserved in t test("THE CLAMP: collected over-report does not imply covered exceeded the denominator", () => { const facts = formatStreamCollectionFacts( - entry({ stream: "items", collected: 5, considered: 4, covered: 3, coverage_condition: "partial" }) + entry({ collected: 5, considered: 4, coverage_condition: "partial", covered: 3, stream: "items" }) ); assert.equal(facts.countsLabel, "3 / 4 covered · 5 collected"); assert.doesNotMatch(facts.countsTitle, COVERED_EXCEEDED_DENOMINATOR_COPY); @@ -238,12 +238,12 @@ test("THE CLAMP: collected over-report does not imply covered exceeded the denom test("a known covered numerator renders covered / considered without hiding the collected count", () => { const facts = formatStreamCollectionFacts( entry({ - stream: "items", collected: 0, considered: 10, - covered: 10, coverage_condition: "complete", + covered: 10, forward_disposition: "complete", + stream: "items", }) ); assert.equal(facts.countsLabel, "10 / 10 covered · 0 collected"); @@ -255,11 +255,11 @@ test("a known covered numerator renders covered / considered without hiding the test("a satisfied known denominator can read complete (the reference's verdict, not ours)", () => { const facts = formatStreamCollectionFacts( entry({ - stream: "items", collected: 10, considered: 10, coverage_condition: "complete", forward_disposition: "complete", + stream: "items", }) ); assert.equal(facts.countsLabel, "10 / 10 collected"); @@ -271,7 +271,7 @@ test("a satisfied known denominator can read complete (the reference's verdict, }); test("zero collected with no considered denominator shows no fabricated progress number", () => { - const facts = formatStreamCollectionFacts(entry({ stream: "items", collected: 0, considered: "unknown" })); + const facts = formatStreamCollectionFacts(entry({ collected: 0, considered: "unknown", stream: "items" })); assert.equal(facts.countsLabel, null); }); @@ -385,7 +385,7 @@ test("an accepted-absence coverage axis never degrades a stream's coverage tone" // the coverage axis specifically, which is the signal that must not degrade.) for (const axis of ["deferred", "inventory_only", "unavailable", "unsupported"] as const) { const facts = formatStreamCollectionFacts( - entry({ coverage_condition: axis, forward_disposition: "unmeasured", collected: 0, considered: "unknown" }) + entry({ collected: 0, considered: "unknown", coverage_condition: axis, forward_disposition: "unmeasured" }) ); assert.equal(facts.coverage.tone, "neutral", `${axis} coverage tone must stay neutral`); assert.notEqual(facts.tone, "warning"); @@ -398,7 +398,7 @@ test("the stream-row deferred pill reads optional/not-collected, not policy jarg // per-stream row must pick up the same visible fix as the connection-level // chip — the owner reads this exact value on the source detail page. const facts = formatStreamCollectionFacts( - entry({ coverage_condition: "deferred", forward_disposition: "unmeasured", collected: 0, considered: "unknown" }) + entry({ collected: 0, considered: "unknown", coverage_condition: "deferred", forward_disposition: "unmeasured" }) ); assert.doesNotMatch(facts.coverage.value, /\bdeferre?s?\b/i); assert.match(facts.coverage.value, /optional/i); @@ -411,7 +411,7 @@ test("required missing evidence (unknown coverage) stays distinct from accepted // evidence, never as a settled accepted-absence policy, so the two states // remain distinguishable on the stream row after the copy-only fix. const unmeasured = formatStreamCollectionFacts( - entry({ coverage_condition: "unknown", forward_disposition: "unmeasured", collected: 0, considered: "unknown" }) + entry({ collected: 0, considered: "unknown", coverage_condition: "unknown", forward_disposition: "unmeasured" }) ); assert.equal(unmeasured.coverage.value, "unknown"); assert.notEqual( @@ -428,7 +428,7 @@ test("active pending work (checking) stays distinct from accepted absence", () = // A stream mid-run reads `checking`, which is a different signal than a // settled accepted-absence policy — neither implies the other. const checking = formatStreamCollectionFacts( - entry({ coverage_condition: "unknown", forward_disposition: "checking", collected: 0, considered: "unknown" }) + entry({ collected: 0, considered: "unknown", coverage_condition: "unknown", forward_disposition: "checking" }) ); assert.equal(checking.disposition?.value, "checking"); for (const axis of ["deferred", "inventory_only", "unavailable", "unsupported"] as const) { diff --git a/apps/console/src/app/(console)/lib/collection-report.ts b/apps/console/src/app/(console)/lib/collection-report.ts index 34e0cfdd6..0019695e0 100644 --- a/apps/console/src/app/(console)/lib/collection-report.ts +++ b/apps/console/src/app/(console)/lib/collection-report.ts @@ -157,6 +157,8 @@ export function runStatusWithCollectionReportGaps( * nothing and has no denominator returns `null` — there is no honest progress * number to show. */ + +// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: collection status combinations map to one owner-facing count and title at this presentation boundary. function buildCountsLine(entry: RefCollectionReportEntry): { label: string | null; title: string } { const collected = Number.isFinite(entry.collected) ? entry.collected : 0; const collectedText = collected.toLocaleString(); @@ -241,9 +243,9 @@ function buildSkipLabel(skip: RefCollectionReportEntry["skipped"]): string | nul const TONE_RANK: Record = { danger: 3, - warning: 2, - success: 1, neutral: 0, + success: 1, + warning: 2, }; /** The stronger of two tones (danger > warning > success > neutral). */ @@ -285,15 +287,15 @@ export function formatStreamCollectionFacts(entry: RefCollectionReportEntry): St } return { - stream: entry.stream, - coverage, - disposition, countsLabel: counts.label, countsTitle: counts.title, + coverage, + disposition, pendingDetailGaps, pendingDetailGapsIsFloor, pendingDetailGapsLabel: formatPendingDetailGapsLabel(pendingDetailGaps, pendingDetailGapsIsFloor), skipLabel, + stream: entry.stream, tone, }; } diff --git a/apps/console/src/app/(console)/lib/connection-catalog.test.ts b/apps/console/src/app/(console)/lib/connection-catalog.test.ts index 705108447..e6fae454a 100644 --- a/apps/console/src/app/(console)/lib/connection-catalog.test.ts +++ b/apps/console/src/app/(console)/lib/connection-catalog.test.ts @@ -54,18 +54,16 @@ async function loadCommittedManifests(): Promise { const repoRoot = new URL("../../../../../../", import.meta.url); const manifestsDir = new URL("packages/polyfill-connectors/manifests/", repoRoot); const files = await readdir(fileURLToPath(manifestsDir)); - const manifests: CatalogManifestLike[] = []; - for (const file of files) { - if (!file.endsWith(".json")) { - continue; - } - const raw = await readFile(fileURLToPath(new URL(file, manifestsDir)), "utf8"); - const m = JSON.parse(raw) as CatalogManifestLike; - if (m.connector_id) { - manifests.push(m); - } - } - return manifests; + const manifests = await Promise.all( + files + .filter((file) => file.endsWith(".json")) + .map(async (file) => { + const raw = await readFile(fileURLToPath(new URL(file, manifestsDir)), "utf8"); + const m = JSON.parse(raw) as CatalogManifestLike; + return m.connector_id ? m : null; + }) + ); + return manifests.filter((manifest): manifest is CatalogManifestLike => manifest !== null); } test("catalogModalityFromManifest mirrors the filesystem>browser>network precedence", () => { @@ -84,7 +82,7 @@ test("catalogModalityFromManifest mirrors the filesystem>browser>network precede assert.equal( catalogModalityFromManifest({ connector_id: "x", - runtime_requirements: { bindings: { filesystem: {}, browser: {} } }, + runtime_requirements: { bindings: { browser: {}, filesystem: {} } }, }), "local_collector" ); @@ -150,12 +148,12 @@ test("browser-bound static-secret-capable connectors get the same dual choice ge display_name: "Browser Sample", runtime_requirements: { bindings: { browser: { required: true } } }, setup: { - modality: "static_secret", credential_capture: { + fields: [{ label: "Provider secret", name: "secret", required: true, secret: true }], kind: "username_password", label: "Browser sign-in", - fields: [{ name: "secret", label: "Provider secret", required: true, secret: true }], }, + modality: "static_secret", }, } as CatalogManifestLike, ]); @@ -176,12 +174,12 @@ test("non-browser static-secret connectors keep the existing single capture path display_name: "Gmail", runtime_requirements: { bindings: { network: { required: true } } }, setup: { - modality: "static_secret", credential_capture: { + fields: [{ label: "Provider secret", name: "secret", required: true, secret: true }], kind: "app_password", label: "Gmail app password", - fields: [{ name: "secret", label: "Provider secret", required: true, secret: true }], }, + modality: "static_secret", }, } as CatalogManifestLike, ]); @@ -247,8 +245,8 @@ function manualUploadConnectManifest(connectorId: string): CatalogManifestLike { return { ...manualUploadManifest(connectorId), setup: { - modality: "manual_or_upload", manual_or_upload: { + accepted_file_names: ["Timeline.json"], acquisition_methods: [ { detail: "Use the phone export and upload the JSON file.", @@ -264,10 +262,10 @@ function manualUploadConnectManifest(connectorId: string): CatalogManifestLike { posture: "advanced", }, ], - accepted_file_names: ["Timeline.json"], import_dir_env_var: "GOOGLE_MAPS_TIMELINE_DIR", label: "Timeline export", }, + modality: "manual_or_upload", }, }; } @@ -381,15 +379,15 @@ test("other network connectors stay flatly api_network_unsupported", async () => test("provider-authorization deployment blockers are separate from unsupported network entries", () => { const catalog = buildConnectorCatalog([ { - connector_id: "fitness_oauth", - display_name: "Fitness OAuth", - runtime_requirements: { bindings: { network: { required: true } } }, capabilities: { auth: { - kind: "oauth", deployment_config: ["FITNESS_OAUTH_CLIENT_ID", "FITNESS_OAUTH_CLIENT_SECRET"], + kind: "oauth", }, }, + connector_id: "fitness_oauth", + display_name: "Fitness OAuth", + runtime_requirements: { bindings: { network: { required: true } } }, }, ]); const [entry] = catalog; diff --git a/apps/console/src/app/(console)/lib/connection-catalog.ts b/apps/console/src/app/(console)/lib/connection-catalog.ts index 65c5a24e9..af65fb757 100644 --- a/apps/console/src/app/(console)/lib/connection-catalog.ts +++ b/apps/console/src/app/(console)/lib/connection-catalog.ts @@ -211,8 +211,8 @@ export function buildConnectorCatalog(manifests: readonly CatalogManifestLike[]) connectorKey, deploymentReadiness: plan.deploymentReadiness, displayName: displayNameFor(manifest, connectorKey), - modality: plan.connectorModality, disposition: plan.catalogDisposition, + modality: plan.connectorModality, nextStepKind: plan.nextStepKind, proofGate: plan.proofGate, runbookPath: plan.runbookPath, diff --git a/apps/console/src/app/(console)/lib/connection-control-result.test.ts b/apps/console/src/app/(console)/lib/connection-control-result.test.ts index c82c75ad8..3c0eea9c6 100644 --- a/apps/console/src/app/(console)/lib/connection-control-result.test.ts +++ b/apps/console/src/app/(console)/lib/connection-control-result.test.ts @@ -65,8 +65,8 @@ test("revoke on an unexpected status throws a described error", () => { test("delete 200 maps to deleted and carries the record count", () => { assert.deepEqual(classifyDeleteConnectionResponse(200, { deleted: true, deleted_record_count: 42 }, null), { - status: "deleted", deletedRecordCount: 42, + status: "deleted", }); }); diff --git a/apps/console/src/app/(console)/lib/connection-control-result.ts b/apps/console/src/app/(console)/lib/connection-control-result.ts index 196c58f82..2e2692b68 100644 --- a/apps/console/src/app/(console)/lib/connection-control-result.ts +++ b/apps/console/src/app/(console)/lib/connection-control-result.ts @@ -82,11 +82,11 @@ export function connectionControlErrorCode(body: unknown): string | null { if (!body || typeof body !== "object") { return null; } - const error = (body as { error?: unknown }).error; + const { error } = body as { error?: unknown }; if (!error || typeof error !== "object") { return null; } - const code = (error as { code?: unknown }).code; + const { code } = error as { code?: unknown }; return typeof code === "string" ? code : null; } diff --git a/apps/console/src/app/(console)/lib/connection-evidence.test.ts b/apps/console/src/app/(console)/lib/connection-evidence.test.ts index eda7871b3..8a1f6a805 100644 --- a/apps/console/src/app/(console)/lib/connection-evidence.test.ts +++ b/apps/console/src/app/(console)/lib/connection-evidence.test.ts @@ -66,12 +66,12 @@ test("formatForwardDisposition renders unmeasured coverage as resting missing ev function backlog(overrides: Partial = {}): RefDetailGapBacklog { return { + max_attempt_count: 0, + next_attempt_at: null, pending: 0, pending_is_floor: false, pending_other: 0, pending_other_is_floor: false, - max_attempt_count: 0, - next_attempt_at: null, recovered: null, ...overrides, }; @@ -79,16 +79,16 @@ function backlog(overrides: Partial = {}): RefDetailGapBack function snapshot(overrides: Partial = {}): RefConnectionHealthSnapshot { return { - state: "healthy", - reason_code: null, - unknown_reasons: [], - last_success_at: "2026-05-19T12:00:00Z", - next_attempt_at: null, - next_action: null, + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "idle" }, badges: { stale: false, syncing: false }, - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "idle" }, conditions: [], dominant_condition_id: null, + last_success_at: "2026-05-19T12:00:00Z", + next_action: null, + next_attempt_at: null, + reason_code: null, + state: "healthy", + unknown_reasons: [], ...overrides, }; } @@ -96,11 +96,11 @@ function snapshot(overrides: Partial = {}): RefConn function baseOverview(overrides: Partial = {}): ConnectorOverview { return { connector: { connector_id: "demo", display_name: "Demo" }, - streams: [], - totalRecords: 0, + isRunning: false, lastRun: null, lastSuccessfulRun: null, - isRunning: false, + streams: [], + totalRecords: 0, ...overrides, }; } @@ -268,39 +268,39 @@ test("outbox active is colour-coded as a progressing (non-neutral) state", () => test("formatSourceOutboxState distinguishes granular local collector states", () => { const failedUploadOutbox = formatSourceOutboxState({ - outbox_state: "dead_letter", outbox_diagnostics: { dead_letter: 1 }, + outbox_state: "dead_letter", }); assert.equal(failedUploadOutbox.tone, "danger"); assert.equal(failedUploadOutbox.label, "Outbox · failed uploads"); assert.equal(failedUploadOutbox.value, "failed uploads"); assert.doesNotMatch(failedUploadOutbox.title, /dead-letter/i); assert.equal( - formatSourceOutboxState({ outbox_state: "stale", outbox_diagnostics: { stale_leases: 1 } }).tone, + formatSourceOutboxState({ outbox_diagnostics: { stale_leases: 1 }, outbox_state: "stale" }).tone, "danger" ); assert.equal( - formatSourceOutboxState({ outbox_state: "retrying", outbox_diagnostics: { retrying: 1 } }).tone, + formatSourceOutboxState({ outbox_diagnostics: { retrying: 1 }, outbox_state: "retrying" }).tone, "warning" ); assert.equal( - formatSourceOutboxState({ outbox_state: "pending", outbox_diagnostics: { pending: 1 } }).label, + formatSourceOutboxState({ outbox_diagnostics: { pending: 1 }, outbox_state: "pending" }).label, "Outbox · pending" ); assert.equal( - formatSourceOutboxState({ outbox_state: "backlog", outbox_diagnostics: { backlog_open: 1 } }).tone, + formatSourceOutboxState({ outbox_diagnostics: { backlog_open: 1 }, outbox_state: "backlog" }).tone, "warning" ); assert.equal( - formatSourceOutboxState({ outbox_state: "drained", outbox_diagnostics: { total: 2, succeeded: 2 } }).tone, + formatSourceOutboxState({ outbox_diagnostics: { succeeded: 2, total: 2 }, outbox_state: "drained" }).tone, "success" ); - assert.equal(formatSourceOutboxState({ outbox_state: undefined, outbox_diagnostics: null }).tone, "neutral"); + assert.equal(formatSourceOutboxState({ outbox_diagnostics: null, outbox_state: undefined }).tone, "neutral"); }); test("summarizeAxisChips omits attention when none and always includes coverage/freshness/outbox", () => { const out = summarizeAxisChips( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "idle" } }).axes + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "idle" } }).axes ); assert.equal( out.some((c) => c.label.startsWith("Attention")), @@ -315,7 +315,7 @@ test("summarizeAxisChips omits attention when none and always includes coverage/ test("summarizeAxisChips surfaces attention when open", () => { const out = summarizeAxisChips( - snapshot({ axes: { coverage: "gaps", freshness: "fresh", attention: "open", outbox: "idle" } }).axes + snapshot({ axes: { attention: "open", coverage: "gaps", freshness: "fresh", outbox: "idle" } }).axes ); assert.equal(out.length, 4); assert.ok(out.some((c) => c.label.startsWith("Attention"))); @@ -354,7 +354,7 @@ test("summarizeAxisChips omits the outbox chip for a non-local connection with u // The exact owner-reported defect: a Gmail/Chase-class connection shows // "Outbox · unknown" purely because the reference has no heartbeats for it. const out = summarizeAxisChips( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "unknown" } }).axes, + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "unknown" } }).axes, { isLocalDeviceBacked: false } ); assert.equal(out.length, 2); @@ -366,7 +366,7 @@ test("summarizeAxisChips omits the outbox chip for a non-local connection with u test("summarizeAxisChips keeps the outbox chip for a local-backed connection with unknown outbox, sharpened to 'evidence unavailable'", () => { const out = summarizeAxisChips( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "unknown" } }).axes, + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "unknown" } }).axes, { isLocalDeviceBacked: true } ); assert.equal(out.length, 3); @@ -382,7 +382,7 @@ test("summarizeAxisChips shows a stalled outbox even for a non-local connection // Defensive: if the reference ever projects a real stalled verdict without a // local_device_progress row, the danger signal must not be hidden. const out = summarizeAxisChips( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "stalled" } }).axes, + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "stalled" } }).axes, { isLocalDeviceBacked: false } ); const outbox = out.find((c) => c.dimension === "Outbox"); @@ -392,7 +392,7 @@ test("summarizeAxisChips shows a stalled outbox even for a non-local connection test("summarizeAxisChips shows a colour-coded active outbox for a local-backed connection", () => { const out = summarizeAxisChips( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "active" } }).axes, + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "active" } }).axes, { isLocalDeviceBacked: true } ); const outbox = out.find((c) => c.dimension === "Outbox"); @@ -404,7 +404,7 @@ test("summarizeAxisChips defaults to omitting an unknown outbox when no local-ba // Back-compat: callers that have not threaded the signal get the honest // (non-local) default — an absence-default unknown outbox is suppressed. const out = summarizeAxisChips( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "unknown" } }).axes + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "unknown" } }).axes ); assert.equal( out.some((c) => c.dimension === "Outbox"), @@ -436,28 +436,28 @@ test("formatProjectionFreshness handles missing snapshot", () => { test("formatDominantCondition surfaces only false dominant evidence", () => { const out = formatDominantCondition( snapshot({ - state: "blocked", - dominant_condition_id: "CredentialsValid:auth_expired", conditions: [ { + expires_at: null, id: "CredentialsValid:auth_expired", - type: "CredentialsValid", - status: "false", - severity: "blocked", - reason: "auth_expired", message: "The source rejected the configured credentials.", - origin: "readiness", observed_at: null, - expires_at: null, - sensitivity: "secret_redacted", + origin: "readiness", + reason: "auth_expired", remediation: { action: "refresh_credentials", label: "Reconnect this account", retryable: false, target: "credentials", }, + sensitivity: "secret_redacted", + severity: "blocked", + status: "false", + type: "CredentialsValid", }, ], + dominant_condition_id: "CredentialsValid:auth_expired", + state: "blocked", }) ); assert.equal(out?.tone, "danger"); @@ -482,12 +482,12 @@ test("formatLastDurableProgress reports last successful event count when present hasError: false, lastRun: null, lastSuccessfulRun: { - run_id: "r1", + event_count: 42, + failure_reason: null, first_at: "x", last_at: "y", - event_count: 42, + run_id: "r1", status: "succeeded", - failure_reason: null, }, totalRecords: 50, }); @@ -499,12 +499,12 @@ test("formatLastDurableProgress reports last attempt when no success and no erro const out = formatLastDurableProgress({ hasError: false, lastRun: { - run_id: "r2", + event_count: 0, + failure_reason: "boom", first_at: "x", last_at: "y", - event_count: 0, + run_id: "r2", status: "failed", - failure_reason: "boom", }, lastSuccessfulRun: null, totalRecords: 0, @@ -579,13 +579,13 @@ test("formatLastDurableProgress still prefers scheduler-run evidence over local- hasError: false, lastRun: null, lastSuccessfulRun: { - run_id: "r9", - first_at: "x", - last_at: "y", event_count: 3, - status: "succeeded", failure_reason: null, + first_at: "x", known_gaps: [], + last_at: "y", + run_id: "r9", + status: "succeeded", }, localDeviceProgress: { last_heartbeat_at: "2026-05-22T16:30:00Z", @@ -603,11 +603,11 @@ test("formatLastDurableProgress still prefers scheduler-run evidence over local- test("summarizeOutboxForRow returns null for idle and a label otherwise", () => { assert.equal(summarizeOutboxForRow(snapshot()), null); const stalled = summarizeOutboxForRow( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "stalled" } }) + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "stalled" } }) ); assert.equal(stalled?.tone, "danger"); const unknown = summarizeOutboxForRow( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "unknown" } }) + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "unknown" } }) ); assert.equal(unknown?.tone, "neutral"); }); @@ -618,22 +618,22 @@ test("summarizeOutboxForRow returns null when there is no snapshot at all", () = function clearBacklogCondition(overrides: Partial = {}): RefConnectionHealthCondition { return { + expires_at: null, id: "cond-backlog", - type: "LocalExporterAvailable", - status: "false", - severity: "error", - reason: "local_exporter_stalled", message: "Local exporter work is stalled or blocked.", - origin: "local_device", observed_at: "2026-05-19T12:00:00Z", - expires_at: null, - sensitivity: "owner", + origin: "local_device", + reason: "local_exporter_stalled", remediation: { action: "clear_backlog", label: "Inspect the local collector backlog", retryable: true, target: "local_device", }, + sensitivity: "owner", + severity: "error", + status: "false", + type: "LocalExporterAvailable", ...overrides, }; } @@ -641,9 +641,9 @@ function clearBacklogCondition(overrides: Partial test("summarizeOutboxStallRemediation surfaces the reference remediation label for a clear_backlog condition", () => { const remediation = summarizeOutboxStallRemediation( snapshot({ - state: "degraded", - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "stalled" }, + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "stalled" }, conditions: [clearBacklogCondition()], + state: "degraded", }) ); assert.equal(remediation?.label, "Inspect the local collector backlog"); @@ -652,7 +652,7 @@ test("summarizeOutboxStallRemediation surfaces the reference remediation label f test("summarizeOutboxStallRemediation stays quiet for a stalled axis without a clear_backlog action", () => { const remediation = summarizeOutboxStallRemediation( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "stalled" } }) + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "stalled" } }) ); assert.equal(remediation, null); @@ -662,7 +662,7 @@ test("summarizeOutboxStallRemediation stays quiet for healthy/idle/active/unknow for (const outbox of ["idle", "active", "unknown"] as const) { assert.equal( summarizeOutboxStallRemediation( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox } }) + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox } }) ), null, `expected no remediation for outbox=${outbox}` @@ -676,7 +676,7 @@ test("summarizeOutboxStallRemediation ignores a clear_backlog remediation on a r // remediation noise once the outbox is no longer stalled. const remediation = summarizeOutboxStallRemediation( snapshot({ - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "idle" }, + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "idle" }, conditions: [clearBacklogCondition({ status: "true" })], }) ); @@ -686,7 +686,7 @@ test("summarizeOutboxStallRemediation ignores a clear_backlog remediation on a r test("summarizeOutboxStallRemediation: scale is null when no count rollup is available", () => { const remediation = summarizeOutboxStallRemediation( snapshot({ - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "stalled" }, + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "stalled" }, conditions: [clearBacklogCondition()], }) ); @@ -696,14 +696,14 @@ test("summarizeOutboxStallRemediation: scale is null when no count rollup is ava test("summarizeOutboxStallRemediation: surfaces a count-backed scale from outbox_counts on a stall", () => { const remediation = summarizeOutboxStallRemediation( snapshot({ - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "stalled" }, + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "stalled" }, conditions: [clearBacklogCondition()], }), { last_heartbeat_at: "2026-05-19T11:55:00Z", last_heartbeat_status: "blocked", last_ingest_at: null, - outbox_counts: { pending: 12, dead_letter: 2, stale_leases: 1, total: 15 }, + outbox_counts: { dead_letter: 2, pending: 12, stale_leases: 1, total: 15 }, records_pending: 12, source_count: 1, } @@ -714,14 +714,14 @@ test("summarizeOutboxStallRemediation: surfaces a count-backed scale from outbox test("summarizeOutboxStallRemediation: scale omits zero categories so it never reads as alarming noise", () => { const remediation = summarizeOutboxStallRemediation( snapshot({ - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "stalled" }, + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "stalled" }, conditions: [clearBacklogCondition()], }), { last_heartbeat_at: "2026-05-19T11:55:00Z", last_heartbeat_status: "blocked", last_ingest_at: null, - outbox_counts: { pending: 0, dead_letter: 3, stale_leases: 0, total: 3 }, + outbox_counts: { dead_letter: 3, pending: 0, stale_leases: 0, total: 3 }, records_pending: 0, source_count: 1, } @@ -735,12 +735,12 @@ test("summarizeOutboxStallRemediation: counts never attach to a quiet (non-stall for (const outbox of ["idle", "active", "unknown"] as const) { assert.equal( summarizeOutboxStallRemediation( - snapshot({ axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox } }), + snapshot({ axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox } }), { last_heartbeat_at: "2026-05-19T11:55:00Z", last_heartbeat_status: "healthy", last_ingest_at: "2026-05-19T11:55:00Z", - outbox_counts: { pending: 5, dead_letter: 1 }, + outbox_counts: { dead_letter: 1, pending: 5 }, records_pending: 5, source_count: 1, } @@ -939,8 +939,8 @@ test("deriveConnectionStatusDisplay: healthy scheduled connection reads as a hea const out = deriveConnectionStatusDisplay({ hasDurableProgress: true, health: snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "idle" }, state: "healthy", - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "idle" }, }), localDeviceProgress: null, }); @@ -957,8 +957,8 @@ test("deriveConnectionStatusDisplay: stale-but-healthy connection does not claim const out = deriveConnectionStatusDisplay({ hasDurableProgress: true, health: snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "stale", outbox: "idle" }, state: "healthy", - axes: { coverage: "complete", freshness: "stale", attention: "none", outbox: "idle" }, }), localDeviceProgress: null, }); @@ -973,8 +973,8 @@ test("deriveConnectionStatusDisplay: fresh-and-healthy connection still reads cu const out = deriveConnectionStatusDisplay({ hasDurableProgress: true, health: snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "idle" }, state: "healthy", - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "idle" }, }), localDeviceProgress: null, }); @@ -1004,8 +1004,8 @@ test("deriveConnectionStatusDisplay: local collector with ingest evidence and id const out = deriveConnectionStatusDisplay({ hasDurableProgress: true, health: snapshot({ + axes: { attention: "none", coverage: "unknown", freshness: "unknown", outbox: "idle" }, state: "idle", - axes: { coverage: "unknown", freshness: "unknown", attention: "none", outbox: "idle" }, }), localDeviceProgress: { last_heartbeat_at: "2026-05-22T16:30:00Z", @@ -1054,8 +1054,8 @@ test("deriveConnectionStatusDisplay: outbox=active overrides idle as Syncing reg const out = deriveConnectionStatusDisplay({ hasDurableProgress: true, health: snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "active" }, state: "idle", - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "active" }, }), localDeviceProgress: null, }); @@ -1067,29 +1067,29 @@ test("deriveConnectionStatusDisplay: blocked connection surfaces dominant condit const out = deriveConnectionStatusDisplay({ hasDurableProgress: false, health: snapshot({ - state: "blocked", - reason_code: "credential_rejected", - dominant_condition_id: "CredentialsValid:credential_rejected", conditions: [ { + expires_at: null, id: "CredentialsValid:credential_rejected", - type: "CredentialsValid", - status: "false", - severity: "blocked", - reason: "credential_rejected", message: "The source rejected the configured credentials.", - origin: "readiness", observed_at: null, - expires_at: null, - sensitivity: "secret_redacted", + origin: "readiness", + reason: "credential_rejected", remediation: { action: "refresh_credentials", label: "Reconnect this account", retryable: false, target: "credentials", }, + sensitivity: "secret_redacted", + severity: "blocked", + status: "false", + type: "CredentialsValid", }, ], + dominant_condition_id: "CredentialsValid:credential_rejected", + reason_code: "credential_rejected", + state: "blocked", }), localDeviceProgress: null, }); @@ -1103,7 +1103,7 @@ test("deriveConnectionStatusDisplay: blocked connection surfaces dominant condit test("deriveConnectionStatusDisplay: source-pressure cooling_off reads as catch-up, never a raw token", () => { const out = deriveConnectionStatusDisplay({ hasDurableProgress: true, - health: snapshot({ state: "cooling_off", reason_code: "source_pressure" }), + health: snapshot({ reason_code: "source_pressure", state: "cooling_off" }), localDeviceProgress: null, }); assert.equal(out.label, "Cooling off"); @@ -1118,7 +1118,7 @@ test("deriveConnectionStatusDisplay: source-pressure cooling_off reads as catch- test("deriveConnectionStatusDisplay: failure-backoff cooling_off keeps the retry-wait copy", () => { const out = deriveConnectionStatusDisplay({ hasDurableProgress: true, - health: snapshot({ state: "cooling_off", reason_code: "scheduler_backoff_active" }), + health: snapshot({ reason_code: "scheduler_backoff_active", state: "cooling_off" }), localDeviceProgress: null, }); assert.equal(out.label, "Cooling off"); @@ -1129,8 +1129,8 @@ test("deriveConnectionStatusDisplay: degraded with partial coverage reads as Par const out = deriveConnectionStatusDisplay({ hasDurableProgress: true, health: snapshot({ + axes: { attention: "none", coverage: "partial", freshness: "fresh", outbox: "idle" }, state: "degraded", - axes: { coverage: "partial", freshness: "fresh", attention: "none", outbox: "idle" }, }), localDeviceProgress: null, }); @@ -1143,8 +1143,8 @@ test("deriveConnectionStatusDisplay: degraded with retryable coverage reads as R const out = deriveConnectionStatusDisplay({ hasDurableProgress: true, health: snapshot({ + axes: { attention: "none", coverage: "retryable_gap", freshness: "unknown", outbox: "unknown" }, state: "degraded", - axes: { coverage: "retryable_gap", freshness: "unknown", attention: "none", outbox: "unknown" }, }), localDeviceProgress: null, }); @@ -1160,28 +1160,28 @@ test("deriveConnectionStatusDisplay: needs_attention surfaces dominant condition const out = deriveConnectionStatusDisplay({ hasDurableProgress: true, health: snapshot({ - state: "needs_attention", - dominant_condition_id: "AttentionClear:otp_required", conditions: [ { + expires_at: null, id: "AttentionClear:otp_required", - type: "AttentionClear", - status: "false", - severity: "warning", - reason: "otp_required", message: "A one-time passcode is required to continue.", - origin: "operator", observed_at: null, - expires_at: null, - sensitivity: "owner", + origin: "operator", + reason: "otp_required", remediation: { action: "provide_value", label: "Provide the OTP", retryable: true, target: "dashboard", }, + sensitivity: "owner", + severity: "warning", + status: "false", + type: "AttentionClear", }, ], + dominant_condition_id: "AttentionClear:otp_required", + state: "needs_attention", }), localDeviceProgress: null, }); @@ -1226,8 +1226,8 @@ test("next-step guidance is suppressed when a structured next_action already ren hasDominantCondition: false, hasStructuredNextAction: true, health: snapshot({ + axes: { attention: "open", coverage: "complete", freshness: "stale", outbox: "idle" }, state: "needs_attention", - axes: { coverage: "complete", freshness: "stale", attention: "open", outbox: "idle" }, }), supportsOwnerSync: true, }); @@ -1277,8 +1277,8 @@ test("action-bearing guidance still fires even when a dominant condition is pres hasDominantCondition: true, hasStructuredNextAction: false, health: snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "stalled" }, state: "degraded", - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "stalled" }, }), supportsOwnerSync: true, }); @@ -1290,7 +1290,7 @@ test("cooling_off guidance names the next attempt time when known", () => { const out = deriveConnectionNextStep({ hasDominantCondition: false, hasStructuredNextAction: false, - health: snapshot({ state: "cooling_off", next_attempt_at: "2026-05-19T13:00:00Z" }), + health: snapshot({ next_attempt_at: "2026-05-19T13:00:00Z", state: "cooling_off" }), supportsOwnerSync: true, }); assert.ok(out); @@ -1304,7 +1304,7 @@ test("cooling_off with no source-pressure reason still reads as failure backoff" const out = deriveConnectionNextStep({ hasDominantCondition: false, hasStructuredNextAction: false, - health: snapshot({ state: "cooling_off", next_attempt_at: "2026-05-19T13:00:00Z" }), + health: snapshot({ next_attempt_at: "2026-05-19T13:00:00Z", state: "cooling_off" }), supportsOwnerSync: true, }); assert.ok(out); @@ -1321,9 +1321,9 @@ test("source-pressure cooling_off reads as catching up, not a failure", () => { hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", next_attempt_at: "2026-05-19T13:00:00Z", + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -1340,7 +1340,7 @@ test("source-pressure cooling_off without a next attempt still reads as catching const out = deriveConnectionNextStep({ hasDominantCondition: false, hasStructuredNextAction: false, - health: snapshot({ state: "cooling_off", reason_code: "source_pressure", next_attempt_at: null }), + health: snapshot({ next_attempt_at: null, reason_code: "source_pressure", state: "cooling_off" }), supportsOwnerSync: true, }); assert.ok(out); @@ -1369,10 +1369,10 @@ test("source-pressure cooling_off attaches a backlog count plus the backlog's ow hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", + detail_gap_backlog: backlog({ next_attempt_at: "2026-05-19T13:30:00Z", pending: 42 }), next_attempt_at: "2026-05-19T13:00:00Z", - detail_gap_backlog: backlog({ pending: 42, next_attempt_at: "2026-05-19T13:30:00Z" }), + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -1390,10 +1390,10 @@ test("the backlog resume floor is independent of the connection-level scheduler hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", + detail_gap_backlog: backlog({ next_attempt_at: "2026-05-19T14:00:00Z", pending: 8 }), next_attempt_at: null, - detail_gap_backlog: backlog({ pending: 8, next_attempt_at: "2026-05-19T14:00:00Z" }), + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -1406,10 +1406,10 @@ test("source-pressure backlog with a bounded read reads as a floor, never exact" hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", - next_attempt_at: null, detail_gap_backlog: backlog({ pending: 100, pending_is_floor: true }), + next_attempt_at: null, + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -1424,10 +1424,10 @@ test("an unreadable backlog rollup never invents a count", () => { hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", - next_attempt_at: "2026-05-19T13:00:00Z", detail_gap_backlog: null, + next_attempt_at: "2026-05-19T13:00:00Z", + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -1440,10 +1440,10 @@ test("a drained backlog with recoveries reads as caught up, not broken", () => { hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", - next_attempt_at: null, detail_gap_backlog: backlog({ pending: 0, recovered: 17 }), + next_attempt_at: null, + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -1457,10 +1457,10 @@ test("a drained backlog with no recovery aggregate still reads as caught up (rea hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", - next_attempt_at: null, detail_gap_backlog: backlog({ pending: 0, recovered: null }), + next_attempt_at: null, + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -1473,10 +1473,10 @@ test("a source-pressure-drained backlog does not say caught up when other detail hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", - next_attempt_at: null, detail_gap_backlog: backlog({ pending: 0, pending_other: 1899, pending_other_is_floor: true }), + next_attempt_at: null, + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -1490,10 +1490,10 @@ test("degraded+retryable_gap manual path carries the backlog count", () => { hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "degraded", - next_attempt_at: null, - axes: { coverage: "retryable_gap", freshness: "unknown", attention: "none", outbox: "unknown" }, + axes: { attention: "none", coverage: "retryable_gap", freshness: "unknown", outbox: "unknown" }, detail_gap_backlog: backlog({ pending: 3 }), + next_attempt_at: null, + state: "degraded", }), supportsOwnerSync: true, }); @@ -1507,10 +1507,10 @@ test("a singular pending gap uses singular noun", () => { hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "degraded", - next_attempt_at: null, - axes: { coverage: "retryable_gap", freshness: "unknown", attention: "none", outbox: "idle" }, + axes: { attention: "none", coverage: "retryable_gap", freshness: "unknown", outbox: "idle" }, detail_gap_backlog: backlog({ pending: 1 }), + next_attempt_at: null, + state: "degraded", }), supportsOwnerSync: false, }); @@ -1526,9 +1526,9 @@ test("the device-outbox scale and the source-pressure backlog scale never collid hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", detail_gap_backlog: backlog({ pending: 5 }), + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -1541,8 +1541,8 @@ test("degraded+partial coverage routes to a coverage review, not a sync", () => hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ + axes: { attention: "none", coverage: "gaps", freshness: "fresh", outbox: "idle" }, state: "degraded", - axes: { coverage: "gaps", freshness: "fresh", attention: "none", outbox: "idle" }, }), supportsOwnerSync: true, }); @@ -1556,9 +1556,9 @@ test("degraded+retryable_gap with owner sync offers a non-alarming way to contin hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "degraded", + axes: { attention: "none", coverage: "retryable_gap", freshness: "unknown", outbox: "unknown" }, next_attempt_at: null, - axes: { coverage: "retryable_gap", freshness: "unknown", attention: "none", outbox: "unknown" }, + state: "degraded", }), supportsOwnerSync: true, }); @@ -1573,9 +1573,9 @@ test("degraded+retryable_gap without owner sync points at the collector host", ( hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "degraded", + axes: { attention: "none", coverage: "retryable_gap", freshness: "unknown", outbox: "idle" }, next_attempt_at: null, - axes: { coverage: "retryable_gap", freshness: "unknown", attention: "none", outbox: "idle" }, + state: "degraded", }), supportsOwnerSync: false, }); @@ -1590,9 +1590,9 @@ test("degraded+retryable_gap suppresses the CTA only when an automatic attempt i hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "degraded", + axes: { attention: "none", coverage: "retryable_gap", freshness: "unknown", outbox: "unknown" }, next_attempt_at: "2026-05-19T13:00:00Z", - axes: { coverage: "retryable_gap", freshness: "unknown", attention: "none", outbox: "unknown" }, + state: "degraded", }), supportsOwnerSync: true, }); @@ -1604,8 +1604,8 @@ test("stale freshness suggests Sync now only when owner sync is supported", () = hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "stale", outbox: "idle" }, state: "degraded", - axes: { coverage: "complete", freshness: "stale", attention: "none", outbox: "idle" }, }), supportsOwnerSync: true, }); @@ -1615,8 +1615,8 @@ test("stale freshness suggests Sync now only when owner sync is supported", () = hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "stale", outbox: "idle" }, state: "degraded", - axes: { coverage: "complete", freshness: "stale", attention: "none", outbox: "idle" }, }), supportsOwnerSync: false, }); @@ -1629,8 +1629,8 @@ test("a stalled outbox always routes to the host, never a remote button", () => hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "stalled" }, state: "degraded", - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "stalled" }, }), supportsOwnerSync: true, }); @@ -1644,8 +1644,8 @@ test("an otherwise-healthy but stale connection still gets a nudge", () => { hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "stale", outbox: "idle" }, state: "healthy", - axes: { coverage: "complete", freshness: "stale", attention: "none", outbox: "idle" }, }), supportsOwnerSync: true, }); @@ -1663,8 +1663,8 @@ test("an otherwise-healthy but stale connection still gets a nudge", () => { function stalledHealth(): RefConnectionHealthSnapshot { return snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "stalled" }, state: "degraded", - axes: { coverage: "complete", freshness: "fresh", attention: "none", outbox: "stalled" }, }); } @@ -1684,7 +1684,7 @@ test("stalled-row guidance carries a count-backed scale from outbox_counts", () hasDominantCondition: false, hasStructuredNextAction: false, health: stalledHealth(), - localDeviceProgress: localDeviceProgress({ pending: 12, dead_letter: 2, stale_leases: 1, total: 15 }), + localDeviceProgress: localDeviceProgress({ dead_letter: 2, pending: 12, stale_leases: 1, total: 15 }), supportsOwnerSync: false, }); assert.ok(out); @@ -1698,7 +1698,7 @@ test("stalled-row scale omits zero categories so a counted rollup never reads as hasDominantCondition: false, hasStructuredNextAction: false, health: stalledHealth(), - localDeviceProgress: localDeviceProgress({ pending: 0, dead_letter: 3, stale_leases: 0, total: 3 }), + localDeviceProgress: localDeviceProgress({ dead_letter: 3, pending: 0, stale_leases: 0, total: 3 }), supportsOwnerSync: false, }); assert.equal(out?.scale, "3 failed uploads"); @@ -1735,7 +1735,7 @@ test("stalled-row scale is null when every stuck-work category is zero", () => { hasDominantCondition: false, hasStructuredNextAction: false, health: stalledHealth(), - localDeviceProgress: localDeviceProgress({ pending: 0, dead_letter: 0, stale_leases: 0, succeeded: 40, total: 40 }), + localDeviceProgress: localDeviceProgress({ dead_letter: 0, pending: 0, stale_leases: 0, succeeded: 40, total: 40 }), supportsOwnerSync: false, }); assert.equal(out?.scale, null); @@ -1750,12 +1750,12 @@ test("non-stalled guidance never carries a count scale, even with a populated ro hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "stale", outbox }, // freshness=stale forces a non-null guidance (Sync now / collector) so // we can assert scale is still null on that non-stalled path. state: "degraded", - axes: { coverage: "complete", freshness: "stale", attention: "none", outbox }, }), - localDeviceProgress: localDeviceProgress({ pending: 9, dead_letter: 4, total: 13 }), + localDeviceProgress: localDeviceProgress({ dead_letter: 4, pending: 9, total: 13 }), supportsOwnerSync: false, }); assert.ok(out, `expected guidance for outbox=${outbox}`); @@ -1809,12 +1809,12 @@ test("derivePrimaryRowAction keeps Sync now for an existing browser-bound connec test("derivePrimaryRowAction disables ordinary sync during source-pressure cooldown", () => { const action = derivePrimaryRowAction({ connectorId: "chatgpt", + hasLocalDeviceProgress: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", next_attempt_at: "2026-06-10T22:30:00.000Z", + reason_code: "source_pressure", + state: "cooling_off", }), - hasLocalDeviceProgress: false, }); assert.equal(action.kind, "cooldown_wait"); if (action.kind === "cooldown_wait") { @@ -1827,17 +1827,17 @@ test("derivePrimaryRowAction disables ordinary sync during source-pressure coold test("derivePrimaryRowAction disables ordinary sync when source-pressure backlog coexists with a blocked headline state", () => { const action = derivePrimaryRowAction({ connectorId: "chatgpt", + hasLocalDeviceProgress: false, health: snapshot({ - state: "blocked", - reason_code: "connector_reported_failed", - next_attempt_at: "2026-06-11T14:17:24.984Z", detail_gap_backlog: backlog({ + max_attempt_count: 10, pending: 100, pending_is_floor: true, - max_attempt_count: 10, }), + next_attempt_at: "2026-06-11T14:17:24.984Z", + reason_code: "connector_reported_failed", + state: "blocked", }), - hasLocalDeviceProgress: false, }); assert.equal(action.kind, "cooldown_wait"); if (action.kind === "cooldown_wait") { @@ -1851,8 +1851,8 @@ test("derivePrimaryRowAction disables ordinary sync when source-pressure backlog test("derivePrimaryRowAction keeps ordinary sync for non-pressure cooling-off", () => { const action = derivePrimaryRowAction({ connectorId: "github", - health: snapshot({ state: "cooling_off", reason_code: "scheduler_backoff_active" }), hasLocalDeviceProgress: false, + health: snapshot({ reason_code: "scheduler_backoff_active", state: "cooling_off" }), }); assert.equal(action.kind, "sync"); }); @@ -1907,7 +1907,7 @@ test("deriveFailureSummary returns null for null input", () => { test("deriveFailureSummary degraded → 'What's missing?' trigger, view_runs CTA", () => { const result = deriveFailureSummary( - snapshot({ state: "degraded", axes: { coverage: "gaps", freshness: "fresh", attention: "none", outbox: "idle" } }) + snapshot({ axes: { attention: "none", coverage: "gaps", freshness: "fresh", outbox: "idle" }, state: "degraded" }) ); assert.ok(result); assert.equal(result.triggerLabel, "What's missing?"); @@ -1918,8 +1918,8 @@ test("deriveFailureSummary degraded → 'What's missing?' trigger, view_runs CTA test("deriveFailureSummary degraded with complete coverage → generic prose", () => { const result = deriveFailureSummary( snapshot({ + axes: { attention: "none", coverage: "complete", freshness: "stale", outbox: "idle" }, state: "degraded", - axes: { coverage: "complete", freshness: "stale", attention: "none", outbox: "idle" }, }) ); assert.ok(result); @@ -1928,7 +1928,7 @@ test("deriveFailureSummary degraded with complete coverage → generic prose", ( }); test("deriveFailureSummary cooling_off source_pressure → honest prose about throttling", () => { - const result = deriveFailureSummary(snapshot({ state: "cooling_off", reason_code: "source_pressure" })); + const result = deriveFailureSummary(snapshot({ reason_code: "source_pressure", state: "cooling_off" })); assert.ok(result); assert.equal(result.triggerLabel, "What's wrong?"); assert.equal(result.cta, "wait"); @@ -1936,7 +1936,7 @@ test("deriveFailureSummary cooling_off source_pressure → honest prose about th }); test("deriveFailureSummary cooling_off failure back-off → retry prose", () => { - const result = deriveFailureSummary(snapshot({ state: "cooling_off", reason_code: "reddit_login_unexpected_ui" })); + const result = deriveFailureSummary(snapshot({ reason_code: "reddit_login_unexpected_ui", state: "cooling_off" })); assert.ok(result); assert.equal(result.cta, "wait"); assert.match(result.prose, PROSE_BACKOFF_RE); @@ -1957,19 +1957,19 @@ test("deriveFailureSummary needs_attention → reconnect CTA", () => { }); test("deriveFailureSummary passes through reason_code", () => { - const result = deriveFailureSummary(snapshot({ state: "blocked", reason_code: "browser_context_died" })); + const result = deriveFailureSummary(snapshot({ reason_code: "browser_context_died", state: "blocked" })); assert.ok(result); assert.equal(result.reasonCode, "browser_context_died"); }); test("deriveFailureSummary passes through next_attempt_at", () => { - const result = deriveFailureSummary(snapshot({ state: "cooling_off", next_attempt_at: "2026-05-15T15:36:00Z" })); + const result = deriveFailureSummary(snapshot({ next_attempt_at: "2026-05-15T15:36:00Z", state: "cooling_off" })); assert.ok(result); assert.equal(result.nextAttemptAt, "2026-05-15T15:36:00Z"); }); test("deriveFailureSummary passes through last_success_at", () => { - const result = deriveFailureSummary(snapshot({ state: "blocked", last_success_at: "2026-04-28T19:33:00Z" })); + const result = deriveFailureSummary(snapshot({ last_success_at: "2026-04-28T19:33:00Z", state: "blocked" })); assert.ok(result); assert.equal(result.lastSuccessAt, "2026-04-28T19:33:00Z"); }); @@ -1980,7 +1980,7 @@ test("deriveFailureSummary passes through last_success_at", () => { // and confusing. deriveFailureSummary applies the shared isSourcePressureCooldown // guard (also load-bearing for the blocked-branch fallback in this file). (spec §6.2) test("deriveFailureSummary blocked + source_pressure reason_code → cta is 'wait', NOT 'reconnect'", () => { - const result = deriveFailureSummary(snapshot({ state: "blocked", reason_code: "source_pressure" })); + const result = deriveFailureSummary(snapshot({ reason_code: "source_pressure", state: "blocked" })); assert.ok(result); assert.notEqual(result.cta, "reconnect", "source-pressure blocked must not yield reconnect CTA"); assert.equal(result.cta, "wait"); @@ -1992,10 +1992,10 @@ test("deriveFailureSummary blocked + backlog + next_attempt_at (inferred source- // deriveFailureSummary blocked branch must honour the same inference. const result = deriveFailureSummary( snapshot({ - state: "blocked", - reason_code: null, - next_attempt_at: "2026-05-20T10:00:00Z", detail_gap_backlog: backlog({ pending: 5 }), + next_attempt_at: "2026-05-20T10:00:00Z", + reason_code: null, + state: "blocked", }) ); assert.ok(result); @@ -2015,10 +2015,10 @@ test("§6.3 backlog with terminal>0 does NOT say 'caught up' — emits caveat ab hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", - next_attempt_at: null, detail_gap_backlog: backlog({ pending: 0, recovered: 10, terminal: 3 }), + next_attempt_at: null, + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -2032,10 +2032,10 @@ test("§6.3 backlog with terminal===0 still says 'caught up' (no false caveat)", hasDominantCondition: false, hasStructuredNextAction: false, health: snapshot({ - state: "cooling_off", - reason_code: "source_pressure", - next_attempt_at: null, detail_gap_backlog: backlog({ pending: 0, recovered: 10, terminal: 0 }), + next_attempt_at: null, + reason_code: "source_pressure", + state: "cooling_off", }), supportsOwnerSync: true, }); @@ -2050,7 +2050,7 @@ test("deriveStreakDots returns empty array for no runs", () => { }); test("deriveStreakDots maps succeeded → ✓ success", () => { - const dots = deriveStreakDots([{ status: "succeeded", first_at: "2026-05-15T00:00:00Z" }]); + const dots = deriveStreakDots([{ first_at: "2026-05-15T00:00:00Z", status: "succeeded" }]); assert.equal(dots.length, 1); assert.ok(dots[0]); assert.equal(dots[0].symbol, "✓"); @@ -2059,7 +2059,7 @@ test("deriveStreakDots maps succeeded → ✓ success", () => { test("deriveStreakDots maps failed → ✕ danger", () => { const dots = deriveStreakDots([ - { status: "failed", first_at: "2026-05-15T00:00:00Z", failure_reason: "browser_timeout" }, + { failure_reason: "browser_timeout", first_at: "2026-05-15T00:00:00Z", status: "failed" }, ]); assert.ok(dots[0]); assert.equal(dots[0].symbol, "✕"); @@ -2068,21 +2068,21 @@ test("deriveStreakDots maps failed → ✕ danger", () => { }); test("deriveStreakDots maps cancelled → ⊘ neutral", () => { - const dots = deriveStreakDots([{ status: "cancelled", first_at: "2026-05-15T00:00:00Z" }]); + const dots = deriveStreakDots([{ first_at: "2026-05-15T00:00:00Z", status: "cancelled" }]); assert.ok(dots[0]); assert.equal(dots[0].symbol, "⊘"); assert.equal(dots[0].tone, "neutral"); }); test("deriveStreakDots maps degraded → ⚠ warning", () => { - const dots = deriveStreakDots([{ status: "degraded", first_at: "2026-05-15T00:00:00Z" }]); + const dots = deriveStreakDots([{ first_at: "2026-05-15T00:00:00Z", status: "degraded" }]); assert.ok(dots[0]); assert.equal(dots[0].symbol, "⚠"); assert.equal(dots[0].tone, "warning"); }); test("deriveStreakDots maps succeeded_with_gaps → ⚠ warning", () => { - const dots = deriveStreakDots([{ status: "succeeded_with_gaps", first_at: "2026-05-15T00:00:00Z" }]); + const dots = deriveStreakDots([{ first_at: "2026-05-15T00:00:00Z", status: "succeeded_with_gaps" }]); assert.ok(dots[0]); assert.equal(dots[0].symbol, "⚠"); assert.equal(dots[0].tone, "warning"); @@ -2091,18 +2091,18 @@ test("deriveStreakDots maps succeeded_with_gaps → ⚠ warning", () => { test("summarizeStreakDots distinguishes clean, failed, and completed-with-gaps runs", () => { assert.equal( - summarizeStreakDots(deriveStreakDots([{ status: "succeeded", first_at: "2026-05-15T00:00:00Z" }])), + summarizeStreakDots(deriveStreakDots([{ first_at: "2026-05-15T00:00:00Z", status: "succeeded" }])), "0 failures" ); assert.equal( - summarizeStreakDots(deriveStreakDots([{ status: "succeeded_with_gaps", first_at: "2026-05-15T00:00:00Z" }])), + summarizeStreakDots(deriveStreakDots([{ first_at: "2026-05-15T00:00:00Z", status: "succeeded_with_gaps" }])), "1 with gaps" ); assert.equal( summarizeStreakDots( deriveStreakDots([ - { status: "failed", first_at: "2026-05-15T00:00:00Z" }, - { status: "succeeded_with_gaps", first_at: "2026-05-14T00:00:00Z" }, + { first_at: "2026-05-15T00:00:00Z", status: "failed" }, + { first_at: "2026-05-14T00:00:00Z", status: "succeeded_with_gaps" }, ]) ), "1 failure · 1 with gaps" @@ -2111,15 +2111,15 @@ test("summarizeStreakDots distinguishes clean, failed, and completed-with-gaps r test("deriveStreakDots caps at 14 runs", () => { const runs = Array.from({ length: 20 }, (_, i) => ({ - status: "succeeded", first_at: `2026-05-${String(i + 1).padStart(2, "0")}T00:00:00Z`, + status: "succeeded", })); assert.equal(deriveStreakDots(runs).length, 14); }); test("deriveStreakDots preserves the at timestamp for tooltip", () => { const ts = "2026-05-15T13:00:00Z"; - const dots = deriveStreakDots([{ status: "succeeded", first_at: ts }]); + const dots = deriveStreakDots([{ first_at: ts, status: "succeeded" }]); assert.ok(dots[0]); assert.equal(dots[0].at, ts); }); diff --git a/apps/console/src/app/(console)/lib/connection-evidence.ts b/apps/console/src/app/(console)/lib/connection-evidence.ts index b3b3baea0..1ec9c9701 100644 --- a/apps/console/src/app/(console)/lib/connection-evidence.ts +++ b/apps/console/src/app/(console)/lib/connection-evidence.ts @@ -18,7 +18,6 @@ * without a browser harness. */ -import { formatTotalRecordsLabel, isTotalRecordsAuthoritative } from "../sources/sources-view-model.ts"; import type { DeviceSourceInstance, RefCollectionRateSnapshot, @@ -31,6 +30,7 @@ import type { RefSchedule, } from "./ref-client.ts"; import type { ConnectorOverview, ConnectorRunRef } from "./rs-client.ts"; +import { formatTotalRecordsLabel, isTotalRecordsAuthoritative } from "./total-records.ts"; export type EvidenceTone = "neutral" | "success" | "warning" | "danger"; @@ -49,13 +49,17 @@ export interface AxisChip { const COVERAGE_LABELS: Record = { complete: { dimension: "Coverage", - value: "complete", label: "Coverage · complete", title: "All required streams have durable evidence of complete coverage.", tone: "success", + value: "complete", }, deferred: { dimension: "Coverage", + label: "Coverage · optional, not collected", + title: + "The manifest declares this coverage out of scope. This is an accepted, settled state — not a queued task — and does not block connection health.", + tone: "neutral", // The underlying axis key stays "deferred" (durable manifest/runtime // contract — see AcceptedCoveragePolicy in connector-coverage-policy.ts). // "Deferred" read as queued/pending work to owners, contradicting the @@ -63,43 +67,43 @@ const COVERAGE_LABELS: Record = { fresh: { dimension: "Freshness", - value: "fresh", label: "Freshness · fresh", title: "The last successful run is within policy.", tone: "success", + value: "fresh", }, stale: { dimension: "Freshness", - value: "stale", label: "Freshness · stale", title: "The last successful run is outside the configured freshness window.", tone: "warning", + value: "stale", }, unknown: { dimension: "Freshness", - value: "unknown", label: "Freshness · unknown", title: "Freshness cannot be derived from current evidence.", tone: "neutral", + value: "unknown", }, }; const OUTBOX_LABELS: Record = { - idle: { - dimension: "Outbox", - value: "idle", - label: "Outbox · idle", - title: "No retryable outbound work is pending.", - tone: "success", - }, active: { dimension: "Outbox", - value: "active", label: "Outbox · active", title: "Outbound work is making progress.", // `active` means the local-device outbox is draining — a healthy, @@ -184,45 +176,53 @@ const OUTBOX_LABELS: Record = { - none: null, - open: { - dimension: "Attention", - value: "open", - label: "Attention · open", - title: "Owner action is open.", - tone: "warning", - }, acknowledged: { dimension: "Attention", - value: "acknowledged", label: "Attention · acknowledged", title: "Owner action is acknowledged but not yet resolved.", tone: "warning", + value: "acknowledged", }, in_progress: { dimension: "Attention", - value: "in progress", label: "Attention · in progress", title: "Owner action is in progress.", tone: "warning", + value: "in progress", + }, + none: null, + open: { + dimension: "Attention", + label: "Attention · open", + title: "Owner action is open.", + tone: "warning", + value: "open", }, }; @@ -247,6 +247,7 @@ export function formatOutboxAxis( export function formatAttentionAxis( axis: RefConnectionHealthSnapshot["axes"]["attention"] | null | string | undefined ): AxisChip | null { + // biome-ignore lint/suspicious/noEqualsToNull: The reference payload may omit this optional axis. if (axis == null) { return null; } @@ -255,10 +256,10 @@ export function formatAttentionAxis( } return { dimension: "Attention", - value: "unknown", label: "Attention · unknown", title: `Unknown attention axis "${axis}" from the reference server.`, tone: "neutral", + value: "unknown", }; } @@ -268,18 +269,20 @@ function formatKnownAxis( fallback: T, labelPrefix: string ): AxisChip { + // biome-ignore lint/suspicious/noEqualsToNull: The reference payload may omit this optional axis. if (axis != null && Object.hasOwn(labels, axis)) { return labels[axis as T]; } const fallbackChip = labels[fallback]; + // biome-ignore lint/suspicious/noEqualsToNull: The reference payload may omit this optional axis. if (axis == null) { return fallbackChip; } return { ...fallbackChip, dimension: labelPrefix, - value: "unknown", title: `Unknown ${labelPrefix.toLowerCase()} axis "${axis}" from the reference server.`, + value: "unknown", }; } @@ -353,10 +356,10 @@ export function summarizeAxisChips( if (axes.outbox === "unknown") { out.push({ ...outbox, - value: "evidence unavailable", label: "Outbox · evidence unavailable", title: "This local collector's outbox evidence could not be read right now. It is not a current data-loss signal; retry, or open the connection's diagnostics for the device state.", + value: "evidence unavailable", }); } else { out.push(outbox); @@ -394,61 +397,61 @@ export interface ForwardDispositionSummary { } const FORWARD_DISPOSITION_LABELS: Record = { - complete: { - value: "complete", - label: "nothing owed", - ownerActionNeeded: false, + awaiting_owner: { + label: "blocked on you", + ownerActionNeeded: true, title: - "Coverage is established and fresh. A future run re-checks the source but is not expected to collect anything new or fill a gap.", - tone: "success", + "A coverage gap is blocked on open owner attention (such as re-auth or a prompt). A run cannot make progress until you act; open the connection's attention to resolve it.", + tone: "warning", + value: "awaiting_owner", }, checking: { - value: "checking", label: "checking coverage", ownerActionNeeded: false, title: "Active work is expected to produce coverage evidence. This is a checking state, not an owner-action prompt.", tone: "neutral", + value: "checking", }, - unmeasured: { - value: "unmeasured", - label: "not measured", - ownerActionNeeded: false, - title: - "Coverage evidence is not available in the latest report. This is not an owner-action prompt and not an active checking state.", - tone: "neutral", - }, - resumable: { - value: "resumable", - label: "resumes collection", + complete: { + label: "nothing owed", ownerActionNeeded: false, title: - "There is outstanding work an ordinary future run is expected to pick up — an open boundary or retryable gap. Records already collected stay valid; no owner action is needed.", - tone: "warning", - }, - awaiting_owner: { - value: "awaiting_owner", - label: "blocked on you", - ownerActionNeeded: true, - title: - "A coverage gap is blocked on open owner attention (such as re-auth or a prompt). A run cannot make progress until you act; open the connection's attention to resolve it.", - tone: "warning", + "Coverage is established and fresh. A future run re-checks the source but is not expected to collect anything new or fill a gap.", + tone: "success", + value: "complete", }, owner_refresh_due: { - value: "owner_refresh_due", label: "refresh due", ownerActionNeeded: true, title: "Coverage is complete but the retained data has gone stale, and this connection needs an owner-initiated run to refresh — either because it refreshes only when you run it, or because it refreshes on schedule but may need your bounded help to catch up. Start a run on your instance to bring it current. This is aged data, not missing data.", tone: "warning", + value: "owner_refresh_due", + }, + resumable: { + label: "resumes collection", + ownerActionNeeded: false, + title: + "There is outstanding work an ordinary future run is expected to pick up — an open boundary or retryable gap. Records already collected stay valid; no owner action is needed.", + tone: "warning", + value: "resumable", }, terminal: { - value: "terminal", label: "won't backfill", ownerActionNeeded: false, title: "An outstanding gap will not backfill on its own — the source or connector cannot recover it without a change. Records already collected stay valid and usable; open the latest run to see which streams are affected.", tone: "danger", + value: "terminal", + }, + unmeasured: { + label: "not measured", + ownerActionNeeded: false, + title: + "Coverage evidence is not available in the latest report. This is not an owner-action prompt and not an active checking state.", + tone: "neutral", + value: "unmeasured", }, }; @@ -462,6 +465,7 @@ const FORWARD_DISPOSITION_LABELS: Record 0 || (backlog.pending_other ?? 0) > 0)); return hasPendingBacklog && Boolean(health.next_attempt_at); } @@ -1277,17 +1289,17 @@ function staleFreshnessGuidance(supportsOwnerSync: boolean): NextStepGuidance { if (supportsOwnerSync) { return { backlogScale: null, - label: "Sync now", detail: "The last successful sync is outside the freshness window. Sync now to refresh this connection.", + label: "Sync now", scale: null, tone: "warning", }; } return { backlogScale: null, - label: "Check the collector", detail: "The last successful sync is outside the freshness window. This connection fills in when its local-collector device pushes — confirm the collector is running on the host.", + label: "Check the collector", scale: null, tone: "warning", }; @@ -1318,20 +1330,20 @@ function coolingOffGuidance(health: RefConnectionHealthSnapshot): NextStepGuidan const backlogScale = formatSourcePressureBacklogScale(health.detail_gap_backlog); return { backlogScale, - label: "Catching up — cooling off", detail: health.next_attempt_at ? `The source is throttling this connection, so the scheduler is spacing out automatic attempts; the captured progress is retained and it resumes at ${health.next_attempt_at}.` : "The source is throttling this connection, so the scheduler is spacing out automatic attempts. The captured progress is retained and it resumes on the next scheduled attempt.", + label: "Catching up — cooling off", scale: null, tone: "warning", }; } return { backlogScale: null, - label: "Wait for the next retry", detail: health.next_attempt_at ? `In scheduler backoff after recent failures; the next automatic attempt is at ${health.next_attempt_at}. Open the connection to see the failure detail.` : "In scheduler backoff after recent failures. Open the connection to see the failure detail and the next attempt time.", + label: "Wait for the next retry", scale: null, tone: "warning", }; @@ -1351,18 +1363,18 @@ function degradedGuidance(health: RefConnectionHealthSnapshot, supportsOwnerSync if (supportsOwnerSync) { return { backlogScale, - label: "Continue the sync", detail: "Some required detail is still outstanding. The records already collected stay valid; sync this connection when you're ready and an ordinary run fills the rest.", + label: "Continue the sync", scale: null, tone: "warning", }; } return { backlogScale, - label: "Check the collector", detail: "Some required detail is still outstanding. The records already collected stay valid; this connection fills in when its local-collector device pushes the rest — confirm the collector is running on the host.", + label: "Check the collector", scale: null, tone: "warning", }; @@ -1370,9 +1382,9 @@ function degradedGuidance(health: RefConnectionHealthSnapshot, supportsOwnerSync if (health.axes.coverage === "gaps" || health.axes.coverage === "partial") { return { backlogScale: null, - label: "Review partial coverage", detail: "Useful data exists, but some required streams have gaps. Open the connection's latest run to see which streams are incomplete.", + label: "Review partial coverage", scale: null, tone: "warning", }; @@ -1382,8 +1394,8 @@ function degradedGuidance(health: RefConnectionHealthSnapshot, supportsOwnerSync } return { backlogScale: null, - label: "Open the connection", detail: "Coverage or freshness is incomplete. Open the connection to see which axis is degraded.", + label: "Open the connection", scale: null, tone: "warning", }; @@ -1427,9 +1439,9 @@ export function deriveConnectionNextStep(input: { if (health.axes.outbox === "stalled") { return { backlogScale: null, - label: "Check the collector host", detail: "Retryable work on the local collector is not draining. Open the connection for the exact command to run on the host that holds the data.", + label: "Check the collector host", scale: formatOutboxCountScale(localDeviceProgress?.outbox_counts), tone: "danger", }; @@ -1443,8 +1455,8 @@ export function deriveConnectionNextStep(input: { ? null : { backlogScale: null, - label: "Open the connection", detail: "This connection cannot make progress. Open it to read the blocking condition and how to clear it.", + label: "Open the connection", scale: null, tone: "danger", }; @@ -1455,8 +1467,8 @@ export function deriveConnectionNextStep(input: { ? null : { backlogScale: null, - label: "Open the connection", detail: "Owner action is required. Open the connection to see exactly what's needed.", + label: "Open the connection", scale: null, tone: "warning", }; @@ -1580,7 +1592,7 @@ function deriveRenderedFailureSummary( ): FailureSummary | null { const primaryAction = verdict.required_actions[0] ?? null; const ownerAction = renderedActionIsOwnerSatisfiable(primaryAction); - const statusAction = primaryAction != null && !ownerAction && primaryAction.kind !== "wait"; + const statusAction = primaryAction !== null && !ownerAction && primaryAction.kind !== "wait"; if (verdict.channel === "calm" && !ownerAction && !statusAction) { return null; @@ -1758,26 +1770,26 @@ export function deriveStreakDots( return runs.slice(0, 14).map((r): StreakDot => { const s = r.status; if (s === "succeeded_with_gaps") { - return { symbol: "⚠", tone: "warning", at: r.first_at, statusLabel: "Succeeded with gaps" }; + return { at: r.first_at, statusLabel: "Succeeded with gaps", symbol: "⚠", tone: "warning" }; } if (s === "succeeded" || s === "success" || s === "completed") { - return { symbol: "✓", tone: "success", at: r.first_at, statusLabel: "Succeeded" }; + return { at: r.first_at, statusLabel: "Succeeded", symbol: "✓", tone: "success" }; } if (s === "failed" || s === "error") { - return { symbol: "✕", tone: "danger", at: r.first_at, statusLabel: r.failure_reason ?? "Failed" }; + return { at: r.first_at, statusLabel: r.failure_reason ?? "Failed", symbol: "✕", tone: "danger" }; } if (s === "cancelled" || s === "canceled") { - return { symbol: "⊘", tone: "neutral", at: r.first_at, statusLabel: "Cancelled" }; + return { at: r.first_at, statusLabel: "Cancelled", symbol: "⊘", tone: "neutral" }; } if (s === "paused" || s === "skipped") { - return { symbol: "⏸", tone: "neutral", at: r.first_at, statusLabel: "Skipped" }; + return { at: r.first_at, statusLabel: "Skipped", symbol: "⏸", tone: "neutral" }; } if (s === "degraded" || s === "partial") { - return { symbol: "⚠", tone: "warning", at: r.first_at, statusLabel: "Partial" }; + return { at: r.first_at, statusLabel: "Partial", symbol: "⚠", tone: "warning" }; } // in_progress / started — not shown in a historical strip but included // defensively so the map never throws. - return { symbol: "⚠", tone: "neutral", at: r.first_at, statusLabel: s.replace(/_/g, " ") }; + return { at: r.first_at, statusLabel: s.replace(/_/g, " "), symbol: "⚠", tone: "neutral" }; }); } @@ -1838,9 +1850,9 @@ export function deriveAutoPausedBanner( } return { consecutiveFailures: backoff.consecutive_failures, - reasonLabel: backoff.reason_class ? backoff.reason_class.replace(/[_-]+/g, " ") : null, - nextRunAt: backoff.next_run_at, isTerminal: backoff.recommended_health_state === "blocked", + nextRunAt: backoff.next_run_at, + reasonLabel: backoff.reason_class ? backoff.reason_class.replace(/[_-]+/g, " ") : null, }; } @@ -1853,20 +1865,20 @@ export function derivePrimaryRowAction(input: { const { hasLocalDeviceProgress, health } = input; if (hasLocalDeviceProgress) { return { - kind: "device_wait", - label: "Waiting for the local device", detail: "This connection fills in when its local-collector device pushes new data — the dashboard cannot start a run. Confirm the collector is running on the host that holds the data.", + kind: "device_wait", + label: "Waiting for the local device", }; } if (health?.state === "cooling_off" && health.reason_code === SOURCE_PRESSURE_REASON_CODE) { return { - kind: "cooldown_wait", - label: "Cooling off", detail: health.next_attempt_at ? `This source is throttling PDPP, so the next ordinary sync waits until ${health.next_attempt_at}. Captured progress is retained.` : "This source is throttling PDPP, so ordinary sync is paused until the next scheduled retry. Captured progress is retained.", + kind: "cooldown_wait", + label: "Cooling off", }; } @@ -1874,11 +1886,11 @@ export function derivePrimaryRowAction(input: { if (sourcePressureBacklog && sourcePressureBacklog.pending > 0) { const pending = `${sourcePressureBacklog.pending_is_floor ? "at least " : ""}${sourcePressureBacklog.pending.toLocaleString()}`; return { - kind: "cooldown_wait", - label: "Cooling off", detail: `This connection has ${pending} pending provider-pressure gap${ sourcePressureBacklog.pending === 1 ? "" : "s" }, so ordinary sync may be rejected by the provider-pressure cooldown. Captured progress is retained.`, + kind: "cooldown_wait", + label: "Cooling off", }; } @@ -1911,7 +1923,7 @@ export function formatCollectionRateReadout( : null; return { backoffLabel, - currentLabel: `${rate.effective_rate_per_min.toLocaleString()}/min · interval ${rate.current_interval_ms.toLocaleString()}ms`, ceilingLabel: `ceiling ${rate.ceiling_rate_per_min.toLocaleString()}/min · interval ${rate.ceiling_interval_ms.toLocaleString()}ms`, + currentLabel: `${rate.effective_rate_per_min.toLocaleString()}/min · interval ${rate.current_interval_ms.toLocaleString()}ms`, }; } diff --git a/apps/console/src/app/(console)/lib/connection-label-ambiguity.test.ts b/apps/console/src/app/(console)/lib/connection-label-ambiguity.test.ts index bb6901c2c..056eca4e6 100644 --- a/apps/console/src/app/(console)/lib/connection-label-ambiguity.test.ts +++ b/apps/console/src/app/(console)/lib/connection-label-ambiguity.test.ts @@ -49,10 +49,10 @@ function overview({ test("a single unnamed connection of a type is NOT label-needed", () => { const overviews = [ - overview({ connectorId: "amazon", displayName: "Amazon", connectionId: "cin_amazon" }), - overview({ connectorId: "usaa", connectionId: "cin_usaa" }), - overview({ connectorId: "github", displayName: "GitHub", connectionId: "cin_github" }), - overview({ connectorId: "ynab", displayName: "YNAB", connectionId: "cin_ynab" }), + overview({ connectionId: "cin_amazon", connectorId: "amazon", displayName: "Amazon" }), + overview({ connectionId: "cin_usaa", connectorId: "usaa" }), + overview({ connectionId: "cin_github", connectorId: "github", displayName: "GitHub" }), + overview({ connectionId: "cin_ynab", connectorId: "ynab", displayName: "YNAB" }), ]; const keys = ambiguousFallbackLabelKeys(overviews); assert.equal(keys.size, 0, "no single-of-type fallback should be flagged"); @@ -62,10 +62,10 @@ test("a single unnamed connection of a type is NOT label-needed", () => { }); test("two unnamed connections of the SAME type are both label-needed", () => { - const loneAmazon = overview({ connectorId: "amazon", displayName: "Amazon", connectionId: "cin_amazon" }); + const loneAmazon = overview({ connectionId: "cin_amazon", connectorId: "amazon", displayName: "Amazon" }); const overviews = [ - overview({ connectorId: "gmail", displayName: "Gmail", connectionId: "cin_gmail_a" }), - overview({ connectorId: "gmail", connectionId: "cin_gmail_b" }), + overview({ connectionId: "cin_gmail_a", connectorId: "gmail", displayName: "Gmail" }), + overview({ connectionId: "cin_gmail_b", connectorId: "gmail" }), loneAmazon, ]; const keys = ambiguousFallbackLabelKeys(overviews); @@ -78,8 +78,8 @@ test("a renamed connection is never label-needed, and disambiguates its siblings // One Gmail owner-named, one unnamed → the named one is not a fallback at // all, so only ONE unnamed Gmail remains. A single unnamed of a type is not // ambiguous, so nothing is flagged. - const namedGmail = overview({ connectorId: "gmail", displayName: "Personal Gmail", connectionId: "cin_gmail_a" }); - const unnamedGmail = overview({ connectorId: "gmail", connectionId: "cin_gmail_b" }); + const namedGmail = overview({ connectionId: "cin_gmail_a", connectorId: "gmail", displayName: "Personal Gmail" }); + const unnamedGmail = overview({ connectionId: "cin_gmail_b", connectorId: "gmail" }); const overviews = [namedGmail, unnamedGmail]; const keys = ambiguousFallbackLabelKeys(overviews); assert.equal(keys.size, 0); @@ -89,17 +89,17 @@ test("a renamed connection is never label-needed, and disambiguates its siblings test("three unnamed of a type are all label-needed", () => { const overviews = [ - overview({ connectorId: "claude-code", connectionId: "cin_cc_1" }), - overview({ connectorId: "claude-code", connectionId: "cin_cc_2" }), - overview({ connectorId: "claude-code", connectionId: "cin_cc_3" }), + overview({ connectionId: "cin_cc_1", connectorId: "claude-code" }), + overview({ connectionId: "cin_cc_2", connectorId: "claude-code" }), + overview({ connectionId: "cin_cc_3", connectorId: "claude-code" }), ]; const keys = ambiguousFallbackLabelKeys(overviews); assert.equal(keys.size, 3); }); test("ambiguity is order-independent", () => { - const a = overview({ connectorId: "slack", connectionId: "cin_s1" }); - const b = overview({ connectorId: "slack", connectionId: "cin_s2" }); + const a = overview({ connectionId: "cin_s1", connectorId: "slack" }); + const b = overview({ connectionId: "cin_s2", connectorId: "slack" }); const forward = ambiguousFallbackLabelKeys([a, b]); const reverse = ambiguousFallbackLabelKeys([b, a]); assert.deepEqual(new Set(forward), new Set(reverse)); diff --git a/apps/console/src/app/(console)/lib/connection-modality.test.ts b/apps/console/src/app/(console)/lib/connection-modality.test.ts index c1a83e2ce..5edffe83a 100644 --- a/apps/console/src/app/(console)/lib/connection-modality.test.ts +++ b/apps/console/src/app/(console)/lib/connection-modality.test.ts @@ -109,22 +109,25 @@ test("BROWSER_BOUND_CONNECTORS exactly matches the canonical keys of browser-bin const repoRoot = new URL("../../../../../../", import.meta.url); const manifestsDir = new URL("packages/polyfill-connectors/manifests/", repoRoot); const files = await readdir(fileURLToPath(manifestsDir)); - const browserBoundFromManifests: string[] = []; - for (const file of files) { - if (!file.endsWith(".json")) { - continue; - } - const raw = await readFile(fileURLToPath(new URL(file, manifestsDir)), "utf8"); - const manifest = JSON.parse(raw) as { - connector_id?: string; - runtime_requirements?: { bindings?: Record | null } | null; - }; - const bindings = manifest.runtime_requirements?.bindings; - if (manifest.connector_id && bindings && Object.hasOwn(bindings, "browser")) { - browserBoundFromManifests.push(canonicalKeyFromManifestId(manifest.connector_id)); - } - } - assert.deepEqual([...BROWSER_BOUND_CONNECTORS].sort(), browserBoundFromManifests.sort()); + const browserBoundFromManifests = await Promise.all( + files + .filter((file) => file.endsWith(".json")) + .map(async (file) => { + const raw = await readFile(fileURLToPath(new URL(file, manifestsDir)), "utf8"); + const manifest = JSON.parse(raw) as { + connector_id?: string; + runtime_requirements?: { bindings?: Record | null } | null; + }; + const bindings = manifest.runtime_requirements?.bindings; + return manifest.connector_id && bindings && Object.hasOwn(bindings, "browser") + ? canonicalKeyFromManifestId(manifest.connector_id) + : null; + }) + ); + assert.deepEqual( + [...BROWSER_BOUND_CONNECTORS].sort(), + browserBoundFromManifests.filter((connectorId): connectorId is string => connectorId !== null).sort() + ); }); test("isBrowserBoundConnector narrows only the browser-bound keys", () => { diff --git a/apps/console/src/app/(console)/lib/connection-summary-stats.test.ts b/apps/console/src/app/(console)/lib/connection-summary-stats.test.ts index 83e203025..796f070c9 100644 --- a/apps/console/src/app/(console)/lib/connection-summary-stats.test.ts +++ b/apps/console/src/app/(console)/lib/connection-summary-stats.test.ts @@ -12,12 +12,12 @@ type ConnectionHealthState = NonNullable[ function overview(partial: Partial): ConnectorOverview { return { - connector: { connector_id: "test", name: "Test", display_name: "Test" }, - streams: [], - totalRecords: 0, + connector: { connector_id: "test", display_name: "Test", name: "Test" }, isRunning: false, lastRun: null, lastSuccessfulRun: null, + streams: [], + totalRecords: 0, ...partial, }; } @@ -32,15 +32,15 @@ function withDataInState( outbox: "active" | "idle" | "stalled" | "unknown" = "unknown" ): ConnectorOverview { return overview({ - totalRecords: 10, connectionHealth: { - state, - reason_code: null, + axes: { attention: "none", coverage: "unknown", freshness, outbox, remote_surface: "none" }, + badges: { stale: freshness === "stale", syncing: false }, last_success_at: null, next_attempt_at: null, - axes: { freshness, coverage: "unknown", attention: "none", outbox, remote_surface: "none" }, - badges: { stale: freshness === "stale", syncing: false }, + reason_code: null, + state, } as ConnectorOverview["connectionHealth"], + totalRecords: 10, }); } @@ -104,17 +104,17 @@ test("stale is counted only when the freshness axis says stale", () => { test("running counts active runs and push-mode syncing badges", () => { const syncing = overview({ - totalRecords: 3, connectionHealth: { - state: "healthy", - reason_code: null, + axes: { attention: "none", coverage: "complete", freshness: "fresh", outbox: "active", remote_surface: "none" }, + badges: { stale: false, syncing: true }, last_success_at: null, next_attempt_at: null, - axes: { freshness: "fresh", coverage: "complete", attention: "none", outbox: "active", remote_surface: "none" }, - badges: { stale: false, syncing: true }, + reason_code: null, + state: "healthy", } as ConnectorOverview["connectionHealth"], + totalRecords: 3, }); - const stats = summarizeConnectionHealth([overview({ totalRecords: 1, isRunning: true }), syncing]); + const stats = summarizeConnectionHealth([overview({ isRunning: true, totalRecords: 1 }), syncing]); assert.equal(stats.running, 2); }); diff --git a/apps/console/src/app/(console)/lib/connection-summary-stats.ts b/apps/console/src/app/(console)/lib/connection-summary-stats.ts index 59f3bdd8f..9e1640656 100644 --- a/apps/console/src/app/(console)/lib/connection-summary-stats.ts +++ b/apps/console/src/app/(console)/lib/connection-summary-stats.ts @@ -118,11 +118,11 @@ export function summarizeConnectionHealth(overviews: readonly ConnectorOverview[ } } return { + degraded, + needsAttention, + noData: overviews.length - primaryList.length, primaryList: primaryList.length, registeredTotal: overviews.length, - noData: overviews.length - primaryList.length, - needsAttention, - degraded, running, stale, }; diff --git a/apps/console/src/app/(console)/lib/data-source.test.ts b/apps/console/src/app/(console)/lib/data-source.test.ts index 9dd34369c..3ba294f17 100644 --- a/apps/console/src/app/(console)/lib/data-source.test.ts +++ b/apps/console/src/app/(console)/lib/data-source.test.ts @@ -47,6 +47,7 @@ async function walk(dir: string, files: string[] = []): Promise { for (const entry of entries) { const full = join(dir, entry.name); if (entry.isDirectory()) { + // biome-ignore lint/performance/noAwaitInLoops: sequential by design await walk(full, files); } else if (TS_FILE_RE.test(entry.name)) { files.push(full); @@ -116,6 +117,7 @@ test("/** never imports from /sandbox/**", async () => { if (file === SELF) { continue; } + // biome-ignore lint/performance/noAwaitInLoops: sequential by design const src = await readFile(file, "utf8"); // Match Next/TS import statements that resolve to the sandbox tree: // import ... from "@/app/sandbox/..." @@ -192,6 +194,7 @@ test("/** never references the sandbox data source binding", async () => { if (file === SELF) { continue; } + // biome-ignore lint/performance/noAwaitInLoops: sequential by design const src = stripComments(await readFile(file, "utf8")); if (src.includes("sandboxDashboardDataSource")) { offenders.push(file); diff --git a/apps/console/src/app/(console)/lib/data-source.ts b/apps/console/src/app/(console)/lib/data-source.ts index 5d1423aa0..016c513dc 100644 --- a/apps/console/src/app/(console)/lib/data-source.ts +++ b/apps/console/src/app/(console)/lib/data-source.ts @@ -79,7 +79,7 @@ export interface DashboardDataSource { * over-time chart's bars. True per-bucket totals over the filtered, grant-scoped * corpus (NOT loaded entries). */ - aggregateRecordsByTime( + aggregateRecordsByTime: ( connectorId: string, stream: string, opts: { @@ -89,31 +89,31 @@ export interface DashboardDataSource { groupByTime: string; timeZone?: string; } - ): Promise; - getConnectorOverview(connector: ConnectorManifest): Promise; + ) => Promise; + getConnectorOverview: (connector: ConnectorManifest) => Promise; // ── Overview / deployment / approvals ────────────────────────────────── - getDatasetSummary(): Promise; - getDeploymentDiagnostics(): Promise; - getGrantTimeline(grantId: string): Promise; - getRecord( + getDatasetSummary: () => Promise; + getDeploymentDiagnostics: () => Promise; + getGrantTimeline: (grantId: string) => Promise; + getRecord: ( connectorId: string, stream: string, recordId: string, opts?: { connectorInstanceId?: string | null } - ): Promise; - getRunTimeline(runId: string): Promise; - getStreamMetadata( + ) => Promise; + getRunTimeline: (runId: string) => Promise; + getStreamMetadata: ( connectorId: string, stream: string, opts?: { connectorInstanceId?: string | null } - ): Promise; - getTraceTimeline(traceId: string): Promise; - isHybridRetrievalAdvertised(): Promise; - isSemanticRetrievalAdvertised(): Promise; + ) => Promise; + getTraceTimeline: (traceId: string) => Promise; + isHybridRetrievalAdvertised: () => Promise; + isSemanticRetrievalAdvertised: () => Promise; readonly kind: "live" | "sandbox"; - listConnectorManifests(): Promise; + listConnectorManifests: () => Promise; // ── Records ──────────────────────────────────────────────────────────── - listConnectorSummaries(): Promise; + listConnectorSummaries: () => Promise; /** * Index-backed, single-call over-time bucket aggregate for the Explore chart — * the honest replacement for the per-(connection, stream) `aggregateRecordsByTime` @@ -121,7 +121,7 @@ export interface DashboardDataSource { * `extent.count` (count == reachability) over the SAME structural scope the feed * shows, so the bars reconcile with the list. `GET /_ref/explore/records/buckets`. */ - listExploreRecordBuckets(opts: { + listExploreRecordBuckets: (opts: { connections?: readonly string[]; streams?: readonly string[]; excludeConnections?: readonly string[]; @@ -130,9 +130,9 @@ export interface DashboardDataSource { until?: string | null; granularity?: TimeBucketGranularity | "auto"; timeZone?: string; - }): Promise; + }) => Promise; // ── Explore merged timeline (Phase 3) ───────────────────────────────── - listExploreTimeline(opts?: { + listExploreTimeline: (opts?: { connectionIds?: readonly string[]; cursor?: string | null; limit?: number; @@ -148,14 +148,14 @@ export interface DashboardDataSource { * "asc" = the `order=oldest` re-page (earliest record first, paging forward). */ direction?: "asc" | "desc"; - }): Promise; + }) => Promise; // ── Grants / runs / traces / timelines ───────────────────────────────── - listGrants(opts?: ListQuery): Promise>; - listPendingApprovals(): Promise>; - listRuns(opts?: ListQuery): Promise>; - listStreams(connectorId: string): Promise; - listTraces(opts?: ListQuery): Promise>; - queryRecords( + listGrants: (opts?: ListQuery) => Promise>; + listPendingApprovals: () => Promise>; + listRuns: (opts?: ListQuery) => Promise>; + listStreams: (connectorId: string) => Promise; + listTraces: (opts?: ListQuery) => Promise>; + queryRecords: ( connectorId: string, stream: string, opts?: { @@ -166,24 +166,24 @@ export interface DashboardDataSource { order?: "asc" | "desc"; window?: "exact" | "none"; } - ): Promise; + ) => Promise; // ── Search ───────────────────────────────────────────────────────────── - refSearch(query: string): Promise<{ + refSearch: (query: string) => Promise<{ object: "search_result"; traces: TraceSummary[]; grants: GrantSummary[]; runs: RunSummary[]; exact: { kind: "trace" | "grant" | "run"; id: string } | null; }>; - searchRecordsHybrid(query: string, opts?: { streams?: string[]; limit?: number }): Promise; - searchRecordsLexical( + searchRecordsHybrid: (query: string, opts?: { streams?: string[]; limit?: number }) => Promise; + searchRecordsLexical: ( query: string, opts?: { streams?: string[]; limit?: number; cursor?: string; order?: "relevance" | "recent" } - ): Promise; - searchRecordsSemantic( + ) => Promise; + searchRecordsSemantic: ( query: string, opts?: { streams?: string[]; limit?: number; cursor?: string } - ): Promise; + ) => Promise; supportsExploreTimelineDirection?: () => Promise; } @@ -193,31 +193,31 @@ export interface DashboardDataSource { * weakening this binding would weaken `/**` owner gating. */ export const liveDashboardDataSource: DashboardDataSource = { - kind: "live", - supportsExploreTimelineDirection: async () => process.env.PDPP_EXPLORE_TIMELINE_DIRECTION === "1", - listConnectorSummaries, - listConnectorManifests, - listStreams, - getStreamMetadata, - getConnectorOverview, aggregateRecordsByTime, - listExploreRecordBuckets, - queryRecords, + getConnectorOverview, + getDatasetSummary, + getDeploymentDiagnostics, + getGrantTimeline, getRecord, - refSearch, - searchRecordsLexical, - searchRecordsSemantic, - searchRecordsHybrid, - isSemanticRetrievalAdvertised, + getRunTimeline, + getStreamMetadata, + getTraceTimeline, isHybridRetrievalAdvertised, + isSemanticRetrievalAdvertised, + kind: "live", + listConnectorManifests, + listConnectorSummaries, + listExploreRecordBuckets, listExploreTimeline, listGrants, + listPendingApprovals, listRuns, + listStreams, listTraces, - getGrantTimeline, - getRunTimeline, - getTraceTimeline, - getDatasetSummary, - listPendingApprovals, - getDeploymentDiagnostics, + queryRecords, + refSearch, + searchRecordsHybrid, + searchRecordsLexical, + searchRecordsSemantic, + supportsExploreTimelineDirection: async () => process.env.PDPP_EXPLORE_TIMELINE_DIRECTION === "1", }; diff --git a/apps/console/src/app/(console)/lib/describe-error.test.ts b/apps/console/src/app/(console)/lib/describe-error.test.ts index d3097ae42..63122dfaf 100644 --- a/apps/console/src/app/(console)/lib/describe-error.test.ts +++ b/apps/console/src/app/(console)/lib/describe-error.test.ts @@ -10,10 +10,10 @@ const FALLBACK = "request failed (409)"; test("describeError prefers the reference-server envelope error.message", () => { const body = { error: { - type: "about:blank", code: "subscription_already_disabled", message: "Subscription sub_abc is already disabled.", request_id: "req_1", + type: "about:blank", }, }; assert.equal(describeError(body, FALLBACK), "Subscription sub_abc is already disabled."); @@ -43,7 +43,7 @@ test("describeErrorText parses a JSON envelope string into the friendly message" // reference server's error envelope. The operator must see the message, // not the stringified blob. const raw = JSON.stringify({ - error: { type: "about:blank", code: "not_found", message: "Subscription not found." }, + error: { code: "not_found", message: "Subscription not found.", type: "about:blank" }, }); assert.equal( describeErrorText(raw, "_ref /_ref/event-subscriptions/sub_x/disable failed (404)"), diff --git a/apps/console/src/app/(console)/lib/grant-request-connection-pin.ts b/apps/console/src/app/(console)/lib/grant-request-connection-pin.ts index cce0e1e12..54f474843 100644 --- a/apps/console/src/app/(console)/lib/grant-request-connection-pin.ts +++ b/apps/console/src/app/(console)/lib/grant-request-connection-pin.ts @@ -117,7 +117,7 @@ export function buildConnectionPinOptions( connectorId, displayName: owned ? summary.display_name : null, }); - return { value: trim(summary.connection_id), base }; + return { base, value: trim(summary.connection_id) }; }); const labelCounts = new Map(); @@ -128,10 +128,10 @@ export function buildConnectionPinOptions( const seen = new Map(); return matching.map((entry) => { if ((labelCounts.get(entry.base) ?? 0) <= 1) { - return { value: entry.value, label: entry.base }; + return { label: entry.base, value: entry.value }; } const ordinal = (seen.get(entry.base) ?? 0) + 1; seen.set(entry.base, ordinal); - return { value: entry.value, label: `${entry.base} · account ${ordinal}` }; + return { label: `${entry.base} · account ${ordinal}`, value: entry.value }; }); } diff --git a/apps/console/src/app/(console)/lib/hybrid-preference.test.ts b/apps/console/src/app/(console)/lib/hybrid-preference.test.ts index 01e12a7b8..5a0cf736e 100644 --- a/apps/console/src/app/(console)/lib/hybrid-preference.test.ts +++ b/apps/console/src/app/(console)/lib/hybrid-preference.test.ts @@ -36,19 +36,19 @@ function shouldAttemptHybrid(input: HybridPreferenceInput): boolean { // ─── Tests ─────────────────────────────────────────────────────────────────── test("hybrid is preferred when advertised on first page", () => { - assert.equal(shouldAttemptHybrid({ hybridAdvertised: true, cursor: null }), true); + assert.equal(shouldAttemptHybrid({ cursor: null, hybridAdvertised: true }), true); }); test("hybrid is skipped when not advertised", () => { - assert.equal(shouldAttemptHybrid({ hybridAdvertised: false, cursor: null }), false); + assert.equal(shouldAttemptHybrid({ cursor: null, hybridAdvertised: false }), false); }); test("hybrid is skipped on subsequent pages even when advertised", () => { - assert.equal(shouldAttemptHybrid({ hybridAdvertised: true, cursor: "some-cursor" }), false); + assert.equal(shouldAttemptHybrid({ cursor: "some-cursor", hybridAdvertised: true }), false); }); test("hybrid is skipped on subsequent pages when not advertised", () => { - assert.equal(shouldAttemptHybrid({ hybridAdvertised: false, cursor: "some-cursor" }), false); + assert.equal(shouldAttemptHybrid({ cursor: "some-cursor", hybridAdvertised: false }), false); }); // ─── Decision gate: semantic uplift fallback ────────────────────────────────── @@ -77,7 +77,7 @@ test("semantic uplift is blocked when both advertised is false and participation test("when hybrid is advertised, semantic gate result does not matter for hybrid path", () => { // If hybrid is available, semantic should not be attempted regardless of gate. // This is enforced by searchRecords() returning early when hybrid succeeds. - const hybridWouldRun = shouldAttemptHybrid({ hybridAdvertised: true, cursor: null }); + const hybridWouldRun = shouldAttemptHybrid({ cursor: null, hybridAdvertised: true }); const semanticWouldRun = shouldAttemptSemanticUplift({ advertised: true, participationFieldCount: 3 }); // Both gates are open, but the code path that reaches semantic uplift is // only entered when hybrid is NOT advertised or NOT on page 1. The test @@ -87,7 +87,7 @@ test("when hybrid is advertised, semantic gate result does not matter for hybrid }); test("when hybrid is not advertised, semantic uplift can activate independently", () => { - const hybridWouldRun = shouldAttemptHybrid({ hybridAdvertised: false, cursor: null }); + const hybridWouldRun = shouldAttemptHybrid({ cursor: null, hybridAdvertised: false }); const semanticWouldRun = shouldAttemptSemanticUplift({ advertised: true, participationFieldCount: 3 }); assert.equal(hybridWouldRun, false); assert.equal(semanticWouldRun, true); diff --git a/apps/console/src/app/(console)/lib/next-action.ts b/apps/console/src/app/(console)/lib/next-action.ts index 7854a65da..0ee469962 100644 --- a/apps/console/src/app/(console)/lib/next-action.ts +++ b/apps/console/src/app/(console)/lib/next-action.ts @@ -85,9 +85,9 @@ export function humanizeReasonCode(code: string | null | undefined): string | nu } const OWNER_ACTION_VERB: Record, string> = { - provide_value: "Provide input", act_elsewhere: "Continue on the provider", operate_attachment: "Open the linked tool", + provide_value: "Provide input", }; const REASON_LABELS: Record = { @@ -133,11 +133,11 @@ export function formatNextAction(action: RefNextAction | null | undefined): Form action.source === "schedule_fallback" ? "Details unavailable — open the connection to see what's needed." : null; return { - label, - caveat, actionTarget: action.action_target ?? null, - variant: action.source === "schedule_fallback" ? "schedule_fallback" : "structured", + caveat, + label, notificationHint: formatNotificationHint(action.notification_state ?? null), + variant: action.source === "schedule_fallback" ? "schedule_fallback" : "structured", }; } diff --git a/apps/console/src/app/(console)/lib/operator-approvals.ts b/apps/console/src/app/(console)/lib/operator-approvals.ts index 1de18564f..80640c3c4 100644 --- a/apps/console/src/app/(console)/lib/operator-approvals.ts +++ b/apps/console/src/app/(console)/lib/operator-approvals.ts @@ -36,7 +36,9 @@ async function fetchAs(path: string, init: RequestInit): Promise { }) ); } catch (err) { - throw new ReferenceServerUnreachableError(`Cannot reach authorization server at ${getAsInternalUrl()}`, err); + throw new ReferenceServerUnreachableError(`Cannot reach authorization server at ${getAsInternalUrl()}`, { + cause: err, + }); } } @@ -48,12 +50,12 @@ async function fetchAs(path: string, init: RequestInit): Promise { */ export async function approveConsentRequest(requestUri: string, subjectId = "owner_local") { const response = await fetchAs("/consent/approve", { - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ request_uri: requestUri, subject_id: subjectId, }), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!response.ok) { @@ -64,11 +66,11 @@ export async function approveConsentRequest(requestUri: string, subjectId = "own export async function denyConsentRequest(requestUri: string) { const response = await fetchAs("/consent/deny", { - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ request_uri: requestUri, }), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!response.ok) { @@ -90,12 +92,12 @@ export async function approvePendingApproval(input: { if (input.kind === "consent") { const response = await fetchAs("/consent/approve", { - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ approval_id: input.approvalId, subject_id: subjectId, }), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!response.ok) { @@ -105,12 +107,12 @@ export async function approvePendingApproval(input: { } const response = await fetchAs("/device/approve", { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: asForm({ approval_id: input.approvalId, subject_id: subjectId, }), + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + method: "POST", }); const body = await readBody(response); if (!response.ok) { @@ -132,11 +134,11 @@ export async function denyPendingApproval(input: { if (input.kind === "consent") { const response = await fetchAs("/consent/deny", { - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ approval_id: input.approvalId, }), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!response.ok) { @@ -146,12 +148,12 @@ export async function denyPendingApproval(input: { } const response = await fetchAs("/device/deny", { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: asForm({ approval_id: input.approvalId, subject_id: subjectId, }), + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + method: "POST", }); const body = await readBody(response); if (!response.ok) { diff --git a/apps/console/src/app/(console)/lib/operator-bootstrap.ts b/apps/console/src/app/(console)/lib/operator-bootstrap.ts index cb787db06..c54cb6f23 100644 --- a/apps/console/src/app/(console)/lib/operator-bootstrap.ts +++ b/apps/console/src/app/(console)/lib/operator-bootstrap.ts @@ -87,7 +87,9 @@ async function fetchAs(path: string, init: RequestInit): Promise { }) ); } catch (err) { - throw new ReferenceServerUnreachableError(`Cannot reach authorization server at ${getAsInternalUrl()}`, err); + throw new ReferenceServerUnreachableError(`Cannot reach authorization server at ${getAsInternalUrl()}`, { + cause: err, + }); } } @@ -131,15 +133,15 @@ export async function startOwnerBootstrapFlow( throw new Error("Token name is required"); } const registerResp = await fetchAs("/oauth/register", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${DEFAULT_DCR_INITIAL_ACCESS_TOKEN}`, - }, body: JSON.stringify({ client_name: label, token_endpoint_auth_method: "none", }), + headers: { + Authorization: `Bearer ${DEFAULT_DCR_INITIAL_ACCESS_TOKEN}`, + "Content-Type": "application/json", + }, + method: "POST", }); const registerBody = await readBody(registerResp); if (!(registerResp.ok && registerBody) || typeof registerBody !== "object") { @@ -154,9 +156,9 @@ export async function startOwnerBootstrapFlow( // JSON content-type uses the documented CSRF exemption, like every other // server-to-server BFF call from the dashboard. const response = await fetchAs("/oauth/device_authorization", { - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: clientId }), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!(response.ok && body) || typeof body !== "object") { @@ -181,26 +183,26 @@ export async function startOwnerBootstrapFlow( } const flow: OwnerBootstrapFlow = { - flowId: crypto.randomUUID(), + approvalUpdatedAt: null, clientId, - name: label, - subjectId: null, - status: "pending_approval", - startedAt: new Date().toISOString(), + deviceCode: payload.device_code, expiresAt: typeof payload.expires_in === "number" ? new Date(Date.now() + payload.expires_in * 1000).toISOString() : null, + flowId: crypto.randomUUID(), intervalSeconds: typeof payload.interval === "number" ? payload.interval : 5, - deviceCode: payload.device_code, + introspectedAt: null, + introspection: null, + lastError: null, + name: label, + startedAt: new Date().toISOString(), + status: "pending_approval", + subjectId: null, + token: null, + tokenIssuedAt: null, + tokenResponse: null, userCode: payload.user_code, verificationUri: payload.verification_uri ?? null, verificationUriComplete: payload.verification_uri_complete ?? null, - approvalUpdatedAt: null, - tokenIssuedAt: null, - token: null, - tokenResponse: null, - introspection: null, - introspectedAt: null, - lastError: null, }; return saveFlow(flow); @@ -215,9 +217,9 @@ export async function approveOwnerBootstrapFlow(flowId: string, subjectId: strin // derives the approved subject from the owner session when owner-auth is on; // `subjectId` here is retained only for the local UI transcript state. const response = await fetchAs("/device/approve", { - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_code: flow.userCode }), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!response.ok) { @@ -225,10 +227,10 @@ export async function approveOwnerBootstrapFlow(flowId: string, subjectId: strin } return saveFlow({ ...flow, - subjectId, - status: "approved", approvalUpdatedAt: new Date().toISOString(), lastError: null, + status: "approved", + subjectId, }); } @@ -237,9 +239,9 @@ export async function denyOwnerBootstrapFlow(flowId: string, subjectId: string): // See approveOwnerBootstrapFlow: JSON content-type uses the documented CSRF // exemption for server-to-server BFF callers. const response = await fetchAs("/device/deny", { - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_code: flow.userCode }), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!response.ok) { @@ -247,27 +249,27 @@ export async function denyOwnerBootstrapFlow(flowId: string, subjectId: string): } return saveFlow({ ...flow, - subjectId, - status: "denied", approvalUpdatedAt: new Date().toISOString(), - token: null, - tokenResponse: null, - introspection: null, introspectedAt: null, + introspection: null, lastError: null, + status: "denied", + subjectId, + token: null, + tokenResponse: null, }); } export async function exchangeOwnerBootstrapToken(flowId: string): Promise { const flow = requireFlow(flowId); const response = await fetchAs("/oauth/token", { - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - grant_type: "urn:ietf:params:oauth:grant-type:device_code", - device_code: flow.deviceCode, client_id: flow.clientId, + device_code: flow.deviceCode, + grant_type: "urn:ietf:params:oauth:grant-type:device_code", }), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!(response.ok && body) || typeof body !== "object") { @@ -281,11 +283,11 @@ export async function exchangeOwnerBootstrapToken(flowId: string): Promise, introspectedAt: new Date().toISOString(), + introspection: body as Record, lastError: null, }); } @@ -316,21 +318,16 @@ export async function buildOwnerBootstrapExamples(flow: OwnerBootstrapFlow) { const asUrl = referenceOrigin; const rsUrl = referenceOrigin; return { - cliLogin: `pdpp auth login --client-id ${shellQuote(flow.clientId)} --as-url ${shellQuote(asUrl)} --format json`, + approveCurl: `curl -sS -X POST ${shellQuote(`${asUrl}/device/approve`)} \\\n -H 'Content-Type: application/json' \\\n -H 'Cookie: pdpp_owner_session=' \\\n --data ${shellQuote(JSON.stringify({ user_code: flow.userCode }))}`, cliIntrospect: flow.token ? `pdpp auth introspect --as-url ${shellQuote(asUrl)} --token ${shellQuote(flow.token)} --format json` : "pdpp auth introspect --as-url --token --format json", - // Curl examples mirror what the BFF actually sends on the wire: - // application/json bodies with the owner-session cookie, using the - // documented isJsonRequest CSRF exemption. /device/approve does not - // accept subject_id from the body — the AS derives it from the session. - startCurl: `curl -sS -X POST ${shellQuote(`${asUrl}/oauth/device_authorization`)} \\\n -H 'Content-Type: application/json' \\\n --data ${shellQuote(JSON.stringify({ client_id: flow.clientId }))}`, - approveCurl: `curl -sS -X POST ${shellQuote(`${asUrl}/device/approve`)} \\\n -H 'Content-Type: application/json' \\\n -H 'Cookie: pdpp_owner_session=' \\\n --data ${shellQuote(JSON.stringify({ user_code: flow.userCode }))}`, + cliLogin: `pdpp auth login --client-id ${shellQuote(flow.clientId)} --as-url ${shellQuote(asUrl)} --format json`, exchangeCurl: `curl -sS -X POST ${shellQuote(`${asUrl}/oauth/token`)} \\\n -H 'Content-Type: application/json' \\\n --data ${shellQuote( JSON.stringify({ - grant_type: "urn:ietf:params:oauth:grant-type:device_code", - device_code: flow.deviceCode, client_id: flow.clientId, + device_code: flow.deviceCode, + grant_type: "urn:ietf:params:oauth:grant-type:device_code", }) )}`, introspectCurl: flow.token @@ -339,6 +336,11 @@ export async function buildOwnerBootstrapExamples(flow: OwnerBootstrapFlow) { ownerReadExample: flow.token ? `curl -sS ${shellQuote(`${rsUrl}/v1/streams`)} -H 'Authorization: Bearer ${flow.token}'` : `curl -sS ${shellQuote(`${rsUrl}/v1/streams`)} -H 'Authorization: Bearer '`, + // Curl examples mirror what the BFF actually sends on the wire: + // application/json bodies with the owner-session cookie, using the + // documented isJsonRequest CSRF exemption. /device/approve does not + // accept subject_id from the body — the AS derives it from the session. + startCurl: `curl -sS -X POST ${shellQuote(`${asUrl}/oauth/device_authorization`)} \\\n -H 'Content-Type: application/json' \\\n --data ${shellQuote(JSON.stringify({ client_id: flow.clientId }))}`, }; } diff --git a/apps/console/src/app/(console)/lib/operator-grant-request.ts b/apps/console/src/app/(console)/lib/operator-grant-request.ts index 823f27b67..208df07a2 100644 --- a/apps/console/src/app/(console)/lib/operator-grant-request.ts +++ b/apps/console/src/app/(console)/lib/operator-grant-request.ts @@ -94,8 +94,8 @@ function normalizeSourceKind(value: string | undefined): GrantRequestDraft["sour function sourceFromDraft(draft: GrantRequestDraft) { return { - kind: draft.sourceKind, id: draft.sourceId, + kind: draft.sourceKind, }; } @@ -122,44 +122,44 @@ export async function loadConnectionPinOptions(draft: GrantRequestDraft): Promis export function createDefaultGrantRequestDraft(): GrantRequestDraft { return { - initialAccessToken: DEFAULT_DCR_INITIAL_ACCESS_TOKEN, + accessMode: "single_use", clientId: "", clientName: "Longview", clientUri: "", - redirectUri: "", - sourceKind: "connector", - sourceId: "", + connectionId: "", + fields: "", + initialAccessToken: DEFAULT_DCR_INITIAL_ACCESS_TOKEN, purposeCode: "https://pdpp.org/purpose/financial_planning", purposeDescription: "Compare personal data across providers.", - accessMode: "single_use", + redirectUri: "", retention: "P30D", + sourceId: "", + sourceKind: "connector", streamName: "", - connectionId: "", - fields: "", - view: "", subjectId: "owner_local", + view: "", }; } function sanitizeDraft(input: Partial = {}): GrantRequestDraft { const base = createDefaultGrantRequestDraft(); return { - initialAccessToken: trim(input.initialAccessToken) || base.initialAccessToken, + accessMode: trim(input.accessMode) || base.accessMode, clientId: trim(input.clientId), clientName: trim(input.clientName) || base.clientName, clientUri: trim(input.clientUri), - redirectUri: trim(input.redirectUri), - sourceKind: normalizeSourceKind(input.sourceKind), - sourceId: trim(input.sourceId), + connectionId: trim(input.connectionId), + fields: trim(input.fields), + initialAccessToken: trim(input.initialAccessToken) || base.initialAccessToken, purposeCode: trim(input.purposeCode) || base.purposeCode, purposeDescription: trim(input.purposeDescription) || base.purposeDescription, - accessMode: trim(input.accessMode) || base.accessMode, + redirectUri: trim(input.redirectUri), retention: trim(input.retention) || base.retention, + sourceId: trim(input.sourceId), + sourceKind: normalizeSourceKind(input.sourceKind), streamName: trim(input.streamName), - connectionId: trim(input.connectionId), - fields: trim(input.fields), - view: trim(input.view), subjectId: trim(input.subjectId) || base.subjectId, + view: trim(input.view), }; } @@ -195,18 +195,19 @@ function saveWorkspace(workspace: GrantRequestWorkspace): GrantRequestWorkspace function upsertWorkspace(workspaceId: string | undefined, input: Partial): GrantRequestWorkspace { const existing = workspaceId ? workspaceOrNull(workspaceId) : null; const draft = sanitizeDraft({ + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic ...(existing?.draft ?? {}), ...input, }); const now = nowIso(); const workspace: GrantRequestWorkspace = { - workspaceId: existing?.workspaceId || crypto.randomUUID(), createdAt: existing?.createdAt || now, - updatedAt: now, draft, + lastError: null, registeredClient: existing?.registeredClient ?? null, stagedRequest: existing?.stagedRequest ?? null, - lastError: null, + updatedAt: now, + workspaceId: existing?.workspaceId || crypto.randomUUID(), }; return saveWorkspace(workspace); } @@ -229,7 +230,9 @@ async function fetchAs(path: string, init: RequestInit): Promise { }) ); } catch (err) { - throw new ReferenceServerUnreachableError(`Cannot reach authorization server at ${getAsInternalUrl()}`, err); + throw new ReferenceServerUnreachableError(`Cannot reach authorization server at ${getAsInternalUrl()}`, { + cause: err, + }); } } @@ -244,8 +247,8 @@ export function setGrantRequestWorkspaceError(workspaceId: string, message: stri } return saveWorkspace({ ...workspace, - updatedAt: nowIso(), lastError: message, + updatedAt: nowIso(), }); } @@ -269,12 +272,12 @@ export async function registerGrantRequestClient( }; const response = await fetchAs("/oauth/register", { - method: "POST", + body: JSON.stringify(metadata), headers: { - "Content-Type": "application/json", Authorization: `Bearer ${workspace.draft.initialAccessToken}`, + "Content-Type": "application/json", }, - body: JSON.stringify(metadata), + method: "POST", }); const body = await readBody(response); if (!(response.ok && body) || typeof body !== "object") { @@ -284,13 +287,13 @@ export async function registerGrantRequestClient( const registeredClient = body as Record; return saveWorkspace({ ...workspace, - updatedAt: nowIso(), draft: { ...workspace.draft, clientId: typeof registeredClient.client_id === "string" ? registeredClient.client_id : workspace.draft.clientId, }, - registeredClient, lastError: null, + registeredClient, + updatedAt: nowIso(), }); } @@ -310,25 +313,25 @@ export async function stageGrantRequest( } const request = { - client_id: clientId, - client_display: workspace.draft.clientName ? { name: workspace.draft.clientName } : undefined, authorization_details: [ { - type: "https://pdpp.org/data-access", - source: sourceFromDraft(workspace.draft), + access_mode: workspace.draft.accessMode, purpose_code: workspace.draft.purposeCode, purpose_description: workspace.draft.purposeDescription, - access_mode: workspace.draft.accessMode, retention: workspace.draft.retention, + source: sourceFromDraft(workspace.draft), streams: [streamSelectionFromDraft(workspace.draft, workspace.draft.streamName)], + type: "https://pdpp.org/data-access", }, ], + client_display: workspace.draft.clientName ? { name: workspace.draft.clientName } : undefined, + client_id: clientId, }; const response = await fetchAs("/oauth/par", { - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify(request), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!(response.ok && body) || typeof body !== "object") { @@ -337,13 +340,13 @@ export async function stageGrantRequest( return saveWorkspace({ ...workspace, - updatedAt: nowIso(), draft: { ...workspace.draft, clientId, }, - stagedRequest: body as Record, lastError: null, + stagedRequest: body as Record, + updatedAt: nowIso(), }); } @@ -357,8 +360,8 @@ export async function approveGrantRequestWorkspace(workspaceId: string): Promise await approveConsentRequest(requestUri, workspace.draft.subjectId); return saveWorkspace({ ...workspace, - updatedAt: nowIso(), lastError: null, + updatedAt: nowIso(), }); } @@ -372,8 +375,8 @@ export async function denyGrantRequestWorkspace(workspaceId: string): Promise"); const request = { - client_id: - workspace.draft.clientId || - (typeof workspace.registeredClient?.client_id === "string" - ? workspace.registeredClient.client_id - : ""), - client_display: workspace.draft.clientName ? { name: workspace.draft.clientName } : undefined, authorization_details: [ { - type: "https://pdpp.org/data-access", - source: sourceFromDraft(workspace.draft), + access_mode: workspace.draft.accessMode, purpose_code: workspace.draft.purposeCode, purpose_description: workspace.draft.purposeDescription, - access_mode: workspace.draft.accessMode, retention: workspace.draft.retention, + source: sourceFromDraft(workspace.draft), streams: [streamSelection], + type: "https://pdpp.org/data-access", }, ], + client_display: workspace.draft.clientName ? { name: workspace.draft.clientName } : undefined, + client_id: + workspace.draft.clientId || + (typeof workspace.registeredClient?.client_id === "string" + ? workspace.registeredClient.client_id + : ""), }; return { diff --git a/apps/console/src/app/(console)/lib/operator-runs.ts b/apps/console/src/app/(console)/lib/operator-runs.ts index 72e4013e7..5ad0ccf89 100644 --- a/apps/console/src/app/(console)/lib/operator-runs.ts +++ b/apps/console/src/app/(console)/lib/operator-runs.ts @@ -48,7 +48,9 @@ async function fetchAs(path: string, init: RequestInit): Promise { }) ); } catch (err) { - throw new ReferenceServerUnreachableError(`Cannot reach authorization server at ${getAsInternalUrl()}`, err); + throw new ReferenceServerUnreachableError(`Cannot reach authorization server at ${getAsInternalUrl()}`, { + cause: err, + }); } } @@ -62,10 +64,10 @@ function parseDurationInput(value: string, label: string): number { const amount = Number.parseInt(match[1] ?? "0", 10); const unit = (match[2] || "s").toLowerCase(); const multipliers: Record = { - s: 1, - m: 60, - h: 60 * 60, d: 24 * 60 * 60, + h: 60 * 60, + m: 60, + s: 1, }; const multiplier = multipliers[unit] ?? 1; return amount * multiplier; @@ -89,8 +91,8 @@ async function runNowAt(path: string, options: RunNowOptions = {}) { method: "POST", ...(force ? { - headers: { "Content-Type": "application/json" }, body: asJson({ force: true }), + headers: { "Content-Type": "application/json" }, } : {}), }); @@ -110,15 +112,15 @@ async function saveScheduleAt( } ) { const body = { - interval_seconds: parseDurationInput(input.every, "schedule interval"), enabled: input.enabled, + interval_seconds: parseDurationInput(input.every, "schedule interval"), ...(input.jitter?.trim() ? { jitter_seconds: parseDurationInput(input.jitter, "schedule jitter") } : {}), }; const response = await fetchAs(path, { - method: "PUT", - headers: { "Content-Type": "application/json" }, body: asJson(body), + headers: { "Content-Type": "application/json" }, + method: "PUT", }); const responseBody = await readBody(response); if (!response.ok) { @@ -147,9 +149,9 @@ async function postScheduleMutationAt(path: string, fallback: string) { */ export async function setConnectionDisplayName(connectionId: string, displayName: string) { const response = await fetchAs(connectionControlPath(connectionId, ""), { - method: "PATCH", - headers: { "Content-Type": "application/json" }, body: asJson({ display_name: displayName }), + headers: { "Content-Type": "application/json" }, + method: "PATCH", }); const body = await readBody(response); if (!response.ok) { @@ -229,9 +231,9 @@ export async function submitRunInteraction( payload.data = input.data; } const response = await fetchAs(`/_ref/runs/${encodeURIComponent(runId)}/interaction`, { - method: "POST", - headers: { "Content-Type": "application/json" }, body: asJson(payload), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!response.ok) { @@ -283,7 +285,7 @@ function isUnavailableErrorBody(body: unknown): boolean { if (!body || typeof body !== "object") { return false; } - const error = (body as { error?: unknown }).error; + const { error } = body as { error?: unknown }; if (!error || typeof error !== "object") { return false; } @@ -319,9 +321,9 @@ export async function mintRunInteractionStream( payload.idempotency_key = input.idempotencyKey; } const response = await fetchAs(`/_ref/runs/${encodeURIComponent(runId)}/run-interaction-stream`, { - method: "POST", - headers: { "Content-Type": "application/json" }, body: asJson(payload), + headers: { "Content-Type": "application/json" }, + method: "POST", }); const body = await readBody(response); if (!response.ok) { @@ -346,13 +348,13 @@ export async function reportRunInteractionStreamReachFailure( input: { interactionId: string; reason: string; httpStatus: number | null } ): Promise { const response = await fetchAs(`/_ref/runs/${encodeURIComponent(runId)}/run-interaction-stream/reach-failure`, { - method: "POST", - headers: { "Content-Type": "application/json" }, body: asJson({ + http_status: input.httpStatus, interaction_id: input.interactionId, reason: input.reason, - http_status: input.httpStatus, }), + headers: { "Content-Type": "application/json" }, + method: "POST", }); if (!response.ok) { const body = await readBody(response); diff --git a/apps/console/src/app/(console)/lib/owner-token.ts b/apps/console/src/app/(console)/lib/owner-token.ts index 859f97539..37990c832 100644 --- a/apps/console/src/app/(console)/lib/owner-token.ts +++ b/apps/console/src/app/(console)/lib/owner-token.ts @@ -174,10 +174,9 @@ export async function readDashboardOwnerSession() { export class ReferenceServerUnreachableError extends Error { readonly cause: unknown; - constructor(message: string, cause: unknown) { - super(message); + constructor(message: string, options: ErrorOptions) { + super(message, options); this.name = "ReferenceServerUnreachableError"; - this.cause = cause; } } @@ -238,14 +237,14 @@ async function mintOwnerToken(): Promise { deviceRes = await fetch( `${asUrl}/oauth/device_authorization`, await withOwnerSessionCookie({ - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: CLIENT_ID }), cache: "no-store", + headers: { "Content-Type": "application/json" }, + method: "POST", }) ); } catch (err) { - throw new ReferenceServerUnreachableError(`Cannot reach authorization server at ${asUrl}`, err); + throw new ReferenceServerUnreachableError(`Cannot reach authorization server at ${asUrl}`, { cause: err }); } if (!deviceRes.ok) { const body = await deviceRes.text(); @@ -259,10 +258,10 @@ async function mintOwnerToken(): Promise { const approveRes = await fetch( `${asUrl}/device/approve`, await withOwnerSessionCookie({ - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_code: device.user_code }), cache: "no-store", + headers: { "Content-Type": "application/json" }, + method: "POST", }) ); if (!approveRes.ok) { @@ -276,14 +275,14 @@ async function mintOwnerToken(): Promise { const tokenRes = await fetch( `${asUrl}/oauth/token`, await withOwnerSessionCookie({ - method: "POST", - headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - grant_type: "urn:ietf:params:oauth:grant-type:device_code", - device_code: device.device_code, client_id: CLIENT_ID, + device_code: device.device_code, + grant_type: "urn:ietf:params:oauth:grant-type:device_code", }), cache: "no-store", + headers: { "Content-Type": "application/json" }, + method: "POST", }) ); if (!tokenRes.ok) { diff --git a/apps/console/src/app/(console)/lib/read-envelope.test.ts b/apps/console/src/app/(console)/lib/read-envelope.test.ts index 330b96896..3c673dfce 100644 --- a/apps/console/src/app/(console)/lib/read-envelope.test.ts +++ b/apps/console/src/app/(console)/lib/read-envelope.test.ts @@ -30,13 +30,13 @@ test("adaptListEnvelope reads canonical links.next and meta.warnings when presen const env = adaptListEnvelope<{ id: string }>({ data: [], has_more: false, - links: { self: "/v1/streams/x/records", next: "/v1/streams/x/records?cursor=abc" }, + links: { next: "/v1/streams/x/records?cursor=abc", self: "/v1/streams/x/records" }, meta: { + count: { kind: "estimated", value: 42 }, warnings: [ { code: "deprecated_alias", message: "connector_instance_id is deprecated" }, { code: "count_downgraded", dropped_parameter: "count=exact" }, ], - count: { kind: "estimated", value: 42 }, }, }); assert.equal(env.links.self, "/v1/streams/x/records"); @@ -70,9 +70,9 @@ test("adaptListEnvelope ignores unknown count kinds", () => { test("extractReadWarnings works on single-record envelopes", () => { const warnings = extractReadWarnings({ - object: "record", data: { id: "rec_1" }, meta: { warnings: [{ code: "skipped_source", message: "x not applicable" }] }, + object: "record", }); assert.equal(warnings.length, 1); assert.equal(warnings[0]?.code, "skipped_source"); diff --git a/apps/console/src/app/(console)/lib/read-envelope.ts b/apps/console/src/app/(console)/lib/read-envelope.ts index 6cadf6a1a..b889ac200 100644 --- a/apps/console/src/app/(console)/lib/read-envelope.ts +++ b/apps/console/src/app/(console)/lib/read-envelope.ts @@ -66,7 +66,7 @@ function extractMeta(value: unknown): CanonicalEnvelopeMeta { : []; let count: CanonicalCountMeta | null = null; if (isPlainObject(meta.count)) { - const kind = (meta.count as { kind?: unknown }).kind; + const { kind } = meta.count as { kind?: unknown }; const rawValue = (meta.count as { value?: unknown }).value; if (kind === "exact" || kind === "estimated" || kind === "none") { count = { @@ -108,9 +108,9 @@ export function adaptListEnvelope(body: unknown): CanonicalListEnvelope { return { data, has_more: hasMore, - next_cursor: nextCursor, links: extractLinks((root as { links?: unknown }).links), meta: extractMeta((root as { meta?: unknown }).meta), + next_cursor: nextCursor, }; } diff --git a/apps/console/src/app/(console)/lib/record-timestamps.ts b/apps/console/src/app/(console)/lib/record-timestamps.ts index 119326934..66e45c601 100644 --- a/apps/console/src/app/(console)/lib/record-timestamps.ts +++ b/apps/console/src/app/(console)/lib/record-timestamps.ts @@ -48,14 +48,14 @@ export function primaryTimestamp( ): { value: string; label: string; secondary: { label: string; value: string } | null } { if (semanticTimestamp) { return { - value: formatSemanticTimestamp(semanticTimestamp.value), label: humanizeFieldName(semanticTimestamp.field), secondary: { label: "ingested", value: formatTimestamp(emittedAt) }, + value: formatSemanticTimestamp(semanticTimestamp.value), }; } return { - value: formatTimestamp(emittedAt), label: "ingested", secondary: null, + value: formatTimestamp(emittedAt), }; } diff --git a/apps/console/src/app/(console)/lib/ref-client.ts b/apps/console/src/app/(console)/lib/ref-client.ts index 6273998e4..d6c2a6d25 100644 --- a/apps/console/src/app/(console)/lib/ref-client.ts +++ b/apps/console/src/app/(console)/lib/ref-client.ts @@ -124,7 +124,7 @@ function normalizeTimeline(raw: unknown): TimelineEnvelope { }; let events: SpineEvent[] = []; if (Array.isArray(r.events)) { - events = r.events; + ({ events: events } = r); } else if (Array.isArray(r.data)) { events = r.data; } @@ -136,12 +136,12 @@ function normalizeTimeline(raw: unknown): TimelineEnvelope { ? r.terminal_status : null; return { - object: r.object ?? "timeline", - trace_id: r.trace_id ?? null, event_count: typeof r.event_count === "number" ? r.event_count : events.length, events, next_cursor: typeof r.next_cursor === "string" && r.next_cursor.length > 0 ? r.next_cursor : undefined, + object: r.object ?? "timeline", terminal_status: terminalStatus, + trace_id: r.trace_id ?? null, truncated: r.truncated === true, }; } @@ -1203,7 +1203,9 @@ export async function refFetch( }) ); } catch (err) { - throw new ReferenceServerUnreachableError(`Cannot reach authorization server at ${getAsInternalUrl()}`, err); + throw new ReferenceServerUnreachableError(`Cannot reach authorization server at ${getAsInternalUrl()}`, { + cause: err, + }); } if (res.status === 404) { throw new RefNotFoundError(`not found: ${path}`); @@ -1233,8 +1235,8 @@ export { RefNotFoundError, RefRequestError }; // run. export class StaticSecretValidationError extends Error { readonly code = "static_secret_credential_rejected"; - constructor(message: string) { - super(message); + constructor(message: string, options?: ErrorOptions) { + super(message, options); this.name = "StaticSecretValidationError"; } } @@ -1439,14 +1441,14 @@ export async function listExploreTimeline( return (await refFetch("/_ref/explore/records", { connection: connection || undefined, cursor: opts.cursor ?? undefined, + // Only the oldest-first re-page sends a direction; newest-first is the default. + direction: opts.direction === "asc" ? "asc" : undefined, limit: opts.limit, - upcoming_limit: opts.upcomingLimit, rewind: opts.rewindToFirstPage ? 1 : undefined, stream: stream || undefined, + upcoming_limit: opts.upcomingLimit, xconnection: xconnection || undefined, xstream: xstream || undefined, - // Only the oldest-first re-page sends a direction; newest-first is the default. - direction: opts.direction === "asc" ? "asc" : undefined, })) as ExploreTimelinePage; } @@ -1914,7 +1916,7 @@ export async function captureStaticSecretCredential(input: { // becomes a typed error so the action keeps the owner on the form. The // message is the provider-named, owner-causal reason from the route. if (err instanceof RefRequestError && err.status === 400 && isCredentialRejectionBody(err.bodyText)) { - throw new StaticSecretValidationError(err.message); + throw new StaticSecretValidationError(err.message, { cause: err }); } throw err; } @@ -2124,15 +2126,17 @@ async function postManualUploadFile( url.searchParams.set("display_name", options.displayName); } const init = await withOwnerSessionCookie({ - method: "POST", body: file, cache: "no-store", + method: "POST", }); let res: Response; try { res = await fetch(url.toString(), init); } catch (err) { - throw new ReferenceServerUnreachableError(`Cannot reach authorization server at ${getAsInternalUrl()}`, err); + throw new ReferenceServerUnreachableError(`Cannot reach authorization server at ${getAsInternalUrl()}`, { + cause: err, + }); } if (!res.ok) { const body = await res.text(); @@ -2563,8 +2567,8 @@ export interface GrantPackageRevokeResult { export class GrantPackageRevokePartialFailureError extends Error { readonly result: GrantPackageRevokeResult; - constructor(result: GrantPackageRevokeResult) { - super(formatGrantPackageRevokePartialFailure(result)); + constructor(result: GrantPackageRevokeResult, options?: ErrorOptions) { + super(formatGrantPackageRevokePartialFailure(result), options); this.name = "GrantPackageRevokePartialFailureError"; this.result = result; } @@ -2621,7 +2625,7 @@ export async function lookupGrantPackageIdForGrant(grantId: string): Promise 0) { return row.grant_package_id; @@ -2672,7 +2676,7 @@ export async function revokeGrantPackage(packageId: string): Promise { } for (const candidate of MANIFESTS_DIR_CANDIDATES) { try { + // biome-ignore lint/performance/noAwaitInLoops: sequential by design const entries = await readdir(candidate); if (entries.length > 0) { resolvedManifestsDir = candidate; @@ -229,11 +230,11 @@ async function authedFetch(path: string, params?: Record { const body = (await authedFetch("/v1/streams", { - connector_id: connectorId, connection_id: opts.connectionId, + connector_id: connectorId, connector_instance_id: opts.connectionId ? undefined : opts.connectorInstanceId, })) as { data: StreamSummary[]; @@ -264,8 +265,8 @@ export async function getStreamMetadata( opts: ConnectionReadOptions = {} ): Promise { return (await authedFetch(`/v1/streams/${encodeURIComponent(stream)}`, { - connector_id: connectorId, connection_id: opts.connectionId, + connector_id: connectorId, connector_instance_id: opts.connectionId ? undefined : opts.connectorInstanceId, })) as StreamMetadata; } @@ -297,12 +298,12 @@ export async function queryRecords( } } const body = (await authedFetch(`/v1/streams/${encodeURIComponent(stream)}/records`, { - connector_id: connectorId, connection_id: opts.connectionId, + connector_id: connectorId, connector_instance_id: opts.connectionId ? undefined : opts.connectorInstanceId, count: opts.count, - limit: opts.limit ?? 50, cursor: opts.cursor, + limit: opts.limit ?? 50, order: opts.order, window: opts.window, ...filterParams, @@ -378,12 +379,12 @@ export async function aggregateRecordsByTime( } ): Promise { const body = (await authedFetch(`/v1/streams/${encodeURIComponent(stream)}/aggregate`, { - connector_id: connectorId, connection_id: opts.connectionId, + connector_id: connectorId, connector_instance_id: opts.connectionId ? undefined : opts.connectorInstanceId, - metric: "count", - group_by_time: opts.groupByTime, granularity: opts.granularity, + group_by_time: opts.groupByTime, + metric: "count", time_zone: opts.timeZone, })) as TimeBucketAggregate; return { @@ -456,13 +457,13 @@ export async function listExploreRecordBuckets(opts: { // which silently suppressed the over-time chart on the default browse. const body = (await refFetch("/_ref/explore/records/buckets", { connections: joinList(opts.connections), + granularity: opts.granularity, + since: opts.since ?? undefined, streams: joinList(opts.streams), + time_zone: opts.timeZone, + until: opts.until ?? undefined, xconnections: joinList(opts.excludeConnections), xstreams: joinList(opts.excludeStreams), - since: opts.since ?? undefined, - until: opts.until ?? undefined, - granularity: opts.granularity, - time_zone: opts.timeZone, })) as ExploreRecordBucketsResponse; return { ...body, @@ -486,8 +487,8 @@ export async function getRecord( opts: ConnectionReadOptions = {} ): Promise { const body = (await authedFetch(`/v1/streams/${encodeURIComponent(stream)}/records/${encodeURIComponent(recordId)}`, { - connector_id: connectorId, connection_id: opts.connectionId, + connector_id: connectorId, connector_instance_id: opts.connectionId ? undefined : opts.connectorInstanceId, })) as StreamRecord; return { @@ -580,11 +581,11 @@ export async function searchRecordsLexical( let res: Response; try { res = await fetch(url.toString(), { - headers: { Authorization: `Bearer ${token}` }, cache: "no-store", + headers: { Authorization: `Bearer ${token}` }, }); } catch (err) { - throw new ReferenceServerUnreachableError(`Cannot reach resource server at ${getRsInternalUrl()}`, err); + throw new ReferenceServerUnreachableError(`Cannot reach resource server at ${getRsInternalUrl()}`, { cause: err }); } if (!res.ok) { const body = await res.text(); @@ -639,11 +640,11 @@ export async function searchRecordsSemantic( let res: Response; try { res = await fetch(url.toString(), { - headers: { Authorization: `Bearer ${token}` }, cache: "no-store", + headers: { Authorization: `Bearer ${token}` }, }); } catch (err) { - throw new ReferenceServerUnreachableError(`Cannot reach resource server at ${getRsInternalUrl()}`, err); + throw new ReferenceServerUnreachableError(`Cannot reach resource server at ${getRsInternalUrl()}`, { cause: err }); } if (!res.ok) { const body = await res.text(); @@ -725,11 +726,11 @@ export async function searchRecordsHybrid( let res: Response; try { res = await fetch(url.toString(), { - headers: { Authorization: `Bearer ${token}` }, cache: "no-store", + headers: { Authorization: `Bearer ${token}` }, }); } catch (err) { - throw new ReferenceServerUnreachableError(`Cannot reach resource server at ${getRsInternalUrl()}`, err); + throw new ReferenceServerUnreachableError(`Cannot reach resource server at ${getRsInternalUrl()}`, { cause: err }); } if (!res.ok) { const body = await res.text(); @@ -753,23 +754,23 @@ export async function listConnectorManifests(): Promise { return []; } const files = await readdir(dir); - const manifests: ConnectorManifest[] = []; - for (const file of files) { - if (!file.endsWith(".json")) { - continue; - } - try { - const raw = await readFile(join(dir, file), "utf8"); - const m = JSON.parse(raw) as ConnectorManifest; - if (m.connector_id) { - manifests.push(m); - } - } catch { - // skip malformed - } - } - manifests.sort((a, b) => a.connector_id.localeCompare(b.connector_id)); - return manifests; + const manifests = await Promise.all( + files + .filter((file) => file.endsWith(".json")) + .map(async (file) => { + try { + const raw = await readFile(join(dir, file), "utf8"); + const m = JSON.parse(raw) as ConnectorManifest; + return m.connector_id ? m : null; + } catch { + // skip malformed + return null; + } + }) + ); + return manifests + .filter((manifest): manifest is ConnectorManifest => manifest !== null) + .sort((a, b) => a.connector_id.localeCompare(b.connector_id)); } export interface ConnectorOverview { @@ -904,7 +905,7 @@ function computeColumnStats(key: string, records: StreamRecord[]): ColumnStats { allLong = false; } } - return { nonNullCount, allSameValue, allLong }; + return { allLong, allSameValue, nonNullCount }; } function shouldKeepColumn(key: string, records: StreamRecord[]): boolean { @@ -1156,11 +1157,12 @@ async function paginateSampleRecords( let cursor: string | undefined; while (records.length < sampleLimit) { const remaining = sampleLimit - records.length; + // biome-ignore lint/performance/noAwaitInLoops: sequential by design const page = await queryRecords(connectorId, streamName, { connectionId: opts.connectionId, connectorInstanceId: opts.connectorInstanceId, - limit: Math.min(pageSize, remaining), cursor, + limit: Math.min(pageSize, remaining), }); records.push(...page.data); if (!(page.has_more && page.next_cursor)) { @@ -1199,11 +1201,11 @@ function initAggMap(fieldNames: Set): Map { const agg = new Map(); for (const f of fieldNames) { agg.set(f, { - present: false, - nullCount: 0, - nonNullCount: 0, distinct: new Set(), distinctCapped: false, + nonNullCount: 0, + nullCount: 0, + present: false, sampleValue: null, }); } @@ -1263,8 +1265,8 @@ interface ScanResult { function scanRecords(records: StreamRecord[], fieldNames: Set, cursorField: string | null): ScanResult { const agg = initAggMap(fieldNames); - const emittedRange: RangeTracker = { min: null, max: null }; - const cursorRange: RangeTracker = { min: null, max: null }; + const emittedRange: RangeTracker = { max: null, min: null }; + const cursorRange: RangeTracker = { max: null, min: null }; for (const r of records) { if (r.emitted_at) { @@ -1283,19 +1285,19 @@ function scanRecords(records: StreamRecord[], fieldNames: Set, cursorFie } } - return { agg, emittedRange, cursorRange }; + return { agg, cursorRange, emittedRange }; } function projectFields(agg: Map, declaredProps: string[]): FieldHealth[] { const declaredSet = new Set(declaredProps); return Array.from(agg, ([name, a]) => ({ - name, declared: declaredSet.has(name), - present: a.present, - nullCount: a.nullCount, - nonNullCount: a.nonNullCount, - distinctValues: a.distinct.size, distinctCapped: a.distinctCapped, + distinctValues: a.distinct.size, + name, + nonNullCount: a.nonNullCount, + nullCount: a.nullCount, + present: a.present, sampleValue: a.sampleValue, })).sort((x, y) => { // declared-first, then by name, for stable display @@ -1308,11 +1310,11 @@ function projectFields(agg: Map, declaredProps: string[]): Fie function computeFieldSummary(fields: FieldHealth[]) { return { - declared: fields.filter((f) => f.declared).length, - present: fields.filter((f) => f.present).length, - entirelyNull: fields.filter((f) => f.present && f.nonNullCount === 0).length, constValued: fields.filter((f) => f.nonNullCount > 0 && f.distinctValues === 1).length, + declared: fields.filter((f) => f.declared).length, declaredButAbsent: fields.filter((f) => f.declared && !f.present).length, + entirelyNull: fields.filter((f) => f.present && f.nonNullCount === 0).length, + present: fields.filter((f) => f.present).length, undeclaredPresent: fields.filter((f) => !f.declared && f.present).length, }; } @@ -1348,16 +1350,16 @@ export async function streamHealth( return { connectorId, - streamName, - totalRecords, - sampled: records.length, - sampleLimit, - limited: totalRecords > records.length, - emittedAt: { min: emittedRange.min, max: emittedRange.max }, cursorField, - cursorRange: cursorField ? { min: cursorRange.min, max: cursorRange.max } : null, + cursorRange: cursorField ? { max: cursorRange.max, min: cursorRange.min } : null, + emittedAt: { max: emittedRange.max, min: emittedRange.min }, fields, + limited: totalRecords > records.length, + sampled: records.length, + sampleLimit, + streamName, summary, + totalRecords, }; } @@ -1378,13 +1380,13 @@ function projectRun( return null; } return { - run_id: summary.run_id, - first_at: summary.first_at, - last_at: summary.last_at, event_count: summary.event_count, - status: summary.status, failure_reason: summary.failure_reason, + first_at: summary.first_at, known_gaps: summary.known_gaps ?? [], + last_at: summary.last_at, + run_id: summary.run_id, + status: summary.status, }; } @@ -1399,19 +1401,19 @@ export async function getConnectorOverview(connector: ConnectorManifest): Promis const { listRuns } = await import("./ref-client.ts"); const [latestResp, successResp] = await Promise.all([ listRuns({ connector_id: connector.connector_id, limit: 1 }), - listRuns({ connector_id: connector.connector_id, status: "succeeded", limit: 1 }), + listRuns({ connector_id: connector.connector_id, limit: 1, status: "succeeded" }), ]); const lastRun = projectRun(latestResp.data?.[0]); const lastSuccessfulRun = projectRun(successResp.data?.[0]); - const isRunning = lastRun != null && isActiveConnectorRunSummaryStatus(lastRun.status); + const isRunning = lastRun !== null && isActiveConnectorRunSummaryStatus(lastRun.status); return { connector, - streams, - totalRecords, + isRunning, lastRun, lastSuccessfulRun, - isRunning, + streams, + totalRecords, }; } catch (err) { if (err instanceof ReferenceServerUnreachableError) { @@ -1419,12 +1421,12 @@ export async function getConnectorOverview(connector: ConnectorManifest): Promis } return { connector, - streams: [], - totalRecords: 0, + error: err instanceof Error ? err.message : String(err), + isRunning: false, lastRun: null, lastSuccessfulRun: null, - isRunning: false, - error: err instanceof Error ? err.message : String(err), + streams: [], + totalRecords: 0, }; } } diff --git a/apps/console/src/app/(console)/lib/run-assistance.test.ts b/apps/console/src/app/(console)/lib/run-assistance.test.ts index ce01a12d6..7bf2cbce3 100644 --- a/apps/console/src/app/(console)/lib/run-assistance.test.ts +++ b/apps/console/src/app/(console)/lib/run-assistance.test.ts @@ -149,8 +149,8 @@ test("active browser-surface events keep stream fallback in the browser-preparin }), event("run.browser_surface_ready", { browser_surface: { - browser_surface_status: "leased", browser_surface_lease_id: "lease_1", + browser_surface_status: "leased", }, }), event("run.started", { diff --git a/apps/console/src/app/(console)/lib/run-assistance.ts b/apps/console/src/app/(console)/lib/run-assistance.ts index be8b3953a..83f489c7f 100644 --- a/apps/console/src/app/(console)/lib/run-assistance.ts +++ b/apps/console/src/app/(console)/lib/run-assistance.ts @@ -114,7 +114,7 @@ function findCurrentStructuredAssistance( ): CurrentRunAssistance | null { for (let index = events.length - 1; index >= 0; index -= 1) { const event = events[index]; - if (!event || event.event_type !== "run.assistance_requested") { + if (event?.event_type !== "run.assistance_requested") { continue; } const id = getEventAssistanceId(event); @@ -140,7 +140,7 @@ function findCurrentLegacyInteraction( ): CurrentRunAssistance | null { for (let index = events.length - 1; index >= 0; index -= 1) { const event = events[index]; - if (!event || event.event_type !== "run.interaction_required") { + if (event?.event_type !== "run.interaction_required") { continue; } const id = getEventAssistanceId(event); @@ -193,49 +193,55 @@ function isStreamableBrowserSurfaceAssistance(assistance: CurrentRunAssistance): } function assistanceFromEvent(event: SpineEvent, id: string): CurrentRunAssistance { + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic const data = event.data ?? {}; return { + attachments: parseAttachments(data.attachments), + fields: parseFields(data.input_schema ?? data.schema), id, isLegacyInteraction: false, kind: stringField(data.kind) ?? "assistance", message: stringField(data.message) ?? "Waiting for the requested run assistance.", - progressPosture: progressPostureField(data.progress_posture) ?? "blocked", ownerAction: ownerActionField(data.owner_action) ?? "provide_value", + progressPosture: progressPostureField(data.progress_posture) ?? "blocked", responseContract: responseContractField(data.response_contract) ?? "response_required", - attachments: parseAttachments(data.attachments), - fields: parseFields(data.input_schema ?? data.schema), timeoutLabel: timeoutLabel(data.timeout_seconds), }; } function assistanceFromLegacyInteraction(event: SpineEvent, id: string): CurrentRunAssistance { + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic const data = event.data ?? {}; const kind = stringField(data.kind) ?? "interaction"; const isManualAction = kind === "manual_action"; return { + attachments: isManualAction ? [{ kind: "browser_surface", label: null, ref: null, status: null }] : [], + fields: parseFields(data.schema), id, isLegacyInteraction: true, kind, message: stringField(data.message) ?? "Awaiting operator response.", - progressPosture: "blocked", ownerAction: isManualAction ? "operate_attachment" : "provide_value", + progressPosture: "blocked", responseContract: "response_required", - attachments: isManualAction ? [{ kind: "browser_surface", label: null, ref: null, status: null }] : [], - fields: parseFields(data.schema), timeoutLabel: timeoutLabel(data.timeout_seconds), }; } function getEventAssistanceId(event: SpineEvent): string | null { return ( + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic stringField(event.data?.assistance_request_id) ?? + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic stringField(event.data?.assistance_id) ?? + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic stringField(event.data?.interaction_id) ?? stringField(event.interaction_id) ); } function readBrowserSurfaceStatus(event: SpineEvent): string | null { + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic const browserSurface = event.data?.browser_surface; if (browserSurface && typeof browserSurface === "object" && !Array.isArray(browserSurface)) { return stringField((browserSurface as Record).browser_surface_status) ?? stringField(event.status); @@ -298,9 +304,9 @@ function parseFields(schema: unknown): AssistanceField[] { .map(([name, rawDef]): AssistanceField => { const def = rawDef && typeof rawDef === "object" ? (rawDef as Record) : {}; return { - name, - label: stringField(def.title), format: def.format === "password" ? "password" : "text", + label: stringField(def.title), + name, required: requiredFields.has(name), }; }) diff --git a/apps/console/src/app/(console)/lib/run-gaps.test.ts b/apps/console/src/app/(console)/lib/run-gaps.test.ts index 7c5530f01..695d3cc77 100644 --- a/apps/console/src/app/(console)/lib/run-gaps.test.ts +++ b/apps/console/src/app/(console)/lib/run-gaps.test.ts @@ -70,29 +70,29 @@ test("classifyKnownGaps keeps protocol violations distinct from source coverage test("connectorHasPartialCoverageHint requires produced records and a non-protocol coverage gap", () => { assert.equal( connectorHasPartialCoverageHint({ - totalRecords: 12, lastRunKnownGaps: [{ kind: "skip_result", reason: "missing_credentials" }], + totalRecords: 12, }), true ); assert.equal( connectorHasPartialCoverageHint({ - totalRecords: 0, lastRunKnownGaps: [{ kind: "skip_result", reason: "missing_credentials" }], + totalRecords: 0, }), false ); assert.equal( connectorHasPartialCoverageHint({ - totalRecords: 12, lastRunKnownGaps: [{ kind: "run_failed", reason: "connector_protocol_violation" }], + totalRecords: 12, }), false ); assert.equal( connectorHasPartialCoverageHint({ - totalRecords: 12, lastRunKnownGaps: [{ kind: "skip_result", reason: "not_available", severity: "informational" }], + totalRecords: 12, }), false ); @@ -111,7 +111,7 @@ test("extractTerminalKnownGaps reads the latest terminal event payload", () => { recovery_hint: { action: "retry_by_runtime" }, }, ], - known_gaps_summary: { count: 1, truncated: false, by_reason: { partially_committed: 1 } }, + known_gaps_summary: { by_reason: { partially_committed: 1 }, count: 1, truncated: false }, }, event_id: "evt_1", event_type: "run.failed", @@ -164,25 +164,25 @@ test("normalizeKnownGaps and formatRecoveryHint tolerate unknown payloads", () = }); test("normalizeKnownGaps propagates bounded SKIP_RESULT diagnostics object", () => { - const diagnostics = { phase: "export_artifact_wait_failed", error: "download_empty", dialogs_open: 1 }; - const [gap] = normalizeKnownGaps([{ kind: "skip_result", reason: "export_no_download", diagnostics }]); + const diagnostics = { dialogs_open: 1, error: "download_empty", phase: "export_artifact_wait_failed" }; + const [gap] = normalizeKnownGaps([{ diagnostics, kind: "skip_result", reason: "export_no_download" }]); assert.ok(gap); assert.deepEqual(gap.diagnostics, diagnostics); }); test("normalizeKnownGaps drops diagnostics when value is an array or scalar", () => { - const [gapArray] = normalizeKnownGaps([{ kind: "skip_result", reason: "r", diagnostics: ["a", "b"] }]); + const [gapArray] = normalizeKnownGaps([{ diagnostics: ["a", "b"], kind: "skip_result", reason: "r" }]); assert.ok(gapArray); assert.equal(gapArray.diagnostics, undefined); - const [gapString] = normalizeKnownGaps([{ kind: "skip_result", reason: "r", diagnostics: "text" }]); + const [gapString] = normalizeKnownGaps([{ diagnostics: "text", kind: "skip_result", reason: "r" }]); assert.ok(gapString); assert.equal(gapString.diagnostics, undefined); }); test("normalizeKnownGaps passes sentinel diagnostics object through unchanged", () => { - const sentinel = { truncated: true, reason: "size_overflow" }; - const [gap] = normalizeKnownGaps([{ kind: "skip_result", reason: "export_no_download", diagnostics: sentinel }]); + const sentinel = { reason: "size_overflow", truncated: true }; + const [gap] = normalizeKnownGaps([{ diagnostics: sentinel, kind: "skip_result", reason: "export_no_download" }]); assert.ok(gap); assert.deepEqual(gap.diagnostics, sentinel); }); @@ -194,8 +194,8 @@ test("extractTerminalKnownGaps preserves diagnostics on known gaps", () => { actor_type: "runtime", client_id: null, data: { - known_gaps: [{ kind: "skip_result", reason: "export_no_download", diagnostics }], - known_gaps_summary: { count: 1, truncated: false, by_reason: { export_no_download: 1 } }, + known_gaps: [{ diagnostics, kind: "skip_result", reason: "export_no_download" }], + known_gaps_summary: { by_reason: { export_no_download: 1 }, count: 1, truncated: false }, }, event_id: "evt_2", event_type: "run.completed", diff --git a/apps/console/src/app/(console)/lib/run-gaps.ts b/apps/console/src/app/(console)/lib/run-gaps.ts index e5c3ef11a..cbd772735 100644 --- a/apps/console/src/app/(console)/lib/run-gaps.ts +++ b/apps/console/src/app/(console)/lib/run-gaps.ts @@ -97,7 +97,7 @@ export function resolvePartialCoverageCue({ lastRunKnownGaps: readonly KnownGap[] | null | undefined; totalRecords: number; }): boolean { - if (collectionReport != null) { + if (collectionReport !== null) { return connectorHasPartialCoverageFromReport(collectionReport); } return connectorHasPartialCoverageHint({ lastRunKnownGaps, totalRecords }); @@ -244,8 +244,8 @@ function summarizeKnownGaps(gaps: readonly KnownGap[]): KnownGapSummary { byReason[gap.reason] = (byReason[gap.reason] ?? 0) + 1; } return { + by_reason: byReason, count: gaps.length, truncated: false, - by_reason: byReason, }; } diff --git a/apps/console/src/app/(console)/lib/source-actionability.test.ts b/apps/console/src/app/(console)/lib/source-actionability.test.ts index 8213134e4..29e44122a 100644 --- a/apps/console/src/app/(console)/lib/source-actionability.test.ts +++ b/apps/console/src/app/(console)/lib/source-actionability.test.ts @@ -244,8 +244,8 @@ test("source actionability routes a Needs refresh pill (no wired owner action) t connector({ rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Needs refresh", tone: "amber" }, forward_statement: "This connection is paused.", + pill: { label: "Needs refresh", tone: "amber" }, required_actions: [], }), }) @@ -269,8 +269,8 @@ test("source actionability routes a Syncing pill (active run over stale/owner-re }), rendered_verdict: verdict({ channel: "calm", - pill: { label: "Syncing", tone: "amber" }, forward_statement: "Refreshing now.", + pill: { label: "Syncing", tone: "amber" }, required_actions: [], }), }) @@ -288,8 +288,8 @@ test("source actionability keeps a Degraded pill (no wired owner action) in syst connector({ rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Degraded", tone: "amber" }, forward_statement: "Connector code needs a fix before this can collect again.", + pill: { label: "Degraded", tone: "amber" }, required_actions: [ action({ audience: "maintainer", @@ -438,7 +438,7 @@ test("source actionability projects a draft connection as needs-you setup_in_pro test("source actionability: revoked outranks draft — a revoked connection never reads as setup_in_progress", () => { const actionability = projectSourceActionability( - draftConnector({ status: "revoked", revoked_at: "2026-07-10T00:00:00Z" }) + draftConnector({ revoked_at: "2026-07-10T00:00:00Z", status: "revoked" }) ); assert.equal(actionability.revoked, true); @@ -493,8 +493,8 @@ test("source actionability headline counts only needs-owner work and exposes sta display_name: "Review source", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Healthy", tone: "green" }, forward_statement: "Run a refresh to bring this up to date.", + pill: { label: "Healthy", tone: "green" }, required_actions: [ action({ cta: "Refresh now", @@ -510,8 +510,8 @@ test("source actionability headline counts only needs-owner work and exposes sta display_name: "System source", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Degraded", tone: "amber" }, forward_statement: "Connector code needs a fix before this can collect again.", + pill: { label: "Degraded", tone: "amber" }, required_actions: [ action({ audience: "maintainer", @@ -528,8 +528,8 @@ test("source actionability headline counts only needs-owner work and exposes sta display_name: "Not measured source", rendered_verdict: verdict({ channel: "calm", - pill: { label: "Not measured", tone: "grey" }, forward_statement: "Freshness has not been measured yet.", + pill: { label: "Not measured", tone: "grey" }, required_actions: [], }), }), @@ -538,8 +538,8 @@ test("source actionability headline counts only needs-owner work and exposes sta display_name: "Working source", rendered_verdict: verdict({ channel: "calm", - pill: { label: "Checking", tone: "grey" }, forward_statement: "Measuring coverage now.", + pill: { label: "Checking", tone: "grey" }, required_actions: [], }), }), @@ -556,6 +556,10 @@ test("source actionability headline counts only needs-owner work and exposes sta label: "Needs you", note: "Requires your input before collection can continue.", }, + notMeasured: { + label: "Not measured", + note: "Evidence is missing and no active check is running.", + }, review: { label: "Available actions", note: "Optional refreshes and retries you can start.", @@ -568,10 +572,6 @@ test("source actionability headline counts only needs-owner work and exposes sta label: "PDPP is working", note: "Collection, recovery, or a bounded check is active.", }, - notMeasured: { - label: "Not measured", - note: "Evidence is missing and no active check is running.", - }, }); }); @@ -586,8 +586,8 @@ test("source actionability groups a Needs refresh connection under review, never display_name: "Paused source", rendered_verdict: verdict({ channel: "advisory", - pill: { label: "Needs refresh", tone: "amber" }, forward_statement: "This connection is paused.", + pill: { label: "Needs refresh", tone: "amber" }, required_actions: [], }), }), @@ -665,7 +665,7 @@ test("recovery grouping: an inactive queued recovery row is passive progress, ne assert.equal(groups.working.length, 1); assert.equal(groups.systemIssues.length, 0); assert.equal(groups.notMeasured.length, 0); - const row = groups.working[0]; + const [row] = groups.working; assert.ok(row); assert.doesNotMatch(row.statusLabel, RECOVERY_CHECKING_RE); assert.doesNotMatch(row.what, RECOVERY_CHECKING_RE); @@ -702,7 +702,7 @@ test("recovery grouping: active recovery names the work like syncing order detai ]); assert.equal(groups.working.length, 1); - const row = groups.working[0]; + const [row] = groups.working; assert.ok(row); // The row names the work ("is syncing details" / "Syncing details now."), // never a generic "Checking" bucket. @@ -784,7 +784,7 @@ test("recovery grouping: an inactive backlog routes to NAMED recovery before the const groups = sourceWorkFromConnectors([summary]); assert.equal(groups.working.length, 1); - const row = groups.working[0]; + const [row] = groups.working; assert.ok(row); // Named recovery copy ("is catching up" / "Catching up …"), never "Checking". assert.doesNotMatch(row.statusLabel, RECOVERY_CHECKING_RE); diff --git a/apps/console/src/app/(console)/lib/source-actionability.ts b/apps/console/src/app/(console)/lib/source-actionability.ts index 03eee4a68..ad7bb826a 100644 --- a/apps/console/src/app/(console)/lib/source-actionability.ts +++ b/apps/console/src/app/(console)/lib/source-actionability.ts @@ -103,6 +103,10 @@ export const SOURCE_WORK_GROUP_COPY: Record> = { - green: { kind: "healthy", dot: "●", tone: "success" }, - amber: { kind: "degraded", dot: "◐", tone: "warning" }, - red: { kind: "blocked", dot: "⊘", tone: "destructive" }, - grey: { kind: "unknown", dot: "○", tone: "muted" }, + amber: { dot: "◐", kind: "degraded", tone: "warning" }, + green: { dot: "●", kind: "healthy", tone: "success" }, + grey: { dot: "○", kind: "unknown", tone: "muted" }, + red: { dot: "⊘", kind: "blocked", tone: "destructive" }, }; function readableConnectorId(connectorId: string): string { @@ -157,6 +157,7 @@ function connectionRouteId(connector: RefConnectorSummary): string { function connectorLabel(connector: RefConnectorSummary): string { return ( + // biome-ignore lint/suspicious/noUnnecessaryConditions: API display names can be absent at runtime despite optimistic generated types. connector.display_name?.trim() || connector.connector_display_name?.trim() || readableConnectorId(connector.connector_id) @@ -211,6 +212,7 @@ export function primaryOwnerActionRemediation( } export function hasPrimaryOwnerLocalDeviceRemediation(verdict: RefRenderedVerdict | null | undefined): boolean { + // biome-ignore lint/suspicious/noUnnecessaryConditions: a valid owner action may omit remediation at runtime. return primaryOwnerActionRemediation(verdict)?.target.kind === "local_device"; } @@ -232,30 +234,30 @@ export function deriveRenderedSourceStatus( pending = false ): SourceStatusFlag { if (revoked) { - return { kind: "revoked", dot: "⊘", tone: "muted", label: "Revoked", freshnessNote: null }; + return { dot: "⊘", freshnessNote: null, kind: "revoked", label: "Revoked", tone: "muted" }; } // Setup-in-progress overrides any verdict shape, same priority as revoked: // a draft has no meaningful health/coverage evidence yet (see // `isSetupInProgressConnector`), so its verdict tone (if any) must never be // shown as the status. if (pending) { - return { kind: "pending", dot: "◌", tone: "muted", label: "Setup in progress", freshnessNote: null }; + return { dot: "◌", freshnessNote: null, kind: "pending", label: "Setup in progress", tone: "muted" }; } if (!verdict) { return { - kind: "unknown", dot: "○", - tone: "muted", - label: "Verdict unavailable", freshnessNote: null, + kind: "unknown", + label: "Verdict unavailable", + tone: "muted", }; } const status = VERDICT_TONE_STATUS[verdict.pill.tone]; const freshnessNote = freshnessNoteFromVerdict(verdict); return { ...status, - label: labelWithFreshness(verdict.pill.label, freshnessNote), freshnessNote, + label: labelWithFreshness(verdict.pill.label, freshnessNote), }; } @@ -404,15 +406,15 @@ function isNotMeasured(verdict: NonNullable> = { active: "is syncing details", - queued: "is catching up", cooling: "is waiting to retry", + queued: "is catching up", stalled: "recovery is stalled", }; const RECOVERY_WHAT: Partial> = { active: "Syncing details now.", - queued: "Catching up details when it is safe to retry.", cooling: "Waiting until it is safe to retry details.", + queued: "Catching up details when it is safe to retry.", stalled: "Recovery has stopped making progress and needs a look.", }; @@ -510,6 +512,7 @@ export function sourceWorkItemFromConnector(connector: RefConnectorSummary): Sou if (verdict.channel === "attention" && ownerAction) { return itemFromConnector(connector, "needsOwner", { actionLabel: ownerAction.cta, + // biome-ignore lint/suspicious/noUnnecessaryConditions: a valid owner action may omit remediation at runtime. deviceLocal: ownerAction.remediation?.target.kind === "local_device", statusLabel: "needs you", what: verdict.forward_statement, @@ -519,6 +522,7 @@ export function sourceWorkItemFromConnector(connector: RefConnectorSummary): Sou if (ownerAction) { return itemFromConnector(connector, "review", { actionLabel: ownerAction.cta, + // biome-ignore lint/suspicious/noUnnecessaryConditions: a valid owner action may omit remediation at runtime. deviceLocal: ownerAction.remediation?.target.kind === "local_device", // The concrete CTA (`ownerAction.cta`, e.g. "Refresh now" / "Retry now") // carries the row copy; the statusLabel is a neutral fallback, never the diff --git a/apps/console/src/app/(console)/lib/source-add-support.test.ts b/apps/console/src/app/(console)/lib/source-add-support.test.ts index ffd0ced3a..5be813152 100644 --- a/apps/console/src/app/(console)/lib/source-add-support.test.ts +++ b/apps/console/src/app/(console)/lib/source-add-support.test.ts @@ -41,11 +41,11 @@ function staticSecretManifest(connectorId: string): CatalogManifestLike { display_name: connectorId, runtime_requirements: { bindings: { network: {} } }, setup: { - modality: "static_secret", credential_capture: { credential_kind: "api_token", - fields: [{ name: "api_token", label: "API token", secret: true }], + fields: [{ label: "API token", name: "api_token", secret: true }], }, + modality: "static_secret", }, } as unknown as CatalogManifestLike; } @@ -80,12 +80,12 @@ function manualUploadManifest(connectorId: string): CatalogManifestLike { display_name: connectorId, runtime_requirements: { bindings: { filesystem: {} } }, setup: { - modality: "manual_or_upload", manual_or_upload: { accepted_file_names: ["Timeline.json"], import_dir_env_var: "GOOGLE_MAPS_TIMELINE_DIR", label: "Timeline export", }, + modality: "manual_or_upload", }, }; } @@ -108,8 +108,8 @@ function providerAuthManifest(connectorId: string): CatalogManifestLike { display_name: connectorId, runtime_requirements: { bindings: { network: {} } }, setup: { - modality: "provider_authorization", deployment_config: ["GOOGLE_DATAPORTABILITY_CLIENT_ID", "GOOGLE_DATAPORTABILITY_CLIENT_SECRET"], + modality: "provider_authorization", }, }; } diff --git a/apps/console/src/app/(console)/lib/source-add-support.ts b/apps/console/src/app/(console)/lib/source-add-support.ts index 48a0440a5..891fa313d 100644 --- a/apps/console/src/app/(console)/lib/source-add-support.ts +++ b/apps/console/src/app/(console)/lib/source-add-support.ts @@ -45,17 +45,17 @@ export interface SourceAddSupport { } const SUPPORT_LABELS: Record = { - self_service: "Add another account", - packaged_path_pending: "Add path not packaged", deployment_prerequisite: "Server setup required to add another account", not_self_service: "Add path not available here", + packaged_path_pending: "Add path not packaged", + self_service: "Add another account", }; const SUPPORT_TONES: Record = { - self_service: "border-emerald-500/30 bg-emerald-500/10 text-emerald-700", - packaged_path_pending: "border-amber-500/30 bg-amber-500/10 text-amber-700", deployment_prerequisite: "border-amber-500/30 bg-amber-500/10 text-amber-700", not_self_service: "border-border bg-muted/30 text-muted-foreground", + packaged_path_pending: "border-amber-500/30 bg-amber-500/10 text-amber-700", + self_service: "border-emerald-500/30 bg-emerald-500/10 text-emerald-700", }; /** @@ -91,9 +91,9 @@ export function buildSourceAddSupport(manifests: readonly CatalogManifestLike[]) for (const entry of catalog) { const support = addAccountSupport(entry); map.set(entry.connectorKey, { + action: addAnotherAccountAction(entry), connectorKey: entry.connectorKey, support, - action: addAnotherAccountAction(entry), supportLabel: SUPPORT_LABELS[support], supportTone: SUPPORT_TONES[support], }); diff --git a/apps/console/src/app/(console)/lib/source-copy-negative.test.ts b/apps/console/src/app/(console)/lib/source-copy-negative.test.ts index 18089ed5e..c3377ae27 100644 --- a/apps/console/src/app/(console)/lib/source-copy-negative.test.ts +++ b/apps/console/src/app/(console)/lib/source-copy-negative.test.ts @@ -86,8 +86,8 @@ function stripComments(src: string): string { function entryForDisposition(disposition: ConnectorCatalogEntry["disposition"]): ConnectorCatalogEntry { return { connectorKey: `stub-${disposition}`, + deploymentReadiness: { blockers: [], ready: true }, disposition, - deploymentReadiness: { ready: true, blockers: [] }, } as unknown as ConnectorCatalogEntry; } @@ -172,8 +172,8 @@ test("the agreed add-account labels are exactly the realignment-plan vocabulary" display_name: "ynab", runtime_requirements: { bindings: { network: {} } }, setup: { + credential_capture: { credential_kind: "api_token", fields: [{ label: "T", name: "t", secret: true }] }, modality: "static_secret", - credential_capture: { credential_kind: "api_token", fields: [{ name: "t", label: "T", secret: true }] }, }, } as never, { diff --git a/apps/console/src/app/(console)/lib/source-groups.test.ts b/apps/console/src/app/(console)/lib/source-groups.test.ts index af5e2f7b2..5f9541506 100644 --- a/apps/console/src/app/(console)/lib/source-groups.test.ts +++ b/apps/console/src/app/(console)/lib/source-groups.test.ts @@ -17,31 +17,31 @@ type ConnectionHealthState = NonNullable[ function overview(partial: Partial): ConnectorOverview { return { - connector: { connector_id: "test", name: "Test", display_name: "Test" }, - streams: [], - totalRecords: 0, + connector: { connector_id: "test", display_name: "Test", name: "Test" }, isRunning: false, lastRun: null, lastSuccessfulRun: null, + streams: [], + totalRecords: 0, ...partial, }; } function healthState(state: ConnectionHealthState): ConnectorOverview["connectionHealth"] { return { - state, - reason_code: null, + axes: { attention: "none", coverage: "unknown", freshness: "unknown", outbox: "unknown", remote_surface: "none" }, + badges: { stale: false, syncing: false }, last_success_at: null, next_attempt_at: null, - axes: { freshness: "unknown", coverage: "unknown", attention: "none", outbox: "unknown", remote_surface: "none" }, - badges: { stale: false, syncing: false }, + reason_code: null, + state, } as ConnectorOverview["connectionHealth"]; } function connection(connectorId: string, opts: Partial = {}): ConnectorOverview { return overview({ - connector: { connector_id: connectorId, name: connectorId, display_name: connectorId }, connectionId: `${connectorId}-${Math.random().toString(36).slice(2, 8)}`, + connector: { connector_id: connectorId, display_name: connectorId, name: connectorId }, ...opts, }); } @@ -75,11 +75,11 @@ test("existing-data count is distinct from connection count", () => { test("needs_attention and blocked connections drive the attention count and a repair route", () => { const group = only( groupSourcesByConnector([ - connection("chase", { totalRecords: 500, connectionHealth: healthState("healthy") }), + connection("chase", { connectionHealth: healthState("healthy"), totalRecords: 500 }), connection("chase", { - totalRecords: 10, - connectionId: "chase-broken", connectionHealth: healthState("needs_attention"), + connectionId: "chase-broken", + totalRecords: 10, }), ]) ); @@ -104,8 +104,8 @@ test("revoked connections stay counted as existing source connections", () => { test("sources needing attention sort ahead of healthy sources", () => { const groups = groupSourcesByConnector([ - connection("aaa_healthy", { totalRecords: 1, connectionHealth: healthState("healthy") }), - connection("zzz_broken", { totalRecords: 1, connectionHealth: healthState("blocked") }), + connection("aaa_healthy", { connectionHealth: healthState("healthy"), totalRecords: 1 }), + connection("zzz_broken", { connectionHealth: healthState("blocked"), totalRecords: 1 }), ]); assert.equal(groups[0]?.connectorId, "zzz_broken", "attention-needed source leads despite later alphabetically"); assert.equal(groups[1]?.connectorId, "aaa_healthy"); @@ -113,7 +113,7 @@ test("sources needing attention sort ahead of healthy sources", () => { test("a healthy source carries no repair route", () => { const group = only( - groupSourcesByConnector([connection("gmail", { totalRecords: 100, connectionHealth: healthState("healthy") })]) + groupSourcesByConnector([connection("gmail", { connectionHealth: healthState("healthy"), totalRecords: 100 })]) ); assert.equal(group.needsAttentionCount, 0); assert.equal(group.attentionRouteId, null); diff --git a/apps/console/src/app/(console)/lib/source-groups.ts b/apps/console/src/app/(console)/lib/source-groups.ts index 006026636..67da6d895 100644 --- a/apps/console/src/app/(console)/lib/source-groups.ts +++ b/apps/console/src/app/(console)/lib/source-groups.ts @@ -53,17 +53,17 @@ function needsAttention(overview: ConnectorOverview): boolean { function seedGroup(overview: ConnectorOverview): SourceGroup { const attention = needsAttention(overview); return { + attentionRouteId: attention ? routeId(overview) : null, + connectionCount: 1, connectorId: overview.connector.connector_id, displayName: formatConnectorNameForDisplay({ connectorId: overview.connector.connector_id, displayName: overview.connectorDisplayName, name: overview.connector.name, }), - connectionCount: 1, needsAttentionCount: attention ? 1 : 0, revokedCount: isRevokedConnection(overview) ? 1 : 0, withDataCount: overview.totalRecords > 0 ? 1 : 0, - attentionRouteId: attention ? routeId(overview) : null, }; } diff --git a/apps/console/src/app/(console)/lib/source-recovery-state.test.ts b/apps/console/src/app/(console)/lib/source-recovery-state.test.ts index 368345af5..418f58f05 100644 --- a/apps/console/src/app/(console)/lib/source-recovery-state.test.ts +++ b/apps/console/src/app/(console)/lib/source-recovery-state.test.ts @@ -120,8 +120,8 @@ test("recovery: an active retry floor makes the step cooling", () => { const step = deriveRecoveryStep( verdict(), health({ + detail_gap_backlog: backlog({ next_attempt_at: "2026-07-06T15:40:00Z", pending: 2093 }), state: "cooling_off", - detail_gap_backlog: backlog({ pending: 2093, next_attempt_at: "2026-07-06T15:40:00Z" }), }) ); assert.equal(step, "cooling"); @@ -136,7 +136,7 @@ test("recovery: an owner-satisfiable non-attention action makes recovery eligibl action({ audience: "owner", cta: "Retry now", kind: "retry_gap", satisfied_when: { kind: "gap_recovered" } }), ], }), - health({ state: "degraded", detail_gap_backlog: backlog({ pending: 12 }) }) + health({ detail_gap_backlog: backlog({ pending: 12 }), state: "degraded" }) ); assert.equal(step, "eligible"); assert.equal(recoveryStateGroup(step), "review"); @@ -157,7 +157,7 @@ test("recovery: an attention-channel owner action is owner-required, not passive }), ], }), - health({ state: "blocked", detail_gap_backlog: backlog({ pending: 12 }) }) + health({ detail_gap_backlog: backlog({ pending: 12 }), state: "blocked" }) ); assert.equal(step, "owner_required"); assert.equal(recoveryStateGroup(step), "needsOwner"); @@ -172,7 +172,7 @@ test("recovery: a connector-defect code_fix verdict is a system issue with no re action({ audience: "maintainer", cta: "Connector code needs a fix", kind: "code_fix", terminal: true }), ], }), - health({ state: "degraded", detail_gap_backlog: backlog({ pending: 12 }) }) + health({ detail_gap_backlog: backlog({ pending: 12 }), state: "degraded" }) ); assert.equal(step, "system_issue"); assert.equal(recoveryStateGroup(step), "systemIssue"); @@ -181,8 +181,8 @@ test("recovery: a connector-defect code_fix verdict is a system issue with no re test("recovery: eligible work with a stale attempt floor beyond cadence is stalled, not queued", () => { const step = deriveRecoveryStep( verdict(), - health({ detail_gap_backlog: backlog({ pending: 2093, next_attempt_at: "2026-07-06T10:00:00Z" }) }), - { now: "2026-07-06T14:00:00Z", cadenceWindowMs: 60 * 60 * 1000 } + health({ detail_gap_backlog: backlog({ next_attempt_at: "2026-07-06T10:00:00Z", pending: 2093 }) }), + { cadenceWindowMs: 60 * 60 * 1000, now: "2026-07-06T14:00:00Z" } ); assert.equal(step, "stalled"); assert.equal(recoveryStateGroup(step), "systemIssue"); @@ -192,10 +192,10 @@ test("recovery: a future retry floor is a live cooldown, never a stall", () => { const step = deriveRecoveryStep( verdict(), health({ + detail_gap_backlog: backlog({ next_attempt_at: "2026-07-06T15:40:00Z", pending: 2093 }), state: "cooling_off", - detail_gap_backlog: backlog({ pending: 2093, next_attempt_at: "2026-07-06T15:40:00Z" }), }), - { now: "2026-07-06T14:00:00Z", cadenceWindowMs: 60 * 60 * 1000 } + { cadenceWindowMs: 60 * 60 * 1000, now: "2026-07-06T14:00:00Z" } ); assert.equal(step, "cooling"); }); @@ -223,10 +223,10 @@ test("panel: queued recovery carries progress floor counts and a next eligible a verdict(), health({ detail_gap_backlog: backlog({ + next_attempt_at: "2026-07-06T15:40:00Z", pending: 2093, pending_is_floor: true, recovered: 396, - next_attempt_at: "2026-07-06T15:40:00Z", }), }) ); @@ -241,8 +241,8 @@ test("panel: a cooldown blocker shows a wait/next-attempt line and no normal ret const model = buildRecoveryPanelViewModel( verdict(), health({ + detail_gap_backlog: backlog({ next_attempt_at: "2026-07-06T15:40:00Z", pending: 2093 }), state: "cooling_off", - detail_gap_backlog: backlog({ pending: 2093, next_attempt_at: "2026-07-06T15:40:00Z" }), }) ); assert.equal(model.step, "cooling"); @@ -270,8 +270,8 @@ const RAW_INTERNAL_LABEL_RE = /pending_is_floor|detail_gap|reason_code|upstream_ test("panel: eligible work with no attempt beyond the cadence window renders as a system condition, not passive progress", () => { const model = buildRecoveryPanelViewModel( verdict(), - health({ detail_gap_backlog: backlog({ pending: 2093, next_attempt_at: "2026-07-06T10:00:00Z" }) }), - { now: "2026-07-06T18:00:00Z", cadenceWindowMs: 6 * 60 * 60 * 1000 } + health({ detail_gap_backlog: backlog({ next_attempt_at: "2026-07-06T10:00:00Z", pending: 2093 }) }), + { cadenceWindowMs: 6 * 60 * 60 * 1000, now: "2026-07-06T18:00:00Z" } ); // A stale attempt floor beyond the cadence window is a stall — a system // condition surfaced with evidence, never an owner retry prompt. @@ -288,10 +288,10 @@ test("panel: a fresh attempt floor within the cadence window stays queued/coolin const model = buildRecoveryPanelViewModel( verdict(), health({ + detail_gap_backlog: backlog({ next_attempt_at: "2026-07-06T17:30:00Z", pending: 2093 }), state: "cooling_off", - detail_gap_backlog: backlog({ pending: 2093, next_attempt_at: "2026-07-06T17:30:00Z" }), }), - { now: "2026-07-06T18:00:00Z", cadenceWindowMs: 6 * 60 * 60 * 1000 } + { cadenceWindowMs: 6 * 60 * 60 * 1000, now: "2026-07-06T18:00:00Z" } ); assert.notEqual(model.step, "stalled"); }); @@ -309,15 +309,15 @@ test("panel: every recovery step exposes exactly one primary sentence, evidence buildRecoveryPanelViewModel( verdict(), health({ + detail_gap_backlog: backlog({ next_attempt_at: "2026-07-06T20:00:00Z", pending: 2093 }), state: "cooling_off", - detail_gap_backlog: backlog({ pending: 2093, next_attempt_at: "2026-07-06T20:00:00Z" }), }) ), // stalled buildRecoveryPanelViewModel( verdict(), - health({ detail_gap_backlog: backlog({ pending: 2093, next_attempt_at: "2026-07-06T10:00:00Z" }) }), - { now: "2026-07-06T18:00:00Z", cadenceWindowMs: 6 * 60 * 60 * 1000 } + health({ detail_gap_backlog: backlog({ next_attempt_at: "2026-07-06T10:00:00Z", pending: 2093 }) }), + { cadenceWindowMs: 6 * 60 * 60 * 1000, now: "2026-07-06T18:00:00Z" } ), ]; for (const model of cases) { diff --git a/apps/console/src/app/(console)/lib/source-recovery-state.ts b/apps/console/src/app/(console)/lib/source-recovery-state.ts index 831334c83..daac54208 100644 --- a/apps/console/src/app/(console)/lib/source-recovery-state.ts +++ b/apps/console/src/app/(console)/lib/source-recovery-state.ts @@ -149,7 +149,7 @@ function progressCount(value: number, isFloor: boolean | null | undefined): Reco if (count === 0) { return null; } - return { value: count, isFloor: Boolean(isFloor) }; + return { isFloor: Boolean(isFloor), value: count }; } /** @@ -197,6 +197,7 @@ function readEvidence( // action or a red terminal verdict with no owner path is a system issue. const systemIssue = primary?.kind === "code_fix" || + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic (verdict?.pill.tone === "red" && !ownerSatisfiable && verdict.channel !== "attention"); const backlogFloor = backlog?.next_attempt_at ?? null; const nextAttemptAt = backlogFloor ?? health?.next_attempt_at ?? null; @@ -207,8 +208,9 @@ function readEvidence( nextAttemptAt, ownerRequired, recoverableWork: hasRecoverableWork(backlog), - systemIssue: Boolean(systemIssue), + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic syncing: Boolean(health?.badges.syncing), + systemIssue: Boolean(systemIssue), }; } @@ -294,13 +296,13 @@ function isStalled( const STEP_GROUP: Record = { active: "working", - queued: "working", cooling: "working", eligible: "review", + none: null, owner_required: "needsOwner", - system_issue: "systemIssue", + queued: "working", stalled: "systemIssue", - none: null, + system_issue: "systemIssue", }; /** @@ -352,13 +354,13 @@ function progressEvidence(progress: RecoveryProgress): string[] { const PRIMARY_SENTENCE: Record = { active: "Syncing details now.", - queued: "Catching up details when it is safe to retry.", cooling: "Waiting until it is safe to retry details.", eligible: "Recoverable details are ready for another run.", + none: "No recovery work is queued.", owner_required: "Waiting on you before recovery can continue.", - system_issue: "This connector needs a fix before it can recover this.", + queued: "Catching up details when it is safe to retry.", stalled: "Recovery has stopped making progress and needs a look.", - none: "No recovery work is queued.", + system_issue: "This connector needs a fix before it can recover this.", }; /** @@ -382,12 +384,12 @@ export function buildRecoveryPanelViewModel( } return { - step, - primarySentence: PRIMARY_SENTENCE[step], - progress, - nextEligibleAt: step === "active" ? null : nextEligibleAt, blocker: blockerFor(step, nextEligibleAt), evidence, + nextEligibleAt: step === "active" ? null : nextEligibleAt, + primarySentence: PRIMARY_SENTENCE[step], + progress, + step, }; } diff --git a/apps/console/src/app/(console)/lib/stream-evidence-state.ts b/apps/console/src/app/(console)/lib/stream-evidence-state.ts index f7cda09a3..359ad6ef0 100644 --- a/apps/console/src/app/(console)/lib/stream-evidence-state.ts +++ b/apps/console/src/app/(console)/lib/stream-evidence-state.ts @@ -33,29 +33,29 @@ const COUNT_STATE_LABELS: Record< > = { known: (count) => ({ text: count === null ? "count unavailable" : `${count.toLocaleString()} records`, - tone: "neutral", title: "The canonical stable-records snapshot for this stream is current.", + tone: "neutral", }), known_zero: () => ({ text: "0 records", - tone: "neutral", title: "The canonical snapshot proves this stream has exactly zero retained records right now.", - }), - unobserved: () => ({ - text: "count not yet observed", tone: "neutral", - title: - "No observation has completed for this stream yet; a count will appear once the next read reconciles evidence.", }), stale: () => ({ text: "count unavailable", - tone: "warning", title: "The last known count is stale relative to the live canonical checkpoint.", + tone: "warning", }), unknown: () => ({ text: "count unavailable", - tone: "warning", title: "The count could not be determined; evidence collection failed or is otherwise unreliable.", + tone: "warning", + }), + unobserved: () => ({ + text: "count not yet observed", + title: + "No observation has completed for this stream yet; a count will appear once the next read reconciles evidence.", + tone: "neutral", }), }; @@ -73,8 +73,8 @@ export function streamCountLabel(record: { } return { text: record.record_count === null ? "count unavailable" : `${record.record_count.toLocaleString()} records`, - tone: "neutral", title: "", + tone: "neutral", }; } diff --git a/apps/console/src/app/(console)/lib/total-records.ts b/apps/console/src/app/(console)/lib/total-records.ts new file mode 100644 index 000000000..a557ffefc --- /dev/null +++ b/apps/console/src/app/(console)/lib/total-records.ts @@ -0,0 +1,24 @@ +// Copyright The PDP-Connect Contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { RefCountState } from "./ref-client.ts"; + +/** Shared predicate for whether a total_records value is an authoritative exact count. */ +export function isTotalRecordsAuthoritative(totalRecordsState?: RefCountState): boolean { + return totalRecordsState === undefined || totalRecordsState === "known" || totalRecordsState === "known_zero"; +} + +/** Format total_records without presenting stale or unavailable evidence as exact. */ +export function formatTotalRecordsLabel( + totalRecords: number, + totalRecordsState: RefCountState | undefined, + unit: string +): string { + if (totalRecordsState === "stale") { + return `${totalRecords.toLocaleString()} ${unit} (unverified)`; + } + if (totalRecordsState === "unobserved" || totalRecordsState === "unknown") { + return `${unit} unavailable`; + } + return `${totalRecords.toLocaleString()} ${unit}`; +} diff --git a/apps/console/src/app/(console)/lib/version-churn-summary.test.ts b/apps/console/src/app/(console)/lib/version-churn-summary.test.ts index 438048bfe..96e66c305 100644 --- a/apps/console/src/app/(console)/lib/version-churn-summary.test.ts +++ b/apps/console/src/app/(console)/lib/version-churn-summary.test.ts @@ -118,9 +118,9 @@ test("summarizeVersionChurn returns null for an empty row set", () => { test("summarizeVersionChurn does NOT say 'needs review' when every row is expected retained history", () => { const rows = [ - expectedRow({ connector_id: "github", stream: "user", risk_level: "high" }), - expectedRow({ connector_id: "slack", stream: "channels", risk_level: "high" }), - expectedRow({ connector_id: "ynab", stream: "accounts", risk_level: "watch" }), + expectedRow({ connector_id: "github", risk_level: "high", stream: "user" }), + expectedRow({ connector_id: "slack", risk_level: "high", stream: "channels" }), + expectedRow({ connector_id: "ynab", risk_level: "watch", stream: "accounts" }), ]; const summary = summarizeVersionChurn(rows); assert.ok(summary); @@ -134,9 +134,9 @@ test("summarizeVersionChurn does NOT say 'needs review' when every row is expect test("recurring snapshots fold into the 'expected retained history' headline bucket", () => { const rows = [ - recurringRow({ connector_id: "claude-code", stream: "sessions", risk_level: "watch" }), - recurringRow({ connector_id: "codex", stream: "sessions", risk_level: "watch" }), - expectedRow({ connector_id: "github", stream: "user", risk_level: "high" }), + recurringRow({ connector_id: "claude-code", risk_level: "watch", stream: "sessions" }), + recurringRow({ connector_id: "codex", risk_level: "watch", stream: "sessions" }), + expectedRow({ connector_id: "github", risk_level: "high", stream: "user" }), ]; const summary = summarizeVersionChurn(rows); assert.ok(summary); @@ -148,9 +148,9 @@ test("recurring snapshots fold into the 'expected retained history' headline buc test("summarizeVersionChurn does NOT say 'needs review' when every row is a registered compaction candidate", () => { const rows = [ - candidateRow({ connector_id: "ynab", stream: "budgets", risk_level: "high" }), - candidateRow({ connector_id: "gmail", stream: "labels", risk_level: "watch" }), - candidateRow({ connector_id: "amazon", stream: "orders", risk_level: "watch" }), + candidateRow({ connector_id: "ynab", risk_level: "high", stream: "budgets" }), + candidateRow({ connector_id: "gmail", risk_level: "watch", stream: "labels" }), + candidateRow({ connector_id: "amazon", risk_level: "watch", stream: "orders" }), ]; const summary = summarizeVersionChurn(rows); assert.ok(summary); @@ -162,8 +162,8 @@ test("summarizeVersionChurn does NOT say 'needs review' when every row is a regi test("summarizeVersionChurn SAYS 'needs review' when at least one row is unclassified", () => { const rows = [ - expectedRow({ connector_id: "github", stream: "user", risk_level: "high" }), - candidateRow({ connector_id: "ynab", stream: "budgets", risk_level: "high" }), + expectedRow({ connector_id: "github", risk_level: "high", stream: "user" }), + candidateRow({ connector_id: "ynab", risk_level: "high", stream: "budgets" }), unclassifiedRow({ risk_level: "high" }), ]; const summary = summarizeVersionChurn(rows); @@ -205,7 +205,7 @@ test("countChurnDispositions buckets rows by disposition", () => { candidateRow({ connector_id: "ynab", stream: "budgets" }), unclassifiedRow(), ]); - assert.deepEqual(counts, { needsReview: 1, compactionCandidates: 1, expectedRetained: 2, reviewedResidueCount: 0 }); + assert.deepEqual(counts, { compactionCandidates: 1, expectedRetained: 2, needsReview: 1, reviewedResidueCount: 0 }); }); test("countChurnDispositions folds recurring snapshots into expectedRetained", () => { @@ -214,7 +214,7 @@ test("countChurnDispositions folds recurring snapshots into expectedRetained", ( recurringRow({ connector_id: "codex", stream: "sessions" }), expectedRow({ connector_id: "github", stream: "user" }), ]); - assert.deepEqual(counts, { needsReview: 0, compactionCandidates: 0, expectedRetained: 3, reviewedResidueCount: 0 }); + assert.deepEqual(counts, { compactionCandidates: 0, expectedRetained: 3, needsReview: 0, reviewedResidueCount: 0 }); }); test("isExpectedRetainedHistory / needsReview predicates agree with the disposition", () => { @@ -230,7 +230,7 @@ test("isExpectedRetainedHistory / needsReview predicates agree with the disposit }); test("churnRowLabel falls back to the connector key when no display name is set", () => { - assert.equal(churnRowLabel(row({ display_name: null, connector_id: "ynab" })), "ynab / budgets"); + assert.equal(churnRowLabel(row({ connector_id: "ynab", display_name: null })), "ynab / budgets"); }); test("churnRowLabel prefers an owner-set display name", () => { @@ -240,7 +240,7 @@ test("churnRowLabel prefers an owner-set display name", () => { // ─── buildChurnDrilldownRows ───────────────────────────────────────────────── test("buildChurnDrilldownRows surfaces all supplied rows in order", () => { - const rows = [candidateRow({ stream: "budgets" }), candidateRow({ stream: "accounts", risk_level: "watch" })]; + const rows = [candidateRow({ stream: "budgets" }), candidateRow({ risk_level: "watch", stream: "accounts" })]; const built = buildChurnDrilldownRows(rows); assert.equal(built.length, 2); assert.equal(built[0]?.label, "ynab / budgets"); @@ -314,7 +314,7 @@ test("churnDryRunCommand shell-quotes metadata and omits absent connector id", ( test("buildChurnDrilldownRows omits the dry-run command for point-in-time rows and carries guidance", () => { const built = buildChurnDrilldownRows([ - expectedRow({ connector_id: "github", stream: "user", connector_instance_id: "cin_gh_1" }), + expectedRow({ connector_id: "github", connector_instance_id: "cin_gh_1", stream: "user" }), ]); assert.equal(built[0]?.remediation, "point_in_time_retained_history"); assert.equal(built[0]?.dryRunCommand, null, "point-in-time rows must not offer a (failing) compaction command"); @@ -327,7 +327,7 @@ test("buildChurnDrilldownRows omits the dry-run command for point-in-time rows a test("buildChurnDrilldownRows omits the dry-run command for recurring snapshots and carries guidance", () => { const built = buildChurnDrilldownRows([ - recurringRow({ connector_id: "claude-code", stream: "sessions", connector_instance_id: "cin_cc_1" }), + recurringRow({ connector_id: "claude-code", connector_instance_id: "cin_cc_1", stream: "sessions" }), ]); assert.equal(built[0]?.remediation, "recurring_point_in_time_snapshot"); assert.equal(built[0]?.dryRunCommand, null, "recurring snapshots are not compactable — no command"); @@ -352,7 +352,7 @@ test("buildChurnDrilldownRows keeps the dry-run command (as a diagnostic) for un test("buildChurnDrilldownRows keeps the dry-run command for reviewed residue rows", () => { const built = buildChurnDrilldownRows([ - reviewedRow({ connector_id: "usaa", stream: "accounts", connector_instance_id: "cin_usaa_1" }), + reviewedRow({ connector_id: "usaa", connector_instance_id: "cin_usaa_1", stream: "accounts" }), ]); assert.equal(built[0]?.remediation, "reviewed_historical_residue"); assert.ok(built[0]?.dryRunCommand, "reviewed residue rows keep the dry-run command (--apply frees disk)"); @@ -386,10 +386,10 @@ test("pointInTimeGuidance describes recurring snapshots as expected non-compacta test("summarizeVersionChurn names reviewed residue streams and says no review needed when all classified", () => { const rows = [ - reviewedRow({ connector_id: "usaa", stream: "accounts", risk_level: "watch" }), - reviewedRow({ connector_id: "usaa", stream: "statements", risk_level: "watch" }), - reviewedRow({ connector_id: "chase", stream: "statements", risk_level: "watch" }), - recurringRow({ connector_id: "claude-code", stream: "sessions", risk_level: "watch" }), + reviewedRow({ connector_id: "usaa", risk_level: "watch", stream: "accounts" }), + reviewedRow({ connector_id: "usaa", risk_level: "watch", stream: "statements" }), + reviewedRow({ connector_id: "chase", risk_level: "watch", stream: "statements" }), + recurringRow({ connector_id: "claude-code", risk_level: "watch", stream: "sessions" }), ]; const summary = summarizeVersionChurn(rows); assert.ok(summary); @@ -406,9 +406,9 @@ test("summarizeVersionChurn names reviewed residue streams and says no review ne test("summarizeVersionChurn includes reviewed residue count even alongside other dispositions", () => { const rows = [ - reviewedRow({ connector_id: "usaa", stream: "accounts", risk_level: "watch" }), - candidateRow({ connector_id: "ynab", stream: "budgets", risk_level: "high" }), - expectedRow({ connector_id: "github", stream: "user", risk_level: "high" }), + reviewedRow({ connector_id: "usaa", risk_level: "watch", stream: "accounts" }), + candidateRow({ connector_id: "ynab", risk_level: "high", stream: "budgets" }), + expectedRow({ connector_id: "github", risk_level: "high", stream: "user" }), ]; const summary = summarizeVersionChurn(rows); assert.ok(summary); diff --git a/apps/console/src/app/(console)/lib/version-churn-summary.ts b/apps/console/src/app/(console)/lib/version-churn-summary.ts index b0c67ecf6..42734a295 100644 --- a/apps/console/src/app/(console)/lib/version-churn-summary.ts +++ b/apps/console/src/app/(console)/lib/version-churn-summary.ts @@ -125,6 +125,7 @@ function normalizeConnectorId(connectorId: string | null): string | null { * row somehow arrives without the field (defensive; the contract requires it). */ export function classifyChurnRow(row: RefRecordVersionStatsRow): ChurnRemediation { + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic return row.version_disposition ?? "active_defect_or_unclassified"; } @@ -138,6 +139,7 @@ export function classifyChurnRow(row: RefRecordVersionStatsRow): ChurnRemediatio * contract requires it). */ export function remediationForRow(row: RefRecordVersionStatsRow): RefRecordVersionRemediation { + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic return row.version_remediation ?? "none"; } @@ -285,7 +287,7 @@ export function countChurnDispositions(rows: readonly RefRecordVersionStatsRow[] break; } } - return { needsReview: needsReviewCount, compactionCandidates, expectedRetained, reviewedResidueCount }; + return { compactionCandidates, expectedRetained, needsReview: needsReviewCount, reviewedResidueCount }; } function pluralStreams(n: number): string { @@ -322,7 +324,7 @@ export function summarizeVersionChurn(rows: readonly RefRecordVersionStatsRow[]) /** Per-disposition counts, for the view's section framing. */ dispositions: ChurnDispositionCounts; } | null { - const strongest = rows[0]; + const [strongest] = rows; if (!strongest) { return null; } @@ -355,10 +357,10 @@ export function summarizeVersionChurn(rows: readonly RefRecordVersionStatsRow[]) : `Version churn is classified — no review needed: ${breakdown}.`; return { + dispositions, headline, highestSignal: `Highest signal: ${churnRowLabel(strongest)} retains ${strongest.versions_per_record.toLocaleString()} versions per current record.`, needsReview: dispositions.needsReview > 0, - dispositions, }; } @@ -461,14 +463,7 @@ export function buildChurnDrilldownRows(rows: readonly RefRecordVersionStatsRow[ return { connectorId: row.connector_id, connectorInstanceId: row.connector_instance_id, - key: `${row.connector_instance_id}:${row.stream}`, - label: churnRowLabel(row), - risk: row.risk_level, - stream: row.stream, - remediation, - remediationAction: remediationForRow(row), - remediationChip: remediationChipLabel(row), - remediationGuidance: remediationGuidance(row), + current: countCell(row.current_record_count), // Non-compactable rows have no compaction policy the script resolves (it // exits 2), so they carry redesign/expected-history guidance instead of a // command. Compaction candidates, reviewed residue, and unclassified rows @@ -476,15 +471,22 @@ export function buildChurnDrilldownRows(rows: readonly RefRecordVersionStatsRow[ // plan, for reviewed residue `--apply` frees disk, and for an unclassified // row it is the safe first diagnostic (it prints whether a policy exists). dryRunCommand: notCompactable ? null : churnDryRunCommand(row), - pointInTimeGuidance: notCompactable ? pointInTimeGuidance(row) : null, - versionsPerRecord: { - label: row.versions_per_record.toLocaleString(undefined, { maximumFractionDigits: 2 }), - }, - current: countCell(row.current_record_count), history: countCell(row.record_history_count), + key: `${row.connector_instance_id}:${row.stream}`, keys: countCell(row.record_key_count), + label: churnRowLabel(row), lastHistoryAt: row.last_history_at, + pointInTimeGuidance: notCompactable ? pointInTimeGuidance(row) : null, reasons: row.risk_reasons.length > 0 ? row.risk_reasons.join("; ") : null, + remediation, + remediationAction: remediationForRow(row), + remediationChip: remediationChipLabel(row), + remediationGuidance: remediationGuidance(row), + risk: row.risk_level, + stream: row.stream, + versionsPerRecord: { + label: row.versions_per_record.toLocaleString(undefined, { maximumFractionDigits: 2 }), + }, }; }); } diff --git a/apps/console/src/app/(console)/page.tsx b/apps/console/src/app/(console)/page.tsx index 9238eb057..224b916bd 100644 --- a/apps/console/src/app/(console)/page.tsx +++ b/apps/console/src/app/(console)/page.tsx @@ -48,18 +48,18 @@ export const dynamic = "force-dynamic"; const SCHEME_RE = /^https?:\/\//; const HREFS: StandingHrefs = { - grants: dashboardRoutes.section.grants, - grantPackages: `${dashboardRoutes.section.grants}/packages`, - runs: dashboardRoutes.section.runs, - sources: dashboardRoutes.section.records, - traces: dashboardRoutes.section.traces, + connection: (connectorKey) => dashboardRoutes.connector(connectorKey), deployment: dashboardRoutes.section.deployment, deploymentTokens: dashboardRoutes.section.deploymentTokens, - notifications: dashboardRoutes.section.notifications, - connection: (connectorKey) => dashboardRoutes.connector(connectorKey), grant: (id) => dashboardRoutes.grant(id), + grantPackages: `${dashboardRoutes.section.grants}/packages`, + grants: dashboardRoutes.section.grants, + notifications: dashboardRoutes.section.notifications, run: (id) => dashboardRoutes.run(id), + runs: dashboardRoutes.section.runs, + sources: dashboardRoutes.section.records, trace: (id) => dashboardRoutes.trace(id), + traces: dashboardRoutes.section.traces, }; interface SafeRead { @@ -119,21 +119,21 @@ async function loadStandingInputs(): Promise { const connectors = connectorsRes.value.data; return { - now: new Date(), - hrefs: HREFS, - summary: summary.value, - grants: grantsRes.value.data, - grantPackageCount: packageCountRes.value.count, - traces: tracesRes.value.data, - failedTraces: [], + advisoryOwnerActions: advisoryOwnerActionsFromConnectors(connectors), + attentionConnections: attentionConnectionsFromConnectors(connectors), + bearerClients: clientsRes.value.data, failedRuns: [], + failedTraces: [], + grantPackageCount: packageCountRes.value.count, + grants: grantsRes.value.data, + hrefs: HREFS, + now: new Date(), overviewLoadIssues, pendingApprovals: pendingRes.value.data, - bearerClients: clientsRes.value.data, - sourceWork: sourceWorkFromConnectors(connectors), - advisoryOwnerActions: advisoryOwnerActionsFromConnectors(connectors), - attentionConnections: attentionConnectionsFromConnectors(connectors), sourceIssues: sourceIssueConnectionsFromConnectors(connectors), + sourceWork: sourceWorkFromConnectors(connectors), + summary: summary.value, + traces: tracesRes.value.data, }; } diff --git a/apps/console/src/app/(console)/schedules/actions.ts b/apps/console/src/app/(console)/schedules/actions.ts index fba0460cc..cdd38619a 100644 --- a/apps/console/src/app/(console)/schedules/actions.ts +++ b/apps/console/src/app/(console)/schedules/actions.ts @@ -29,9 +29,10 @@ export async function upsertScheduleAction( }; revalidatePath("/schedules"); revalidatePath("/sources"); + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic return { ok: true, policy_warning: body?.policy_warning ?? null }; } catch (err) { - return { ok: false, message: err instanceof Error ? err.message : String(err) }; + return { message: err instanceof Error ? err.message : String(err), ok: false }; } } @@ -45,7 +46,7 @@ export async function pauseScheduleAction( revalidatePath("/sources"); return { ok: true }; } catch (err) { - return { ok: false, message: err instanceof Error ? err.message : String(err) }; + return { message: err instanceof Error ? err.message : String(err), ok: false }; } } @@ -59,7 +60,7 @@ export async function resumeScheduleAction( revalidatePath("/sources"); return { ok: true }; } catch (err) { - return { ok: false, message: err instanceof Error ? err.message : String(err) }; + return { message: err instanceof Error ? err.message : String(err), ok: false }; } } @@ -73,6 +74,6 @@ export async function deleteScheduleAction( revalidatePath("/sources"); return { ok: true }; } catch (err) { - return { ok: false, message: err instanceof Error ? err.message : String(err) }; + return { message: err instanceof Error ? err.message : String(err), ok: false }; } } diff --git a/apps/console/src/app/(console)/schedules/page.tsx b/apps/console/src/app/(console)/schedules/page.tsx index 03b072ef9..b01ae62a1 100644 --- a/apps/console/src/app/(console)/schedules/page.tsx +++ b/apps/console/src/app/(console)/schedules/page.tsx @@ -29,13 +29,14 @@ export default async function SchedulesPage() { throw err; } - const hasActiveRun = summaries.some((s) => s.schedule?.active_run_id != null); + const hasActiveRun = summaries.some((s) => s.schedule?.active_run_id !== null); return ( ( { startTransition(async () => { const res = await upsertScheduleAction(summary.connector_id, { + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic connectionId: summary.connection_id ?? summary.connector_instance_id ?? null, + enabled: true, every, jitter: jitter || undefined, - enabled: true, }); if (!res.ok) { showToast("error", res.message); @@ -130,6 +132,7 @@ export function ScheduleRow({ summary, runsHref }: ScheduleRowProps) { startTransition(async () => { const res = await pauseScheduleAction( summary.connector_id, + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic summary.connection_id ?? summary.connector_instance_id ?? null ); if (!res.ok) { @@ -143,6 +146,7 @@ export function ScheduleRow({ summary, runsHref }: ScheduleRowProps) { startTransition(async () => { const res = await resumeScheduleAction( summary.connector_id, + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic summary.connection_id ?? summary.connector_instance_id ?? null ); if (!res.ok) { @@ -156,6 +160,7 @@ export function ScheduleRow({ summary, runsHref }: ScheduleRowProps) { startTransition(async () => { const res = await deleteScheduleAction( summary.connector_id, + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic summary.connection_id ?? summary.connector_instance_id ?? null ); if (!res.ok) { @@ -179,6 +184,7 @@ export function ScheduleRow({ summary, runsHref }: ScheduleRowProps) { const connectorKey = formatConnectorKeyForDisplay(summary.connector_id); const recordsHref = recordsHrefForSummary(summary); const activeRunId = schedule?.active_run_id; + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime value, TS type is optimistic const needsHuman = schedule?.human_attention_needed ?? false; const recInterval = recommendedIntervalLabel(policy); const recMode = policy?.recommended_mode; @@ -240,6 +246,7 @@ export function ScheduleRow({ summary, runsHref }: ScheduleRowProps) { )} { setEvery(formatIntervalForInput(schedule.interval_seconds)); setJitter(schedule.jitter_seconds ? formatIntervalForInput(schedule.jitter_seconds) : ""); @@ -256,6 +263,7 @@ export function ScheduleRow({ summary, runsHref }: ScheduleRowProps) { )} {showScheduleSetup && ( + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional setEditState("editing")} size="sm" variant="ghost"> Set schedule @@ -327,6 +335,7 @@ export function ScheduleRow({ summary, runsHref }: ScheduleRowProps) { every={every} isPending={isPending} jitter={jitter} + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onCancel={() => setEditState("idle")} onEveryChange={setEvery} onJitterChange={setJitter} @@ -412,6 +421,7 @@ function ScheduleEditor({ onEveryChange(e.target.value)} placeholder="e.g. 30m" type="text" @@ -422,6 +432,7 @@ function ScheduleEditor({ Jitter (optional) onJitterChange(e.target.value)} placeholder="e.g. 5m" type="text" diff --git a/apps/console/src/app/(console)/search/page.tsx b/apps/console/src/app/(console)/search/page.tsx index 50beeed36..e7fcf8180 100644 --- a/apps/console/src/app/(console)/search/page.tsx +++ b/apps/console/src/app/(console)/search/page.tsx @@ -57,9 +57,9 @@ async function loadSearchResult(query: string, jump: string | undefined): Promis return { result: { exact: spineResult.exact, - traces: spineResult.traces, grants: spineResult.grants, runs: spineResult.runs, + traces: spineResult.traces, }, unreachable: false, }; diff --git a/apps/console/src/app/(console)/sources/[connector]/[stream]/[recordKey]/page.tsx b/apps/console/src/app/(console)/sources/[connector]/[stream]/[recordKey]/page.tsx index 65428374c..1101c1115 100644 --- a/apps/console/src/app/(console)/sources/[connector]/[stream]/[recordKey]/page.tsx +++ b/apps/console/src/app/(console)/sources/[connector]/[stream]/[recordKey]/page.tsx @@ -106,8 +106,8 @@ export default async function RecordDetailPage({ connectorInstanceId, }).catch(() => null); return { - parentStream, expandCapabilities: Array.isArray(metadata?.expand_capabilities) ? metadata.expand_capabilities : [], + parentStream, }; }) ); @@ -209,7 +209,7 @@ export default async function RecordDetailPage({ ); const reverseChildListLinks = reverseChildListLinksFromManifest( connectorStreams, - { connectionId, parentStream: streamName, parentRecordKey: record.id }, + { connectionId, parentRecordKey: record.id, parentStream: streamName }, forwardChildListKeys ); // Child → parent back-links from two sources: @@ -228,9 +228,9 @@ export default async function RecordDetailPage({ // chase current_activity `amount: 3000` (declared `currency`) must read // `$30.00`, never the raw `3000`. const rendered = renderValue(3000, "currency"); - assert.deepEqual(rendered, { text: "$30.00", empty: false, money: true }); + assert.deepEqual(rendered, { empty: false, money: true, text: "$30.00" }); assert.ok(valueClassName(rendered).includes("tabular-nums")); }); @@ -38,12 +38,12 @@ test("an undeclared integer is NOT reinterpreted as cents", () => { // Without a declared currency type the value is shown verbatim; the detail // table never guesses a unit from magnitude. const rendered = renderValue(3000, undefined); - assert.deepEqual(rendered, { text: "3000", empty: false, money: false }); + assert.deepEqual(rendered, { empty: false, money: false, text: "3000" }); }); test("a plain string renders verbatim and is not marked empty or money", () => { const rendered = renderValue("posted", "text"); - assert.deepEqual(rendered, { text: "posted", empty: false, money: false }); + assert.deepEqual(rendered, { empty: false, money: false, text: "posted" }); assert.equal(valueClassName(rendered), "pdpp-caption break-words"); }); diff --git a/apps/console/src/app/(console)/sources/[connector]/[stream]/[recordKey]/record-fields-display.ts b/apps/console/src/app/(console)/sources/[connector]/[stream]/[recordKey]/record-fields-display.ts index a248c7f3f..5e4bfd76f 100644 --- a/apps/console/src/app/(console)/sources/[connector]/[stream]/[recordKey]/record-fields-display.ts +++ b/apps/console/src/app/(console)/sources/[connector]/[stream]/[recordKey]/record-fields-display.ts @@ -52,16 +52,16 @@ export interface RenderedValue { */ export function renderValue(value: unknown, declaredType: string | undefined): RenderedValue { if (value === null || value === undefined) { - return { text: value === null ? "null" : "—", empty: true, money: false }; + return { empty: true, money: false, text: value === null ? "null" : "—" }; } const amount = formatDeclaredAmount(value, declaredType); if (amount) { - return { text: amount.text, empty: false, money: true }; + return { empty: false, money: true, text: amount.text }; } if (typeof value === "string" && value.length === 0) { - return { text: "empty", empty: true, money: false }; + return { empty: true, money: false, text: "empty" }; } - return { text: stringifyValue(value), empty: false, money: false }; + return { empty: false, money: false, text: stringifyValue(value) }; } /** Tailwind classes for a value cell, tinting empties and aligning money. */ diff --git a/apps/console/src/app/(console)/sources/[connector]/[stream]/columns-menu.tsx b/apps/console/src/app/(console)/sources/[connector]/[stream]/columns-menu.tsx index fe629d26a..505522bb0 100644 --- a/apps/console/src/app/(console)/sources/[connector]/[stream]/columns-menu.tsx +++ b/apps/console/src/app/(console)/sources/[connector]/[stream]/columns-menu.tsx @@ -87,6 +87,7 @@ export function ColumnsMenu({
- + Reload Sources
diff --git a/apps/console/src/app/(console)/sources/last-known-read.test.ts b/apps/console/src/app/(console)/sources/last-known-read.test.ts index 576acc3f3..6149a7e50 100644 --- a/apps/console/src/app/(console)/sources/last-known-read.test.ts +++ b/apps/console/src/app/(console)/sources/last-known-read.test.ts @@ -26,12 +26,12 @@ function withFakeSessionStorage(run: (store: Map) => void): void const fakeWindow = { sessionStorage: { getItem: (k: string) => (store.has(k) ? (store.get(k) as string) : null), - setItem: (k: string, v: string) => { - store.set(k, v); - }, removeItem: (k: string) => { store.delete(k); }, + setItem: (k: string, v: string) => { + store.set(k, v); + }, }, }; const g = globalThis as unknown as { window?: unknown }; diff --git a/apps/console/src/app/(console)/sources/last-known-sync-start.test.ts b/apps/console/src/app/(console)/sources/last-known-sync-start.test.ts index b82d31d96..546a8f000 100644 --- a/apps/console/src/app/(console)/sources/last-known-sync-start.test.ts +++ b/apps/console/src/app/(console)/sources/last-known-sync-start.test.ts @@ -20,12 +20,12 @@ function withFakeSessionStorage(run: (store: Map) => void): void const fakeWindow = { sessionStorage: { getItem: (k: string) => (store.has(k) ? (store.get(k) as string) : null), - setItem: (k: string, v: string) => { - store.set(k, v); - }, removeItem: (k: string) => { store.delete(k); }, + setItem: (k: string, v: string) => { + store.set(k, v); + }, }, }; const g = globalThis as unknown as { window?: unknown }; @@ -74,7 +74,7 @@ test("sync-start marker round-trips while unexpired", () => { test("expired or corrupt sync-start markers are cleared", () => { withFakeSessionStorage((store) => { const key = syncStartToastKey("source-a"); - store.set(key, JSON.stringify({ message: "Sync started.", tone: "info", expiresAt: 1 })); + store.set(key, JSON.stringify({ expiresAt: 1, message: "Sync started.", tone: "info" })); assert.equal(readSyncStartToast("source-a"), null); store.set(key, "not-json"); assert.equal(readSyncStartToast("source-a"), null); diff --git a/apps/console/src/app/(console)/sources/last-known-sync-start.ts b/apps/console/src/app/(console)/sources/last-known-sync-start.ts index 87a44d238..993a0077e 100644 --- a/apps/console/src/app/(console)/sources/last-known-sync-start.ts +++ b/apps/console/src/app/(console)/sources/last-known-sync-start.ts @@ -68,10 +68,7 @@ export function markSyncStartToast( } } -export function syncStartToastDismissDelayMs( - toast: { readonly expiresAt?: number }, - now: number = Date.now() -): number { +export function syncStartToastDismissDelayMs(toast: { readonly expiresAt?: number }, now: number = Date.now()): number { if (typeof toast.expiresAt === "number" && Number.isFinite(toast.expiresAt)) { return Math.max(0, toast.expiresAt - now); } diff --git a/apps/console/src/app/(console)/sources/lib/relationships.test.ts b/apps/console/src/app/(console)/sources/lib/relationships.test.ts index a491c4cbd..d9faf33c5 100644 --- a/apps/console/src/app/(console)/sources/lib/relationships.test.ts +++ b/apps/console/src/app/(console)/sources/lib/relationships.test.ts @@ -24,20 +24,20 @@ import { } from "./relationships.ts"; const USER_STATS_CAP: ExpandCapability = { - name: "user_stats", - stream: "user_stats", - target_stream: "user_stats", cardinality: "has_many", child_parent_key_field: "user_id", foreign_key: "user_id", granted: true, + name: "user_stats", + stream: "user_stats", + target_stream: "user_stats", usable: true, }; function onlyLink(caps: ExpandCapability[], parentRecordKey: string) { const links = buildRelatedLinks(caps, { connectionId: "github", parentRecordKey }); assert.equal(links.length, 1); - const link = links[0]; + const [link] = links; assert.ok(link); return link; } @@ -53,9 +53,9 @@ test("has_many usable relation links to the filtered child list, not a child det }); test("has_many link percent-encodes connection, stream, field, and parent key", () => { - const cap: ExpandCapability = { ...USER_STATS_CAP, target_stream: "user stats", child_parent_key_field: "user id" }; + const cap: ExpandCapability = { ...USER_STATS_CAP, child_parent_key_field: "user id", target_stream: "user stats" }; const links = buildRelatedLinks([cap], { connectionId: "git hub", parentRecordKey: "1/0 1" }); - const link = links[0]; + const [link] = links; assert.ok(link); assert.equal(link.href, "/sources/git%20hub/user%20stats?filter[user%20id]=1%2F0%201"); }); @@ -64,8 +64,8 @@ test("unusable relation renders inert with the manifest reason as advisory", () const cap: ExpandCapability = { ...USER_STATS_CAP, granted: false, - usable: false, reason: "related_stream_not_granted", + usable: false, }; const link = onlyLink([cap], "101"); assert.equal(link.navigable, false); @@ -82,13 +82,13 @@ test("each declared reason maps to calm advisory copy", () => { test("usable has_one without a resolvable child key renders inert, never a parent-key child URL", () => { const cap: ExpandCapability = { - name: "profile", - target_stream: "profile", - stream: "profile", cardinality: "has_one", child_parent_key_field: "user_id", foreign_key: "user_id", granted: true, + name: "profile", + stream: "profile", + target_stream: "profile", usable: true, }; const link = onlyLink([cap], "101"); @@ -107,8 +107,8 @@ test("no expand capabilities yields no related links", () => { test("child field matching a declared relation links back to the parent record", () => { const link = findParentBackLink( "user_stats", - { id: "101:2026-04-01", user_id: "101", observed_on: "2026-04-01" }, - [{ parentStream: "user", capability: USER_STATS_CAP }], + { id: "101:2026-04-01", observed_on: "2026-04-01", user_id: "101" }, + [{ capability: USER_STATS_CAP, parentStream: "user" }], { connectionId: "github" } ); assert.ok(link); @@ -127,8 +127,8 @@ test("child back-link can be resolved for the requested relation field", () => { "user_stats", { id: "101:2026-04-01", owner_id: "owner-1", user_id: "user-1" }, [ - { parentStream: "user", capability: USER_STATS_CAP }, - { parentStream: "owners", capability: ownerStatsCap }, + { capability: USER_STATS_CAP, parentStream: "user" }, + { capability: ownerStatsCap, parentStream: "owners" }, ], { childParentKeyField: "owner_id", connectionId: "github" } ); @@ -144,7 +144,7 @@ test("child back-link is absent when no declared relation targets the child stre const link = findParentBackLink( "issues", { id: "i1", repository_id: "r1" }, - [{ parentStream: "user", capability: USER_STATS_CAP }], + [{ capability: USER_STATS_CAP, parentStream: "user" }], { connectionId: "github" } ); assert.equal(link, null); @@ -154,18 +154,18 @@ test("candidateParentStreamsForChild uses the manifest only to prune parent meta const streams = [ { name: "user", + query: { expand: [{ name: "user_stats" }] }, relationships: [ - { name: "user_stats", stream: "user_stats", foreign_key: "user_id", cardinality: "has_many" as const }, + { cardinality: "has_many" as const, foreign_key: "user_id", name: "user_stats", stream: "user_stats" }, ], - query: { expand: [{ name: "user_stats" }] }, }, { name: "repositories", + query: { expand: [] }, // Declared but NOT enabled in query.expand → must be ignored. relationships: [ - { name: "issues", stream: "issues", foreign_key: "repository_id", cardinality: "has_many" as const }, + { cardinality: "has_many" as const, foreign_key: "repository_id", name: "issues", stream: "issues" }, ], - query: { expand: [] }, }, { name: "user_stats" }, ]; @@ -189,7 +189,7 @@ test("manifest lookup tolerates URL-form connector_id and short connector_key", streams: [{ name: "user" }], }, ]; - const chaseManifest = manifests[0]; + const [chaseManifest] = manifests; assert.ok(chaseManifest); assert.equal(manifestMatchesConnectorId(chaseManifest, "chase"), true); @@ -217,7 +217,7 @@ test("short connection connector key resolves child-declared relationship manife const stream = connectorManifest?.streams.find((candidate) => candidate.name === "transactions"); const links = childHasOneBackLinksFromManifest( stream, - { id: "1212486749|2026042024323046109400600036029", account_id: "1212486749" }, + { account_id: "1212486749", id: "1212486749|2026042024323046109400600036029" }, { connectionId: "cin_029a67a16d8a252f6e3eb896" } ); @@ -227,22 +227,22 @@ test("short connection connector key resolves child-declared relationship manife test("parentRelationsForChild derives linkable relations from live expand_capabilities metadata", () => { const relations = parentRelationsForChild( [ - { parentStream: "user", expandCapabilities: [USER_STATS_CAP] }, + { expandCapabilities: [USER_STATS_CAP], parentStream: "user" }, { - parentStream: "repositories", expandCapabilities: [ { - name: "issues", - stream: "issues", - target_stream: "issues", cardinality: "has_many", child_parent_key_field: "repository_id", foreign_key: "repository_id", granted: false, - usable: false, + name: "issues", reason: "related_stream_not_granted", + stream: "issues", + target_stream: "issues", + usable: false, }, ], + parentStream: "repositories", }, ], "user_stats" @@ -258,7 +258,7 @@ test("child back-link is absent when the child field value is missing or empty", const missing = findParentBackLink( "user_stats", { id: "101:2026-04-01", observed_on: "2026-04-01" }, - [{ parentStream: "user", capability: USER_STATS_CAP }], + [{ capability: USER_STATS_CAP, parentStream: "user" }], { connectionId: "github" } ); assert.equal(missing, null); @@ -266,7 +266,7 @@ test("child back-link is absent when the child field value is missing or empty", const empty = findParentBackLink( "user_stats", { id: "x", user_id: "" }, - [{ parentStream: "user", capability: USER_STATS_CAP }], + [{ capability: USER_STATS_CAP, parentStream: "user" }], { connectionId: "github" } ); assert.equal(empty, null); @@ -276,17 +276,17 @@ test("child back-link is absent when the child field value is missing or empty", const CHASE_TRANSACTIONS_MANIFEST_STREAM = { name: "transactions", - relationships: [{ name: "account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }], + relationships: [{ cardinality: "has_one", foreign_key: "account_id", name: "account", stream: "accounts" }], }; test("child-declared has_one links to the parent record detail page", () => { const links = childHasOneBackLinksFromManifest( CHASE_TRANSACTIONS_MANIFEST_STREAM, - { id: "1212486749|2026042024323046109400600036029", account_id: "1212486749", amount: -1234 }, + { account_id: "1212486749", amount: -1234, id: "1212486749|2026042024323046109400600036029" }, { connectionId: "cin_029a67a16d8a252f6e3eb896" } ); assert.equal(links.length, 1); - const link = links[0]; + const [link] = links; assert.ok(link); assert.equal(link.parentStream, "accounts"); assert.equal(link.childParentKeyField, "account_id"); @@ -297,12 +297,12 @@ test("child-declared has_one percent-encodes connection, stream, and key value", const links = childHasOneBackLinksFromManifest( { name: "items", - relationships: [{ name: "order", stream: "open orders", foreign_key: "order id", cardinality: "has_one" }], + relationships: [{ cardinality: "has_one", foreign_key: "order id", name: "order", stream: "open orders" }], }, { "order id": "ref/42" }, { connectionId: "my conn" } ); - const link = links[0]; + const [link] = links; assert.ok(link); assert.equal(link.href, "/sources/my%20conn/open%20orders/ref%2F42"); }); @@ -311,7 +311,7 @@ test("child-declared has_many relationships are ignored by childHasOneBackLinksF const links = childHasOneBackLinksFromManifest( { name: "transactions", - relationships: [{ name: "tags", stream: "tags", foreign_key: "transaction_id", cardinality: "has_many" }], + relationships: [{ cardinality: "has_many", foreign_key: "transaction_id", name: "tags", stream: "tags" }], }, { id: "tx1", transaction_id: "tx1" }, { connectionId: "conn" } @@ -323,7 +323,7 @@ test("unrelated id-looking fields do not link when not covered by a declared has // account_id is NOT declared in this stream's relationships — must not produce a link. const links = childHasOneBackLinksFromManifest( { name: "transactions", relationships: [] }, - { id: "tx1", account_id: "1212486749" }, + { account_id: "1212486749", id: "tx1" }, { connectionId: "conn" } ); assert.deepEqual(links, []); @@ -341,7 +341,7 @@ test("child-declared has_one yields no link when foreign_key field is absent fro test("child-declared has_one yields no link when foreign_key value is empty", () => { const links = childHasOneBackLinksFromManifest( CHASE_TRANSACTIONS_MANIFEST_STREAM, - { id: "tx1", account_id: "" }, + { account_id: "", id: "tx1" }, { connectionId: "cin" } ); assert.deepEqual(links, []); @@ -371,10 +371,10 @@ test("childHasOneLinkFields ignores has_many and incomplete relations, and toler const fields = childHasOneLinkFields({ name: "transactions", relationships: [ - { name: "tags", stream: "tags", foreign_key: "transaction_id", cardinality: "has_many" }, - { name: "account", stream: "accounts", cardinality: "has_one" }, // missing foreign_key - { name: "owner", foreign_key: "owner_id", cardinality: "has_one" }, // missing stream - { name: "category", stream: "categories", foreign_key: "category_id", cardinality: "has_one" }, + { cardinality: "has_many", foreign_key: "transaction_id", name: "tags", stream: "tags" }, + { cardinality: "has_one", name: "account", stream: "accounts" }, // missing foreign_key + { cardinality: "has_one", foreign_key: "owner_id", name: "owner" }, // missing stream + { cardinality: "has_one", foreign_key: "category_id", name: "category", stream: "categories" }, ], }); assert.deepEqual([...fields], ["category_id"]); @@ -385,7 +385,7 @@ test("childHasOneLinkFields ignores has_many and incomplete relations, and toler test("childHasOneBackLinkForField links a declared has_one cell to the parent record", () => { const link = childHasOneBackLinkForField( CHASE_TRANSACTIONS_MANIFEST_STREAM, - { id: "tx1", account_id: "1212486749", amount: -1234 }, + { account_id: "1212486749", amount: -1234, id: "tx1" }, "account_id", { connectionId: "cin_chase" } ); @@ -398,7 +398,7 @@ test("childHasOneBackLinkForField links a declared has_one cell to the parent re test("childHasOneBackLinkForField resolves each field of a two-edges-to-same-parent stream independently", () => { // YNAB transactions: account_id and transfer_account_id are different columns // and resolve to DIFFERENT account records — the list page links each cell. - const record = { id: "t1", account_id: "acc-A", transfer_account_id: "acc-B" }; + const record = { account_id: "acc-A", id: "t1", transfer_account_id: "acc-B" }; const a = childHasOneBackLinkForField(YNAB_TRANSACTIONS_TWO_ACCOUNT_EDGES, record, "account_id", { connectionId: "cin_ynab", }); @@ -414,7 +414,7 @@ test("childHasOneBackLinkForField percent-encodes connection, parent stream, and const link = childHasOneBackLinkForField( { name: "items", - relationships: [{ name: "order", stream: "open orders", foreign_key: "order id", cardinality: "has_one" }], + relationships: [{ cardinality: "has_one", foreign_key: "order id", name: "order", stream: "open orders" }], }, { "order id": "ref/42" }, "order id", @@ -433,7 +433,7 @@ test("childHasOneBackLinkForField returns null for an undeclared field or empty/ ); // Declared field but empty value. assert.equal( - childHasOneBackLinkForField(CHASE_TRANSACTIONS_MANIFEST_STREAM, { id: "tx1", account_id: "" }, "account_id", { + childHasOneBackLinkForField(CHASE_TRANSACTIONS_MANIFEST_STREAM, { account_id: "", id: "tx1" }, "account_id", { connectionId: "cin", }), null @@ -455,18 +455,18 @@ const CHASE_STREAMS = [ { name: "accounts" }, { name: "transactions", - relationships: [{ name: "account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }], + relationships: [{ cardinality: "has_one", foreign_key: "account_id", name: "account", stream: "accounts" }], }, ]; test("Chase accounts parent yields a transactions filtered-list link, never a detail URL", () => { const links = reverseChildListLinksFromManifest(CHASE_STREAMS, { connectionId: "cin_029a67a16d8a252f6e3eb896", - parentStream: "accounts", parentRecordKey: "1212486749", + parentStream: "accounts", }); assert.equal(links.length, 1); - const link = links[0]; + const [link] = links; assert.ok(link); assert.equal(link.childStream, "transactions"); assert.equal(link.foreignKey, "account_id"); @@ -479,8 +479,8 @@ test("Chase accounts parent yields a transactions filtered-list link, never a de test("reverse link is a filtered list URL with a filter[…] query, never a detail segment", () => { const [link] = reverseChildListLinksFromManifest(CHASE_STREAMS, { connectionId: "chase", - parentStream: "accounts", parentRecordKey: "acc1", + parentStream: "accounts", }); assert.ok(link); // The path part ends at the child stream; the parent key is only in the query. @@ -496,15 +496,15 @@ test("a child-declared has_many produces no reverse link", () => { name: "tags", // has_many back to the parent — must NOT yield a reverse link by this rule. relationships: [ - { name: "transaction", stream: "transactions", foreign_key: "transaction_id", cardinality: "has_many" }, + { cardinality: "has_many", foreign_key: "transaction_id", name: "transaction", stream: "transactions" }, ], }, ]; assert.deepEqual( reverseChildListLinksFromManifest(streams, { connectionId: "conn", - parentStream: "transactions", parentRecordKey: "tx1", + parentStream: "transactions", }), [] ); @@ -516,8 +516,8 @@ test("a parent stream not targeted by any child has_one produces no reverse link // field exists elsewhere. const links = reverseChildListLinksFromManifest(CHASE_STREAMS, { connectionId: "chase", - parentStream: "merchants", parentRecordKey: "m1", + parentStream: "merchants", }); assert.deepEqual(links, []); }); @@ -525,13 +525,13 @@ test("a parent stream not targeted by any child has_one produces no reverse link test("a child has_one without a foreign_key produces no reverse link", () => { const streams = [ { name: "accounts" }, - { name: "transactions", relationships: [{ name: "account", stream: "accounts", cardinality: "has_one" }] }, + { name: "transactions", relationships: [{ cardinality: "has_one", name: "account", stream: "accounts" }] }, ]; assert.deepEqual( reverseChildListLinksFromManifest(streams, { connectionId: "chase", - parentStream: "accounts", parentRecordKey: "a1", + parentStream: "accounts", }), [] ); @@ -542,13 +542,13 @@ test("reverse link percent-encodes connection, child stream, filter field, and p { name: "open orders" }, { name: "line items", - relationships: [{ name: "order", stream: "open orders", foreign_key: "order id", cardinality: "has_one" }], + relationships: [{ cardinality: "has_one", foreign_key: "order id", name: "order", stream: "open orders" }], }, ]; const [link] = reverseChildListLinksFromManifest(streams, { connectionId: "my conn", - parentStream: "open orders", parentRecordKey: "ref/42", + parentStream: "open orders", }); assert.ok(link); assert.equal(link.href, "/sources/my%20conn/line%20items?filter[order%20id]=ref%2F42"); @@ -568,8 +568,8 @@ test("reverse links resolve via findManifestForConnectorId for URL-form and shor assert.ok(manifest, `manifest should resolve for ${id}`); const links = reverseChildListLinksFromManifest(manifest.streams, { connectionId: "cin_live", - parentStream: "accounts", parentRecordKey: "a1", + parentStream: "accounts", }); assert.equal(links.length, 1); assert.equal(links[0]?.href, "/sources/cin_live/transactions?filter[account_id]=a1"); @@ -582,13 +582,13 @@ test("reverse link deduplicates against a forward has_many target on the same ch const forwardLinks = buildRelatedLinks( [ { - name: "transactions", - stream: "transactions", - target_stream: "transactions", cardinality: "has_many", child_parent_key_field: "account_id", foreign_key: "account_id", granted: true, + name: "transactions", + stream: "transactions", + target_stream: "transactions", usable: true, }, ], @@ -603,7 +603,7 @@ test("reverse link deduplicates against a forward has_many target on the same ch const reverse = reverseChildListLinksFromManifest( CHASE_STREAMS, - { connectionId: "chase", parentStream: "accounts", parentRecordKey: "a1" }, + { connectionId: "chase", parentRecordKey: "a1", parentStream: "accounts" }, forwardKeys ); // Forward already covers (transactions, account_id) → reverse suppresses it. @@ -614,7 +614,7 @@ test("reverse link is kept when a forward has_many targets a different child str const forwardKeys = new Set([reverseChildListDedupKey("other_stream", "account_id")]); const reverse = reverseChildListLinksFromManifest( CHASE_STREAMS, - { connectionId: "chase", parentStream: "accounts", parentRecordKey: "a1" }, + { connectionId: "chase", parentRecordKey: "a1", parentStream: "accounts" }, forwardKeys ); assert.equal(reverse.length, 1); @@ -627,15 +627,15 @@ test("reverse link self-deduplicates a child stream declaring the same has_one t { name: "transactions", relationships: [ - { name: "account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }, - { name: "owning_account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }, + { cardinality: "has_one", foreign_key: "account_id", name: "account", stream: "accounts" }, + { cardinality: "has_one", foreign_key: "account_id", name: "owning_account", stream: "accounts" }, ], }, ]; const links = reverseChildListLinksFromManifest(streams, { connectionId: "chase", - parentStream: "accounts", parentRecordKey: "a1", + parentStream: "accounts", }); assert.equal(links.length, 1); }); @@ -644,20 +644,20 @@ test("reverseChildListLinksFromManifest returns empty for missing streams or arg assert.deepEqual( reverseChildListLinksFromManifest(undefined, { connectionId: "c", - parentStream: "accounts", parentRecordKey: "a1", + parentStream: "accounts", }), [] ); assert.deepEqual( - reverseChildListLinksFromManifest(CHASE_STREAMS, { connectionId: "c", parentStream: "", parentRecordKey: "a1" }), + reverseChildListLinksFromManifest(CHASE_STREAMS, { connectionId: "c", parentRecordKey: "a1", parentStream: "" }), [] ); assert.deepEqual( reverseChildListLinksFromManifest(CHASE_STREAMS, { connectionId: "c", - parentStream: "accounts", parentRecordKey: "", + parentStream: "accounts", }), [] ); @@ -704,7 +704,7 @@ test("reverseChildListEdgesFromManifest ignores a child-declared has_many", () = { name: "tags", relationships: [ - { name: "transaction", stream: "transactions", foreign_key: "transaction_id", cardinality: "has_many" }, + { cardinality: "has_many", foreign_key: "transaction_id", name: "transaction", stream: "transactions" }, ], }, ]; @@ -717,8 +717,8 @@ test("reverseChildListEdgesFromManifest self-dedups a child declaring the same h { name: "transactions", relationships: [ - { name: "account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }, - { name: "owning_account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }, + { cardinality: "has_one", foreign_key: "account_id", name: "account", stream: "accounts" }, + { cardinality: "has_one", foreign_key: "account_id", name: "owning_account", stream: "accounts" }, ], }, ]; @@ -735,20 +735,20 @@ test("reverseChildListEdgesFromManifest lists multiple distinct child streams an { name: "accounts" }, { name: "balances", - relationships: [{ name: "account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }], + relationships: [{ cardinality: "has_one", foreign_key: "account_id", name: "account", stream: "accounts" }], }, { name: "statements", - relationships: [{ name: "account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }], + relationships: [{ cardinality: "has_one", foreign_key: "account_id", name: "account", stream: "accounts" }], }, { name: "transactions", - relationships: [{ name: "account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }], + relationships: [{ cardinality: "has_one", foreign_key: "account_id", name: "account", stream: "accounts" }], }, // A non-matching child (points at a different parent) must not appear. { name: "merchants", - relationships: [{ name: "category", stream: "categories", foreign_key: "category_id", cardinality: "has_one" }], + relationships: [{ cardinality: "has_one", foreign_key: "category_id", name: "category", stream: "categories" }], }, ]; assert.deepEqual(reverseChildListEdgesFromManifest(streams, "accounts"), [ @@ -770,8 +770,8 @@ test("a Chase accounts list renders a distinct filtered-transactions link per ro const perRow = rows.map((row) => reverseChildListLinksFromManifest(CHASE_STREAMS, { connectionId: "cin_live", - parentStream: "accounts", parentRecordKey: row.id, + parentStream: "accounts", }) ); assert.equal(perRow[0]?.length, 1); @@ -794,8 +794,8 @@ test("a list page for a childless stream yields no per-row reverse links", () => assert.equal(reverseChildListEdgesFromManifest(CHASE_STREAMS, "transactions").length, 0); const links = reverseChildListLinksFromManifest(CHASE_STREAMS, { connectionId: "cin_live", - parentStream: "transactions", parentRecordKey: "t1", + parentStream: "transactions", }); assert.deepEqual(links, []); }); @@ -810,15 +810,15 @@ test("a list page for a childless stream yields no per-row reverse links", () => const YNAB_TRANSACTIONS_TWO_ACCOUNT_EDGES = { name: "transactions", relationships: [ - { name: "account", stream: "accounts", foreign_key: "account_id", cardinality: "has_one" }, - { name: "transfer_account", stream: "accounts", foreign_key: "transfer_account_id", cardinality: "has_one" }, + { cardinality: "has_one", foreign_key: "account_id", name: "account", stream: "accounts" }, + { cardinality: "has_one", foreign_key: "transfer_account_id", name: "transfer_account", stream: "accounts" }, ], }; test("two child-declared has_one edges to the same parent stream via different fields both render", () => { const childLinks = childHasOneBackLinksFromManifest( YNAB_TRANSACTIONS_TWO_ACCOUNT_EDGES, - { id: "t1", account_id: "acc-A", transfer_account_id: "acc-B" }, + { account_id: "acc-A", id: "t1", transfer_account_id: "acc-B" }, { connectionId: "cin_ynab" } ); const merged = mergeParentBackLinks(null, childLinks); @@ -834,13 +834,13 @@ test("mergeParentBackLinks collapses the SAME edge discovered via both sources", // metadata source and child-declared source describe the same (accounts, // account_id) edge → one link, metadata-derived preferred. const metadata: ParentBackLink = { - parentStream: "accounts", childParentKeyField: "account_id", href: "/sources/cin/accounts/acc-A", + parentStream: "accounts", }; const childDeclared = childHasOneBackLinksFromManifest( CHASE_TRANSACTIONS_MANIFEST_STREAM, - { id: "t1", account_id: "acc-A" }, + { account_id: "acc-A", id: "t1" }, { connectionId: "cin" } ); const merged = mergeParentBackLinks(metadata, childDeclared); @@ -850,9 +850,9 @@ test("mergeParentBackLinks collapses the SAME edge discovered via both sources", test("mergeParentBackLinks keeps distinct parent streams and is order-stable", () => { const childLinks: ParentBackLink[] = [ - { parentStream: "accounts", childParentKeyField: "account_id", href: "/a" }, - { parentStream: "payees", childParentKeyField: "payee_id", href: "/p" }, - { parentStream: "categories", childParentKeyField: "category_id", href: "/c" }, + { childParentKeyField: "account_id", href: "/a", parentStream: "accounts" }, + { childParentKeyField: "payee_id", href: "/p", parentStream: "payees" }, + { childParentKeyField: "category_id", href: "/c", parentStream: "categories" }, ]; const merged = mergeParentBackLinks(null, childLinks); assert.deepEqual( diff --git a/apps/console/src/app/(console)/sources/lib/relationships.ts b/apps/console/src/app/(console)/sources/lib/relationships.ts index 2ba24a42b..e25aecb37 100644 --- a/apps/console/src/app/(console)/sources/lib/relationships.ts +++ b/apps/console/src/app/(console)/sources/lib/relationships.ts @@ -187,7 +187,7 @@ export function parentRelationsForChild( if (!childParentKeyField) { continue; } - out.push({ parentStream: parent.parentStream, capability }); + out.push({ capability, parentStream: parent.parentStream }); } } return out; @@ -234,11 +234,11 @@ export function buildRelatedLinks( const cardinality: "has_one" | "has_many" = cap.cardinality === "has_one" ? "has_one" : "has_many"; const link: RelatedLink = { - relation: cap.name, - targetStream, cardinality, childParentKeyField, navigable: false, + relation: cap.name, + targetStream, }; if (cap.usable !== true) { @@ -257,8 +257,8 @@ export function buildRelatedLinks( // Child list filtered by the parent's key. The parent key is NOT a child // record key, so this is the only correct target. link.href = filteredChildListHref({ - connectionId: args.connectionId, childStream: targetStream, + connectionId: args.connectionId, foreignKey: childParentKeyField, parentKey: args.parentRecordKey, }); @@ -309,8 +309,8 @@ export function childHasOneBackLinksFromManifest( } out.push({ childParentKeyField: rel.foreign_key, - parentStream: rel.stream, href: `${base}/${encodeURIComponent(rel.stream)}/${encodeURIComponent(value)}`, + parentStream: rel.stream, }); } return out; @@ -415,8 +415,8 @@ export function reverseChildListLinksFromManifest( childStream: childStream.name, foreignKey: rel.foreign_key, href: filteredChildListHref({ - connectionId: args.connectionId, childStream: childStream.name, + connectionId: args.connectionId, foreignKey: rel.foreign_key, parentKey: args.parentRecordKey, }), @@ -563,8 +563,8 @@ export function findParentBackLink( } return { childParentKeyField, - parentStream, href: `${base}/${encodeURIComponent(parentStream)}/${encodeURIComponent(value)}`, + parentStream, }; } return null; diff --git a/apps/console/src/app/(console)/sources/page.tsx b/apps/console/src/app/(console)/sources/page.tsx index c06943672..1cde9c38b 100644 --- a/apps/console/src/app/(console)/sources/page.tsx +++ b/apps/console/src/app/(console)/sources/page.tsx @@ -112,7 +112,7 @@ export default async function RecordsIndexPage({ // run) only selects the fast vs. idle cadence. Named `runningCount` to match // the records-poller mount invariant. const runningCount = summaries.filter( - (s) => s.last_run != null && isActiveConnectorRunSummaryStatus(s.last_run.status) + (s) => s.last_run !== null && isActiveConnectorRunSummaryStatus(s.last_run.status) ).length; return ( @@ -138,9 +138,9 @@ function SourcesHeader({ error, message, notice }: { error?: string; message?: s

diff --git a/apps/console/src/app/(console)/sources/sources-demo-data.ts b/apps/console/src/app/(console)/sources/sources-demo-data.ts index 8b108ae7a..ebdb4e158 100644 --- a/apps/console/src/app/(console)/sources/sources-demo-data.ts +++ b/apps/console/src/app/(console)/sources/sources-demo-data.ts @@ -37,13 +37,13 @@ function health( extra: Partial = {} ): RefConnectionHealthSnapshot { return { - state, - reason_code: null, - last_success_at: "2026-06-12T08:00:00Z", - next_attempt_at: null, - badges: { stale: false, syncing: false }, axes: EMPTY_AXES, + badges: { stale: false, syncing: false }, + last_success_at: "2026-06-12T08:00:00Z", next_action: null, + next_attempt_at: null, + reason_code: null, + state, unknown_reasons: [], ...extra, }; @@ -66,56 +66,53 @@ function summary( partial: Pick & Partial ): RefConnectorSummary { return { + connection_health: health("healthy"), // The TYPE label (distinct from the owner's per-instance display_name), so // the demo exercises the real "type · account" list line shape. connector_display_name: typeLabel(partial.connector_id), connector_instance_id: partial.connection_id, freshness: {}, - manifest_version: "1.0.0", last_run: { - run_id: "run_demo_0001", - status: "succeeded", - started_at: "2026-06-12T08:00:00Z", + event_count: 42, + failure_reason: null, finished_at: "2026-06-12T08:01:00Z", first_at: "2026-04-01T00:00:00Z", last_at: "2026-06-12T08:01:00Z", - event_count: 42, - failure_reason: null, - }, - last_successful_run: { run_id: "run_demo_0001", - status: "succeeded", started_at: "2026-06-12T08:00:00Z", + status: "succeeded", + }, + last_successful_run: { + event_count: 42, + failure_reason: null, finished_at: "2026-06-12T08:01:00Z", first_at: "2026-04-01T00:00:00Z", last_at: "2026-06-12T08:01:00Z", - event_count: 42, - failure_reason: null, + run_id: "run_demo_0001", + started_at: "2026-06-12T08:00:00Z", + status: "succeeded", }, - schedule: null, + manifest_version: "1.0.0", next_action: null, - connection_health: health("healthy"), - streams: ["messages", "threads"], + schedule: null, stream_count: 2, + streams: ["messages", "threads"], total_records: 1234, ...partial, }; } const GMAIL = summary({ - connector_id: "gmail", + connection_health: health("healthy"), connection_id: "conn_gmail_personal_01", + connector_id: "gmail", display_name: "Gmail (personal)", - streams: ["messages", "threads", "labels", "attachments"], - stream_count: 4, - total_records: 48_201, - connection_health: health("healthy"), schedule: { - object: "schedule", - connector_id: "gmail", - trigger_kind: "scheduled", + active_run_id: null, automation_mode: "unattended", automation_summary: "Runs automatically every day.", + connector_id: "gmail", + created_at: "2026-04-01T00:00:00Z", effective_mode: "automatic", enabled: true, human_attention_needed: false, @@ -129,72 +126,75 @@ const GMAIL = summary({ minimum_interval_warning: null, next_due_at: "2026-06-13T08:00:00Z", notification_posture: "none", + object: "schedule", policy_warning: null, recommended_policy: null, scheduler_backoff: null, - active_run_id: null, - created_at: "2026-04-01T00:00:00Z", + trigger_kind: "scheduled", updated_at: "2026-06-12T08:01:00Z", }, + stream_count: 4, + streams: ["messages", "threads", "labels", "attachments"], + total_records: 48_201, }); const CHATGPT = summary({ - connector_id: "chatgpt", - connection_id: "conn_chatgpt_01", - display_name: "ChatGPT", - streams: ["conversations", "current_activity"], - stream_count: 2, - total_records: 5108, connection_health: health("needs_attention", { - reason_code: "owner_refresh_due", next_action: { - source: "structured", - reason_code: "owner_refresh_due", - owner_action: "act_elsewhere", action_target: "open_detail", attention_id: "att_demo_01", expires_at: null, - response_contract: "none", notification_state: "sent", + owner_action: "act_elsewhere", + reason_code: "owner_refresh_due", + response_contract: "none", + source: "structured", }, + reason_code: "owner_refresh_due", }), + connection_id: "conn_chatgpt_01", + connector_id: "chatgpt", + display_name: "ChatGPT", + stream_count: 2, + streams: ["conversations", "current_activity"], + total_records: 5108, }); const CHASE = summary({ - connector_id: "chase", + connection_health: health("blocked", { reason_code: "reauthorize_required" }), connection_id: "conn_chase_01", + connector_id: "chase", display_name: "Chase", - streams: ["statements", "current_activity"], stream_count: 2, + streams: ["statements", "current_activity"], total_records: 902, - connection_health: health("blocked", { reason_code: "reauthorize_required" }), }); const SPOTIFY_REVOKED = summary({ - connector_id: "spotify", + connection_health: health("idle"), connection_id: "conn_spotify_01", + connector_id: "spotify", display_name: "Spotify", - streams: ["plays", "playlists"], + revoked_at: "2026-06-01T00:00:00Z", + status: "revoked", stream_count: 2, + streams: ["plays", "playlists"], total_records: 12_044, - status: "revoked", - revoked_at: "2026-06-01T00:00:00Z", - connection_health: health("idle"), }); const AMAZON = summary({ - connector_id: "amazon", + connection_health: health("degraded", { reason_code: "partial_coverage" }), connection_id: "conn_amazon_01", + connector_id: "amazon", display_name: "Amazon", - streams: ["orders", "returns", "addresses"], stream_count: 3, + streams: ["orders", "returns", "addresses"], total_records: 311, - connection_health: health("degraded", { reason_code: "partial_coverage" }), }); export function buildSourcesDemoSummaries(scenario: SourcesDemoScenario): RefConnectorSummary[] { if (scenario === "healthy") { - return [GMAIL, summary({ connector_id: "github", connection_id: "conn_github_01", display_name: "GitHub" })]; + return [GMAIL, summary({ connection_id: "conn_github_01", connector_id: "github", display_name: "GitHub" })]; } if (scenario === "attention") { return [CHATGPT, CHASE, AMAZON]; diff --git a/apps/console/src/app/(console)/sources/sources-view-model.test.ts b/apps/console/src/app/(console)/sources/sources-view-model.test.ts index e53916f5c..e30a36784 100644 --- a/apps/console/src/app/(console)/sources/sources-view-model.test.ts +++ b/apps/console/src/app/(console)/sources/sources-view-model.test.ts @@ -57,33 +57,33 @@ const EMPTY_AXES = { function health(state: RefConnectionHealthSnapshot["state"]): RefConnectionHealthSnapshot { return { - state, - reason_code: null, - last_success_at: null, - next_attempt_at: null, - badges: { stale: false, syncing: false }, axes: EMPTY_AXES, + badges: { stale: false, syncing: false }, + last_success_at: null, next_action: null, + next_attempt_at: null, + reason_code: null, + state, unknown_reasons: [], }; } function summary(partial: Partial = {}): RefConnectorSummary { return { - connector_id: "gmail", + connection_health: health("healthy"), connection_id: "conn_1", + connector_display_name: "Gmail", + connector_id: "gmail", connector_instance_id: "conn_1", display_name: "Gmail", - connector_display_name: "Gmail", freshness: {}, - manifest_version: "1.0.0", last_run: null, last_successful_run: null, - schedule: null, + manifest_version: "1.0.0", next_action: null, - connection_health: health("healthy"), - streams: ["messages", "threads"], + schedule: null, stream_count: 2, + streams: ["messages", "threads"], total_records: 100, ...partial, }; @@ -129,11 +129,11 @@ function manualUploadManifest(connectorId = "whatsapp") { connector_id: connectorId, connector_key: connectorId, setup: { - modality: "manual_or_upload", manual_or_upload: { - import_dir_env_var: "WHATSAPP_EXPORT_DIR", accepted_file_extensions: [".zip"], + import_dir_env_var: "WHATSAPP_EXPORT_DIR", }, + modality: "manual_or_upload", }, }; } @@ -176,13 +176,12 @@ test("toSourceInstanceView reads status from rendered_verdict when present", () test("toSourceInstanceView derives local-device modality from persisted source_kind, not heartbeat presence", () => { const localWithoutHeartbeat = toSourceInstanceView( summary({ - source_kind: "local_device", local_device_progress: null, + source_kind: "local_device", }) ); const remoteWithHeartbeatShapedProgress = toSourceInstanceView( summary({ - source_kind: "account", local_device_progress: { last_heartbeat_at: "2026-06-03T11:59:00.000Z", last_heartbeat_status: "healthy", @@ -190,6 +189,7 @@ test("toSourceInstanceView derives local-device modality from persisted source_k records_pending: 0, source_count: 1, }, + source_kind: "account", }) ); assert.equal(localWithoutHeartbeat.isLocalDevicePush, true); @@ -251,13 +251,13 @@ test("toSourceInstanceView reads owner CTA from rendered_verdict required action test("toSourceInstanceView surfaces owner-runnable advisory action cues for source rows", () => { const view = toSourceInstanceView( summary({ - connector_id: "reddit", connector_display_name: "Reddit", + connector_id: "reddit", display_name: "Reddit", rendered_verdict: renderedVerdict({ channel: "advisory", - pill: { label: "Healthy", tone: "green" }, forward_statement: "Run a refresh when you want the latest saved posts.", + pill: { label: "Healthy", tone: "green" }, required_actions: [ { affects: [], @@ -365,8 +365,8 @@ test("toSourceInstanceView does not render maintainer or wait actions as owner C test("toSourceInstanceView renders calibrated live-journey verdict copy without inspection counts", () => { const chatgpt = toSourceInstanceView( summary({ - connector_id: "chatgpt", connector_display_name: "ChatGPT", + connector_id: "chatgpt", display_name: "ChatGPT", rendered_verdict: renderedVerdict({ annotations: [{ kind: "freshness", text: "Fresh today." }], @@ -389,8 +389,8 @@ test("toSourceInstanceView renders calibrated live-journey verdict copy without const amazon = toSourceInstanceView( summary({ - connector_id: "amazon", connector_display_name: "Amazon", + connector_id: "amazon", display_name: "Amazon", rendered_verdict: renderedVerdict({ annotations: [{ kind: "freshness", text: "Last refreshed 31 days ago." }], @@ -415,8 +415,8 @@ test("toSourceInstanceView renders calibrated live-journey verdict copy without const chase = toSourceInstanceView( summary({ - connector_id: "chase", connector_display_name: "Chase", + connector_id: "chase", display_name: "Chase", rendered_verdict: renderedVerdict({ annotations: [{ kind: "freshness", text: "Transactions stuck since Apr 22." }], @@ -507,8 +507,8 @@ test("toSourceInstanceView uses the same stream count for config and stream tabl const view = toSourceInstanceView( summary({ stream_count: 5, + stream_records: [{ last_updated: "2026-07-01T17:58:46.531Z", record_count: 133_848, stream: "messages" }], streams: ["conversations", "messages", "memories", "custom_gpts", "custom_instructions", "shared_conversations"], - stream_records: [{ stream: "messages", record_count: 133_848, last_updated: "2026-07-01T17:58:46.531Z" }], }) ); @@ -519,21 +519,21 @@ test("toSourceInstanceView uses the same stream count for config and stream tabl test("buildSourcesRuntimeAdvisory renders one global runtime fault and ignores healthy runtime", () => { assert.equal( buildSourcesRuntimeAdvisory({ + label: "Collection runtime ready", + message: null, object: "ref_runtime_status", ok: true, reason: null, - label: "Collection runtime ready", - message: null, }), null ); assert.deepEqual( buildSourcesRuntimeAdvisory({ + label: "Collection runtime unavailable", + message: null, object: "ref_runtime_status", ok: false, reason: "controller_unavailable", - label: "Collection runtime unavailable", - message: null, }), { headline: "Collection runtime unavailable", @@ -545,11 +545,11 @@ test("buildSourcesRuntimeAdvisory renders one global runtime fault and ignores h test("formatSchedule is honest about no schedule, paused, and policy-ineligible", () => { assert.equal(formatSchedule(null), "manual — no schedule"); const base: RefSchedule = { - object: "schedule", - connector_id: "gmail", - trigger_kind: "scheduled", + active_run_id: null, automation_mode: "unattended", automation_summary: "", + connector_id: "gmail", + created_at: "", effective_mode: "automatic", enabled: true, human_attention_needed: false, @@ -563,10 +563,10 @@ test("formatSchedule is honest about no schedule, paused, and policy-ineligible" minimum_interval_warning: null, next_due_at: null, notification_posture: "none", + object: "schedule", recommended_policy: null, scheduler_backoff: null, - active_run_id: null, - created_at: "", + trigger_kind: "scheduled", updated_at: "", }; assert.equal(formatSchedule(base), "every 1d · automatic"); @@ -626,23 +626,23 @@ test("toSourceInstanceView surfaces server-owned collection report facts per str test("toSourceInstanceView surfaces retained stream counts without conflating them with latest collection", () => { const view = toSourceInstanceView( summary({ - stream_records: [ - { stream: "messages", record_count: 42, last_updated: "2026-06-17T11:00:00.000Z" }, - { stream: "archived", record_count: 3, last_updated: "2026-06-16T11:00:00.000Z" }, - ], collection_report: [ { - stream: "messages", + checkpoint: "committed", collected: 8, considered: 10, - checkpoint: "committed", + coverage_condition: "retryable_gap", covered: 8, + forward_disposition: "resumable", pending_detail_gaps: 0, skipped: null, - coverage_condition: "retryable_gap", - forward_disposition: "resumable", + stream: "messages", }, ], + stream_records: [ + { last_updated: "2026-06-17T11:00:00.000Z", record_count: 42, stream: "messages" }, + { last_updated: "2026-06-16T11:00:00.000Z", record_count: 3, stream: "archived" }, + ], }) ); @@ -693,7 +693,7 @@ test("toSourceInstanceView drops blank stream names", () => { }); test("toSourceInstanceView surfaces a revoked instance with a struck status", () => { - const view = toSourceInstanceView(summary({ status: "revoked", revoked_at: "2026-06-01T00:00:00Z" })); + const view = toSourceInstanceView(summary({ revoked_at: "2026-06-01T00:00:00Z", status: "revoked" })); assert.equal(view.revoked, true); assert.equal(view.status.kind, "revoked"); }); @@ -701,8 +701,8 @@ test("toSourceInstanceView surfaces a revoked instance with a struck status", () test("toSourceInstanceView omits passport identity rows that duplicate the source title", () => { const view = toSourceInstanceView( summary({ - connector_id: "amazon", connector_display_name: "Amazon", + connector_id: "amazon", display_name: "Amazon - Personal", }) ); @@ -727,15 +727,15 @@ test("toSourceInstanceView omits passport identity rows that duplicate the sourc test("toSourceInstanceView keeps connector type when the source title does not identify it", () => { const view = toSourceInstanceView( summary({ - connector_id: "amazon", connector_display_name: "Amazon", + connector_id: "amazon", display_name: "Personal", }) ); assert.equal(view.displayName, "Personal"); assert.equal(view.listKind, "Amazon"); - assert.deepEqual(view.passportFields[0], { k: "type", value: "Amazon", mono: false }); + assert.deepEqual(view.passportFields[0], { k: "type", mono: false, value: "Amazon" }); }); test("toSourceInstanceView links the detail page, never a raw action target", () => { @@ -793,7 +793,7 @@ test("toSourceInstanceView: a revoked-and-draft row (should not arise in practic // the href does NOT independently re-check revoked. On real data these two // signals never disagree; if that guarantee ever breaks, this test is the // one that will start failing instead of silently drifting. - const view = toSourceInstanceView(draftSummary({ status: "revoked", revoked_at: "2026-07-10T00:00:00Z" })); + const view = toSourceInstanceView(draftSummary({ revoked_at: "2026-07-10T00:00:00Z", status: "revoked" })); assert.equal(view.revoked, true); assert.equal(view.status.kind, "revoked"); assert.equal(view.detailHref, "/connect/status/conn_1"); @@ -802,22 +802,22 @@ test("toSourceInstanceView: a revoked-and-draft row (should not arise in practic test("toSourcesView disambiguates duplicate unnamed connections without exposing ids", () => { const views = toSourcesView([ summary({ - connector_id: "amazon", + connection_id: "cin_a", connector_display_name: "Amazon", + connector_id: "amazon", display_name: "Amazon", - connection_id: "cin_a", }), summary({ - connector_id: "amazon", + connection_id: "cin_b", connector_display_name: "Amazon", + connector_id: "amazon", display_name: "Amazon", - connection_id: "cin_b", }), summary({ - connector_id: "amazon", + connection_id: "cin_named", connector_display_name: "Amazon", + connector_id: "amazon", display_name: "Amazon - Personal", - connection_id: "cin_named", }), ]); @@ -832,23 +832,23 @@ test("toSourcesView disambiguates duplicate unnamed connections without exposing test("duplicate source review flags same-type unnamed active sources without hiding them", () => { const views = toSourcesView([ summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_named", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_named", display_name: "Amazon - Personal", }), summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_a", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_a", display_name: "Amazon", }), summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_b", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_b", display_name: "Amazon", }), @@ -882,16 +882,16 @@ test("duplicate source review flags same-type unnamed active sources without hid test("duplicate source review ignores revoked fallback sources", () => { const views = toSourcesView([ summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_active", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_active", display_name: "Amazon", }), summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_revoked", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_revoked", display_name: "Amazon", revoked_at: "2026-06-17T00:00:00Z", @@ -905,37 +905,37 @@ test("duplicate source review ignores revoked fallback sources", () => { test("duplicate fallback collapse keeps named sources visible and groups 3+ unnamed active sources", () => { const views = toSourcesView([ summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_named", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_named", display_name: "Amazon - Personal", }), summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_a", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_a", display_name: "Amazon", }), summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_b", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_b", display_name: "Amazon", }), summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_c", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_c", display_name: "Amazon", }), summary({ - connector_id: "gmail", - connector_display_name: "Gmail", connection_id: "cin_gmail", + connector_display_name: "Gmail", + connector_id: "gmail", connector_instance_id: "cin_gmail", display_name: "Gmail", }), @@ -958,16 +958,16 @@ test("duplicate fallback collapse keeps named sources visible and groups 3+ unna test("duplicate fallback collapse leaves small duplicate sets visible", () => { const views = toSourcesView([ summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_a", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_a", display_name: "Amazon", }), summary({ - connector_id: "amazon", - connector_display_name: "Amazon", connection_id: "cin_b", + connector_display_name: "Amazon", + connector_id: "amazon", connector_instance_id: "cin_b", display_name: "Amazon", }), @@ -982,7 +982,7 @@ test("duplicate fallback collapse leaves small duplicate sets visible", () => { }); test("manual/upload sources link to importing another file into the same source", () => { - const view = toSourceInstanceView(summary({ connector_id: "whatsapp", connection_id: "cin_whatsapp_1" }), { + const view = toSourceInstanceView(summary({ connection_id: "cin_whatsapp_1", connector_id: "whatsapp" }), { manifests: [manualUploadManifest()], }); assert.equal(view.manualUploadHref, "/connect/manual-upload/whatsapp?connection_id=cin_whatsapp_1"); @@ -993,12 +993,12 @@ test("manual/upload sources link to importing another file into the same source" }); test("manual/upload source href is absent when the connector has no packaged import binding", () => { - const href = manualUploadHrefForSource(summary({ connector_id: "whatsapp", connection_id: "cin_whatsapp_1" }), [ + const href = manualUploadHrefForSource(summary({ connection_id: "cin_whatsapp_1", connector_id: "whatsapp" }), [ { connector_id: "whatsapp", setup: { - modality: "manual_or_upload", manual_or_upload: { accepted_file_extensions: [".zip"] }, + modality: "manual_or_upload", }, }, ]); diff --git a/apps/console/src/app/(console)/sources/sources-view-model.ts b/apps/console/src/app/(console)/sources/sources-view-model.ts index 6346a9484..11b474de4 100644 --- a/apps/console/src/app/(console)/sources/sources-view-model.ts +++ b/apps/console/src/app/(console)/sources/sources-view-model.ts @@ -51,6 +51,7 @@ import { type SourcePrimaryVerdictAction, type SourceStatusFlag, } from "../lib/source-actionability.ts"; +import { formatTotalRecordsLabel, isTotalRecordsAuthoritative } from "../lib/total-records.ts"; import { summarizeVersionChurn } from "../lib/version-churn-summary.ts"; /** @@ -230,9 +231,7 @@ function formatInterval(seconds: number): string { * this field) is treated as authoritative, preserving the exact prior * always-numeric rendering for every existing caller. */ -export function isTotalRecordsAuthoritative(totalRecordsState?: RefCountState): boolean { - return totalRecordsState === undefined || totalRecordsState === "known" || totalRecordsState === "known_zero"; -} +// Shared by all count renderers via ../lib/total-records.ts. /** * Centralized state-aware label for a `total_records` count value, shared @@ -248,19 +247,6 @@ export function isTotalRecordsAuthoritative(totalRecordsState?: RefCountState): * - `"known"`/`"known_zero"`/omitted: the exact prior always-numeric * rendering. */ -export function formatTotalRecordsLabel( - totalRecords: number, - totalRecordsState: RefCountState | undefined, - unit: string -): string { - if (totalRecordsState === "stale") { - return `${totalRecords.toLocaleString()} ${unit} (unverified)`; - } - if (totalRecordsState === "unobserved" || totalRecordsState === "unknown") { - return `${unit} unavailable`; - } - return `${totalRecords.toLocaleString()} ${unit}`; -} /** * Connector detail-page header count. A failed/never-observed record @@ -400,6 +386,7 @@ export function manualUploadHrefForSource( summary: Pick, manifests: readonly SourceManifestLike[] | undefined ): string | null { + // biome-ignore lint/suspicious/noUnnecessaryConditions: older API responses can omit either runtime identifier despite optimistic generated types. const connectionId = summary.connection_id ?? summary.connector_instance_id ?? null; if (!(connectionId && manifests)) { return null; @@ -488,15 +475,17 @@ export function toSourceInstanceView( options: { fallbackDisambiguator?: string | null; manifests?: readonly SourceManifestLike[] } = {} ): SourceInstanceView { const connectorId = summary.connector_id; + // biome-ignore lint/suspicious/noUnnecessaryConditions: connection identifiers can be absent in transitional API responses. const connectionId = summary.connection_id ?? null; const connectorInstanceId = summary.connector_instance_id ?? null; const actionability = projectSourceActionability(summary); + // biome-ignore lint/suspicious/noUnnecessaryConditions: runtime identifier fallbacks preserve navigation for older API responses. const routeId = connectionId ?? connectorInstanceId ?? actionability.routeId; const revoked = isRevokedConnector(summary); // Modality is persisted server authority. A missing heartbeat must not // resurrect remote Sync controls for a local-device connection. const isLocalDevicePush = summary.source_kind === "local_device"; - const isRunning = summary.last_run != null && isActiveConnectorRunSummaryStatus(summary.last_run.status); + const isRunning = summary.last_run !== null && isActiveConnectorRunSummaryStatus(summary.last_run.status); const manualUploadHref = manualUploadHrefForSource(summary, options.manifests); const collectionFactsByStream = new Map( [...indexCollectionReportByStream(summary.collection_report)].map(([stream, entry]) => [ @@ -533,22 +522,15 @@ export function toSourceInstanceView( } else { accountLine = formatSourceListFacts(summary, sourceStreamNames.length); } - const primaryVerdictAction = actionability.primaryVerdictAction; + const { primaryVerdictAction } = actionability; const nextAction = primaryVerdictAction?.ownerRunnable ? null : actionability.nextAction; - const ownerActionCue = actionability.ownerActionCue; + const { ownerActionCue } = actionability; const status = actionability.renderedStatus; const streams: SourceStreamManifestRow[] = sourceStreamNames.map((name) => { const facts = collectionFactsByStream.get(name) ?? null; const retained = streamRecordsByStream.get(name) ?? null; return { - name, - recordCount: retained ? retained.record_count : null, - // The index summary exposes no cursor or searchable flag per stream; - // render them as unknown rather than guessing. Collection-report facts - // are server-owned and safe to show here without another read. - cursor: null, - searchable: null, collection: facts ? { countsLabel: facts.countsLabel, @@ -564,18 +546,26 @@ export function toSourceInstanceView( tone: facts.tone, } : null, + // The index summary exposes no cursor or searchable flag per stream; + // render them as unknown rather than guessing. Collection-report facts + // are server-owned and safe to show here without another read. + cursor: null, exploreHref: exploreHrefFor(routeId, name), + name, + recordCount: retained ? retained.record_count : null, + searchable: null, }; }); const passportFields: SourcePassportField[] = [ - ...(listKind ? [{ k: "type", value: kind, mono: false } satisfies SourcePassportField] : []), - { k: "config", value: `${sourceStreamNames.length} streams`, mono: true }, + ...(listKind ? [{ k: "type", mono: false, value: kind } satisfies SourcePassportField] : []), + { k: "config", mono: true, value: `${sourceStreamNames.length} streams` }, { k: "auth", value: deriveAuthLine(primaryVerdictAction, isLocalDevicePush, manualUploadHref) }, - { k: "schedule", value: formatSchedule(summary.schedule), mono: true }, - { k: "last run", value: formatLastRun(summary.last_run), mono: true }, + { k: "schedule", mono: true, value: formatSchedule(summary.schedule) }, + { k: "last run", mono: true, value: formatLastRun(summary.last_run) }, { k: "records", + mono: true, // Sol fourth-verdict P1.3: the passport independently rendered the // raw number, bypassing `total_records_state` entirely — the second // of the two concrete authoritative-zero-rendering sites the verdict @@ -583,34 +573,34 @@ export function toSourceInstanceView( value: isTotalRecordsAuthoritative(summary.total_records_state) ? summary.total_records.toLocaleString() : formatTotalRecordsLabel(summary.total_records, summary.total_records_state, "records"), - mono: true, }, - { k: "added", value: summary.last_successful_run?.first_at ?? null, mono: true }, + // biome-ignore lint/suspicious/noUnnecessaryConditions: historical run payloads can omit the timestamp despite optimistic generated types. + { k: "added", mono: true, value: summary.last_successful_run?.first_at ?? null }, ]; return { - id: routeId, - connectorId, + accountLine, connectionId, + connectorId, connectorInstanceId, detailHref: sourceDetailHrefFor(routeId, summary), displayName, - kind, - listKind, - accountLine, - revoked, + id: routeId, isLocalDevicePush, isRunning, + kind, + listKind, manualUploadHref, needsOwnerLabel: hasFallbackLabel, - status, nextAction, ownerActionCue, + passportFields, primaryVerdictAction, + revoked, + status, streams, totalRecords: summary.total_records, totalRecordsState: summary.total_records_state, - passportFields, }; } diff --git a/apps/console/src/app/(console)/sources/sources-view.css b/apps/console/src/app/(console)/sources/sources-view.css index 8a1199392..6ad064e53 100644 --- a/apps/console/src/app/(console)/sources/sources-view.css +++ b/apps/console/src/app/(console)/sources/sources-view.css @@ -22,33 +22,33 @@ .rr-s-list { display: flex; flex-direction: column; - border-top: 1px solid var(--border-strong); min-width: 0; + border-top: 1px solid var(--border-strong); } /* The mobile affordance is a (anchor) that must render identically to the desktop

onOpenChange(false)} size="sm" type="button" @@ -5158,6 +5169,7 @@ function ClipboardSheet({
Paste from device + {/** biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional */} Send to browser {remoteInputSensitive ? ( setRevealLocalText((shown) => !shown)} size="sm" type="button" @@ -5185,10 +5199,12 @@ function ClipboardSheet({ autoCorrect="off" className="pdpp-stream-clipboard-textarea min-h-24 resize-y rounded-lg border border-border/80 bg-muted/30 p-3 text-foreground text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/40" data-masked={localInputMasked ? "true" : "false"} + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onChange={(event) => { setLocalText(event.target.value); setPasteState(event.target.value.length > 0 ? "ready" : "idle"); }} + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onPaste={(event) => { const text = event.clipboardData.getData("text"); logDebug( @@ -5214,6 +5230,7 @@ function ClipboardSheet({
@@ -5355,6 +5374,7 @@ function CornerControls({ aria-label={expanded ? `Hide ${connectorName} browser actions` : `More ${connectorName} browser actions`} className="pdpp-stream-control-button" data-pdpp-stream-ui + // biome-ignore lint/performance/noJsxPropsBind: non-memoized, inline binding intentional onClick={() => setExpanded((prev) => !prev)} type="button" > @@ -5684,6 +5704,7 @@ function StreamInteractionDock({