@telemetryos/cli 1.11.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/commands/claude-code.d.ts +2 -0
  3. package/dist/commands/claude-code.js +29 -0
  4. package/dist/commands/init.js +22 -9
  5. package/dist/index.js +2 -0
  6. package/dist/services/create-project.d.ts +13 -0
  7. package/dist/services/create-project.js +188 -0
  8. package/dist/services/project-config.d.ts +3 -0
  9. package/dist/services/project-config.js +3 -0
  10. package/dist/services/run-server.js +186 -60
  11. package/dist/utils/ansi.d.ts +1 -0
  12. package/dist/utils/ansi.js +1 -0
  13. package/dist/utils/template.d.ts +2 -0
  14. package/dist/utils/template.js +30 -0
  15. package/package.json +4 -4
  16. package/templates/{vite-react-typescript → claude-code}/CLAUDE.md +10 -3
  17. package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-architecture/SKILL.md +138 -61
  18. package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-debugging/SKILL.md +2 -2
  19. package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-media-api/SKILL.md +97 -10
  20. package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-multi-mode/SKILL.md +97 -4
  21. package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-requirements/SKILL.md +70 -5
  22. package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-store-sync/SKILL.md +4 -2
  23. package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-weather-api/SKILL.md +7 -6
  24. package/templates/claude-code/_claude/skills/tos-web-ui-design/SKILL.md +373 -0
  25. package/templates/vite-react-typescript/_gitignore +4 -2
  26. package/templates/vite-react-typescript/telemetry.config.json +2 -1
  27. package/templates/vite-react-typescript-web/_gitignore +32 -0
  28. package/templates/vite-react-typescript-web/assets/telemetryos-wordmark.svg +11 -0
  29. package/templates/vite-react-typescript-web/assets/tos-app.svg +12 -0
  30. package/templates/vite-react-typescript-web/index.html +15 -0
  31. package/templates/vite-react-typescript-web/package.json +24 -0
  32. package/templates/vite-react-typescript-web/src/App.tsx +25 -0
  33. package/templates/vite-react-typescript-web/src/hooks/store.ts +8 -0
  34. package/templates/vite-react-typescript-web/src/index.css +24 -0
  35. package/templates/vite-react-typescript-web/src/index.tsx +11 -0
  36. package/templates/vite-react-typescript-web/src/views/Render.css +67 -0
  37. package/templates/vite-react-typescript-web/src/views/Render.tsx +44 -0
  38. package/templates/vite-react-typescript-web/src/views/Settings.tsx +72 -0
  39. package/templates/vite-react-typescript-web/src/views/Web.css +105 -0
  40. package/templates/vite-react-typescript-web/src/views/Web.tsx +52 -0
  41. package/templates/vite-react-typescript-web/telemetry.config.json +16 -0
  42. package/templates/vite-react-typescript-web/tsconfig.json +19 -0
  43. package/templates/vite-react-typescript-web/vite.config.ts +18 -0
  44. package/templates/vite-react-typescript/_claude/skills/tos-render-design/SKILL.md +0 -624
  45. /package/templates/{vite-react-typescript → claude-code}/AGENTS.md +0 -0
  46. /package/templates/{vite-react-typescript → claude-code}/_claude/settings.local.json +0 -0
  47. /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-proxy-fetch/SKILL.md +0 -0
  48. /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-render-kiosk-design/SKILL.md +0 -0
  49. /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-render-signage-design/SKILL.md +0 -0
  50. /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-render-ui-design/SKILL.md +0 -0
  51. /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-settings-ui/SKILL.md +0 -0
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  name: tos-architecture
3
- description: Understand TelemetryOS two-mount-point architecture (Render vs Settings). Use when debugging mount-point issues or understanding how Settings and Render communicate.
3
+ description: Understand TelemetryOS mount point architecture (Render, Settings, and optional Web). Use when debugging mount-point issues or understanding how mount points communicate through the store.
4
4
  ---
5
5
 
6
6
  # TelemetryOS Architecture
7
7
 
8
8
  TelemetryOS applications run on digital signage devices (TVs, kiosks, displays). Understanding the architecture is key to building effective apps.
9
9
 
10
- ## Two Mount Points
10
+ ## Mount Points
11
11
 
12
- Every standard TOS app has two entry points:
12
+ Every TOS app has at least two entry points (Render and Settings). Apps that need a browser-accessible interface add a third: the Web mount point.
13
13
 
