Skip to content

Commit 3922dcb

Browse files
committed
Add Control Center global Demo mode toggle.
Keep simulated catalog and seed showcase data available by default while preserving a runtime-only view when Demo mode is turned off.
1 parent db0a1c8 commit 3922dcb

15 files changed

Lines changed: 540 additions & 273 deletions

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
for homeostasis/attention; Smart Spaces facility/zone/energy pickers from inventory with simulated
1515
telemetry badges; solution domain tabs (SAR/Healthcare/Warehouse/Agriculture/Maritime) and ADAS
1616
labeled as composite views over shared fleet APIs.
17+
- **Control Center Demo mode:** global header toggle (persisted, default on) shows catalog reflex
18+
examples and treats Smart Spaces / solution composite notices as intentional showcase; turn off
19+
for runtime-only empty states.
1720

1821
### Added
1922

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1185,7 +1185,7 @@ parallel — see [docs/organizational-gates.md](docs/organizational-gates.md).
11851185
|----------|-------|---------------------|-------------|
11861186
| **P0** | Organizational gates | 30-day field soak, third-party security audit prep + sign-off ([#51](https://github.com/Davalgi/Spanda/issues/51)) | v1.0 blocker |
11871187
| **P1** | Differentiation hardening | Mission contracts, explainability, decision audit trail — **Control Center + REST wired** (2026-07-04) | Stable |
1188-
| **P1** | Control Center promotion | Playground auto-load, traceability matrix polish, structured assure/diagnose UI, Differentiation + Cognitive tabs — **shipped**; honesty pass (no silent catalog/sim demos; composite domain-tab labels) — **shipped** | Stable |
1188+
| **P1** | Control Center promotion | Playground auto-load, traceability matrix polish, structured assure/diagnose UI, Differentiation + Cognitive tabs — **shipped**; honesty pass + global **Demo mode** toggle (catalog/sim showcase vs runtime-only) — **shipped** | Stable |
11891189
| **P2** | Adoption blockers | VS Code Marketplace listing (PAT configured; Microsoft scanner review) | Stable (extension) |
11901190
| **P2** | Live paths | Swarm quorum, live vehicle I/O, env-gated AI/IoT golden paths | Experimental → Stable |
11911191
| **P2** | Compiler backend | Native codegen golden paths for selected HAL profiles | Experimental → Stable |

crates/spanda-api/src/static/control-center-ui/assets/control-center-C0TJD1Ys.js

Lines changed: 0 additions & 151 deletions
This file was deleted.

crates/spanda-api/src/static/control-center-ui/assets/control-center-CIkfFes5.js

Lines changed: 151 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/spanda-api/src/static/control-center-ui/assets/control-center-sOe3d297.css renamed to crates/spanda-api/src/static/control-center-ui/assets/control-center-DMi5R8Qj.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/spanda-api/src/static/control-center-ui/control-center.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Space+Grotesk:wght@400;600&display=swap"
1111
rel="stylesheet"
1212
/>
13-
<script type="module" crossorigin src="/assets/control-center-C0TJD1Ys.js"></script>
14-
<link rel="stylesheet" crossorigin href="/assets/control-center-sOe3d297.css">
13+
<script type="module" crossorigin src="/assets/control-center-CIkfFes5.js"></script>
14+
<link rel="stylesheet" crossorigin href="/assets/control-center-DMi5R8Qj.css">
1515
</head>
1616
<body>
1717
<div id="root"></div>

docs/control-center.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -839,11 +839,15 @@ spanda control-center serve \
839839
The **Smart Spaces** tab shows building and facility operations across residential, commercial, and
840840
campus deployments. See [solutions/smart-spaces.md](./solutions/smart-spaces.md).
841841

842-
**Honesty:** Facility / zone / device **inventory** comes from `--config`. Occupancy, environment,
843-
and energy **detail readings** are often seed / zone-profile simulated unless a live twin or
844-
`SPANDA_LIVE_BACNET=1` bridge is configured — the UI badges `source: simulated|zone_profile|twin`.
845-
Floor map is a **schematic** tiled from zone inventory, not a CAD plan. Pick facility / zone /
846-
energy system from the Selection controls (no hardcoded demo IDs).
842+
**Honesty / Demo mode:** Facility / zone / device **inventory** comes from `--config`. Occupancy,
843+
environment, and energy **detail readings** are often seed / zone-profile simulated unless a live
844+
twin or `SPANDA_LIVE_BACNET=1` bridge is configured — the UI badges
845+
`source: simulated|zone_profile|twin`. Floor map is a **schematic** tiled from zone inventory, not a
846+
CAD plan. Pick facility / zone / energy system from the Selection controls (no hardcoded demo IDs).
847+
848+
Use the header **Demo mode** toggle (default on, persisted in `localStorage`) to keep simulated
849+
telemetry and Cognitive catalog examples visible for showcase; turn it off for runtime-only empty
850+
states.
847851

848852
**Smart Spaces tab and panels:**
849853

@@ -1129,6 +1133,19 @@ spanda control-center serve --config spanda.toml --program rover.sd
11291133

11301134
---
11311135

1136+
## Demo mode
1137+
1138+
The main header includes a **Demo mode** toggle (default **on**, stored as
1139+
`spanda.control_center.demo_mode.v1` in `localStorage`):
1140+
1141+
| On (default) | Off |
1142+
|--------------|-----|
1143+
| Cognitive catalog reflex examples fill an empty runtime buffer | Runtime traces only (empty until reflexes fire) |
1144+
| Smart Spaces / solution notices describe intentional showcase data | Warn when detail telemetry is simulated / composite |
1145+
| Shell banner: “Demo mode on…” | No demo banner |
1146+
1147+
Turn Demo mode **off** for field/ops views that should never look populated by catalog or seed data.
1148+
11321149
## Control Center UI sections
11331150

11341151
The embedded HTML at `/` and the `@davalgi-spanda/web` Control Center panel share the same

packages/web/src/AdasPanel.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useCallback, useEffect, useState } from "react";
44
import { AnalyticsSection } from "./AnalyticsSection";
55
import { CcMiniStats, CcNotice, CcSection } from "./controlCenterUi";
6+
import { useControlCenterDemoMode } from "./useControlCenterDemoMode";
67
import { useRegisterTabRefresh } from "./useControlCenterTabRefresh";
78

89
type DevicePool = {
@@ -80,6 +81,7 @@ export function AdasPanel({ baseUrl, devicePool, alertCount = 0 }: Props) {
8081
// Example:
8182
// <AdasPanel baseUrl={url} devicePool={pool} />
8283

84+
const { demoMode } = useControlCenterDemoMode();
8385
const [health, setHealth] = useState<Record<string, unknown> | null>(null);
8486
const [assurance, setAssurance] = useState<Record<string, unknown> | null>(null);
8587
const [diagnosis, setDiagnosis] = useState<Record<string, unknown> | null>(null);
@@ -156,7 +158,14 @@ export function AdasPanel({ baseUrl, devicePool, alertCount = 0 }: Props) {
156158
<div className="cc-panel">
157159
{error && <div className="error">{error}</div>}
158160

159-
<CcNotice tone="info" title="ADAS composite — serve the ADAS blueprint for vehicle devices">
161+
<CcNotice
162+
tone={demoMode ? "info" : "warn"}
163+
title={
164+
demoMode
165+
? "Demo mode — ADAS composite showcase"
166+
: "ADAS composite — serve the ADAS blueprint for vehicle devices"
167+
}
168+
>
160169
Health, assurance, diagnosis, and OTA are live fleet APIs. Trust probes the first package
161170
found in the device pool (ADAS sensor candidates otherwise). Example:{" "}
162171
<code>

packages/web/src/ControlCenterMainHeader.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { TAB_DESCRIPTIONS, tabLabel } from "./controlCenterNavConfig";
22
import type { ControlCenterTab } from "./controlCenterRbac";
3+
import { CcBadge } from "./controlCenterUi";
4+
import { useControlCenterDemoMode } from "./useControlCenterDemoMode";
35
import { useControlCenterTabRefresh } from "./useControlCenterTabRefresh";
46

57
type Props = {
@@ -17,8 +19,26 @@ export function ControlCenterMainHeader({
1719
onCoreRefresh,
1820
onReadinessRefresh,
1921
}: Props) {
22+
// Render the active-tab title, Demo mode toggle, and refresh action.
23+
//
24+
// Parameters:
25+
// - `tab` — active Control Center tab id
26+
// - `pluginTitle` — optional plugin panel title overriding the tab label
27+
// - `coreBusy` — dashboard core refresh busy flag
28+
// - `onCoreRefresh` / `onReadinessRefresh` — shell-level refresh handlers
29+
//
30+
// Returns:
31+
// Main header element.
32+
//
33+
// Options:
34+
// None.
35+
//
36+
// Example:
37+
// <ControlCenterMainHeader tab="dashboard" pluginTitle={null} … />
38+
2039
const refreshCtx = useControlCenterTabRefresh();
2140
const panelHandler = refreshCtx?.handler ?? null;
41+
const { demoMode, toggleDemoMode } = useControlCenterDemoMode();
2242

2343
const sectionTitle = pluginTitle ?? tabLabel(tab);
2444
const sectionHint = pluginTitle ? null : TAB_DESCRIPTIONS[tab];
@@ -44,6 +64,15 @@ export function ControlCenterMainHeader({
4464
{sectionHint && <p className="cc-section-hint">{sectionHint}</p>}
4565
</div>
4666
<div className="cc-main-actions">
67+
<label className={`cc-demo-toggle${demoMode ? " is-on" : ""}`} title="Show simulated and catalog examples when live data is empty">
68+
<input
69+
type="checkbox"
70+
checked={demoMode}
71+
onChange={() => toggleDemoMode()}
72+
/>
73+
<span>Demo mode</span>
74+
{demoMode ? <CcBadge tone="info">on</CcBadge> : <CcBadge tone="neutral">off</CcBadge>}
75+
</label>
4776
<button type="button" onClick={handleRefresh} disabled={busy}>
4877
{busy ? "Refreshing…" : "Refresh"}
4978
</button>

packages/web/src/ControlCenterPanel.tsx

Lines changed: 123 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import { ControlCenterNav } from "./ControlCenterNav";
44
import { ControlCenterPluginPanel } from "./ControlCenterPluginPanel";
55
import { ControlCenterTabContent } from "./ControlCenterTabContent";
66
import { ControlCenterMainHeader } from "./ControlCenterMainHeader";
7+
import {
8+
ControlCenterDemoModeProvider,
9+
useControlCenterDemoMode,
10+
} from "./useControlCenterDemoMode";
711
import { ControlCenterTabRefreshProvider } from "./useControlCenterTabRefresh";
812
import {
913
addProfile,
@@ -24,6 +28,31 @@ type Props = {
2428
apiBase: string;
2529
};
2630

31+
function DemoModeBanner() {
32+
// Show a shell banner while Demo mode fills empty live state.
33+
//
34+
// Parameters:
35+
// None.
36+
//
37+
// Returns:
38+
// Banner element, or null when Demo mode is off.
39+
//
40+
// Options:
41+
// None.
42+
//
43+
// Example:
44+
// <DemoModeBanner />
45+
46+
const { demoMode } = useControlCenterDemoMode();
47+
if (!demoMode) return null;
48+
return (
49+
<div className="banner cc-demo-banner" role="status">
50+
Demo mode on — simulated telemetry and catalog examples fill empty live state. Turn off for
51+
runtime-only views.
52+
</div>
53+
);
54+
}
55+
2756
export function ControlCenterPanel({ apiBase }: Props) {
2857
const [profiles, setProfiles] = useState<ControlCenterProfile[]>(() => ensureProfile(apiBase));
2958
const [activeProfileId, setActiveProfileIdState] = useState(
@@ -138,99 +167,103 @@ export function ControlCenterPanel({ apiBase }: Props) {
138167
};
139168

140169
return (
141-
<div className="cc-shell">
142-
<aside className="cc-sidebar">
143-
<div className="cc-sidebar-brand">
144-
<div className="cc-sidebar-title-row">
145-
<span className="cc-sidebar-title">Control Center</span>
146-
<span className="cc-sidebar-version">v{controlCenterVersion}</span>
170+
<ControlCenterDemoModeProvider>
171+
<div className="cc-shell">
172+
<aside className="cc-sidebar">
173+
<div className="cc-sidebar-brand">
174+
<div className="cc-sidebar-title-row">
175+
<span className="cc-sidebar-title">Control Center</span>
176+
<span className="cc-sidebar-version">v{controlCenterVersion}</span>
177+
</div>
178+
<span className="cc-sidebar-host" title={base}>
179+
{apiHost}
180+
</span>
147181
</div>
148-
<span className="cc-sidebar-host" title={base}>
149-
{apiHost}
150-
</span>
151-
</div>
152-
<ControlCenterNav
153-
activeTab={pluginTab ? null : tab}
154-
pluginTab={pluginTab}
155-
pluginPanels={core.pluginPanels}
156-
isTabAllowed={isTabAllowed}
157-
onSelectTab={selectTab}
158-
onSelectPlugin={setPluginTab}
159-
/>
160-
<div className="cc-sidebar-footer">
161-
<ControlCenterAuthBanner
162-
compact
163-
apiHost={apiHost}
164-
effectiveRole={effectiveRole}
165-
roleMeta={roleMeta}
166-
keyId={rbacCtx?.key_id}
167-
tenantId={rbacCtx?.tenant_id ?? activeProfile?.tenantId}
168-
permissions={rbacCtx?.permissions ?? []}
169-
hasToken={hasToken}
170-
showAuthSetup={showAuthSetup}
171-
envKeyLocked={envKeyLocked}
172-
authError={authError}
173-
profiles={profiles}
174-
activeProfileId={activeProfile?.id}
175-
onSwitchProfile={switchProfile}
176-
onAddConnection={handleAddConnection}
177-
onVerify={verifyAndSetApiKey}
178-
onSignInWithOidc={signInWithOidc}
179-
oidcLoginEnabled={oidcLoginEnabled}
180-
onForget={forgetTokenAndProfile}
181-
onOpenSetup={() => setShowAuthSetup(true)}
182-
/>
183-
</div>
184-
</aside>
185-
186-
<main className="cc-main">
187-
<ControlCenterTabRefreshProvider tab={tab}>
188-
<ControlCenterMainHeader
189-
tab={tab}
190-
pluginTitle={activePlugin?.title ?? null}
191-
coreBusy={core.busy}
192-
onCoreRefresh={() => void core.load()}
193-
onReadinessRefresh={() => void core.runReadiness()}
182+
<ControlCenterNav
183+
activeTab={pluginTab ? null : tab}
184+
pluginTab={pluginTab}
185+
pluginPanels={core.pluginPanels}
186+
isTabAllowed={isTabAllowed}
187+
onSelectTab={selectTab}
188+
onSelectPlugin={setPluginTab}
194189
/>
190+
<div className="cc-sidebar-footer">
191+
<ControlCenterAuthBanner
192+
compact
193+
apiHost={apiHost}
194+
effectiveRole={effectiveRole}
195+
roleMeta={roleMeta}
196+
keyId={rbacCtx?.key_id}
197+
tenantId={rbacCtx?.tenant_id ?? activeProfile?.tenantId}
198+
permissions={rbacCtx?.permissions ?? []}
199+
hasToken={hasToken}
200+
showAuthSetup={showAuthSetup}
201+
envKeyLocked={envKeyLocked}
202+
authError={authError}
203+
profiles={profiles}
204+
activeProfileId={activeProfile?.id}
205+
onSwitchProfile={switchProfile}
206+
onAddConnection={handleAddConnection}
207+
onVerify={verifyAndSetApiKey}
208+
onSignInWithOidc={signInWithOidc}
209+
oidcLoginEnabled={oidcLoginEnabled}
210+
onForget={forgetTokenAndProfile}
211+
onOpenSetup={() => setShowAuthSetup(true)}
212+
/>
213+
</div>
214+
</aside>
195215

196-
{core.error && <div className="error">{core.error}</div>}
197-
{core.usingCache && (
198-
<div className="banner cc-offline-banner">
199-
Showing cached dashboard from {core.cacheAge ?? "earlier"} — API unreachable.
200-
</div>
201-
)}
202-
203-
<div className="cc-main-body">
204-
{pluginTab && activePlugin ? (
205-
<ControlCenterPluginPanel panel={activePlugin} baseUrl={base} />
206-
) : (
207-
<ControlCenterTabContent
208-
tab={tab}
209-
baseUrl={base}
210-
authHeaders={authHeaders}
211-
can={can}
212-
hasToken={hasToken}
213-
busy={core.busy}
214-
pool={core.pool}
215-
dashboard={core.dashboard}
216-
readiness={core.readiness}
217-
devices={core.devices}
218-
robots={core.robots}
219-
fleets={core.fleets}
220-
agents={core.agents}
221-
selectedRobot={core.selectedRobot}
222-
onRobotChange={core.setSelectedRobot}
223-
robotId={core.robotId}
224-
onNavigate={selectTab}
225-
onRunReadiness={() => void core.runReadiness()}
226-
onRefresh={() => void core.load()}
227-
onSignIn={() => setShowAuthSetup(true)}
228-
deviceWorkflow={deviceWorkflow}
229-
/>
216+
<main className="cc-main">
217+
<ControlCenterTabRefreshProvider tab={tab}>
218+
<ControlCenterMainHeader
219+
tab={tab}
220+
pluginTitle={activePlugin?.title ?? null}
221+
coreBusy={core.busy}
222+
onCoreRefresh={() => void core.load()}
223+
onReadinessRefresh={() => void core.runReadiness()}
224+
/>
225+
226+
<DemoModeBanner />
227+
228+
{core.error && <div className="error">{core.error}</div>}
229+
{core.usingCache && (
230+
<div className="banner cc-offline-banner">
231+
Showing cached dashboard from {core.cacheAge ?? "earlier"} — API unreachable.
232+
</div>
230233
)}
231-
</div>
232-
</ControlCenterTabRefreshProvider>
233-
</main>
234-
</div>
234+
235+
<div className="cc-main-body">
236+
{pluginTab && activePlugin ? (
237+
<ControlCenterPluginPanel panel={activePlugin} baseUrl={base} />
238+
) : (
239+
<ControlCenterTabContent
240+
tab={tab}
241+
baseUrl={base}
242+
authHeaders={authHeaders}
243+
can={can}
244+
hasToken={hasToken}
245+
busy={core.busy}
246+
pool={core.pool}
247+
dashboard={core.dashboard}
248+
readiness={core.readiness}
249+
devices={core.devices}
250+
robots={core.robots}
251+
fleets={core.fleets}
252+
agents={core.agents}
253+
selectedRobot={core.selectedRobot}
254+
onRobotChange={core.setSelectedRobot}
255+
robotId={core.robotId}
256+
onNavigate={selectTab}
257+
onRunReadiness={() => void core.runReadiness()}
258+
onRefresh={() => void core.load()}
259+
onSignIn={() => setShowAuthSetup(true)}
260+
deviceWorkflow={deviceWorkflow}
261+
/>
262+
)}
263+
</div>
264+
</ControlCenterTabRefreshProvider>
265+
</main>
266+
</div>
267+
</ControlCenterDemoModeProvider>
235268
);
236269
}

0 commit comments

Comments
 (0)