pawmode 1.2.0 → 1.3.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/README.md CHANGED
@@ -74,7 +74,7 @@ npx pawmode --preset developer --yes
74
74
 
75
75
  ## What is OpenPaw?
76
76
 
77
- OpenPaw turns **Claude Code** into a full personal assistant. One command, 39 skills, and a really good boy who fetches your emails, plays your music, and controls your smart home.
77
+ OpenPaw turns **Claude Code** into a full personal assistant. One command, 38 skills, and a really good boy who fetches your emails, plays your music, and controls your smart home.
78
78
 
79
79
  ```
80
80
  npx pawmode
@@ -211,10 +211,10 @@ Get started fast with a preset, or choose `Custom` to sniff through skills one b
211
211
 
212
212
  | Preset | Skills |
213
213
  |---|---|
214
- | **Everything** | All 39 skills for your platform |
214
+ | **Everything** | All 38 skills for your platform |
215
215
  | **Essentials** | Email, calendar, notes, music, weather, clipboard, browser, system, notifications |
216
216
  | **Productivity** | Notes, Obsidian, tasks, email, calendar, Slack, cloud files, notifications |
217
- | **Developer** | GitHub, Linear, Jira, browser, network, AI, cron |
217
+ | **Developer** | GitHub, Linear, Jira, browser, network, cron |
218
218
  | **Creative & Media** | Music, video, screen, voice, browser, research |
219
219
  | **Smart Home** | Lights, speakers, Bluetooth, system, display, notifications |
220
220
 
@@ -222,7 +222,7 @@ Get started fast with a preset, or choose `Custom` to sniff through skills one b
222
222
 
223
223
  ## Skills
224
224
 
225
- 39 capabilities across 8 categories. Install only what you need.
225
+ 38 capabilities across 8 categories. Install only what you need.
226
226
 
227
227
  ### Productivity
228
228
 
@@ -231,7 +231,7 @@ Get started fast with a preset, or choose `Custom` to sniff through skills one b
231
231
  | `c-notes` | Apple Notes + Reminders | [`memo`](https://github.com/antoniorodr/memo) [`remindctl`](https://github.com/nicklama/remindctl) |
232
232
  | `c-obsidian` | Obsidian vault management | [`obsidian-cli`](https://github.com/yakitrak/obsidian-cli) |
233
233
  | `c-notion` | Notion pages + databases | [`notion-cli`](https://github.com/litencatt/notion-cli) |
234
- | `c-tasks` | Todoist / Things 3 / Taskwarrior | [`todoist-cli`](https://github.com/sachaos/todoist) [`things-cli`](https://github.com/thingsapi/things-cli) [`taskwarrior`](https://github.com/GothenburgBitFactory/taskwarrior) |
234
+ | `c-tasks` | Todoist / Taskwarrior | [`todoist-cli`](https://github.com/sachaos/todoist) [`taskwarrior`](https://github.com/GothenburgBitFactory/taskwarrior) |
235
235
 
236
236
  ### Communication
237
237
 
@@ -249,7 +249,7 @@ Get started fast with a preset, or choose `Custom` to sniff through skills one b
249
249
  |---|---|---|
250
250
  | `c-music` | Spotify playback + search | [`spogo`](https://github.com/steipete/spogo) |
251
251
  | `c-video` | YouTube download + convert | [`yt-dlp`](https://github.com/yt-dlp/yt-dlp) [`ffmpeg`](https://ffmpeg.org/) |
252
- | `c-video-edit` | Programmatic video creation | [`remotion`](https://github.com/remotion-dev/remotion) [`editly`](https://github.com/mifi/editly) |
252
+ | `c-video-edit` | Programmatic video creation | [`remotion`](https://github.com/remotion-dev/remotion) |
253
253
  | `c-screen` | Screenshots, OCR, webcam | [`peekaboo`](https://github.com/steipete/peekaboo) [`camsnap`](https://github.com/nicklama/camsnap) |
254
254
  | `c-voice` | Speech-to-text + TTS | [`sag`](https://github.com/steipete/sag) |
255
255
 
@@ -293,7 +293,6 @@ Get started fast with a preset, or choose `Custom` to sniff through skills one b
293
293
  | `c-tracking` | Package tracking (UPS, FedEx, etc.) | [`ordercli`](https://github.com/steipete/ordercli) |
294
294
  | `c-secrets` | 1Password / Bitwarden | [`op`](https://developer.1password.com/docs/cli/) [`bw`](https://github.com/bitwarden/clients) |
295
295
  | `c-network` | DNS lookups + HTTP client | [`doggo`](https://github.com/mr-karan/doggo) [`httpie`](https://github.com/httpie/cli) |
296
- | `c-ai` | Query LLMs — pipe text, chat, summarize | [`llm`](https://github.com/simonw/llm) [`aichat`](https://github.com/sigoden/aichat) |
297
296
 
298
297
  ### Developer
299
298
 
@@ -321,6 +320,7 @@ Get started fast with a preset, or choose `Custom` to sniff through skills one b
321
320
  | `openpaw schedule list` | List scheduled jobs |
322
321
  | `openpaw schedule costs` | View cost usage and daily cap |
323
322
  | `openpaw schedule set-cap <usd>` | Set daily cost cap |
323
+ | `openpaw configure` | Configure your setup — add skills, personality, dashboard |
324
324
  | `openpaw list` | Show all available skills |
325
325
  | `openpaw add <skills>` | Add skills — `openpaw add notes music email` |
326
326
  | `openpaw remove <skills>` | Remove skills |
@@ -452,7 +452,7 @@ Instructions for Claude on how to use the CLI tool...
452
452
  - **5-minute setup** — from zero to personal assistant
453
453
  - **Telegram built-in** — talk to Claude from your phone
454
454
  - **Task dashboard** — local kanban board with 3 themes
455
- - **39 skills** — email, music, smart home, GitHub, scheduling, video editing, and more
455
+ - **38 skills** — email, music, smart home, GitHub, scheduling, video editing, and more
456
456
  - **Cost-controlled scheduling** — automate tasks without runaway bills
457
457
  - **Open source** — MIT license, community-driven
458
458
 
@@ -48,12 +48,13 @@ function generateDashboardHTML(theme, botName) {
48
48
  }
49
49
  };
50
50
  const t = themes[theme];
51
+ const safeBotName = botName.replace(/[&<>"']/g, "");
51
52
  return `<!DOCTYPE html>
