@better-state/server 0.1.0 → 0.1.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAO9B,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAYnC,QAAA,MAAM,GAAG,EAAE,UAAU,CAAC,OAAO,OAAO,CAAa,CAAC;AAKlD,QAAA,MAAM,EAAE,+HAEN,CAAC;AAgRH,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAO9B,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAYnC,QAAA,MAAM,GAAG,EAAE,UAAU,CAAC,OAAO,OAAO,CAAa,CAAC;AAKlD,QAAA,MAAM,EAAE,+HAEN,CAAC;AAuVH,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC"}
package/dist/index.js CHANGED
@@ -197,6 +197,14 @@ io.on("connection", (socket) => {
197
197
  });
198
198
  }
199
199
  });
200
+ socket.on("subscribe:studio", () => {
201
+ if (!authedNamespace) {
202
+ socket.emit("auth:error", { message: "Not authenticated" });
203
+ return;
204
+ }
205
+ socket.join(`studio:${authedNamespace}`);
206
+ console.log(`[ws] studio subscribed: socket=${socket.id} namespace=${authedNamespace}`);
207
+ });
200
208
  socket.on("unsubscribe", (payload) => {
201
209
  if (!authedNamespace)
202
210
  return;
@@ -214,10 +222,13 @@ io.on("connection", (socket) => {
214
222
  const { value, version, mutationIds } = processMutations(db, authedNamespace, payload.key, clientId, payload.mutations);
215
223
  socket.emit("mutate:ack", { mutationIds });
216
224
  const room = `${authedNamespace}:${payload.key}`;
217
- io.to(room).emit("state:update", {
218
- key: payload.key,
219
- value,
220
- version,
225
+ const studioRoom = `studio:${authedNamespace}`;
226
+ const updatePayload = { key: payload.key, value, version };
227
+ io.to(room).emit("state:update", updatePayload);
228
+ io.to(studioRoom).emit("studio:mutation", {
229
+ ...updatePayload,
230
+ clientId,
231
+ timestamp: Date.now(),
221
232
  });
222
233
  console.log(`[ws] broadcast: key=${payload.key} version=${version}`);
223
234
  }
@@ -233,6 +244,56 @@ io.on("connection", (socket) => {
233
244
  clientId = null;
234
245
  });
235
246
  });
247
+ // ─── Studio API ──────────────────────────────────────────────────────────────
248
+ app.get("/api/v1/studio/stats", (_req, res) => {
249
+ const namespaces = db.prepare("SELECT COUNT(*) as count FROM namespaces").get();
250
+ const states = db.prepare("SELECT COUNT(*) as count FROM states").get();
251
+ const events = db.prepare("SELECT COUNT(*) as count FROM event_log").get();
252
+ const connectedSockets = io.sockets.sockets.size;
253
+ res.json({
254
+ namespaces: namespaces.count,
255
+ states: states.count,
256
+ events: events.count,
257
+ connections: connectedSockets,
258
+ uptime: process.uptime(),
259
+ });
260
+ });
261
+ app.get("/api/v1/studio/states", (_req, res) => {
262
+ const rows = db.prepare(`
263
+ SELECT s.id, s.namespace, s.key, s.snapshot, s.version, s.created_at, s.updated_at, n.name as namespace_name
264
+ FROM states s
265
+ JOIN namespaces n ON s.namespace = n.id
266
+ ORDER BY s.updated_at DESC
267
+ `).all();
268
+ res.json(rows);
269
+ });
270
+ app.get("/api/v1/studio/events/recent", (req, res) => {
271
+ const limit = parseInt(req.query.limit, 10) || 30;
272
+ const rows = db.prepare(`
273
+ SELECT e.id, e.state_id, e.client_id, e.client_ts, e.server_ts, e.mutation, e.meta, s.key as state_key
274
+ FROM event_log e
275
+ JOIN states s ON e.state_id = s.id
276
+ ORDER BY e.server_ts DESC, e.seq DESC
277
+ LIMIT ?
278
+ `).all(limit);
279
+ res.json(rows.reverse());
280
+ });
281
+ const STUDIO_HTML = path.resolve(__dirname, "../public/studio.html");
282
+ app.get("/studio", (_req, res) => {
283
+ const devKeyPath = process.env.DEV_KEY_PATH || path.resolve(__dirname, "../data/.dev-key");
284
+ let apiKey = "";
285
+ try {
286
+ apiKey = readFileSync(devKeyPath, "utf-8").trim();
287
+ }
288
+ catch { /* no dev key */ }
289
+ if (!existsSync(STUDIO_HTML)) {
290
+ res.status(404).send("Studio not available");
291
+ return;
292
+ }
293
+ const html = readFileSync(STUDIO_HTML, "utf-8").replace("__API_KEY__", apiKey);
294
+ res.setHeader("Content-Type", "text/html");
295
+ res.send(html);
296
+ });
236
297
  // ─── Start ───────────────────────────────────────────────────────────────────
237
298
  httpServer.listen(PORT, () => {
238
299
  console.log(`[better-state] server running on http://localhost:${PORT}`);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-state/server",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Better-State server — shared state primitive for developers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,293 +9,262 @@
9
9
 
10
10
  body {
11
11
  font-family: "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif;
12
- background: #0a0a0f;
12
+ background: #0a0a12;
13
13
  color: #e2e8f0;
14
14
  min-height: 100vh;
15
- padding: 2rem;
16
- }
17
-
18
- .header {
19
15
  display: flex;
20
16
  align-items: center;
21
- gap: 0.75rem;
22
- margin-bottom: 2rem;
23
- }
24
- .header h1 {
25
- font-size: 1.5rem;
26
- font-weight: 700;
27
- letter-spacing: -0.02em;
28
- }
29
- .header h1 span { color: #36adf6; }
30
- .badge {
31
- font-size: 0.7rem;
32
- background: rgba(12, 147, 231, 0.15);
33
- color: #7cc8fb;
34
- padding: 0.15rem 0.5rem;
35
- border-radius: 9999px;
17
+ justify-content: center;
18
+ padding: 1.5rem;
36
19
  }
37
20
 
38
- .status-bar {
39
- display: flex;
40
- gap: 1.5rem;
41
- margin-bottom: 2rem;
42
- font-size: 0.85rem;
43
- color: #94a3b8;
44
- }
45
- .status-dot {
46
- display: inline-block;
47
- width: 8px;
48
- height: 8px;
49
- border-radius: 50%;
50
- margin-right: 0.4rem;
51
- vertical-align: middle;
21
+ .container { width: 100%; max-width: 520px; }
22
+
23
+ .header { margin-bottom: 1.5rem; }
24
+ .header h1 { font-size: 1.5rem; font-weight: 700; letter-spacing: -0.02em; }
25
+ .header h1 span { color: #38bdf8; }
26
+ .subtitle { font-size: 0.8rem; color: #64748b; margin-top: 0.25rem; }
27
+
28
+ .status {
29
+ display: flex; align-items: center; gap: 0.4rem;
30
+ font-size: 0.75rem; color: #94a3b8; margin-bottom: 1.5rem;
52
31
  }
53
- .status-dot.green { background: #34d399; }
54
- .status-dot.red { background: #f87171; }
55
- .status-dot.yellow { background: #fbbf24; }
56
-
57
- .grid {
58
- display: grid;
59
- grid-template-columns: 1fr 1fr;
60
- gap: 1.5rem;
61
- margin-bottom: 2rem;
32
+ .dot {
33
+ width: 7px; height: 7px; border-radius: 50%;
34
+ transition: background 0.3s;
62
35
  }
36
+ .dot.connecting { background: #fbbf24; animation: pulse 1.2s infinite; }
37
+ .dot.connected { background: #34d399; }
38
+ .dot.disconnected { background: #f87171; }
39
+ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
63
40
 
64
41
  .card {
65
- background: rgba(15, 23, 42, 0.6);
42
+ background: rgba(15, 23, 42, 0.7);
66
43
  border: 1px solid #1e293b;
67
- border-radius: 12px;
68
- padding: 1.25rem;
69
- }
70
- .card h2 {
71
- font-size: 0.75rem;
72
- font-weight: 600;
73
- text-transform: uppercase;
74
- letter-spacing: 0.05em;
75
- color: #64748b;
44
+ border-radius: 16px;
45
+ padding: 1.5rem;
76
46
  margin-bottom: 1rem;
77
47
  }
78
48
 
79
- .state-display {
80
- background: #0f172a;
81
- border: 1px solid #1e293b;
82
- border-radius: 8px;
83
- padding: 1rem;
84
- font-family: "JetBrains Mono", "Fira Code", monospace;
85
- font-size: 0.85rem;
86
- min-height: 120px;
87
- white-space: pre-wrap;
88
- word-break: break-all;
89
- color: #a5b4fc;
90
- margin-bottom: 1rem;
49
+ .card-label {
50
+ font-size: 0.65rem; font-weight: 600; text-transform: uppercase;
51
+ letter-spacing: 0.08em; color: #475569; margin-bottom: 1rem;
52
+ }
53
+
54
+ /* Counter */
55
+ .counter-value {
56
+ font-size: 4rem; font-weight: 800; text-align: center;
57
+ color: #38bdf8; padding: 0.5rem 0; transition: transform 0.15s;
58
+ font-variant-numeric: tabular-nums;
91
59
  }
60
+ .counter-value.bump { transform: scale(1.08); }
92
61
 
93
- .controls { display: flex; gap: 0.5rem; flex-wrap: wrap; }
94
-
95
- button {
96
- background: linear-gradient(135deg, #0c93e7, #015da0);
97
- color: white;
98
- border: none;
99
- padding: 0.5rem 1rem;
100
- border-radius: 8px;
101
- font-size: 0.8rem;
102
- font-weight: 500;
103
- cursor: pointer;
104
- transition: opacity 0.15s;
62
+ .btn-row {
63
+ display: flex; gap: 0.5rem; margin-top: 1rem;
105
64
  }
106
- button:hover { opacity: 0.85; }
107
- button.secondary {
108
- background: rgba(30, 41, 59, 0.8);
109
- border: 1px solid #334155;
65
+ .btn {
66
+ flex: 1; padding: 0.6rem; border-radius: 10px;
67
+ font-size: 0.9rem; font-weight: 600; cursor: pointer;
68
+ border: 1px solid #1e293b; background: #1e293b; color: #e2e8f0;
69
+ transition: background 0.15s, border-color 0.15s;
110
70
  }
111
- button.danger {
112
- background: linear-gradient(135deg, #dc2626, #991b1b);
71
+ .btn:hover { background: #334155; border-color: #334155; }
72
+ .btn.primary { background: #2563eb; border-color: #2563eb; }
73
+ .btn.primary:hover { background: #1d4ed8; border-color: #1d4ed8; }
74
+ .btn.danger { color: #f87171; }
75
+ .btn.danger:hover { background: #991b1b22; border-color: #7f1d1d; }
76
+
77
+ /* Todos */
78
+ .todo-input-row { display: flex; gap: 0.5rem; margin-bottom: 0.75rem; }
79
+ .todo-input {
80
+ flex: 1; padding: 0.55rem 0.75rem; border-radius: 10px;
81
+ background: #0f172a; border: 1px solid #334155; color: #e2e8f0;
82
+ font-size: 0.85rem;
113
83
  }
84
+ .todo-input:focus { outline: none; border-color: #38bdf8; }
114
85
 
115
- input[type="text"] {
116
- background: #0f172a;
117
- border: 1px solid #334155;
118
- color: #e2e8f0;
119
- padding: 0.5rem 0.75rem;
120
- border-radius: 8px;
86
+ .todo-list { list-style: none; }
87
+ .todo-item {
88
+ display: flex; align-items: center; gap: 0.6rem;
89
+ padding: 0.5rem 0; border-bottom: 1px solid #1e293b;
121
90
  font-size: 0.85rem;
122
- flex: 1;
123
- min-width: 180px;
124
91
  }
125
- input[type="text"]:focus { outline: none; border-color: #36adf6; }
92
+ .todo-item:last-child { border-bottom: none; }
93
+ .todo-check {
94
+ width: 18px; height: 18px; border-radius: 50%;
95
+ border: 2px solid #475569; cursor: pointer;
96
+ display: flex; align-items: center; justify-content: center;
97
+ flex-shrink: 0; transition: all 0.2s;
98
+ }
99
+ .todo-check.done { background: #34d399; border-color: #34d399; }
100
+ .todo-check.done::after { content: "✓"; color: #0a0a12; font-size: 0.7rem; font-weight: 700; }
101
+ .todo-text { flex: 1; }
102
+ .todo-text.done { text-decoration: line-through; color: #64748b; }
103
+ .todo-delete {
104
+ background: none; border: none; color: #475569; cursor: pointer;
105
+ font-size: 1rem; padding: 0 0.25rem; transition: color 0.15s;
106
+ }
107
+ .todo-delete:hover { color: #f87171; }
108
+ .empty { color: #475569; font-size: 0.8rem; text-align: center; padding: 1rem 0; }
126
109
 
110
+ /* Log */
127
111
  .log {
128
- background: #0f172a;
129
- border: 1px solid #1e293b;
130
- border-radius: 8px;
131
- padding: 1rem;
112
+ background: #0a0a12; border: 1px solid #1e293b; border-radius: 10px;
113
+ padding: 0.75rem; max-height: 160px; overflow-y: auto;
132
114
  font-family: "JetBrains Mono", "Fira Code", monospace;
133
- font-size: 0.75rem;
134
- max-height: 300px;
135
- overflow-y: auto;
136
- line-height: 1.6;
137
- }
138
- .log .entry { margin-bottom: 0.25rem; }
139
- .log .ts { color: #475569; }
140
- .log .event { color: #34d399; }
141
- .log .data { color: #94a3b8; }
142
- .log .error { color: #f87171; }
143
-
144
- .hint {
145
- font-size: 0.8rem;
146
- color: #475569;
147
- margin-top: 0.5rem;
148
- line-height: 1.5;
115
+ font-size: 0.7rem; line-height: 1.7;
149
116
  }
117
+ .log-entry .ts { color: #334155; }
118
+ .log-entry .ev { color: #34d399; }
119
+ .log-entry .dt { color: #64748b; }
150
120
 
151
- @media (max-width: 768px) {
152
- .grid { grid-template-columns: 1fr; }
121
+ .footer {
122
+ text-align: center; font-size: 0.7rem; color: #334155;
123
+ margin-top: 1.5rem; line-height: 1.6;
153
124
  }
125
+ .footer code { color: #475569; }
154
126
  </style>
155
127
  </head>
156
128
  <body>
157
- <div class="header">
158
- <h1><span>Better</span>-State</h1>
159
- <span class="badge">Playground</span>
160
- </div>
129
+ <div class="container">
130
+ <div class="header">
131
+ <h1><span>Better</span>-State</h1>
132
+ <div class="subtitle">Playground — open in multiple tabs to see real-time sync</div>
133
+ </div>
161
134
 
162
- <div class="status-bar">
163
- <div><span id="conn-dot" class="status-dot yellow"></span> <span id="conn-text">Connecting...</span></div>
164
- <div>Client: <code id="client-id">—</code></div>
165
- <div>Version: <code id="version">0</code></div>
166
- </div>
135
+ <div class="status">
136
+ <div id="dot" class="dot connecting"></div>
137
+ <span id="status-text">connecting</span>
138
+ </div>
167
139
 
168
- <div class="grid">
169
- <!-- Counter state -->
140
+ <!-- Counter -->
170
141
  <div class="card">
171
- <h2>State: counter</h2>
172
- <div id="counter-display" class="state-display">0</div>
173
- <div class="controls">
174
- <button onclick="increment()">+ Increment</button>
175
- <button onclick="decrement()" class="secondary">- Decrement</button>
176
- <button onclick="resetCounter()" class="danger">Reset</button>
142
+ <div class="card-label">Synced Counter</div>
143
+ <div id="counter" class="counter-value">0</div>
144
+ <div class="btn-row">
145
+ <button class="btn" onclick="dec()">- 1</button>
146
+ <button class="btn danger" onclick="resetCounter()">Reset</button>
147
+ <button class="btn primary" onclick="inc()">+ 1</button>
177
148
  </div>
178
- <p class="hint">Click buttons in multiple tabs — they all sync in real-time.</p>
179
149
  </div>
180
150
 
181
- <!-- Todos state -->
151
+ <!-- Todos -->
182
152
  <div class="card">
183
- <h2>State: todos</h2>
184
- <div id="todos-display" class="state-display">[]</div>
185
- <div class="controls">
186
- <input id="todo-input" type="text" placeholder="Add a todo..." onkeydown="if(event.key==='Enter') addTodo()" />
187
- <button onclick="addTodo()">Add</button>
188
- <button onclick="clearTodos()" class="danger">Clear</button>
153
+ <div class="card-label">Synced Todo List</div>
154
+ <div class="todo-input-row">
155
+ <input id="input" class="todo-input" placeholder="Add a todo..." onkeydown="if(event.key==='Enter')addTodo()" />
156
+ <button class="btn primary" style="flex:none;padding:.55rem .9rem" onclick="addTodo()">Add</button>
189
157
  </div>
158
+ <ul id="list" class="todo-list">
159
+ <li class="empty">No todos yet</li>
160
+ </ul>
190
161
  </div>
191
- </div>
192
162
 
193
- <!-- Event log -->
194
- <div class="card">
195
- <h2>Event Log</h2>
196
- <div id="log" class="log">
197
- <div class="entry"><span class="ts">[init]</span> <span class="data">Waiting for connection...</span></div>
163
+ <!-- Log -->
164
+ <div class="card">
165
+ <div class="card-label">Event Log</div>
166
+ <div id="log" class="log"></div>
167
+ </div>
168
+
169
+ <div class="footer">
170
+ State powered by <code>@better-state/client</code><br/>
171
+ Changes sync instantly across every connected tab
198
172
  </div>
199
173
  </div>
200
174
 
201
175
  <script type="module">
202
- // --- Config ---
203
- // The API key is injected by the server into the page.
204
176
  const API_KEY = "__API_KEY__";
205
- const SERVER_URL = window.location.origin;
206
-
207
- // --- Load SDK ---
208
177
  const { createClient } = await import("/sdk/browser.mjs");
209
178
 
210
- const log = document.getElementById("log");
211
- const connDot = document.getElementById("conn-dot");
212
- const connText = document.getElementById("conn-text");
213
- const clientIdEl = document.getElementById("client-id");
214
- const versionEl = document.getElementById("version");
215
-
216
- function addLog(event, data, isError = false) {
217
- const ts = new Date().toLocaleTimeString();
218
- const entry = document.createElement("div");
219
- entry.className = "entry";
220
- entry.innerHTML = `<span class="ts">[${ts}]</span> <span class="${isError ? "error" : "event"}">${event}</span> <span class="data">${data}</span>`;
221
- log.appendChild(entry);
222
- log.scrollTop = log.scrollHeight;
179
+ const logEl = document.getElementById("log");
180
+ const counterEl = document.getElementById("counter");
181
+ const listEl = document.getElementById("list");
182
+ const dotEl = document.getElementById("dot");
183
+ const statusEl = document.getElementById("status-text");
184
+ const inputEl = document.getElementById("input");
185
+
186
+ function log(ev, data) {
187
+ const t = new Date().toLocaleTimeString("en", { hour12: false });
188
+ const d = document.createElement("div");
189
+ d.className = "log-entry";
190
+ d.innerHTML = `<span class="ts">${t}</span> <span class="ev">${ev}</span> <span class="dt">${data}</span>`;
191
+ logEl.appendChild(d);
192
+ logEl.scrollTop = logEl.scrollHeight;
223
193
  }
224
194
 
225
- // --- Create client ---
226
- const client = createClient(SERVER_URL, {
227
- apiKey: API_KEY,
228
- debug: true,
229
- });
195
+ const bs = createClient(location.origin, { apiKey: API_KEY, debug: false });
230
196
 
231
- clientIdEl.textContent = "connecting...";
197
+ bs.onStatusChange(s => {
198
+ dotEl.className = "dot " + s;
199
+ statusEl.textContent = s;
200
+ log("status", s);
201
+ });
232
202
 
233
- // --- Counter state ---
234
- const counter = client.state("counter", 0);
203
+ bs.onError(e => log("error", e.message));
235
204
 
236
- counter.subscribe((val) => {
237
- document.getElementById("counter-display").textContent = JSON.stringify(val, null, 2);
238
- versionEl.textContent = counter.getVersion();
239
- addLog("counter:update", `value=${val} version=${counter.getVersion()}`);
205
+ // Counter
206
+ const counter = bs.state("counter", 0);
207
+ counter.subscribe(v => {
208
+ counterEl.textContent = v;
209
+ counterEl.classList.add("bump");
210
+ setTimeout(() => counterEl.classList.remove("bump"), 150);
211
+ log("counter", `value=${v}`);
240
212
  });
241
213
 
242
- // --- Todos state ---
243
- const todos = client.state("todos", []);
214
+ window.inc = () => counter.update(n => n + 1);
215
+ window.dec = () => counter.update(n => n - 1);
216
+ window.resetCounter = () => counter.set(0);
244
217
 
245
- todos.subscribe((val) => {
246
- document.getElementById("todos-display").textContent = JSON.stringify(val, null, 2);
247
- addLog("todos:update", `items=${Array.isArray(val) ? val.length : "?"}`);
248
- });
218
+ // Todos
219
+ const todos = bs.state("todos", []);
249
220
 
250
- // --- Connection status ---
251
- // Poll connection status (simple approach)
252
- setInterval(() => {
253
- const connected = counter.getVersion() >= 0;
254
- if (connText.textContent === "Connecting..." && counter.getVersion() > 0) {
255
- connDot.className = "status-dot green";
256
- connText.textContent = "Connected";
257
- clientIdEl.textContent = client._clientId || "active";
221
+ function renderTodos(items) {
222
+ if (!Array.isArray(items) || items.length === 0) {
223
+ listEl.innerHTML = '<li class="empty">No todos yet</li>';
224
+ return;
258
225
  }
259
- }, 500);
260
-
261
- // Mark connected once we get first state
262
- setTimeout(() => {
263
- connDot.className = "status-dot green";
264
- connText.textContent = "Connected";
265
- }, 2000);
266
-
267
- // --- Actions (exposed globally for onclick handlers) ---
268
- window.increment = () => {
269
- counter.update((n) => n + 1);
270
- addLog("action", "increment");
271
- };
226
+ listEl.innerHTML = items.map(t => `
227
+ <li class="todo-item">
228
+ <div class="todo-check ${t.done ? "done" : ""}" onclick="toggleTodo('${t.id}')"></div>
229
+ <span class="todo-text ${t.done ? "done" : ""}">${esc(t.text)}</span>
230
+ <button class="todo-delete" onclick="deleteTodo('${t.id}')">×</button>
231
+ </li>
232
+ `).join("");
233
+ }
272
234
 
273
- window.decrement = () => {
274
- counter.update((n) => n - 1);
275
- addLog("action", "decrement");
276
- };
235
+ function esc(s) {
236
+ const d = document.createElement("div");
237
+ d.textContent = s;
238
+ return d.innerHTML;
239
+ }
277
240
 
278
- window.resetCounter = () => {
279
- counter.update(() => 0);
280
- addLog("action", "reset counter");
281
- };
241
+ todos.subscribe(v => {
242
+ renderTodos(v);
243
+ log("todos", `count=${Array.isArray(v) ? v.length : 0}`);
244
+ });
282
245
 
283
246
  window.addTodo = () => {
284
- const input = document.getElementById("todo-input");
285
- const text = input.value.trim();
247
+ const text = inputEl.value.trim();
286
248
  if (!text) return;
287
249
  const id = crypto.randomUUID();
288
- todos.update((list) => [...list, { id, text, done: false }]);
289
- addLog("action", `add todo: "${text}"`);
290
- input.value = "";
250
+ const current = todos.get();
251
+ todos.set([...(Array.isArray(current) ? current : []), { id, text, done: false }]);
252
+ inputEl.value = "";
253
+ };
254
+
255
+ window.toggleTodo = (id) => {
256
+ const current = todos.get();
257
+ if (!Array.isArray(current)) return;
258
+ todos.set(current.map(t => t.id === id ? { ...t, done: !t.done } : t));
291
259
  };
292
260
 
293
- window.clearTodos = () => {
294
- todos.update(() => []);
295
- addLog("action", "clear todos");
261
+ window.deleteTodo = (id) => {
262
+ const current = todos.get();
263
+ if (!Array.isArray(current)) return;
264
+ todos.set(current.filter(t => t.id !== id));
296
265
  };
297
266
 
298
- addLog("init", "SDK loaded, connecting to server...");
267
+ log("init", "SDK loaded");
299
268
  </script>
300
269
  </body>
301
270
  </html>
@@ -0,0 +1,653 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Better-State Studio</title>
7
+ <script src="/socket.io/socket.io.js"></script>
8
+ <style>
9
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
10
+
11
+ :root {
12
+ --bg: #06080d;
13
+ --surface: #0d1117;
14
+ --border: #1b2230;
15
+ --border-hl: #2a3548;
16
+ --text: #e2e8f0;
17
+ --text-2: #8b98a9;
18
+ --text-3: #4a5568;
19
+ --accent: #38bdf8;
20
+ --green: #34d399;
21
+ --amber: #fbbf24;
22
+ --red: #f87171;
23
+ --violet: #a78bfa;
24
+ --mono: "JetBrains Mono", "Fira Code", "Cascadia Code", monospace;
25
+ }
26
+
27
+ body {
28
+ font-family: "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif;
29
+ background: var(--bg);
30
+ color: var(--text);
31
+ min-height: 100vh;
32
+ }
33
+
34
+ /* Top bar */
35
+ .topbar {
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: space-between;
39
+ padding: 0.75rem 1.25rem;
40
+ border-bottom: 1px solid var(--border);
41
+ background: var(--surface);
42
+ position: sticky;
43
+ top: 0;
44
+ z-index: 10;
45
+ }
46
+ .topbar-left {
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 0.75rem;
50
+ }
51
+ .logo {
52
+ font-size: 0.95rem;
53
+ font-weight: 700;
54
+ letter-spacing: -0.02em;
55
+ }
56
+ .logo span { color: var(--accent); }
57
+ .badge {
58
+ font-size: 0.6rem;
59
+ font-weight: 600;
60
+ text-transform: uppercase;
61
+ letter-spacing: 0.06em;
62
+ background: rgba(56, 189, 248, 0.1);
63
+ color: var(--accent);
64
+ padding: 0.15rem 0.5rem;
65
+ border-radius: 4px;
66
+ }
67
+ .topbar-right {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 1.25rem;
71
+ }
72
+ .status-pill {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 0.35rem;
76
+ font-size: 0.7rem;
77
+ color: var(--text-2);
78
+ }
79
+ .dot {
80
+ width: 6px;
81
+ height: 6px;
82
+ border-radius: 50%;
83
+ }
84
+ .dot.green { background: var(--green); }
85
+ .dot.amber { background: var(--amber); animation: pulse 1.2s infinite; }
86
+ .dot.red { background: var(--red); }
87
+ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
88
+
89
+ /* Stats bar */
90
+ .stats-bar {
91
+ display: flex;
92
+ gap: 0;
93
+ border-bottom: 1px solid var(--border);
94
+ background: var(--surface);
95
+ }
96
+ .stat {
97
+ flex: 1;
98
+ padding: 0.75rem 1.25rem;
99
+ border-right: 1px solid var(--border);
100
+ }
101
+ .stat:last-child { border-right: none; }
102
+ .stat-label {
103
+ font-size: 0.6rem;
104
+ font-weight: 600;
105
+ text-transform: uppercase;
106
+ letter-spacing: 0.06em;
107
+ color: var(--text-3);
108
+ margin-bottom: 0.2rem;
109
+ }
110
+ .stat-value {
111
+ font-size: 1.25rem;
112
+ font-weight: 700;
113
+ font-variant-numeric: tabular-nums;
114
+ }
115
+
116
+ /* Main layout */
117
+ .main {
118
+ display: grid;
119
+ grid-template-columns: 1fr 1fr;
120
+ height: calc(100vh - 104px);
121
+ }
122
+
123
+ /* Panel */
124
+ .panel {
125
+ border-right: 1px solid var(--border);
126
+ display: flex;
127
+ flex-direction: column;
128
+ overflow: hidden;
129
+ }
130
+ .panel:last-child { border-right: none; }
131
+ .panel-header {
132
+ padding: 0.6rem 1rem;
133
+ border-bottom: 1px solid var(--border);
134
+ font-size: 0.65rem;
135
+ font-weight: 600;
136
+ text-transform: uppercase;
137
+ letter-spacing: 0.06em;
138
+ color: var(--text-3);
139
+ background: var(--surface);
140
+ display: flex;
141
+ align-items: center;
142
+ justify-content: space-between;
143
+ }
144
+ .panel-body {
145
+ flex: 1;
146
+ overflow-y: auto;
147
+ }
148
+
149
+ /* State list */
150
+ .state-item {
151
+ padding: 0.75rem 1rem;
152
+ border-bottom: 1px solid var(--border);
153
+ cursor: pointer;
154
+ transition: background 0.1s;
155
+ }
156
+ .state-item:hover { background: rgba(56, 189, 248, 0.03); }
157
+ .state-item.active { background: rgba(56, 189, 248, 0.06); border-left: 2px solid var(--accent); }
158
+ .state-key {
159
+ font-family: var(--mono);
160
+ font-size: 0.8rem;
161
+ font-weight: 600;
162
+ color: var(--accent);
163
+ margin-bottom: 0.3rem;
164
+ }
165
+ .state-meta {
166
+ display: flex;
167
+ gap: 1rem;
168
+ font-size: 0.65rem;
169
+ color: var(--text-3);
170
+ }
171
+ .state-meta code {
172
+ font-family: var(--mono);
173
+ color: var(--text-2);
174
+ }
175
+ .state-preview {
176
+ margin-top: 0.4rem;
177
+ font-family: var(--mono);
178
+ font-size: 0.7rem;
179
+ color: var(--text-2);
180
+ background: rgba(0,0,0,0.3);
181
+ padding: 0.4rem 0.5rem;
182
+ border-radius: 6px;
183
+ max-height: 80px;
184
+ overflow: hidden;
185
+ white-space: pre-wrap;
186
+ word-break: break-all;
187
+ }
188
+
189
+ /* Detail / right panel */
190
+ .detail-empty {
191
+ display: flex;
192
+ align-items: center;
193
+ justify-content: center;
194
+ height: 100%;
195
+ color: var(--text-3);
196
+ font-size: 0.8rem;
197
+ }
198
+ .detail-header {
199
+ padding: 1rem;
200
+ border-bottom: 1px solid var(--border);
201
+ background: var(--surface);
202
+ }
203
+ .detail-key {
204
+ font-family: var(--mono);
205
+ font-size: 1rem;
206
+ font-weight: 700;
207
+ color: var(--accent);
208
+ margin-bottom: 0.3rem;
209
+ }
210
+ .detail-info {
211
+ display: flex;
212
+ gap: 1.5rem;
213
+ font-size: 0.7rem;
214
+ color: var(--text-3);
215
+ }
216
+ .detail-info strong { color: var(--text-2); }
217
+ .detail-value {
218
+ padding: 1rem;
219
+ border-bottom: 1px solid var(--border);
220
+ }
221
+ .detail-value-label {
222
+ font-size: 0.6rem;
223
+ font-weight: 600;
224
+ text-transform: uppercase;
225
+ letter-spacing: 0.06em;
226
+ color: var(--text-3);
227
+ margin-bottom: 0.5rem;
228
+ }
229
+ .detail-value pre {
230
+ font-family: var(--mono);
231
+ font-size: 0.75rem;
232
+ color: var(--green);
233
+ background: rgba(0,0,0,0.4);
234
+ padding: 0.75rem;
235
+ border-radius: 8px;
236
+ max-height: 240px;
237
+ overflow-y: auto;
238
+ white-space: pre-wrap;
239
+ word-break: break-all;
240
+ }
241
+
242
+ /* Event feed */
243
+ .event-row {
244
+ padding: 0.4rem 1rem;
245
+ border-bottom: 1px solid var(--border);
246
+ font-size: 0.7rem;
247
+ display: flex;
248
+ gap: 0.6rem;
249
+ align-items: baseline;
250
+ transition: background 0.3s;
251
+ }
252
+ .event-row.flash { background: rgba(52, 211, 153, 0.06); }
253
+ .event-ts {
254
+ font-family: var(--mono);
255
+ color: var(--text-3);
256
+ font-size: 0.65rem;
257
+ flex-shrink: 0;
258
+ width: 65px;
259
+ }
260
+ .event-key {
261
+ font-family: var(--mono);
262
+ color: var(--accent);
263
+ font-weight: 600;
264
+ flex-shrink: 0;
265
+ max-width: 120px;
266
+ overflow: hidden;
267
+ text-overflow: ellipsis;
268
+ white-space: nowrap;
269
+ }
270
+ .event-type {
271
+ font-family: var(--mono);
272
+ font-size: 0.6rem;
273
+ padding: 0.1rem 0.35rem;
274
+ border-radius: 3px;
275
+ flex-shrink: 0;
276
+ }
277
+ .event-type.set { background: rgba(167, 139, 250, 0.15); color: var(--violet); }
278
+ .event-type.fn { background: rgba(56, 189, 248, 0.15); color: var(--accent); }
279
+ .event-detail {
280
+ color: var(--text-3);
281
+ font-family: var(--mono);
282
+ overflow: hidden;
283
+ text-overflow: ellipsis;
284
+ white-space: nowrap;
285
+ }
286
+ .event-client {
287
+ color: var(--text-3);
288
+ font-family: var(--mono);
289
+ font-size: 0.6rem;
290
+ margin-left: auto;
291
+ flex-shrink: 0;
292
+ }
293
+
294
+ .empty-msg {
295
+ padding: 2rem;
296
+ text-align: center;
297
+ color: var(--text-3);
298
+ font-size: 0.8rem;
299
+ }
300
+
301
+ .refresh-btn {
302
+ background: none;
303
+ border: 1px solid var(--border);
304
+ color: var(--text-2);
305
+ font-size: 0.6rem;
306
+ padding: 0.2rem 0.5rem;
307
+ border-radius: 4px;
308
+ cursor: pointer;
309
+ font-weight: 600;
310
+ text-transform: uppercase;
311
+ letter-spacing: 0.04em;
312
+ }
313
+ .refresh-btn:hover { border-color: var(--border-hl); color: var(--text); }
314
+
315
+ @media (max-width: 768px) {
316
+ .main { grid-template-columns: 1fr; }
317
+ .stats-bar { flex-wrap: wrap; }
318
+ .stat { min-width: 50%; }
319
+ }
320
+ </style>
321
+ </head>
322
+ <body>
323
+
324
+ <!-- Top bar -->
325
+ <div class="topbar">
326
+ <div class="topbar-left">
327
+ <div class="logo"><span>Better</span>-State</div>
328
+ <span class="badge">Studio</span>
329
+ </div>
330
+ <div class="topbar-right">
331
+ <div class="status-pill">
332
+ <div id="dot" class="dot amber"></div>
333
+ <span id="status">connecting</span>
334
+ </div>
335
+ </div>
336
+ </div>
337
+
338
+ <!-- Stats -->
339
+ <div class="stats-bar">
340
+ <div class="stat">
341
+ <div class="stat-label">State Keys</div>
342
+ <div class="stat-value" id="stat-states">—</div>
343
+ </div>
344
+ <div class="stat">
345
+ <div class="stat-label">Total Events</div>
346
+ <div class="stat-value" id="stat-events">—</div>
347
+ </div>
348
+ <div class="stat">
349
+ <div class="stat-label">Connections</div>
350
+ <div class="stat-value" id="stat-conns">—</div>
351
+ </div>
352
+ <div class="stat">
353
+ <div class="stat-label">Uptime</div>
354
+ <div class="stat-value" id="stat-uptime">—</div>
355
+ </div>
356
+ </div>
357
+
358
+ <!-- Main -->
359
+ <div class="main">
360
+ <!-- Left: state list -->
361
+ <div class="panel">
362
+ <div class="panel-header">
363
+ <span>State Explorer</span>
364
+ <button class="refresh-btn" onclick="loadStates()">Refresh</button>
365
+ </div>
366
+ <div class="panel-body" id="state-list">
367
+ <div class="empty-msg">Loading states...</div>
368
+ </div>
369
+ </div>
370
+
371
+ <!-- Right: detail + live feed -->
372
+ <div class="panel">
373
+ <div class="panel-header">
374
+ <span id="right-title">Live Event Feed</span>
375
+ </div>
376
+ <div class="panel-body" id="right-panel">
377
+ <div id="detail-view" style="display:none"></div>
378
+ <div id="feed"></div>
379
+ </div>
380
+ </div>
381
+ </div>
382
+
383
+ <script type="module">
384
+ const API_KEY = "__API_KEY__";
385
+ const BASE = window.location.origin;
386
+
387
+ const stateListEl = document.getElementById("state-list");
388
+ const rightTitle = document.getElementById("right-title");
389
+ const detailView = document.getElementById("detail-view");
390
+ const feedEl = document.getElementById("feed");
391
+ const dotEl = document.getElementById("dot");
392
+ const statusEl = document.getElementById("status");
393
+
394
+ let allStates = [];
395
+ let selectedKey = null;
396
+
397
+ // --- Stats ---
398
+ async function loadStats() {
399
+ try {
400
+ const res = await fetch(`${BASE}/api/v1/studio/stats`);
401
+ const d = await res.json();
402
+ document.getElementById("stat-states").textContent = d.states;
403
+ document.getElementById("stat-events").textContent = d.events.toLocaleString();
404
+ document.getElementById("stat-conns").textContent = d.connections;
405
+ const m = Math.floor(d.uptime / 60);
406
+ const s = Math.floor(d.uptime % 60);
407
+ document.getElementById("stat-uptime").textContent = m > 0 ? `${m}m ${s}s` : `${s}s`;
408
+ } catch {}
409
+ }
410
+
411
+ // --- States ---
412
+ async function loadStates() {
413
+ try {
414
+ const res = await fetch(`${BASE}/api/v1/studio/states`);
415
+ allStates = await res.json();
416
+ renderStateList();
417
+ } catch {
418
+ stateListEl.innerHTML = '<div class="empty-msg">Failed to load states</div>';
419
+ }
420
+ }
421
+
422
+ function renderStateList() {
423
+ if (allStates.length === 0) {
424
+ stateListEl.innerHTML = '<div class="empty-msg">No state keys yet. Use the SDK to create some.</div>';
425
+ return;
426
+ }
427
+
428
+ stateListEl.innerHTML = allStates.map(s => {
429
+ const val = tryParse(s.snapshot);
430
+ const preview = JSON.stringify(val, null, 2);
431
+ const shortPreview = preview.length > 120 ? preview.slice(0, 120) + "…" : preview;
432
+ const age = timeAgo(s.updated_at);
433
+ const active = s.key === selectedKey ? "active" : "";
434
+
435
+ return `
436
+ <div class="state-item ${active}" onclick="selectState('${s.key}')">
437
+ <div class="state-key">${esc(s.key)}</div>
438
+ <div class="state-meta">
439
+ <span>v<code>${s.version}</code></span>
440
+ <span>updated <code>${age}</code></span>
441
+ <span><code>${s.namespace_name}</code></span>
442
+ </div>
443
+ <div class="state-preview">${esc(shortPreview)}</div>
444
+ </div>
445
+ `;
446
+ }).join("");
447
+ }
448
+
449
+ window.selectState = async function(key) {
450
+ selectedKey = key;
451
+ renderStateList();
452
+
453
+ const s = allStates.find(x => x.key === key);
454
+ if (!s) return;
455
+
456
+ rightTitle.textContent = `State: ${key}`;
457
+ detailView.style.display = "block";
458
+
459
+ const val = tryParse(s.snapshot);
460
+ const pretty = JSON.stringify(val, null, 2);
461
+
462
+ let historyHtml = '<div class="empty-msg">Loading history...</div>';
463
+
464
+ detailView.innerHTML = `
465
+ <div class="detail-header">
466
+ <div class="detail-key">${esc(key)}</div>
467
+ <div class="detail-info">
468
+ <span>Version: <strong>${s.version}</strong></span>
469
+ <span>Namespace: <strong>${esc(s.namespace_name)}</strong></span>
470
+ <span>Updated: <strong>${timeAgo(s.updated_at)}</strong></span>
471
+ </div>
472
+ </div>
473
+ <div class="detail-value">
474
+ <div class="detail-value-label">Current Value</div>
475
+ <pre id="detail-json">${esc(pretty)}</pre>
476
+ </div>
477
+ <div style="padding:0.6rem 1rem;border-bottom:1px solid var(--border)">
478
+ <div class="detail-value-label" style="margin-bottom:0">Recent Mutations</div>
479
+ </div>
480
+ <div id="detail-history">${historyHtml}</div>
481
+ `;
482
+
483
+ try {
484
+ const nsId = s.namespace;
485
+ const res = await fetch(`${BASE}/api/v1/states/${encodeURIComponent(key)}/history?namespace=${nsId}&limit=20`);
486
+ const data = await res.json();
487
+ const histEl = document.getElementById("detail-history");
488
+ if (data.events.length === 0) {
489
+ histEl.innerHTML = '<div class="empty-msg">No mutations yet</div>';
490
+ } else {
491
+ histEl.innerHTML = data.events.map(e => renderEventRow(e, key)).join("");
492
+ }
493
+ } catch {
494
+ document.getElementById("detail-history").innerHTML = '<div class="empty-msg">Failed to load history</div>';
495
+ }
496
+ };
497
+
498
+ // --- Live feed ---
499
+ async function loadRecentEvents() {
500
+ try {
501
+ const res = await fetch(`${BASE}/api/v1/studio/events/recent?limit=50`);
502
+ const events = await res.json();
503
+ if (events.length === 0) {
504
+ feedEl.innerHTML = '<div class="empty-msg">No events yet. Mutations will appear here in real-time.</div>';
505
+ } else {
506
+ feedEl.innerHTML = events.map(e => renderEventRow(e, e.state_key)).join("");
507
+ feedEl.scrollTop = feedEl.scrollHeight;
508
+ }
509
+ } catch {}
510
+ }
511
+
512
+ function renderEventRow(e, key) {
513
+ const ts = new Date(e.server_ts || e.client_ts).toLocaleTimeString("en", { hour12: false });
514
+ const isSet = e.mutation === "__SET__";
515
+ const typeClass = isSet ? "set" : "fn";
516
+ const typeLabel = isSet ? "SET" : "FN";
517
+
518
+ let detail = "";
519
+ if (isSet && e.meta) {
520
+ const meta = tryParse(e.meta);
521
+ if (meta && meta.fallbackValue !== undefined) {
522
+ detail = JSON.stringify(meta.fallbackValue);
523
+ if (detail.length > 60) detail = detail.slice(0, 60) + "…";
524
+ }
525
+ } else if (!isSet) {
526
+ detail = e.mutation;
527
+ if (detail.length > 60) detail = detail.slice(0, 60) + "…";
528
+ }
529
+
530
+ const clientShort = (e.client_id || "").slice(0, 8);
531
+
532
+ return `
533
+ <div class="event-row">
534
+ <span class="event-ts">${ts}</span>
535
+ <span class="event-key">${esc(key)}</span>
536
+ <span class="event-type ${typeClass}">${typeLabel}</span>
537
+ <span class="event-detail">${esc(detail)}</span>
538
+ <span class="event-client">${clientShort}</span>
539
+ </div>
540
+ `;
541
+ }
542
+
543
+ function addLiveEvent(data) {
544
+ if (!feedEl) return;
545
+
546
+ const ts = new Date(data.timestamp).toLocaleTimeString("en", { hour12: false });
547
+ const val = JSON.stringify(data.value);
548
+ const short = val.length > 60 ? val.slice(0, 60) + "…" : val;
549
+ const clientShort = (data.clientId || "").slice(0, 8);
550
+
551
+ const row = document.createElement("div");
552
+ row.className = "event-row flash";
553
+ row.innerHTML = `
554
+ <span class="event-ts">${ts}</span>
555
+ <span class="event-key">${esc(data.key)}</span>
556
+ <span class="event-type set">UPD</span>
557
+ <span class="event-detail">→ ${esc(short)}</span>
558
+ <span class="event-client">${clientShort}</span>
559
+ `;
560
+ feedEl.appendChild(row);
561
+ feedEl.scrollTop = feedEl.scrollHeight;
562
+ setTimeout(() => row.classList.remove("flash"), 1500);
563
+
564
+ // Update stats
565
+ loadStats();
566
+
567
+ // Update the state in our local list
568
+ const existing = allStates.find(s => s.key === data.key);
569
+ if (existing) {
570
+ existing.snapshot = JSON.stringify(data.value);
571
+ existing.version = data.version;
572
+ existing.updated_at = data.timestamp;
573
+ renderStateList();
574
+
575
+ // Update detail view if this key is selected
576
+ if (selectedKey === data.key) {
577
+ const jsonEl = document.getElementById("detail-json");
578
+ if (jsonEl) jsonEl.textContent = JSON.stringify(data.value, null, 2);
579
+ }
580
+ } else {
581
+ // New state key — reload
582
+ loadStates();
583
+ }
584
+ }
585
+
586
+ // --- WebSocket for live updates ---
587
+ function connectWs() {
588
+ if (typeof window.io === "undefined") {
589
+ console.warn("socket.io client not loaded");
590
+ dotEl.className = "dot red";
591
+ statusEl.textContent = "ws unavailable";
592
+ return;
593
+ }
594
+ const socket = window.io(BASE, { transports: ["websocket"] });
595
+
596
+ socket.on("connect", () => {
597
+ dotEl.className = "dot amber";
598
+ statusEl.textContent = "authenticating";
599
+ socket.emit("auth", { apiKey: API_KEY, clientId: "studio-" + Math.random().toString(36).slice(2, 8) }, (res) => {
600
+ if (res && res.ok) {
601
+ dotEl.className = "dot green";
602
+ statusEl.textContent = "connected";
603
+ socket.emit("subscribe:studio");
604
+ } else {
605
+ dotEl.className = "dot red";
606
+ statusEl.textContent = "auth failed";
607
+ }
608
+ });
609
+ });
610
+
611
+ socket.on("auth:ok", () => {
612
+ dotEl.className = "dot green";
613
+ statusEl.textContent = "connected";
614
+ });
615
+
616
+ socket.on("studio:mutation", (data) => {
617
+ addLiveEvent(data);
618
+ });
619
+
620
+ socket.on("disconnect", () => {
621
+ dotEl.className = "dot red";
622
+ statusEl.textContent = "disconnected";
623
+ });
624
+ }
625
+
626
+ // --- Helpers ---
627
+ function tryParse(s) {
628
+ try { return JSON.parse(s); } catch { return s; }
629
+ }
630
+
631
+ function esc(s) {
632
+ const d = document.createElement("div");
633
+ d.textContent = String(s);
634
+ return d.innerHTML;
635
+ }
636
+
637
+ function timeAgo(ts) {
638
+ const diff = Date.now() - ts;
639
+ if (diff < 60000) return "just now";
640
+ if (diff < 3600000) return Math.floor(diff / 60000) + "m ago";
641
+ if (diff < 86400000) return Math.floor(diff / 3600000) + "h ago";
642
+ return Math.floor(diff / 86400000) + "d ago";
643
+ }
644
+
645
+ // --- Init ---
646
+ loadStats();
647
+ loadStates();
648
+ loadRecentEvents();
649
+ setInterval(loadStats, 10000);
650
+ connectWs();
651
+ </script>
652
+ </body>
653
+ </html>