|
| 1 | +// Demo mode (#5963): a build-time-flagged, zero-backend mock data layer so this dashboard can be deployed as a |
| 2 | +// static demo with no real miner harness, local ledger files, or operator credentials behind it -- the same |
| 3 | +// mechanism family as loopover-ui's signInPreview() escape hatch (apps/loopover-ui/src/lib/api/session.ts), just |
| 4 | +// covering N fabricated API responses instead of one fake session object. `import.meta.env.VITE_DEMO_MODE` is a |
| 5 | +// build-time constant, so the "off" branch (the real fetch calls) is dead-code-eliminated from a demo bundle and |
| 6 | +// vice versa -- a production self-host build never carries this module's data. |
| 7 | +// |
| 8 | +// Scope: the five REST fetchers backing the three main dashboard routes (run-history, ledgers, portfolio + |
| 9 | +// its queue actions, governor). discover/attempt/chat are NOT covered here -- those trigger a real coding-agent |
| 10 | +// iteration or ground against a live MCP connection, and fabricating a convincing multi-minute agent run is a |
| 11 | +// separate, much larger content-design task than tabular summary data (tracked as follow-up work, not this PR). |
| 12 | +// |
| 13 | +// Every value below is entirely synthetic -- no real repo, run, ledger entry, or account referenced anywhere. |
| 14 | + |
| 15 | +import type { RunStateRow } from "./run-history"; |
| 16 | +import type { LedgersSummary } from "./ledgers"; |
| 17 | +import type { PortfolioQueueSummary } from "./portfolio-queue"; |
| 18 | +import type { PortfolioQueueActionItem } from "./portfolio-queue-actions"; |
| 19 | +import type { GovernorPauseState } from "./governor"; |
| 20 | + |
| 21 | +export function isDemoMode(): boolean { |
| 22 | + return import.meta.env.VITE_DEMO_MODE === "1"; |
| 23 | +} |
| 24 | + |
| 25 | +export const DEMO_RUN_STATES: RunStateRow[] = [ |
| 26 | + { |
| 27 | + apiBaseUrl: "https://forge.example.com", |
| 28 | + repoFullName: "acme/widgets", |
| 29 | + state: "preparing", |
| 30 | + updatedAt: "2026-07-18T14:02:00.000Z", |
| 31 | + }, |
| 32 | + { |
| 33 | + apiBaseUrl: "https://forge.example.com", |
| 34 | + repoFullName: "acme/api-gateway", |
| 35 | + state: "discovering", |
| 36 | + updatedAt: "2026-07-18T13:47:00.000Z", |
| 37 | + }, |
| 38 | + { |
| 39 | + apiBaseUrl: "https://forge.example.com", |
| 40 | + repoFullName: "acme/docs-site", |
| 41 | + state: "idle", |
| 42 | + updatedAt: "2026-07-18T11:15:00.000Z", |
| 43 | + }, |
| 44 | + { |
| 45 | + apiBaseUrl: "https://forge.example.com", |
| 46 | + repoFullName: "northwind/inventory", |
| 47 | + state: "planning", |
| 48 | + updatedAt: "2026-07-18T12:30:00.000Z", |
| 49 | + }, |
| 50 | +]; |
| 51 | + |
| 52 | +export const DEMO_LEDGERS_SUMMARY: LedgersSummary = { |
| 53 | + claims: { total: 18, byStatus: { active: 3, released: 12, expired: 3 } }, |
| 54 | + events: { |
| 55 | + total: 142, |
| 56 | + byType: { claimed: 41, released: 38, event_recorded: 63 }, |
| 57 | + recent: [ |
| 58 | + { eventType: "claimed", repoFullName: "acme/widgets", createdAt: "2026-07-18T14:00:00.000Z" }, |
| 59 | + { eventType: "released", repoFullName: "acme/api-gateway", createdAt: "2026-07-18T13:45:00.000Z" }, |
| 60 | + { eventType: "event_recorded", repoFullName: "acme/docs-site", createdAt: "2026-07-18T11:10:00.000Z" }, |
| 61 | + { eventType: "claimed", repoFullName: "northwind/inventory", createdAt: "2026-07-18T09:30:00.000Z" }, |
| 62 | + { eventType: "released", repoFullName: "acme/widgets", createdAt: "2026-07-17T22:14:00.000Z" }, |
| 63 | + ], |
| 64 | + }, |
| 65 | + governor: { total: 9, byEventType: { paused: 4, resumed: 5 } }, |
| 66 | +}; |
| 67 | + |
| 68 | +export const DEMO_PORTFOLIO_QUEUE_SUMMARY: PortfolioQueueSummary = { |
| 69 | + total: 27, |
| 70 | + byStatus: { queued: 9, in_progress: 3, done: 15 }, |
| 71 | + repos: [ |
| 72 | + { repoFullName: "acme/widgets", byStatus: { queued: 4, in_progress: 1, done: 6 }, total: 11 }, |
| 73 | + { repoFullName: "acme/api-gateway", byStatus: { queued: 2, in_progress: 1, done: 4 }, total: 7 }, |
| 74 | + { repoFullName: "acme/docs-site", byStatus: { queued: 1, in_progress: 0, done: 3 }, total: 4 }, |
| 75 | + { repoFullName: "northwind/inventory", byStatus: { queued: 2, in_progress: 1, done: 2 }, total: 5 }, |
| 76 | + ], |
| 77 | + oldestQueuedAgeMs: 6 * 60 * 60 * 1000, // 6h |
| 78 | +}; |
| 79 | + |
| 80 | +const DEFAULT_DEMO_PORTFOLIO_QUEUE_ITEMS: PortfolioQueueActionItem[] = [ |
| 81 | + { |
| 82 | + apiBaseUrl: "https://forge.example.com", |
| 83 | + repoFullName: "acme/widgets", |
| 84 | + identifier: "wgt-2451", |
| 85 | + status: "in_progress", |
| 86 | + }, |
| 87 | + { |
| 88 | + apiBaseUrl: "https://forge.example.com", |
| 89 | + repoFullName: "acme/api-gateway", |
| 90 | + identifier: "gw-118", |
| 91 | + status: "in_progress", |
| 92 | + }, |
| 93 | + { apiBaseUrl: "https://forge.example.com", repoFullName: "acme/widgets", identifier: "wgt-2438", status: "done" }, |
| 94 | + { |
| 95 | + apiBaseUrl: "https://forge.example.com", |
| 96 | + repoFullName: "northwind/inventory", |
| 97 | + identifier: "inv-77", |
| 98 | + status: "done", |
| 99 | + }, |
| 100 | +]; |
| 101 | + |
| 102 | +// Mutable, in-memory, browser-session-only copy -- release/requeue removes the item from this actionable list |
| 103 | +// (simulating it going back to "queued", which this endpoint doesn't itself track), same session-only-state |
| 104 | +// reasoning as the governor pause state below: a demo control that visibly does nothing is a worse demo than |
| 105 | +// one that responds, and there's no real queue here to protect from a fabricated write. |
| 106 | +let demoPortfolioQueueItems: PortfolioQueueActionItem[] = [...DEFAULT_DEMO_PORTFOLIO_QUEUE_ITEMS]; |
| 107 | + |
| 108 | +export function getDemoPortfolioQueueItems(): PortfolioQueueActionItem[] { |
| 109 | + return demoPortfolioQueueItems; |
| 110 | +} |
| 111 | + |
| 112 | +/** Remove one item (by repoFullName + identifier) from the demo actionable list, simulating a release/requeue. |
| 113 | + * Returns the removed item, or null if no matching item was found (mirrors the real API's not-found shape). */ |
| 114 | +export function removeDemoPortfolioQueueItem( |
| 115 | + repoFullName: string, |
| 116 | + identifier: string, |
| 117 | +): PortfolioQueueActionItem | null { |
| 118 | + const index = demoPortfolioQueueItems.findIndex( |
| 119 | + (item) => item.repoFullName === repoFullName && item.identifier === identifier, |
| 120 | + ); |
| 121 | + if (index === -1) return null; |
| 122 | + const [removed] = demoPortfolioQueueItems.splice(index, 1); |
| 123 | + // A valid index always has exactly one element to splice out; the fallback only guards the array-access |
| 124 | + // type, not a real runtime path. |
| 125 | + return removed ?? null; |
| 126 | +} |
| 127 | + |
| 128 | +/** Test-only: restores the module-level mutable demo state (governor pause state + queue items) to its |
| 129 | + * defaults, so one test's release/pause doesn't leak into the next. Never called from app code. */ |
| 130 | +export function resetDemoDataForTest(): void { |
| 131 | + demoPortfolioQueueItems = [...DEFAULT_DEMO_PORTFOLIO_QUEUE_ITEMS]; |
| 132 | + demoGovernorState = { paused: false, reason: null, pausedAt: null }; |
| 133 | +} |
| 134 | + |
| 135 | +// Mutable, in-memory, browser-session-only -- pause/resume is harmless to actually simulate (no real governor, |
| 136 | +// nothing to protect), and a static read-only demo of a control that visibly does nothing is a worse demo than |
| 137 | +// one that responds. Resets to this default on every page reload; never persisted anywhere. |
| 138 | +let demoGovernorState: GovernorPauseState = { paused: false, reason: null, pausedAt: null }; |
| 139 | + |
| 140 | +export function getDemoGovernorState(): GovernorPauseState { |
| 141 | + return demoGovernorState; |
| 142 | +} |
| 143 | + |
| 144 | +export function setDemoGovernorPaused(reason: string | null): GovernorPauseState { |
| 145 | + demoGovernorState = { paused: true, reason, pausedAt: new Date().toISOString() }; |
| 146 | + return demoGovernorState; |
| 147 | +} |
| 148 | + |
| 149 | +export function setDemoGovernorResumed(): GovernorPauseState { |
| 150 | + demoGovernorState = { paused: false, reason: null, pausedAt: null }; |
| 151 | + return demoGovernorState; |
| 152 | +} |
0 commit comments