52
53
  <html lang="en">
53
54
  <head>
54
55
  <meta charset="UTF-8">
55
56
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
56
- <title>${botName} — Task Dashboard</title>
57
+ <title>${safeBotName} — Task Dashboard</title>
57
58
  <link rel="preconnect" href="https://fonts.googleapis.com">
58
59
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
59
60
  <style>
@@ -113,168 +114,273 @@ header{display:flex;align-items:center;justify-content:space-between;padding:20p
113
114
  .form-actions .save{background:var(--accent);color:var(--bg);border-color:var(--accent);font-weight:600}
114
115
  .form-actions .save:hover{opacity:.9}
115
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}
116
121
  </style>
117
122
  </head>
118
123
  <body>
119
124
  <header>
120
- <div class="logo"><span>&#x1F43E;</span> ${botName}</div>
125
+ <div class="logo"><span>&#x1F43E;</span> ${safeBotName}</div>
121
126
  <div class="theme-switcher">
122
127
  <div class="theme-dot${theme === "paw" ? " active" : ""}" data-theme="paw" title="Paw"></div>
123
128
  <div class="theme-dot${theme === "midnight" ? " active" : ""}" data-theme="midnight" title="Midnight"></div>
124
129
  <div class="theme-dot${theme === "neon" ? " active" : ""}" data-theme="neon" title="Neon"></div>
125
130
  </div>
126
131
  </header>
