Skip to content

Commit 6764195

Browse files
committed
dashboard: hide "Run scripted demo" card by default
1 parent 73eff25 commit 6764195

5 files changed

Lines changed: 36 additions & 40 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Use `bun run dev` alone to (re)start. It already stops any running instance firs
4545

4646
- `DISABLE_POLLING=1` — disable all workers (useful for tests or when running standalone service scripts).
4747
- `POLL_INTERVAL_MS` — override poll interval. Default 1000ms. Standalone scripts still use their own 60000ms default.
48-
- `DEMO_MODE` — default-on. Controls the Dashboard's "Run demo now" endpoint (`POST /demo/run-scenario`). Only `DEMO_MODE=off` disables (returns 403).
48+
- `DEMO_MODE` — default-off. Set to `on` to render the Dashboard's "Run scripted demo" card and enable `POST /demo/run-scenario`. Anything else (unset, empty, `off`, `1`, `true`) hides the card and returns 403 from the endpoint.
4949

5050
## US Core demographic extension runtime note
5151

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ Most deployments only need a `.env` file. Defaults work for local demo.
9494
| `BILLING_APP` / `BILLING_FAC` || Receiving app/facility (MSH-5/6) in outbound BAR |
9595
| `DISABLE_POLLING` | unset | Set to `1` to disable all workers |
9696
| `POLL_INTERVAL_MS` | `1000` | Worker poll interval |
97-
| `DEMO_MODE` | on | Set to `off` to disable `/demo/run-scenario` |
97+
| `DEMO_MODE` | unset | Set to `on` to show the Dashboard's "Run scripted demo" card and enable `/demo/run-scenario` |
9898

9999
**Production checklist:**
100100
- Change `AIDBOX_CLIENT_SECRET` + `BOX_ADMIN_PASSWORD` in `docker-compose.yaml`.

