uv-suite 0.24.0 → 0.26.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/package.json +1 -1
- package/watchtower/dashboard.html +357 -88
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uv-suite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Portable framework for AI-assisted software development. 10 agents, 9 skills, 5 hooks, 4 personas. Works with Claude Code, Cursor, and Codex.",
|
|
5
5
|
"author": "Utsav Anand",
|
|
6
6
|
"license": "MIT",
|
|
@@ -5,95 +5,340 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>UV Suite Watchtower</title>
|
|
7
7
|
<style>
|
|
8
|
+
:root {
|
|
9
|
+
color-scheme: dark;
|
|
10
|
+
--bg: #0b0b0d;
|
|
11
|
+
--surface: #16161a;
|
|
12
|
+
--surface-hover: #1d1d22;
|
|
13
|
+
--border: #26262c;
|
|
14
|
+
--border-subtle: #1a1a1e;
|
|
15
|
+
--text: #e9e9ec;
|
|
16
|
+
--text-muted: #9a9aa3;
|
|
17
|
+
--text-dim: #6a6a73;
|
|
18
|
+
--accent: #0a84ff;
|
|
19
|
+
--accent-contrast: #ffffff;
|
|
20
|
+
--success: #30d158;
|
|
21
|
+
--success-soft: rgba(48, 209, 88, 0.18);
|
|
22
|
+
--danger: #ff453a;
|
|
23
|
+
--danger-soft: #ff375f;
|
|
24
|
+
--warning: #ff9f0a;
|
|
25
|
+
--yellow: #ffd60a;
|
|
26
|
+
--info: #64d2ff;
|
|
27
|
+
--purple: #bf5af2;
|
|
28
|
+
--purple-soft: #ac8ee0;
|
|
29
|
+
--peach: #ff6961;
|
|
30
|
+
|
|
31
|
+
--event-latest-bg: #17171c;
|
|
32
|
+
--needs-human-bg: rgba(255, 55, 95, 0.14);
|
|
33
|
+
--failure-bg: rgba(255, 105, 97, 0.12);
|
|
34
|
+
--session-boundary-bg: rgba(48, 209, 88, 0.06);
|
|
35
|
+
--user-prompt-bg: rgba(255, 214, 10, 0.06);
|
|
36
|
+
--user-prompt-text: #ffd60a;
|
|
37
|
+
|
|
38
|
+
--font-sans: -apple-system, BlinkMacSystemFont, 'Inter', 'SF Pro Text', system-ui, sans-serif;
|
|
39
|
+
--font-mono: 'SF Mono', ui-monospace, Menlo, Consolas, monospace;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
[data-theme="light"] {
|
|
43
|
+
color-scheme: light;
|
|
44
|
+
--bg: #fafafa;
|
|
45
|
+
--surface: #ffffff;
|
|
46
|
+
--surface-hover: #f4f4f5;
|
|
47
|
+
--border: #e4e4e7;
|
|
48
|
+
--border-subtle: #ededef;
|
|
49
|
+
--text: #18181b;
|
|
50
|
+
--text-muted: #52525b;
|
|
51
|
+
--text-dim: #8a8a93;
|
|
52
|
+
--accent: #0066cc;
|
|
53
|
+
--accent-contrast: #ffffff;
|
|
54
|
+
--success: #1a9e3e;
|
|
55
|
+
--success-soft: rgba(26, 158, 62, 0.15);
|
|
56
|
+
--danger: #d8302a;
|
|
57
|
+
--danger-soft: #dc184a;
|
|
58
|
+
--warning: #c07300;
|
|
59
|
+
--yellow: #a5720d;
|
|
60
|
+
--info: #0b8aa4;
|
|
61
|
+
--purple: #7c2fbc;
|
|
62
|
+
--purple-soft: #8b3fd4;
|
|
63
|
+
--peach: #c13a35;
|
|
64
|
+
|
|
65
|
+
--event-latest-bg: #f4f4f5;
|
|
66
|
+
--needs-human-bg: rgba(220, 24, 74, 0.08);
|
|
67
|
+
--failure-bg: rgba(216, 48, 42, 0.07);
|
|
68
|
+
--session-boundary-bg: rgba(26, 158, 62, 0.06);
|
|
69
|
+
--user-prompt-bg: rgba(165, 114, 13, 0.06);
|
|
70
|
+
--user-prompt-text: #8a5f0b;
|
|
71
|
+
}
|
|
72
|
+
|
|
8
73
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
.
|
|
34
|
-
|
|
35
|
-
.
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
|
|
39
|
-
.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
.
|
|
67
|
-
.
|
|
68
|
-
|
|
74
|
+
|
|
75
|
+
html, body { height: 100%; }
|
|
76
|
+
body {
|
|
77
|
+
font-family: var(--font-sans);
|
|
78
|
+
background: var(--bg);
|
|
79
|
+
color: var(--text);
|
|
80
|
+
font-size: 14px;
|
|
81
|
+
line-height: 1.5;
|
|
82
|
+
-webkit-font-smoothing: antialiased;
|
|
83
|
+
-moz-osx-font-smoothing: grayscale;
|
|
84
|
+
display: flex;
|
|
85
|
+
flex-direction: column;
|
|
86
|
+
transition: background-color 0.2s ease, color 0.2s ease;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.header {
|
|
90
|
+
padding: 18px 28px;
|
|
91
|
+
border-bottom: 1px solid var(--border);
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: space-between;
|
|
95
|
+
gap: 16px;
|
|
96
|
+
}
|
|
97
|
+
.header h1 { font-size: 18px; font-weight: 600; letter-spacing: -0.02em; }
|
|
98
|
+
.header .meta { display: flex; align-items: center; gap: 14px; }
|
|
99
|
+
.header .status { font-size: 13px; color: var(--text-muted); font-variant-numeric: tabular-nums; }
|
|
100
|
+
.header .status .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 7px; vertical-align: 1px; }
|
|
101
|
+
.header .status .dot.on { background: var(--success); box-shadow: 0 0 0 3px var(--success-soft); }
|
|
102
|
+
.header .status .dot.off { background: var(--danger); }
|
|
103
|
+
|
|
104
|
+
.theme-toggle {
|
|
105
|
+
background: transparent;
|
|
106
|
+
color: var(--text-muted);
|
|
107
|
+
border: 1px solid var(--border);
|
|
108
|
+
border-radius: 7px;
|
|
109
|
+
width: 34px;
|
|
110
|
+
height: 30px;
|
|
111
|
+
display: inline-flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
justify-content: center;
|
|
114
|
+
cursor: pointer;
|
|
115
|
+
padding: 0;
|
|
116
|
+
transition: color 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
|
|
117
|
+
}
|
|
118
|
+
.theme-toggle:hover { color: var(--text); border-color: var(--text-dim); background: var(--surface); }
|
|
119
|
+
.theme-toggle svg { width: 15px; height: 15px; }
|
|
120
|
+
.theme-toggle .icon-sun { display: block; }
|
|
121
|
+
.theme-toggle .icon-moon { display: none; }
|
|
122
|
+
[data-theme="light"] .theme-toggle .icon-sun { display: none; }
|
|
123
|
+
[data-theme="light"] .theme-toggle .icon-moon { display: block; }
|
|
124
|
+
|
|
125
|
+
.stats {
|
|
126
|
+
padding: 16px 28px;
|
|
127
|
+
display: flex;
|
|
128
|
+
gap: 36px;
|
|
129
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
130
|
+
}
|
|
131
|
+
.stat { display: flex; flex-direction: column; }
|
|
132
|
+
.stat .n {
|
|
133
|
+
font-size: 26px;
|
|
134
|
+
font-weight: 600;
|
|
135
|
+
letter-spacing: -0.02em;
|
|
136
|
+
font-variant-numeric: tabular-nums;
|
|
137
|
+
line-height: 1.1;
|
|
138
|
+
}
|
|
139
|
+
.stat .l {
|
|
140
|
+
font-size: 11px;
|
|
141
|
+
color: var(--text-muted);
|
|
142
|
+
text-transform: uppercase;
|
|
143
|
+
letter-spacing: 0.08em;
|
|
144
|
+
font-weight: 500;
|
|
145
|
+
margin-top: 4px;
|
|
146
|
+
}
|
|
147
|
+
.stat .n.alert { color: var(--danger-soft); }
|
|
148
|
+
|
|
149
|
+
.sessions {
|
|
150
|
+
padding: 12px 28px;
|
|
151
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
152
|
+
display: flex;
|
|
153
|
+
gap: 8px;
|
|
154
|
+
flex-wrap: wrap;
|
|
155
|
+
}
|
|
156
|
+
.session-tag {
|
|
157
|
+
padding: 4px 12px;
|
|
158
|
+
border-radius: 14px;
|
|
159
|
+
font-size: 13px;
|
|
160
|
+
font-weight: 500;
|
|
161
|
+
cursor: pointer;
|
|
162
|
+
border: 1px solid transparent;
|
|
163
|
+
transition: border-color 0.15s ease;
|
|
164
|
+
}
|
|
165
|
+
.session-tag:hover { border-color: var(--text-dim); }
|
|
166
|
+
.session-tag.active { border-color: var(--text); }
|
|
167
|
+
|
|
168
|
+
.filters {
|
|
169
|
+
padding: 10px 28px;
|
|
170
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
171
|
+
display: flex;
|
|
172
|
+
gap: 10px;
|
|
173
|
+
flex-wrap: wrap;
|
|
174
|
+
align-items: center;
|
|
175
|
+
}
|
|
176
|
+
.filters select, .filters button {
|
|
177
|
+
background: var(--surface);
|
|
178
|
+
color: var(--text-muted);
|
|
179
|
+
border: 1px solid var(--border);
|
|
180
|
+
border-radius: 6px;
|
|
181
|
+
padding: 0 12px;
|
|
182
|
+
font-size: 13px;
|
|
183
|
+
font-family: inherit;
|
|
184
|
+
cursor: pointer;
|
|
185
|
+
height: 32px;
|
|
186
|
+
transition: color 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
|
|
187
|
+
}
|
|
188
|
+
.filters select:hover, .filters button:hover { border-color: var(--text-dim); color: var(--text); }
|
|
189
|
+
.filters select:focus { outline: none; border-color: var(--accent); }
|
|
190
|
+
.filters button.active { background: var(--accent); color: var(--accent-contrast); border-color: var(--accent); }
|
|
191
|
+
|
|
192
|
+
.timeline { flex: 1; min-height: 0; overflow-y: auto; padding: 4px 0; }
|
|
193
|
+
|
|
194
|
+
.event {
|
|
195
|
+
padding: 14px 28px;
|
|
196
|
+
display: grid;
|
|
197
|
+
grid-template-columns: 82px 150px 170px 90px 1fr;
|
|
198
|
+
gap: 14px;
|
|
199
|
+
align-items: start;
|
|
200
|
+
font-size: 14px;
|
|
201
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
202
|
+
transition: background-color 0.15s ease, opacity 0.15s ease;
|
|
203
|
+
opacity: 0.82;
|
|
204
|
+
}
|
|
205
|
+
.event:hover { background: var(--surface-hover); opacity: 1; }
|
|
206
|
+
.event .time {
|
|
207
|
+
color: var(--text-dim);
|
|
208
|
+
font-variant-numeric: tabular-nums;
|
|
209
|
+
font-family: var(--font-mono);
|
|
210
|
+
font-size: 12.5px;
|
|
211
|
+
padding-top: 2px;
|
|
212
|
+
}
|
|
213
|
+
.event .type {
|
|
214
|
+
font-weight: 600;
|
|
215
|
+
font-size: 14px;
|
|
216
|
+
letter-spacing: -0.01em;
|
|
217
|
+
}
|
|
218
|
+
.event .session {
|
|
219
|
+
font-size: 12.5px;
|
|
220
|
+
font-weight: 500;
|
|
221
|
+
border-radius: 10px;
|
|
222
|
+
padding: 3px 10px;
|
|
223
|
+
display: inline-block;
|
|
224
|
+
max-width: 170px;
|
|
225
|
+
overflow: hidden;
|
|
226
|
+
text-overflow: ellipsis;
|
|
227
|
+
white-space: nowrap;
|
|
228
|
+
}
|
|
229
|
+
.event .tool {
|
|
230
|
+
color: var(--text-muted);
|
|
231
|
+
font-family: var(--font-mono);
|
|
232
|
+
font-size: 12.5px;
|
|
233
|
+
padding-top: 2px;
|
|
234
|
+
}
|
|
235
|
+
.event .detail {
|
|
236
|
+
color: var(--text-muted);
|
|
237
|
+
font-size: 14px;
|
|
238
|
+
line-height: 1.55;
|
|
239
|
+
word-break: break-word;
|
|
240
|
+
}
|
|
241
|
+
.event .detail .filepath { color: var(--info); }
|
|
242
|
+
.event .detail .cmd { color: var(--yellow); font-family: var(--font-mono); }
|
|
243
|
+
|
|
244
|
+
.event.latest {
|
|
245
|
+
opacity: 1;
|
|
246
|
+
background: var(--event-latest-bg);
|
|
247
|
+
border-bottom: 2px solid var(--border);
|
|
248
|
+
}
|
|
249
|
+
.event.latest .type { font-size: 15px; }
|
|
250
|
+
.event.latest .detail { font-size: 15px; color: var(--text); }
|
|
251
|
+
|
|
252
|
+
.event.needs-human {
|
|
253
|
+
background: var(--needs-human-bg);
|
|
254
|
+
border-left: 4px solid var(--danger-soft);
|
|
255
|
+
padding-left: 24px;
|
|
256
|
+
opacity: 1;
|
|
257
|
+
}
|
|
258
|
+
.event.needs-human .type { color: var(--danger-soft); }
|
|
259
|
+
.event.needs-human .human-badge {
|
|
260
|
+
display: inline-block;
|
|
261
|
+
background: var(--danger-soft);
|
|
262
|
+
color: #fff;
|
|
263
|
+
font-size: 10px;
|
|
264
|
+
font-weight: 700;
|
|
265
|
+
padding: 2px 7px;
|
|
266
|
+
border-radius: 4px;
|
|
267
|
+
letter-spacing: 0.06em;
|
|
268
|
+
margin-left: 10px;
|
|
269
|
+
vertical-align: middle;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.event.failure {
|
|
273
|
+
background: var(--failure-bg);
|
|
274
|
+
border-left: 4px solid var(--peach);
|
|
275
|
+
padding-left: 24px;
|
|
276
|
+
opacity: 1;
|
|
277
|
+
}
|
|
278
|
+
.event.session-boundary { background: var(--session-boundary-bg); }
|
|
279
|
+
.event.user-prompt { background: var(--user-prompt-bg); }
|
|
280
|
+
.event.user-prompt .detail { color: var(--user-prompt-text); font-style: italic; }
|
|
281
|
+
|
|
282
|
+
.timeline-end { padding: 20px 24px; text-align: center; border-bottom: 1px solid var(--border-subtle); }
|
|
283
|
+
.loader { display: inline-block; width: 48px; height: 4px; position: relative; }
|
|
284
|
+
.loader::before {
|
|
285
|
+
content: '';
|
|
286
|
+
position: absolute;
|
|
287
|
+
width: 10px;
|
|
288
|
+
height: 4px;
|
|
289
|
+
background: var(--text-dim);
|
|
290
|
+
border-radius: 2px;
|
|
291
|
+
animation: loader 1.4s ease-in-out infinite;
|
|
292
|
+
}
|
|
69
293
|
@keyframes loader {
|
|
70
294
|
0%, 100% { left: 0; }
|
|
71
|
-
50% { left:
|
|
72
|
-
}
|
|
73
|
-
.timeline-end .waiting {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.
|
|
81
|
-
.
|
|
82
|
-
.
|
|
83
|
-
|
|
84
|
-
.
|
|
85
|
-
.
|
|
86
|
-
.
|
|
87
|
-
.
|
|
88
|
-
|
|
89
|
-
.type-
|
|
295
|
+
50% { left: 38px; }
|
|
296
|
+
}
|
|
297
|
+
.timeline-end .waiting {
|
|
298
|
+
font-size: 13px;
|
|
299
|
+
color: var(--text-dim);
|
|
300
|
+
margin-top: 10px;
|
|
301
|
+
font-variant-numeric: tabular-nums;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.empty { padding: 72px 28px; text-align: center; color: var(--text-dim); }
|
|
305
|
+
.empty p { margin-top: 10px; font-size: 14px; }
|
|
306
|
+
.empty p strong { color: var(--text-muted); font-weight: 600; font-size: 15px; }
|
|
307
|
+
|
|
308
|
+
.timeline::-webkit-scrollbar { width: 10px; }
|
|
309
|
+
.timeline::-webkit-scrollbar-track { background: transparent; }
|
|
310
|
+
.timeline::-webkit-scrollbar-thumb { background: var(--border); border-radius: 5px; }
|
|
311
|
+
.timeline::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
|
|
312
|
+
|
|
313
|
+
.type-SessionStart { color: var(--success); }
|
|
314
|
+
.type-SessionEnd, .type-Stop { color: var(--danger); }
|
|
315
|
+
.type-PreToolUse { color: var(--accent); }
|
|
316
|
+
.type-PostToolUse { color: var(--info); }
|
|
317
|
+
.type-PostToolUseFailure { color: var(--peach); }
|
|
318
|
+
.type-UserPromptSubmit { color: var(--yellow); }
|
|
319
|
+
.type-SubagentStart { color: var(--purple); }
|
|
320
|
+
.type-SubagentStop { color: var(--purple-soft); }
|
|
321
|
+
.type-Notification { color: var(--warning); }
|
|
322
|
+
.type-PermissionRequest { color: var(--danger-soft); }
|
|
323
|
+
.type-PreCompact { color: var(--text-muted); }
|
|
90
324
|
</style>
|
|
91
325
|
</head>
|
|
92
326
|
<body>
|
|
93
327
|
|
|
94
328
|
<div class="header">
|
|
95
329
|
<h1>UV Suite Watchtower</h1>
|
|
96
|
-
<div class="
|
|
330
|
+
<div class="meta">
|
|
331
|
+
<div class="status"><span class="dot" id="statusDot"></span><span id="statusText">Connecting...</span></div>
|
|
332
|
+
<button class="theme-toggle" id="themeToggle" type="button" aria-label="Toggle theme" title="Toggle theme">
|
|
333
|
+
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
334
|
+
<circle cx="12" cy="12" r="4"/>
|
|
335
|
+
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
|
336
|
+
</svg>
|
|
337
|
+
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
338
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
|
339
|
+
</svg>
|
|
340
|
+
</button>
|
|
341
|
+
</div>
|
|
97
342
|
</div>
|
|
98
343
|
|
|
99
344
|
<div class="stats">
|
|
@@ -111,7 +356,12 @@
|
|
|
111
356
|
<select id="filterSession"><option value="">All sessions</option></select>
|
|
112
357
|
<button id="btnHumanOnly" onclick="toggleHumanOnly()">Human needed</button>
|
|
113
358
|
<button id="btnClear" onclick="clearEvents()">Clear</button>
|
|
114
|
-
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<!-- Loader at top — latest events appear here -->
|
|
362
|
+
<div class="timeline-end" id="timelineEnd">
|
|
363
|
+
<div class="loader"></div>
|
|
364
|
+
<div class="waiting" id="waitingText">Listening for events...</div>
|
|
115
365
|
</div>
|
|
116
366
|
|
|
117
367
|
<div class="timeline" id="timeline">
|
|
@@ -121,11 +371,6 @@
|
|
|
121
371
|
</div>
|
|
122
372
|
</div>
|
|
123
373
|
|
|
124
|
-
<!-- Bottom loader — always visible, shows system is listening -->
|
|
125
|
-
<div class="timeline-end" id="timelineEnd">
|
|
126
|
-
<div class="loader"></div>
|
|
127
|
-
<div class="waiting" id="waitingText">Listening for events...</div>
|
|
128
|
-
</div>
|
|
129
374
|
|
|
130
375
|
<script>
|
|
131
376
|
const timeline = document.getElementById('timeline');
|
|
@@ -329,14 +574,17 @@ function addEvent(ev) {
|
|
|
329
574
|
const div = renderEvent(ev);
|
|
330
575
|
div.classList.add('latest');
|
|
331
576
|
lastEventDiv = div;
|
|
332
|
-
|
|
577
|
+
|
|
578
|
+
// Prepend — newest at top
|
|
579
|
+
timeline.prepend(div);
|
|
333
580
|
|
|
334
581
|
updateStats();
|
|
335
582
|
updateFilterType(ev);
|
|
336
583
|
updateWaitingText(ev);
|
|
337
584
|
|
|
585
|
+
// Scroll to top to show latest
|
|
338
586
|
if (autoScroll) {
|
|
339
|
-
|
|
587
|
+
timeline.scrollTop = 0;
|
|
340
588
|
}
|
|
341
589
|
}
|
|
342
590
|
|
|
@@ -453,6 +701,27 @@ function connect() {
|
|
|
453
701
|
};
|
|
454
702
|
}
|
|
455
703
|
|
|
704
|
+
// Theme: persist choice in localStorage, fall back to system preference
|
|
705
|
+
const THEME_KEY = 'uv-watchtower-theme';
|
|
706
|
+
const themeToggle = document.getElementById('themeToggle');
|
|
707
|
+
const mql = window.matchMedia('(prefers-color-scheme: light)');
|
|
708
|
+
|
|
709
|
+
function applyTheme(t) { document.documentElement.setAttribute('data-theme', t); }
|
|
710
|
+
function resolvedTheme() {
|
|
711
|
+
return localStorage.getItem(THEME_KEY) || (mql.matches ? 'light' : 'dark');
|
|
712
|
+
}
|
|
713
|
+
applyTheme(resolvedTheme());
|
|
714
|
+
|
|
715
|
+
themeToggle.onclick = () => {
|
|
716
|
+
const next = resolvedTheme() === 'light' ? 'dark' : 'light';
|
|
717
|
+
localStorage.setItem(THEME_KEY, next);
|
|
718
|
+
applyTheme(next);
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
mql.addEventListener('change', () => {
|
|
722
|
+
if (!localStorage.getItem(THEME_KEY)) applyTheme(resolvedTheme());
|
|
723
|
+
});
|
|
724
|
+
|
|
456
725
|
connect();
|
|
457
726
|
</script>
|
|
458
727
|
</body>
|