@telemetryos/cli 1.7.4 → 1.8.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.
- package/CHANGELOG.md +22 -0
- package/dist/commands/init.js +11 -0
- package/dist/services/generate-application.d.ts +1 -0
- package/dist/services/generate-application.js +127 -4
- package/dist/utils/validate-project-name.d.ts +19 -0
- package/dist/utils/validate-project-name.js +44 -0
- package/package.json +2 -2
- package/templates/vite-react-typescript/CLAUDE.md +68 -1244
- package/templates/vite-react-typescript/_claude/settings.local.json +17 -0
- package/templates/vite-react-typescript/_claude/skills/tos-architecture/SKILL.md +313 -0
- package/templates/vite-react-typescript/_claude/skills/tos-debugging/SKILL.md +299 -0
- package/templates/vite-react-typescript/_claude/skills/tos-media-api/SKILL.md +335 -0
- package/templates/vite-react-typescript/_claude/skills/tos-proxy-fetch/SKILL.md +319 -0
- package/templates/vite-react-typescript/_claude/skills/tos-render-design/SKILL.md +332 -0
- package/templates/vite-react-typescript/_claude/skills/tos-requirements/SKILL.md +252 -0
- package/templates/vite-react-typescript/_claude/skills/tos-settings-ui/SKILL.md +636 -0
- package/templates/vite-react-typescript/_claude/skills/tos-store-sync/SKILL.md +359 -0
- package/templates/vite-react-typescript/_claude/skills/tos-weather-api/SKILL.md +384 -0
- package/templates/vite-react-typescript/src/hooks/store.ts +3 -3
- package/templates/vite-react-typescript/src/index.css +0 -1
- package/templates/vite-react-typescript/src/views/Render.css +2 -1
- package/templates/vite-react-typescript/src/views/Render.tsx +2 -3
- package/templates/vite-react-typescript/src/views/Settings.tsx +2 -3
- package/templates/vite-react-typescript/AGENTS.md +0 -7
- /package/templates/vite-react-typescript/{gitignore → _gitignore} +0 -0
|
@@ -1,1295 +1,119 @@
|
|
|
1
|
-
# TelemetryOS
|
|
1
|
+
# TelemetryOS Application
|
|
2
2
|
|
|
3
|
-
**Application:**
|
|
4
|
-
**Purpose:** [What this application does]
|
|
3
|
+
**Application:** {{name}}
|
|
5
4
|
|
|
6
|
-
##
|
|
5
|
+
## Quick Start
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
3. **Workers** (optional) - Background JavaScript (runs on device, no DOM)
|
|
13
|
-
4. **Containers** (optional) - Docker containers for backend services (runs on device)
|
|
14
|
-
|
|
15
|
-
**Runtime Environment:**
|
|
16
|
-
- Chrome browser (platform-controlled version)
|
|
17
|
-
- Iframe sandbox execution
|
|
18
|
-
- Client-side only (no SSR, no Node.js APIs)
|
|
19
|
-
- Modern web APIs available (Fetch, WebSockets, WebGL, Canvas)
|
|
20
|
-
- External APIs require CORS proxy
|
|
21
|
-
|
|
22
|
-
**Communication:**
|
|
23
|
-
- Settings and Render share instance storage
|
|
24
|
-
- Settings saves config → Render subscribes to config
|
|
25
|
-
- Device storage only available in Render (not Settings)
|
|
26
|
-
|
|
27
|
-
## Project Structure
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
project-root/
|
|
31
|
-
├── telemetry.config.json # Platform configuration
|
|
32
|
-
├── package.json
|
|
33
|
-
├── tsconfig.json
|
|
34
|
-
├── vite.config.ts
|
|
35
|
-
├── index.html
|
|
36
|
-
└── src/
|
|
37
|
-
├── main.tsx # Entry point (configure SDK here)
|
|
38
|
-
├── App.tsx # Routing logic
|
|
39
|
-
├── views/
|
|
40
|
-
│ ├── Settings.tsx # /settings mount point
|
|
41
|
-
│ └── Render.tsx # /render mount point
|
|
42
|
-
├── components/ # Reusable components
|
|
43
|
-
├── hooks/
|
|
44
|
-
│ └── store.ts # Store state hooks (createUseStoreState)
|
|
45
|
-
├── types/ # TypeScript interfaces
|
|
46
|
-
└── utils/ # Helper functions
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Configuration Files
|
|
50
|
-
|
|
51
|
-
### telemetry.config.json (project root)
|
|
52
|
-
```json
|
|
53
|
-
{
|
|
54
|
-
"name": "app-name",
|
|
55
|
-
"version": "1.0.0",
|
|
56
|
-
"mountPoints": {
|
|
57
|
-
"render": "/render",
|
|
58
|
-
"settings": "/settings"
|
|
59
|
-
},
|
|
60
|
-
"backgroundWorkers": {
|
|
61
|
-
"background": "workers/background.js"
|
|
62
|
-
},
|
|
63
|
-
"serverWorkers": {
|
|
64
|
-
"api": "workers/api.js"
|
|
65
|
-
},
|
|
66
|
-
"devServer": {
|
|
67
|
-
"runCommand": "vite --port 3000",
|
|
68
|
-
"url": "http://localhost:3000"
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### package.json scripts
|
|
74
|
-
```json
|
|
75
|
-
{
|
|
76
|
-
"scripts": {
|
|
77
|
-
"dev": "vite",
|
|
78
|
-
"build": "tsc && vite build",
|
|
79
|
-
"preview": "vite preview"
|
|
80
|
-
},
|
|
81
|
-
"dependencies": {
|
|
82
|
-
"@telemetryos/sdk": "latest",
|
|
83
|
-
"react": "latest",
|
|
84
|
-
"react-dom": "latest"
|
|
85
|
-
},
|
|
86
|
-
"devDependencies": {
|
|
87
|
-
"@types/react": "latest",
|
|
88
|
-
"@types/react-dom": "latest",
|
|
89
|
-
"@vitejs/plugin-react": "latest",
|
|
90
|
-
"typescript": "latest",
|
|
91
|
-
"vite": "latest"
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
## Complete File Implementations
|
|
97
|
-
|
|
98
|
-
### src/main.tsx (Entry Point)
|
|
99
|
-
```typescript
|
|
100
|
-
import { configure } from '@telemetryos/sdk';
|
|
101
|
-
import React from 'react';
|
|
102
|
-
import ReactDOM from 'react-dom/client';
|
|
103
|
-
import App from './App';
|
|
104
|
-
import './index.css';
|
|
105
|
-
|
|
106
|
-
// Configure SDK ONCE before React renders
|
|
107
|
-
// Name must match telemetry.config.json
|
|
108
|
-
configure('app-name');
|
|
109
|
-
|
|
110
|
-
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
111
|
-
<React.StrictMode>
|
|
112
|
-
<App />
|
|
113
|
-
</React.StrictMode>
|
|
114
|
-
);
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### src/App.tsx (Routing)
|
|
118
|
-
```typescript
|
|
119
|
-
import Settings from './views/Settings';
|
|
120
|
-
import Render from './views/Render';
|
|
121
|
-
|
|
122
|
-
export default function App() {
|
|
123
|
-
const path = window.location.pathname;
|
|
124
|
-
|
|
125
|
-
if (path === '/settings') return <Settings />;
|
|
126
|
-
if (path === '/render') return <Render />;
|
|
127
|
-
|
|
128
|
-
return <div>Invalid mount point: {path}</div>;
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### src/hooks/store.ts (Store Hooks)
|
|
133
|
-
```typescript
|
|
134
|
-
import { createUseStoreState } from '@telemetryos/sdk/react'
|
|
135
|
-
|
|
136
|
-
// Create typed hooks for each store key
|
|
137
|
-
export const useTeamStoreState = createUseStoreState<string>('team', '')
|
|
138
|
-
export const useLeagueStoreState = createUseStoreState<string>('league', 'nfl')
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
### src/views/Settings.tsx (Complete Reference)
|
|
142
|
-
```typescript
|
|
143
|
-
import { store } from '@telemetryos/sdk'
|
|
144
|
-
import {
|
|
145
|
-
SettingsContainer,
|
|
146
|
-
SettingsField,
|
|
147
|
-
SettingsLabel,
|
|
148
|
-
SettingsInputFrame,
|
|
149
|
-
SettingsSelectFrame,
|
|
150
|
-
} from '@telemetryos/sdk/react'
|
|
151
|
-
import { useTeamStoreState, useLeagueStoreState } from '../hooks/store'
|
|
152
|
-
|
|
153
|
-
export default function Settings() {
|
|
154
|
-
const [isLoadingTeam, team, setTeam] = useTeamStoreState(store().instance)
|
|
155
|
-
const [isLoadingLeague, league, setLeague] = useLeagueStoreState(store().instance)
|
|
156
|
-
|
|
157
|
-
return (
|
|
158
|
-
<SettingsContainer>
|
|
159
|
-
<SettingsField>
|
|
160
|
-
<SettingsLabel>Team Name</SettingsLabel>
|
|
161
|
-
<SettingsInputFrame>
|
|
162
|
-
<input
|
|
163
|
-
type="text"
|
|
164
|
-
placeholder="Enter team name..."
|
|
165
|
-
disabled={isLoadingTeam}
|
|
166
|
-
value={team}
|
|
167
|
-
onChange={(e) => setTeam(e.target.value)}
|
|
168
|
-
/>
|
|
169
|
-
</SettingsInputFrame>
|
|
170
|
-
</SettingsField>
|
|
171
|
-
|
|
172
|
-
<SettingsField>
|
|
173
|
-
<SettingsLabel>League</SettingsLabel>
|
|
174
|
-
<SettingsSelectFrame>
|
|
175
|
-
<select
|
|
176
|
-
disabled={isLoadingLeague}
|
|
177
|
-
value={league}
|
|
178
|
-
onChange={(e) => setLeague(e.target.value)}
|
|
179
|
-
>
|
|
180
|
-
<option value="nfl">NFL</option>
|
|
181
|
-
<option value="nba">NBA</option>
|
|
182
|
-
<option value="mlb">MLB</option>
|
|
183
|
-
<option value="nhl">NHL</option>
|
|
184
|
-
</select>
|
|
185
|
-
</SettingsSelectFrame>
|
|
186
|
-
</SettingsField>
|
|
187
|
-
</SettingsContainer>
|
|
188
|
-
)
|
|
189
|
-
}
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
### src/views/Render.tsx (Complete Reference)
|
|
193
|
-
```typescript
|
|
194
|
-
import { useEffect, useState } from 'react'
|
|
195
|
-
import { proxy } from '@telemetryos/sdk'
|
|
196
|
-
import { store } from '@telemetryos/sdk'
|
|
197
|
-
import { useTeamStoreState, useLeagueStoreState } from '../hooks/store'
|
|
198
|
-
|
|
199
|
-
interface GameScore {
|
|
200
|
-
homeTeam: string
|
|
201
|
-
awayTeam: string
|
|
202
|
-
homeScore: number
|
|
203
|
-
awayScore: number
|
|
204
|
-
status: string
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export default function Render() {
|
|
208
|
-
// Use same hooks as Settings - automatically syncs when Settings changes
|
|
209
|
-
const [isLoadingTeam, team] = useTeamStoreState(store().instance)
|
|
210
|
-
const [isLoadingLeague, league] = useLeagueStoreState(store().instance)
|
|
211
|
-
|
|
212
|
-
const [score, setScore] = useState<GameScore | null>(null)
|
|
213
|
-
const [loading, setLoading] = useState(false)
|
|
214
|
-
const [error, setError] = useState<string | null>(null)
|
|
215
|
-
|
|
216
|
-
// Fetch scores when config changes
|
|
217
|
-
useEffect(() => {
|
|
218
|
-
if (!team) return
|
|
219
|
-
|
|
220
|
-
const fetchScore = async () => {
|
|
221
|
-
setLoading(true)
|
|
222
|
-
setError(null)
|
|
223
|
-
|
|
224
|
-
try {
|
|
225
|
-
// Platform handles caching automatically - no manual cache needed
|
|
226
|
-
const response = await proxy().fetch(
|
|
227
|
-
`https://api.sportsdata.io/v3/${league}/scores/json/GamesByTeam/${team}`
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
if (!response.ok) throw new Error(`API error: ${response.status}`)
|
|
231
|
-
|
|
232
|
-
const data = await response.json()
|
|
233
|
-
if (data.length > 0) {
|
|
234
|
-
const game = data[0]
|
|
235
|
-
setScore({
|
|
236
|
-
homeTeam: game.HomeTeam,
|
|
237
|
-
awayTeam: game.AwayTeam,
|
|
238
|
-
homeScore: game.HomeScore,
|
|
239
|
-
awayScore: game.AwayScore,
|
|
240
|
-
status: game.Status,
|
|
241
|
-
})
|
|
242
|
-
}
|
|
243
|
-
} catch (err) {
|
|
244
|
-
setError(err instanceof Error ? err.message : 'Unknown error')
|
|
245
|
-
} finally {
|
|
246
|
-
setLoading(false)
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
fetchScore()
|
|
251
|
-
}, [team, league])
|
|
252
|
-
|
|
253
|
-
// Loading state
|
|
254
|
-
if (isLoadingTeam || isLoadingLeague) return <div>Loading config...</div>
|
|
255
|
-
if (!team) return <div>Configure team in Settings</div>
|
|
256
|
-
if (loading && !score) return <div>Loading scores...</div>
|
|
257
|
-
if (error && !score) return <div>Error: {error}</div>
|
|
258
|
-
|
|
259
|
-
return (
|
|
260
|
-
<div>
|
|
261
|
-
<h1>{team} - {league.toUpperCase()}</h1>
|
|
262
|
-
{score && (
|
|
263
|
-
<div>
|
|
264
|
-
<div>{score.awayTeam} @ {score.homeTeam}</div>
|
|
265
|
-
<div>{score.awayScore} - {score.homeScore}</div>
|
|
266
|
-
<div>{score.status}</div>
|
|
267
|
-
</div>
|
|
268
|
-
)}
|
|
269
|
-
</div>
|
|
270
|
-
)
|
|
271
|
-
}
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
## SDK API Reference
|
|
275
|
-
|
|
276
|
-
Import from `@telemetryos/sdk`.
|
|
277
|
-
|
|
278
|
-
### Initialization
|
|
279
|
-
|
|
280
|
-
```typescript
|
|
281
|
-
configure(applicationName: string): void
|
|
282
|
-
```
|
|
283
|
-
- Call once in main.tsx before React renders
|
|
284
|
-
- Name must match telemetry.config.json
|
|
285
|
-
- Throws if called multiple times
|
|
286
|
-
|
|
287
|
-
### Storage API
|
|
288
|
-
|
|
289
|
-
**Type Signatures:**
|
|
290
|
-
```typescript
|
|
291
|
-
store().application.set<T>(key: string, value: T): Promise<boolean>
|
|
292
|
-
store().application.get<T>(key: string): Promise<T | undefined>
|
|
293
|
-
store().application.subscribe<T>(key: string, handler: (value: T | undefined) => void): Promise<boolean>
|
|
294
|
-
store().application.unsubscribe<T>(key: string, handler?: (value: T | undefined) => void): Promise<boolean>
|
|
295
|
-
store().application.delete(key: string): Promise<boolean>
|
|
296
|
-
|
|
297
|
-
// Same methods for instance, device, shared(namespace)
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
**Four Scopes:**
|
|
301
|
-
|
|
302
|
-
1. **application** - Shared across all instances of app in account
|
|
303
|
-
```typescript
|
|
304
|
-
await store().application.set('companyLogo', 'https://...');
|
|
305
|
-
const logo = await store().application.get<string>('companyLogo');
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
2. **instance** - This specific app instance (Settings ↔ Render communication)
|
|
309
|
-
```typescript
|
|
310
|
-
// Settings saves
|
|
311
|
-
await store().instance.set('config', { city: 'NYC' });
|
|
312
|
-
|
|
313
|
-
// Render subscribes
|
|
314
|
-
const handler = (newConfig) => updateDisplay(newConfig);
|
|
315
|
-
await store().instance.subscribe('config', handler);
|
|
316
|
-
|
|
317
|
-
// Later: unsubscribe
|
|
318
|
-
await store().instance.unsubscribe('config', handler);
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
3. **device** - This physical device only (NOT available in Settings)
|
|
322
|
-
```typescript
|
|
323
|
-
// Only in Render mount point
|
|
324
|
-
await store().device.set('cache', data);
|
|
325
|
-
const cached = await store().device.get<CacheType>('cache');
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
4. **shared(namespace)** - Inter-app communication
|
|
329
|
-
```typescript
|
|
330
|
-
// App A publishes
|
|
331
|
-
await store().shared('weather').set('temp', '72°F');
|
|
332
|
-
|
|
333
|
-
// App B subscribes
|
|
334
|
-
store().shared('weather').subscribe('temp', (temp) => console.log(temp));
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
**Constraints:**
|
|
338
|
-
- All operations timeout after 30 seconds (throws Error)
|
|
339
|
-
- Returns `Promise<boolean>` for set/delete/subscribe/unsubscribe (true = success)
|
|
340
|
-
- Returns `Promise<T | undefined>` for get
|
|
341
|
-
- To unsubscribe, call `unsubscribe(key, handler)` with the same handler function
|
|
342
|
-
- Device scope throws Error in Settings mount point
|
|
343
|
-
|
|
344
|
-
### Proxy API
|
|
345
|
-
|
|
346
|
-
```typescript
|
|
347
|
-
proxy().fetch(url: string, options?: RequestInit): Promise<Response>
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
- Same interface as standard fetch()
|
|
351
|
-
- Use when external APIs don't include CORS headers
|
|
352
|
-
- Returns standard Response object
|
|
353
|
-
- Regular `fetch()` works fine when CORS is not an issue (and has advanced caching in the player)
|
|
354
|
-
|
|
355
|
-
**Example:**
|
|
356
|
-
```typescript
|
|
357
|
-
import { proxy } from '@telemetryos/sdk';
|
|
358
|
-
|
|
359
|
-
const response = await proxy().fetch('https://api.example.com/data');
|
|
360
|
-
const json = await response.json();
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
### Media API
|
|
364
|
-
|
|
365
|
-
```typescript
|
|
366
|
-
media().getAllFolders(): Promise<MediaFolder[]>
|
|
367
|
-
media().getAllByFolderId(folderId: string): Promise<MediaContent[]>
|
|
368
|
-
media().getAllByTag(tagName: string): Promise<MediaContent[]>
|
|
369
|
-
media().getById(id: string): Promise<MediaContent>
|
|
370
|
-
|
|
371
|
-
interface MediaContent {
|
|
372
|
-
id: string;
|
|
373
|
-
contentFolderId: string;
|
|
374
|
-
contentType: string;
|
|
375
|
-
name: string;
|
|
376
|
-
description: string;
|
|
377
|
-
thumbnailUrl: string;
|
|
378
|
-
keys: string[];
|
|
379
|
-
publicUrls: string[];
|
|
380
|
-
hidden: boolean;
|
|
381
|
-
validFrom?: Date;
|
|
382
|
-
validTo?: Date;
|
|
383
|
-
createdAt: Date;
|
|
384
|
-
updatedAt: Date;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
interface MediaFolder {
|
|
388
|
-
id: string;
|
|
389
|
-
parentId: string;
|
|
390
|
-
name: string;
|
|
391
|
-
size: number;
|
|
392
|
-
default: boolean;
|
|
393
|
-
createdAt: Date;
|
|
394
|
-
updatedAt: Date;
|
|
395
|
-
}
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
### Playlist API
|
|
399
|
-
|
|
400
|
-
```typescript
|
|
401
|
-
playlist().nextPage(): Promise<boolean>
|
|
402
|
-
playlist().previousPage(): Promise<boolean>
|
|
403
|
-
playlist().setDuration(duration: number): Promise<boolean> // duration in milliseconds
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
### Overrides API
|
|
407
|
-
|
|
408
|
-
```typescript
|
|
409
|
-
overrides().setOverride(name: string): Promise<boolean>
|
|
410
|
-
overrides().clearOverride(name: string): Promise<boolean>
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
Note: Override names must be pre-configured in Freeform Editor.
|
|
414
|
-
|
|
415
|
-
### Platform Information
|
|
416
|
-
|
|
417
|
-
```typescript
|
|
418
|
-
accounts().getCurrent(): Promise<Account>
|
|
419
|
-
users().getCurrent(): Promise<User>
|
|
420
|
-
devices().getInformation(): Promise<DeviceInformation> // Render only
|
|
421
|
-
|
|
422
|
-
interface DeviceInformation {
|
|
423
|
-
deviceSerialNumber: string;
|
|
424
|
-
deviceModel: string;
|
|
425
|
-
deviceManufacturer: string;
|
|
426
|
-
devicePlatform: string;
|
|
427
|
-
}
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
### Environment API
|
|
431
|
-
|
|
432
|
-
```typescript
|
|
433
|
-
environment().getColorScheme(): Promise<'light' | 'dark' | 'system'>
|
|
434
|
-
environment().subscribeColorScheme(handler: (scheme: 'light' | 'dark' | 'system') => void): void
|
|
435
|
-
environment().unsubscribeColorScheme(handler: (scheme: 'light' | 'dark' | 'system') => void): void
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
**Example:**
|
|
439
|
-
```typescript
|
|
440
|
-
import { environment } from '@telemetryos/sdk';
|
|
441
|
-
|
|
442
|
-
// Get current color scheme
|
|
443
|
-
const scheme = await environment().getColorScheme();
|
|
444
|
-
|
|
445
|
-
// Subscribe to color scheme changes
|
|
446
|
-
environment().subscribeColorScheme((newScheme) => {
|
|
447
|
-
document.body.className = newScheme;
|
|
448
|
-
});
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
### Weather API
|
|
452
|
-
|
|
453
|
-
```typescript
|
|
454
|
-
weather().getConditions(params: WeatherRequestParams): Promise<WeatherConditions>
|
|
455
|
-
weather().getDailyForecast(params: DailyForecastParams): Promise<WeatherForecast[]>
|
|
456
|
-
weather().getHourlyForecast(params: HourlyForecastParams): Promise<WeatherForecast[]>
|
|
457
|
-
|
|
458
|
-
interface WeatherRequestParams {
|
|
459
|
-
city?: string; // City name (e.g., "New York" or "London, UK")
|
|
460
|
-
postalCode?: string; // Alternative to city
|
|
461
|
-
lat?: string; // Latitude (if city not provided)
|
|
462
|
-
lon?: string; // Longitude (if city not provided)
|
|
463
|
-
units?: 'imperial' | 'metric';
|
|
464
|
-
language?: string;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
interface DailyForecastParams extends WeatherRequestParams {
|
|
468
|
-
days?: number; // Number of days to forecast
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
interface HourlyForecastParams extends WeatherRequestParams {
|
|
472
|
-
hours?: number; // Number of hours to forecast
|
|
473
|
-
}
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
**Example:**
|
|
477
|
-
```typescript
|
|
478
|
-
import { weather } from '@telemetryos/sdk';
|
|
479
|
-
|
|
480
|
-
// Get current conditions
|
|
481
|
-
const conditions = await weather().getConditions({
|
|
482
|
-
city: 'New York',
|
|
483
|
-
units: 'imperial'
|
|
484
|
-
});
|
|
485
|
-
console.log(`${conditions.Temp}°F - ${conditions.WeatherText}`);
|
|
486
|
-
|
|
487
|
-
// Get 5-day forecast
|
|
488
|
-
const forecast = await weather().getDailyForecast({
|
|
489
|
-
city: 'London',
|
|
490
|
-
units: 'metric',
|
|
491
|
-
days: 5
|
|
492
|
-
});
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
### Applications API
|
|
496
|
-
|
|
497
|
-
```typescript
|
|
498
|
-
applications().getAllByMountPoint(mountPoint: string): Promise<Application[]>
|
|
499
|
-
applications().getByName(name: string): Promise<Application | null>
|
|
500
|
-
applications().setDependencies(specifiers: string[]): Promise<{ ready: string[], unavailable: string[] }>
|
|
501
|
-
|
|
502
|
-
interface Application {
|
|
503
|
-
name: string;
|
|
504
|
-
mountPoints: Record<string, { path: string }>;
|
|
505
|
-
}
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
**Example:**
|
|
509
|
-
```typescript
|
|
510
|
-
import { applications } from '@telemetryos/sdk';
|
|
511
|
-
|
|
512
|
-
// Find all apps with a specific mount point
|
|
513
|
-
const widgets = await applications().getAllByMountPoint('widget');
|
|
514
|
-
|
|
515
|
-
// Get a specific app by name
|
|
516
|
-
const mapApp = await applications().getByName('interactive-map');
|
|
517
|
-
|
|
518
|
-
// Declare dependencies before loading sub-apps
|
|
519
|
-
const result = await applications().setDependencies(['app-specifier-hash']);
|
|
520
|
-
if (result.ready.includes('app-specifier-hash')) {
|
|
521
|
-
// Safe to load in iframe
|
|
522
|
-
}
|
|
7
|
+
```bash
|
|
8
|
+
npm install # Install dependencies
|
|
9
|
+
npm run build # Build and check for TypeScript errors
|
|
10
|
+
tos serve # Start dev server (or: npm run dev)
|
|
523
11
|
```
|
|
524
12
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
Import from `@telemetryos/sdk/react`. These hooks simplify store interactions by handling subscriptions, loading states, and cleanup automatically.
|
|
528
|
-
|
|
529
|
-
### useStoreState
|
|
530
|
-
|
|
531
|
-
Syncs React state with a store key. Handles subscription/unsubscription automatically.
|
|
13
|
+
**IMPORTANT:** Always run `npm run build` after making changes to check for TypeScript errors. Do not rely solely on the dev server.
|
|
532
14
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
function MyComponent() {
|
|
538
|
-
const [isLoading, value, setValue] = useStoreState<string>(
|
|
539
|
-
store().instance, // Store slice
|
|
540
|
-
'myKey', // Key name
|
|
541
|
-
'default value', // Initial state (optional)
|
|
542
|
-
300 // Debounce delay in ms (optional)
|
|
543
|
-
)
|
|
544
|
-
|
|
545
|
-
return (
|
|
546
|
-
<input
|
|
547
|
-
disabled={isLoading}
|
|
548
|
-
value={value}
|
|
549
|
-
onChange={(e) => setValue(e.target.value)}
|
|
550
|
-
/>
|
|
551
|
-
)
|
|
552
|
-
}
|
|
553
|
-
```
|
|
15
|
+
**Development Host:** http://localhost:2026
|
|
16
|
+
- Settings: http://localhost:2026/settings
|
|
17
|
+
- Render: http://localhost:2026/render
|
|
554
18
|
|
|
555
|
-
|
|
19
|
+
## Architecture
|
|
556
20
|
|
|
557
|
-
|
|
558
|
-
- `value` - Current value (from store or local optimistic update)
|
|
559
|
-
- `setValue` - Updates both local state and store (with optional debounce)
|
|
21
|
+
TelemetryOS apps have two mount points:
|
|
560
22
|
|
|
561
|
-
|
|
23
|
+
| Mount | Purpose | Runs On |
|
|
24
|
+
|-------|---------|---------|
|
|
25
|
+
| `/render` | Content displayed on devices | Physical device (TV, kiosk) |
|
|
26
|
+
| `/settings` | Configuration UI | Studio admin portal |
|
|
562
27
|
|
|
563
|
-
|
|
28
|
+
Settings and Render communicate via instance store hooks.
|
|
564
29
|
|
|
565
|
-
|
|
566
|
-
// hooks/store.ts
|
|
567
|
-
import { createUseStoreState } from '@telemetryos/sdk/react'
|
|
30
|
+
## Project Structure
|
|
568
31
|
|
|
569
|
-
// Create typed hooks for each store key
|
|
570
|
-
export const useTeamStoreState = createUseStoreState<string>('team', '')
|
|
571
|
-
export const useLeagueStoreState = createUseStoreState<string>('league', 'nfl')
|
|
572
|
-
export const useRefreshIntervalStoreState = createUseStoreState<number>('refreshInterval', 30)
|
|
573
32
|
```
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
const [isLoadingLeague, league, setLeague] = useLeagueStoreState(store().instance)
|
|
583
|
-
// ... use in components
|
|
584
|
-
}
|
|
33
|
+
src/
|
|
34
|
+
├── index.tsx # Entry point (configure SDK here)
|
|
35
|
+
├── App.tsx # Mount point routing
|
|
36
|
+
├── views/
|
|
37
|
+
│ ├── Settings.tsx # Configuration UI
|
|
38
|
+
│ └── Render.tsx # Display content
|
|
39
|
+
└── hooks/
|
|
40
|
+
└── store.ts # Store state hooks
|
|
585
41
|
```
|
|
586
42
|
|
|
587
|
-
|
|
588
|
-
- Type-safe: TypeScript knows the exact type of each store key
|
|
589
|
-
- Reusable: Same hook works in Settings and Render
|
|
590
|
-
- Automatic cleanup: No manual subscribe/unsubscribe needed
|
|
591
|
-
- Immediate sync: Changes sync to store automatically (no save button needed)
|
|
43
|
+
## Core Pattern
|
|
592
44
|
|
|
593
|
-
### Store
|
|
45
|
+
### Store Hooks (Settings ↔ Render sync)
|
|
594
46
|
|
|
595
|
-
**Recommended: Separate store entry per field**
|
|
596
47
|
```typescript
|
|
597
48
|
// hooks/store.ts
|
|
598
|
-
|
|
599
|
-
export const useLeagueStoreState = createUseStoreState<string>('league', 'nfl')
|
|
600
|
-
export const useShowScoresStoreState = createUseStoreState<boolean>('showScores', true)
|
|
601
|
-
```
|
|
49
|
+
import { createUseInstanceStoreState } from '@telemetryos/sdk/react'
|
|
602
50
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
// hooks/store.ts
|
|
606
|
-
interface SportsSlide {
|
|
607
|
-
team: string
|
|
608
|
-
league: string
|
|
609
|
-
displaySeconds: number
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
export const useSlidesStoreState = createUseStoreState<SportsSlide[]>('slides', [])
|
|
51
|
+
export const useCityState = createUseInstanceStoreState<string>('city', '')
|
|
52
|
+
export const useUnitsState = createUseInstanceStoreState<'imperial' | 'metric'>('units', 'imperial')
|
|
613
53
|
```
|
|
614
54
|
|
|
615
|
-
## Settings Components
|
|
616
|
-
|
|
617
|
-
Import from `@telemetryos/sdk/react`. These components ensure your Settings UI matches the Studio design system.
|
|
618
|
-
|
|
619
|
-
**Important:** Always use these components for Settings. Raw HTML won't look correct in Studio.
|
|
620
|
-
|
|
621
|
-
### Container & Layout
|
|
622
|
-
|
|
623
|
-
#### SettingsContainer
|
|
624
|
-
|
|
625
|
-
Root wrapper for all Settings content. Handles color scheme synchronization with Studio.
|
|
626
|
-
|
|
627
55
|
```typescript
|
|
628
|
-
|
|
56
|
+
// In Settings.tsx or Render.tsx
|
|
57
|
+
import { useCityState } from '../hooks/store'
|
|
629
58
|
|
|
630
|
-
|
|
631
|
-
return (
|
|
632
|
-
<SettingsContainer>
|
|
633
|
-
{/* All settings content goes here */}
|
|
634
|
-
</SettingsContainer>
|
|
635
|
-
)
|
|
636
|
-
}
|
|
59
|
+
const [isLoading, city, setCity] = useCityState()
|
|
637
60
|
```
|
|
638
61
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
Container with border for grouping related settings.
|
|
62
|
+
### Settings Components
|
|
642
63
|
|
|
643
64
|
```typescript
|
|
644
|
-
import { SettingsBox } from '@telemetryos/sdk/react'
|
|
645
|
-
|
|
646
|
-
<SettingsBox>
|
|
647
|
-
{/* Group of related fields */}
|
|
648
|
-
</SettingsBox>
|
|
649
|
-
```
|
|
650
|
-
|
|
651
|
-
#### SettingsDivider
|
|
652
|
-
|
|
653
|
-
Horizontal rule separator between sections.
|
|
654
|
-
|
|
655
|
-
```typescript
|
|
656
|
-
import { SettingsDivider } from '@telemetryos/sdk/react'
|
|
657
|
-
|
|
658
|
-
<SettingsDivider />
|
|
659
|
-
```
|
|
660
|
-
|
|
661
|
-
### Field Structure
|
|
662
|
-
|
|
663
|
-
#### SettingsField, SettingsLabel
|
|
664
|
-
|
|
665
|
-
Wrapper for each form field with its label.
|
|
666
|
-
|
|
667
|
-
```typescript
|
|
668
|
-
import { SettingsField, SettingsLabel } from '@telemetryos/sdk/react'
|
|
669
|
-
|
|
670
|
-
<SettingsField>
|
|
671
|
-
<SettingsLabel>Field Label</SettingsLabel>
|
|
672
|
-
{/* Input component goes here */}
|
|
673
|
-
</SettingsField>
|
|
674
|
-
```
|
|
675
|
-
|
|
676
|
-
### Text Inputs
|
|
677
|
-
|
|
678
|
-
#### SettingsInputFrame (Text Input)
|
|
679
|
-
|
|
680
|
-
```typescript
|
|
681
|
-
import { store } from '@telemetryos/sdk'
|
|
682
65
|
import {
|
|
683
66
|
SettingsContainer,
|
|
684
67
|
SettingsField,
|
|
685
68
|
SettingsLabel,
|
|
686
69
|
SettingsInputFrame,
|
|
687
70
|
} from '@telemetryos/sdk/react'
|
|
688
|
-
import { useTeamStoreState } from '../hooks/store'
|
|
689
|
-
|
|
690
|
-
function Settings() {
|
|
691
|
-
const [isLoading, team, setTeam] = useTeamStoreState(store().instance)
|
|
692
|
-
|
|
693
|
-
return (
|
|
694
|
-
<SettingsContainer>
|
|
695
|
-
<SettingsField>
|
|
696
|
-
<SettingsLabel>Team Name</SettingsLabel>
|
|
697
|
-
<SettingsInputFrame>
|
|
698
|
-
<input
|
|
699
|
-
type="text"
|
|
700
|
-
placeholder="Enter team name..."
|
|
701
|
-
disabled={isLoading}
|
|
702
|
-
value={team}
|
|
703
|
-
onChange={(e) => setTeam(e.target.value)}
|
|
704
|
-
/>
|
|
705
|
-
</SettingsInputFrame>
|
|
706
|
-
</SettingsField>
|
|
707
|
-
</SettingsContainer>
|
|
708
|
-
)
|
|
709
|
-
}
|
|
710
|
-
```
|
|
711
|
-
|
|
712
|
-
#### SettingsTextAreaFrame (Multiline Text)
|
|
713
|
-
|
|
714
|
-
```typescript
|
|
715
|
-
import { SettingsTextAreaFrame } from '@telemetryos/sdk/react'
|
|
716
|
-
import { useDescriptionStoreState } from '../hooks/store'
|
|
717
|
-
|
|
718
|
-
const [isLoading, description, setDescription] = useDescriptionStoreState(store().instance)
|
|
719
|
-
|
|
720
|
-
<SettingsField>
|
|
721
|
-
<SettingsLabel>Description</SettingsLabel>
|
|
722
|
-
<SettingsTextAreaFrame>
|
|
723
|
-
<textarea
|
|
724
|
-
placeholder="Enter description..."
|
|
725
|
-
disabled={isLoading}
|
|
726
|
-
value={description}
|
|
727
|
-
onChange={(e) => setDescription(e.target.value)}
|
|
728
|
-
rows={4}
|
|
729
|
-
/>
|
|
730
|
-
</SettingsTextAreaFrame>
|
|
731
|
-
</SettingsField>
|
|
732
|
-
```
|
|
733
|
-
|
|
734
|
-
### Selection Inputs
|
|
735
|
-
|
|
736
|
-
#### SettingsSelectFrame (Dropdown)
|
|
737
|
-
|
|
738
|
-
```typescript
|
|
739
|
-
import { SettingsSelectFrame } from '@telemetryos/sdk/react'
|
|
740
|
-
import { useLeagueStoreState } from '../hooks/store'
|
|
741
|
-
|
|
742
|
-
const [isLoading, league, setLeague] = useLeagueStoreState(store().instance)
|
|
743
|
-
|
|
744
|
-
<SettingsField>
|
|
745
|
-
<SettingsLabel>League</SettingsLabel>
|
|
746
|
-
<SettingsSelectFrame>
|
|
747
|
-
<select
|
|
748
|
-
disabled={isLoading}
|
|
749
|
-
value={league}
|
|
750
|
-
onChange={(e) => setLeague(e.target.value)}
|
|
751
|
-
>
|
|
752
|
-
<option value="nfl">NFL</option>
|
|
753
|
-
<option value="nba">NBA</option>
|
|
754
|
-
<option value="mlb">MLB</option>
|
|
755
|
-
<option value="nhl">NHL</option>
|
|
756
|
-
</select>
|
|
757
|
-
</SettingsSelectFrame>
|
|
758
|
-
</SettingsField>
|
|
759
|
-
```
|
|
760
|
-
|
|
761
|
-
#### SettingsSliderFrame (Range Slider)
|
|
762
|
-
|
|
763
|
-
```typescript
|
|
764
|
-
import { SettingsSliderFrame } from '@telemetryos/sdk/react'
|
|
765
|
-
import { useVolumeStoreState } from '../hooks/store'
|
|
766
|
-
|
|
767
|
-
const [isLoading, volume, setVolume] = useVolumeStoreState(store().instance)
|
|
768
|
-
|
|
769
|
-
<SettingsField>
|
|
770
|
-
<SettingsLabel>Volume</SettingsLabel>
|
|
771
|
-
<SettingsSliderFrame>
|
|
772
|
-
<input
|
|
773
|
-
type="range"
|
|
774
|
-
min="0"
|
|
775
|
-
max="100"
|
|
776
|
-
disabled={isLoading}
|
|
777
|
-
value={volume}
|
|
778
|
-
onChange={(e) => setVolume(Number(e.target.value))}
|
|
779
|
-
/>
|
|
780
|
-
<span>{volume}%</span> {/* Optional value label */}
|
|
781
|
-
</SettingsSliderFrame>
|
|
782
|
-
</SettingsField>
|
|
783
|
-
```
|
|
784
|
-
|
|
785
|
-
The frame uses flexbox layout, so you can optionally add a `<span>` after the input to display the current value.
|
|
786
|
-
|
|
787
|
-
### Toggle Inputs
|
|
788
|
-
|
|
789
|
-
#### SettingsSwitchFrame, SettingsSwitchLabel (Toggle Switch)
|
|
790
|
-
|
|
791
|
-
```typescript
|
|
792
|
-
import { SettingsSwitchFrame, SettingsSwitchLabel } from '@telemetryos/sdk/react'
|
|
793
|
-
import { useShowScoresStoreState } from '../hooks/store'
|
|
794
|
-
|
|
795
|
-
const [isLoading, showScores, setShowScores] = useShowScoresStoreState(store().instance)
|
|
796
|
-
|
|
797
|
-
<SettingsField>
|
|
798
|
-
<SettingsSwitchFrame>
|
|
799
|
-
<input
|
|
800
|
-
type="checkbox"
|
|
801
|
-
role="switch"
|
|
802
|
-
disabled={isLoading}
|
|
803
|
-
checked={showScores}
|
|
804
|
-
onChange={(e) => setShowScores(e.target.checked)}
|
|
805
|
-
/>
|
|
806
|
-
<SettingsSwitchLabel>Show Live Scores</SettingsSwitchLabel>
|
|
807
|
-
</SettingsSwitchFrame>
|
|
808
|
-
</SettingsField>
|
|
809
71
|
```
|
|
810
72
|
|
|
811
|
-
|
|
73
|
+
### External APIs
|
|
812
74
|
|
|
813
75
|
```typescript
|
|
814
|
-
import {
|
|
815
|
-
import { useAutoRefreshStoreState } from '../hooks/store'
|
|
816
|
-
|
|
817
|
-
const [isLoading, autoRefresh, setAutoRefresh] = useAutoRefreshStoreState(store().instance)
|
|
818
|
-
|
|
819
|
-
<SettingsField>
|
|
820
|
-
<SettingsCheckboxFrame>
|
|
821
|
-
<input
|
|
822
|
-
type="checkbox"
|
|
823
|
-
disabled={isLoading}
|
|
824
|
-
checked={autoRefresh}
|
|
825
|
-
onChange={(e) => setAutoRefresh(e.target.checked)}
|
|
826
|
-
/>
|
|
827
|
-
<SettingsCheckboxLabel>Enable Auto-Refresh</SettingsCheckboxLabel>
|
|
828
|
-
</SettingsCheckboxFrame>
|
|
829
|
-
</SettingsField>
|
|
830
|
-
```
|
|
831
|
-
|
|
832
|
-
#### SettingsRadioFrame, SettingsRadioLabel (Radio Buttons)
|
|
833
|
-
|
|
834
|
-
```typescript
|
|
835
|
-
import { SettingsRadioFrame, SettingsRadioLabel } from '@telemetryos/sdk/react'
|
|
836
|
-
import { useDisplayModeStoreState } from '../hooks/store'
|
|
837
|
-
|
|
838
|
-
const [isLoading, displayMode, setDisplayMode] = useDisplayModeStoreState(store().instance)
|
|
839
|
-
|
|
840
|
-
<SettingsField>
|
|
841
|
-
<SettingsLabel>Display Mode</SettingsLabel>
|
|
842
|
-
<SettingsRadioFrame>
|
|
843
|
-
<input
|
|
844
|
-
type="radio"
|
|
845
|
-
name="displayMode"
|
|
846
|
-
value="compact"
|
|
847
|
-
disabled={isLoading}
|
|
848
|
-
checked={displayMode === 'compact'}
|
|
849
|
-
onChange={(e) => setDisplayMode(e.target.value)}
|
|
850
|
-
/>
|
|
851
|
-
<SettingsRadioLabel>Compact</SettingsRadioLabel>
|
|
852
|
-
</SettingsRadioFrame>
|
|
853
|
-
<SettingsRadioFrame>
|
|
854
|
-
<input
|
|
855
|
-
type="radio"
|
|
856
|
-
name="displayMode"
|
|
857
|
-
value="expanded"
|
|
858
|
-
disabled={isLoading}
|
|
859
|
-
checked={displayMode === 'expanded'}
|
|
860
|
-
onChange={(e) => setDisplayMode(e.target.value)}
|
|
861
|
-
/>
|
|
862
|
-
<SettingsRadioLabel>Expanded</SettingsRadioLabel>
|
|
863
|
-
</SettingsRadioFrame>
|
|
864
|
-
</SettingsField>
|
|
865
|
-
```
|
|
866
|
-
|
|
867
|
-
### Actions
|
|
868
|
-
|
|
869
|
-
#### SettingsButtonFrame
|
|
870
|
-
|
|
871
|
-
```typescript
|
|
872
|
-
import { SettingsButtonFrame } from '@telemetryos/sdk/react'
|
|
76
|
+
import { proxy } from '@telemetryos/sdk'
|
|
873
77
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
<button onClick={handleReset}>Reset to Defaults</button>
|
|
877
|
-
</SettingsButtonFrame>
|
|
878
|
-
</SettingsField>
|
|
78
|
+
// Use proxy for APIs without CORS headers
|
|
79
|
+
const response = await proxy().fetch('https://api.example.com/data')
|
|
879
80
|
```
|
|
880
81
|
|
|
881
82
|
## Hard Constraints
|
|
882
83
|
|
|
883
|
-
**
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
- Settings runs in Studio browser, not on devices
|
|
887
|
-
- `store().device.*` throws Error in Settings
|
|
888
|
-
- Use `store().instance` or `store().application` instead
|
|
889
|
-
|
|
890
|
-
2. **CORS errors on external APIs**
|
|
891
|
-
- Some external APIs don't include CORS headers
|
|
892
|
-
- Use `proxy().fetch()` when you encounter CORS errors
|
|
893
|
-
- Regular `fetch()` is fine when CORS is not an issue (and has advanced caching in the player)
|
|
894
|
-
|
|
895
|
-
3. **Missing configure()**
|
|
896
|
-
- SDK methods throw "SDK not configured" Error
|
|
897
|
-
- Call `configure()` once in main.tsx before React renders
|
|
898
|
-
|
|
899
|
-
4. **Subscription memory leaks**
|
|
900
|
-
- Store a reference to your handler function
|
|
901
|
-
- Must call `unsubscribe(key, handler)` on component unmount
|
|
902
|
-
- Call unsubscribe in useEffect cleanup
|
|
903
|
-
|
|
904
|
-
5. **Timeout errors**
|
|
905
|
-
- All SDK operations timeout after 30 seconds
|
|
906
|
-
- Throws Error with message containing 'timeout'
|
|
907
|
-
- Handle with try/catch
|
|
908
|
-
|
|
909
|
-
## TypeScript Patterns
|
|
910
|
-
|
|
911
|
-
**Define interfaces for all configs and data:**
|
|
912
|
-
```typescript
|
|
913
|
-
interface AppConfig {
|
|
914
|
-
city: string;
|
|
915
|
-
units: 'celsius' | 'fahrenheit';
|
|
916
|
-
refreshInterval: number;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
const config = await store().instance.get<AppConfig>('config');
|
|
920
|
-
if (config) {
|
|
921
|
-
console.log(config.city); // TypeScript knows this exists
|
|
922
|
-
}
|
|
923
|
-
```
|
|
924
|
-
|
|
925
|
-
**Component with proper types:**
|
|
926
|
-
```typescript
|
|
927
|
-
interface Props {
|
|
928
|
-
data: WeatherData;
|
|
929
|
-
onRefresh: () => void;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
export default function WeatherCard({ data, onRefresh }: Props) {
|
|
933
|
-
return <div>{data.temperature}</div>;
|
|
934
|
-
}
|
|
935
|
-
```
|
|
936
|
-
|
|
937
|
-
## React Patterns
|
|
938
|
-
|
|
939
|
-
**Prefer SDK hooks over manual store operations:**
|
|
940
|
-
```typescript
|
|
941
|
-
// RECOMMENDED: Use SDK hooks
|
|
942
|
-
import { useTeamStoreState } from '../hooks/store'
|
|
943
|
-
const [isLoading, team, setTeam] = useTeamStoreState(store().instance)
|
|
944
|
-
|
|
945
|
-
// AVOID: Manual subscription (only for special cases)
|
|
946
|
-
const [team, setTeam] = useState('')
|
|
947
|
-
useEffect(() => {
|
|
948
|
-
const handler = (value) => setTeam(value)
|
|
949
|
-
store().instance.subscribe('team', handler)
|
|
950
|
-
return () => store().instance.unsubscribe('team', handler)
|
|
951
|
-
}, [])
|
|
952
|
-
```
|
|
953
|
-
|
|
954
|
-
**Subscription behavior:**
|
|
955
|
-
When you call `subscribe()`, the handler is immediately called with the current value. No separate `get()` call is needed:
|
|
956
|
-
```typescript
|
|
957
|
-
// WRONG - unnecessary get() call
|
|
958
|
-
useEffect(() => {
|
|
959
|
-
store().instance.get('config').then(setConfig) // Not needed!
|
|
960
|
-
store().instance.subscribe('config', handler)
|
|
961
|
-
// ...
|
|
962
|
-
}, [])
|
|
963
|
-
|
|
964
|
-
// CORRECT - subscribe handles initial value
|
|
965
|
-
useEffect(() => {
|
|
966
|
-
const handler = (value) => setConfig(value)
|
|
967
|
-
store().instance.subscribe('config', handler) // Immediately receives current value
|
|
968
|
-
return () => store().instance.unsubscribe('config', handler)
|
|
969
|
-
}, [])
|
|
970
|
-
```
|
|
971
|
-
|
|
972
|
-
**No manual caching needed:**
|
|
973
|
-
The platform automatically caches SDK API calls, `fetch()`, and `proxy().fetch()` requests. Don't implement your own cache:
|
|
974
|
-
```typescript
|
|
975
|
-
// WRONG - manual caching
|
|
976
|
-
const response = await fetch(url)
|
|
977
|
-
await store().device.set('cached', data) // Don't do this!
|
|
978
|
-
|
|
979
|
-
// CORRECT - just fetch, platform handles caching
|
|
980
|
-
const response = await fetch(url)
|
|
981
|
-
```
|
|
982
|
-
|
|
983
|
-
**Error handling:**
|
|
984
|
-
```typescript
|
|
985
|
-
const [error, setError] = useState<string | null>(null)
|
|
986
|
-
|
|
987
|
-
try {
|
|
988
|
-
await store().instance.set('key', value)
|
|
989
|
-
} catch (err) {
|
|
990
|
-
setError(err instanceof Error ? err.message : 'Unknown error')
|
|
991
|
-
}
|
|
992
|
-
```
|
|
993
|
-
|
|
994
|
-
**Loading states:**
|
|
995
|
-
```typescript
|
|
996
|
-
const [loading, setLoading] = useState(false)
|
|
997
|
-
|
|
998
|
-
const handleAction = async () => {
|
|
999
|
-
setLoading(true)
|
|
1000
|
-
try {
|
|
1001
|
-
await someAsyncOperation()
|
|
1002
|
-
} finally {
|
|
1003
|
-
setLoading(false)
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
```
|
|
1007
|
-
|
|
1008
|
-
## Low-Level Store API (Alternative)
|
|
1009
|
-
|
|
1010
|
-
For special cases where SDK hooks don't fit, you can use the store API directly. This requires manual subscription management.
|
|
1011
|
-
|
|
1012
|
-
**Manual subscription pattern:**
|
|
1013
|
-
```typescript
|
|
1014
|
-
import { useEffect, useState } from 'react'
|
|
1015
|
-
import { store } from '@telemetryos/sdk'
|
|
1016
|
-
import {
|
|
1017
|
-
SettingsContainer,
|
|
1018
|
-
SettingsField,
|
|
1019
|
-
SettingsLabel,
|
|
1020
|
-
SettingsInputFrame,
|
|
1021
|
-
} from '@telemetryos/sdk/react'
|
|
1022
|
-
|
|
1023
|
-
interface Config {
|
|
1024
|
-
team: string
|
|
1025
|
-
league: string
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
export default function Settings() {
|
|
1029
|
-
const [config, setConfig] = useState<Config>({ team: '', league: 'nfl' })
|
|
1030
|
-
const [isLoading, setIsLoading] = useState(true)
|
|
1031
|
-
|
|
1032
|
-
// Subscribe on mount - subscribe() immediately sends current value
|
|
1033
|
-
useEffect(() => {
|
|
1034
|
-
const handler = (value: Config | undefined) => {
|
|
1035
|
-
if (value) setConfig(value)
|
|
1036
|
-
setIsLoading(false)
|
|
1037
|
-
}
|
|
1038
|
-
store().instance.subscribe<Config>('config', handler)
|
|
1039
|
-
|
|
1040
|
-
return () => {
|
|
1041
|
-
store().instance.unsubscribe('config', handler)
|
|
1042
|
-
}
|
|
1043
|
-
}, [])
|
|
1044
|
-
|
|
1045
|
-
// Update store on change
|
|
1046
|
-
const updateConfig = (updates: Partial<Config>) => {
|
|
1047
|
-
const newConfig = { ...config, ...updates }
|
|
1048
|
-
setConfig(newConfig)
|
|
1049
|
-
store().instance.set('config', newConfig)
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
return (
|
|
1053
|
-
<SettingsContainer>
|
|
1054
|
-
<SettingsField>
|
|
1055
|
-
<SettingsLabel>Team Name</SettingsLabel>
|
|
1056
|
-
<SettingsInputFrame>
|
|
1057
|
-
<input
|
|
1058
|
-
type="text"
|
|
1059
|
-
value={config.team}
|
|
1060
|
-
onChange={(e) => updateConfig({ team: e.target.value })}
|
|
1061
|
-
disabled={isLoading}
|
|
1062
|
-
/>
|
|
1063
|
-
</SettingsInputFrame>
|
|
1064
|
-
</SettingsField>
|
|
1065
|
-
</SettingsContainer>
|
|
1066
|
-
)
|
|
1067
|
-
}
|
|
1068
|
-
```
|
|
1069
|
-
|
|
1070
|
-
**Form submission pattern** (for cases requiring validation before save):
|
|
1071
|
-
```typescript
|
|
1072
|
-
import { useState, FormEvent } from 'react'
|
|
1073
|
-
import { store } from '@telemetryos/sdk'
|
|
1074
|
-
import {
|
|
1075
|
-
SettingsContainer,
|
|
1076
|
-
SettingsField,
|
|
1077
|
-
SettingsLabel,
|
|
1078
|
-
SettingsInputFrame,
|
|
1079
|
-
} from '@telemetryos/sdk/react'
|
|
1080
|
-
|
|
1081
|
-
export default function Settings() {
|
|
1082
|
-
const [team, setTeam] = useState('')
|
|
1083
|
-
const [saving, setSaving] = useState(false)
|
|
1084
|
-
const [error, setError] = useState<string | null>(null)
|
|
1085
|
-
|
|
1086
|
-
const handleSubmit = async (e: FormEvent) => {
|
|
1087
|
-
e.preventDefault()
|
|
1088
|
-
setSaving(true)
|
|
1089
|
-
setError(null)
|
|
1090
|
-
|
|
1091
|
-
try {
|
|
1092
|
-
// Validate before saving
|
|
1093
|
-
if (team.length < 2) throw new Error('Team name too short')
|
|
1094
|
-
|
|
1095
|
-
const success = await store().instance.set('team', team)
|
|
1096
|
-
if (!success) throw new Error('Storage operation failed')
|
|
1097
|
-
} catch (err) {
|
|
1098
|
-
setError(err instanceof Error ? err.message : 'Unknown error')
|
|
1099
|
-
} finally {
|
|
1100
|
-
setSaving(false)
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
return (
|
|
1105
|
-
<SettingsContainer>
|
|
1106
|
-
<form onSubmit={handleSubmit}>
|
|
1107
|
-
{error && <div style={{ color: 'red' }}>{error}</div>}
|
|
1108
|
-
<SettingsField>
|
|
1109
|
-
<SettingsLabel>Team Name</SettingsLabel>
|
|
1110
|
-
<SettingsInputFrame>
|
|
1111
|
-
<input
|
|
1112
|
-
type="text"
|
|
1113
|
-
value={team}
|
|
1114
|
-
onChange={(e) => setTeam(e.target.value)}
|
|
1115
|
-
/>
|
|
1116
|
-
</SettingsInputFrame>
|
|
1117
|
-
</SettingsField>
|
|
1118
|
-
<button type="submit" disabled={saving}>
|
|
1119
|
-
{saving ? 'Saving...' : 'Save'}
|
|
1120
|
-
</button>
|
|
1121
|
-
</form>
|
|
1122
|
-
</SettingsContainer>
|
|
1123
|
-
)
|
|
1124
|
-
}
|
|
1125
|
-
```
|
|
1126
|
-
|
|
1127
|
-
**When to use low-level API:**
|
|
1128
|
-
- Complex validation logic before saving
|
|
1129
|
-
- Batching multiple store operations
|
|
1130
|
-
- Custom debouncing behavior
|
|
1131
|
-
- Integration with form libraries
|
|
1132
|
-
|
|
1133
|
-
**When to use SDK hooks (recommended):**
|
|
1134
|
-
- Most Settings components
|
|
1135
|
-
- Simple form fields
|
|
1136
|
-
- Real-time sync between Settings and Render
|
|
1137
|
-
|
|
1138
|
-
## Code Style
|
|
1139
|
-
|
|
1140
|
-
**Naming:**
|
|
1141
|
-
- Components: PascalCase (`WeatherCard.tsx`)
|
|
1142
|
-
- Functions: camelCase (`fetchWeatherData`)
|
|
1143
|
-
- Constants: UPPER_SNAKE_CASE (`API_BASE_URL`)
|
|
1144
|
-
- Interfaces: PascalCase (`WeatherData`, `AppConfig`)
|
|
1145
|
-
|
|
1146
|
-
**Imports order:**
|
|
1147
|
-
```typescript
|
|
1148
|
-
// 1. SDK imports
|
|
1149
|
-
import { configure, store, proxy } from '@telemetryos/sdk'
|
|
1150
|
-
import {
|
|
1151
|
-
SettingsContainer,
|
|
1152
|
-
SettingsField,
|
|
1153
|
-
SettingsLabel,
|
|
1154
|
-
SettingsInputFrame,
|
|
1155
|
-
} from '@telemetryos/sdk/react'
|
|
1156
|
-
|
|
1157
|
-
// 2. React imports
|
|
1158
|
-
import { useEffect, useState } from 'react'
|
|
1159
|
-
|
|
1160
|
-
// 3. Local imports (hooks, components, types)
|
|
1161
|
-
import { useTeamStoreState } from '../hooks/store'
|
|
1162
|
-
import ScoreCard from '@/components/ScoreCard'
|
|
1163
|
-
import type { GameScore } from '@/types'
|
|
1164
|
-
```
|
|
1165
|
-
|
|
1166
|
-
**TypeScript:**
|
|
1167
|
-
- Use strict mode
|
|
1168
|
-
- Define interfaces for all configs and data
|
|
1169
|
-
- Use generics with storage: `get<Type>(key)`
|
|
1170
|
-
- Prefer `interface` over `type` for objects
|
|
1171
|
-
|
|
1172
|
-
**React:**
|
|
1173
|
-
- Functional components only
|
|
1174
|
-
- Use hooks (useState, useEffect, useMemo, useCallback)
|
|
1175
|
-
- Implement loading, error, empty states
|
|
1176
|
-
- Clean up subscriptions in useEffect return
|
|
1177
|
-
|
|
1178
|
-
## Development Commands
|
|
1179
|
-
|
|
1180
|
-
```bash
|
|
1181
|
-
# Install dependencies
|
|
1182
|
-
npm install
|
|
1183
|
-
|
|
1184
|
-
# Start local dev server
|
|
1185
|
-
tos serve
|
|
1186
|
-
# Or: npm run dev
|
|
1187
|
-
|
|
1188
|
-
# Build for production
|
|
1189
|
-
npm run build
|
|
1190
|
-
|
|
1191
|
-
# Type check
|
|
1192
|
-
tsc --noEmit
|
|
1193
|
-
```
|
|
1194
|
-
|
|
1195
|
-
**Local testing:**
|
|
1196
|
-
- Settings: http://localhost:3000/settings
|
|
1197
|
-
- Render: http://localhost:3000/render
|
|
1198
|
-
|
|
1199
|
-
**Deployment:**
|
|
1200
|
-
```bash
|
|
1201
|
-
git add .
|
|
1202
|
-
git commit -m "Description"
|
|
1203
|
-
git push origin main
|
|
1204
|
-
# GitHub integration auto-deploys
|
|
1205
|
-
```
|
|
1206
|
-
|
|
1207
|
-
## Common Errors
|
|
1208
|
-
|
|
1209
|
-
**"SDK not configured"**
|
|
1210
|
-
→ Call `configure('app-name')` in main.tsx before React renders
|
|
1211
|
-
|
|
1212
|
-
**"device storage not available"**
|
|
1213
|
-
→ Using `store().device` in Settings - use `store().instance` instead
|
|
1214
|
-
|
|
1215
|
-
**CORS error**
|
|
1216
|
-
→ External API doesn't include CORS headers - use `proxy().fetch()` for that API
|
|
1217
|
-
|
|
1218
|
-
**"Request timeout"**
|
|
1219
|
-
→ SDK operation exceeded 30 seconds - handle with try/catch
|
|
1220
|
-
|
|
1221
|
-
**Render not updating**
|
|
1222
|
-
→ Missing subscription - use `store().instance.subscribe()` in Render
|
|
1223
|
-
|
|
1224
|
-
**Memory leak**
|
|
1225
|
-
→ Not calling `unsubscribe(key, handler)` in useEffect cleanup
|
|
1226
|
-
|
|
1227
|
-
## Project-Specific Context
|
|
1228
|
-
|
|
1229
|
-
[Add your project details here:]
|
|
1230
|
-
|
|
1231
|
-
**Application Name:** [Your app name]
|
|
1232
|
-
**External APIs:**
|
|
1233
|
-
- [API name]: [endpoint]
|
|
1234
|
-
- Authentication: [method]
|
|
1235
|
-
- Rate limits: [limits]
|
|
1236
|
-
|
|
1237
|
-
**Custom Components:**
|
|
1238
|
-
- [ComponentName]: [purpose]
|
|
1239
|
-
- Location: [path]
|
|
1240
|
-
- Props: [interface]
|
|
1241
|
-
|
|
1242
|
-
**Business Logic:**
|
|
1243
|
-
- [Key algorithms or calculations]
|
|
1244
|
-
- [Data transformation rules]
|
|
84
|
+
1. **No device storage in Settings** - Use instance store hooks instead
|
|
85
|
+
2. **CORS on external APIs** - Use `proxy().fetch()` when needed
|
|
86
|
+
3. **configure() required** - Call in index.tsx before React renders
|
|
1245
87
|
|
|
1246
|
-
##
|
|
88
|
+
## Commands
|
|
1247
89
|
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
-
|
|
1251
|
-
-
|
|
1252
|
-
|
|
90
|
+
| Command | Purpose |
|
|
91
|
+
|---------|---------|
|
|
92
|
+
| `/add-setting` | Add a Settings control |
|
|
93
|
+
| `/add-store-key` | Add a store key with hook |
|
|
94
|
+
| `/add-api-fetch` | Add external API integration |
|
|
95
|
+
| `/build-deploy` | Build and deploy workflow |
|
|
96
|
+
| `/debug-render` | Debug render view issues |
|
|
1253
97
|
|
|
1254
|
-
|
|
1255
|
-
- [SDK Method Reference](https://docs.telemetryos.com/docs/sdk-method-reference) - Complete reference for all SDK methods
|
|
1256
|
-
- [Storage Methods](https://docs.telemetryos.com/docs/storage-methods) - Complete storage scope reference
|
|
1257
|
-
- [Platform Methods](https://docs.telemetryos.com/docs/platform-methods) - Proxy, media, accounts, users, devices
|
|
1258
|
-
- [Media Methods](https://docs.telemetryos.com/docs/media-methods) - Media content queries
|
|
1259
|
-
- [Playlist Methods](https://docs.telemetryos.com/docs/playlist-methods) - Page navigation methods
|
|
1260
|
-
- [Overrides Methods](https://docs.telemetryos.com/docs/overrides-methods) - Dynamic content control
|
|
1261
|
-
- [Proxy Methods](https://docs.telemetryos.com/docs/proxy-methods) - Fetch external content through TelemetryOS proxy
|
|
1262
|
-
- [Weather Methods](https://docs.telemetryos.com/docs/weather-methods) - Access weather data and forecasts
|
|
1263
|
-
- [Client Methods](https://docs.telemetryos.com/docs/client-methods) - Low-level messaging for advanced use cases
|
|
98
|
+
## Skills (REQUIRED)
|
|
1264
99
|
|
|
1265
|
-
**
|
|
1266
|
-
- [Application Components](https://docs.telemetryos.com/docs/application-components) - Modular pieces of a TelemetryOS application
|
|
1267
|
-
- [Mount Points](https://docs.telemetryos.com/docs/mount-points) - /render vs /settings execution contexts
|
|
1268
|
-
- [Rendering](https://docs.telemetryos.com/docs/rendering) - Visual component displayed on playlist pages
|
|
1269
|
-
- [Settings](https://docs.telemetryos.com/docs/settings) - Configuration UI in Studio side panel
|
|
1270
|
-
- [Workers](https://docs.telemetryos.com/docs/workers) - Background JavaScript patterns
|
|
1271
|
-
- [Containers](https://docs.telemetryos.com/docs/containers) - Docker integration patterns
|
|
1272
|
-
- [Configuration](https://docs.telemetryos.com/docs/configuration) - telemetry.config.json schema
|
|
100
|
+
**IMPORTANT:** You MUST invoke the relevant skill BEFORE writing code for these tasks:
|
|
1273
101
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
102
|
+
| Task | Required Skill | Why |
|
|
103
|
+
|------|----------------|-----|
|
|
104
|
+
| Building Render views | `tos-render-design` | Digital signage constraints, UI scaling, no hover/scroll |
|
|
105
|
+
| Adding ANY Settings UI | `tos-settings-ui` | SDK components are required - raw HTML won't work |
|
|
106
|
+
| Calling external APIs | `tos-proxy-fetch` | Proxy patterns prevent CORS errors |
|
|
107
|
+
| Adding store keys | `tos-store-sync` | Hook patterns ensure Settings↔Render sync |
|
|
108
|
+
| Weather integration | `tos-weather-api` | API-specific patterns and credentials |
|
|
109
|
+
| Media library access | `tos-media-api` | SDK media methods and types |
|
|
110
|
+
| Starting new project | `tos-requirements` | Gather requirements before coding |
|
|
111
|
+
| Debugging issues | `tos-debugging` | Common errors and fixes |
|
|
1280
112
|
|
|
1281
|
-
**
|
|
1282
|
-
- [Offline Capabilities](https://docs.telemetryos.com/docs/offline-capabilities) - How apps run locally on devices
|
|
1283
|
-
- [Languages Supported](https://docs.telemetryos.com/docs/languages-supported) - Runtime environment constraints
|
|
1284
|
-
- [Use Cases](https://docs.telemetryos.com/docs/use-cases) - Real-world applications and use cases
|
|
1285
|
-
- [Platform Architecture](https://docs.telemetryos.com/docs/platform-architecture) - Technical deep dive
|
|
113
|
+
**Never write Render layouts, Settings components, or proxy.fetch code without invoking the skill first.**
|
|
1286
114
|
|
|
1287
|
-
|
|
1288
|
-
- [LLMS.txt](https://docs.telemetryos.com/llms.txt) - Complete documentation index for AI agents
|
|
1289
|
-
- [MCP Server](https://docs.telemetryos.com/docs/mcp-server) - Model Context Protocol server integration
|
|
1290
|
-
- [Using AI with TelemetryOS](https://docs.telemetryos.com/docs/using-ai-with-telemetryos) - AI tools overview
|
|
115
|
+
## Documentation
|
|
1291
116
|
|
|
1292
|
-
|
|
1293
|
-
- [
|
|
1294
|
-
- [
|
|
1295
|
-
- [API Tokens](https://docs.telemetryos.com/docs/api-tokens) - Token management for programmatic access
|
|
117
|
+
- [SDK Getting Started](https://docs.telemetryos.com/docs/sdk-getting-started)
|
|
118
|
+
- [SDK Method Reference](https://docs.telemetryos.com/docs/sdk-method-reference)
|
|
119
|
+
- [Building Applications](https://docs.telemetryos.com/docs/applications)
|