14
14
  ### /render - Device Display
15
15
 
@@ -19,7 +19,7 @@ Every standard TOS app has two entry points:
19
19
  │ (TV, Kiosk, Digital Sign) │
20
20
  │ │
21
21
  │ ┌───────────────────────────┐ │
22
- │ │ Chrome Browser │ │
22
+ │ │ TelemetryOS Electron App │ │
23
23
  │ │ ┌─────────────────────┐ │ │
24
24
  │ │ │ Your App │ │ │
25
25
  │ │ │ /render route │ │ │
@@ -33,7 +33,7 @@ Every standard TOS app has two entry points:
33
33
 
34
34
  **Characteristics:**
35
35
  - Runs on physical device (TelemetryOS player)
36
- - Chrome browser in iframe sandbox
36
+ - Iframe sandbox in Electron app
37
37
  - Has access to `store().device` (device-local storage)
38
38
  - Displays content to viewers/customers
39
39
  - Full screen, optimized for viewing from distance
@@ -66,6 +66,38 @@ Every standard TOS app has two entry points:
66
66
  - Side panel in Studio editor
67
67
  - Form-based controls for settings
68
68
 
69
+ ### /web - Browser-Accessible Interface (Optional)
70
+
71
+ ```
72
+ ┌─────────────────────────────────┐
73
+ │ Any Browser (Phone/Tablet/ │
74
+ │ Desktop — Staff or Public) │
75
+ │ │
76
+ │ ┌───────────────────────────┐ │
77
+ │ │ TelemetryOS Web Host │ │
78
+ │ │ ┌─────────────────────┐ │ │
79
+ │ │ │ Your App │ │ │
80
+ │ │ │ /app-name route │ │ │
81
+ │ │ │ │ │ │
82
+ │ │ │ Operator desk, │ │ │
83
+ │ │ │ public form, staff │ │ │
84
+ │ │ │ dashboard │ │ │
85
+ │ │ └─────────────────────┘ │ │
86
+ │ └───────────────────────────┘ │
87
+ └─────────────────────────────────┘
88
+ ```
89
+
90
+ **Characteristics:**
91
+ - Runs in any browser, accessible via URL
92
+ - Can be staff-facing (private) or public-facing
93
+ - Application and dynamic namespace store scopes only
94
+ - **NO access to `store().instance` or `store().device`**
95
+ - Multi-level navigation with React Router
96
+ - Standard HTML/CSS (no SDK settings components, no UI scaling)
97
+ - Always pairs with multi-mode architecture and entity-driven routing
98
+
99
+ See `tos-web-ui-design` for full design patterns.
100
+
69
101
  ## Communication Flow
70
102
 
71
103
  ```
@@ -78,10 +110,31 @@ Every standard TOS app has two entry points:
78
110
  └─────────────┘ └─────────────────┘ └─────────────┘
79
111
  ```
80
112
 
81
- 1. **Settings writes** to instance store via hooks
82
- 2. **Render subscribes** to instance store via same hooks
83
- 3. Changes sync automatically (real-time)
84
- 4. Same hooks work in both mount points
113
+ With a Web mount point, communication extends through shared namespaces:
114
+
115
+ ```
116
+ ┌─────────────┐ ┌─────────────────┐ ┌─────────────┐
117
+ │ Settings │ ───▶ │ store(). │ ◀─── │ Web │
118
+ │ /settings │ WRITE │ shared() │ READ │ /app-name │
119
+ │ │ │ namespace │ +WRITE │ │
120
+ │ Admin │ │ │ │ Operator │
121
+ │ configures │ │ { queues, │ │ manages │
122
+ │ entities │ │ counters } │ │ live data │
123
+ └─────────────┘ └─────────────────┘ └─────────────┘
124
+
125
+ ▼ READ + SUB
126
+ ┌─────────────┐
127
+ │ Render │
128
+ │ /render │
129
+ │ Displays │
130
+ │ live data │
131
+ └─────────────┘
132
+ ```
133
+
134
+ 1. **Settings writes** configuration to instance and application store
135
+ 2. **Web reads and writes** entity-scoped data via dynamic namespace store
136
+ 3. **Render subscribes** and displays changes in real-time
137
+ 4. Same store hooks work across all mount points (scoped by what each can access)
85
138
 