src/api/demo-scenario.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,10 @@ export async function runDemoScenario(
160160
export function isDemoEnabled(
161161
env: NodeJS.ProcessEnv = process.env,
162162
): boolean {
163-
// Default-on: unset, empty, or any non-"off" value enables the endpoint.
164-
// Only DEMO_MODE=off disables. Matches the plan's env-flag semantics so
165-
// the demo ships as part of the default-dev experience.
166-
return env.DEMO_MODE !== "off";
163+
// Default-off: the scripted-demo card is hidden unless DEMO_MODE=on.
164+
// The endpoint is gated on the same flag so a hidden card can't be
165+
// poked by hand-crafted POSTs.
166+
return env.DEMO_MODE === "on";
167167
}
168168

169169
export async function handleRunDemoScenario(): Promise<Response> {

src/ui/pages/dashboard.ts

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -222,32 +222,28 @@ function renderHero(): string {
222222
}
223223

224224
function renderDemoConductor(demoEnabled: boolean): string {
225+
if (!demoEnabled) {return "";}
226+
225227
const steps = DEMO_STEPS.map((s, i) => {
226228
const arrow = i < DEMO_STEPS.length - 1 ? `<div class="flex-[0_1_24px]">${arrowSvg()}</div>` : "";
227229
return renderDemoStep(s, i) + arrow;
228230
}).join("");
229231

230-
const button = demoEnabled
231-
? `
232-
<button
233-
type="button"
234-
class="btn btn-primary text-[15px] py-3 px-5 justify-center"
235-
:disabled="running"
236-
x-on:click="run()"
237-
>
238-
<template x-if="!running">
239-
<span class="inline-flex items-center gap-1.5">${renderIcon("play", "sm")}<span x-text="done ? 'Demo sent ✓' : 'Run demo now'"></span></span>
240-
</template>
241-
<template x-if="running">
242-
<span class="inline-flex items-center gap-1.5"><span class="spinner"></span><span>Firing step <span x-text="Math.min(currentIndex + 1, ${DEMO_STEPS.length})"></span>/${DEMO_STEPS.length}…</span></span>
243-
</template>
244-
</button>
245-
`
246-
: `
247-
<button type="button" class="btn btn-primary text-[15px] py-3 px-5 justify-center opacity-60 cursor-not-allowed" disabled>
248-
${renderIcon("play", "sm")} Disabled (DEMO_MODE=off)
249-
</button>
250-
`;
232+
const button = `
233+
<button
234+
type="button"
235+
class="btn btn-primary text-[15px] py-3 px-5 justify-center"
236+
:disabled="running"
237+
x-on:click="run()"
238+
>
239+
<template x-if="!running">
240+
<span class="inline-flex items-center gap-1.5">${renderIcon("play", "sm")}<span x-text="done ? 'Demo sent ✓' : 'Run demo now'"></span></span>
241+
</template>
242+
<template x-if="running">
243+
<span class="inline-flex items-center gap-1.5"><span class="spinner"></span><span>Firing step <span x-text="Math.min(currentIndex + 1, ${DEMO_STEPS.length})"></span>/${DEMO_STEPS.length}…</span></span>
244+
</template>
245+
</button>
246+
`;
251247

252248
// Alpine state: `currentIndex` drives both the stepper's active-state
253249
// highlight and the button's "Firing step N/4" label. Server fires

test/unit/api/demo-scenario.test.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -220,33 +220,34 @@ describe("runDemoScenario", () => {
220220
});
221221

222222
describe("isDemoEnabled", () => {
223-
test("default-on when env var is unset", () => {
224-
expect(isDemoEnabled({})).toBe(true);
223+
test("default-off when env var is unset", () => {
224+
expect(isDemoEnabled({})).toBe(false);
225225
});
226226

227-
test("enabled for empty string", () => {
228-
expect(isDemoEnabled({ DEMO_MODE: "" })).toBe(true);
227+
test("disabled for empty string", () => {
228+
expect(isDemoEnabled({ DEMO_MODE: "" })).toBe(false);
229229
});
230230

231-
test("enabled for any non-'off' value", () => {
231+
test("enabled only for literal 'on'", () => {
232232
expect(isDemoEnabled({ DEMO_MODE: "on" })).toBe(true);
233-
expect(isDemoEnabled({ DEMO_MODE: "1" })).toBe(true);
234-
expect(isDemoEnabled({ DEMO_MODE: "staging" })).toBe(true);
235233
});
236234

237-
test("disabled only for literal 'off'", () => {
235+
test("disabled for other truthy-looking values", () => {
236+
expect(isDemoEnabled({ DEMO_MODE: "1" })).toBe(false);
237+
expect(isDemoEnabled({ DEMO_MODE: "true" })).toBe(false);
238238
expect(isDemoEnabled({ DEMO_MODE: "off" })).toBe(false);
239239
});
240240

241-
test("case-sensitive — 'OFF' is NOT treated as off", () => {
241+
test("case-sensitive — 'ON' is NOT treated as on", () => {
242242
// Documented tradeoff: matches the `DISABLE_POLLING=1` exact-string
243243
// convention elsewhere. If we ever want case-insensitive, normalize here.
244-
expect(isDemoEnabled({ DEMO_MODE: "OFF" })).toBe(true);
244+
expect(isDemoEnabled({ DEMO_MODE: "ON" })).toBe(false);
245245
});
246246
});
247247

248248
describe("handleRunDemoScenario", () => {
249-
test("returns 202 and fires the scenario when enabled", async () => {
249+
test("returns 202 and fires the scenario when DEMO_MODE=on", async () => {
250+
process.env.DEMO_MODE = "on";
250251
const res = await handleRunDemoScenario();
251252
expect(res.status).toBe(202);
252253
// Fire-and-forget — we can't easily await the scenario, but at least one
@@ -255,8 +256,7 @@ describe("handleRunDemoScenario", () => {
255256
expect(sendCalls.length).toBeGreaterThanOrEqual(1);
256257
});
257258

258-
test("returns 403 when DEMO_MODE=off", async () => {
259-
process.env.DEMO_MODE = "off";
259+
test("returns 403 when DEMO_MODE is unset", async () => {
260260
const res = await handleRunDemoScenario();
261261
expect(res.status).toBe(403);
262262
expect(await res.text()).toContain("disabled");

0 commit comments

Comments
 (0)