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.
@@ -1,707 +0,0 @@
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
- };
49
- function generateDashboardHTML(theme, botName) {
50
- const t = THEME_COLORS[theme];
51
- const safeBotName = botName.replace(/[&<>"']/g, "");
52
- return `<!DOCTYPE html>
53
- <html lang="en">
54
- <head>
55
- <meta charset="UTF-8">
56
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
57
- <title>${safeBotName} — Task Dashboard</title>
58
- <link rel="preconnect" href="https://fonts.googleapis.com">
59
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
60
- <style>
61
- *{margin:0;padding:0;box-sizing:border-box}
62
- :root{
63
- --bg:${t.bg};--surface:${t.surface};--surface-hover:${t.surfaceHover};
64
- --border:${t.border};--text:${t.text};--text-dim:${t.textDim};
65
- --accent:${t.accent};--accent-dim:${t.accentDim};--done:${t.done};
66
- --high:${t.high};--low:${t.low};
67
- }
68
- body{font-family:'JetBrains Mono',monospace;background:var(--bg);color:var(--text);min-height:100vh;font-size:13px}
69
- header{display:flex;align-items:center;justify-content:space-between;padding:20px 28px;border-bottom:1px solid var(--border)}
70
- .logo{display:flex;align-items:center;gap:10px;font-size:18px;font-weight:700;color:var(--accent)}
71
- .theme-switcher{display:flex;gap:8px}
72
- .theme-dot{width:14px;height:14px;border-radius:50%;cursor:pointer;border:2px solid var(--border);transition:border-color .2s}
73
- .theme-dot:hover,.theme-dot.active{border-color:var(--text)}
74
- .theme-dot[data-theme="paw"]{background:#b4783c}
75
- .theme-dot[data-theme="midnight"]{background:#6688cc}
76
- .theme-dot[data-theme="neon"]{background:#00ff88}
77
- .board{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;padding:24px 28px;min-height:calc(100vh - 80px)}
78
- .column{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:16px;display:flex;flex-direction:column;min-height:300px}
79
- .column.drag-over{border-color:var(--accent);background:var(--surface-hover)}
80
- .col-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid var(--border)}
81
- .col-title{font-weight:600;font-size:14px;text-transform:uppercase;letter-spacing:1px}
82
- .col-count{font-size:11px;color:var(--text-dim);background:var(--bg);padding:2px 8px;border-radius:10px}
83
- .col-todo .col-title{color:var(--accent)}
84
- .col-progress .col-title{color:var(--text)}
85
- .col-done .col-title{color:var(--done)}
86
- .cards{flex:1;display:flex;flex-direction:column;gap:8px;min-height:50px}
87
- .card{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px;cursor:grab;transition:all .15s;position:relative}
88
- .card:hover{border-color:var(--accent);transform:translateY(-1px)}
89
- .card.dragging{opacity:.4;transform:scale(.95)}
90
- .card-title{font-size:13px;font-weight:500;margin-bottom:6px;outline:none}
91
- .card-title:focus{border-bottom:1px solid var(--accent)}
92
- .card-desc{font-size:11px;color:var(--text-dim);margin-bottom:8px;outline:none}
93
- .card-desc:focus{border-bottom:1px solid var(--accent)}
94
- .card-footer{display:flex;align-items:center;justify-content:space-between}
95
- .priority{display:flex;gap:4px}
96
- .priority-dot{width:8px;height:8px;border-radius:50%;cursor:pointer;transition:transform .1s}
97
- .priority-dot:hover{transform:scale(1.4)}
98
- .priority-dot.high{background:var(--high)}
99
- .priority-dot.normal{background:var(--accent)}
100
- .priority-dot.low{background:var(--low)}
101
- .priority-dot.active{box-shadow:0 0 0 2px var(--bg),0 0 0 4px currentColor}
102
- .card-delete{font-size:11px;color:var(--text-dim);cursor:pointer;opacity:0;transition:opacity .15s}
103
- .card:hover .card-delete{opacity:1}
104
- .card-delete:hover{color:var(--high)}
105
- .add-btn{display:flex;align-items:center;justify-content:center;gap:6px;padding:10px;margin-top:8px;border:1px dashed var(--border);border-radius:8px;color:var(--text-dim);cursor:pointer;font-family:inherit;font-size:12px;background:none;transition:all .15s;width:100%}
106
- .add-btn:hover{border-color:var(--accent);color:var(--accent)}
107
- .empty{text-align:center;padding:40px 16px;color:var(--text-dim);font-size:12px;line-height:1.8}
108
- .add-form{display:none;flex-direction:column;gap:8px;margin-top:8px}
109
- .add-form.show{display:flex}
110
- .add-form input,.add-form textarea{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;resize:none}
111
- .add-form input:focus,.add-form textarea:focus{border-color:var(--accent)}
112
- .form-actions{display:flex;gap:6px}
113
- .form-actions button{flex:1;padding:6px;border:1px solid var(--border);border-radius:6px;font-family:inherit;font-size:11px;cursor:pointer;background:var(--surface);color:var(--text);transition:all .15s}
114
- .form-actions .save{background:var(--accent);color:var(--bg);border-color:var(--accent);font-weight:600}
115
- .form-actions .save:hover{opacity:.9}
116
- .form-actions .cancel:hover{border-color:var(--text-dim)}
117
- .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}
118
- .toast.show{opacity:1}
119
- .toast.error{background:var(--high);color:#fff}
120
- .toast.success{background:var(--done);color:#fff}
121
- </style>
122
- </head>
123
- <body>
124
- <header>
125
- <div class="logo"><span>&#x1F43E;</span> ${safeBotName}</div>
126
- <div class="theme-switcher">
127
- <div class="theme-dot${theme === "paw" ? " active" : ""}" data-theme="paw" title="Paw"></div>
128
- <div class="theme-dot${theme === "midnight" ? " active" : ""}" data-theme="midnight" title="Midnight"></div>
129
- <div class="theme-dot${theme === "neon" ? " active" : ""}" data-theme="neon" title="Neon"></div>
130
- </div>
131
- </header>
132
- <div class="board" id="board"></div>
133
- <div class="toast" id="toast"></div>
134
- <script>
135
- var BOTNAME = "${safeBotName}";
136
- var tasks = [];
137
- var dragId = null;
138
-
139
- function toast(msg, type) {
140
- var el = document.getElementById("toast");
141
- el.textContent = msg;
142
- el.className = "toast show " + (type || "success");
143
- setTimeout(function() { el.className = "toast"; }, 2000);
144
- }
145
-
146
- function api(path, opts) {
147
- return fetch("/api/" + path, Object.assign({ headers: {"Content-Type": "application/json"} }, opts || {}))
148
- .then(function(r) {
149
- if (!r.ok) throw new Error("Request failed: " + r.status);
150
- return r.json();
151
- })
152
- .catch(function(err) {
153
- toast(err.message, "error");
154
- throw err;
155
- });
156
- }
157
-
158
- function load() {
159
- api("tasks").then(function(data) {
160
- tasks = data;
161
- render();
162
- });
163
- }
164
-
165
- function esc(s) {
166
- var d = document.createElement("div");
167
- d.textContent = s;
168
- return d.innerHTML;
169
- }
170
-
171
- function render() {
172
- var statuses = ["todo", "in-progress", "done"];
173
- var labels = { "todo": "Todo", "in-progress": "In Progress", "done": "Done" };
174
- var colClass = { "todo": "col-todo", "in-progress": "col-progress", "done": "col-done" };
175
- var board = document.getElementById("board");
176
- board.innerHTML = "";
177
-
178
- statuses.forEach(function(status) {
179
- var filtered = tasks.filter(function(t) { return t.status === status; }).sort(function(a, b) { return a.order - b.order; });
180
-
181
- var col = document.createElement("div");
182
- col.className = "column " + colClass[status];
183
- col.setAttribute("data-status", status);
184
-
185
- // Header
186
- var header = document.createElement("div");
187
- header.className = "col-header";
188
- header.innerHTML = '<span class="col-title">' + labels[status] + '</span><span class="col-count">' + filtered.length + '</span>';
189
- col.appendChild(header);
190
-
191
- // Cards container
192
- var cardsDiv = document.createElement("div");
193
- cardsDiv.className = "cards";
194
-
195
- if (filtered.length === 0) {
196
- cardsDiv.innerHTML = '<div class="empty">No tasks here yet.<br>' + BOTNAME + ' is waiting for work! &#x1F43E;</div>';
197
- } else {
198
- filtered.forEach(function(task) {
199
- var card = document.createElement("div");
200
- card.className = "card";
201
- card.draggable = true;
202
- card.setAttribute("data-id", task.id);
203
-
204
- var titleDiv = document.createElement("div");
205
- titleDiv.className = "card-title";
206
- titleDiv.contentEditable = "true";
207
- titleDiv.textContent = task.title;
208
- titleDiv.addEventListener("blur", function() {
209
- var newTitle = this.textContent.trim();
210
- if (newTitle && newTitle !== task.title) {
211
- api("tasks/" + task.id, { method: "PUT", body: JSON.stringify({ title: newTitle }) });
212
- task.title = newTitle;
213
- }
214
- });
215
- card.appendChild(titleDiv);
216
-
217
- if (task.description) {
218
- var descDiv = document.createElement("div");
219
- descDiv.className = "card-desc";
220
- descDiv.contentEditable = "true";
221
- descDiv.textContent = task.description;
222
- descDiv.addEventListener("blur", function() {
223
- var newDesc = this.textContent.trim();
224
- api("tasks/" + task.id, { method: "PUT", body: JSON.stringify({ description: newDesc || undefined }) });
225
- task.description = newDesc || undefined;
226
- });
227
- card.appendChild(descDiv);
228
- }
229
-
230
- var footer = document.createElement("div");
231
- footer.className = "card-footer";
232
-
233
- var priorityDiv = document.createElement("div");
234
- priorityDiv.className = "priority";
235
- ["high", "normal", "low"].forEach(function(p) {
236
- var dot = document.createElement("div");
237
- dot.className = "priority-dot " + p + (task.priority === p ? " active" : "");
238
- dot.title = p.charAt(0).toUpperCase() + p.slice(1);
239
- dot.addEventListener("click", function(e) {
240
- e.stopPropagation();
241
- api("tasks/" + task.id, { method: "PUT", body: JSON.stringify({ priority: p }) }).then(function() {
242
- task.priority = p;
243
- render();
244
- });
245
- });
246
- priorityDiv.appendChild(dot);
247
- });
248
- footer.appendChild(priorityDiv);
249
-
250
- var delBtn = document.createElement("span");
251
- delBtn.className = "card-delete";
252
- delBtn.innerHTML = "&#x2715;";
253
- delBtn.addEventListener("click", function(e) {
254
- e.stopPropagation();
255
- api("tasks/" + task.id, { method: "DELETE" }).then(function() {
256
- tasks = tasks.filter(function(t) { return t.id !== task.id; });
257
- render();
258
- });
259
- });
260
- footer.appendChild(delBtn);
261
- card.appendChild(footer);
262
-
263
- // Drag events
264
- card.addEventListener("dragstart", function(e) {
265
- dragId = task.id;
266
- this.classList.add("dragging");
267
- e.dataTransfer.effectAllowed = "move";
268
- });
269
- card.addEventListener("dragend", function() {
270
- this.classList.remove("dragging");
271
- dragId = null;
272
- document.querySelectorAll(".column").forEach(function(c) { c.classList.remove("drag-over"); });
273
- });
274
-
275
- cardsDiv.appendChild(card);
276
- });
277
- }
278
- col.appendChild(cardsDiv);
279
-
280
- // Add button
281
- var addBtn = document.createElement("button");
282
- addBtn.type = "button";
283
- addBtn.className = "add-btn";
284
- addBtn.textContent = "+ Add task";
285
- addBtn.addEventListener("click", function() {
286
- formDiv.classList.add("show");
287
- titleInput.focus();
288
- });
289
- col.appendChild(addBtn);
290
-
291
- // Add form
292
- var formDiv = document.createElement("div");
293
- formDiv.className = "add-form";
294
-
295
- var titleInput = document.createElement("input");
296
- titleInput.type = "text";
297
- titleInput.placeholder = "Task title...";
298
- titleInput.addEventListener("keydown", function(e) {
299
- if (e.key === "Enter") doSave();
300
- });
301
- formDiv.appendChild(titleInput);
302
-
303
- var descInput = document.createElement("textarea");
304
- descInput.placeholder = "Description (optional)";
305
- descInput.rows = 2;
306
- formDiv.appendChild(descInput);
307
-
308
- var actions = document.createElement("div");
309
- actions.className = "form-actions";
310
-
311
- var cancelBtn = document.createElement("button");
312
- cancelBtn.type = "button";
313
- cancelBtn.className = "cancel";
314
- cancelBtn.textContent = "Cancel";
315
- cancelBtn.addEventListener("click", function() {
316
- formDiv.classList.remove("show");
317
- titleInput.value = "";
318
- descInput.value = "";
319
- });
320
- actions.appendChild(cancelBtn);
321
-
322
- var saveBtn = document.createElement("button");
323
- saveBtn.type = "button";
324
- saveBtn.className = "save";
325
- saveBtn.textContent = "Add";
326
-
327
- function doSave() {
328
- var title = titleInput.value.trim();
329
- if (!title) return;
330
- var desc = descInput.value.trim();
331
- saveBtn.disabled = true;
332
- saveBtn.textContent = "...";
333
- var body = { title: title, status: status, priority: "normal" };
334
- if (desc) body.description = desc;
335
- api("tasks", { method: "POST", body: JSON.stringify(body) })
336
- .then(function(task) {
337
- tasks.push(task);
338
- render();
339
- toast("Task added!");
340
- })
341
- .catch(function() {
342
- saveBtn.disabled = false;
343
- saveBtn.textContent = "Add";
344
- });
345
- }
346
-
347
- saveBtn.addEventListener("click", doSave);
348
- actions.appendChild(saveBtn);
349
- formDiv.appendChild(actions);
350
- col.appendChild(formDiv);
351
-
352
- // Drop events on column
353
- col.addEventListener("dragover", function(e) {
354
- e.preventDefault();
355
- e.dataTransfer.dropEffect = "move";
356
- this.classList.add("drag-over");
357
- });
358
- col.addEventListener("dragleave", function() {
359
- this.classList.remove("drag-over");
360
- });
361
- col.addEventListener("drop", function(e) {
362
- e.preventDefault();
363
- this.classList.remove("drag-over");
364
- if (!dragId) return;
365
- var newStatus = this.getAttribute("data-status");
366
- var order = tasks.filter(function(t) { return t.status === newStatus; }).length;
367
- api("tasks/" + dragId, { method: "PUT", body: JSON.stringify({ status: newStatus, order: order }) })
368
- .then(function() {
369
- var task = tasks.find(function(t) { return t.id === dragId; });
370
- if (task) { task.status = newStatus; task.order = order; }
371
- render();
372
- });
373
- });
374
-
375
- board.appendChild(col);
376
- });
377
- }
378
-
379
- // Theme switcher
380
- document.querySelectorAll(".theme-dot").forEach(function(dot) {
381
- dot.addEventListener("click", function() {
382
- window.location.href = "/?theme=" + this.getAttribute("data-theme");
383
- });
384
- });
385
-
386
- load();
387
- </script>
388
- </body>
389
- </html>`;
390
- }
391
- function generateFocusTimerHTML(theme, botName, endsAt, duration) {
392
- const t = THEME_COLORS[theme];
393
- const safeBotName = botName.replace(/[&<>"']/g, "");
394
- const safeEndsAt = endsAt.replace(/[&<>"']/g, "");
395
- const safeDuration = duration.replace(/[^0-9]/g, "");
396
- return `<!DOCTYPE html>
397
- <html lang="en">
398
- <head>
399
- <meta charset="UTF-8">
400
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
401
- <title>${safeBotName} — Locked In</title>
402
- <link rel="preconnect" href="https://fonts.googleapis.com">
403
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
404
- <style>
405
- *{margin:0;padding:0;box-sizing:border-box}
406
- body{
407
- font-family:'JetBrains Mono',monospace;
408
- background:${t.bg};color:${t.text};
409
- min-height:100vh;display:flex;flex-direction:column;
410
- align-items:center;justify-content:center;
411
- overflow:hidden;
412
- }
413
- .container{text-align:center;position:relative;z-index:1}
414
- .ring-wrap{position:relative;width:200px;height:200px;margin:0 auto 20px}
415
- .ring-svg{width:200px;height:200px;transform:rotate(-90deg)}
416
- .ring-bg{fill:none;stroke:${t.border};stroke-width:4}
417
- .ring-fg{fill:none;stroke:${t.accent};stroke-width:4;stroke-linecap:round;transition:stroke-dashoffset 1s linear}
418
- .paw-ascii{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:13px;line-height:1.15;color:${t.accent};white-space:pre;animation:breathe 4s ease-in-out infinite}
419
- .trail{font-size:14px;color:${t.textDim};letter-spacing:8px;margin-bottom:16px;height:20px}
420
- .trail span{display:inline-block;animation:walk 2s ease-in-out infinite}
421
- .trail span:nth-child(1){animation-delay:0s}
422
- .trail span:nth-child(2){animation-delay:.25s}
423
- .trail span:nth-child(3){animation-delay:.5s}
424
- .trail span:nth-child(4){animation-delay:.75s}
425
- .trail span:nth-child(5){animation-delay:1s}
426
- .label{font-size:12px;text-transform:uppercase;letter-spacing:3px;color:${t.accent};font-weight:600;margin-bottom:24px}
427
- .timer{font-size:64px;font-weight:700;letter-spacing:4px;color:${t.text};line-height:1;margin-bottom:12px;font-variant-numeric:tabular-nums}
428
- .sub{font-size:12px;color:${t.textDim};margin-bottom:20px}
429
- .quote{font-size:11px;color:${t.textDim};font-style:italic;height:16px;transition:opacity .8s}
430
- .session-info{font-size:11px;color:${t.textDim};display:flex;gap:20px;justify-content:center;margin-top:20px}
431
- .session-info span{display:flex;align-items:center;gap:5px}
432
- .dot{width:5px;height:5px;border-radius:50%;background:${t.accent};animation:pulse 2s ease-in-out infinite}
433
- .complete .label{color:${t.done}}
434
- .complete .timer{color:${t.done}}
435
- .complete .dot{background:${t.done};animation:none}
436
- .complete .ring-fg{stroke:${t.done}}
437
- .complete .paw-ascii{color:${t.done};animation:none}
438
- .complete .trail span{animation:none;color:${t.done}}
439
- @keyframes breathe{0%,100%{opacity:.6;transform:translate(-50%,-50%) scale(.95)}50%{opacity:1;transform:translate(-50%,-50%) scale(1.05)}}
440
- @keyframes walk{0%,100%{opacity:.3;transform:translateY(0)}50%{opacity:1;transform:translateY(-3px)}}
441
- @keyframes pulse{0%,100%{opacity:.4}50%{opacity:1}}
442
- @keyframes flash{0%,100%{opacity:1}50%{opacity:.3}}
443
- .flash .timer{animation:flash .5s ease-in-out 3}
444
- .glow{position:fixed;width:250px;height:250px;border-radius:50%;background:${t.accent};opacity:.03;filter:blur(80px);pointer-events:none}
445
- .glow-1{top:-80px;left:-80px}
446
- .glow-2{bottom:-80px;right:-80px}
447
- .glow-3{top:50%;left:50%;transform:translate(-50%,-50%);opacity:.015}
448
- </style>
449
- </head>
450
- <body>
451
- <div class="glow glow-1"></div>
452
- <div class="glow glow-2"></div>
453
- <div class="glow glow-3"></div>
454
- <div class="container" id="container">
455
- <div class="ring-wrap">
456
- <svg class="ring-svg" viewBox="0 0 200 200">
457
- <circle class="ring-bg" cx="100" cy="100" r="90"/>
458
- <circle class="ring-fg" id="ring" cx="100" cy="100" r="90" stroke-dasharray="565.49" stroke-dashoffset="565.49"/>
459
- </svg>
460
- <pre class="paw-ascii" id="paw"> __
461
- / \\
462
- | .. |
463
- \\ -- /
464
- \\__/
465
- ||</pre>
466
- </div>
467
- <div class="trail" id="trail"><span>.</span><span>o</span><span>O</span><span>o</span><span>.</span></div>
468
- <div class="label" id="label">Locked In</div>
469
- <div class="timer" id="timer">--:--</div>
470
- <div class="sub" id="sub">${safeDuration} min session</div>
471
- <div class="quote" id="quote"></div>
472
- <div class="session-info">
473
- <span><span class="dot"></span> Focus active</span>
474
- </div>
475
- </div>
476
- <script>
477
- var endsAt = "${safeEndsAt}";
478
- var durationMs = ${safeDuration} * 60000;
479
- var endTime = endsAt ? new Date(endsAt).getTime() : 0;
480
- var startTime = endTime - durationMs;
481
- var circumference = 2 * Math.PI * 90;
482
- var done = false;
483
- var ring = document.getElementById("ring");
484
- var quotes = [
485
- "Deep work is the superpower of the 21st century.",
486
- "Focus is not about saying yes. It's about saying no.",
487
- "The successful warrior is the average person with laser focus.",
488
- "What you stay focused on will grow.",
489
- "Starve your distractions. Feed your focus.",
490
- "Small daily improvements lead to stunning results.",
491
- "You don't need more time. You need more focus.",
492
- "Discipline is choosing what you want most over what you want now."
493
- ];
494
- var qIdx = 0;
495
-
496
- function pad(n) { return n < 10 ? "0" + n : "" + n; }
497
-
498
- function updateRing(progress) {
499
- var offset = circumference * (1 - progress);
500
- ring.style.strokeDashoffset = Math.max(0, offset);
501
- }
502
-
503
- function tick() {
504
- if (!endTime || done) return;
505
- var now = Date.now();
506
- var diff = endTime - now;
507
- var elapsed = now - startTime;
508
- var progress = Math.min(1, Math.max(0, elapsed / durationMs));
509
- updateRing(progress);
510
-
511
- if (diff <= 0) {
512
- done = true;
513
- document.getElementById("timer").textContent = "00:00";
514
- document.getElementById("label").textContent = "Session Complete";
515
- document.getElementById("sub").textContent = "Great work! Take a break.";
516
- document.getElementById("paw").textContent = " __\\n / \\\\\\n| ^^ |\\n\\\\ \\u2323 /\\n \\\\__/\\n ||";
517
- document.getElementById("container").classList.add("complete", "flash");
518
- document.title = "${safeBotName} \\u2014 Done!";
519
- updateRing(1);
520
- return;
521
- }
522
-
523
- var h = Math.floor(diff / 3600000);
524
- var m = Math.floor((diff % 3600000) / 60000);
525
- var s = Math.floor((diff % 60000) / 1000);
526
- document.getElementById("timer").textContent = h > 0 ? pad(h) + ":" + pad(m) + ":" + pad(s) : pad(m) + ":" + pad(s);
527
- }
528
-
529
- function rotateQuote() {
530
- var el = document.getElementById("quote");
531
- el.style.opacity = "0";
532
- setTimeout(function() {
533
- el.textContent = quotes[qIdx % quotes.length];
534
- el.style.opacity = "1";
535
- qIdx++;
536
- }, 800);
537
- }
538
-
539
- tick();
540
- setInterval(tick, 1000);
541
- rotateQuote();
542
- setInterval(rotateQuote, 12000);
543
- </script>
544
- </body>
545
- </html>`;
546
- }
547
-
548
- //#endregion
549
- //#region src/core/dashboard-server.ts
550
- const CONFIG_DIR = path$1.join(os$1.homedir(), ".config", "openpaw");
551
- const CONFIG_FILE = path$1.join(CONFIG_DIR, "dashboard.json");
552
- function readConfig() {
553
- try {
554
- return JSON.parse(fs$1.readFileSync(CONFIG_FILE, "utf-8"));
555
- } catch {
556
- return {
557
- theme: "paw",
558
- botName: "Paw",
559
- port: 3141,
560
- tasks: []
561
- };
562
- }
563
- }
564
- function writeConfig(config) {
565
- if (!fs$1.existsSync(CONFIG_DIR)) fs$1.mkdirSync(CONFIG_DIR, { recursive: true });
566
- fs$1.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
567
- }
568
- function parseBody(req) {
569
- return new Promise((resolve, reject) => {
570
- let body = "";
571
- req.on("data", (chunk) => {
572
- body += chunk.toString();
573
- });
574
- req.on("end", () => {
575
- try {
576
- resolve(body ? JSON.parse(body) : {});
577
- } catch {
578
- reject(new Error("Invalid JSON"));
579
- }
580
- });
581
- req.on("error", reject);
582
- });
583
- }
584
- function json(res, data, status = 200) {
585
- res.writeHead(status, { "Content-Type": "application/json" });
586
- res.end(JSON.stringify(data));
587
- }
588
- function html(res, content) {
589
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
590
- res.end(content);
591
- }
592
- function startDashboard(opts) {
593
- const config = readConfig();
594
- if (opts.theme) config.theme = opts.theme;
595
- if (opts.botName) config.botName = opts.botName;
596
- if (opts.port) config.port = opts.port;
597
- writeConfig(config);
598
- const port = config.port || 3141;
599
- const server = http.createServer(async (req, res) => {
600
- const url = new URL(req.url || "/", `http://localhost:${port}`);
601
- const method = req.method || "GET";
602
- const pathname = url.pathname;
603
- try {
604
- if (method === "GET" && pathname === "/") {
605
- const current = readConfig();
606
- const themeParam = url.searchParams.get("theme");
607
- if (themeParam && (themeParam === "paw" || themeParam === "midnight" || themeParam === "neon")) {
608
- current.theme = themeParam;
609
- writeConfig(current);
610
- }
611
- html(res, generateDashboardHTML(current.theme, current.botName));
612
- return;
613
- }
614
- if (method === "GET" && pathname === "/focus") {
615
- const current = readConfig();
616
- const ends = url.searchParams.get("ends") || "";
617
- const dur = url.searchParams.get("duration") || "0";
618
- html(res, generateFocusTimerHTML(current.theme, current.botName, ends, dur));
619
- return;
620
- }
621
- if (method === "GET" && pathname === "/api/focus") {
622
- try {
623
- const sessionPath = path$1.join(CONFIG_DIR, "lockin-session.json");
624
- const raw = fs$1.readFileSync(sessionPath, "utf-8");
625
- json(res, JSON.parse(raw));
626
- } catch {
627
- json(res, { active: false });
628
- }
629
- return;
630
- }
631
- if (method === "GET" && pathname === "/api/tasks") {
632
- json(res, readConfig().tasks);
633
- return;
634
- }
635
- if (method === "POST" && pathname === "/api/tasks") {
636
- const body = await parseBody(req);
637
- const current = readConfig();
638
- const task = {
639
- id: crypto.randomUUID().slice(0, 8),
640
- title: String(body.title || "Untitled"),
641
- description: body.description ? String(body.description) : void 0,
642
- status: body.status || "todo",
643
- priority: body.priority || "normal",
644
- order: current.tasks.filter((t) => t.status === (body.status || "todo")).length,
645
- createdAt: new Date().toISOString()
646
- };
647
- current.tasks.push(task);
648
- writeConfig(current);
649
- json(res, task, 201);
650
- return;
651
- }
652
- const putMatch = method === "PUT" && pathname.match(/^\/api\/tasks\/(.+)$/);
653
- if (putMatch) {
654
- const id = putMatch[1];
655
- const body = await parseBody(req);
656
- const current = readConfig();
657
- const task = current.tasks.find((t) => t.id === id);
658
- if (!task) {
659
- json(res, { error: "Not found" }, 404);
660
- return;
661
- }
662
- if (body.title !== void 0) task.title = String(body.title);
663
- if (body.description !== void 0) task.description = body.description ? String(body.description) : void 0;
664
- if (body.status !== void 0) task.status = body.status;
665
- if (body.priority !== void 0) task.priority = body.priority;
666
- if (body.order !== void 0) task.order = Number(body.order);
667
- writeConfig(current);
668
- json(res, task);
669
- return;
670
- }
671
- const delMatch = method === "DELETE" && pathname.match(/^\/api\/tasks\/(.+)$/);
672
- if (delMatch) {
673
- const id = delMatch[1];
674
- const current = readConfig();
675
- current.tasks = current.tasks.filter((t) => t.id !== id);
676
- writeConfig(current);
677
- json(res, { ok: true });
678
- return;
679
- }
680
- if (method === "GET" && pathname === "/api/config") {
681
- const { theme, botName, port: p } = readConfig();
682
- json(res, {
683
- theme,
684
- botName,
685
- port: p
686
- });
687
- return;
688
- }
689
- json(res, { error: "Not found" }, 404);
690
- } catch (err) {
691
- json(res, { error: "Internal error" }, 500);
692
- }
693
- });
694
- server.listen(port, () => {
695
- const url = `http://localhost:${port}`;
696
- console.log(`\n Dashboard running at ${url}\n`);
697
- if (!opts.noOpen) {
698
- const platform = os$1.platform();
699
- if (platform === "darwin") import("node:child_process").then((cp) => cp.exec(`open ${url}`));
700
- else if (platform === "linux") import("node:child_process").then((cp) => cp.exec(`xdg-open ${url}`));
701
- }
702
- });
703
- return server;
704
- }
705
-
706
- //#endregion
707
- export { CONFIG_FILE, readConfig, startDashboard, writeConfig };