86
139
  ## Project Structure
87
140
 
@@ -91,9 +144,12 @@ src/
91
144
  ├── App.tsx # Router - directs to correct view
92
145
  ├── views/
93
146
  │ ├── Settings.tsx # /settings mount point
94
- └── Render.tsx # /render mount point
147
+ ├── Render.tsx # /render mount point
148
+ │ ├── Render.css # /render styles
149
+ │ ├── Web.tsx # /app-name mount point (if using web)
150
+ │ └── Web.css # Web view styles (if using web)
95
151
  ├── hooks/
96
- │ └── store.ts # Shared store hooks (used in both views)
152
+ │ └── store.ts # Shared store hooks (used across all mount points)
97
153
  ├── components/ # Reusable UI components
98
154
  ├── types/ # TypeScript interfaces
99
155
  └── utils/ # Helper functions
@@ -103,29 +159,34 @@ src/
103
159
 
104
160
  ```typescript
105
161
  // App.tsx
162
+ import { createBrowserRouter, RouterProvider } from 'react-router'
106
163
  import Settings from './views/Settings'
107
164
  import Render from './views/Render'
108
165
 
109
- export function App() {
110
- const path = window.location.pathname
111
-
112
- if (path === '/settings') return <Settings />
113
- if (path === '/render') return <Render />
166
+ const router = createBrowserRouter([
167
+ { path: '/render', element: <Render /> },
168
+ { path: '/settings', element: <Settings /> },
169
+ ])
114
170
 
115
- return <div>Invalid mount point: {path}</div>
171
+ export function App() {
172
+ return <RouterProvider router={router} />
116
173
  }
117
174
  ```
118
175
 
119
- Or with React Router:
176
+ With a web mount point, add routes under the app name:
120
177
 
121
178
  ```typescript
122
179
  import { createBrowserRouter, RouterProvider } from 'react-router'
123
- import Settings from './views/Settings'
124
- import Render from './views/Render'
180
+ import { Render } from './views/Render'
181
+ import { Settings } from './views/Settings'
182
+ import { WebEntities, WebDetail, WebOperator } from './views/Web'
125
183
 
126
184
  const router = createBrowserRouter([
127
- { path: '/render', element: <Render /> },
128
- { path: '/settings', element: <Settings /> },
185
+ { path: '/render', Component: Render },
186
+ { path: '/settings', Component: Settings },
187
+ { path: '/my-app', Component: WebEntities },
188
+ { path: '/my-app/locations/:locationName', Component: WebDetail },
189
+ { path: '/my-app/locations/:locationName/counters/:counterId', Component: WebOperator },
129
190
  ])
130
191
 
131
192
  export function App() {
@@ -157,6 +218,15 @@ export function App() {
157
218
  - Form-based interaction
158
219
  - Preview pane integration
159
220
 
221
+ ### Web Only
222
+
223
+ - Standard HTML/CSS (no SDK settings components)
224
+ - Runs in any browser (phone, tablet, desktop)
225
+ - `store().application` and `store().shared(namespace)` only
226
+ - NO access to `store().instance` or `store().device`
227
+ - Multi-level React Router navigation
228
+ - No UI scaling (`useUiScaleToSetRem` not used)
229
+
160
230
  ## telemetry.config.json
161
231
 
162
232
  ```json
@@ -174,12 +244,32 @@ export function App() {
174
244
  }
175
245
  ```
176
246
 
247
+ With a web mount point:
248
+
249
+ ```json
250
+ {
251
+ "name": "my-app",
252
+ "version": "1.0.0",
253
+ "useSpaRouting": true,
254
+ "mountPoints": {
255
+ "render": "/render",
256
+ "settings": "/settings",
257
+ "web": "/my-app"
258
+ },
259
+ "devServer": {
260
+ "runCommand": "vite --port 3000",
261
+ "url": "http://localhost:3000"
262
+ }
263
+ }
264
+ ```
265
+
177
266
  ### Mount Point Configuration
178
267
 
179
- | Mount Point | Purpose | Route |
180
- |-------------|---------|-------|
181
- | render | Device display | /render |
182
- | settings | Admin config UI | /settings |
268
+ | Mount Point | Purpose | Route | Store Access |
269
+ |-------------|---------|-------|--------------|
270
+ | render | Device display | /render | instance, application, device, shared |
271
+ | settings | Admin config UI | /settings | instance, application, shared |
272
+ | web (optional) | Browser interface | /app-name | application, shared |
183
273
 
184
274
  ### Optional: Workers
185
275
 
@@ -198,34 +288,32 @@ export function App() {
198
288
 
199
289
  ```
