From 136563691ce3bea45f304371b29e7639650eb79e Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Thu, 25 Jun 2026 23:23:03 +0000 Subject: [PATCH 01/20] feat(analytics): add strategic accounts management actions --- .../actions/list-strategic-accounts.ts | 39 +++ .../actions/strategic-accounts.spec.ts | 119 +++++++++ .../actions/update-strategic-account.ts | 35 +++ .../actions/upsert-strategic-accounts.ts | 50 ++++ templates/analytics/server/db/index.ts | 10 + templates/analytics/server/db/schema.ts | 32 +++ .../server/lib/strategic-accounts-store.ts | 230 ++++++++++++++++++ 7 files changed, 515 insertions(+) create mode 100644 templates/analytics/actions/list-strategic-accounts.ts create mode 100644 templates/analytics/actions/strategic-accounts.spec.ts create mode 100644 templates/analytics/actions/update-strategic-account.ts create mode 100644 templates/analytics/actions/upsert-strategic-accounts.ts create mode 100644 templates/analytics/server/lib/strategic-accounts-store.ts diff --git a/templates/analytics/actions/list-strategic-accounts.ts b/templates/analytics/actions/list-strategic-accounts.ts new file mode 100644 index 0000000000..f4f42e87d7 --- /dev/null +++ b/templates/analytics/actions/list-strategic-accounts.ts @@ -0,0 +1,39 @@ +import { defineAction } from "@agent-native/core"; +import { + getRequestOrgId, + getRequestUserEmail, + buildDeepLink, +} from "@agent-native/core/server"; +import { z } from "zod"; + +import { listStrategicAccounts } from "../server/lib/strategic-accounts-store"; + +export default defineAction({ + description: + "List the curated Strategic Accounts roster for the current org. This is the source of truth for the Strategic Accounts overview dashboard and extension — the account names live only in the database, never in source. Returns each account's company name, optional HubSpot company id, editable deployment status, notes, and sort order.", + schema: z.object({}), + http: { method: "GET" }, + readOnly: true, + publicAgent: { expose: true, readOnly: true, requiresAuth: true }, + link: () => ({ + url: buildDeepLink({ + app: "analytics", + view: "adhoc", + params: { dashboardId: "strategic-accounts" }, + }), + label: "Open Strategic Accounts", + view: "adhoc", + }), + run: async () => { + const orgId = getRequestOrgId() || null; + const email = getRequestUserEmail(); + if (!email) throw new Error("no authenticated user"); + const accounts = await listStrategicAccounts({ email, orgId }); + return { + count: accounts.length, + accounts, + /** Comma-separated names, ready to feed the dashboard `accounts` variable. */ + accountsCsv: accounts.map((a) => a.companyName).join(","), + }; + }, +}); diff --git a/templates/analytics/actions/strategic-accounts.spec.ts b/templates/analytics/actions/strategic-accounts.spec.ts new file mode 100644 index 0000000000..f057c802ec --- /dev/null +++ b/templates/analytics/actions/strategic-accounts.spec.ts @@ -0,0 +1,119 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + listStrategicAccounts: vi.fn(), + replaceStrategicAccounts: vi.fn(), + updateStrategicAccount: vi.fn(), + orgId: null as string | null, + email: "alice@example.com" as string | null, +})); + +vi.mock("@agent-native/core", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual }; +}); + +vi.mock("@agent-native/core/server", () => ({ + buildDeepLink: vi.fn( + ({ app, view }: { app: string; view: string }) => `/${app}/${view}`, + ), + getRequestOrgId: () => mocks.orgId, + getRequestUserEmail: () => mocks.email, +})); + +vi.mock("../server/lib/strategic-accounts-store", () => ({ + listStrategicAccounts: mocks.listStrategicAccounts, + replaceStrategicAccounts: mocks.replaceStrategicAccounts, + updateStrategicAccount: mocks.updateStrategicAccount, +})); + +const { default: listAction } = await import("./list-strategic-accounts"); +const { default: upsertAction } = await import("./upsert-strategic-accounts"); +const { default: updateAction } = await import("./update-strategic-account"); + +beforeEach(() => { + mocks.listStrategicAccounts.mockReset(); + mocks.replaceStrategicAccounts.mockReset(); + mocks.updateStrategicAccount.mockReset(); + mocks.orgId = null; + mocks.email = "alice@example.com"; +}); + +describe("list-strategic-accounts", () => { + it("returns accounts and a CSV of names for the dashboard variable", async () => { + mocks.listStrategicAccounts.mockResolvedValue([ + { companyName: "Acme" }, + { companyName: "Globex" }, + ]); + const result = (await listAction.run({})) as { + count: number; + accountsCsv: string; + }; + expect(result.count).toBe(2); + expect(result.accountsCsv).toBe("Acme,Globex"); + expect(mocks.listStrategicAccounts).toHaveBeenCalledWith({ + email: "alice@example.com", + orgId: null, + }); + }); + + it("throws when unauthenticated", async () => { + mocks.email = null; + await expect(listAction.run({})).rejects.toThrow("no authenticated user"); + }); +}); + +describe("upsert-strategic-accounts", () => { + it("replaces the roster atomically and reports the new count", async () => { + mocks.replaceStrategicAccounts.mockResolvedValue([ + { id: "1", companyName: "Acme" }, + ]); + const result = (await upsertAction.run({ + accounts: [{ companyName: "Acme" }], + })) as { ok: boolean; count: number }; + expect(result.ok).toBe(true); + expect(result.count).toBe(1); + expect(mocks.replaceStrategicAccounts).toHaveBeenCalledWith( + [{ companyName: "Acme" }], + { email: "alice@example.com", orgId: null }, + ); + }); + + it("accepts a JSON string for accounts", async () => { + mocks.replaceStrategicAccounts.mockResolvedValue([]); + await upsertAction.run({ + accounts: JSON.stringify([{ companyName: "Acme" }]) as any, + }); + expect(mocks.replaceStrategicAccounts).toHaveBeenCalledWith( + [{ companyName: "Acme" }], + expect.anything(), + ); + }); +}); + +describe("update-strategic-account", () => { + it("passes only the patch fields and returns the updated row", async () => { + mocks.updateStrategicAccount.mockResolvedValue({ + id: "1", + deploymentStatus: "Production", + }); + const result = (await updateAction.run({ + id: "1", + deploymentStatus: "Production", + })) as { ok: boolean; account: { deploymentStatus: string } }; + expect(result.ok).toBe(true); + expect(result.account.deploymentStatus).toBe("Production"); + expect(mocks.updateStrategicAccount).toHaveBeenCalledWith( + "1", + { deploymentStatus: "Production" }, + { email: "alice@example.com", orgId: null }, + ); + }); + + it("throws when the row is missing or inaccessible", async () => { + mocks.updateStrategicAccount.mockResolvedValue(null); + await expect( + updateAction.run({ id: "missing", notes: "x" }), + ).rejects.toThrow(/not found/); + }); +}); diff --git a/templates/analytics/actions/update-strategic-account.ts b/templates/analytics/actions/update-strategic-account.ts new file mode 100644 index 0000000000..00d256d678 --- /dev/null +++ b/templates/analytics/actions/update-strategic-account.ts @@ -0,0 +1,35 @@ +import { defineAction } from "@agent-native/core"; +import { + getRequestOrgId, + getRequestUserEmail, +} from "@agent-native/core/server"; +import { z } from "zod"; + +import { updateStrategicAccount } from "../server/lib/strategic-accounts-store"; + +export default defineAction({ + description: + "Edit one Strategic Account's manual fields (deployment status, notes, company name/id, or sort order). Used by the Strategic Accounts extension to persist deployment-status badge edits. Requires editor access to the account's org.", + schema: z.object({ + id: z.string().min(1).describe("Strategic account row id."), + companyName: z.string().optional(), + companyId: z.string().nullish(), + deploymentStatus: z.string().optional(), + notes: z.string().optional(), + sortOrder: z.number().optional(), + }), + http: { method: "POST" }, + run: async (args) => { + const orgId = getRequestOrgId() || null; + const email = getRequestUserEmail(); + if (!email) throw new Error("no authenticated user"); + const { id, ...patch } = args; + const updated = await updateStrategicAccount(id, patch, { email, orgId }); + if (!updated) { + throw new Error( + `strategic account "${id}" not found (or you don't have access).`, + ); + } + return { ok: true, account: updated }; + }, +}); diff --git a/templates/analytics/actions/upsert-strategic-accounts.ts b/templates/analytics/actions/upsert-strategic-accounts.ts new file mode 100644 index 0000000000..11c9b26fcf --- /dev/null +++ b/templates/analytics/actions/upsert-strategic-accounts.ts @@ -0,0 +1,50 @@ +import { defineAction } from "@agent-native/core"; +import { + getRequestOrgId, + getRequestUserEmail, + buildDeepLink, +} from "@agent-native/core/server"; +import { z } from "zod"; + +import { replaceStrategicAccounts } from "../server/lib/strategic-accounts-store"; + +const accountSchema = z.object({ + companyName: z.string().min(1), + companyId: z.string().nullish(), + deploymentStatus: z.string().optional(), + notes: z.string().optional(), + sortOrder: z.number().optional(), +}); + +export default defineAction({ + description: + "Seed or replace the org's curated Strategic Accounts roster in ONE atomic write. Pass the full list of accounts — this replaces the org's existing roster (it does not append). Use this to seed the list from a curated set or from a live warehouse query (e.g. top enterprise accounts); never hardcode account names in source. Each account needs at least `companyName`; `companyId`, `deploymentStatus`, `notes`, and `sortOrder` are optional. New rows are created with org visibility so the whole organization sees the list.", + schema: z.object({ + accounts: z.preprocess( + (v) => (typeof v === "string" ? JSON.parse(v) : v), + z.array(accountSchema), + ), + }), + http: { method: "POST" }, + run: async (args) => { + const orgId = getRequestOrgId() || null; + const email = getRequestUserEmail(); + if (!email) throw new Error("no authenticated user"); + const rows = await replaceStrategicAccounts(args.accounts, { + email, + orgId, + }); + return { + ok: true, + count: rows.length, + accounts: rows, + summary: `Replaced Strategic Accounts roster; it now has ${rows.length} account(s).`, + urlPath: "/dashboards/strategic-accounts", + deepLink: buildDeepLink({ + app: "analytics", + view: "adhoc", + params: { dashboardId: "strategic-accounts" }, + }), + }; + }, +}); diff --git a/templates/analytics/server/db/index.ts b/templates/analytics/server/db/index.ts index f59ef5727c..438d3ff778 100644 --- a/templates/analytics/server/db/index.ts +++ b/templates/analytics/server/db/index.ts @@ -25,3 +25,13 @@ registerShareableResource({ getResourcePath: (analysis) => `/analyses/${analysis.id}`, getDb, }); + +registerShareableResource({ + type: "strategic-account", + resourceTable: schema.strategicAccounts, + sharesTable: schema.strategicAccountShares, + displayName: "Strategic Account", + titleColumn: "companyName", + getResourcePath: () => `/dashboards/strategic-accounts`, + getDb, +}); diff --git a/templates/analytics/server/db/schema.ts b/templates/analytics/server/db/schema.ts index 527b8ddb64..34d5154f82 100644 --- a/templates/analytics/server/db/schema.ts +++ b/templates/analytics/server/db/schema.ts @@ -79,6 +79,38 @@ export const analyses = table("analyses", { export const analysisShares = createSharesTable("analysis_shares"); +/** + * Curated "Strategic Accounts" list — the source of truth for the migrated + * fusion-analytics Strategic Accounts overview. The (sensitive) account names + * live ONLY in this org-scoped table and the dashboard config row, never in + * source or git. Both surfaces read from it: the extension reads it live, and + * the SQL dashboard's `accounts` variable is projected from it. + * + * `deploymentStatus` and `notes` are editable manual fields (the source kept + * these in localStorage); everything else is the curated roster. Metrics are + * always queried live from BigQuery, never stored here. + */ +export const strategicAccounts = table("strategic_accounts", { + id: text("id").primaryKey(), + /** HubSpot company name — the join key against warehouse tables. */ + companyName: text("company_name").notNull(), + /** Optional HubSpot company id when known. */ + companyId: text("company_id"), + /** Editable deployment-status badge (e.g. "Production", "Pilot"). */ + deploymentStatus: text("deployment_status").notNull().default(""), + /** Editable free-text note. */ + notes: text("notes").notNull().default(""), + /** Manual ordering for the grid. */ + sortOrder: integer("sort_order").notNull().default(0), + createdAt: text("created_at").notNull().default(now()), + updatedAt: text("updated_at").notNull().default(now()), + ...ownableColumns(), +}); + +export const strategicAccountShares = createSharesTable( + "strategic_account_shares", +); + /** * BigQuery result cache (pre-existing — moved here from db plugin so a * single drizzle schema covers the template). diff --git a/templates/analytics/server/lib/strategic-accounts-store.ts b/templates/analytics/server/lib/strategic-accounts-store.ts new file mode 100644 index 0000000000..498f49e0a1 --- /dev/null +++ b/templates/analytics/server/lib/strategic-accounts-store.ts @@ -0,0 +1,230 @@ +import { recordChange } from "@agent-native/core/server"; +import { + accessFilter, + assertAccess, + resolveAccess, +} from "@agent-native/core/sharing"; +import { and, asc, eq, isNull, or } from "drizzle-orm"; + +import { getDb, schema } from "../db/index.js"; + +/** + * Org-scoped store for the curated Strategic Accounts roster. The list is the + * source of truth for the migrated overview and lives ONLY here (and the + * dashboard config row) — never in source/git. Reads/writes are scoped through + * the framework sharing helpers so a user only ever sees their org's rows. + */ + +export interface AccessCtx { + email: string; + orgId: string | null; +} + +export interface StrategicAccountRecord { + id: string; + companyName: string; + companyId: string | null; + deploymentStatus: string; + notes: string; + sortOrder: number; + ownerEmail: string; + orgId: string | null; + visibility: "private" | "org" | "public"; + createdAt: string; + updatedAt: string; +} + +export interface StrategicAccountInput { + companyName: string; + companyId?: string | null; + deploymentStatus?: string; + notes?: string; + sortOrder?: number; +} + +function nowIso(): string { + return new Date().toISOString(); +} + +function newId(): string { + if (typeof crypto !== "undefined" && "randomUUID" in crypto) { + return crypto.randomUUID(); + } + return ( + Math.random().toString(36).slice(2, 10) + + Math.random().toString(36).slice(2, 10) + ); +} + +function rowToRecord(row: any): StrategicAccountRecord { + return { + id: row.id, + companyName: row.companyName, + companyId: row.companyId ?? null, + deploymentStatus: row.deploymentStatus ?? "", + notes: row.notes ?? "", + sortOrder: row.sortOrder ?? 0, + ownerEmail: row.ownerEmail, + orgId: row.orgId ?? null, + visibility: row.visibility, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; +} + +/** + * Rows the caller can write/replace in the current scope: their org's rows + * when in an org, otherwise their own org-less rows. Mirrors the owner-scope + * branch of `accessFilter` so seeding never touches another scope's data. + */ +function writableScope(ctx: AccessCtx) { + if (ctx.orgId) { + return or( + eq(schema.strategicAccounts.orgId, ctx.orgId), + and( + eq(schema.strategicAccounts.ownerEmail, ctx.email), + isNull(schema.strategicAccounts.orgId), + ), + ); + } + return and( + eq(schema.strategicAccounts.ownerEmail, ctx.email), + isNull(schema.strategicAccounts.orgId), + ); +} + +function recordScoped( + type: "change" | "delete", + id: string, + ctx: AccessCtx, +): void { + recordChange({ + source: "strategic-accounts", + type, + key: id, + ...(ctx.orgId ? { orgId: ctx.orgId } : { owner: ctx.email }), + }); +} + +/** List the org's curated accounts, ordered for the grid. */ +export async function listStrategicAccounts( + ctx: AccessCtx, +): Promise { + const db = getDb() as any; + const where = accessFilter( + schema.strategicAccounts, + schema.strategicAccountShares, + { userEmail: ctx.email, orgId: ctx.orgId ?? undefined }, + ); + const rows = await db + .select() + .from(schema.strategicAccounts) + .where(where) + .orderBy( + asc(schema.strategicAccounts.sortOrder), + asc(schema.strategicAccounts.companyName), + ); + return rows.map(rowToRecord); +} + +/** + * Atomically replace the caller-scope's curated roster in ONE write batch + * (per the reliable-mutations rule — never loop inserts). Seeds visibility to + * `org` so the whole organization sees the list. Returns the new rows. + */ +export async function replaceStrategicAccounts( + accounts: StrategicAccountInput[], + ctx: AccessCtx, +): Promise { + const db = getDb() as any; + const now = nowIso(); + const rows = accounts + .map((a, i) => ({ + companyName: String(a.companyName ?? "").trim(), + companyId: + a.companyId === undefined || a.companyId === null + ? null + : String(a.companyId).trim() || null, + deploymentStatus: String(a.deploymentStatus ?? "").trim(), + notes: String(a.notes ?? "").trim(), + sortOrder: + typeof a.sortOrder === "number" && Number.isFinite(a.sortOrder) + ? a.sortOrder + : i, + })) + .filter((a) => a.companyName !== "") + .map((a) => ({ + id: newId(), + ...a, + ownerEmail: ctx.email, + orgId: ctx.orgId, + visibility: "org" as const, + createdAt: now, + updatedAt: now, + })); + + const replace = async (tx: any) => { + await tx.delete(schema.strategicAccounts).where(writableScope(ctx)); + if (rows.length > 0) { + await tx.insert(schema.strategicAccounts).values(rows); + } + }; + if (typeof db.transaction === "function") { + await db.transaction(replace); + } else { + await replace(db); + } + + recordScoped("change", "*", ctx); + return rows.map(rowToRecord); +} + +/** Edit one row's manual fields. Requires editor access to that row. */ +export async function updateStrategicAccount( + id: string, + patch: Partial< + Pick< + StrategicAccountInput, + "companyName" | "companyId" | "deploymentStatus" | "notes" | "sortOrder" + > + >, + ctx: AccessCtx, +): Promise { + const access = await resolveAccess("strategic-account", id, { + userEmail: ctx.email, + orgId: ctx.orgId ?? undefined, + }); + if (!access) return null; + await assertAccess("strategic-account", id, "editor", { + userEmail: ctx.email, + orgId: ctx.orgId ?? undefined, + }); + + const set: Record = { updatedAt: nowIso() }; + if (patch.companyName !== undefined) { + set.companyName = String(patch.companyName).trim(); + } + if (patch.companyId !== undefined) { + set.companyId = + patch.companyId === null ? null : String(patch.companyId).trim() || null; + } + if (patch.deploymentStatus !== undefined) { + set.deploymentStatus = String(patch.deploymentStatus).trim(); + } + if (patch.notes !== undefined) set.notes = String(patch.notes).trim(); + if (patch.sortOrder !== undefined && Number.isFinite(patch.sortOrder)) { + set.sortOrder = patch.sortOrder; + } + + const db = getDb() as any; + await db + .update(schema.strategicAccounts) + .set(set) + .where(eq(schema.strategicAccounts.id, id)); + const [row] = await db + .select() + .from(schema.strategicAccounts) + .where(eq(schema.strategicAccounts.id, id)); + recordScoped("change", id, ctx); + return row ? rowToRecord(row) : null; +} From b30fdb119e34907d1e4645d6a15416e0eca5aa55 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Fri, 26 Jun 2026 02:19:13 +0000 Subject: [PATCH 02/20] feat(analytics): add strategic accounts schema and pipe-separated output --- templates/analytics/.gitignore | 3 ++ .../actions/list-strategic-accounts.ts | 8 +++- templates/analytics/server/plugins/db.ts | 39 +++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/templates/analytics/.gitignore b/templates/analytics/.gitignore index 65277e1c63..d416e5015c 100644 --- a/templates/analytics/.gitignore +++ b/templates/analytics/.gitignore @@ -53,3 +53,6 @@ build /local.db .vercel/ + +# Local scratch (never commit) +scratch/ diff --git a/templates/analytics/actions/list-strategic-accounts.ts b/templates/analytics/actions/list-strategic-accounts.ts index f4f42e87d7..ed3a4081fa 100644 --- a/templates/analytics/actions/list-strategic-accounts.ts +++ b/templates/analytics/actions/list-strategic-accounts.ts @@ -32,8 +32,14 @@ export default defineAction({ return { count: accounts.length, accounts, - /** Comma-separated names, ready to feed the dashboard `accounts` variable. */ + /** Comma-separated names (display only — names may contain commas). */ accountsCsv: accounts.map((a) => a.companyName).join(","), + /** + * Pipe-separated names — the value to feed the dashboard `accounts` + * variable. Pipe avoids collisions with commas inside company names + * (e.g. "Orgill, Inc."); panels expand it with SPLIT('{{accounts}}', '|'). + */ + accountsPipe: accounts.map((a) => a.companyName).join("|"), }; }, }); diff --git a/templates/analytics/server/plugins/db.ts b/templates/analytics/server/plugins/db.ts index 720935de2b..abb932d4a1 100644 --- a/templates/analytics/server/plugins/db.ts +++ b/templates/analytics/server/plugins/db.ts @@ -219,6 +219,45 @@ export default runMigrations( version: 38, sql: `SELECT 1`, }, + // --- v39+: Strategic Accounts curated roster (migrated from + // fusion-analytics). Org-scoped source of truth for the overview + // dashboard + extension. Account names live only here, never in source. + { + version: 39, + sql: `CREATE TABLE IF NOT EXISTS strategic_accounts ( + id TEXT PRIMARY KEY, + company_name TEXT NOT NULL, + company_id TEXT, + deployment_status TEXT NOT NULL DEFAULT '', + notes TEXT NOT NULL DEFAULT '', + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + owner_email TEXT NOT NULL DEFAULT 'local@localhost', + org_id TEXT, + visibility TEXT NOT NULL DEFAULT 'private' + )`, + }, + { + version: 40, + sql: `CREATE TABLE IF NOT EXISTS strategic_account_shares ( + id TEXT PRIMARY KEY, + resource_id TEXT NOT NULL, + principal_type TEXT NOT NULL, + principal_id TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'viewer', + created_by TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + )`, + }, + { + version: 41, + sql: `CREATE INDEX IF NOT EXISTS strategic_accounts_owner_org_idx ON strategic_accounts (owner_email, org_id, sort_order)`, + }, + { + version: 42, + sql: `CREATE INDEX IF NOT EXISTS strategic_account_shares_resource_idx ON strategic_account_shares (resource_id)`, + }, ], { table: "analytics_migrations" }, ); From 2af78984d5bb1fad6beae3f7e76d7d6fc271cbbd Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Fri, 26 Jun 2026 17:00:27 +0000 Subject: [PATCH 03/20] chore: add data privacy and security guidelines --- .builderrules | 1 + 1 file changed, 1 insertion(+) create mode 100644 .builderrules diff --git a/.builderrules b/.builderrules new file mode 100644 index 0000000000..f99f1fa99f --- /dev/null +++ b/.builderrules @@ -0,0 +1 @@ +Data privacy and security is high priority here!! \ No newline at end of file From 158f7806302b7facb83f32b3eaf63a798073a807 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Fri, 26 Jun 2026 17:22:08 +0000 Subject: [PATCH 04/20] feat: add cards chart type for displaying data as individual cards --- .../app/components/dashboard/SqlChart.tsx | 77 +++++++++++++++++++ templates/analytics/app/i18n-data.ts | 10 +++ .../adhoc/sql-dashboard/PanelEditorDialog.tsx | 1 + .../app/pages/adhoc/sql-dashboard/types.ts | 9 +++ 4 files changed, 97 insertions(+) diff --git a/templates/analytics/app/components/dashboard/SqlChart.tsx b/templates/analytics/app/components/dashboard/SqlChart.tsx index f12b8c72cf..a1d51bf8ec 100644 --- a/templates/analytics/app/components/dashboard/SqlChart.tsx +++ b/templates/analytics/app/components/dashboard/SqlChart.tsx @@ -12,6 +12,7 @@ import { } from "@tabler/icons-react"; import { createContext, + Fragment, useCallback, useContext, useEffect, @@ -953,6 +954,10 @@ export function SqlChart({ ); } + if (chartType === "cards") { + return withConfigWarning(); + } + if (chartType === "pie") { return withConfigWarning( []; + panel: SqlPanel; +}) { + const config = panel.config; + + const columns = useMemo(() => { + const rowKeys = new Set(Object.keys(rows[0] ?? {})); + if (config?.columns?.length) { + const configured = config.columns.filter( + (c) => !c.hidden && rowKeys.has(c.key), + ); + if (configured.length > 0) return configured; + } + return Object.keys(rows[0] ?? {}).map((key) => ({ key })); + }, [config?.columns, rows]); + + const titleKey = config?.titleKey ?? columns[0]?.key; + const badgeKey = config?.badgeKey; + const limit = config?.limit; + const displayRows = useMemo( + () => (limit != null && rows.length > limit ? rows.slice(0, limit) : rows), + [rows, limit], + ); + + const detailColumns = columns.filter( + (c) => c.key !== titleKey && c.key !== badgeKey, + ); + + return ( +
+ {displayRows.map((row, i) => { + const titleCol = columns.find((c) => c.key === titleKey); + const badge = + badgeKey != null ? formatCell(row[badgeKey], undefined) : ""; + return ( +
+
+
+ {formatCell(row[titleKey], titleCol?.format)} +
+ {badge && ( + + {badge} + + )} +
+
+ {detailColumns.map((col) => ( + +
+ {col.label ?? col.key} +
+
+ {formatCell(row[col.key], col.format)} +
+
+ ))} +
+
+ ); + })} +
+ ); +} + function TableRenderer({ rows, panel, diff --git a/templates/analytics/app/i18n-data.ts b/templates/analytics/app/i18n-data.ts index d038b670ba..234422c4cc 100644 --- a/templates/analytics/app/i18n-data.ts +++ b/templates/analytics/app/i18n-data.ts @@ -588,6 +588,7 @@ const enUS = { chartTypePie: "Pie", chartTypeMetric: "Metric", chartTypeTable: "Table", + chartTypeCards: "Cards", failedToSavePanel: "Failed to save panel", failedToFormatSql: "Failed to format SQL", title: "Title", @@ -881,6 +882,7 @@ const analyticsSliceTranslations: { chartTypePie: "馅饼", chartTypeMetric: "公制", chartTypeTable: "桌子", + chartTypeCards: "Cards", failedToSavePanel: "保存面板失败", failedToFormatSql: "格式化SQL失败", title: "标题", @@ -1148,6 +1150,7 @@ const analyticsSliceTranslations: { chartTypePie: "Pastel", chartTypeMetric: "Métrico", chartTypeTable: "Mesa", + chartTypeCards: "Cards", failedToSavePanel: "No se pudo guardar el panel", failedToFormatSql: "No se pudo formatear SQL", title: "Título", @@ -1431,6 +1434,7 @@ const analyticsSliceTranslations: { chartTypePie: "Tarte", chartTypeMetric: "Métrique", chartTypeTable: "Tableau", + chartTypeCards: "Cards", failedToSavePanel: "Échec de l'enregistrement du panneau", failedToFormatSql: "Échec du formatage du SQL", title: "Titre", @@ -1712,6 +1716,7 @@ const analyticsSliceTranslations: { chartTypePie: "Torte", chartTypeMetric: "Metrisch", chartTypeTable: "Tisch", + chartTypeCards: "Cards", failedToSavePanel: "Das Panel konnte nicht gespeichert werden", failedToFormatSql: "SQL konnte nicht formatiert werden", title: "Titel", @@ -1989,6 +1994,7 @@ const analyticsSliceTranslations: { chartTypePie: "パイ", chartTypeMetric: "メトリック", chartTypeTable: "テーブル", + chartTypeCards: "Cards", failedToSavePanel: "パネルの保存に失敗しました", failedToFormatSql: "SQLのフォーマットに失敗しました", title: "タイトル", @@ -2260,6 +2266,7 @@ const analyticsSliceTranslations: { chartTypePie: "파이", chartTypeMetric: "미터법", chartTypeTable: "테이블", + chartTypeCards: "Cards", failedToSavePanel: "패널을 저장하지 못했습니다.", failedToFormatSql: "SQL 포맷 실패", title: "제목", @@ -2530,6 +2537,7 @@ const analyticsSliceTranslations: { chartTypePie: "Torta", chartTypeMetric: "Métrica", chartTypeTable: "Mesa", + chartTypeCards: "Cards", failedToSavePanel: "Falha ao salvar painel", failedToFormatSql: "Falha ao formatar SQL", title: "Título", @@ -2803,6 +2811,7 @@ const analyticsSliceTranslations: { chartTypePie: "पाई", chartTypeMetric: "मीट्रिक", chartTypeTable: "मेज़", + chartTypeCards: "Cards", failedToSavePanel: "पैनल सहेजने में विफल", failedToFormatSql: "SQL को फ़ॉर्मेट करने में विफल", title: "शीर्षक", @@ -3070,6 +3079,7 @@ const analyticsSliceTranslations: { chartTypePie: "فطيرة", chartTypeMetric: "متري", chartTypeTable: "طاولة", + chartTypeCards: "Cards", failedToSavePanel: "فشل حفظ اللوحة", failedToFormatSql: "فشل تهيئة SQL", title: "عنوان", diff --git a/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx b/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx index 3ef0ca1e36..fe7ca75d77 100644 --- a/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +++ b/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx @@ -50,6 +50,7 @@ const CHART_TYPES: { value: ChartType; labelKey: string }[] = [ { value: "pie", labelKey: "panelEditor.chartTypePie" }, { value: "metric", labelKey: "panelEditor.chartTypeMetric" }, { value: "table", labelKey: "panelEditor.chartTypeTable" }, + { value: "cards", labelKey: "panelEditor.chartTypeCards" }, ]; const SOURCES: { value: DataSourceType; label: string }[] = [ diff --git a/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts b/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts index 6147b10d63..5cf60cae34 100644 --- a/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +++ b/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts @@ -12,6 +12,7 @@ export type ChartType = | "bar" | "metric" | "table" + | "cards" | "pie" | "section" | "heatmap" @@ -79,6 +80,14 @@ export interface SqlPanelConfig { sortable?: boolean; columns?: TableColumnConfig[]; limit?: number; + /** + * For the `cards` chart type: which column supplies each card's heading. + * Defaults to the first configured column. The remaining columns render as + * label/value rows inside the card. + */ + titleKey?: string; + /** For `cards`: optional column rendered as a status badge/pill on the card. */ + badgeKey?: string; } export interface SqlPanel { From 5db9e5ac6a2eca4700a15ed5783538da8bbfa89f Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Fri, 26 Jun 2026 17:40:37 +0000 Subject: [PATCH 05/20] fix: remove accounts filter container from SQL dashboard --- .../analytics/app/pages/adhoc/sql-dashboard/index.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx b/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx index 9def2fa155..b996338268 100644 --- a/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +++ b/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx @@ -1236,17 +1236,6 @@ export default function SqlDashboardPage() { ) : null} - ) : canEdit ? ( - ) : null} {/* Tabs */} From 81d78fbf7963c8ad75de31be381b7f3c984ef4ab Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Fri, 26 Jun 2026 18:13:00 +0000 Subject: [PATCH 06/20] feat(analytics): add strategic account contacts with multi-child support --- .../actions/list-implementation-blockers.ts | 34 +++ .../list-strategic-account-contacts.ts | 34 +++ .../analytics/actions/update-dashboard.ts | 15 +- .../actions/update-implementation-blocker.ts | 39 +++ .../update-strategic-account-contact.ts | 41 +++ .../actions/upsert-implementation-blockers.ts | 51 ++++ .../upsert-strategic-account-contacts.ts | 53 ++++ .../app/components/layout/Sidebar.tsx | 92 ++++++- templates/analytics/app/lib/format-sql.ts | 1 + .../adhoc/sql-dashboard/PanelEditorDialog.tsx | 1 + .../adhoc/sql-dashboard/ViewSqlPopover.tsx | 1 + .../app/pages/adhoc/sql-dashboard/types.ts | 1 + templates/analytics/data/q2.json | 1 + templates/analytics/data/q3.json | 1 + templates/analytics/server/db/index.ts | 20 ++ templates/analytics/server/db/schema.ts | 59 +++++ .../analytics/server/handlers/sql-query.ts | 11 +- .../analytics/server/lib/app-sql.spec.ts | 53 ++++ templates/analytics/server/lib/app-sql.ts | 186 +++++++++++++ .../lib/implementation-blockers-store.ts | 224 ++++++++++++++++ .../lib/strategic-account-contacts-store.ts | 245 ++++++++++++++++++ templates/analytics/server/plugins/db.ts | 79 ++++++ 22 files changed, 1226 insertions(+), 16 deletions(-) create mode 100644 templates/analytics/actions/list-implementation-blockers.ts create mode 100644 templates/analytics/actions/list-strategic-account-contacts.ts create mode 100644 templates/analytics/actions/update-implementation-blocker.ts create mode 100644 templates/analytics/actions/update-strategic-account-contact.ts create mode 100644 templates/analytics/actions/upsert-implementation-blockers.ts create mode 100644 templates/analytics/actions/upsert-strategic-account-contacts.ts create mode 100644 templates/analytics/data/q2.json create mode 100644 templates/analytics/data/q3.json create mode 100644 templates/analytics/server/lib/app-sql.spec.ts create mode 100644 templates/analytics/server/lib/app-sql.ts create mode 100644 templates/analytics/server/lib/implementation-blockers-store.ts create mode 100644 templates/analytics/server/lib/strategic-account-contacts-store.ts diff --git a/templates/analytics/actions/list-implementation-blockers.ts b/templates/analytics/actions/list-implementation-blockers.ts new file mode 100644 index 0000000000..3cee7e0f05 --- /dev/null +++ b/templates/analytics/actions/list-implementation-blockers.ts @@ -0,0 +1,34 @@ +import { defineAction } from "@agent-native/core"; +import { + getRequestOrgId, + getRequestUserEmail, + buildDeepLink, +} from "@agent-native/core/server"; +import { z } from "zod"; + +import { listImplementationBlockers } from "../server/lib/implementation-blockers-store"; + +export default defineAction({ + description: + "List the curated Implementation Blockers for the current org. This is the source of truth for the Implementation Blockers dashboard — the blocker details live only in the database, never in source. Returns each blocker's company, type, status, summary, and details.", + schema: z.object({}), + http: { method: "GET" }, + readOnly: true, + publicAgent: { expose: true, readOnly: true, requiresAuth: true }, + link: () => ({ + url: buildDeepLink({ + app: "analytics", + view: "adhoc", + params: { dashboardId: "implementation-blockers" }, + }), + label: "Open Implementation Blockers", + view: "adhoc", + }), + run: async () => { + const orgId = getRequestOrgId() || null; + const email = getRequestUserEmail(); + if (!email) throw new Error("no authenticated user"); + const blockers = await listImplementationBlockers({ email, orgId }); + return { count: blockers.length, blockers }; + }, +}); diff --git a/templates/analytics/actions/list-strategic-account-contacts.ts b/templates/analytics/actions/list-strategic-account-contacts.ts new file mode 100644 index 0000000000..b2615d5073 --- /dev/null +++ b/templates/analytics/actions/list-strategic-account-contacts.ts @@ -0,0 +1,34 @@ +import { defineAction } from "@agent-native/core"; +import { + getRequestOrgId, + getRequestUserEmail, + buildDeepLink, +} from "@agent-native/core/server"; +import { z } from "zod"; + +import { listStrategicAccountContacts } from "../server/lib/strategic-account-contacts-store"; + +export default defineAction({ + description: + "List the curated Strategic Account Coverage contacts for the current org (champions, enablers, exec sponsors). This is the source of truth for the Strategic Account Coverage dashboard — the contact details live only in the database, never in source. Returns each contact's company, role, name, title, email, confidence, and rationale.", + schema: z.object({}), + http: { method: "GET" }, + readOnly: true, + publicAgent: { expose: true, readOnly: true, requiresAuth: true }, + link: () => ({ + url: buildDeepLink({ + app: "analytics", + view: "adhoc", + params: { dashboardId: "strategic-account-coverage" }, + }), + label: "Open Strategic Account Coverage", + view: "adhoc", + }), + run: async () => { + const orgId = getRequestOrgId() || null; + const email = getRequestUserEmail(); + if (!email) throw new Error("no authenticated user"); + const contacts = await listStrategicAccountContacts({ email, orgId }); + return { count: contacts.length, contacts }; + }, +}); diff --git a/templates/analytics/actions/update-dashboard.ts b/templates/analytics/actions/update-dashboard.ts index 304f1c1036..01698fc4a3 100644 --- a/templates/analytics/actions/update-dashboard.ts +++ b/templates/analytics/actions/update-dashboard.ts @@ -12,6 +12,7 @@ import { import { z } from "zod"; import { interpolate } from "../app/pages/adhoc/sql-dashboard/interpolate"; +import { validateAppSql } from "../server/lib/app-sql.js"; import { dryRunQuery } from "../server/lib/bigquery"; import { getDashboard, upsertDashboard } from "../server/lib/dashboards-store"; import { parseDemoDescriptor } from "../server/lib/demo-source"; @@ -252,6 +253,7 @@ function validateDashboardConfig( "ga4", "amplitude", "first-party", + "app", "demo", "prometheus", ]); @@ -285,7 +287,7 @@ function validateDashboardConfig( } } if (!isSection && !validSources.has(p.source as string)) { - return `panel[${i}].source must be 'bigquery', 'ga4', 'amplitude', 'first-party', 'demo', or 'prometheus' (got '${p.source}'). source selects the backend — put the PromQL/SQL/table name in sql, not here.`; + return `panel[${i}].source must be 'bigquery', 'ga4', 'amplitude', 'first-party', 'app', 'demo', or 'prometheus' (got '${p.source}'). source selects the backend — put the PromQL/SQL/table name in sql, not here.`; } if ( isSection && @@ -353,6 +355,17 @@ async function validatePanelSql( } continue; } + if (p.source === "app") { + const raw = typeof p.sql === "string" ? p.sql : ""; + if (raw.trim()) { + try { + validateAppSql(interpolate(raw, vars)); + } catch (e: any) { + return `panel[${i}] "${p.title || p.id}" app SQL is invalid: ${e?.message ?? e}`; + } + } + continue; + } if (p.source !== "bigquery") continue; const raw = typeof p.sql === "string" ? p.sql : ""; if (!raw.trim()) continue; diff --git a/templates/analytics/actions/update-implementation-blocker.ts b/templates/analytics/actions/update-implementation-blocker.ts new file mode 100644 index 0000000000..1726cea3e5 --- /dev/null +++ b/templates/analytics/actions/update-implementation-blocker.ts @@ -0,0 +1,39 @@ +import { defineAction } from "@agent-native/core"; +import { + getRequestOrgId, + getRequestUserEmail, +} from "@agent-native/core/server"; +import { z } from "zod"; + +import { updateImplementationBlocker } from "../server/lib/implementation-blockers-store"; + +export default defineAction({ + description: + "Edit one Implementation Blocker (type, status, summary, details, company, or sort order). Requires editor access to the blocker's org.", + schema: z.object({ + id: z.string().min(1).describe("Blocker row id."), + companyName: z.string().optional(), + blockerType: z.string().optional(), + status: z.enum(["active", "monitoring", "resolved"]).optional(), + summary: z.string().optional(), + details: z.string().optional(), + sortOrder: z.number().optional(), + }), + http: { method: "POST" }, + run: async (args) => { + const orgId = getRequestOrgId() || null; + const email = getRequestUserEmail(); + if (!email) throw new Error("no authenticated user"); + const { id, ...patch } = args; + const updated = await updateImplementationBlocker(id, patch, { + email, + orgId, + }); + if (!updated) { + throw new Error( + `implementation blocker "${id}" not found (or you don't have access).`, + ); + } + return { ok: true, blocker: updated }; + }, +}); diff --git a/templates/analytics/actions/update-strategic-account-contact.ts b/templates/analytics/actions/update-strategic-account-contact.ts new file mode 100644 index 0000000000..082dd8e009 --- /dev/null +++ b/templates/analytics/actions/update-strategic-account-contact.ts @@ -0,0 +1,41 @@ +import { defineAction } from "@agent-native/core"; +import { + getRequestOrgId, + getRequestUserEmail, +} from "@agent-native/core/server"; +import { z } from "zod"; + +import { updateStrategicAccountContact } from "../server/lib/strategic-account-contacts-store"; + +export default defineAction({ + description: + "Edit one Strategic Account Coverage contact (role, name, title, email, confidence, rationale, company, or sort order). Requires editor access to the contact's org.", + schema: z.object({ + id: z.string().min(1).describe("Contact row id."), + companyName: z.string().optional(), + role: z.enum(["champion", "enabler", "exec_sponsor"]).optional(), + contactName: z.string().optional(), + title: z.string().optional(), + email: z.string().optional(), + confidence: z.enum(["high", "medium", "low"]).optional(), + rationale: z.string().optional(), + sortOrder: z.number().optional(), + }), + http: { method: "POST" }, + run: async (args) => { + const orgId = getRequestOrgId() || null; + const email = getRequestUserEmail(); + if (!email) throw new Error("no authenticated user"); + const { id, ...patch } = args; + const updated = await updateStrategicAccountContact(id, patch, { + email, + orgId, + }); + if (!updated) { + throw new Error( + `coverage contact "${id}" not found (or you don't have access).`, + ); + } + return { ok: true, contact: updated }; + }, +}); diff --git a/templates/analytics/actions/upsert-implementation-blockers.ts b/templates/analytics/actions/upsert-implementation-blockers.ts new file mode 100644 index 0000000000..9aa6629db8 --- /dev/null +++ b/templates/analytics/actions/upsert-implementation-blockers.ts @@ -0,0 +1,51 @@ +import { defineAction } from "@agent-native/core"; +import { + getRequestOrgId, + getRequestUserEmail, + buildDeepLink, +} from "@agent-native/core/server"; +import { z } from "zod"; + +import { replaceImplementationBlockers } from "../server/lib/implementation-blockers-store"; + +const blockerSchema = z.object({ + companyName: z.string().min(1), + blockerType: z.string().optional(), + status: z.enum(["active", "monitoring", "resolved"]).optional(), + summary: z.string().optional(), + details: z.string().optional(), + sortOrder: z.number().optional(), +}); + +export default defineAction({ + description: + "Seed or replace the org's curated Implementation Blockers in ONE atomic write. Pass the full list — this replaces the org's existing blockers (it does not append). Never hardcode blocker details in source; populate them here. Each blocker needs at least `companyName`; `blockerType`, `status` (active/monitoring/resolved), `summary`, `details`, and `sortOrder` are optional. New rows are created with org visibility.", + schema: z.object({ + blockers: z.preprocess( + (v) => (typeof v === "string" ? JSON.parse(v) : v), + z.array(blockerSchema), + ), + }), + http: { method: "POST" }, + run: async (args) => { + const orgId = getRequestOrgId() || null; + const email = getRequestUserEmail(); + if (!email) throw new Error("no authenticated user"); + const rows = await replaceImplementationBlockers(args.blockers, { + email, + orgId, + }); + return { + ok: true, + count: rows.length, + blockers: rows, + summary: `Replaced Implementation Blockers; it now has ${rows.length} blocker(s).`, + urlPath: "/dashboards/implementation-blockers", + deepLink: buildDeepLink({ + app: "analytics", + view: "adhoc", + params: { dashboardId: "implementation-blockers" }, + }), + }; + }, +}); diff --git a/templates/analytics/actions/upsert-strategic-account-contacts.ts b/templates/analytics/actions/upsert-strategic-account-contacts.ts new file mode 100644 index 0000000000..2c39c17936 --- /dev/null +++ b/templates/analytics/actions/upsert-strategic-account-contacts.ts @@ -0,0 +1,53 @@ +import { defineAction } from "@agent-native/core"; +import { + getRequestOrgId, + getRequestUserEmail, + buildDeepLink, +} from "@agent-native/core/server"; +import { z } from "zod"; + +import { replaceStrategicAccountContacts } from "../server/lib/strategic-account-contacts-store"; + +const contactSchema = z.object({ + companyName: z.string().min(1), + role: z.enum(["champion", "enabler", "exec_sponsor"]).optional(), + contactName: z.string().optional(), + title: z.string().optional(), + email: z.string().optional(), + confidence: z.enum(["high", "medium", "low"]).optional(), + rationale: z.string().optional(), + sortOrder: z.number().optional(), +}); + +export default defineAction({ + description: + "Seed or replace the org's curated Strategic Account Coverage contacts in ONE atomic write. Pass the full list — this replaces the org's existing contacts (it does not append). Never hardcode contact details in source; populate them here. Each contact needs at least `companyName`; `role` (champion/enabler/exec_sponsor), `contactName`, `title`, `email`, `confidence` (high/medium/low), `rationale`, and `sortOrder` are optional. New rows are created with org visibility.", + schema: z.object({ + contacts: z.preprocess( + (v) => (typeof v === "string" ? JSON.parse(v) : v), + z.array(contactSchema), + ), + }), + http: { method: "POST" }, + run: async (args) => { + const orgId = getRequestOrgId() || null; + const email = getRequestUserEmail(); + if (!email) throw new Error("no authenticated user"); + const rows = await replaceStrategicAccountContacts(args.contacts, { + email, + orgId, + }); + return { + ok: true, + count: rows.length, + contacts: rows, + summary: `Replaced Strategic Account Coverage contacts; it now has ${rows.length} contact(s).`, + urlPath: "/dashboards/strategic-account-coverage", + deepLink: buildDeepLink({ + app: "analytics", + view: "adhoc", + params: { dashboardId: "strategic-account-coverage" }, + }), + }; + }, +}); diff --git a/templates/analytics/app/components/layout/Sidebar.tsx b/templates/analytics/app/components/layout/Sidebar.tsx index 92293a9b7b..4cb1015503 100644 --- a/templates/analytics/app/components/layout/Sidebar.tsx +++ b/templates/analytics/app/components/layout/Sidebar.tsx @@ -51,6 +51,11 @@ type SidebarDashboard = { subviews?: DashboardSubview[]; source: "static" | "sql"; visibility?: Visibility; + /** Parent dashboard id — when set and the parent is present, this dashboard + * renders nested (indented) beneath it in the sidebar. */ + parentId?: string; + /** Nesting depth in the rendered tree (0 = top level, 1 = child). */ + depth?: number; }; import { DevDatabaseLink, @@ -338,6 +343,7 @@ type Visibility = "private" | "org" | "public"; function SortableRow({ id, + depth = 0, favoriteKey, name, href, @@ -356,6 +362,7 @@ function SortableRow({ children, }: { id: string; + depth?: number; favoriteKey: string; name: string; href: string; @@ -520,7 +527,20 @@ function SortableRow({ }, [href, t]); return ( -
+
0 ? { marginInlineStart: depth * 14 } : null), + }} + className="group/item relative min-w-0" + > + {depth > 0 && ( + + )} ) : null} + {/* Embedded extension — renders in place of panels when configured */} + {dashboard.embedExtensionId ? ( + + ) : null} + {/* Tabs */} - {tabs.length > 0 && activeTab && ( + {!dashboard.embedExtensionId && tabs.length > 0 && activeTab && (
{groupedTabs.hasNestedTabs && groupedTabs.groups.length > 1 ? ( 0 && ( - - )} + {!dashboard.embedExtensionId && + dashboard.filters && + dashboard.filters.length > 0 && ( + + )} {/* Panels grid */} - {dashboard.panels.length === 0 ? ( + {dashboard.embedExtensionId ? null : dashboard.panels.length === 0 ? (

{t("sqlDashboard.noPanels")}

diff --git a/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts b/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts index 85c1264e7e..81cbb340ee 100644 --- a/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +++ b/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts @@ -144,6 +144,12 @@ export interface SqlDashboardConfig { * not only at narrow viewports). Defaults to 2. */ columns?: number; + /** + * When set, the dashboard route renders this extension (by id) inline instead + * of the SQL panels — used to surface a rich extension UI at a dashboard URL. + * The id is stored in the dashboard config (data), never hardcoded in source. + */ + embedExtensionId?: string; panels: SqlPanel[]; } From 92853344c59c7a85106fa458ddd8fd6e980f5afa Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Fri, 26 Jun 2026 19:08:57 +0000 Subject: [PATCH 10/20] chore: remove .builderrules file --- .builderrules | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .builderrules diff --git a/.builderrules b/.builderrules deleted file mode 100644 index f99f1fa99f..0000000000 --- a/.builderrules +++ /dev/null @@ -1 +0,0 @@ -Data privacy and security is high priority here!! \ No newline at end of file From 620b067d78043dee9ed225c68d6214a3cfae3ee2 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Fri, 26 Jun 2026 20:32:10 +0000 Subject: [PATCH 11/20] Fix strategic accounts metadata preservation and SQL access control --- ...generate-strategic-accounts-roster.spec.ts | 65 ++++++- .../generate-strategic-accounts-roster.ts | 23 ++- templates/analytics/server/db/index.ts | 6 + .../analytics/server/lib/app-sql.spec.ts | 46 +++++ templates/analytics/server/lib/app-sql.ts | 183 +++++++++++++++--- .../lib/implementation-blockers-store.ts | 2 + .../analytics/server/lib/org-write-guard.ts | 32 +++ .../lib/strategic-account-contacts-store.ts | 2 + .../server/lib/strategic-accounts-store.ts | 2 + 9 files changed, 329 insertions(+), 32 deletions(-) create mode 100644 templates/analytics/server/lib/org-write-guard.ts diff --git a/templates/analytics/actions/generate-strategic-accounts-roster.spec.ts b/templates/analytics/actions/generate-strategic-accounts-roster.spec.ts index 43bd22901e..fe5493f597 100644 --- a/templates/analytics/actions/generate-strategic-accounts-roster.spec.ts +++ b/templates/analytics/actions/generate-strategic-accounts-roster.spec.ts @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ runQuery: vi.fn(), replaceStrategicAccounts: vi.fn(), + listStrategicAccounts: vi.fn(), getDashboard: vi.fn(), upsertDashboard: vi.fn(), orgId: null as string | null, @@ -25,6 +26,7 @@ vi.mock("@agent-native/core/server", () => ({ vi.mock("../server/lib/bigquery", () => ({ runQuery: mocks.runQuery })); vi.mock("../server/lib/strategic-accounts-store", () => ({ replaceStrategicAccounts: mocks.replaceStrategicAccounts, + listStrategicAccounts: mocks.listStrategicAccounts, })); vi.mock("../server/lib/dashboards-store", () => ({ getDashboard: mocks.getDashboard, @@ -37,6 +39,8 @@ const { default: generateAction } = beforeEach(() => { mocks.runQuery.mockReset(); mocks.replaceStrategicAccounts.mockReset(); + mocks.listStrategicAccounts.mockReset(); + mocks.listStrategicAccounts.mockResolvedValue([]); mocks.getDashboard.mockReset(); mocks.upsertDashboard.mockReset(); mocks.orgId = null; @@ -75,8 +79,20 @@ describe("generate-strategic-accounts-roster", () => { // Roster written with sortOrder reflecting rank, blanks dropped. expect(mocks.replaceStrategicAccounts).toHaveBeenCalledWith( [ - { companyName: "Acme", sortOrder: 0 }, - { companyName: "Globex", sortOrder: 1 }, + { + companyName: "Acme", + sortOrder: 0, + companyId: null, + deploymentStatus: "", + notes: "", + }, + { + companyName: "Globex", + sortOrder: 1, + companyId: null, + deploymentStatus: "", + notes: "", + }, ], { email: "alice@example.com", orgId: null }, ); @@ -91,6 +107,51 @@ describe("generate-strategic-accounts-roster", () => { ); }); + it("preserves manually-curated metadata for companies that already exist", async () => { + mocks.runQuery.mockResolvedValue({ + rows: [ + { company_name: "Acme", active_users: 50 }, + { company_name: "Globex", active_users: 20 }, + ], + }); + mocks.listStrategicAccounts.mockResolvedValue([ + { + id: "x", + companyName: "Acme", + companyId: "org_123", + deploymentStatus: "live", + notes: "key account", + sortOrder: 5, + }, + ]); + mocks.replaceStrategicAccounts.mockImplementation(async (rows: any[]) => + rows.map((r, i) => ({ id: String(i), ...r })), + ); + mocks.getDashboard.mockResolvedValue(null); + + await generateAction.run({}); + + expect(mocks.replaceStrategicAccounts).toHaveBeenCalledWith( + [ + { + companyName: "Acme", + sortOrder: 0, + companyId: "org_123", + deploymentStatus: "live", + notes: "key account", + }, + { + companyName: "Globex", + sortOrder: 1, + companyId: null, + deploymentStatus: "", + notes: "", + }, + ], + { email: "alice@example.com", orgId: null }, + ); + }); + it("dryRun previews without writing", async () => { mocks.runQuery.mockResolvedValue({ rows: [{ company_name: "Acme", active_users: 9 }], diff --git a/templates/analytics/actions/generate-strategic-accounts-roster.ts b/templates/analytics/actions/generate-strategic-accounts-roster.ts index c3a493d798..a2635d2127 100644 --- a/templates/analytics/actions/generate-strategic-accounts-roster.ts +++ b/templates/analytics/actions/generate-strategic-accounts-roster.ts @@ -8,7 +8,10 @@ import { z } from "zod"; import { runQuery } from "../server/lib/bigquery"; import { getDashboard, upsertDashboard } from "../server/lib/dashboards-store"; -import { replaceStrategicAccounts } from "../server/lib/strategic-accounts-store"; +import { + listStrategicAccounts, + replaceStrategicAccounts, +} from "../server/lib/strategic-accounts-store"; /** * Derives the Strategic Accounts roster LIVE from the warehouse so a fresh @@ -137,8 +140,24 @@ export default defineAction({ }; } + // Preserve manually-curated fields (companyId, deploymentStatus, notes) for + // companies that already exist — the warehouse ranking only knows the name, + // so a blind replace would silently wipe human-entered metadata. + const existing = await listStrategicAccounts(ctx); + const priorByName = new Map( + existing.map((a) => [a.companyName.trim().toLowerCase(), a]), + ); const written = await replaceStrategicAccounts( - ranked.map((r, i) => ({ companyName: r.companyName, sortOrder: i })), + ranked.map((r, i) => { + const prior = priorByName.get(r.companyName.toLowerCase()); + return { + companyName: r.companyName, + sortOrder: i, + companyId: prior?.companyId ?? null, + deploymentStatus: prior?.deploymentStatus ?? "", + notes: prior?.notes ?? "", + }; + }), ctx, ); const dashboardSynced = await syncDashboardVariable(accountsPipe, ctx); diff --git a/templates/analytics/server/db/index.ts b/templates/analytics/server/db/index.ts index 0e3791168c..80acce2a6f 100644 --- a/templates/analytics/server/db/index.ts +++ b/templates/analytics/server/db/index.ts @@ -45,6 +45,8 @@ registerShareableResource({ displayName: "Strategic Account", titleColumn: "companyName", getResourcePath: () => `/dashboards/strategic-accounts`, + allowPublic: false, + requireOrgMemberForUserShares: true, getDb, }); @@ -55,6 +57,8 @@ registerShareableResource({ displayName: "Strategic Account Contact", titleColumn: "contactName", getResourcePath: () => `/dashboards/strategic-account-coverage`, + allowPublic: false, + requireOrgMemberForUserShares: true, getDb, }); @@ -65,5 +69,7 @@ registerShareableResource({ displayName: "Implementation Blocker", titleColumn: "summary", getResourcePath: () => `/dashboards/implementation-blockers`, + allowPublic: false, + requireOrgMemberForUserShares: true, getDb, }); diff --git a/templates/analytics/server/lib/app-sql.spec.ts b/templates/analytics/server/lib/app-sql.spec.ts index 5c1be880e0..b4aeb2cd27 100644 --- a/templates/analytics/server/lib/app-sql.spec.ts +++ b/templates/analytics/server/lib/app-sql.spec.ts @@ -50,4 +50,50 @@ describe("validateAppSql", () => { /must read from/i, ); }); + + it("accepts an explicit JOIN between two allowed tables", () => { + expect(() => + validateAppSql( + "SELECT sa.company_name FROM strategic_accounts sa JOIN implementation_blockers b ON b.company_name = sa.company_name", + ), + ).not.toThrow(); + }); + + it("rejects a comma join that smuggles in a non-allowed table", () => { + expect(() => + validateAppSql("SELECT * FROM strategic_accounts, dashboards"), + ).toThrow(/comma joins/i); + }); + + it("rejects a comma join even between two allowed tables", () => { + expect(() => + validateAppSql( + "SELECT * FROM strategic_accounts, implementation_blockers", + ), + ).toThrow(/comma joins/i); + }); + + it("rejects double-quoted table identifiers", () => { + expect(() => + validateAppSql('SELECT * FROM strategic_accounts JOIN "user" ON 1=1'), + ).toThrow(/quoted identifiers/i); + }); + + it("rejects backtick-quoted table identifiers", () => { + expect(() => validateAppSql("SELECT * FROM `secrets`")).toThrow( + /quoted identifiers/i, + ); + }); + + it("rejects bracket-quoted table identifiers", () => { + expect(() => validateAppSql("SELECT * FROM [secrets]")).toThrow( + /quoted identifiers/i, + ); + }); + + it("rejects schema-qualified table names", () => { + expect(() => + validateAppSql("SELECT * FROM main.strategic_accounts"), + ).toThrow(/schema-qualified/i); + }); }); diff --git a/templates/analytics/server/lib/app-sql.ts b/templates/analytics/server/lib/app-sql.ts index f7685e0450..d75ea0ca93 100644 --- a/templates/analytics/server/lib/app-sql.ts +++ b/templates/analytics/server/lib/app-sql.ts @@ -1,4 +1,7 @@ import { getDbExec } from "@agent-native/core/db"; +import { accessFilter } from "@agent-native/core/sharing"; + +import { getDb, schema } from "../db/index.js"; /** * The `app` panel data source: lets SQL dashboards read the app's OWN @@ -30,10 +33,58 @@ const MAX_QUERY_ROWS = 5_000; * MUST expose ownableColumns (owner_email, org_id) so the scope filter below is * valid — do not add a table without those columns. */ -const ALLOWED_TABLES = new Set([ - "strategic_accounts", - "strategic_account_contacts", - "implementation_blockers", +/** + * Maps each readable table to its drizzle table + shares table so the scoped + * subquery can be built from the SAME `accessFilter` the store/action layer + * uses — honoring visibility (private/org/public) and explicit share rows, not + * just owner/org_id. + */ +const ALLOWED_TABLES_MAP: Record< + string, + { table: any; shares: any } +> = { + strategic_accounts: { + table: schema.strategicAccounts, + shares: schema.strategicAccountShares, + }, + strategic_account_contacts: { + table: schema.strategicAccountContacts, + shares: schema.strategicAccountContactShares, + }, + implementation_blockers: { + table: schema.implementationBlockers, + shares: schema.implementationBlockerShares, + }, +}; + +const ALLOWED_TABLES = new Set(Object.keys(ALLOWED_TABLES_MAP)); + +/** Keywords that can follow a table spec and end the FROM/JOIN table list. */ +const CLAUSE_STOP_WORDS = new Set([ + "where", + "group", + "order", + "limit", + "having", + "union", + "except", + "intersect", + "on", + "using", + "join", + "left", + "right", + "inner", + "outer", + "cross", + "full", + "natural", + "select", + "window", + "returning", + "as", + "and", + "or", ]); const RESERVED_ALIAS_WORDS = new Set([ @@ -61,6 +112,64 @@ function stripSqlLiterals(sql: string): string { .replace(/\/\*[\s\S]*?\*\//g, " "); } +/** Advance past a balanced parenthesized group starting at `open` ('('). */ +function skipBalancedParens(text: string, open: number): number { + let depth = 0; + for (let i = open; i < text.length; i++) { + if (text[i] === "(") depth++; + else if (text[i] === ")") { + depth--; + if (depth === 0) return i + 1; + } + } + return text.length; +} + +/** + * Extract every table identifier that appears in a FROM/JOIN table position, + * including comma-separated lists. Derived tables (subqueries) are skipped here + * because their inner FROM/JOIN is matched independently. Throws on + * comma-style joins, which the scoping rewriter cannot safely rewrite. + */ +function extractTableRefs(stripped: string): string[] { + const refs: string[] = []; + const kw = /\b(from|join)\b/gi; + let m: RegExpExecArray | null; + while ((m = kw.exec(stripped))) { + let i = m.index + m[0].length; + while (i < stripped.length && /\s/.test(stripped[i])) i++; + if (stripped[i] === "(") { + // Derived table / subquery; its inner FROM is matched by the outer scan. + continue; + } + const idMatch = /^[A-Za-z_][A-Za-z0-9_$]*(?:\.[A-Za-z_][A-Za-z0-9_$]*)*/.exec( + stripped.slice(i), + ); + if (!idMatch) continue; + const ident = idMatch[0]; + if (CLAUSE_STOP_WORDS.has(ident.toLowerCase())) continue; + refs.push(ident); + i += ident.length; + + // Optional alias: [AS] (unless it's a clause keyword). + while (i < stripped.length && /\s/.test(stripped[i])) i++; + const aliasMatch = /^(?:as\s+)?[A-Za-z_][A-Za-z0-9_$]*/i.exec( + stripped.slice(i), + ); + if (aliasMatch) { + const aliasWord = aliasMatch[0].replace(/^as\s+/i, "").toLowerCase(); + if (!CLAUSE_STOP_WORDS.has(aliasWord)) i += aliasMatch[0].length; + } + while (i < stripped.length && /\s/.test(stripped[i])) i++; + if (stripped[i] === ",") { + throw new Error( + "Comma joins are not allowed in dashboard SQL; use an explicit JOIN", + ); + } + } + return refs; +} + export function validateAppSql(sql: string): void { const stripped = stripSqlLiterals(sql).trim(); const lowered = stripped.toLowerCase(); @@ -80,6 +189,12 @@ export function validateAppSql(sql: string): void { if (stripped.includes("?") || /\$\d+\b/.test(stripped)) { throw new Error("Bind placeholders are not supported in dashboard SQL"); } + // Quoted identifiers ("user", `user`, [user]) could smuggle in tables that + // the plain-identifier allow-list never inspects. Our curated tables/columns + // are all bare snake_case, so reject quoting outright. + if (/["`\[\]]/.test(stripped)) { + throw new Error("Quoted identifiers are not allowed in dashboard SQL"); + } const cteNames = new Set(); const cteRe = /\b([a-zA-Z_][a-zA-Z0-9_]*)\s+as\s*\(/gi; @@ -88,16 +203,20 @@ export function validateAppSql(sql: string): void { } let usesAllowed = false; - const tableRe = /\b(?:from|join)\s+([a-zA-Z_][a-zA-Z0-9_]*)/gi; - for (const match of stripped.matchAll(tableRe)) { - const ref = match[1].toLowerCase(); - if (ALLOWED_TABLES.has(ref)) { + for (const ref of extractTableRefs(stripped)) { + const lower = ref.toLowerCase(); + if (lower.includes(".")) { + throw new Error( + `Schema-qualified table names are not allowed (found ${ref})`, + ); + } + if (ALLOWED_TABLES.has(lower)) { usesAllowed = true; continue; } - if (cteNames.has(ref)) continue; + if (cteNames.has(lower)) continue; throw new Error( - `App queries can only read ${[...ALLOWED_TABLES].join(", ")} (found ${match[1]})`, + `App queries can only read ${[...ALLOWED_TABLES].join(", ")} (found ${ref})`, ); } if (!usesAllowed) { @@ -107,27 +226,35 @@ export function validateAppSql(sql: string): void { } } -function scopeClause(scope: AppQueryScope): { - sql: string; - args: Array; -} { - if (scope.orgId) { - return { - sql: "(org_id = ? OR (org_id IS NULL AND owner_email = ?))", - args: [scope.orgId, scope.userEmail], - }; +/** + * Build the scoped subquery for one allowed table by compiling the SAME + * `accessFilter` predicate the store layer uses. This guarantees app-source + * dashboard reads honor each row's visibility AND explicit user/org shares + * (and the resource's org-only policy) — not just owner_email/org_id. + */ +function scopedTableSubquery( + tableName: string, + scope: AppQueryScope, +): { sql: string; args: unknown[] } { + const entry = ALLOWED_TABLES_MAP[tableName.toLowerCase()]; + if (!entry) { + // Should never happen: validateAppSql already rejected unknown tables. + throw new Error(`Table not readable via app source: ${tableName}`); } - return { - sql: "(org_id IS NULL AND owner_email = ?)", - args: [scope.userEmail], - }; + const db = getDb() as any; + const where = accessFilter(entry.table, entry.shares, { + userEmail: scope.userEmail, + orgId: scope.orgId ?? undefined, + }); + const compiled = db.select().from(entry.table).where(where).toSQL(); + return { sql: compiled.sql as string, args: (compiled.params ?? []) as unknown[] }; } function scopedAppSql( sql: string, scope: AppQueryScope, -): { sql: string; args: Array } { - const args: Array = []; +): { sql: string; args: unknown[] } { + const args: unknown[] = []; const tableAlt = [...ALLOWED_TABLES].join("|"); const aliasRe = new RegExp( `\\b(from|join)\\s+(${tableAlt})\\b(\\s+(?:as\\s+)?(?!where\\b|on\\b|group\\b|order\\b|limit\\b|join\\b|left\\b|right\\b|inner\\b|outer\\b|cross\\b|full\\b|having\\b|union\\b)([a-zA-Z_][a-zA-Z0-9_]*))?`, @@ -144,9 +271,9 @@ function scopedAppSql( !RESERVED_ALIAS_WORDS.has(normalizedAlias) ? aliasPart : ` AS ${tableName}`; - const scopeDef = scopeClause(scope); - args.push(...scopeDef.args); - return `${keyword} (SELECT * FROM ${tableName} WHERE ${scopeDef.sql})${usableAlias}`; + const sub = scopedTableSubquery(tableName, scope); + args.push(...sub.args); + return `${keyword} (${sub.sql})${usableAlias}`; }, ); return { sql: rewritten, args }; diff --git a/templates/analytics/server/lib/implementation-blockers-store.ts b/templates/analytics/server/lib/implementation-blockers-store.ts index 53def79662..911fa8346b 100644 --- a/templates/analytics/server/lib/implementation-blockers-store.ts +++ b/templates/analytics/server/lib/implementation-blockers-store.ts @@ -7,6 +7,7 @@ import { import { and, asc, eq, isNull, or } from "drizzle-orm"; import { getDb, schema } from "../db/index.js"; +import { assertCanManageOrgRoster } from "./org-write-guard.js"; /** * Org-scoped store for "Implementation Blockers". Customer-specific blocker @@ -140,6 +141,7 @@ export async function replaceImplementationBlockers( blockers: BlockerInput[], ctx: AccessCtx, ): Promise { + await assertCanManageOrgRoster(ctx); const db = getDb() as any; const now = nowIso(); const rows = blockers diff --git a/templates/analytics/server/lib/org-write-guard.ts b/templates/analytics/server/lib/org-write-guard.ts new file mode 100644 index 0000000000..3887e11fb9 --- /dev/null +++ b/templates/analytics/server/lib/org-write-guard.ts @@ -0,0 +1,32 @@ +import { getDbExec } from "@agent-native/core/db"; + +/** + * Guards destructive bulk writes (seed/replace) on the org-shared curated + * tables (strategic accounts, coverage contacts, implementation blockers). + * + * Per-row edits already go through `assertAccess(..., "editor", ...)`, but the + * atomic replace path wipes and rewrites the WHOLE scope. Without this check + * any ordinary org member could erase the shared roster. We require an org + * owner/admin for org-scoped writes; in solo mode (no org) the caller only ever + * touches their own org-less rows, so authentication alone is sufficient. + */ +export async function assertCanManageOrgRoster(ctx: { + email: string; + orgId: string | null; +}): Promise { + if (!ctx.email) throw new Error("no authenticated user"); + // Solo / org-less scope: the writable scope is the caller's own rows only. + if (!ctx.orgId) return; + + const exec = getDbExec(); + const { rows } = await exec.execute({ + sql: `SELECT role FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`, + args: [ctx.orgId, ctx.email.toLowerCase()], + }); + const role = rows[0]?.role ? String(rows[0].role) : null; + if (role !== "owner" && role !== "admin") { + throw new Error( + "Only organization owners or admins can seed or replace shared roster data.", + ); + } +} diff --git a/templates/analytics/server/lib/strategic-account-contacts-store.ts b/templates/analytics/server/lib/strategic-account-contacts-store.ts index afa0aa7000..f0ed34d8e2 100644 --- a/templates/analytics/server/lib/strategic-account-contacts-store.ts +++ b/templates/analytics/server/lib/strategic-account-contacts-store.ts @@ -7,6 +7,7 @@ import { import { and, asc, eq, isNull, or } from "drizzle-orm"; import { getDb, schema } from "../db/index.js"; +import { assertCanManageOrgRoster } from "./org-write-guard.js"; /** * Org-scoped store for "Strategic Account Coverage" contacts. Sensitive contact @@ -153,6 +154,7 @@ export async function replaceStrategicAccountContacts( contacts: ContactInput[], ctx: AccessCtx, ): Promise { + await assertCanManageOrgRoster(ctx); const db = getDb() as any; const now = nowIso(); const rows = contacts diff --git a/templates/analytics/server/lib/strategic-accounts-store.ts b/templates/analytics/server/lib/strategic-accounts-store.ts index 498f49e0a1..474c5b3fdd 100644 --- a/templates/analytics/server/lib/strategic-accounts-store.ts +++ b/templates/analytics/server/lib/strategic-accounts-store.ts @@ -7,6 +7,7 @@ import { import { and, asc, eq, isNull, or } from "drizzle-orm"; import { getDb, schema } from "../db/index.js"; +import { assertCanManageOrgRoster } from "./org-write-guard.js"; /** * Org-scoped store for the curated Strategic Accounts roster. The list is the @@ -136,6 +137,7 @@ export async function replaceStrategicAccounts( accounts: StrategicAccountInput[], ctx: AccessCtx, ): Promise { + await assertCanManageOrgRoster(ctx); const db = getDb() as any; const now = nowIso(); const rows = accounts From 278ca1635f82c78e132e2b75bd58ff5c398ec090 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Fri, 26 Jun 2026 20:32:52 +0000 Subject: [PATCH 12/20] fix: format code to pass pnpm fmt:check validation --- templates/analytics/server/lib/app-sql.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/templates/analytics/server/lib/app-sql.ts b/templates/analytics/server/lib/app-sql.ts index d75ea0ca93..cfb9d11a41 100644 --- a/templates/analytics/server/lib/app-sql.ts +++ b/templates/analytics/server/lib/app-sql.ts @@ -39,10 +39,7 @@ const MAX_QUERY_ROWS = 5_000; * uses — honoring visibility (private/org/public) and explicit share rows, not * just owner/org_id. */ -const ALLOWED_TABLES_MAP: Record< - string, - { table: any; shares: any } -> = { +const ALLOWED_TABLES_MAP: Record = { strategic_accounts: { table: schema.strategicAccounts, shares: schema.strategicAccountShares, @@ -142,9 +139,10 @@ function extractTableRefs(stripped: string): string[] { // Derived table / subquery; its inner FROM is matched by the outer scan. continue; } - const idMatch = /^[A-Za-z_][A-Za-z0-9_$]*(?:\.[A-Za-z_][A-Za-z0-9_$]*)*/.exec( - stripped.slice(i), - ); + const idMatch = + /^[A-Za-z_][A-Za-z0-9_$]*(?:\.[A-Za-z_][A-Za-z0-9_$]*)*/.exec( + stripped.slice(i), + ); if (!idMatch) continue; const ident = idMatch[0]; if (CLAUSE_STOP_WORDS.has(ident.toLowerCase())) continue; @@ -247,7 +245,10 @@ function scopedTableSubquery( orgId: scope.orgId ?? undefined, }); const compiled = db.select().from(entry.table).where(where).toSQL(); - return { sql: compiled.sql as string, args: (compiled.params ?? []) as unknown[] }; + return { + sql: compiled.sql as string, + args: (compiled.params ?? []) as unknown[], + }; } function scopedAppSql( From b3c1e0053bf187ec59c14dd58b317640e1b68b4a Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Fri, 26 Jun 2026 22:01:04 +0000 Subject: [PATCH 13/20] Remove strategic accounts roster generation action --- ...generate-strategic-accounts-roster.spec.ts | 191 ----------- .../generate-strategic-accounts-roster.ts | 182 ---------- .../actions/list-implementation-blockers.ts | 34 -- .../list-strategic-account-contacts.ts | 34 -- .../actions/list-strategic-accounts.ts | 45 --- .../actions/strategic-accounts.spec.ts | 119 ------- .../analytics/actions/update-dashboard.ts | 15 +- .../actions/update-implementation-blocker.ts | 39 --- .../update-strategic-account-contact.ts | 41 --- .../actions/update-strategic-account.ts | 35 -- .../actions/upsert-implementation-blockers.ts | 51 --- .../upsert-strategic-account-contacts.ts | 53 --- .../actions/upsert-strategic-accounts.ts | 50 --- .../app/components/dashboard/SqlChart.tsx | 77 ----- .../app/components/layout/Sidebar.tsx | 59 +--- templates/analytics/app/i18n-data.ts | 10 - templates/analytics/app/i18n/zh-TW.ts | 1 - templates/analytics/app/lib/format-sql.ts | 1 - .../adhoc/sql-dashboard/PanelEditorDialog.tsx | 2 - .../adhoc/sql-dashboard/ViewSqlPopover.tsx | 1 - .../app/pages/adhoc/sql-dashboard/index.tsx | 33 +- .../app/pages/adhoc/sql-dashboard/types.ts | 8 - templates/analytics/data/app.db.premerge-bak | Bin 1974272 -> 0 bytes templates/analytics/server/db/index.ts | 36 -- templates/analytics/server/db/schema.ts | 91 ----- .../analytics/server/handlers/sql-query.ts | 2 +- .../analytics/server/lib/app-sql.spec.ts | 99 ------ templates/analytics/server/lib/app-sql.ts | 314 ------------------ .../server/lib/dashboard-panel-query.ts | 9 - .../lib/implementation-blockers-store.ts | 226 ------------- .../analytics/server/lib/org-write-guard.ts | 32 -- .../lib/strategic-account-contacts-store.ts | 247 -------------- .../server/lib/strategic-accounts-store.ts | 232 ------------- templates/analytics/server/plugins/db.ts | 118 ------- 34 files changed, 12 insertions(+), 2475 deletions(-) delete mode 100644 templates/analytics/actions/generate-strategic-accounts-roster.spec.ts delete mode 100644 templates/analytics/actions/generate-strategic-accounts-roster.ts delete mode 100644 templates/analytics/actions/list-implementation-blockers.ts delete mode 100644 templates/analytics/actions/list-strategic-account-contacts.ts delete mode 100644 templates/analytics/actions/list-strategic-accounts.ts delete mode 100644 templates/analytics/actions/strategic-accounts.spec.ts delete mode 100644 templates/analytics/actions/update-implementation-blocker.ts delete mode 100644 templates/analytics/actions/update-strategic-account-contact.ts delete mode 100644 templates/analytics/actions/update-strategic-account.ts delete mode 100644 templates/analytics/actions/upsert-implementation-blockers.ts delete mode 100644 templates/analytics/actions/upsert-strategic-account-contacts.ts delete mode 100644 templates/analytics/actions/upsert-strategic-accounts.ts delete mode 100644 templates/analytics/data/app.db.premerge-bak delete mode 100644 templates/analytics/server/lib/app-sql.spec.ts delete mode 100644 templates/analytics/server/lib/app-sql.ts delete mode 100644 templates/analytics/server/lib/implementation-blockers-store.ts delete mode 100644 templates/analytics/server/lib/org-write-guard.ts delete mode 100644 templates/analytics/server/lib/strategic-account-contacts-store.ts delete mode 100644 templates/analytics/server/lib/strategic-accounts-store.ts diff --git a/templates/analytics/actions/generate-strategic-accounts-roster.spec.ts b/templates/analytics/actions/generate-strategic-accounts-roster.spec.ts deleted file mode 100644 index fe5493f597..0000000000 --- a/templates/analytics/actions/generate-strategic-accounts-roster.spec.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const mocks = vi.hoisted(() => ({ - runQuery: vi.fn(), - replaceStrategicAccounts: vi.fn(), - listStrategicAccounts: vi.fn(), - getDashboard: vi.fn(), - upsertDashboard: vi.fn(), - orgId: null as string | null, - email: "alice@example.com" as string | null, -})); - -vi.mock("@agent-native/core", async (importOriginal) => { - const actual = await importOriginal(); - return { ...actual }; -}); - -vi.mock("@agent-native/core/server", () => ({ - buildDeepLink: vi.fn( - ({ app, view }: { app: string; view: string }) => `/${app}/${view}`, - ), - getRequestOrgId: () => mocks.orgId, - getRequestUserEmail: () => mocks.email, -})); - -vi.mock("../server/lib/bigquery", () => ({ runQuery: mocks.runQuery })); -vi.mock("../server/lib/strategic-accounts-store", () => ({ - replaceStrategicAccounts: mocks.replaceStrategicAccounts, - listStrategicAccounts: mocks.listStrategicAccounts, -})); -vi.mock("../server/lib/dashboards-store", () => ({ - getDashboard: mocks.getDashboard, - upsertDashboard: mocks.upsertDashboard, -})); - -const { default: generateAction } = - await import("./generate-strategic-accounts-roster"); - -beforeEach(() => { - mocks.runQuery.mockReset(); - mocks.replaceStrategicAccounts.mockReset(); - mocks.listStrategicAccounts.mockReset(); - mocks.listStrategicAccounts.mockResolvedValue([]); - mocks.getDashboard.mockReset(); - mocks.upsertDashboard.mockReset(); - mocks.orgId = null; - mocks.email = "alice@example.com"; -}); - -describe("generate-strategic-accounts-roster", () => { - it("ranks warehouse rows, writes the roster, and syncs the dashboard variable", async () => { - mocks.runQuery.mockResolvedValue({ - rows: [ - { company_name: "Acme", active_users: 50 }, - { company_name: "Globex", active_users: 20 }, - { company_name: " ", active_users: 5 }, - ], - }); - mocks.replaceStrategicAccounts.mockImplementation(async (rows: any[]) => - rows.map((r, i) => ({ id: String(i), ...r })), - ); - mocks.getDashboard.mockResolvedValue({ - kind: "sql", - config: { name: "Strategic Accounts", variables: { accounts: "old" } }, - }); - mocks.upsertDashboard.mockResolvedValue(undefined); - - const result = (await generateAction.run({})) as { - ok: boolean; - count: number; - accountsPipe: string; - dashboardSynced: boolean; - }; - - expect(result.ok).toBe(true); - expect(result.count).toBe(2); - expect(result.accountsPipe).toBe("Acme|Globex"); - expect(result.dashboardSynced).toBe(true); - // Roster written with sortOrder reflecting rank, blanks dropped. - expect(mocks.replaceStrategicAccounts).toHaveBeenCalledWith( - [ - { - companyName: "Acme", - sortOrder: 0, - companyId: null, - deploymentStatus: "", - notes: "", - }, - { - companyName: "Globex", - sortOrder: 1, - companyId: null, - deploymentStatus: "", - notes: "", - }, - ], - { email: "alice@example.com", orgId: null }, - ); - // Dashboard variable updated to the new pipe list. - expect(mocks.upsertDashboard).toHaveBeenCalledWith( - "strategic-accounts", - "sql", - expect.objectContaining({ - variables: expect.objectContaining({ accounts: "Acme|Globex" }), - }), - { email: "alice@example.com", orgId: null }, - ); - }); - - it("preserves manually-curated metadata for companies that already exist", async () => { - mocks.runQuery.mockResolvedValue({ - rows: [ - { company_name: "Acme", active_users: 50 }, - { company_name: "Globex", active_users: 20 }, - ], - }); - mocks.listStrategicAccounts.mockResolvedValue([ - { - id: "x", - companyName: "Acme", - companyId: "org_123", - deploymentStatus: "live", - notes: "key account", - sortOrder: 5, - }, - ]); - mocks.replaceStrategicAccounts.mockImplementation(async (rows: any[]) => - rows.map((r, i) => ({ id: String(i), ...r })), - ); - mocks.getDashboard.mockResolvedValue(null); - - await generateAction.run({}); - - expect(mocks.replaceStrategicAccounts).toHaveBeenCalledWith( - [ - { - companyName: "Acme", - sortOrder: 0, - companyId: "org_123", - deploymentStatus: "live", - notes: "key account", - }, - { - companyName: "Globex", - sortOrder: 1, - companyId: null, - deploymentStatus: "", - notes: "", - }, - ], - { email: "alice@example.com", orgId: null }, - ); - }); - - it("dryRun previews without writing", async () => { - mocks.runQuery.mockResolvedValue({ - rows: [{ company_name: "Acme", active_users: 9 }], - }); - const result = (await generateAction.run({ dryRun: true })) as { - dryRun: boolean; - count: number; - }; - expect(result.dryRun).toBe(true); - expect(result.count).toBe(1); - expect(mocks.replaceStrategicAccounts).not.toHaveBeenCalled(); - expect(mocks.upsertDashboard).not.toHaveBeenCalled(); - }); - - it("succeeds even when the dashboard doesn't exist yet", async () => { - mocks.runQuery.mockResolvedValue({ - rows: [{ company_name: "Acme", active_users: 9 }], - }); - mocks.replaceStrategicAccounts.mockResolvedValue([ - { id: "0", companyName: "Acme", sortOrder: 0 }, - ]); - mocks.getDashboard.mockResolvedValue(null); - const result = (await generateAction.run({})) as { - ok: boolean; - dashboardSynced: boolean; - }; - expect(result.ok).toBe(true); - expect(result.dashboardSynced).toBe(false); - }); - - it("throws when unauthenticated", async () => { - mocks.email = null; - await expect(generateAction.run({})).rejects.toThrow( - "no authenticated user", - ); - }); -}); diff --git a/templates/analytics/actions/generate-strategic-accounts-roster.ts b/templates/analytics/actions/generate-strategic-accounts-roster.ts deleted file mode 100644 index a2635d2127..0000000000 --- a/templates/analytics/actions/generate-strategic-accounts-roster.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { defineAction } from "@agent-native/core"; -import { - getRequestOrgId, - getRequestUserEmail, - buildDeepLink, -} from "@agent-native/core/server"; -import { z } from "zod"; - -import { runQuery } from "../server/lib/bigquery"; -import { getDashboard, upsertDashboard } from "../server/lib/dashboards-store"; -import { - listStrategicAccounts, - replaceStrategicAccounts, -} from "../server/lib/strategic-accounts-store"; - -/** - * Derives the Strategic Accounts roster LIVE from the warehouse so a fresh - * deployment can self-populate without any hardcoded account names. Ranks - * enterprise companies by distinct active Fusion users over a recent window, - * takes the top N, writes them to the org-scoped `strategic_accounts` table, - * and syncs the dashboard's `accounts` variable so the metrics panels reflect - * the new roster immediately. - * - * Warehouse tables mirror the existing Strategic Accounts dashboard panels: - * - builder-3b0a2.dbt_mart.enterprise_companies (company_name, root_org_id) - * - builder-3b0a2.amplitude.EVENTS_182198 (event_type, event_time, user_id, - * event_properties.$.rootOrganizationId) - */ - -function clampInt( - raw: number | undefined, - min: number, - max: number, - fallback: number, -): number { - if (typeof raw !== "number" || !Number.isFinite(raw)) return fallback; - const n = Math.floor(raw); - if (n < min) return min; - if (n > max) return max; - return n; -} - -function buildRosterSql(limit: number, windowDays: number): string { - // limit/windowDays are validated integers (clampInt) so they are safe to - // inline — no string interpolation of user text reaches the SQL. - return `WITH usage AS ( - SELECT ec.company_name AS company_name, - COUNT(DISTINCT e.user_id) AS active_users - FROM \`builder-3b0a2.amplitude.EVENTS_182198\` e - JOIN \`builder-3b0a2.dbt_mart.enterprise_companies\` ec - ON ec.root_org_id = JSON_VALUE(e.event_properties, '$.rootOrganizationId') - WHERE e.event_type = 'fusion chat message submitted' - AND DATE(e.event_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL ${windowDays} DAY) - AND ec.company_name IS NOT NULL - AND TRIM(ec.company_name) != '' - GROUP BY company_name -) -SELECT company_name, active_users -FROM usage -ORDER BY active_users DESC, company_name ASC -LIMIT ${limit}`; -} - -async function syncDashboardVariable( - accountsPipe: string, - ctx: { email: string; orgId: string | null }, -): Promise { - try { - const existing = await getDashboard("strategic-accounts", ctx); - if (!existing) return false; - const config = existing.config as Record; - const variables = - config.variables && typeof config.variables === "object" - ? (config.variables as Record) - : {}; - config.variables = { ...variables, accounts: accountsPipe }; - await upsertDashboard("strategic-accounts", existing.kind, config, ctx); - return true; - } catch { - // Best-effort: roster write is the source of truth. If the dashboard - // doesn't exist yet (fresh install before it's created), skip silently. - return false; - } -} - -export default defineAction({ - description: - "Generate the Strategic Accounts roster LIVE from the warehouse (no hardcoding). Ranks enterprise companies by distinct active Fusion users over a recent window, takes the top N, replaces the org's `strategic_accounts` roster in one atomic write, and syncs the dashboard's `accounts` variable. Use this to bootstrap the roster on a fresh deployment or to refresh it. Set `dryRun: true` to preview the ranked list without writing.", - schema: z.object({ - limit: z - .number() - .int() - .min(1) - .max(200) - .optional() - .describe("How many top accounts to include (default 25)."), - windowDays: z - .number() - .int() - .min(1) - .max(365) - .optional() - .describe("Activity window in days for ranking (default 90)."), - dryRun: z - .boolean() - .optional() - .describe("Preview the ranked roster without writing (default false)."), - }), - http: { method: "POST" }, - run: async (args) => { - const orgId = getRequestOrgId() || null; - const email = getRequestUserEmail(); - if (!email) throw new Error("no authenticated user"); - const ctx = { email, orgId }; - - const limit = clampInt(args.limit, 1, 200, 25); - const windowDays = clampInt(args.windowDays, 1, 365, 90); - - const result = await runQuery(buildRosterSql(limit, windowDays)); - const rows = (result?.rows ?? []) as Array>; - const ranked = rows - .map((r) => ({ - companyName: String(r.company_name ?? "").trim(), - activeUsers: Number(r.active_users ?? 0), - })) - .filter((r) => r.companyName !== ""); - - const accountsPipe = ranked.map((r) => r.companyName).join("|"); - - if (args.dryRun) { - return { - ok: true, - dryRun: true, - count: ranked.length, - windowDays, - limit, - accounts: ranked, - accountsPipe, - summary: `Preview: ${ranked.length} account(s) ranked by active Fusion users over ${windowDays}d. Nothing written.`, - }; - } - - // Preserve manually-curated fields (companyId, deploymentStatus, notes) for - // companies that already exist — the warehouse ranking only knows the name, - // so a blind replace would silently wipe human-entered metadata. - const existing = await listStrategicAccounts(ctx); - const priorByName = new Map( - existing.map((a) => [a.companyName.trim().toLowerCase(), a]), - ); - const written = await replaceStrategicAccounts( - ranked.map((r, i) => { - const prior = priorByName.get(r.companyName.toLowerCase()); - return { - companyName: r.companyName, - sortOrder: i, - companyId: prior?.companyId ?? null, - deploymentStatus: prior?.deploymentStatus ?? "", - notes: prior?.notes ?? "", - }; - }), - ctx, - ); - const dashboardSynced = await syncDashboardVariable(accountsPipe, ctx); - - return { - ok: true, - count: written.length, - windowDays, - limit, - dashboardSynced, - accounts: written, - accountsPipe, - summary: `Generated Strategic Accounts roster from the warehouse: ${written.length} account(s) by active Fusion users over ${windowDays}d.${dashboardSynced ? " Dashboard variable synced." : ""}`, - urlPath: "/dashboards/strategic-accounts", - deepLink: buildDeepLink({ - app: "analytics", - view: "adhoc", - params: { dashboardId: "strategic-accounts" }, - }), - }; - }, -}); diff --git a/templates/analytics/actions/list-implementation-blockers.ts b/templates/analytics/actions/list-implementation-blockers.ts deleted file mode 100644 index 3cee7e0f05..0000000000 --- a/templates/analytics/actions/list-implementation-blockers.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { defineAction } from "@agent-native/core"; -import { - getRequestOrgId, - getRequestUserEmail, - buildDeepLink, -} from "@agent-native/core/server"; -import { z } from "zod"; - -import { listImplementationBlockers } from "../server/lib/implementation-blockers-store"; - -export default defineAction({ - description: - "List the curated Implementation Blockers for the current org. This is the source of truth for the Implementation Blockers dashboard — the blocker details live only in the database, never in source. Returns each blocker's company, type, status, summary, and details.", - schema: z.object({}), - http: { method: "GET" }, - readOnly: true, - publicAgent: { expose: true, readOnly: true, requiresAuth: true }, - link: () => ({ - url: buildDeepLink({ - app: "analytics", - view: "adhoc", - params: { dashboardId: "implementation-blockers" }, - }), - label: "Open Implementation Blockers", - view: "adhoc", - }), - run: async () => { - const orgId = getRequestOrgId() || null; - const email = getRequestUserEmail(); - if (!email) throw new Error("no authenticated user"); - const blockers = await listImplementationBlockers({ email, orgId }); - return { count: blockers.length, blockers }; - }, -}); diff --git a/templates/analytics/actions/list-strategic-account-contacts.ts b/templates/analytics/actions/list-strategic-account-contacts.ts deleted file mode 100644 index b2615d5073..0000000000 --- a/templates/analytics/actions/list-strategic-account-contacts.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { defineAction } from "@agent-native/core"; -import { - getRequestOrgId, - getRequestUserEmail, - buildDeepLink, -} from "@agent-native/core/server"; -import { z } from "zod"; - -import { listStrategicAccountContacts } from "../server/lib/strategic-account-contacts-store"; - -export default defineAction({ - description: - "List the curated Strategic Account Coverage contacts for the current org (champions, enablers, exec sponsors). This is the source of truth for the Strategic Account Coverage dashboard — the contact details live only in the database, never in source. Returns each contact's company, role, name, title, email, confidence, and rationale.", - schema: z.object({}), - http: { method: "GET" }, - readOnly: true, - publicAgent: { expose: true, readOnly: true, requiresAuth: true }, - link: () => ({ - url: buildDeepLink({ - app: "analytics", - view: "adhoc", - params: { dashboardId: "strategic-account-coverage" }, - }), - label: "Open Strategic Account Coverage", - view: "adhoc", - }), - run: async () => { - const orgId = getRequestOrgId() || null; - const email = getRequestUserEmail(); - if (!email) throw new Error("no authenticated user"); - const contacts = await listStrategicAccountContacts({ email, orgId }); - return { count: contacts.length, contacts }; - }, -}); diff --git a/templates/analytics/actions/list-strategic-accounts.ts b/templates/analytics/actions/list-strategic-accounts.ts deleted file mode 100644 index ed3a4081fa..0000000000 --- a/templates/analytics/actions/list-strategic-accounts.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { defineAction } from "@agent-native/core"; -import { - getRequestOrgId, - getRequestUserEmail, - buildDeepLink, -} from "@agent-native/core/server"; -import { z } from "zod"; - -import { listStrategicAccounts } from "../server/lib/strategic-accounts-store"; - -export default defineAction({ - description: - "List the curated Strategic Accounts roster for the current org. This is the source of truth for the Strategic Accounts overview dashboard and extension — the account names live only in the database, never in source. Returns each account's company name, optional HubSpot company id, editable deployment status, notes, and sort order.", - schema: z.object({}), - http: { method: "GET" }, - readOnly: true, - publicAgent: { expose: true, readOnly: true, requiresAuth: true }, - link: () => ({ - url: buildDeepLink({ - app: "analytics", - view: "adhoc", - params: { dashboardId: "strategic-accounts" }, - }), - label: "Open Strategic Accounts", - view: "adhoc", - }), - run: async () => { - const orgId = getRequestOrgId() || null; - const email = getRequestUserEmail(); - if (!email) throw new Error("no authenticated user"); - const accounts = await listStrategicAccounts({ email, orgId }); - return { - count: accounts.length, - accounts, - /** Comma-separated names (display only — names may contain commas). */ - accountsCsv: accounts.map((a) => a.companyName).join(","), - /** - * Pipe-separated names — the value to feed the dashboard `accounts` - * variable. Pipe avoids collisions with commas inside company names - * (e.g. "Orgill, Inc."); panels expand it with SPLIT('{{accounts}}', '|'). - */ - accountsPipe: accounts.map((a) => a.companyName).join("|"), - }; - }, -}); diff --git a/templates/analytics/actions/strategic-accounts.spec.ts b/templates/analytics/actions/strategic-accounts.spec.ts deleted file mode 100644 index f057c802ec..0000000000 --- a/templates/analytics/actions/strategic-accounts.spec.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const mocks = vi.hoisted(() => ({ - listStrategicAccounts: vi.fn(), - replaceStrategicAccounts: vi.fn(), - updateStrategicAccount: vi.fn(), - orgId: null as string | null, - email: "alice@example.com" as string | null, -})); - -vi.mock("@agent-native/core", async (importOriginal) => { - const actual = await importOriginal(); - return { ...actual }; -}); - -vi.mock("@agent-native/core/server", () => ({ - buildDeepLink: vi.fn( - ({ app, view }: { app: string; view: string }) => `/${app}/${view}`, - ), - getRequestOrgId: () => mocks.orgId, - getRequestUserEmail: () => mocks.email, -})); - -vi.mock("../server/lib/strategic-accounts-store", () => ({ - listStrategicAccounts: mocks.listStrategicAccounts, - replaceStrategicAccounts: mocks.replaceStrategicAccounts, - updateStrategicAccount: mocks.updateStrategicAccount, -})); - -const { default: listAction } = await import("./list-strategic-accounts"); -const { default: upsertAction } = await import("./upsert-strategic-accounts"); -const { default: updateAction } = await import("./update-strategic-account"); - -beforeEach(() => { - mocks.listStrategicAccounts.mockReset(); - mocks.replaceStrategicAccounts.mockReset(); - mocks.updateStrategicAccount.mockReset(); - mocks.orgId = null; - mocks.email = "alice@example.com"; -}); - -describe("list-strategic-accounts", () => { - it("returns accounts and a CSV of names for the dashboard variable", async () => { - mocks.listStrategicAccounts.mockResolvedValue([ - { companyName: "Acme" }, - { companyName: "Globex" }, - ]); - const result = (await listAction.run({})) as { - count: number; - accountsCsv: string; - }; - expect(result.count).toBe(2); - expect(result.accountsCsv).toBe("Acme,Globex"); - expect(mocks.listStrategicAccounts).toHaveBeenCalledWith({ - email: "alice@example.com", - orgId: null, - }); - }); - - it("throws when unauthenticated", async () => { - mocks.email = null; - await expect(listAction.run({})).rejects.toThrow("no authenticated user"); - }); -}); - -describe("upsert-strategic-accounts", () => { - it("replaces the roster atomically and reports the new count", async () => { - mocks.replaceStrategicAccounts.mockResolvedValue([ - { id: "1", companyName: "Acme" }, - ]); - const result = (await upsertAction.run({ - accounts: [{ companyName: "Acme" }], - })) as { ok: boolean; count: number }; - expect(result.ok).toBe(true); - expect(result.count).toBe(1); - expect(mocks.replaceStrategicAccounts).toHaveBeenCalledWith( - [{ companyName: "Acme" }], - { email: "alice@example.com", orgId: null }, - ); - }); - - it("accepts a JSON string for accounts", async () => { - mocks.replaceStrategicAccounts.mockResolvedValue([]); - await upsertAction.run({ - accounts: JSON.stringify([{ companyName: "Acme" }]) as any, - }); - expect(mocks.replaceStrategicAccounts).toHaveBeenCalledWith( - [{ companyName: "Acme" }], - expect.anything(), - ); - }); -}); - -describe("update-strategic-account", () => { - it("passes only the patch fields and returns the updated row", async () => { - mocks.updateStrategicAccount.mockResolvedValue({ - id: "1", - deploymentStatus: "Production", - }); - const result = (await updateAction.run({ - id: "1", - deploymentStatus: "Production", - })) as { ok: boolean; account: { deploymentStatus: string } }; - expect(result.ok).toBe(true); - expect(result.account.deploymentStatus).toBe("Production"); - expect(mocks.updateStrategicAccount).toHaveBeenCalledWith( - "1", - { deploymentStatus: "Production" }, - { email: "alice@example.com", orgId: null }, - ); - }); - - it("throws when the row is missing or inaccessible", async () => { - mocks.updateStrategicAccount.mockResolvedValue(null); - await expect( - updateAction.run({ id: "missing", notes: "x" }), - ).rejects.toThrow(/not found/); - }); -}); diff --git a/templates/analytics/actions/update-dashboard.ts b/templates/analytics/actions/update-dashboard.ts index d2dce90ced..22b4ae534c 100644 --- a/templates/analytics/actions/update-dashboard.ts +++ b/templates/analytics/actions/update-dashboard.ts @@ -12,7 +12,6 @@ import { import { z } from "zod"; import { interpolate } from "../app/pages/adhoc/sql-dashboard/interpolate"; -import { validateAppSql } from "../server/lib/app-sql.js"; import { dryRunQuery } from "../server/lib/bigquery"; import { getDashboard, upsertDashboard } from "../server/lib/dashboards-store"; import { parseDemoDescriptor } from "../server/lib/demo-source"; @@ -313,7 +312,6 @@ export function validateDashboardConfig( "ga4", "amplitude", "first-party", - "app", "demo", "prometheus", ]); @@ -347,7 +345,7 @@ export function validateDashboardConfig( } } if (!isSection && !validSources.has(p.source as string)) { - return `panel[${i}].source must be 'bigquery', 'ga4', 'amplitude', 'first-party', 'app', 'demo', or 'prometheus' (got '${p.source}'). source selects the backend — put the PromQL/SQL/table name in sql, not here.`; + return `panel[${i}].source must be 'bigquery', 'ga4', 'amplitude', 'first-party', 'demo', or 'prometheus' (got '${p.source}'). source selects the backend — put the PromQL/SQL/table name in sql, not here.`; } if ( isSection && @@ -415,17 +413,6 @@ export async function validatePanelSql( } continue; } - if (p.source === "app") { - const raw = typeof p.sql === "string" ? p.sql : ""; - if (raw.trim()) { - try { - validateAppSql(interpolate(raw, vars)); - } catch (e: any) { - return `panel[${i}] "${p.title || p.id}" app SQL is invalid: ${e?.message ?? e}`; - } - } - continue; - } if (p.source !== "bigquery") continue; const raw = typeof p.sql === "string" ? p.sql : ""; if (!raw.trim()) continue; diff --git a/templates/analytics/actions/update-implementation-blocker.ts b/templates/analytics/actions/update-implementation-blocker.ts deleted file mode 100644 index 1726cea3e5..0000000000 --- a/templates/analytics/actions/update-implementation-blocker.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { defineAction } from "@agent-native/core"; -import { - getRequestOrgId, - getRequestUserEmail, -} from "@agent-native/core/server"; -import { z } from "zod"; - -import { updateImplementationBlocker } from "../server/lib/implementation-blockers-store"; - -export default defineAction({ - description: - "Edit one Implementation Blocker (type, status, summary, details, company, or sort order). Requires editor access to the blocker's org.", - schema: z.object({ - id: z.string().min(1).describe("Blocker row id."), - companyName: z.string().optional(), - blockerType: z.string().optional(), - status: z.enum(["active", "monitoring", "resolved"]).optional(), - summary: z.string().optional(), - details: z.string().optional(), - sortOrder: z.number().optional(), - }), - http: { method: "POST" }, - run: async (args) => { - const orgId = getRequestOrgId() || null; - const email = getRequestUserEmail(); - if (!email) throw new Error("no authenticated user"); - const { id, ...patch } = args; - const updated = await updateImplementationBlocker(id, patch, { - email, - orgId, - }); - if (!updated) { - throw new Error( - `implementation blocker "${id}" not found (or you don't have access).`, - ); - } - return { ok: true, blocker: updated }; - }, -}); diff --git a/templates/analytics/actions/update-strategic-account-contact.ts b/templates/analytics/actions/update-strategic-account-contact.ts deleted file mode 100644 index 082dd8e009..0000000000 --- a/templates/analytics/actions/update-strategic-account-contact.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { defineAction } from "@agent-native/core"; -import { - getRequestOrgId, - getRequestUserEmail, -} from "@agent-native/core/server"; -import { z } from "zod"; - -import { updateStrategicAccountContact } from "../server/lib/strategic-account-contacts-store"; - -export default defineAction({ - description: - "Edit one Strategic Account Coverage contact (role, name, title, email, confidence, rationale, company, or sort order). Requires editor access to the contact's org.", - schema: z.object({ - id: z.string().min(1).describe("Contact row id."), - companyName: z.string().optional(), - role: z.enum(["champion", "enabler", "exec_sponsor"]).optional(), - contactName: z.string().optional(), - title: z.string().optional(), - email: z.string().optional(), - confidence: z.enum(["high", "medium", "low"]).optional(), - rationale: z.string().optional(), - sortOrder: z.number().optional(), - }), - http: { method: "POST" }, - run: async (args) => { - const orgId = getRequestOrgId() || null; - const email = getRequestUserEmail(); - if (!email) throw new Error("no authenticated user"); - const { id, ...patch } = args; - const updated = await updateStrategicAccountContact(id, patch, { - email, - orgId, - }); - if (!updated) { - throw new Error( - `coverage contact "${id}" not found (or you don't have access).`, - ); - } - return { ok: true, contact: updated }; - }, -}); diff --git a/templates/analytics/actions/update-strategic-account.ts b/templates/analytics/actions/update-strategic-account.ts deleted file mode 100644 index 00d256d678..0000000000 --- a/templates/analytics/actions/update-strategic-account.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { defineAction } from "@agent-native/core"; -import { - getRequestOrgId, - getRequestUserEmail, -} from "@agent-native/core/server"; -import { z } from "zod"; - -import { updateStrategicAccount } from "../server/lib/strategic-accounts-store"; - -export default defineAction({ - description: - "Edit one Strategic Account's manual fields (deployment status, notes, company name/id, or sort order). Used by the Strategic Accounts extension to persist deployment-status badge edits. Requires editor access to the account's org.", - schema: z.object({ - id: z.string().min(1).describe("Strategic account row id."), - companyName: z.string().optional(), - companyId: z.string().nullish(), - deploymentStatus: z.string().optional(), - notes: z.string().optional(), - sortOrder: z.number().optional(), - }), - http: { method: "POST" }, - run: async (args) => { - const orgId = getRequestOrgId() || null; - const email = getRequestUserEmail(); - if (!email) throw new Error("no authenticated user"); - const { id, ...patch } = args; - const updated = await updateStrategicAccount(id, patch, { email, orgId }); - if (!updated) { - throw new Error( - `strategic account "${id}" not found (or you don't have access).`, - ); - } - return { ok: true, account: updated }; - }, -}); diff --git a/templates/analytics/actions/upsert-implementation-blockers.ts b/templates/analytics/actions/upsert-implementation-blockers.ts deleted file mode 100644 index 9aa6629db8..0000000000 --- a/templates/analytics/actions/upsert-implementation-blockers.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { defineAction } from "@agent-native/core"; -import { - getRequestOrgId, - getRequestUserEmail, - buildDeepLink, -} from "@agent-native/core/server"; -import { z } from "zod"; - -import { replaceImplementationBlockers } from "../server/lib/implementation-blockers-store"; - -const blockerSchema = z.object({ - companyName: z.string().min(1), - blockerType: z.string().optional(), - status: z.enum(["active", "monitoring", "resolved"]).optional(), - summary: z.string().optional(), - details: z.string().optional(), - sortOrder: z.number().optional(), -}); - -export default defineAction({ - description: - "Seed or replace the org's curated Implementation Blockers in ONE atomic write. Pass the full list — this replaces the org's existing blockers (it does not append). Never hardcode blocker details in source; populate them here. Each blocker needs at least `companyName`; `blockerType`, `status` (active/monitoring/resolved), `summary`, `details`, and `sortOrder` are optional. New rows are created with org visibility.", - schema: z.object({ - blockers: z.preprocess( - (v) => (typeof v === "string" ? JSON.parse(v) : v), - z.array(blockerSchema), - ), - }), - http: { method: "POST" }, - run: async (args) => { - const orgId = getRequestOrgId() || null; - const email = getRequestUserEmail(); - if (!email) throw new Error("no authenticated user"); - const rows = await replaceImplementationBlockers(args.blockers, { - email, - orgId, - }); - return { - ok: true, - count: rows.length, - blockers: rows, - summary: `Replaced Implementation Blockers; it now has ${rows.length} blocker(s).`, - urlPath: "/dashboards/implementation-blockers", - deepLink: buildDeepLink({ - app: "analytics", - view: "adhoc", - params: { dashboardId: "implementation-blockers" }, - }), - }; - }, -}); diff --git a/templates/analytics/actions/upsert-strategic-account-contacts.ts b/templates/analytics/actions/upsert-strategic-account-contacts.ts deleted file mode 100644 index 2c39c17936..0000000000 --- a/templates/analytics/actions/upsert-strategic-account-contacts.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { defineAction } from "@agent-native/core"; -import { - getRequestOrgId, - getRequestUserEmail, - buildDeepLink, -} from "@agent-native/core/server"; -import { z } from "zod"; - -import { replaceStrategicAccountContacts } from "../server/lib/strategic-account-contacts-store"; - -const contactSchema = z.object({ - companyName: z.string().min(1), - role: z.enum(["champion", "enabler", "exec_sponsor"]).optional(), - contactName: z.string().optional(), - title: z.string().optional(), - email: z.string().optional(), - confidence: z.enum(["high", "medium", "low"]).optional(), - rationale: z.string().optional(), - sortOrder: z.number().optional(), -}); - -export default defineAction({ - description: - "Seed or replace the org's curated Strategic Account Coverage contacts in ONE atomic write. Pass the full list — this replaces the org's existing contacts (it does not append). Never hardcode contact details in source; populate them here. Each contact needs at least `companyName`; `role` (champion/enabler/exec_sponsor), `contactName`, `title`, `email`, `confidence` (high/medium/low), `rationale`, and `sortOrder` are optional. New rows are created with org visibility.", - schema: z.object({ - contacts: z.preprocess( - (v) => (typeof v === "string" ? JSON.parse(v) : v), - z.array(contactSchema), - ), - }), - http: { method: "POST" }, - run: async (args) => { - const orgId = getRequestOrgId() || null; - const email = getRequestUserEmail(); - if (!email) throw new Error("no authenticated user"); - const rows = await replaceStrategicAccountContacts(args.contacts, { - email, - orgId, - }); - return { - ok: true, - count: rows.length, - contacts: rows, - summary: `Replaced Strategic Account Coverage contacts; it now has ${rows.length} contact(s).`, - urlPath: "/dashboards/strategic-account-coverage", - deepLink: buildDeepLink({ - app: "analytics", - view: "adhoc", - params: { dashboardId: "strategic-account-coverage" }, - }), - }; - }, -}); diff --git a/templates/analytics/actions/upsert-strategic-accounts.ts b/templates/analytics/actions/upsert-strategic-accounts.ts deleted file mode 100644 index 11c9b26fcf..0000000000 --- a/templates/analytics/actions/upsert-strategic-accounts.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { defineAction } from "@agent-native/core"; -import { - getRequestOrgId, - getRequestUserEmail, - buildDeepLink, -} from "@agent-native/core/server"; -import { z } from "zod"; - -import { replaceStrategicAccounts } from "../server/lib/strategic-accounts-store"; - -const accountSchema = z.object({ - companyName: z.string().min(1), - companyId: z.string().nullish(), - deploymentStatus: z.string().optional(), - notes: z.string().optional(), - sortOrder: z.number().optional(), -}); - -export default defineAction({ - description: - "Seed or replace the org's curated Strategic Accounts roster in ONE atomic write. Pass the full list of accounts — this replaces the org's existing roster (it does not append). Use this to seed the list from a curated set or from a live warehouse query (e.g. top enterprise accounts); never hardcode account names in source. Each account needs at least `companyName`; `companyId`, `deploymentStatus`, `notes`, and `sortOrder` are optional. New rows are created with org visibility so the whole organization sees the list.", - schema: z.object({ - accounts: z.preprocess( - (v) => (typeof v === "string" ? JSON.parse(v) : v), - z.array(accountSchema), - ), - }), - http: { method: "POST" }, - run: async (args) => { - const orgId = getRequestOrgId() || null; - const email = getRequestUserEmail(); - if (!email) throw new Error("no authenticated user"); - const rows = await replaceStrategicAccounts(args.accounts, { - email, - orgId, - }); - return { - ok: true, - count: rows.length, - accounts: rows, - summary: `Replaced Strategic Accounts roster; it now has ${rows.length} account(s).`, - urlPath: "/dashboards/strategic-accounts", - deepLink: buildDeepLink({ - app: "analytics", - view: "adhoc", - params: { dashboardId: "strategic-accounts" }, - }), - }; - }, -}); diff --git a/templates/analytics/app/components/dashboard/SqlChart.tsx b/templates/analytics/app/components/dashboard/SqlChart.tsx index 440ba324ab..aef18bde5b 100644 --- a/templates/analytics/app/components/dashboard/SqlChart.tsx +++ b/templates/analytics/app/components/dashboard/SqlChart.tsx @@ -12,7 +12,6 @@ import { } from "@tabler/icons-react"; import { createContext, - Fragment, useCallback, useContext, useEffect, @@ -990,10 +989,6 @@ export function SqlChart({ ); } - if (chartType === "cards") { - return withConfigWarning(); - } - if (chartType === "pie") { return withConfigWarning( []; - panel: SqlPanel; -}) { - const config = panel.config; - - const columns = useMemo(() => { - const rowKeys = new Set(Object.keys(rows[0] ?? {})); - if (config?.columns?.length) { - const configured = config.columns.filter( - (c) => !c.hidden && rowKeys.has(c.key), - ); - if (configured.length > 0) return configured; - } - return Object.keys(rows[0] ?? {}).map((key) => ({ key })); - }, [config?.columns, rows]); - - const titleKey = config?.titleKey ?? columns[0]?.key; - const badgeKey = config?.badgeKey; - const limit = config?.limit; - const displayRows = useMemo( - () => (limit != null && rows.length > limit ? rows.slice(0, limit) : rows), - [rows, limit], - ); - - const detailColumns = columns.filter( - (c) => c.key !== titleKey && c.key !== badgeKey, - ); - - return ( -
- {displayRows.map((row, i) => { - const titleCol = columns.find((c) => c.key === titleKey); - const badge = - badgeKey != null ? formatCell(row[badgeKey], undefined) : ""; - return ( -
-
-
- {formatCell(row[titleKey], titleCol?.format)} -
- {badge && ( - - {badge} - - )} -
-
- {detailColumns.map((col) => ( - -
- {col.label ?? col.key} -
-
- {formatCell(row[col.key], col.format)} -
-
- ))} -
-
- ); - })} -
- ); -} - function rowString( row: Record, keys: string[], diff --git a/templates/analytics/app/components/layout/Sidebar.tsx b/templates/analytics/app/components/layout/Sidebar.tsx index ed8d6b31df..4509fe9088 100644 --- a/templates/analytics/app/components/layout/Sidebar.tsx +++ b/templates/analytics/app/components/layout/Sidebar.tsx @@ -51,11 +51,6 @@ type SidebarDashboard = { subviews?: DashboardSubview[]; source: "static" | "sql"; visibility?: Visibility; - /** Parent dashboard id — when set and the parent is present, this dashboard - * renders nested (indented) beneath it in the sidebar. */ - parentId?: string; - /** Nesting depth in the rendered tree (0 = top level, 1 = child). */ - depth?: number; }; import { DevDatabaseLink, @@ -343,7 +338,6 @@ type Visibility = "private" | "org" | "public"; function SortableRow({ id, - depth = 0, favoriteKey, name, href, @@ -362,7 +356,6 @@ function SortableRow({ children, }: { id: string; - depth?: number; favoriteKey: string; name: string; href: string; @@ -529,18 +522,9 @@ function SortableRow({ return (
0 ? { marginInlineStart: depth * 14 } : null), - }} + style={style} className="group/item relative min-w-0" > - {depth > 0 && ( - - )} ) : null} - {/* Embedded extension — renders in place of panels when configured */} - {dashboard.embedExtensionId ? ( - - ) : null} - {/* Tabs */} - {!dashboard.embedExtensionId && tabs.length > 0 && activeTab && ( + {tabs.length > 0 && activeTab && (
{groupedTabs.hasNestedTabs && groupedTabs.groups.length > 1 ? ( 0 && ( - - )} + {dashboard.filters && dashboard.filters.length > 0 && ( + + )} {/* Panels grid */} - {dashboard.embedExtensionId ? null : dashboard.panels.length === 0 ? ( + {dashboard.panels.length === 0 ? (

{t("sqlDashboard.noPanels")}

diff --git a/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts b/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts index 81cbb340ee..238718e958 100644 --- a/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +++ b/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts @@ -3,7 +3,6 @@ export type DataSourceType = | "ga4" | "amplitude" | "first-party" - | "app" | "demo" | "prometheus"; @@ -13,7 +12,6 @@ export type ChartType = | "bar" | "metric" | "table" - | "cards" | "pie" | "section" | "heatmap" @@ -144,12 +142,6 @@ export interface SqlDashboardConfig { * not only at narrow viewports). Defaults to 2. */ columns?: number; - /** - * When set, the dashboard route renders this extension (by id) inline instead - * of the SQL panels — used to surface a rich extension UI at a dashboard URL. - * The id is stored in the dashboard config (data), never hardcoded in source. - */ - embedExtensionId?: string; panels: SqlPanel[]; } diff --git a/templates/analytics/data/app.db.premerge-bak b/templates/analytics/data/app.db.premerge-bak deleted file mode 100644 index 143dc319d08a8cdcfbba4a63f0ae77064cd00080..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1974272 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�iV}_vDlWd!R$HV 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

    xq;m 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 z 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 diff --git a/templates/analytics/server/db/index.ts b/templates/analytics/server/db/index.ts index 80acce2a6f..7d0e0c0760 100644 --- a/templates/analytics/server/db/index.ts +++ b/templates/analytics/server/db/index.ts @@ -37,39 +37,3 @@ registerShareableResource({ requireOrgMemberForUserShares: true, getDb, }); - -registerShareableResource({ - type: "strategic-account", - resourceTable: schema.strategicAccounts, - sharesTable: schema.strategicAccountShares, - displayName: "Strategic Account", - titleColumn: "companyName", - getResourcePath: () => `/dashboards/strategic-accounts`, - allowPublic: false, - requireOrgMemberForUserShares: true, - getDb, -}); - -registerShareableResource({ - type: "strategic-account-contact", - resourceTable: schema.strategicAccountContacts, - sharesTable: schema.strategicAccountContactShares, - displayName: "Strategic Account Contact", - titleColumn: "contactName", - getResourcePath: () => `/dashboards/strategic-account-coverage`, - allowPublic: false, - requireOrgMemberForUserShares: true, - getDb, -}); - -registerShareableResource({ - type: "implementation-blocker", - resourceTable: schema.implementationBlockers, - sharesTable: schema.implementationBlockerShares, - displayName: "Implementation Blocker", - titleColumn: "summary", - getResourcePath: () => `/dashboards/implementation-blockers`, - allowPublic: false, - requireOrgMemberForUserShares: true, - getDb, -}); diff --git a/templates/analytics/server/db/schema.ts b/templates/analytics/server/db/schema.ts index a58113c284..c5b5d99687 100644 --- a/templates/analytics/server/db/schema.ts +++ b/templates/analytics/server/db/schema.ts @@ -113,97 +113,6 @@ export const analyses = table("analyses", { export const analysisShares = createSharesTable("analysis_shares"); -/** - * Curated "Strategic Accounts" list — the source of truth for the migrated - * fusion-analytics Strategic Accounts overview. The (sensitive) account names - * live ONLY in this org-scoped table and the dashboard config row, never in - * source or git. Both surfaces read from it: the extension reads it live, and - * the SQL dashboard's `accounts` variable is projected from it. - * - * `deploymentStatus` and `notes` are editable manual fields (the source kept - * these in localStorage); everything else is the curated roster. Metrics are - * always queried live from BigQuery, never stored here. - */ -export const strategicAccounts = table("strategic_accounts", { - id: text("id").primaryKey(), - /** HubSpot company name — the join key against warehouse tables. */ - companyName: text("company_name").notNull(), - /** Optional HubSpot company id when known. */ - companyId: text("company_id"), - /** Editable deployment-status badge (e.g. "Production", "Pilot"). */ - deploymentStatus: text("deployment_status").notNull().default(""), - /** Editable free-text note. */ - notes: text("notes").notNull().default(""), - /** Manual ordering for the grid. */ - sortOrder: integer("sort_order").notNull().default(0), - createdAt: text("created_at").notNull().default(now()), - updatedAt: text("updated_at").notNull().default(now()), - ...ownableColumns(), -}); - -export const strategicAccountShares = createSharesTable( - "strategic_account_shares", -); - -/** - * Curated "Strategic Account Coverage" contacts — the source of truth for the - * migrated fusion-analytics coverage child dashboard. Each row is one - * stakeholder for an account in a given role (champion / enabler / exec - * sponsor). Sensitive contact details (names, emails) live ONLY in this - * org-scoped table, never in source/git. The dashboard reads it via the `app` - * panel source. - */ -export const strategicAccountContacts = table("strategic_account_contacts", { - id: text("id").primaryKey(), - /** Company name — the join key against the curated roster. */ - companyName: text("company_name").notNull(), - /** Coverage role: "champion" | "enabler" | "exec_sponsor". */ - role: text("role").notNull().default("champion"), - contactName: text("contact_name").notNull().default(""), - title: text("title").notNull().default(""), - email: text("email").notNull().default(""), - /** Confidence in this assignment: "high" | "medium" | "low". */ - confidence: text("confidence").notNull().default("medium"), - /** Why this person fits the role. */ - rationale: text("rationale").notNull().default(""), - sortOrder: integer("sort_order").notNull().default(0), - createdAt: text("created_at").notNull().default(now()), - updatedAt: text("updated_at").notNull().default(now()), - ...ownableColumns(), -}); - -export const strategicAccountContactShares = createSharesTable( - "strategic_account_contact_shares", -); - -/** - * Curated "Implementation Blockers" — the source of truth for the migrated - * fusion-analytics blockers child dashboard. Each row is one blocker affecting - * an account. Customer-specific details live ONLY in this org-scoped table, - * never in source/git. The dashboard reads it via the `app` panel source. - */ -export const implementationBlockers = table("implementation_blockers", { - id: text("id").primaryKey(), - /** Company name affected by the blocker. */ - companyName: text("company_name").notNull(), - /** Blocker category (e.g. "git-integration", "security-vpn-network"). */ - blockerType: text("blocker_type").notNull().default(""), - /** Lifecycle status: "active" | "monitoring" | "resolved". */ - status: text("status").notNull().default("active"), - /** Short headline for the blocker. */ - summary: text("summary").notNull().default(""), - /** Longer free-text detail. */ - details: text("details").notNull().default(""), - sortOrder: integer("sort_order").notNull().default(0), - createdAt: text("created_at").notNull().default(now()), - updatedAt: text("updated_at").notNull().default(now()), - ...ownableColumns(), -}); - -export const implementationBlockerShares = createSharesTable( - "implementation_blocker_shares", -); - /** * BigQuery result cache (pre-existing — moved here from db plugin so a * single drizzle schema covers the template). diff --git a/templates/analytics/server/handlers/sql-query.ts b/templates/analytics/server/handlers/sql-query.ts index b617c47b9e..0690b5aef0 100644 --- a/templates/analytics/server/handlers/sql-query.ts +++ b/templates/analytics/server/handlers/sql-query.ts @@ -19,7 +19,7 @@ export const handleSqlQuery = defineEventHandler(async (event) => { setResponseStatus(event, 400); return { error: - "Invalid source. Must be 'bigquery', 'ga4', 'amplitude', 'first-party', 'app', 'demo', or 'prometheus'", + "Invalid source. Must be 'bigquery', 'ga4', 'amplitude', 'first-party', 'demo', or 'prometheus'", }; } diff --git a/templates/analytics/server/lib/app-sql.spec.ts b/templates/analytics/server/lib/app-sql.spec.ts deleted file mode 100644 index b4aeb2cd27..0000000000 --- a/templates/analytics/server/lib/app-sql.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { describe, it, expect } from "vitest"; - -import { validateAppSql } from "./app-sql"; - -describe("validateAppSql", () => { - it("accepts a single SELECT over an allowed table", () => { - expect(() => - validateAppSql( - "SELECT COUNT(*) AS value FROM strategic_account_contacts", - ), - ).not.toThrow(); - }); - - it("accepts WITH/CTE that reads an allowed table", () => { - expect(() => - validateAppSql( - "WITH t AS (SELECT * FROM implementation_blockers) SELECT COUNT(*) AS value FROM t", - ), - ).not.toThrow(); - }); - - it("rejects non-whitelisted tables", () => { - expect(() => validateAppSql("SELECT * FROM secrets")).toThrow( - /can only read/i, - ); - }); - - it("rejects write statements", () => { - expect(() => - validateAppSql("DELETE FROM strategic_account_contacts"), - ).toThrow(/SELECT or WITH/i); - }); - - it("rejects an UPDATE that targets an allowed table", () => { - expect(() => - validateAppSql("UPDATE strategic_account_contacts SET email = 'x@y.z'"), - ).toThrow(); - }); - - it("rejects multiple statements", () => { - expect(() => - validateAppSql( - "SELECT * FROM strategic_accounts; DROP TABLE strategic_accounts", - ), - ).toThrow(/single SELECT/i); - }); - - it("requires reading from at least one allowed table", () => { - expect(() => validateAppSql("SELECT 1 AS value")).toThrow( - /must read from/i, - ); - }); - - it("accepts an explicit JOIN between two allowed tables", () => { - expect(() => - validateAppSql( - "SELECT sa.company_name FROM strategic_accounts sa JOIN implementation_blockers b ON b.company_name = sa.company_name", - ), - ).not.toThrow(); - }); - - it("rejects a comma join that smuggles in a non-allowed table", () => { - expect(() => - validateAppSql("SELECT * FROM strategic_accounts, dashboards"), - ).toThrow(/comma joins/i); - }); - - it("rejects a comma join even between two allowed tables", () => { - expect(() => - validateAppSql( - "SELECT * FROM strategic_accounts, implementation_blockers", - ), - ).toThrow(/comma joins/i); - }); - - it("rejects double-quoted table identifiers", () => { - expect(() => - validateAppSql('SELECT * FROM strategic_accounts JOIN "user" ON 1=1'), - ).toThrow(/quoted identifiers/i); - }); - - it("rejects backtick-quoted table identifiers", () => { - expect(() => validateAppSql("SELECT * FROM `secrets`")).toThrow( - /quoted identifiers/i, - ); - }); - - it("rejects bracket-quoted table identifiers", () => { - expect(() => validateAppSql("SELECT * FROM [secrets]")).toThrow( - /quoted identifiers/i, - ); - }); - - it("rejects schema-qualified table names", () => { - expect(() => - validateAppSql("SELECT * FROM main.strategic_accounts"), - ).toThrow(/schema-qualified/i); - }); -}); diff --git a/templates/analytics/server/lib/app-sql.ts b/templates/analytics/server/lib/app-sql.ts deleted file mode 100644 index cfb9d11a41..0000000000 --- a/templates/analytics/server/lib/app-sql.ts +++ /dev/null @@ -1,314 +0,0 @@ -import { getDbExec } from "@agent-native/core/db"; -import { accessFilter } from "@agent-native/core/sharing"; - -import { getDb, schema } from "../db/index.js"; - -/** - * The `app` panel data source: lets SQL dashboards read the app's OWN - * org-scoped curated tables (not BigQuery/GA4/etc). This is what makes the - * migrated "Strategic Account Coverage" and "Implementation Blockers" children - * genuinely SQL-backed without copying sensitive data into source or pushing it - * to the warehouse — the data lives only in these local org-scoped tables. - * - * Security model mirrors first-party analytics: only a single read-only SELECT - * is allowed, only whitelisted tables may be referenced, and every whitelisted - * table reference is rewritten into a scoped subquery so a caller can only ever - * read their own org's (or, org-less, their own) rows. - */ - -export interface AppQueryScope { - userEmail: string; - orgId: string | null; -} - -export interface AppQueryResult { - rows: Record[]; - schema: { name: string; type: string }[]; -} - -const MAX_QUERY_ROWS = 5_000; - -/** - * Tables a dashboard panel may read via the `app` source. Every table here - * MUST expose ownableColumns (owner_email, org_id) so the scope filter below is - * valid — do not add a table without those columns. - */ -/** - * Maps each readable table to its drizzle table + shares table so the scoped - * subquery can be built from the SAME `accessFilter` the store/action layer - * uses — honoring visibility (private/org/public) and explicit share rows, not - * just owner/org_id. - */ -const ALLOWED_TABLES_MAP: Record = { - strategic_accounts: { - table: schema.strategicAccounts, - shares: schema.strategicAccountShares, - }, - strategic_account_contacts: { - table: schema.strategicAccountContacts, - shares: schema.strategicAccountContactShares, - }, - implementation_blockers: { - table: schema.implementationBlockers, - shares: schema.implementationBlockerShares, - }, -}; - -const ALLOWED_TABLES = new Set(Object.keys(ALLOWED_TABLES_MAP)); - -/** Keywords that can follow a table spec and end the FROM/JOIN table list. */ -const CLAUSE_STOP_WORDS = new Set([ - "where", - "group", - "order", - "limit", - "having", - "union", - "except", - "intersect", - "on", - "using", - "join", - "left", - "right", - "inner", - "outer", - "cross", - "full", - "natural", - "select", - "window", - "returning", - "as", - "and", - "or", -]); - -const RESERVED_ALIAS_WORDS = new Set([ - "where", - "on", - "group", - "order", - "limit", - "join", - "left", - "right", - "inner", - "outer", - "cross", - "full", - "having", - "union", -]); - -/** Blank out string literals and comments so they can't hide table refs. */ -function stripSqlLiterals(sql: string): string { - return sql - .replace(/'(?:[^']|'')*'/g, "''") - .replace(/--[^\n]*/g, " ") - .replace(/\/\*[\s\S]*?\*\//g, " "); -} - -/** Advance past a balanced parenthesized group starting at `open` ('('). */ -function skipBalancedParens(text: string, open: number): number { - let depth = 0; - for (let i = open; i < text.length; i++) { - if (text[i] === "(") depth++; - else if (text[i] === ")") { - depth--; - if (depth === 0) return i + 1; - } - } - return text.length; -} - -/** - * Extract every table identifier that appears in a FROM/JOIN table position, - * including comma-separated lists. Derived tables (subqueries) are skipped here - * because their inner FROM/JOIN is matched independently. Throws on - * comma-style joins, which the scoping rewriter cannot safely rewrite. - */ -function extractTableRefs(stripped: string): string[] { - const refs: string[] = []; - const kw = /\b(from|join)\b/gi; - let m: RegExpExecArray | null; - while ((m = kw.exec(stripped))) { - let i = m.index + m[0].length; - while (i < stripped.length && /\s/.test(stripped[i])) i++; - if (stripped[i] === "(") { - // Derived table / subquery; its inner FROM is matched by the outer scan. - continue; - } - const idMatch = - /^[A-Za-z_][A-Za-z0-9_$]*(?:\.[A-Za-z_][A-Za-z0-9_$]*)*/.exec( - stripped.slice(i), - ); - if (!idMatch) continue; - const ident = idMatch[0]; - if (CLAUSE_STOP_WORDS.has(ident.toLowerCase())) continue; - refs.push(ident); - i += ident.length; - - // Optional alias: [AS] (unless it's a clause keyword). - while (i < stripped.length && /\s/.test(stripped[i])) i++; - const aliasMatch = /^(?:as\s+)?[A-Za-z_][A-Za-z0-9_$]*/i.exec( - stripped.slice(i), - ); - if (aliasMatch) { - const aliasWord = aliasMatch[0].replace(/^as\s+/i, "").toLowerCase(); - if (!CLAUSE_STOP_WORDS.has(aliasWord)) i += aliasMatch[0].length; - } - while (i < stripped.length && /\s/.test(stripped[i])) i++; - if (stripped[i] === ",") { - throw new Error( - "Comma joins are not allowed in dashboard SQL; use an explicit JOIN", - ); - } - } - return refs; -} - -export function validateAppSql(sql: string): void { - const stripped = stripSqlLiterals(sql).trim(); - const lowered = stripped.toLowerCase(); - if (!/^(select|with)\b/.test(lowered)) { - throw new Error("App queries must start with SELECT or WITH"); - } - if (stripped.includes(";")) { - throw new Error("Only a single SELECT statement is allowed"); - } - if ( - /\b(insert|update|delete|drop|alter|truncate|create|replace|pragma|attach|detach|vacuum|grant|revoke)\b/i.test( - stripped, - ) - ) { - throw new Error("Only read-only SELECT queries are allowed"); - } - if (stripped.includes("?") || /\$\d+\b/.test(stripped)) { - throw new Error("Bind placeholders are not supported in dashboard SQL"); - } - // Quoted identifiers ("user", `user`, [user]) could smuggle in tables that - // the plain-identifier allow-list never inspects. Our curated tables/columns - // are all bare snake_case, so reject quoting outright. - if (/["`\[\]]/.test(stripped)) { - throw new Error("Quoted identifiers are not allowed in dashboard SQL"); - } - - const cteNames = new Set(); - const cteRe = /\b([a-zA-Z_][a-zA-Z0-9_]*)\s+as\s*\(/gi; - for (const match of stripped.matchAll(cteRe)) { - cteNames.add(match[1].toLowerCase()); - } - - let usesAllowed = false; - for (const ref of extractTableRefs(stripped)) { - const lower = ref.toLowerCase(); - if (lower.includes(".")) { - throw new Error( - `Schema-qualified table names are not allowed (found ${ref})`, - ); - } - if (ALLOWED_TABLES.has(lower)) { - usesAllowed = true; - continue; - } - if (cteNames.has(lower)) continue; - throw new Error( - `App queries can only read ${[...ALLOWED_TABLES].join(", ")} (found ${ref})`, - ); - } - if (!usesAllowed) { - throw new Error( - `Query must read from one of: ${[...ALLOWED_TABLES].join(", ")}`, - ); - } -} - -/** - * Build the scoped subquery for one allowed table by compiling the SAME - * `accessFilter` predicate the store layer uses. This guarantees app-source - * dashboard reads honor each row's visibility AND explicit user/org shares - * (and the resource's org-only policy) — not just owner_email/org_id. - */ -function scopedTableSubquery( - tableName: string, - scope: AppQueryScope, -): { sql: string; args: unknown[] } { - const entry = ALLOWED_TABLES_MAP[tableName.toLowerCase()]; - if (!entry) { - // Should never happen: validateAppSql already rejected unknown tables. - throw new Error(`Table not readable via app source: ${tableName}`); - } - const db = getDb() as any; - const where = accessFilter(entry.table, entry.shares, { - userEmail: scope.userEmail, - orgId: scope.orgId ?? undefined, - }); - const compiled = db.select().from(entry.table).where(where).toSQL(); - return { - sql: compiled.sql as string, - args: (compiled.params ?? []) as unknown[], - }; -} - -function scopedAppSql( - sql: string, - scope: AppQueryScope, -): { sql: string; args: unknown[] } { - const args: unknown[] = []; - const tableAlt = [...ALLOWED_TABLES].join("|"); - const aliasRe = new RegExp( - `\\b(from|join)\\s+(${tableAlt})\\b(\\s+(?:as\\s+)?(?!where\\b|on\\b|group\\b|order\\b|limit\\b|join\\b|left\\b|right\\b|inner\\b|outer\\b|cross\\b|full\\b|having\\b|union\\b)([a-zA-Z_][a-zA-Z0-9_]*))?`, - "gi", - ); - const rewritten = sql.replace( - aliasRe, - (_full, keyword, tableName, aliasPart, alias) => { - const normalizedAlias = - typeof alias === "string" ? alias.toLowerCase() : ""; - const usableAlias = - aliasPart && - normalizedAlias && - !RESERVED_ALIAS_WORDS.has(normalizedAlias) - ? aliasPart - : ` AS ${tableName}`; - const sub = scopedTableSubquery(tableName, scope); - args.push(...sub.args); - return `${keyword} (${sub.sql})${usableAlias}`; - }, - ); - return { sql: rewritten, args }; -} - -function valueType(value: unknown): string { - if (typeof value === "number") return "number"; - if (typeof value === "boolean") return "boolean"; - return "string"; -} - -function inferSchema(rows: Record[]): { - name: string; - type: string; -}[] { - const first = rows.find((row) => row && typeof row === "object"); - if (!first) return []; - return Object.entries(first).map(([name, value]) => ({ - name, - type: valueType(value), - })); -} - -export async function queryAppTables( - sql: string, - scope: AppQueryScope, -): Promise { - validateAppSql(sql); - const scoped = scopedAppSql(sql, scope); - const exec = getDbExec(); - const result = await exec.execute({ - sql: `SELECT * FROM (${scoped.sql}) AS app_query LIMIT ${MAX_QUERY_ROWS}`, - args: scoped.args, - }); - const rows = result.rows as Record[]; - return { rows, schema: inferSchema(rows) }; -} diff --git a/templates/analytics/server/lib/dashboard-panel-query.ts b/templates/analytics/server/lib/dashboard-panel-query.ts index 975a8dd109..eb7e0b872b 100644 --- a/templates/analytics/server/lib/dashboard-panel-query.ts +++ b/templates/analytics/server/lib/dashboard-panel-query.ts @@ -3,7 +3,6 @@ import { resolveCredential } from "@agent-native/core/credentials"; import type { MissingKeyResponse } from "@agent-native/core/server"; import { getUserSegmentation, queryEvents } from "./amplitude"; -import { queryAppTables } from "./app-sql"; import { runQuery } from "./bigquery"; import { runDemoPanel, serializeDemoDescriptorInput } from "./demo-source"; import { queryFirstPartyAnalytics } from "./first-party-analytics"; @@ -18,7 +17,6 @@ export const DASHBOARD_PANEL_SOURCES = [ "ga4", "amplitude", "first-party", - "app", "demo", "prometheus", ] as const; @@ -349,13 +347,6 @@ export async function runDashboardPanelQuery(args: { }); } - if (source === "app") { - return await queryAppTables(query, { - userEmail: ctx.userEmail, - orgId: ctx.orgId ?? null, - }); - } - if (source === "demo") { return await runDemoPanel(query); } diff --git a/templates/analytics/server/lib/implementation-blockers-store.ts b/templates/analytics/server/lib/implementation-blockers-store.ts deleted file mode 100644 index 911fa8346b..0000000000 --- a/templates/analytics/server/lib/implementation-blockers-store.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { recordChange } from "@agent-native/core/server"; -import { - accessFilter, - assertAccess, - resolveAccess, -} from "@agent-native/core/sharing"; -import { and, asc, eq, isNull, or } from "drizzle-orm"; - -import { getDb, schema } from "../db/index.js"; -import { assertCanManageOrgRoster } from "./org-write-guard.js"; - -/** - * Org-scoped store for "Implementation Blockers". Customer-specific blocker - * details live ONLY here (and never in source/git). Reads/writes are scoped via - * the framework sharing helpers so a user only ever sees their org's rows. - */ - -export interface AccessCtx { - email: string; - orgId: string | null; -} - -export type BlockerStatus = "active" | "monitoring" | "resolved"; - -export interface BlockerRecord { - id: string; - companyName: string; - blockerType: string; - status: BlockerStatus; - summary: string; - details: string; - sortOrder: number; - ownerEmail: string; - orgId: string | null; - visibility: "private" | "org" | "public"; - createdAt: string; - updatedAt: string; -} - -export interface BlockerInput { - companyName: string; - blockerType?: string; - status?: string; - summary?: string; - details?: string; - sortOrder?: number; -} - -function nowIso(): string { - return new Date().toISOString(); -} - -function newId(): string { - if (typeof crypto !== "undefined" && "randomUUID" in crypto) { - return crypto.randomUUID(); - } - return ( - Math.random().toString(36).slice(2, 10) + - Math.random().toString(36).slice(2, 10) - ); -} - -const STATUSES: ReadonlySet = new Set([ - "active", - "monitoring", - "resolved", -]); - -function normalizeStatus(raw: unknown): BlockerStatus { - const v = String(raw ?? "").trim(); - return (STATUSES.has(v) ? v : "active") as BlockerStatus; -} - -function rowToRecord(row: any): BlockerRecord { - return { - id: row.id, - companyName: row.companyName, - blockerType: row.blockerType ?? "", - status: row.status, - summary: row.summary ?? "", - details: row.details ?? "", - sortOrder: row.sortOrder ?? 0, - ownerEmail: row.ownerEmail, - orgId: row.orgId ?? null, - visibility: row.visibility, - createdAt: row.createdAt, - updatedAt: row.updatedAt, - }; -} - -function writableScope(ctx: AccessCtx) { - if (ctx.orgId) { - return or( - eq(schema.implementationBlockers.orgId, ctx.orgId), - and( - eq(schema.implementationBlockers.ownerEmail, ctx.email), - isNull(schema.implementationBlockers.orgId), - ), - ); - } - return and( - eq(schema.implementationBlockers.ownerEmail, ctx.email), - isNull(schema.implementationBlockers.orgId), - ); -} - -function recordScoped( - type: "change" | "delete", - id: string, - ctx: AccessCtx, -): void { - recordChange({ - source: "implementation-blockers", - type, - key: id, - ...(ctx.orgId ? { orgId: ctx.orgId } : { owner: ctx.email }), - }); -} - -export async function listImplementationBlockers( - ctx: AccessCtx, -): Promise { - const db = getDb() as any; - const where = accessFilter( - schema.implementationBlockers, - schema.implementationBlockerShares, - { userEmail: ctx.email, orgId: ctx.orgId ?? undefined }, - ); - const rows = await db - .select() - .from(schema.implementationBlockers) - .where(where) - .orderBy( - asc(schema.implementationBlockers.sortOrder), - asc(schema.implementationBlockers.companyName), - ); - return rows.map(rowToRecord); -} - -export async function replaceImplementationBlockers( - blockers: BlockerInput[], - ctx: AccessCtx, -): Promise { - await assertCanManageOrgRoster(ctx); - const db = getDb() as any; - const now = nowIso(); - const rows = blockers - .map((b, i) => ({ - companyName: String(b.companyName ?? "").trim(), - blockerType: String(b.blockerType ?? "").trim(), - status: normalizeStatus(b.status), - summary: String(b.summary ?? "").trim(), - details: String(b.details ?? "").trim(), - sortOrder: - typeof b.sortOrder === "number" && Number.isFinite(b.sortOrder) - ? b.sortOrder - : i, - })) - .filter((b) => b.companyName !== "") - .map((b) => ({ - id: newId(), - ...b, - ownerEmail: ctx.email, - orgId: ctx.orgId, - visibility: "org" as const, - createdAt: now, - updatedAt: now, - })); - - const replace = async (tx: any) => { - await tx.delete(schema.implementationBlockers).where(writableScope(ctx)); - if (rows.length > 0) { - await tx.insert(schema.implementationBlockers).values(rows); - } - }; - if (typeof db.transaction === "function") { - await db.transaction(replace); - } else { - await replace(db); - } - - recordScoped("change", "*", ctx); - return rows.map(rowToRecord); -} - -export async function updateImplementationBlocker( - id: string, - patch: Partial, - ctx: AccessCtx, -): Promise { - const access = await resolveAccess("implementation-blocker", id, { - userEmail: ctx.email, - orgId: ctx.orgId ?? undefined, - }); - if (!access) return null; - await assertAccess("implementation-blocker", id, "editor", { - userEmail: ctx.email, - orgId: ctx.orgId ?? undefined, - }); - - const set: Record = { updatedAt: nowIso() }; - if (patch.companyName !== undefined) { - set.companyName = String(patch.companyName).trim(); - } - if (patch.blockerType !== undefined) { - set.blockerType = String(patch.blockerType).trim(); - } - if (patch.status !== undefined) set.status = normalizeStatus(patch.status); - if (patch.summary !== undefined) set.summary = String(patch.summary).trim(); - if (patch.details !== undefined) set.details = String(patch.details).trim(); - if (patch.sortOrder !== undefined && Number.isFinite(patch.sortOrder)) { - set.sortOrder = patch.sortOrder; - } - - const db = getDb() as any; - await db - .update(schema.implementationBlockers) - .set(set) - .where(eq(schema.implementationBlockers.id, id)); - const [row] = await db - .select() - .from(schema.implementationBlockers) - .where(eq(schema.implementationBlockers.id, id)); - recordScoped("change", id, ctx); - return row ? rowToRecord(row) : null; -} diff --git a/templates/analytics/server/lib/org-write-guard.ts b/templates/analytics/server/lib/org-write-guard.ts deleted file mode 100644 index 3887e11fb9..0000000000 --- a/templates/analytics/server/lib/org-write-guard.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { getDbExec } from "@agent-native/core/db"; - -/** - * Guards destructive bulk writes (seed/replace) on the org-shared curated - * tables (strategic accounts, coverage contacts, implementation blockers). - * - * Per-row edits already go through `assertAccess(..., "editor", ...)`, but the - * atomic replace path wipes and rewrites the WHOLE scope. Without this check - * any ordinary org member could erase the shared roster. We require an org - * owner/admin for org-scoped writes; in solo mode (no org) the caller only ever - * touches their own org-less rows, so authentication alone is sufficient. - */ -export async function assertCanManageOrgRoster(ctx: { - email: string; - orgId: string | null; -}): Promise { - if (!ctx.email) throw new Error("no authenticated user"); - // Solo / org-less scope: the writable scope is the caller's own rows only. - if (!ctx.orgId) return; - - const exec = getDbExec(); - const { rows } = await exec.execute({ - sql: `SELECT role FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`, - args: [ctx.orgId, ctx.email.toLowerCase()], - }); - const role = rows[0]?.role ? String(rows[0].role) : null; - if (role !== "owner" && role !== "admin") { - throw new Error( - "Only organization owners or admins can seed or replace shared roster data.", - ); - } -} diff --git a/templates/analytics/server/lib/strategic-account-contacts-store.ts b/templates/analytics/server/lib/strategic-account-contacts-store.ts deleted file mode 100644 index f0ed34d8e2..0000000000 --- a/templates/analytics/server/lib/strategic-account-contacts-store.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { recordChange } from "@agent-native/core/server"; -import { - accessFilter, - assertAccess, - resolveAccess, -} from "@agent-native/core/sharing"; -import { and, asc, eq, isNull, or } from "drizzle-orm"; - -import { getDb, schema } from "../db/index.js"; -import { assertCanManageOrgRoster } from "./org-write-guard.js"; - -/** - * Org-scoped store for "Strategic Account Coverage" contacts. Sensitive contact - * details live ONLY here (and never in source/git). Reads/writes are scoped via - * the framework sharing helpers so a user only ever sees their org's rows. - */ - -export interface AccessCtx { - email: string; - orgId: string | null; -} - -export type CoverageRole = "champion" | "enabler" | "exec_sponsor"; -export type Confidence = "high" | "medium" | "low"; - -export interface ContactRecord { - id: string; - companyName: string; - role: CoverageRole; - contactName: string; - title: string; - email: string; - confidence: Confidence; - rationale: string; - sortOrder: number; - ownerEmail: string; - orgId: string | null; - visibility: "private" | "org" | "public"; - createdAt: string; - updatedAt: string; -} - -export interface ContactInput { - companyName: string; - role?: string; - contactName?: string; - title?: string; - email?: string; - confidence?: string; - rationale?: string; - sortOrder?: number; -} - -function nowIso(): string { - return new Date().toISOString(); -} - -function newId(): string { - if (typeof crypto !== "undefined" && "randomUUID" in crypto) { - return crypto.randomUUID(); - } - return ( - Math.random().toString(36).slice(2, 10) + - Math.random().toString(36).slice(2, 10) - ); -} - -const ROLES: ReadonlySet = new Set([ - "champion", - "enabler", - "exec_sponsor", -]); -const CONFIDENCES: ReadonlySet = new Set(["high", "medium", "low"]); - -function normalizeRole(raw: unknown): CoverageRole { - const v = String(raw ?? "").trim(); - return (ROLES.has(v) ? v : "champion") as CoverageRole; -} - -function normalizeConfidence(raw: unknown): Confidence { - const v = String(raw ?? "").trim(); - return (CONFIDENCES.has(v) ? v : "medium") as Confidence; -} - -function rowToRecord(row: any): ContactRecord { - return { - id: row.id, - companyName: row.companyName, - role: row.role, - contactName: row.contactName ?? "", - title: row.title ?? "", - email: row.email ?? "", - confidence: row.confidence, - rationale: row.rationale ?? "", - sortOrder: row.sortOrder ?? 0, - ownerEmail: row.ownerEmail, - orgId: row.orgId ?? null, - visibility: row.visibility, - createdAt: row.createdAt, - updatedAt: row.updatedAt, - }; -} - -function writableScope(ctx: AccessCtx) { - if (ctx.orgId) { - return or( - eq(schema.strategicAccountContacts.orgId, ctx.orgId), - and( - eq(schema.strategicAccountContacts.ownerEmail, ctx.email), - isNull(schema.strategicAccountContacts.orgId), - ), - ); - } - return and( - eq(schema.strategicAccountContacts.ownerEmail, ctx.email), - isNull(schema.strategicAccountContacts.orgId), - ); -} - -function recordScoped( - type: "change" | "delete", - id: string, - ctx: AccessCtx, -): void { - recordChange({ - source: "strategic-account-contacts", - type, - key: id, - ...(ctx.orgId ? { orgId: ctx.orgId } : { owner: ctx.email }), - }); -} - -export async function listStrategicAccountContacts( - ctx: AccessCtx, -): Promise { - const db = getDb() as any; - const where = accessFilter( - schema.strategicAccountContacts, - schema.strategicAccountContactShares, - { userEmail: ctx.email, orgId: ctx.orgId ?? undefined }, - ); - const rows = await db - .select() - .from(schema.strategicAccountContacts) - .where(where) - .orderBy( - asc(schema.strategicAccountContacts.sortOrder), - asc(schema.strategicAccountContacts.companyName), - ); - return rows.map(rowToRecord); -} - -export async function replaceStrategicAccountContacts( - contacts: ContactInput[], - ctx: AccessCtx, -): Promise { - await assertCanManageOrgRoster(ctx); - const db = getDb() as any; - const now = nowIso(); - const rows = contacts - .map((c, i) => ({ - companyName: String(c.companyName ?? "").trim(), - role: normalizeRole(c.role), - contactName: String(c.contactName ?? "").trim(), - title: String(c.title ?? "").trim(), - email: String(c.email ?? "").trim(), - confidence: normalizeConfidence(c.confidence), - rationale: String(c.rationale ?? "").trim(), - sortOrder: - typeof c.sortOrder === "number" && Number.isFinite(c.sortOrder) - ? c.sortOrder - : i, - })) - .filter((c) => c.companyName !== "") - .map((c) => ({ - id: newId(), - ...c, - ownerEmail: ctx.email, - orgId: ctx.orgId, - visibility: "org" as const, - createdAt: now, - updatedAt: now, - })); - - const replace = async (tx: any) => { - await tx.delete(schema.strategicAccountContacts).where(writableScope(ctx)); - if (rows.length > 0) { - await tx.insert(schema.strategicAccountContacts).values(rows); - } - }; - if (typeof db.transaction === "function") { - await db.transaction(replace); - } else { - await replace(db); - } - - recordScoped("change", "*", ctx); - return rows.map(rowToRecord); -} - -export async function updateStrategicAccountContact( - id: string, - patch: Partial, - ctx: AccessCtx, -): Promise { - const access = await resolveAccess("strategic-account-contact", id, { - userEmail: ctx.email, - orgId: ctx.orgId ?? undefined, - }); - if (!access) return null; - await assertAccess("strategic-account-contact", id, "editor", { - userEmail: ctx.email, - orgId: ctx.orgId ?? undefined, - }); - - const set: Record = { updatedAt: nowIso() }; - if (patch.companyName !== undefined) { - set.companyName = String(patch.companyName).trim(); - } - if (patch.role !== undefined) set.role = normalizeRole(patch.role); - if (patch.contactName !== undefined) { - set.contactName = String(patch.contactName).trim(); - } - if (patch.title !== undefined) set.title = String(patch.title).trim(); - if (patch.email !== undefined) set.email = String(patch.email).trim(); - if (patch.confidence !== undefined) { - set.confidence = normalizeConfidence(patch.confidence); - } - if (patch.rationale !== undefined) { - set.rationale = String(patch.rationale).trim(); - } - if (patch.sortOrder !== undefined && Number.isFinite(patch.sortOrder)) { - set.sortOrder = patch.sortOrder; - } - - const db = getDb() as any; - await db - .update(schema.strategicAccountContacts) - .set(set) - .where(eq(schema.strategicAccountContacts.id, id)); - const [row] = await db - .select() - .from(schema.strategicAccountContacts) - .where(eq(schema.strategicAccountContacts.id, id)); - recordScoped("change", id, ctx); - return row ? rowToRecord(row) : null; -} diff --git a/templates/analytics/server/lib/strategic-accounts-store.ts b/templates/analytics/server/lib/strategic-accounts-store.ts deleted file mode 100644 index 474c5b3fdd..0000000000 --- a/templates/analytics/server/lib/strategic-accounts-store.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { recordChange } from "@agent-native/core/server"; -import { - accessFilter, - assertAccess, - resolveAccess, -} from "@agent-native/core/sharing"; -import { and, asc, eq, isNull, or } from "drizzle-orm"; - -import { getDb, schema } from "../db/index.js"; -import { assertCanManageOrgRoster } from "./org-write-guard.js"; - -/** - * Org-scoped store for the curated Strategic Accounts roster. The list is the - * source of truth for the migrated overview and lives ONLY here (and the - * dashboard config row) — never in source/git. Reads/writes are scoped through - * the framework sharing helpers so a user only ever sees their org's rows. - */ - -export interface AccessCtx { - email: string; - orgId: string | null; -} - -export interface StrategicAccountRecord { - id: string; - companyName: string; - companyId: string | null; - deploymentStatus: string; - notes: string; - sortOrder: number; - ownerEmail: string; - orgId: string | null; - visibility: "private" | "org" | "public"; - createdAt: string; - updatedAt: string; -} - -export interface StrategicAccountInput { - companyName: string; - companyId?: string | null; - deploymentStatus?: string; - notes?: string; - sortOrder?: number; -} - -function nowIso(): string { - return new Date().toISOString(); -} - -function newId(): string { - if (typeof crypto !== "undefined" && "randomUUID" in crypto) { - return crypto.randomUUID(); - } - return ( - Math.random().toString(36).slice(2, 10) + - Math.random().toString(36).slice(2, 10) - ); -} - -function rowToRecord(row: any): StrategicAccountRecord { - return { - id: row.id, - companyName: row.companyName, - companyId: row.companyId ?? null, - deploymentStatus: row.deploymentStatus ?? "", - notes: row.notes ?? "", - sortOrder: row.sortOrder ?? 0, - ownerEmail: row.ownerEmail, - orgId: row.orgId ?? null, - visibility: row.visibility, - createdAt: row.createdAt, - updatedAt: row.updatedAt, - }; -} - -/** - * Rows the caller can write/replace in the current scope: their org's rows - * when in an org, otherwise their own org-less rows. Mirrors the owner-scope - * branch of `accessFilter` so seeding never touches another scope's data. - */ -function writableScope(ctx: AccessCtx) { - if (ctx.orgId) { - return or( - eq(schema.strategicAccounts.orgId, ctx.orgId), - and( - eq(schema.strategicAccounts.ownerEmail, ctx.email), - isNull(schema.strategicAccounts.orgId), - ), - ); - } - return and( - eq(schema.strategicAccounts.ownerEmail, ctx.email), - isNull(schema.strategicAccounts.orgId), - ); -} - -function recordScoped( - type: "change" | "delete", - id: string, - ctx: AccessCtx, -): void { - recordChange({ - source: "strategic-accounts", - type, - key: id, - ...(ctx.orgId ? { orgId: ctx.orgId } : { owner: ctx.email }), - }); -} - -/** List the org's curated accounts, ordered for the grid. */ -export async function listStrategicAccounts( - ctx: AccessCtx, -): Promise { - const db = getDb() as any; - const where = accessFilter( - schema.strategicAccounts, - schema.strategicAccountShares, - { userEmail: ctx.email, orgId: ctx.orgId ?? undefined }, - ); - const rows = await db - .select() - .from(schema.strategicAccounts) - .where(where) - .orderBy( - asc(schema.strategicAccounts.sortOrder), - asc(schema.strategicAccounts.companyName), - ); - return rows.map(rowToRecord); -} - -/** - * Atomically replace the caller-scope's curated roster in ONE write batch - * (per the reliable-mutations rule — never loop inserts). Seeds visibility to - * `org` so the whole organization sees the list. Returns the new rows. - */ -export async function replaceStrategicAccounts( - accounts: StrategicAccountInput[], - ctx: AccessCtx, -): Promise { - await assertCanManageOrgRoster(ctx); - const db = getDb() as any; - const now = nowIso(); - const rows = accounts - .map((a, i) => ({ - companyName: String(a.companyName ?? "").trim(), - companyId: - a.companyId === undefined || a.companyId === null - ? null - : String(a.companyId).trim() || null, - deploymentStatus: String(a.deploymentStatus ?? "").trim(), - notes: String(a.notes ?? "").trim(), - sortOrder: - typeof a.sortOrder === "number" && Number.isFinite(a.sortOrder) - ? a.sortOrder - : i, - })) - .filter((a) => a.companyName !== "") - .map((a) => ({ - id: newId(), - ...a, - ownerEmail: ctx.email, - orgId: ctx.orgId, - visibility: "org" as const, - createdAt: now, - updatedAt: now, - })); - - const replace = async (tx: any) => { - await tx.delete(schema.strategicAccounts).where(writableScope(ctx)); - if (rows.length > 0) { - await tx.insert(schema.strategicAccounts).values(rows); - } - }; - if (typeof db.transaction === "function") { - await db.transaction(replace); - } else { - await replace(db); - } - - recordScoped("change", "*", ctx); - return rows.map(rowToRecord); -} - -/** Edit one row's manual fields. Requires editor access to that row. */ -export async function updateStrategicAccount( - id: string, - patch: Partial< - Pick< - StrategicAccountInput, - "companyName" | "companyId" | "deploymentStatus" | "notes" | "sortOrder" - > - >, - ctx: AccessCtx, -): Promise { - const access = await resolveAccess("strategic-account", id, { - userEmail: ctx.email, - orgId: ctx.orgId ?? undefined, - }); - if (!access) return null; - await assertAccess("strategic-account", id, "editor", { - userEmail: ctx.email, - orgId: ctx.orgId ?? undefined, - }); - - const set: Record = { updatedAt: nowIso() }; - if (patch.companyName !== undefined) { - set.companyName = String(patch.companyName).trim(); - } - if (patch.companyId !== undefined) { - set.companyId = - patch.companyId === null ? null : String(patch.companyId).trim() || null; - } - if (patch.deploymentStatus !== undefined) { - set.deploymentStatus = String(patch.deploymentStatus).trim(); - } - if (patch.notes !== undefined) set.notes = String(patch.notes).trim(); - if (patch.sortOrder !== undefined && Number.isFinite(patch.sortOrder)) { - set.sortOrder = patch.sortOrder; - } - - const db = getDb() as any; - await db - .update(schema.strategicAccounts) - .set(set) - .where(eq(schema.strategicAccounts.id, id)); - const [row] = await db - .select() - .from(schema.strategicAccounts) - .where(eq(schema.strategicAccounts.id, id)); - recordScoped("change", id, ctx); - return row ? rowToRecord(row) : null; -} diff --git a/templates/analytics/server/plugins/db.ts b/templates/analytics/server/plugins/db.ts index 19308315ae..6d497bbb38 100644 --- a/templates/analytics/server/plugins/db.ts +++ b/templates/analytics/server/plugins/db.ts @@ -492,124 +492,6 @@ export default runMigrations( version: 71, sql: `CREATE INDEX IF NOT EXISTS session_replay_ingests_recording_idx ON session_replay_ingests (recording_id)`, }, - // --- v72+: Strategic Accounts curated roster (migrated from - // fusion-analytics). Org-scoped source of truth for the overview - // dashboard + extension. Account names live only here, never in source. - { - version: 72, - sql: `CREATE TABLE IF NOT EXISTS strategic_accounts ( - id TEXT PRIMARY KEY, - company_name TEXT NOT NULL, - company_id TEXT, - deployment_status TEXT NOT NULL DEFAULT '', - notes TEXT NOT NULL DEFAULT '', - sort_order INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')), - owner_email TEXT NOT NULL DEFAULT 'local@localhost', - org_id TEXT, - visibility TEXT NOT NULL DEFAULT 'private' - )`, - }, - { - version: 73, - sql: `CREATE TABLE IF NOT EXISTS strategic_account_shares ( - id TEXT PRIMARY KEY, - resource_id TEXT NOT NULL, - principal_type TEXT NOT NULL, - principal_id TEXT NOT NULL, - role TEXT NOT NULL DEFAULT 'viewer', - created_by TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - )`, - }, - { - version: 74, - sql: `CREATE INDEX IF NOT EXISTS strategic_accounts_owner_org_idx ON strategic_accounts (owner_email, org_id, sort_order)`, - }, - { - version: 75, - sql: `CREATE INDEX IF NOT EXISTS strategic_account_shares_resource_idx ON strategic_account_shares (resource_id)`, - }, - // --- v76+: Strategic Account Coverage contacts + Implementation Blockers - // (migrated from fusion-analytics children). Org-scoped curated tables; - // sensitive contact/customer details live only here, never in source. - { - version: 76, - sql: `CREATE TABLE IF NOT EXISTS strategic_account_contacts ( - id TEXT PRIMARY KEY, - company_name TEXT NOT NULL, - role TEXT NOT NULL DEFAULT 'champion', - contact_name TEXT NOT NULL DEFAULT '', - title TEXT NOT NULL DEFAULT '', - email TEXT NOT NULL DEFAULT '', - confidence TEXT NOT NULL DEFAULT 'medium', - rationale TEXT NOT NULL DEFAULT '', - sort_order INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')), - owner_email TEXT NOT NULL DEFAULT 'local@localhost', - org_id TEXT, - visibility TEXT NOT NULL DEFAULT 'private' - )`, - }, - { - version: 77, - sql: `CREATE TABLE IF NOT EXISTS strategic_account_contact_shares ( - id TEXT PRIMARY KEY, - resource_id TEXT NOT NULL, - principal_type TEXT NOT NULL, - principal_id TEXT NOT NULL, - role TEXT NOT NULL DEFAULT 'viewer', - created_by TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - )`, - }, - { - version: 78, - sql: `CREATE INDEX IF NOT EXISTS strategic_account_contacts_owner_org_idx ON strategic_account_contacts (owner_email, org_id, sort_order)`, - }, - { - version: 79, - sql: `CREATE INDEX IF NOT EXISTS strategic_account_contact_shares_resource_idx ON strategic_account_contact_shares (resource_id)`, - }, - { - version: 80, - sql: `CREATE TABLE IF NOT EXISTS implementation_blockers ( - id TEXT PRIMARY KEY, - company_name TEXT NOT NULL, - blocker_type TEXT NOT NULL DEFAULT '', - status TEXT NOT NULL DEFAULT 'active', - summary TEXT NOT NULL DEFAULT '', - details TEXT NOT NULL DEFAULT '', - sort_order INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')), - owner_email TEXT NOT NULL DEFAULT 'local@localhost', - org_id TEXT, - visibility TEXT NOT NULL DEFAULT 'private' - )`, - }, - { - version: 81, - sql: `CREATE TABLE IF NOT EXISTS implementation_blocker_shares ( - id TEXT PRIMARY KEY, - resource_id TEXT NOT NULL, - principal_type TEXT NOT NULL, - principal_id TEXT NOT NULL, - role TEXT NOT NULL DEFAULT 'viewer', - created_by TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - )`, - }, - { - version: 82, - sql: `CREATE INDEX IF NOT EXISTS implementation_blockers_owner_org_idx ON implementation_blockers (owner_email, org_id, sort_order)`, - }, - { - version: 83, - sql: `CREATE INDEX IF NOT EXISTS implementation_blocker_shares_resource_idx ON implementation_blocker_shares (resource_id)`, - }, ], { table: "analytics_migrations" }, ); From 9a6c3427b46b0b5a8892d9910ae861cd64d1bd9d Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Fri, 26 Jun 2026 22:05:52 +0000 Subject: [PATCH 14/20] Discard changes to templates/analytics/app/components/layout/Sidebar.tsx --- .../app/components/layout/Sidebar.tsx | 37 +++++++------------ 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/templates/analytics/app/components/layout/Sidebar.tsx b/templates/analytics/app/components/layout/Sidebar.tsx index 4509fe9088..4791bb38a6 100644 --- a/templates/analytics/app/components/layout/Sidebar.tsx +++ b/templates/analytics/app/components/layout/Sidebar.tsx @@ -520,11 +520,7 @@ function SortableRow({ }, [href, t]); return ( -

    +
    + ) : canEdit ? ( + ) : null} {/* Tabs */} From 3e9a76dc09705175fd8f1963935a3ae9f5977d32 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Fri, 26 Jun 2026 22:05:59 +0000 Subject: [PATCH 16/20] Discard changes to templates/analytics/app/pages/adhoc/sql-dashboard/types.ts --- .../analytics/app/pages/adhoc/sql-dashboard/types.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts b/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts index 238718e958..6147b10d63 100644 --- a/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +++ b/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts @@ -79,14 +79,6 @@ export interface SqlPanelConfig { sortable?: boolean; columns?: TableColumnConfig[]; limit?: number; - /** - * For the `cards` chart type: which column supplies each card's heading. - * Defaults to the first configured column. The remaining columns render as - * label/value rows inside the card. - */ - titleKey?: string; - /** For `cards`: optional column rendered as a status badge/pill on the card. */ - badgeKey?: string; } export interface SqlPanel { From 53ee5ef721e6692105e5244908a0c886c4a32155 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Fri, 26 Jun 2026 22:06:01 +0000 Subject: [PATCH 17/20] Discard changes to templates/analytics/.gitignore --- templates/analytics/.gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/templates/analytics/.gitignore b/templates/analytics/.gitignore index d416e5015c..65277e1c63 100644 --- a/templates/analytics/.gitignore +++ b/templates/analytics/.gitignore @@ -53,6 +53,3 @@ build /local.db .vercel/ - -# Local scratch (never commit) -scratch/ From f6095255678c249012637b02b3e1a1860661089b Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Sat, 27 Jun 2026 00:43:22 +0000 Subject: [PATCH 18/20] Add sidebar dashboard nesting via parentId field --- pnpm-lock.yaml | 24 ---- .../actions/dashboard-mutation-api.ts | 2 + .../analytics/actions/update-dashboard.ts | 8 ++ .../app/components/layout/Sidebar.tsx | 108 ++++++++++++++---- .../app/pages/adhoc/sql-dashboard/types.ts | 7 ++ ...st-under-a-parent-in-the-sidebar-via-a-.md | 6 + 6 files changed, 109 insertions(+), 46 deletions(-) create mode 100644 templates/analytics/changelog/2026-06-27-dashboards-can-now-nest-under-a-parent-in-the-sidebar-via-a-.md diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f7516ff03..a6948ecfde 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1232,30 +1232,6 @@ importers: specifier: 'catalog:' version: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@25.9.3)(happy-dom@20.10.6)(jsdom@29.1.1(@noble/hashes@2.2.0)(canvas@3.2.3))(vite@8.1.0(@types/node@25.9.3)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) - templates/.retired/calls: {} - - templates/.retired/code: {} - - templates/.retired/contracts: {} - - templates/.retired/images: {} - - templates/.retired/issues: {} - - templates/.retired/meeting-notes: {} - - templates/.retired/migration: {} - - templates/.retired/recruiting: {} - - templates/.retired/scheduling: {} - - templates/.retired/visual-plans: {} - - templates/.retired/voice: {} - - templates/.retired/workbench: {} - templates/analytics: dependencies: '@agent-native/core': diff --git a/templates/analytics/actions/dashboard-mutation-api.ts b/templates/analytics/actions/dashboard-mutation-api.ts index 00d1563cf2..561094fed3 100644 --- a/templates/analytics/actions/dashboard-mutation-api.ts +++ b/templates/analytics/actions/dashboard-mutation-api.ts @@ -17,6 +17,8 @@ type DashboardPatch = { columns?: number; filters?: unknown[]; variables?: Record; + /** Id of another dashboard to nest this one under in the sidebar. */ + parentId?: string; }; type PanelPatch = { diff --git a/templates/analytics/actions/update-dashboard.ts b/templates/analytics/actions/update-dashboard.ts index 22b4ae534c..ca2514d34f 100644 --- a/templates/analytics/actions/update-dashboard.ts +++ b/templates/analytics/actions/update-dashboard.ts @@ -277,6 +277,14 @@ export function validateDashboardConfig( if (typeof config.name !== "string" || config.name.trim().length === 0) { return "config.name is required (non-empty string) — without it the dashboard renders as a blank row in the sidebar"; } + if (config.parentId !== undefined && config.parentId !== null) { + if ( + typeof config.parentId !== "string" || + config.parentId.trim().length === 0 + ) { + return "config.parentId must be a non-empty dashboard id (or omitted) — it nests this dashboard under that parent in the sidebar"; + } + } // Filter ID collisions cause two controls to read/write the same URL param. // For paired start/end dates use a single date-range filter — the FilterBar // expands it to Start / End at runtime, so the SQL can still diff --git a/templates/analytics/app/components/layout/Sidebar.tsx b/templates/analytics/app/components/layout/Sidebar.tsx index 4791bb38a6..b008b3edea 100644 --- a/templates/analytics/app/components/layout/Sidebar.tsx +++ b/templates/analytics/app/components/layout/Sidebar.tsx @@ -30,7 +30,14 @@ import { type QueryKey, } from "@tanstack/react-query"; import { useTheme } from "next-themes"; -import { useState, useEffect, useCallback, useRef, useMemo } from "react"; +import { + useState, + useEffect, + useCallback, + useRef, + useMemo, + Fragment, +} from "react"; import { Link, useLocation, useNavigate } from "react-router"; import { toast } from "sonner"; @@ -51,6 +58,8 @@ type SidebarDashboard = { subviews?: DashboardSubview[]; source: "static" | "sql"; visibility?: Visibility; + /** Id of the dashboard this one nests under in the sidebar, if any. */ + parentId?: string; }; import { DevDatabaseLink, @@ -1024,6 +1033,7 @@ type SqlDashboardListItem = { id: string; name: string; visibility?: Visibility; + parentId?: string; }; async function fetchSqlDashboards( @@ -1043,6 +1053,10 @@ async function fetchSqlDashboards( d.visibility === "org" || d.visibility === "public" ? (d.visibility as Visibility) : ("private" as Visibility), + parentId: + typeof d.parentId === "string" && d.parentId.trim().length > 0 + ? d.parentId + : undefined, })); } catch { return []; @@ -1434,6 +1448,7 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) { name: d.name, source: "sql", visibility: d.visibility, + parentId: d.parentId, })); const all = [...staticItems, ...sqlItems]; if (dashboardSortMode === "alphabetical") { @@ -1471,12 +1486,34 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) { [visibleDashboards, dashFilter], ); + // Group dashboards that declare a parentId beneath their parent. Orphans + // (parent missing/filtered out) and self-references fall back to top level. + const dashboardChildren = useMemo>(() => { + const present = new Set(filteredDashboards.map((d) => d.id)); + const byParent = new Map(); + for (const d of filteredDashboards) { + if (d.parentId && d.parentId !== d.id && present.has(d.parentId)) { + const arr = byParent.get(d.parentId) ?? []; + arr.push(d); + byParent.set(d.parentId, arr); + } + } + return byParent; + }, [filteredDashboards]); + + const topLevelDashboards = useMemo(() => { + const childIds = new Set(); + for (const arr of dashboardChildren.values()) + for (const c of arr) childIds.add(c.id); + return filteredDashboards.filter((d) => !childIds.has(d.id)); + }, [filteredDashboards, dashboardChildren]); + const displayedDashboards = useMemo( () => dashShowAll - ? filteredDashboards - : filteredDashboards.slice(0, SIDEBAR_PREVIEW_COUNT), - [filteredDashboards, dashShowAll], + ? topLevelDashboards + : topLevelDashboards.slice(0, SIDEBAR_PREVIEW_COUNT), + [topLevelDashboards, dashShowAll], ); const handleDashboardDelete = useCallback( @@ -2032,27 +2069,54 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) { onDragEnd={handleDashboardDragEnd} > d.id)} + items={displayedDashboards.flatMap((d) => [ + d.id, + ...(dashboardChildren.get(d.id) ?? []).map((c) => c.id), + ])} strategy={verticalListSortingStrategy} >
    - {displayedDashboards.map((d) => ( - - ))} - {filteredDashboards.length > SIDEBAR_PREVIEW_COUNT && ( + {displayedDashboards.map((d) => { + const children = dashboardChildren.get(d.id) ?? []; + return ( + + + {children.length > 0 && ( +
    + {children.map((child) => ( + + ))} +
    + )} +
    + ); + })} + {topLevelDashboards.length > SIDEBAR_PREVIEW_COUNT && (