pawmode 1.2.0 → 1.4.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.
|
@@ -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>${
|
|
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>🐾</span> ${
|
|
125
|
+
<div class="logo"><span>🐾</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="
|
|
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
|
-
|
|
170
|
-
|
|
135
|
+
var BOTNAME = "${safeBotName}";
|
|
136
|
+
var tasks = [];
|
|
137
|
+
var dragId = null;
|
|
171
138
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if(filtered.length===0){
|
|
188
|
-
container.innerHTML='<div class="empty">No tasks here yet.<br>${botName} is waiting for work! 🐾</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}')">✕</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
|
|
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
|
|
211
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
195
|
+
if (filtered.length === 0) {
|
|
196
|
+
cardsDiv.innerHTML = '<div class="empty">No tasks here yet.<br>' + BOTNAME + ' is waiting for work! 🐾</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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
250
|
+
var delBtn = document.createElement("span");
|
|
251
|
+
delBtn.className = "card-delete";
|
|
252
|
+
delBtn.innerHTML = "✕";
|
|
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();
|