200
290
  ┌────────────────────────────────────────────────────────────────┐
201
- Account
291
+ Account
292
+ │ │
202
293
  │ ┌──────────────────────────────────────────────────────────┐ │
203
- │ │ store().application │ │
204
- │ │ Shared across ALL instances of this app │ │
205
- │ │ (API keys, account-wide settings) │ │
294
+ │ │ store().shared('namespace') │ │
295
+ │ │ Dynamic namespace scoped by entity │ │
296
+ │ │ Accessible from: Settings, Render, Web │ │
206
297
  │ └──────────────────────────────────────────────────────────┘ │
207
-
208
- │ ┌─────────────────────┐ ┌─────────────────────┐ │
209
- │ │ App Instance 1 │ │ App Instance 2 │ │
210
- │ │ instance store │ │ instance store │ │
211
- │ │ (Settings↔Render) │ │ (Settings↔Render) │ │
212
- │ └─────────────────────┘ └─────────────────────┘ │
213
- └────────────────────────────────────────────────────────────────┘
214
-
215
- ┌────────────────────────────────────────────────────────────────┐
216
- │ Physical Device │
298
+
217
299
  │ ┌──────────────────────────────────────────────────────────┐ │
218
- │ │ store().device │ │
219
- │ │ Local to this device only (Render only!) │ │
220
- │ │ (Cache, calibration, device-specific data) │ │
300
+ │ │ Application │ │
301
+ │ │ │ │
302
+ │ │ ┌────────────────────────────────────────────────────┐ │ │
303
+ │ │ │ store().application │ │ │
304
+ │ │ │ Shared across ALL instances of this app │ │ │
305
+ │ │ │ (API keys, account-wide settings) │ │ │
306
+ │ │ │ Accessible from: Settings, Render, Web │ │ │
307
+ │ │ └────────────────────────────────────────────────────┘ │ │
308
+ │ │ │ │
309
+ │ │ ┌──────────────────────┐ ┌──────────────────────┐ │ │
310
+ │ │ │ Device │ │ Instance │ │ │
311
+ │ │ │ store().device │ │ store().instance │ │ │
312
+ │ │ │ Render only! │ │ Settings ↔ Render │ │ │
313
+ │ │ │ (Cache, calibration)│ │ NOT in Web │ │ │
314
+ │ │ └──────────────────────┘ └──────────────────────┘ │ │
221
315
  │ └──────────────────────────────────────────────────────────┘ │
222
316
  └────────────────────────────────────────────────────────────────┘
223
-
224
- ┌────────────────────────────────────────────────────────────────┐
225
- │ store().shared('namespace') │
226
- │ Inter-app communication (any app can read/write) │
227
- │ (Weather data sharing, event broadcasting) │
228
- └────────────────────────────────────────────────────────────────┘
229
317
  ```
230
318
 
231
319
  ## Development Workflow
@@ -236,6 +324,7 @@ export function App() {
236
324
  Both the render and settings mounts points are visible in the development host.
237
325
  The Render mount point is presented in a resizable pane.
238
326
  The Settings mount point shows in the right sidebar.
327
+ The Web mount point (if configured) appears as a tab in the development host, displayed in the same area as the other mount points.
239
328
 
240
329
 
241
330
  ### Build & Deploy
@@ -243,10 +332,6 @@ The Settings mount point shows in the right sidebar.
243
332
  ```bash
244
333
  # Build production
245
334
  npm run build
246
-
247
- # Deploy via Git
248
- git add . && git commit -m "Update" && git push
249
- # GitHub integration auto-deploys
250
335
  ```
251
336
 
252
337
  ## Common Patterns
@@ -265,14 +350,6 @@ return <WeatherDisplay city={city} />
265
350
 
266
351
  ## Debugging
267
352
 
268
- ### Check Current Path
269
-
270
- ```typescript
271
- console.log('Current path:', window.location.pathname)
272
- console.log('Is Settings:', window.location.pathname === '/settings')
273
- console.log('Is Render:', window.location.pathname === '/render')
274
- ```
275
-
276
353
  ### Verify SDK Configuration
277
354
 
278
355
  ```typescript
