diff --git a/cmd/agentsview/pg.go b/cmd/agentsview/pg.go index bfd45ca13..957cc9c40 100644 --- a/cmd/agentsview/pg.go +++ b/cmd/agentsview/pg.go @@ -418,6 +418,14 @@ func runPGServe(appCfg config.Config, basePath string) { ); err != nil { fatal("pg serve: %v", err) } + if err := store.DetectInsightGenerationAvailability( + ctx, + ); err != nil { + fatal( + "pg serve: probing insight generation capability: %v", + err, + ) + } rtOpts := serveRuntimeOptions{ Mode: "pg-serve", @@ -430,10 +438,11 @@ func runPGServe(appCfg config.Config, basePath string) { opts := []server.Option{ server.WithVersion(server.VersionInfo{ - Version: version, - Commit: commit, - BuildDate: buildDate, - ReadOnly: true, + Version: version, + Commit: commit, + BuildDate: buildDate, + ReadOnly: true, + InsightGenerationAvailable: store.InsightGenerationAvailable(), }), server.WithDataDir(appCfg.DataDir), server.WithBaseContext(ctx), diff --git a/frontend/src/lib/api/generated/models/VersionInfo.ts b/frontend/src/lib/api/generated/models/VersionInfo.ts index db993dae1..c8109c321 100644 --- a/frontend/src/lib/api/generated/models/VersionInfo.ts +++ b/frontend/src/lib/api/generated/models/VersionInfo.ts @@ -7,7 +7,7 @@ export type VersionInfo = { build_date: string; commit: string; data_version: number; + insight_generation_available?: boolean; read_only?: boolean; version: string; }; - diff --git a/frontend/src/lib/api/types/core.ts b/frontend/src/lib/api/types/core.ts index e727125da..9cf2d254e 100644 --- a/frontend/src/lib/api/types/core.ts +++ b/frontend/src/lib/api/types/core.ts @@ -3,6 +3,7 @@ export interface VersionInfo { version: string; commit: string; build_date: string; + insight_generation_available?: boolean; read_only?: boolean; } diff --git a/frontend/src/lib/components/activity/ActivityInsight.svelte b/frontend/src/lib/components/activity/ActivityInsight.svelte index d6fd21bc5..5c6b6e285 100644 --- a/frontend/src/lib/components/activity/ActivityInsight.svelte +++ b/frontend/src/lib/components/activity/ActivityInsight.svelte @@ -55,12 +55,15 @@ router.navigate("insights"); } - const readOnly = $derived(sync.serverVersion?.read_only === true); + const insightGenerationAvailable = $derived( + sync.serverVersion?.insight_generation_available === true || + sync.serverVersion?.read_only !== true, + ); const generationUnavailable = $derived( - sync.serverVersion === null || readOnly, + sync.serverVersion === null || !insightGenerationAvailable, ); const unavailableTitle = $derived( - readOnly + sync.serverVersion !== null && !insightGenerationAvailable ? m.activity_insight_unavailable_read_only() : sync.serverVersion === null ? m.activity_insight_waiting_server() diff --git a/frontend/src/lib/components/activity/ActivityInsight.test.ts b/frontend/src/lib/components/activity/ActivityInsight.test.ts index e0a4e3c62..8bbc21e33 100644 --- a/frontend/src/lib/components/activity/ActivityInsight.test.ts +++ b/frontend/src/lib/components/activity/ActivityInsight.test.ts @@ -13,13 +13,20 @@ const mocks = vi.hoisted(() => ({ setAgent: vi.fn(), navigate: vi.fn(), agent: "claude" as string, - serverVersion: { read_only: false } as { read_only: boolean } | null, + serverVersion: { + read_only: false, + } as { + read_only: boolean; + insight_generation_available?: boolean; + } | null, })); vi.mock("../../api/generated/index", () => ({ InsightsService: { getApiV1Insights: mocks.getInsights }, })); vi.mock("../../api/runtime.js", () => ({ configureGeneratedClient: vi.fn() })); -vi.mock("../../api/client.js", () => ({ generateInsight: mocks.generateInsight })); +vi.mock("../../api/client.js", () => ({ + generateInsight: mocks.generateInsight, +})); vi.mock("../../stores/sync.svelte.js", () => ({ sync: { get serverVersion() { @@ -93,7 +100,10 @@ describe("ActivityInsight", () => { }); it("generates for the current range", async () => { - mocks.generateInsight.mockReturnValue({ abort: vi.fn(), done: new Promise(() => {}) }); + mocks.generateInsight.mockReturnValue({ + abort: vi.fn(), + done: new Promise(() => {}), + }); render(ActivityInsight, { dateFrom: "2026-06-15", dateTo: "2026-06-21" }); await settle(); await fireEvent.click(screen.getByRole("button", { name: /generate/i })); @@ -178,7 +188,9 @@ describe("ActivityInsight", () => { it("prefills the Insights page range and navigates", async () => { render(ActivityInsight, { dateFrom: "2026-06-15", dateTo: "2026-06-21" }); await settle(); - const link = screen.getByRole("link", { name: /insights page|open in insights/i }); + const link = screen.getByRole("link", { + name: /insights page|open in insights/i, + }); await fireEvent.click(link); expect(mocks.setType).toHaveBeenCalledWith("daily_activity"); expect(mocks.setDateFrom).toHaveBeenCalledWith("2026-06-15"); @@ -195,6 +207,17 @@ describe("ActivityInsight", () => { expect(btn.hasAttribute("disabled")).toBe(true); }); + it("allows Generate in read-only mode when insight generation is advertised", async () => { + mocks.serverVersion = { + read_only: true, + insight_generation_available: true, + }; + render(ActivityInsight, { dateFrom: "2026-06-15", dateTo: "2026-06-21" }); + await settle(); + const btn = screen.getByRole("button", { name: /generate/i }); + expect(btn.hasAttribute("disabled")).toBe(false); + }); + it("selects the exact-range insight over a newer nested one", async () => { mocks.getInsights.mockResolvedValue({ insights: [ diff --git a/frontend/src/lib/components/insights/InsightsPage.svelte b/frontend/src/lib/components/insights/InsightsPage.svelte index a8904b4d4..7c6b6d187 100644 --- a/frontend/src/lib/components/insights/InsightsPage.svelte +++ b/frontend/src/lib/components/insights/InsightsPage.svelte @@ -81,11 +81,12 @@ ); const loading = $derived(analytics.loading.signals); const error = $derived(analytics.errors.signals); - const readOnly = $derived( - sync.serverVersion?.read_only === true, + const insightGenerationAvailable = $derived( + sync.serverVersion?.insight_generation_available === true || + sync.serverVersion?.read_only !== true, ); const generationUnavailable = $derived( - sync.serverVersion === null || readOnly, + sync.serverVersion === null || !insightGenerationAvailable, ); const earliestSession = $derived(sync.stats?.earliest_session ?? null); const rangeSelection = $derived( @@ -1093,7 +1094,7 @@