127
- <div class="board">
128
- <div class="column col-todo" data-status="todo">
129
- <div class="col-header"><span class="col-title">Todo</span><span class="col-count" id="count-todo">0</span></div>
130
- <div class="cards" id="cards-todo"></div>
131
- <button class="add-btn" onclick="showForm('todo')">+ Add task</button>
132
- <div class="add-form" id="form-todo">
133
- <input type="text" id="input-todo" placeholder="Task title..." onkeydown="if(event.key==='Enter')saveNew('todo')">
134
- <textarea id="desc-todo" placeholder="Description (optional)" rows="2"></textarea>
135
- <div class="form-actions">
136
- <button class="cancel" onclick="hideForm('todo')">Cancel</button>
137
- <button class="save" onclick="saveNew('todo')">Add</button>
138
- </div>
139
- </div>
140
- </div>
141
- <div class="column col-progress" data-status="in-progress">
142
- <div class="col-header"><span class="col-title">In Progress</span><span class="col-count" id="count-in-progress">0</span></div>
143
- <div class="cards" id="cards-in-progress"></div>
144
- <button class="add-btn" onclick="showForm('in-progress')">+ Add task</button>
145
- <div class="add-form" id="form-in-progress">
146
- <input type="text" id="input-in-progress" placeholder="Task title..." onkeydown="if(event.key==='Enter')saveNew('in-progress')">
147
- <textarea id="desc-in-progress" placeholder="Description (optional)" rows="2"></textarea>
148
- <div class="form-actions">
149
- <button class="cancel" onclick="hideForm('in-progress')">Cancel</button>
150
- <button class="save" onclick="saveNew('in-progress')">Add</button>
151
- </div>
152
- </div>
153
- </div>
154
- <div class="column col-done" data-status="done">
155
- <div class="col-header"><span class="col-title">Done</span><span class="col-count" id="count-done">0</span></div>
156
- <div class="cards" id="cards-done"></div>
157
- <button class="add-btn" onclick="showForm('done')">+ Add task</button>
158
- <div class="add-form" id="form-done">
159
- <input type="text" id="input-done" placeholder="Task title..." onkeydown="if(event.key==='Enter')saveNew('done')">
160
- <textarea id="desc-done" placeholder="Description (optional)" rows="2"></textarea>
161
- <div class="form-actions">
162
- <button class="cancel" onclick="hideForm('done')">Cancel</button>
163
- <button class="save" onclick="saveNew('done')">Add</button>
164
- </div>
165
- </div>
166
- </div>
167
- </div>
132
+ <div class="board" id="board"></div>
133
+ <div class="toast" id="toast"></div>
168
134
  <script>
169
- let tasks=[];
170
- let dragId=null;
135
+ var BOTNAME = "${safeBotName}";
136
+ var tasks = [];
137
+ var dragId = null;
171
138
 
172
- async function api(path,opts){
173
- const r=await fetch('/api/'+path,{headers:{'Content-Type':'application/json'},...opts});
174
- return r.json();
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);
175
144
  }
176
145
 
177
- async function load(){
178
- tasks=await api('tasks');
179
- render();
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
+ });
180
156
  }
181
157
 
