pawmode 1.5.0 → 1.7.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.
@@ -0,0 +1,1201 @@
1
+ import * as os$1 from "node:os";
2
+ import * as fs$1 from "node:fs";
3
+ import * as path$1 from "node:path";
4
+ import * as http from "node:http";
5
+ import * as crypto from "node:crypto";
6
+
7
+ //#region src/core/dashboard-html.ts
8
+ const THEME_COLORS = {
9
+ paw: {
10
+ bg: "#1a1008",
11
+ surface: "#241a10",
12
+ surfaceHover: "#2e2218",
13
+ border: "#3a2a18",
14
+ text: "#e8d8c8",
15
+ textDim: "#8a7a6a",
16
+ accent: "#b4783c",
17
+ accentDim: "#8a5a2a",
18
+ done: "#6a9a5a",
19
+ high: "#d44",
20
+ low: "#666"
21
+ },
22
+ midnight: {
23
+ bg: "#0a0a0f",
24
+ surface: "#12121a",
25
+ surfaceHover: "#1a1a25",
26
+ border: "#222233",
27
+ text: "#d0d0e0",
28
+ textDim: "#6a6a8a",
29
+ accent: "#6688cc",
30
+ accentDim: "#445588",
31
+ done: "#5a8a5a",
32
+ high: "#cc5555",
33
+ low: "#555566"
34
+ },
35
+ neon: {
36
+ bg: "#050505",
37
+ surface: "#0a0a0a",
38
+ surfaceHover: "#111111",
39
+ border: "#1a1a1a",
40
+ text: "#e0e0e0",
41
+ textDim: "#555",
42
+ accent: "#00ff88",
43
+ accentDim: "#008844",
44
+ done: "#00cc66",
45
+ high: "#ff3355",
46
+ low: "#444"
47
+ },
48
+ rose: {
49
+ bg: "#120a0e",
50
+ surface: "#1a1014",
51
+ surfaceHover: "#24181e",
52
+ border: "#3a2030",
53
+ text: "#e8d0dc",
54
+ textDim: "#8a6a7a",
55
+ accent: "#d4688a",
56
+ accentDim: "#a04868",
57
+ done: "#6a9a7a",
58
+ high: "#e05555",
59
+ low: "#666"
60
+ }
61
+ };
62
+ function generateDashboardHTML(theme, botName) {
63
+ const t = THEME_COLORS[theme];
64
+ const safeBotName = botName.replace(/[&<>"']/g, "");
65
+ return `<!DOCTYPE html>
66
+ <html lang="en">
67
+ <head>
68
+ <meta charset="UTF-8">
69
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
70
+ <title>${safeBotName} — Task Dashboard</title>
71
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🐾</text></svg>">
72
+ <link rel="preconnect" href="https://fonts.googleapis.com">
73
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
74
+ <style>
75
+ *{margin:0;padding:0;box-sizing:border-box}
76
+ :root{
77
+ --bg:${t.bg};--surface:${t.surface};--surface-hover:${t.surfaceHover};
78
+ --border:${t.border};--text:${t.text};--text-dim:${t.textDim};
79
+ --accent:${t.accent};--accent-dim:${t.accentDim};--done:${t.done};
80
+ --high:${t.high};--low:${t.low};
81
+ }
82
+ body{font-family:'JetBrains Mono',monospace;background:var(--bg);color:var(--text);min-height:100vh;font-size:13px;display:flex;flex-direction:column}
83
+
84
+ /* ── Header (glass) ── */
85
+ header{display:flex;align-items:center;justify-content:space-between;padding:14px 24px;border-bottom:1px solid var(--border);background:var(--surface)80;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);position:sticky;top:0;z-index:50}
86
+ .header-left{display:flex;align-items:center;gap:14px}
87
+ .logo{display:flex;align-items:center;gap:10px;font-size:16px;font-weight:700;color:var(--accent)}
88
+ .logo-ver{font-size:9px;color:var(--text-dim);font-weight:400}
89
+ .header-center{display:flex;align-items:center;gap:16px}
90
+ .header-right{display:flex;align-items:center;gap:12px}
91
+ .clock{font-size:12px;color:var(--text-dim);font-variant-numeric:tabular-nums;letter-spacing:1px}
92
+ .search-trigger{display:flex;align-items:center;gap:6px;background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:5px 12px;color:var(--text-dim);cursor:pointer;font-family:inherit;font-size:11px;transition:all .15s;min-width:160px}
93
+ .search-trigger:hover{border-color:var(--accent)}
94
+ .search-trigger kbd{margin-left:auto;border:1px solid var(--border);padding:1px 5px;border-radius:3px;font-size:9px;font-family:inherit}
95
+ .status-dot{display:flex;align-items:center;gap:5px;font-size:10px;color:var(--text-dim)}
96
+ .status-dot::before{content:'';width:6px;height:6px;border-radius:50%;background:var(--done);animation:pulse 2s ease-in-out infinite}
97
+ .settings-btn{background:none;border:1px solid var(--border);border-radius:8px;padding:5px 10px;color:var(--text-dim);cursor:pointer;font-family:inherit;font-size:13px;transition:all .15s}
98
+ .settings-btn:hover{border-color:var(--accent);color:var(--accent)}
99
+ .theme-switcher{display:flex;gap:6px}
100
+ .theme-dot{width:12px;height:12px;border-radius:50%;cursor:pointer;border:2px solid var(--border);transition:all .2s}
101
+ .theme-dot:hover,.theme-dot.active{border-color:var(--text);transform:scale(1.15)}
102
+ .theme-dot[data-theme="paw"]{background:#b4783c}
103
+ .theme-dot[data-theme="midnight"]{background:#6688cc}
104
+ .theme-dot[data-theme="neon"]{background:#00ff88}
105
+ .theme-dot[data-theme="rose"]{background:#d4688a}
106
+
107
+ /* ── Search overlay (Cmd+K) ── */
108
+ .search-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);backdrop-filter:blur(4px);z-index:300;align-items:flex-start;justify-content:center;padding-top:20vh}
109
+ .search-overlay.open{display:flex}
110
+ .search-box{background:var(--surface);border:1px solid var(--border);border-radius:12px;width:480px;max-width:90vw;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.4)}
111
+ .search-box input{width:100%;background:transparent;border:none;padding:16px 20px;color:var(--text);font-family:inherit;font-size:14px;outline:none}
112
+ .search-box input::placeholder{color:var(--text-dim)}
113
+ .search-results{max-height:300px;overflow-y:auto;border-top:1px solid var(--border)}
114
+ .search-result{padding:10px 20px;cursor:pointer;display:flex;align-items:center;gap:10px;transition:background .1s}
115
+ .search-result:hover,.search-result.active{background:var(--surface-hover)}
116
+ .search-result-badge{font-size:9px;padding:2px 6px;border-radius:4px;text-transform:uppercase;letter-spacing:.5px}
117
+ .search-result-title{font-size:12px}
118
+ .search-empty{padding:20px;text-align:center;color:var(--text-dim);font-size:12px}
119
+
120
+ /* ── Metrics row ── */
121
+ .metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;padding:20px 24px 0}
122
+ .metric{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:16px;display:flex;align-items:center;gap:14px;transition:all .2s;cursor:default}
123
+ .metric:hover{border-color:var(--accent);transform:translateY(-1px)}
124
+ .metric-icon{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}
125
+ .metric-icon.todo-icon{background:${t.accent}18}
126
+ .metric-icon.prog-icon{background:${t.text}12}
127
+ .metric-icon.done-icon{background:${t.done}18}
128
+ .metric-icon.high-icon{background:${t.high}18}
129
+ .metric-data{display:flex;flex-direction:column}
130
+ .metric-value{font-size:24px;font-weight:700;font-variant-numeric:tabular-nums;line-height:1}
131
+ .metric-label{font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px;margin-top:2px}
132
+
133
+ /* ── Main layout ── */
134
+ .main{display:flex;flex:1;overflow:hidden}
135
+ .board-wrap{flex:1;overflow-y:auto;padding:20px 24px}
136
+ .board{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;min-height:calc(100vh - 230px)}
137
+
138
+ /* ── Activity feed (right panel) ── */
139
+ .feed{width:260px;border-left:1px solid var(--border);background:var(--surface);display:flex;flex-direction:column;transition:width .2s;overflow:hidden;flex-shrink:0}
140
+ .feed.collapsed{width:40px;cursor:pointer}
141
+ .feed-header{display:flex;align-items:center;justify-content:space-between;padding:14px;border-bottom:1px solid var(--border);flex-shrink:0}
142
+ .feed-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:1px;color:var(--accent);white-space:nowrap}
143
+ .feed-toggle{background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:14px;font-family:inherit;padding:2px 4px}
144
+ .feed-toggle:hover{color:var(--text)}
145
+ .feed.collapsed .feed-title,.feed.collapsed .feed-list{display:none}
146
+ .feed.collapsed .feed-toggle{transform:rotate(180deg)}
147
+ .feed-list{flex:1;overflow-y:auto;padding:8px}
148
+ .feed-item{padding:8px 10px;border-radius:6px;margin-bottom:4px;font-size:10px;color:var(--text-dim);display:flex;gap:8px;align-items:flex-start;transition:background .1s}
149
+ .feed-item:hover{background:var(--bg)}
150
+ .feed-dot{width:6px;height:6px;border-radius:50%;margin-top:3px;flex-shrink:0}
151
+ .feed-dot.add{background:var(--accent)}
152
+ .feed-dot.move{background:${t.text}80}
153
+ .feed-dot.del{background:var(--high)}
154
+ .feed-dot.edit{background:var(--done)}
155
+ .feed-time{color:var(--text-dim);opacity:.6;font-size:9px;margin-top:2px}
156
+ .feed-empty{padding:24px;text-align:center;color:var(--text-dim);font-size:11px}
157
+
158
+ /* ── Columns ── */
159
+ .column{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:14px;display:flex;flex-direction:column;min-height:200px}
160
+ .column.drag-over{border-color:var(--accent);background:var(--surface-hover)}
161
+ .col-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;padding-bottom:10px;border-bottom:1px solid var(--border)}
162
+ .col-title{font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:1px}
163
+ .col-count{font-size:10px;color:var(--text-dim);background:var(--bg);padding:2px 8px;border-radius:10px}
164
+ .col-todo .col-title{color:var(--accent)}
165
+ .col-progress .col-title{color:var(--text)}
166
+ .col-done .col-title{color:var(--done)}
167
+ .cards{flex:1;display:flex;flex-direction:column;gap:6px;min-height:40px}
168
+
169
+ /* ── Cards ── */
170
+ .card{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:10px 12px;cursor:grab;transition:all .15s;position:relative;border-left:3px solid var(--border);animation:cardIn .2s ease-out}
171
+ .card:hover{border-color:var(--accent);border-left-color:inherit;transform:translateY(-1px)}
172
+ .card.dragging{opacity:.3;transform:scale(.96)}
173
+ .card.p-high{border-left-color:var(--high)}
174
+ .card.p-normal{border-left-color:var(--accent)}
175
+ .card.p-low{border-left-color:var(--low)}
176
+ .card-title{font-size:12px;font-weight:500;margin-bottom:3px;outline:none;line-height:1.4}
177
+ .card-title:focus{border-bottom:1px solid var(--accent)}
178
+ .card-desc{font-size:10px;color:var(--text-dim);margin-bottom:5px;outline:none;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
179
+ .card-desc:focus{-webkit-line-clamp:unset;border-bottom:1px solid var(--accent)}
180
+ .card-tags{display:flex;flex-wrap:wrap;gap:3px;margin-bottom:5px}
181
+ .tag{font-size:8px;padding:1px 6px;border-radius:10px;letter-spacing:.3px;white-space:nowrap}
182
+ .card-footer{display:flex;align-items:center;justify-content:space-between}
183
+ .card-meta{display:flex;align-items:center;gap:6px}
184
+ .priority{display:flex;gap:3px}
185
+ .priority-dot{width:7px;height:7px;border-radius:50%;cursor:pointer;transition:transform .1s}
186
+ .priority-dot:hover{transform:scale(1.4)}
187
+ .priority-dot.high{background:var(--high)}
188
+ .priority-dot.normal{background:var(--accent)}
189
+ .priority-dot.low{background:var(--low)}
190
+ .priority-dot.active{box-shadow:0 0 0 2px var(--bg),0 0 0 3px currentColor}
191
+ .card-time{font-size:8px;color:var(--text-dim);opacity:.5}
192
+ .card-delete{font-size:10px;color:var(--text-dim);cursor:pointer;opacity:0;transition:opacity .15s}
193
+ .card:hover .card-delete{opacity:1}
194
+ .card-delete:hover{color:var(--high)}
195
+
196
+ /* ── Add form ── */
197
+ .add-btn{display:flex;align-items:center;justify-content:center;gap:6px;padding:8px;margin-top:6px;border:1px dashed var(--border);border-radius:8px;color:var(--text-dim);cursor:pointer;font-family:inherit;font-size:11px;background:none;transition:all .15s;width:100%}
198
+ .add-btn:hover{border-color:var(--accent);color:var(--accent)}
199
+ .empty{text-align:center;padding:30px 12px;color:var(--text-dim);font-size:11px;line-height:1.8}
200
+ .add-form{display:none;flex-direction:column;gap:6px;margin-top:6px}
201
+ .add-form.show{display:flex}
202
+ .add-form input,.add-form textarea{background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:7px 10px;color:var(--text);font-family:inherit;font-size:11px;outline:none;resize:none}
203
+ .add-form input:focus,.add-form textarea:focus{border-color:var(--accent)}
204
+ .form-actions{display:flex;gap:6px}
205
+ .form-actions button{flex:1;padding:5px;border:1px solid var(--border);border-radius:6px;font-family:inherit;font-size:10px;cursor:pointer;background:var(--surface);color:var(--text);transition:all .15s}
206
+ .form-actions .save{background:var(--accent);color:var(--bg);border-color:var(--accent);font-weight:600}
207
+ .form-actions .save:hover{opacity:.9}
208
+ .form-actions .cancel:hover{border-color:var(--text-dim)}
209
+
210
+ /* ── Settings panel ── */
211
+ .settings-panel{display:none;position:fixed;top:0;right:0;bottom:0;width:280px;background:var(--surface);border-left:1px solid var(--border);padding:24px;z-index:200;flex-direction:column;gap:20px}
212
+ .settings-panel.open{display:flex}
213
+ .settings-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:199}
214
+ .settings-overlay.open{display:block}
215
+ .settings-title{font-size:14px;font-weight:600;color:var(--accent);display:flex;justify-content:space-between;align-items:center}
216
+ .settings-close{background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:16px;font-family:inherit}
217
+ .settings-close:hover{color:var(--text)}
218
+ .settings-label{font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px}
219
+ .settings-input{background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:8px 10px;color:var(--text);font-family:inherit;font-size:12px;outline:none;width:100%}
220
+ .settings-input:focus{border-color:var(--accent)}
221
+ .settings-themes{display:flex;gap:8px;flex-wrap:wrap}
222
+ .settings-theme{flex:1;min-width:50px;padding:8px;border:2px solid var(--border);border-radius:8px;cursor:pointer;text-align:center;font-size:10px;color:var(--text-dim);transition:all .15s}
223
+ .settings-theme:hover{border-color:var(--text-dim)}
224
+ .settings-theme.active{border-color:var(--accent);color:var(--accent)}
225
+
226
+ /* ── Toast + Loading ── */
227
+ .toast{position:fixed;bottom:20px;right:20px;padding:10px 16px;border-radius:8px;font-size:12px;opacity:0;transition:opacity .3s;pointer-events:none;z-index:100}
228
+ .toast.show{opacity:1}
229
+ .toast.error{background:var(--high);color:#fff}
230
+ .toast.success{background:var(--done);color:#fff}
231
+ .loading{display:none;padding:20px 24px}
232
+ .loading.show{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
233
+ .shimmer{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:14px;min-height:200px}
234
+ .shimmer-line{height:10px;background:linear-gradient(90deg,var(--border) 25%,var(--surface-hover) 50%,var(--border) 75%);background-size:200% 100%;border-radius:4px;margin-bottom:10px;animation:shimmer 1.5s infinite}
235
+ .shimmer-line.short{width:60%}
236
+ @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
237
+
238
+ /* ── Quick actions bar ── */
239
+ .quick-actions{display:flex;gap:8px;padding:0 24px 16px;flex-wrap:wrap}
240
+ .quick-action{display:flex;align-items:center;gap:6px;padding:7px 14px;background:var(--surface);border:1px solid var(--border);border-radius:8px;color:var(--text-dim);cursor:pointer;font-family:inherit;font-size:10px;transition:all .15s;text-decoration:none}
241
+ .quick-action:hover{border-color:var(--accent);color:var(--accent);transform:translateY(-1px)}
242
+ .quick-action .qa-icon{font-size:13px}
243
+ .quick-action.danger:hover{border-color:var(--high);color:var(--high)}
244
+
245
+ /* ── Confirm dialog ── */
246
+ .confirm-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);backdrop-filter:blur(4px);z-index:400;align-items:center;justify-content:center}
247
+ .confirm-overlay.open{display:flex}
248
+ .confirm-box{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:24px;width:340px;max-width:90vw;text-align:center;box-shadow:0 20px 60px rgba(0,0,0,.4)}
249
+ .confirm-msg{font-size:13px;margin-bottom:16px;line-height:1.5}
250
+ .confirm-actions{display:flex;gap:8px;justify-content:center}
251
+ .confirm-actions button{padding:8px 20px;border:1px solid var(--border);border-radius:8px;font-family:inherit;font-size:11px;cursor:pointer;transition:all .15s}
252
+ .confirm-cancel{background:var(--surface);color:var(--text)}
253
+ .confirm-cancel:hover{border-color:var(--text-dim)}
254
+ .confirm-ok{background:var(--high);color:#fff;border-color:var(--high);font-weight:600}
255
+ .confirm-ok:hover{opacity:.9}
256
+
257
+ /* ── Keyboard hints ── */
258
+ .kbd-hint{position:fixed;bottom:12px;left:20px;font-size:9px;color:var(--text-dim);opacity:.4;display:flex;gap:12px}
259
+ .kbd-hint span{display:flex;align-items:center;gap:3px}
260
+ .kbd-hint kbd{border:1px solid var(--border);padding:0px 4px;border-radius:3px;font-family:inherit;font-size:8px}
261
+
262
+ @keyframes cardIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
263
+ @keyframes pulse{0%,100%{opacity:.4}50%{opacity:1}}
264
+ @media(max-width:900px){.metrics{grid-template-columns:repeat(2,1fr)}.board{grid-template-columns:1fr}.feed{display:none}.kbd-hint{display:none}.quick-actions{display:none}}
265
+ @media(max-width:600px){.metrics{grid-template-columns:1fr}.header-center{display:none}}
266
+
267
+ /* Custom scrollbar */
268
+ ::-webkit-scrollbar{width:5px}
269
+ ::-webkit-scrollbar-track{background:transparent}
270
+ ::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
271
+ ::-webkit-scrollbar-thumb:hover{background:var(--text-dim)}
272
+ </style>
273
+ </head>
274
+ <body>
275
+
276
+ <header>
277
+ <div class="header-left">
278
+ <div class="logo"><span>&#x1F43E;</span> ${safeBotName} <span class="logo-ver">v1.5</span></div>
279
+ </div>
280
+ <div class="header-center">
281
+ <button class="search-trigger" id="searchTrigger">&#x1F50D; Search... <kbd>&#x2318;K</kbd></button>
282
+ <div class="status-dot">Running</div>
283
+ </div>
284
+ <div class="header-right">
285
+ <span class="clock" id="clock"></span>
286
+ <div class="theme-switcher">
287
+ <div class="theme-dot${theme === "paw" ? " active" : ""}" data-theme="paw" title="Paw"></div>
288
+ <div class="theme-dot${theme === "midnight" ? " active" : ""}" data-theme="midnight" title="Midnight"></div>
289
+ <div class="theme-dot${theme === "neon" ? " active" : ""}" data-theme="neon" title="Neon"></div>
290
+ <div class="theme-dot${theme === "rose" ? " active" : ""}" data-theme="rose" title="Rose"></div>
291
+ </div>
292
+ <button class="settings-btn" id="settingsBtn">&#x2699;</button>
293
+ </div>
294
+ </header>
295
+
296
+ <div class="metrics" id="metrics">
297
+ <div class="metric"><div class="metric-icon todo-icon">&#x1F4CB;</div><div class="metric-data"><span class="metric-value" id="mTodo">-</span><span class="metric-label">Todo</span></div></div>
298
+ <div class="metric"><div class="metric-icon prog-icon">&#x26A1;</div><div class="metric-data"><span class="metric-value" id="mProg">-</span><span class="metric-label">In Progress</span></div></div>
299
+ <div class="metric"><div class="metric-icon done-icon">&#x2705;</div><div class="metric-data"><span class="metric-value" id="mDone">-</span><span class="metric-label">Completed</span></div></div>
300
+ <div class="metric"><div class="metric-icon high-icon">&#x1F525;</div><div class="metric-data"><span class="metric-value" id="mHigh">-</span><span class="metric-label">High Priority</span></div></div>
301
+ </div>
302
+
303
+ <div class="loading show" id="loading">
304
+ <div class="shimmer"><div class="shimmer-line"></div><div class="shimmer-line short"></div><div class="shimmer-line"></div></div>
305
+ <div class="shimmer"><div class="shimmer-line short"></div><div class="shimmer-line"></div></div>
306
+ <div class="shimmer"><div class="shimmer-line"></div><div class="shimmer-line short"></div></div>
307
+ </div>
308
+
309
+ <div class="quick-actions" id="quickActions" style="display:none">
310
+ <button class="quick-action" id="qaStandup"><span class="qa-icon">&#x1F4DD;</span> Export Standup</button>
311
+ <button class="quick-action danger" id="qaClearDone"><span class="qa-icon">&#x1F9F9;</span> Clear Done</button>
312
+ <button class="quick-action" id="qaSortPriority"><span class="qa-icon">&#x2B06;</span> Sort by Priority</button>
313
+ </div>
314
+
315
+ <div class="main" id="mainWrap" style="display:none">
316
+ <div class="board-wrap"><div class="board" id="board"></div></div>
317
+ <div class="feed" id="feed">
318
+ <div class="feed-header"><span class="feed-title">&#x1F43E; Activity</span><button class="feed-toggle" id="feedToggle">&#x25B6;</button></div>
319
+ <div class="feed-list" id="feedList"><div class="feed-empty">No activity yet.<br>Start adding tasks!</div></div>
320
+ </div>
321
+ </div>
322
+
323
+ <div class="search-overlay" id="searchOverlay">
324
+ <div class="search-box">
325
+ <input type="text" id="searchInput" placeholder="Search tasks..." autofocus>
326
+ <div class="search-results" id="searchResults"></div>
327
+ </div>
328
+ </div>
329
+
330
+ <div class="toast" id="toast"></div>
331
+ <div class="kbd-hint"><span><kbd>N</kbd> New</span><span><kbd>&#x2318;K</kbd> Search</span><span><kbd>]</kbd> Feed</span><span><kbd>Esc</kbd> Close</span></div>
332
+
333
+ <div class="settings-overlay" id="settingsOverlay"></div>
334
+ <div class="settings-panel" id="settingsPanel">
335
+ <div class="settings-title">Settings <button class="settings-close" id="settingsClose">&#x2715;</button></div>
336
+ <div>
337
+ <div class="settings-label">Bot Name</div>
338
+ <input class="settings-input" id="botNameInput" type="text" value="${safeBotName}" maxlength="20">
339
+ </div>
340
+ <div>
341
+ <div class="settings-label">Theme</div>
342
+ <div class="settings-themes">
343
+ <div class="settings-theme${theme === "paw" ? " active" : ""}" data-theme="paw" style="color:#b4783c">Paw</div>
344
+ <div class="settings-theme${theme === "midnight" ? " active" : ""}" data-theme="midnight" style="color:#6688cc">Midnight</div>
345
+ <div class="settings-theme${theme === "neon" ? " active" : ""}" data-theme="neon" style="color:#00ff88">Neon</div>
346
+ <div class="settings-theme${theme === "rose" ? " active" : ""}" data-theme="rose" style="color:#d4688a">Rose</div>
347
+ </div>
348
+ </div>
349
+ </div>
350
+
351
+ <div class="confirm-overlay" id="confirmOverlay">
352
+ <div class="confirm-box">
353
+ <div class="confirm-msg" id="confirmMsg"></div>
354
+ <div class="confirm-actions">
355
+ <button class="confirm-cancel" id="confirmCancel">Cancel</button>
356
+ <button class="confirm-ok" id="confirmOk">Confirm</button>
357
+ </div>
358
+ </div>
359
+ </div>
360
+
361
+ <script>
362
+ var BOTNAME = "${safeBotName}";
363
+ var tasks = [];
364
+ var dragId = null;
365
+ var searchQuery = "";
366
+ var activityLog = [];
367
+ var feedCollapsed = false;
368
+
369
+ // ── Tag colors (hash-based) ──
370
+ var tagColors = ["#b4783c","#6688cc","#00cc88","#d4688a","#cc9944","#7788bb","#44aa88","#aa5588","#bb7744","#5599aa"];
371
+ function tagColor(t) { var h=0;for(var i=0;i<t.length;i++)h=t.charCodeAt(i)+((h<<5)-h);return tagColors[Math.abs(h)%tagColors.length]; }
372
+
373
+ function toast(msg, type) {
374
+ var el = document.getElementById("toast");
375
+ el.textContent = msg;
376
+ el.className = "toast show " + (type || "success");
377
+ setTimeout(function() { el.className = "toast"; }, 2000);
378
+ }
379
+
380
+ function api(path, opts) {
381
+ return fetch("/api/" + path, Object.assign({ headers: {"Content-Type": "application/json"} }, opts || {}))
382
+ .then(function(r) { if (!r.ok) throw new Error("Failed: " + r.status); return r.json(); })
383
+ .catch(function(err) { toast(err.message, "error"); throw err; });
384
+ }
385
+
386
+ function esc(s) { var d = document.createElement("div"); d.textContent = s; return d.innerHTML; }
387
+
388
+ function timeAgo(iso) {
389
+ var diff = Date.now() - new Date(iso).getTime();
390
+ var m = Math.floor(diff / 60000);
391
+ if (m < 1) return "now";
392
+ if (m < 60) return m + "m";
393
+ var h = Math.floor(m / 60);
394
+ if (h < 24) return h + "h";
395
+ var d = Math.floor(h / 24);
396
+ return d + "d";
397
+ }
398
+
399
+ function logActivity(type, msg) {
400
+ activityLog.unshift({ type: type, msg: msg, time: new Date().toISOString() });
401
+ if (activityLog.length > 50) activityLog.pop();
402
+ renderFeed();
403
+ }
404
+
405
+ function renderFeed() {
406
+ var list = document.getElementById("feedList");
407
+ if (activityLog.length === 0) { list.innerHTML = '<div class="feed-empty">No activity yet.<br>Start adding tasks!</div>'; return; }
408
+ list.innerHTML = activityLog.slice(0, 30).map(function(a) {
409
+ return '<div class="feed-item"><span class="feed-dot ' + a.type + '"></span><div><div>' + esc(a.msg) + '</div><div class="feed-time">' + timeAgo(a.time) + '</div></div></div>';
410
+ }).join("");
411
+ }
412
+
413
+ function updateMetrics() {
414
+ document.getElementById("mTodo").textContent = tasks.filter(function(t){return t.status==="todo"}).length;
415
+ document.getElementById("mProg").textContent = tasks.filter(function(t){return t.status==="in-progress"}).length;
416
+ document.getElementById("mDone").textContent = tasks.filter(function(t){return t.status==="done"}).length;
417
+ document.getElementById("mHigh").textContent = tasks.filter(function(t){return t.priority==="high"}).length;
418
+ }
419
+
420
+ var emptyMsgs = {
421
+ "todo": "Nothing planned yet.<br>Press <kbd>N</kbd> to add a task! &#x1F43E;",
422
+ "in-progress": "Nothing in progress.<br>Drag a task here to start.",
423
+ "done": "No completed tasks yet.<br>Ship something! &#x1F680;"
424
+ };
425
+
426
+ function load() {
427
+ api("tasks").then(function(data) {
428
+ tasks = data;
429
+ document.getElementById("loading").classList.remove("show");
430
+ document.getElementById("mainWrap").style.display = "flex";
431
+ document.getElementById("quickActions").style.display = "flex";
432
+ render();
433
+ checkFocusSession();
434
+ });
435
+ }
436
+
437
+ // ── Focus session indicator ──
438
+ function checkFocusSession() {
439
+ fetch("/api/focus").then(function(r){return r.json()}).then(function(data) {
440
+ if (data && data.endsAt) {
441
+ var remaining = new Date(data.endsAt).getTime() - Date.now();
442
+ if (remaining > 0) {
443
+ var mins = Math.ceil(remaining / 60000);
444
+ var el = document.getElementById("mHigh");
445
+ var label = el.parentElement.querySelector(".metric-label");
446
+ el.textContent = mins + "m";
447
+ label.textContent = "Focus Active";
448
+ el.parentElement.parentElement.querySelector(".metric-icon").innerHTML = "&#x1F512;";
449
+ el.parentElement.parentElement.style.borderColor = "var(--accent)";
450
+ }
451
+ }
452
+ }).catch(function(){});
453
+ }
454
+
455
+ // ── Confirm dialog ──
456
+ var confirmCb = null;
457
+ function showConfirm(msg, cb) {
458
+ document.getElementById("confirmMsg").textContent = msg;
459
+ document.getElementById("confirmOverlay").classList.add("open");
460
+ confirmCb = cb;
461
+ }
462
+ document.getElementById("confirmCancel").addEventListener("click", function() {
463
+ document.getElementById("confirmOverlay").classList.remove("open");
464
+ confirmCb = null;
465
+ });
466
+ document.getElementById("confirmOk").addEventListener("click", function() {
467
+ document.getElementById("confirmOverlay").classList.remove("open");
468
+ if (confirmCb) confirmCb();
469
+ confirmCb = null;
470
+ });
471
+
472
+ // ── Quick actions ──
473
+ document.getElementById("qaClearDone").addEventListener("click", function() {
474
+ var doneCount = tasks.filter(function(t){return t.status==="done"}).length;
475
+ if (doneCount === 0) { toast("No completed tasks to clear"); return; }
476
+ showConfirm("Clear " + doneCount + " completed task" + (doneCount > 1 ? "s" : "") + "?", function() {
477
+ fetch("/api/tasks/done", { method: "DELETE" }).then(function(r){return r.json()}).then(function() {
478
+ tasks = tasks.filter(function(t){return t.status!=="done"});
479
+ logActivity("del", "Cleared " + doneCount + " completed tasks");
480
+ render();
481
+ toast("Cleared " + doneCount + " tasks");
482
+ });
483
+ });
484
+ });
485
+
486
+ document.getElementById("qaStandup").addEventListener("click", function() {
487
+ window.open("/api/standup", "_blank");
488
+ toast("Standup exported!");
489
+ });
490
+
491
+ document.getElementById("qaSortPriority").addEventListener("click", function() {
492
+ var order = { high: 0, normal: 1, low: 2 };
493
+ tasks.sort(function(a, b) { return (order[a.priority] || 1) - (order[b.priority] || 1); });
494
+ tasks.forEach(function(t, i) { t.order = i; });
495
+ logActivity("edit", "Sorted by priority");
496
+ render();
497
+ toast("Sorted by priority");
498
+ });
499
+
500
+ function render() {
501
+ var statuses = ["todo", "in-progress", "done"];
502
+ var labels = { "todo": "Todo", "in-progress": "In Progress", "done": "Done" };
503
+ var colClass = { "todo": "col-todo", "in-progress": "col-progress", "done": "col-done" };
504
+ var board = document.getElementById("board");
505
+ board.innerHTML = "";
506
+ updateMetrics();
507
+
508
+ statuses.forEach(function(status) {
509
+ var filtered = tasks.filter(function(t) {
510
+ if (t.status !== status) return false;
511
+ if (searchQuery) {
512
+ var q = searchQuery.toLowerCase();
513
+ var match = t.title.toLowerCase().indexOf(q) !== -1;
514
+ if (!match && t.description) match = t.description.toLowerCase().indexOf(q) !== -1;
515
+ if (!match && t.tags) match = t.tags.some(function(tag){return tag.toLowerCase().indexOf(q)!==-1});
516
+ return match;
517
+ }
518
+ return true;
519
+ }).sort(function(a, b) { return a.order - b.order; });
520
+
521
+ var col = document.createElement("div");
522
+ col.className = "column " + colClass[status];
523
+ col.setAttribute("data-status", status);
524
+
525
+ var header = document.createElement("div");
526
+ header.className = "col-header";
527
+ header.innerHTML = '<span class="col-title">' + labels[status] + '</span><span class="col-count">' + filtered.length + '</span>';
528
+ col.appendChild(header);
529
+
530
+ var cardsDiv = document.createElement("div");
531
+ cardsDiv.className = "cards";
532
+
533
+ if (filtered.length === 0) {
534
+ cardsDiv.innerHTML = '<div class="empty">' + emptyMsgs[status] + '</div>';
535
+ } else {
536
+ filtered.forEach(function(task) {
537
+ var card = document.createElement("div");
538
+ card.className = "card p-" + task.priority;
539
+ card.draggable = true;
540
+ card.setAttribute("data-id", task.id);
541
+
542
+ var titleDiv = document.createElement("div");
543
+ titleDiv.className = "card-title";
544
+ titleDiv.contentEditable = "true";
545
+ titleDiv.textContent = task.title;
546
+ titleDiv.addEventListener("blur", function() {
547
+ var v = this.textContent.trim();
548
+ if (v && v !== task.title) {
549
+ api("tasks/" + task.id, { method: "PUT", body: JSON.stringify({ title: v }) });
550
+ logActivity("edit", "Renamed: " + v);
551
+ task.title = v;
552
+ }
553
+ });
554
+ titleDiv.addEventListener("keydown", function(e) { if(e.key==="Enter"){e.preventDefault();this.blur()} });
555
+ card.appendChild(titleDiv);
556
+
557
+ if (task.description) {
558
+ var descDiv = document.createElement("div");
559
+ descDiv.className = "card-desc";
560
+ descDiv.contentEditable = "true";
561
+ descDiv.textContent = task.description;
562
+ descDiv.addEventListener("blur", function() {
563
+ var v = this.textContent.trim();
564
+ api("tasks/" + task.id, { method: "PUT", body: JSON.stringify({ description: v || undefined }) });
565
+ task.description = v || undefined;
566
+ });
567
+ card.appendChild(descDiv);
568
+ }
569
+
570
+ if (task.tags && task.tags.length > 0) {
571
+ var tagsDiv = document.createElement("div");
572
+ tagsDiv.className = "card-tags";
573
+ task.tags.forEach(function(tag) {
574
+ var c = tagColor(tag);
575
+ var el = document.createElement("span");
576
+ el.className = "tag";
577
+ el.style.background = c + "18";
578
+ el.style.color = c;
579
+ el.style.border = "1px solid " + c + "30";
580
+ el.textContent = tag;
581
+ tagsDiv.appendChild(el);
582
+ });
583
+ card.appendChild(tagsDiv);
584
+ }
585
+
586
+ var footer = document.createElement("div");
587
+ footer.className = "card-footer";
588
+
589
+ var meta = document.createElement("div");
590
+ meta.className = "card-meta";
591
+
592
+ var priorityDiv = document.createElement("div");
593
+ priorityDiv.className = "priority";
594
+ ["high", "normal", "low"].forEach(function(p) {
595
+ var dot = document.createElement("div");
596
+ dot.className = "priority-dot " + p + (task.priority === p ? " active" : "");
597
+ dot.title = p.charAt(0).toUpperCase() + p.slice(1);
598
+ dot.addEventListener("click", function(e) {
599
+ e.stopPropagation();
600
+ api("tasks/" + task.id, { method: "PUT", body: JSON.stringify({ priority: p }) }).then(function() {
601
+ task.priority = p;
602
+ logActivity("edit", task.title + " -> " + p + " priority");
603
+ render();
604
+ });
605
+ });
606
+ priorityDiv.appendChild(dot);
607
+ });
608
+ meta.appendChild(priorityDiv);
609
+
610
+ var timeSpan = document.createElement("span");
611
+ timeSpan.className = "card-time";
612
+ timeSpan.textContent = timeAgo(task.createdAt);
613
+ meta.appendChild(timeSpan);
614
+
615
+ footer.appendChild(meta);
616
+
617
+ var delBtn = document.createElement("span");
618
+ delBtn.className = "card-delete";
619
+ delBtn.innerHTML = "&#x2715;";
620
+ delBtn.addEventListener("click", function(e) {
621
+ e.stopPropagation();
622
+ showConfirm("Delete \\\"" + task.title + "\\\"?", function() {
623
+ api("tasks/" + task.id, { method: "DELETE" }).then(function() {
624
+ logActivity("del", "Deleted: " + task.title);
625
+ tasks = tasks.filter(function(t) { return t.id !== task.id; });
626
+ render();
627
+ });
628
+ });
629
+ });
630
+ footer.appendChild(delBtn);
631
+ card.appendChild(footer);
632
+
633
+ card.addEventListener("dragstart", function(e) { dragId = task.id; this.classList.add("dragging"); e.dataTransfer.effectAllowed = "move"; });
634
+ card.addEventListener("dragend", function() { this.classList.remove("dragging"); dragId = null; document.querySelectorAll(".column").forEach(function(c) { c.classList.remove("drag-over"); }); });
635
+
636
+ cardsDiv.appendChild(card);
637
+ });
638
+ }
639
+ col.appendChild(cardsDiv);
640
+
641
+ // Add button + form
642
+ var addBtn = document.createElement("button");
643
+ addBtn.type = "button";
644
+ addBtn.className = "add-btn";
645
+ addBtn.textContent = "+ Add task";
646
+ addBtn.addEventListener("click", function() { formDiv.classList.add("show"); titleInput.focus(); });
647
+ col.appendChild(addBtn);
648
+
649
+ var formDiv = document.createElement("div");
650
+ formDiv.className = "add-form";
651
+
652
+ var titleInput = document.createElement("input");
653
+ titleInput.type = "text";
654
+ titleInput.placeholder = "Task title...";
655
+ titleInput.addEventListener("keydown", function(e) { if (e.key === "Enter") doSave(); if (e.key === "Escape") closeForm(); });
656
+ formDiv.appendChild(titleInput);
657
+
658
+ var descInput = document.createElement("textarea");
659
+ descInput.placeholder = "Description (optional)";
660
+ descInput.rows = 2;
661
+ descInput.addEventListener("keydown", function(e) { if (e.key === "Escape") closeForm(); });
662
+ formDiv.appendChild(descInput);
663
+
664
+ var tagsInput = document.createElement("input");
665
+ tagsInput.type = "text";
666
+ tagsInput.placeholder = "Tags (comma-separated)";
667
+ tagsInput.addEventListener("keydown", function(e) { if (e.key === "Enter") doSave(); if (e.key === "Escape") closeForm(); });
668
+ formDiv.appendChild(tagsInput);
669
+
670
+ var actions = document.createElement("div");
671
+ actions.className = "form-actions";
672
+
673
+ var cancelBtn = document.createElement("button");
674
+ cancelBtn.type = "button";
675
+ cancelBtn.className = "cancel";
676
+ cancelBtn.textContent = "Cancel";
677
+
678
+ function closeForm() { formDiv.classList.remove("show"); titleInput.value = ""; descInput.value = ""; tagsInput.value = ""; }
679
+ cancelBtn.addEventListener("click", closeForm);
680
+ actions.appendChild(cancelBtn);
681
+
682
+ var saveBtn = document.createElement("button");
683
+ saveBtn.type = "button";
684
+ saveBtn.className = "save";
685
+ saveBtn.textContent = "Add";
686
+
687
+ function doSave() {
688
+ var title = titleInput.value.trim();
689
+ if (!title) return;
690
+ var desc = descInput.value.trim();
691
+ var rawTags = tagsInput.value.trim();
692
+ var tags = rawTags ? rawTags.split(",").map(function(t){return t.trim()}).filter(Boolean) : undefined;
693
+ saveBtn.disabled = true;
694
+ saveBtn.textContent = "...";
695
+ var body = { title: title, status: status, priority: "normal" };
696
+ if (desc) body.description = desc;
697
+ if (tags) body.tags = tags;
698
+ api("tasks", { method: "POST", body: JSON.stringify(body) })
699
+ .then(function(task) {
700
+ tasks.push(task);
701
+ logActivity("add", "Added: " + title);
702
+ render();
703
+ toast("Task added!");
704
+ })
705
+ .catch(function() { saveBtn.disabled = false; saveBtn.textContent = "Add"; });
706
+ }
707
+
708
+ saveBtn.addEventListener("click", doSave);
709
+ actions.appendChild(saveBtn);
710
+ formDiv.appendChild(actions);
711
+ col.appendChild(formDiv);
712
+
713
+ // Drop events
714
+ col.addEventListener("dragover", function(e) { e.preventDefault(); e.dataTransfer.dropEffect = "move"; this.classList.add("drag-over"); });
715
+ col.addEventListener("dragleave", function() { this.classList.remove("drag-over"); });
716
+ col.addEventListener("drop", function(e) {
717
+ e.preventDefault();
718
+ this.classList.remove("drag-over");
719
+ if (!dragId) return;
720
+ var newStatus = this.getAttribute("data-status");
721
+ var order = tasks.filter(function(t) { return t.status === newStatus; }).length;
722
+ api("tasks/" + dragId, { method: "PUT", body: JSON.stringify({ status: newStatus, order: order }) })
723
+ .then(function() {
724
+ var task = tasks.find(function(t) { return t.id === dragId; });
725
+ if (task) {
726
+ logActivity("move", task.title + " -> " + labels[newStatus]);
727
+ task.status = newStatus;
728
+ task.order = order;
729
+ }
730
+ render();
731
+ });
732
+ });
733
+
734
+ board.appendChild(col);
735
+ });
736
+ }
737
+
738
+ // ── Clock ──
739
+ function updateClock() {
740
+ var now = new Date();
741
+ var h = now.getHours(); var m = now.getMinutes();
742
+ document.getElementById("clock").textContent = (h < 10 ? "0" : "") + h + ":" + (m < 10 ? "0" : "") + m;
743
+ }
744
+ updateClock(); setInterval(updateClock, 10000);
745
+
746
+ // ── Theme switcher ──
747
+ document.querySelectorAll(".theme-dot").forEach(function(dot) {
748
+ dot.addEventListener("click", function() {
749
+ var t = this.getAttribute("data-theme");
750
+ api("config", { method: "PUT", body: JSON.stringify({ theme: t }) }).then(function() { window.location.href = "/?theme=" + t; });
751
+ });
752
+ });
753
+
754
+ // ── Settings panel ──
755
+ var settingsPanel = document.getElementById("settingsPanel");
756
+ var settingsOverlay = document.getElementById("settingsOverlay");
757
+ function openSettings() { settingsPanel.classList.add("open"); settingsOverlay.classList.add("open"); }
758
+ function closeSettings() { settingsPanel.classList.remove("open"); settingsOverlay.classList.remove("open"); }
759
+ document.getElementById("settingsBtn").addEventListener("click", openSettings);
760
+ document.getElementById("settingsClose").addEventListener("click", closeSettings);
761
+ settingsOverlay.addEventListener("click", closeSettings);
762
+ document.querySelectorAll(".settings-theme").forEach(function(el) {
763
+ el.addEventListener("click", function() {
764
+ var t = this.getAttribute("data-theme");
765
+ api("config", { method: "PUT", body: JSON.stringify({ theme: t }) }).then(function() { window.location.href = "/?theme=" + t; });
766
+ });
767
+ });
768
+ var botNameInput = document.getElementById("botNameInput");
769
+ var bnTimer = null;
770
+ botNameInput.addEventListener("input", function() {
771
+ clearTimeout(bnTimer);
772
+ var val = this.value.trim();
773
+ bnTimer = setTimeout(function() {
774
+ if (val) api("config", { method: "PUT", body: JSON.stringify({ botName: val }) }).then(function() {
775
+ document.querySelector(".logo").innerHTML = '<span>&#x1F43E;</span> ' + esc(val) + ' <span class="logo-ver">v1.5</span>';
776
+ BOTNAME = val; document.title = val + " \\u2014 Task Dashboard";
777
+ toast("Name updated!");
778
+ });
779
+ }, 600);
780
+ });
781
+
782
+ // ── Cmd+K Search overlay ──
783
+ var searchOverlay = document.getElementById("searchOverlay");
784
+ var searchInput = document.getElementById("searchInput");
785
+ var searchResults = document.getElementById("searchResults");
786
+ var searchIdx = -1;
787
+
788
+ function openSearch() { searchOverlay.classList.add("open"); searchInput.value = ""; searchInput.focus(); searchResults.innerHTML = ""; searchIdx = -1; }
789
+ function closeSearch() { searchOverlay.classList.remove("open"); searchQuery = ""; }
790
+
791
+ document.getElementById("searchTrigger").addEventListener("click", openSearch);
792
+ searchOverlay.addEventListener("click", function(e) { if (e.target === searchOverlay) closeSearch(); });
793
+
794
+ searchInput.addEventListener("input", function() {
795
+ var q = this.value.toLowerCase();
796
+ searchQuery = q;
797
+ searchIdx = -1;
798
+ if (!q) { searchResults.innerHTML = ""; render(); return; }
799
+ var matches = tasks.filter(function(t) {
800
+ return t.title.toLowerCase().indexOf(q) !== -1 || (t.description && t.description.toLowerCase().indexOf(q) !== -1) || (t.tags && t.tags.some(function(tag){return tag.toLowerCase().indexOf(q)!==-1}));
801
+ }).slice(0, 8);
802
+ if (matches.length === 0) {
803
+ searchResults.innerHTML = '<div class="search-empty">No tasks found</div>';
804
+ } else {
805
+ searchResults.innerHTML = matches.map(function(t, i) {
806
+ var badgeColor = t.status === "done" ? "var(--done)" : t.status === "in-progress" ? "var(--text)" : "var(--accent)";
807
+ return '<div class="search-result" data-id="' + t.id + '"><span class="search-result-badge" style="background:' + badgeColor + '20;color:' + badgeColor + '">' + t.status + '</span><span class="search-result-title">' + esc(t.title) + '</span></div>';
808
+ }).join("");
809
+ }
810
+ render();
811
+ });
812
+
813
+ searchInput.addEventListener("keydown", function(e) {
814
+ var items = searchResults.querySelectorAll(".search-result");
815
+ if (e.key === "ArrowDown") { e.preventDefault(); searchIdx = Math.min(searchIdx + 1, items.length - 1); items.forEach(function(el,i){el.classList.toggle("active",i===searchIdx)}); }
816
+ if (e.key === "ArrowUp") { e.preventDefault(); searchIdx = Math.max(searchIdx - 1, 0); items.forEach(function(el,i){el.classList.toggle("active",i===searchIdx)}); }
817
+ if (e.key === "Enter" && searchIdx >= 0 && items[searchIdx]) { closeSearch(); }
818
+ if (e.key === "Escape") closeSearch();
819
+ });
820
+
821
+ // ── Activity feed toggle ──
822
+ document.getElementById("feedToggle").addEventListener("click", function() {
823
+ var feed = document.getElementById("feed");
824
+ feedCollapsed = !feedCollapsed;
825
+ feed.classList.toggle("collapsed", feedCollapsed);
826
+ });
827
+
828
+ // ── Global keyboard shortcuts ──
829
+ document.addEventListener("keydown", function(e) {
830
+ if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA" || e.target.contentEditable === "true") {
831
+ if (e.key === "Escape") e.target.blur();
832
+ return;
833
+ }
834
+ if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault(); openSearch(); return; }
835
+ if (e.key === "n" || e.key === "N") { e.preventDefault(); var btn = document.querySelector(".col-todo .add-btn"); if(btn) btn.click(); }
836
+ if (e.key === "]") { e.preventDefault(); document.getElementById("feedToggle").click(); }
837
+ if (e.key === "Escape") { closeSearch(); closeSettings(); document.querySelectorAll(".add-form.show").forEach(function(f){f.classList.remove("show")}); }
838
+ });
839
+
840
+ load();
841
+ </script>
842
+ </body>
843
+ </html>`;
844
+ }
845
+ function generateFocusTimerHTML(theme, botName, endsAt, duration) {
846
+ const t = THEME_COLORS[theme];
847
+ const safeBotName = botName.replace(/[&<>"']/g, "");
848
+ const safeEndsAt = endsAt.replace(/[&<>"']/g, "");
849
+ const safeDuration = duration.replace(/[^0-9]/g, "");
850
+ const catArt = "⠀⠀⠀⠀⠀⠀⢀⣀⠠⠐⠈⠄⠂⠁⠁⠂⠄⠈⠐⠠⡀⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠂⠄⠈⠐⠠⣀⢀⠁⠂⠄⠈⠐⠠⡀⢀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⡠⠋⠁⠀⠀⢀⣀⣠⠀⠀⠀⢀⣀⣀⠀⠀⠈⡷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⠉⠀⣀⣠⣀⠀⠀⠈⠳⠀⠀⠀⣀⣀⠀⠀⠀⠾⡆⠀⠀⠀⠀\n⠀⠀⠀⣸⠏⠀⣤⠱⠈⠅⠋⠁⠀⠀⠲⠎⠁⠤⠘⠳⠀⠀⢿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡿⠁⡢⠞⡁⠘⠥⠋⠀⠀⣤⠱⠉⠘⠥⠞⡅⠀⠀⣸⡄⠀⠀⠀\n⠀⠀⢀⣽⡁⠀⢿⡀⠀⠘⡄⡅⠀⠀⣸⡦⡇⠀⢀⡦⢿⠀⠀⢿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣨⣇⠀⣸⣏⠀⠘⡢⣸⠀⠀⢗⡘⢀⠀⠘⡄⢿⠀⠀⣸⣇⠀⠀⠀\n⠀⣰⠉⠋⡷⠀⠛⢿⣦⢄⡣⡅⠀⠀⠸⢇⣰⡤⢴⡿⠁⠀⡴⠋⠁⠉⡱⠀⠀⠀⠀⠀⠀⠀⠀⢀⠎⠉⠻⡆⠉⢿⣳⡤⡣⣼⠁⠀⡻⡁⣦⢼⡿⠃⠀⡴⠋⠁⠋⣦⠀\n⣺⠁⠀⡠⠉⠶⣤⠉⠁⠓⠛⠋⠁⡠⠉⠶⡤⠀⠀⢷⠀⠀⠀⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢐⡿⠀⢀⠉⠶⡤⠉⠁⠓⠻⠋⡠⠉⠶⣤⠉⠁⠉⠋⣦⠀⠀⢨⡆\n⣽⠀⣸⠄⠣⠐⣹⠀⡤⠏⠁⢀⢀⠘⢀⠉⡦⡄⣹⡁⠔⡁⣇⠀⣸⡅⠀⠀⠀⠀⠀⠀⠀⠀⣸⡅⠀⢗⠘⡄⠸⣇⠀⡠⠏⠉⠐⠄⡁⠉⡦⠀⠘⡄⠸⣇⠀⠀⢿\n⠉⢷⡀⢿⣤⢴⠏⢾⡁⠐⢀⠀⠘⠄⢀⠉⡄⣹⢿⣤⢴⡿⠁⡢⠏⠁⠀⠀⠀⠀⠀⠀⠀⠀⠉⢷⡀⡿⣦⢼⣳⣏⠀⠘⠁⢀⠘⠘⢄⠉⡢⣆⡿⡤⢶⡾⠁⣼⠃\n⠀⣸⢷⠉⠉⠉⣸⠏⣆⠀⡠⠘⢀⠉⠘⡆⣈⢾⠉⠉⠉⡴⠏⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡿⡆⠉⠉⠁⢾⣳⠀⡀⠘⡄⠉⡢⠋⡄⢿⠀⠉⠉⠡⡾⡅⠀\n⠐⡅⠀⠀⠀⠀⠸⢿⠞⣦⢴⡢⣰⡶⢿⠏⠀⠀⠀⠀⠀⠀⣰⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢺⠀⠀⠀⠀⠀⡻⣷⢶⣦⢈⢆⡤⢮⢿⠃⠀⠀⠀⠀⠀⣸⠀\n⠀⠸⡄⠀⠀⠀⠀⠉⡻⠶⣮⣝⢻⣳⣧⠏⠁⠀⠀⠀⠀⡠⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠣⡄⠀⠀⠀⠀⠋⡷⢮⣽⢻⠿⣷⠶⠋⠀⠀⠀⠀⠀⡼⠃⠀\n⠀⠀⠉⣇⡢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠀⠀⠀⠀⡀⢾⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡻⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠀⠀⠀⠀⢀⡴⠏⠁⠀⠀\n⠀⠀⠀⣸⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠋⢿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢨⡷⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⡻⡇⠀⠀⠀\n⠀⠀⠀⣸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡅⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⡅⠀⠀⠀\n⠀⠀⠀⣽⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢾⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡨⡅⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⡅⠀⠀⠀";
851
+ return `<!DOCTYPE html>
852
+ <html lang="en">
853
+ <head>
854
+ <meta charset="UTF-8">
855
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
856
+ <title>${safeBotName} — Locked In</title>
857
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🐾</text></svg>">
858
+ <link rel="preconnect" href="https://fonts.googleapis.com">
859
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
860
+ <style>
861
+ *{margin:0;padding:0;box-sizing:border-box}
862
+ body{
863
+ font-family:'JetBrains Mono',monospace;
864
+ background:${t.bg};color:${t.text};
865
+ min-height:100vh;display:flex;flex-direction:column;
866
+ align-items:center;justify-content:center;
867
+ overflow:hidden;
868
+ }
869
+ .container{text-align:center;position:relative;z-index:1;padding:12px 16px}
870
+ .cat{font-size:7px;line-height:1.2;color:${t.accent};white-space:pre;margin:0 auto 14px;animation:breathe 4s ease-in-out infinite;text-shadow:0 0 0 transparent}
871
+ .label{font-size:11px;text-transform:uppercase;letter-spacing:3px;color:${t.accent};font-weight:600;margin-bottom:8px}
872
+ .timer{font-size:48px;font-weight:700;letter-spacing:4px;color:${t.text};line-height:1;margin-bottom:8px;font-variant-numeric:tabular-nums}
873
+ .progress{width:200px;height:3px;background:${t.border};border-radius:2px;margin:0 auto 10px;overflow:hidden}
874
+ .progress-fill{height:100%;background:${t.accent};border-radius:2px;width:0%;transition:width 1s linear}
875
+ .sub{font-size:11px;color:${t.textDim};margin-bottom:12px}
876
+ .quote{font-size:10px;color:${t.textDim};font-style:italic;height:14px;transition:opacity .8s}
877
+ .session-info{font-size:10px;color:${t.textDim};display:flex;gap:16px;justify-content:center;margin-top:12px}
878
+ .session-info span{display:flex;align-items:center;gap:5px}
879
+ .dot{width:5px;height:5px;border-radius:50%;background:${t.accent};animation:pulse 2s ease-in-out infinite}
880
+ .complete .cat{color:${t.done};animation:none;text-shadow:0 0 12px ${t.done}40}
881
+ .complete .label{color:${t.done}}
882
+ .complete .timer{color:${t.done}}
883
+ .complete .dot{background:${t.done};animation:none}
884
+ .complete .progress-fill{background:${t.done}}
885
+ @keyframes breathe{0%,100%{opacity:.6;text-shadow:0 0 0 transparent}50%{opacity:1;text-shadow:0 0 15px ${t.accent}30}}
886
+ @keyframes pulse{0%,100%{opacity:.4}50%{opacity:1}}
887
+ @keyframes flash{0%,100%{opacity:1}50%{opacity:.3}}
888
+ .flash .timer{animation:flash .5s ease-in-out 3}
889
+ .glow{position:fixed;width:200px;height:200px;border-radius:50%;background:${t.accent};opacity:.03;filter:blur(80px);pointer-events:none}
890
+ .glow-1{top:-60px;left:-60px}
891
+ .glow-2{bottom:-60px;right:-60px}
892
+ </style>
893
+ </head>
894
+ <body>
895
+ <div class="glow glow-1"></div>
896
+ <div class="glow glow-2"></div>
897
+ <div class="container" id="container">
898
+ <pre class="cat" id="cat"></pre>
899
+ <div class="label" id="label">Locked In</div>
900
+ <div class="timer" id="timer">--:--</div>
901
+ <div class="progress"><div class="progress-fill" id="progress"></div></div>
902
+ <div class="sub" id="sub">${safeDuration} min session</div>
903
+ <div class="quote" id="quote"></div>
904
+ <div class="session-info">
905
+ <span><span class="dot"></span> Focus active</span>
906
+ </div>
907
+ </div>
908
+ <script>
909
+ var catArt = ${JSON.stringify(catArt)};
910
+ document.getElementById("cat").textContent = catArt;
911
+
912
+ var endsAt = "${safeEndsAt}";
913
+ var durationMs = ${safeDuration} * 60000;
914
+ var endTime = endsAt ? new Date(endsAt).getTime() : 0;
915
+ var startTime = endTime - durationMs;
916
+ var done = false;
917
+ var progressEl = document.getElementById("progress");
918
+ var quotes = [
919
+ "Deep work is the superpower of the 21st century.",
920
+ "Focus is not about saying yes. It\u2019s about saying no.",
921
+ "The successful warrior is the average person with laser focus.",
922
+ "What you stay focused on will grow.",
923
+ "Starve your distractions. Feed your focus.",
924
+ "Small daily improvements lead to stunning results.",
925
+ "You don\u2019t need more time. You need more focus.",
926
+ "Discipline is choosing what you want most over what you want now."
927
+ ];
928
+ var qIdx = 0;
929
+
930
+ function pad(n) { return n < 10 ? "0" + n : "" + n; }
931
+
932
+ function tick() {
933
+ if (!endTime || done) return;
934
+ var now = Date.now();
935
+ var diff = endTime - now;
936
+ var elapsed = now - startTime;
937
+ var pct = Math.min(100, Math.max(0, (elapsed / durationMs) * 100));
938
+ progressEl.style.width = pct + "%";
939
+
940
+ if (diff <= 0) {
941
+ done = true;
942
+ document.getElementById("timer").textContent = "00:00";
943
+ document.getElementById("label").textContent = "Session Complete";
944
+ document.getElementById("sub").textContent = "Great work! Take a break.";
945
+ document.getElementById("container").classList.add("complete", "flash");
946
+ document.title = "${safeBotName} \u2014 Done!";
947
+ progressEl.style.width = "100%";
948
+ return;
949
+ }
950
+
951
+ var h = Math.floor(diff / 3600000);
952
+ var m = Math.floor((diff % 3600000) / 60000);
953
+ var s = Math.floor((diff % 60000) / 1000);
954
+ document.getElementById("timer").textContent = h > 0 ? pad(h) + ":" + pad(m) + ":" + pad(s) : pad(m) + ":" + pad(s);
955
+ }
956
+
957
+ function rotateQuote() {
958
+ var el = document.getElementById("quote");
959
+ el.style.opacity = "0";
960
+ setTimeout(function() {
961
+ el.textContent = quotes[qIdx % quotes.length];
962
+ el.style.opacity = "1";
963
+ qIdx++;
964
+ }, 800);
965
+ }
966
+
967
+ tick();
968
+ setInterval(tick, 1000);
969
+ rotateQuote();
970
+ setInterval(rotateQuote, 12000);
971
+ </script>
972
+ </body>
973
+ </html>`;
974
+ }
975
+
976
+ //#endregion
977
+ //#region src/core/dashboard-server.ts
978
+ const CONFIG_DIR = path$1.join(os$1.homedir(), ".config", "openpaw");
979
+ const CONFIG_FILE = path$1.join(CONFIG_DIR, "dashboard.json");
980
+ function readConfig() {
981
+ try {
982
+ return JSON.parse(fs$1.readFileSync(CONFIG_FILE, "utf-8"));
983
+ } catch {
984
+ return {
985
+ theme: "paw",
986
+ botName: "Paw",
987
+ port: 3141,
988
+ tasks: []
989
+ };
990
+ }
991
+ }
992
+ function writeConfig(config) {
993
+ if (!fs$1.existsSync(CONFIG_DIR)) fs$1.mkdirSync(CONFIG_DIR, { recursive: true });
994
+ fs$1.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
995
+ }
996
+ function parseBody(req) {
997
+ return new Promise((resolve, reject) => {
998
+ let body = "";
999
+ req.on("data", (chunk) => {
1000
+ body += chunk.toString();
1001
+ });
1002
+ req.on("end", () => {
1003
+ try {
1004
+ resolve(body ? JSON.parse(body) : {});
1005
+ } catch {
1006
+ reject(new Error("Invalid JSON"));
1007
+ }
1008
+ });
1009
+ req.on("error", reject);
1010
+ });
1011
+ }
1012
+ function json(res, data, status = 200) {
1013
+ res.writeHead(status, { "Content-Type": "application/json" });
1014
+ res.end(JSON.stringify(data));
1015
+ }
1016
+ function html(res, content) {
1017
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1018
+ res.end(content);
1019
+ }
1020
+ function startDashboard(opts) {
1021
+ const config = readConfig();
1022
+ if (opts.theme) config.theme = opts.theme;
1023
+ if (opts.botName) config.botName = opts.botName;
1024
+ if (opts.port) config.port = opts.port;
1025
+ writeConfig(config);
1026
+ const port = config.port || 3141;
1027
+ const server = http.createServer(async (req, res) => {
1028
+ const url = new URL(req.url || "/", `http://localhost:${port}`);
1029
+ const method = req.method || "GET";
1030
+ const pathname = url.pathname;
1031
+ try {
1032
+ if (method === "GET" && pathname === "/") {
1033
+ const current = readConfig();
1034
+ const themeParam = url.searchParams.get("theme");
1035
+ if (themeParam && (themeParam === "paw" || themeParam === "midnight" || themeParam === "neon" || themeParam === "rose")) {
1036
+ current.theme = themeParam;
1037
+ writeConfig(current);
1038
+ }
1039
+ html(res, generateDashboardHTML(current.theme, current.botName));
1040
+ return;
1041
+ }
1042
+ if (method === "GET" && pathname === "/focus") {
1043
+ const current = readConfig();
1044
+ const ends = url.searchParams.get("ends") || "";
1045
+ const dur = url.searchParams.get("duration") || "0";
1046
+ html(res, generateFocusTimerHTML(current.theme, current.botName, ends, dur));
1047
+ return;
1048
+ }
1049
+ if (method === "GET" && pathname === "/api/focus") {
1050
+ try {
1051
+ const sessionPath = path$1.join(CONFIG_DIR, "lockin-session.json");
1052
+ const raw = fs$1.readFileSync(sessionPath, "utf-8");
1053
+ json(res, JSON.parse(raw));
1054
+ } catch {
1055
+ json(res, { active: false });
1056
+ }
1057
+ return;
1058
+ }
1059
+ if (method === "GET" && pathname === "/api/tasks") {
1060
+ json(res, readConfig().tasks);
1061
+ return;
1062
+ }
1063
+ if (method === "POST" && pathname === "/api/tasks") {
1064
+ const body = await parseBody(req);
1065
+ const current = readConfig();
1066
+ const task = {
1067
+ id: crypto.randomUUID().slice(0, 8),
1068
+ title: String(body.title || "Untitled"),
1069
+ description: body.description ? String(body.description) : void 0,
1070
+ status: body.status || "todo",
1071
+ priority: body.priority || "normal",
1072
+ tags: Array.isArray(body.tags) ? body.tags.map(String).slice(0, 5) : void 0,
1073
+ order: current.tasks.filter((t) => t.status === (body.status || "todo")).length,
1074
+ createdAt: new Date().toISOString()
1075
+ };
1076
+ current.tasks.push(task);
1077
+ writeConfig(current);
1078
+ json(res, task, 201);
1079
+ return;
1080
+ }
1081
+ const putMatch = method === "PUT" && pathname.match(/^\/api\/tasks\/(.+)$/);
1082
+ if (putMatch) {
1083
+ const id = putMatch[1];
1084
+ const body = await parseBody(req);
1085
+ const current = readConfig();
1086
+ const task = current.tasks.find((t) => t.id === id);
1087
+ if (!task) {
1088
+ json(res, { error: "Not found" }, 404);
1089
+ return;
1090
+ }
1091
+ if (body.title !== void 0) task.title = String(body.title);
1092
+ if (body.description !== void 0) task.description = body.description ? String(body.description) : void 0;
1093
+ if (body.status !== void 0) task.status = body.status;
1094
+ if (body.priority !== void 0) task.priority = body.priority;
1095
+ if (body.order !== void 0) task.order = Number(body.order);
1096
+ if (body.tags !== void 0) task.tags = Array.isArray(body.tags) ? body.tags.map(String).slice(0, 5) : void 0;
1097
+ writeConfig(current);
1098
+ json(res, task);
1099
+ return;
1100
+ }
1101
+ const delMatch = method === "DELETE" && pathname.match(/^\/api\/tasks\/(.+)$/);
1102
+ if (delMatch) {
1103
+ const id = delMatch[1];
1104
+ const current = readConfig();
1105
+ current.tasks = current.tasks.filter((t) => t.id !== id);
1106
+ writeConfig(current);
1107
+ json(res, { ok: true });
1108
+ return;
1109
+ }
1110
+ if (method === "DELETE" && pathname === "/api/tasks/done") {
1111
+ const current = readConfig();
1112
+ const removed = current.tasks.filter((t) => t.status === "done").length;
1113
+ current.tasks = current.tasks.filter((t) => t.status !== "done");
1114
+ writeConfig(current);
1115
+ json(res, {
1116
+ ok: true,
1117
+ removed
1118
+ });
1119
+ return;
1120
+ }
1121
+ if (method === "GET" && pathname === "/api/standup") {
1122
+ const current = readConfig();
1123
+ const today = new Date().toISOString().slice(0, 10);
1124
+ const done = current.tasks.filter((t) => t.status === "done");
1125
+ const inProg = current.tasks.filter((t) => t.status === "in-progress");
1126
+ const todo = current.tasks.filter((t) => t.status === "todo");
1127
+ const high = current.tasks.filter((t) => t.priority === "high" && t.status !== "done");
1128
+ let md = `# Standup — ${today}\n\n`;
1129
+ if (done.length) {
1130
+ md += `## Completed (${done.length})\n`;
1131
+ for (const t of done) md += `- ${t.title}${t.tags?.length ? ` [${t.tags.join(", ")}]` : ""}\n`;
1132
+ md += "\n";
1133
+ }
1134
+ if (inProg.length) {
1135
+ md += `## In Progress (${inProg.length})\n`;
1136
+ for (const t of inProg) md += `- ${t.title}${t.description ? ` — ${t.description}` : ""}\n`;
1137
+ md += "\n";
1138
+ }
1139
+ if (todo.length) {
1140
+ md += `## Todo (${todo.length})\n`;
1141
+ for (const t of todo) md += `- ${t.title}\n`;
1142
+ md += "\n";
1143
+ }
1144
+ if (high.length) {
1145
+ md += `## Blockers / High Priority\n`;
1146
+ for (const t of high) md += `- ${t.title}\n`;
1147
+ md += "\n";
1148
+ }
1149
+ md += `---\n*Generated by ${current.botName} Dashboard*\n`;
1150
+ res.writeHead(200, {
1151
+ "Content-Type": "text/markdown; charset=utf-8",
1152
+ "Content-Disposition": `attachment; filename="standup-${today}.md"`
1153
+ });
1154
+ res.end(md);
1155
+ return;
1156
+ }
1157
+ if (method === "GET" && pathname === "/api/config") {
1158
+ const { theme, botName, port: p } = readConfig();
1159
+ json(res, {
1160
+ theme,
1161
+ botName,
1162
+ port: p
1163
+ });
1164
+ return;
1165
+ }
1166
+ if (method === "PUT" && pathname === "/api/config") {
1167
+ const body = await parseBody(req);
1168
+ const current = readConfig();
1169
+ if (body.theme && [
1170
+ "paw",
1171
+ "midnight",
1172
+ "neon",
1173
+ "rose"
1174
+ ].includes(String(body.theme))) current.theme = body.theme;
1175
+ if (body.botName && typeof body.botName === "string") current.botName = body.botName.slice(0, 20);
1176
+ writeConfig(current);
1177
+ json(res, {
1178
+ theme: current.theme,
1179
+ botName: current.botName
1180
+ });
1181
+ return;
1182
+ }
1183
+ json(res, { error: "Not found" }, 404);
1184
+ } catch (err) {
1185
+ json(res, { error: "Internal error" }, 500);
1186
+ }
1187
+ });
1188
+ server.listen(port, () => {
1189
+ const url = `http://localhost:${port}`;
1190
+ console.log(`\n Dashboard running at ${url}\n`);
1191
+ if (!opts.noOpen) {
1192
+ const platform = os$1.platform();
1193
+ if (platform === "darwin") import("node:child_process").then((cp) => cp.exec(`open ${url}`));
1194
+ else if (platform === "linux") import("node:child_process").then((cp) => cp.exec(`xdg-open ${url}`));
1195
+ }
1196
+ });
1197
+ return server;
1198
+ }
1199
+
1200
+ //#endregion
1201
+ export { CONFIG_FILE, readConfig, startDashboard, writeConfig };