@@ -39,10 +39,10 @@ ReactDOM.createRoot(document.getElementById('root')!).render(<App />)
39
39
  **Solution:**
40
40
  ```typescript
41
41
  // WRONG - in Settings.tsx
42
- const [, value, setValue] = useMyState() // using createUseDeviceStoreState
42
+ const [isLoading, value, setValue] = useMyState() // using createUseDeviceStoreState
43
43
 
44
44
  // CORRECT - use instance scope
45
- const [, value, setValue] = useMyState() // using createUseInstanceStoreState
45
+ const [isLoading, value, setValue] = useMyState() // using createUseInstanceStoreState
46
46
  ```
47
47
 
48
48
  **Scope guide:**
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: tos-media-api
3
- description: Access TelemetryOS Media Library for images, videos, and files. Use when building apps that display user-uploaded media content.
3
+ description: Access TelemetryOS Media Library for images, videos, and files. Use when building apps that display or select user-uploaded media content.
4
4
  ---
5
5
 
6
6
  # TelemetryOS Media API
@@ -23,6 +23,10 @@ const tagged = await media().getAllByTag('banner')
23
23
 
24
24
  // Get single item by ID
25
25
  const item = await media().getById('content-id')
26
+
27
+ // Open media picker (user selects from full-screen dialog)
28
+ const selection = await media().openPicker()
29
+ const imagesOnly = await media().openPicker({ accept: ['image/*'] })
26
30
  ```
27
31
 
28
32
  ## Response Types
@@ -61,6 +65,29 @@ interface MediaContent {
61
65
  }
62
66
  ```
63
67
 
68
+ ### MediaSelection
69
+
70
+ Returned by `openPicker()` and used by `SettingsMediaSelect`.
71
+
72
+ ```typescript
73
+ interface MediaSelection {
74
+ id: string
75
+ name: string
76
+ thumbnailUrl: string // Thumbnail for preview
77
+ url: string // Full content URL
78
+ contentType: string // MIME type
79
+ }
80
+ ```
81
+
82
+ ### MediaPickerOptions
83
+
84
+ ```typescript
85
+ interface MediaPickerOptions {
86
+ accept?: string[] // Content type filters: ['image/*', 'video/*']
87
+ currentValue?: MediaSelection // Pre-select current value in picker
88
+ }
89
+ ```
90
+
64
91
  ## Common Patterns
65
92
 
66
93
  ### Folder Picker in Settings
@@ -123,6 +150,64 @@ export default function Settings() {
123
150
  }
124
151
  ```
125
152
 
153
+ ### Media Picker in Settings (SettingsMediaSelect)
154
+
155
+ Use the built-in `SettingsMediaSelect` component to let users pick a media item. Opens a full-screen picker dialog in the host window.
156
+
157
+ ```typescript
158
+ // hooks/store.ts
159
+ import { createUseInstanceStoreState } from '@telemetryos/sdk/react'
160
+ import type { MediaSelection } from '@telemetryos/sdk/react'
161
+
162
+ export const useMediaState = createUseInstanceStoreState<MediaSelection | null>('selectedMedia', null)
163
+ ```
164
+
165
+ ```typescript
166
+ // views/Settings.tsx
167
+ import {
168
+ SettingsContainer,
169
+ SettingsField,
170
+ SettingsLabel,
171
+ SettingsMediaSelect,
172
+ } from '@telemetryos/sdk/react'
173
+ import { useMediaState } from '../hooks/store'
174
+
175
+ export default function Settings() {
176
+ const [isLoading, selectedMedia, setSelectedMedia] = useMediaState()
177
+
178
+ return (
179
+ <SettingsContainer>
180
+ <SettingsField>
181
+ <SettingsLabel>Background Image</SettingsLabel>
182
+ <SettingsMediaSelect
183
+ value={selectedMedia}
184
+ onChange={setSelectedMedia}
185
+ accept={['image/*']}
186
+ disabled={isLoading}
187
+ />
188
+ </SettingsField>
189
+ </SettingsContainer>
190
+ )
191
+ }
192
+ ```
193
+
194
+ ### Using openPicker() Directly
195
+
196
+ For custom UI, call `media().openPicker()` directly instead of using the component.
197
+
198
+ ```typescript
199
+ import { media } from '@telemetryos/sdk'
200
+
201
+ const handleSelectMedia = async () => {
202
+ const selection = await media().openPicker({ accept: ['image/*'] })
203
+ if (selection) {
204
+ // selection has: id, name, thumbnailUrl, url, contentType
205
+ console.log('Selected:', selection.name, selection.url)
206
+ }
207
+ // selection is null if user cancelled
208
+ }
209
+ ```
210
+
126
211
  ### Image Gallery in Render
127
212
 
128
213
  ```typescript