182
- function render(){
183
- for(const s of['todo','in-progress','done']){
184
- const container=document.getElementById('cards-'+s);
185
- const filtered=tasks.filter(t=>t.status===s).sort((a,b)=>a.order-b.order);
186
- document.getElementById('count-'+s).textContent=filtered.length;
187
- if(filtered.length===0){
188
- container.innerHTML='<div class="empty">No tasks here yet.<br>${botName} is waiting for work! &#x1F43E;</div>';
189
- }else{
190
- container.innerHTML=filtered.map(t=>\`
191
- <div class="card" draggable="true" data-id="\${t.id}" ondragstart="onDragStart(event)" ondragend="onDragEnd(event)">
192
- <div class="card-title" contenteditable="true" onblur="updateTitle('\${t.id}',this.textContent)">\${esc(t.title)}</div>
193
- \${t.description?'<div class="card-desc" contenteditable="true" onblur="updateDesc(\\''+t.id+'\\',this.textContent)">'+esc(t.description)+'</div>':''}
194
- <div class="card-footer">
195
- <div class="priority">
196
- <div class="priority-dot high\${t.priority==='high'?' active':''}" onclick="setPriority('\${t.id}','high')" title="High"></div>
197
- <div class="priority-dot normal\${t.priority==='normal'?' active':''}" onclick="setPriority('\${t.id}','normal')" title="Normal"></div>
198
- <div class="priority-dot low\${t.priority==='low'?' active':''}" onclick="setPriority('\${t.id}','low')" title="Low"></div>
199
- </div>
200
- <span class="card-delete" onclick="del('\${t.id}')">&#x2715;</span>
201
- </div>
202
- </div>
203
- \`).join('');
204
- }
158
+ function load() {
159
+ api("tasks").then(function(data) {
160
+ tasks = data;
161
+ render();
162
+ });
205
163
  }
164
+
165
+ function esc(s) {
166
+ var d = document.createElement("div");
167
+ d.textContent = s;
168
+ return d.innerHTML;
206
169
  }
207
170
 
208
- function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML}
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 = "";
209
177
 
210
- function showForm(s){document.getElementById('form-'+s).classList.add('show');document.getElementById('input-'+s).focus()}
211
- function hideForm(s){document.getElementById('form-'+s).classList.remove('show');document.getElementById('input-'+s).value='';document.getElementById('desc-'+s).value=''}
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; });
212
180
 
213
- async function saveNew(status){
214
- const title=document.getElementById('input-'+status).value.trim();
215
- if(!title)return;
216
- const desc=document.getElementById('desc-'+status).value.trim();
217
- const task=await api('tasks',{method:'POST',body:JSON.stringify({title,description:desc||undefined,status,priority:'normal'})});
218
- tasks.push(task);
219
- render();
220
- hideForm(status);
221
- }
181
+ var col = document.createElement("div");
182
+ col.className = "column " + colClass[status];
183
+ col.setAttribute("data-status", status);
222
184
 
223
- async function updateTitle(id,title){
224
- title=title.trim();if(!title)return;
225
- await api('tasks/'+id,{method:'PUT',body:JSON.stringify({title})});
226
- const t=tasks.find(x=>x.id===id);if(t)t.title=title;
227
- }
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);
228
190
 
229
- async function updateDesc(id,desc){
230
- desc=desc.trim();
231
- await api('tasks/'+id,{method:'PUT',body:JSON.stringify({description:desc||undefined})});
232
- const t=tasks.find(x=>x.id===id);if(t)t.description=desc||undefined;
233
- }
191
+ // Cards container
192
+ var cardsDiv = document.createElement("div");
193
+ cardsDiv.className = "cards";
234
194
 
235
- async function setPriority(id,priority){
236
- await api('tasks/'+id,{method:'PUT',body:JSON.stringify({priority})});
237
- const t=tasks.find(x=>x.id===id);if(t)t.priority=priority;
238
- render();
239
- }
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);
240
203
 
241
- async function del(id){
242
- await api('tasks/'+id,{method:'DELETE'});
243
- tasks=tasks.filter(t=>t.id!==id);
244
- render();
245
- }
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);
246
216
 
247
- function onDragStart(e){
248
- dragId=e.target.dataset.id;
249
- e.target.classList.add('dragging');
250
- e.dataTransfer.effectAllowed='move';
251
- }
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
+ }
252
229
 
253
- function onDragEnd(e){
254
- e.target.classList.remove('dragging');
255
- dragId=null;
256
- document.querySelectorAll('.column').forEach(c=>c.classList.remove('drag-over'));
257
- }
230
+ var footer = document.createElement("div");
231
+ footer.className = "card-footer";
258
232
 
259
- document.querySelectorAll('.column').forEach(col=>{
260
- col.addEventListener('dragover',e=>{e.preventDefault();e.dataTransfer.dropEffect='move';col.classList.add('drag-over')});
261
- col.addEventListener('dragleave',()=>col.classList.remove('drag-over'));
262
- col.addEventListener('drop',async e=>{
263
- e.preventDefault();col.classList.remove('drag-over');
264
- if(!dragId)return;
265
- const status=col.dataset.status;
266
- const order=tasks.filter(t=>t.status===status).length;
267
- await api('tasks/'+dragId,{method:'PUT',body:JSON.stringify({status,order})});
268
- const t=tasks.find(x=>x.id===dragId);
269
- if(t){t.status=status;t.order=order}
270
- render();
271
- });
272
- });
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);
273
249
 
274
- document.querySelectorAll('.theme-dot').forEach(dot=>{
275
- dot.addEventListener('click',()=>{
276
- window.location.href='/?theme='+dot.dataset.theme;
277
- });
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
+ });
278
384
  });
279
385
 
280
386
  load();
@@ -1,3 +1,3 @@
1
- import { CONFIG_FILE, readConfig, startDashboard, writeConfig } from "./dashboard-server--wwlA0Pa.js";
1
+ import { CONFIG_FILE, readConfig, startDashboard, writeConfig } from "./dashboard-server-BAyeozOa.js";
2
2
 
3
3
  export { startDashboard };