@@ -249,15 +334,15 @@ import { media } from '@telemetryos/sdk'
249
334
  import { useFolderIdState, useIntervalState } from '../hooks/store'
250
335
 
251
336
  export default function Render() {
252
- const [, folderId] = useFolderIdState()
253
- const [, interval] = useIntervalState() // seconds
337
+ const [isLoadingFolder, folderId] = useFolderIdState()
338
+ const [isLoadingInterval, interval] = useIntervalState() // seconds
254
339
 
255
340
  const [images, setImages] = useState<string[]>([])
256
341
  const [currentIndex, setCurrentIndex] = useState(0)
257
342
 
258
343
  // Load images
259
344
  useEffect(() => {
260
- if (!folderId) return
345
+ if (isLoadingFolder || !folderId) return
261
346
 
262
347
  media().getAllByFolderId(folderId).then(content => {
263
348
  const urls = content
@@ -327,9 +412,11 @@ const activeContent = content.filter(item => {
327
412
 
328
413
  ## Tips
329
414
 
330
- 1. **Use publicUrls[0]** - First URL is the primary CDN URL
331
- 2. **Use thumbnailUrl for previews** - Smaller, faster loading
332
- 3. **Filter by contentType** - Ensure you're displaying compatible content
333
- 4. **Handle empty folders** - Show appropriate message when no content
334
- 5. **Lazy load images** - Use `loading="lazy"` for galleries
335
- 6. **Respect hidden flag** - Filter out hidden items unless intentional
415
+ 1. **Use `SettingsMediaSelect` for settings** - Preferred way to let users pick media in Settings views
416
+ 2. **Use `openPicker()` for custom UI** - When you need full control over the selection flow
417
+ 3. **Use publicUrls[0]** - First URL is the primary CDN URL
418
+ 4. **Use thumbnailUrl for previews** - Smaller, faster loading
419
+ 5. **Filter by contentType** - Ensure you're displaying compatible content
420
+ 6. **Handle empty folders** - Show appropriate message when no content
421
+ 7. **Lazy load images** - Use `loading="lazy"` for galleries
422
+ 8. **Respect hidden flag** - Filter out hidden items unless intentional
@@ -15,8 +15,10 @@ Listen for these indicators during requirements gathering:
15
15
  - "Kiosk and display" or "control panel and viewer"
16
16
  - "Different devices need different views of the same data"
17
17
  - Data that's organized by location, department, topic, or similar grouping
18
+ - "Staff need to manage/control from their phone or browser"
19
+ - "Public signup form" or "public status page" for the same data
18
20
 
19
- If ANY of these come up, this is a multi-mode app.
21
+ If ANY of these come up, this is a multi-mode app. If staff or the public need browser access outside the device, it also needs a **web mount point**.
20
22
 
21
23
  ---
22
24
 
@@ -80,12 +82,26 @@ Multi-mode apps use a 3-tier store pattern:
80
82
  └─────────────────────────────────────────────────────────────┘
81
83
  ```
82
84
 
85
+ With a web mount point, add a fourth access path:
86
+
87
+ ```
88
+ ┌─────────────────────────────────────────────────────────────┐
89
+ │ Web Mount Point (browser — staff or public) │
90
+ │ Entity from URL params, NOT instance store │
91
+ │ ┌──────────────┐ ┌──────────────┐ ┌─────────────────────┐ │
92
+ │ │ Reads/writes │ │ Same │ │ Same namespace │ │
93
+ │ │ namespace │ │ store hooks │ │ helper function │ │
94
+ │ └──────────────┘ └──────────────┘ └─────────────────────┘ │
95
+ └─────────────────────────────────────────────────────────────┘
96
+ ```
97
+
83
98
  **How it connects:**
84
99
 
85
- 1. Admin creates locations in Settings (application scope)
86
- 2. Each device selects a mode AND a location (instance scope)
87
- 3. All devices on the same location share the same data (dynamic namespace scope)
100
+ 1. Admin creates entities in Settings (application scope)
101
+ 2. Each device selects a mode AND an entity (instance scope)
102
+ 3. All devices on the same entity share the same data (dynamic namespace scope)
88
103
  4. A kiosk at "Location A" and a display at "Location A" see the same queues
104
+ 5. Web views navigate to entities via URL and read/write the same namespace data
89
105
 
90
106
  ---
91
107
 
@@ -240,6 +256,75 @@ export function Render() {
240
256
 
241
257
  ---
242
258
 
259
+ ## Web Mount Point Integration
260
+
261
+ When your multi-mode app needs a browser-accessible interface (operator desk, public form, staff dashboard), add a web mount point. The web view accesses the **same entity-scoped data** as Render, but gets the entity from URL params instead of instance store.
262
+
263
+ ### Entity from URL vs Instance Store
264
+
265
+ ```typescript
266
+ // Render — entity from instance store (admin picks which entity this device shows)
267
+ const [isLoading, selectedLocation] = useSelectedLocationStoreState()
268
+ if (isLoading) return <div>Loading...</div>
269
+ const ns = locationNamespace(selectedLocation || 'Location A')
270
+
271
+ // Web — entity from URL params (user navigates via links)
272
+ const { locationName } = useParams()
273
+ const ns = locationNamespace(decodeURIComponent(locationName || ''))
274
+
275
+ // Same namespace → same data → real-time sync
276
+ ```
277
+
278
+ ### Web Routing Maps to Entities
279
+
280
+ Web routes use the app name as base path, with entity-driven sub-routes. Choose path segments that match your domain:
281
+
282
+ ```typescript
283
+ // App.tsx — add web routes alongside render and settings
284
+ const router = createBrowserRouter([
285
+ { path: '/render', Component: Render },
286
+ { path: '/settings', Component: Settings },
287
+ { path: '/my-app', Component: WebEntities },
288
+ { path: '/my-app/locations/:locationName', Component: WebDetail },
289
+ { path: '/my-app/locations/:locationName/counters/:counterId', Component: WebOperator },
290
+ ])
291
+ ```
292
+
293
+ ### Store Access in Web
294
+
295
+ Web views can only use **application** and **dynamic namespace** store hooks. No instance or device store (there's no device/instance context in the browser):
296
+
297
+ ```typescript
298
+ // ✅ Works in web
299
+ const [isLoading, locations] = useLocationsStoreState() // application scope
300
+ const [isLoadingCounters, counters, setCounters] = useCountersStoreState(ns, 250) // dynamic namespace
301
+ if (isLoading || isLoadingCounters) return <div>Loading...</div>
302
+
303
+ // ❌ NOT available in web
304
+ const [isLoading, mode] = useModeStoreState() // instance scope — no context
305
+ const [isLoading, uiScale] = useUiScaleStoreState() // instance scope — no context
306
+ ```
307
+
308
+ ### Config
309
+
310
+ Add the web mount point to `telemetry.config.json`:
311
+
312
+ ```json
313
+ {
314
+ "name": "my-app",
315
+ "useSpaRouting": true,
316
+ "mountPoints": {
317
+ "render": "/render",
318
+ "settings": "/settings",
319
+ "web": "/my-app"
320
+ }
321
+ }
322
+ ```
323
+
324
+ See `tos-web-ui-design` for complete web view design patterns.
325
+
326
+ ---
327
+
243
328
  ## Settings Organization
244
329
 
245
330
  Order Settings sections intentionally:
@@ -357,3 +442,11 @@ Before implementing a multi-mode app, confirm:
357
442
  - [ ] Render view loads all hooks before branching on mode
358
443
  - [ ] Settings ordered: mode → entity selector → config → entity management
359
444
  - [ ] Entity management has add/rename/remove with at-least-one validation
445
+
446
+ If using a web mount point, also confirm:
447
+
448
+ - [ ] `telemetry.config.json` has `"web": "/app-name"` and `useSpaRouting: true`
449
+ - [ ] Web routes registered in App.tsx under the app name
450
+ - [ ] Web views get entity from URL params (not instance store)
451
+ - [ ] Web views only use application and dynamic namespace store hooks
452
+ - [ ] Entity names are URL-encoded in links and decoded in components