claudeboard 2.11.0 → 2.12.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/dashboard/index.html +149 -38
- package/dashboard/server.js +20 -0
- package/package.json +1 -1
package/dashboard/index.html
CHANGED
|
@@ -847,6 +847,36 @@
|
|
|
847
847
|
border-color: rgba(248,113,113,0.5);
|
|
848
848
|
}
|
|
849
849
|
|
|
850
|
+
/* ── CARD ACTION BAR (edit/delete/retry) ── */
|
|
851
|
+
.card-actions {
|
|
852
|
+
display: flex;
|
|
853
|
+
gap: 4px;
|
|
854
|
+
margin-top: 8px;
|
|
855
|
+
opacity: 0;
|
|
856
|
+
transition: opacity 0.15s;
|
|
857
|
+
}
|
|
858
|
+
.card:hover .card-actions { opacity: 1; }
|
|
859
|
+
.card-action-btn {
|
|
860
|
+
flex: 1;
|
|
861
|
+
padding: 4px 0;
|
|
862
|
+
border-radius: 4px;
|
|
863
|
+
border: 1px solid var(--border);
|
|
864
|
+
background: rgba(255,255,255,0.03);
|
|
865
|
+
color: var(--muted);
|
|
866
|
+
font-family: var(--mono);
|
|
867
|
+
font-size: 10px;
|
|
868
|
+
cursor: pointer;
|
|
869
|
+
transition: all 0.15s;
|
|
870
|
+
text-align: center;
|
|
871
|
+
}
|
|
872
|
+
.card-action-btn:hover { background: rgba(255,255,255,0.08); color: var(--text); }
|
|
873
|
+
.card-action-btn.retry { color: var(--yellow); border-color: rgba(251,191,36,0.3); }
|
|
874
|
+
.card-action-btn.retry:hover { background: rgba(251,191,36,0.1); }
|
|
875
|
+
.card-action-btn.del { color: var(--red); border-color: rgba(248,113,113,0.2); }
|
|
876
|
+
.card-action-btn.del:hover { background: rgba(248,113,113,0.12); }
|
|
877
|
+
.card-action-btn.edit { color: var(--accent); border-color: rgba(108,138,255,0.2); }
|
|
878
|
+
.card-action-btn.edit:hover { background: rgba(108,138,255,0.1); }
|
|
879
|
+
|
|
850
880
|
/* Empty column state */
|
|
851
881
|
.col-empty {
|
|
852
882
|
text-align: center;
|
|
@@ -1011,6 +1041,46 @@
|
|
|
1011
1041
|
</div>
|
|
1012
1042
|
</div>
|
|
1013
1043
|
|
|
1044
|
+
<!-- EDIT TASK MODAL -->
|
|
1045
|
+
<div class="overlay" id="editModal" onclick="if(event.target===this)closeEditTask()">
|
|
1046
|
+
<div class="modal">
|
|
1047
|
+
<div class="modal-title" data-i18n="editTaskTitle">Edit Task</div>
|
|
1048
|
+
<input type="hidden" id="e-id">
|
|
1049
|
+
<div class="field">
|
|
1050
|
+
<label data-i18n="fieldTitle">Title</label>
|
|
1051
|
+
<input type="text" id="e-title">
|
|
1052
|
+
</div>
|
|
1053
|
+
<div class="field">
|
|
1054
|
+
<label data-i18n="fieldDesc">Description</label>
|
|
1055
|
+
<textarea id="e-desc" rows="4"></textarea>
|
|
1056
|
+
</div>
|
|
1057
|
+
<div class="modal-grid">
|
|
1058
|
+
<div class="field">
|
|
1059
|
+
<label data-i18n="fieldPriority">Priority</label>
|
|
1060
|
+
<select id="e-priority">
|
|
1061
|
+
<option value="high" data-i18n="prioHigh">High</option>
|
|
1062
|
+
<option value="medium" data-i18n="prioMed">Medium</option>
|
|
1063
|
+
<option value="low" data-i18n="prioLow">Low</option>
|
|
1064
|
+
</select>
|
|
1065
|
+
</div>
|
|
1066
|
+
<div class="field">
|
|
1067
|
+
<label data-i18n="fieldType">Type</label>
|
|
1068
|
+
<select id="e-type">
|
|
1069
|
+
<option value="feature" data-i18n="typeFeature">Feature</option>
|
|
1070
|
+
<option value="bug" data-i18n="typeBug">Bug</option>
|
|
1071
|
+
<option value="config" data-i18n="typeConfig">Config</option>
|
|
1072
|
+
<option value="refactor" data-i18n="typeRefactor">Refactor</option>
|
|
1073
|
+
<option value="test" data-i18n="typeTest">Test</option>
|
|
1074
|
+
</select>
|
|
1075
|
+
</div>
|
|
1076
|
+
</div>
|
|
1077
|
+
<div class="modal-actions">
|
|
1078
|
+
<button class="btn-cancel" onclick="closeEditTask()" data-i18n="cancel">Cancel</button>
|
|
1079
|
+
<button class="btn-create" onclick="submitEditTask()" data-i18n="saveTask">Save Changes</button>
|
|
1080
|
+
</div>
|
|
1081
|
+
</div>
|
|
1082
|
+
</div>
|
|
1083
|
+
|
|
1014
1084
|
<!-- RETRY / EDIT MODAL -->
|
|
1015
1085
|
<div class="overlay" id="retryModal" onclick="if(event.target===this)closeRetry()">
|
|
1016
1086
|
<div class="modal">
|
|
@@ -1131,7 +1201,7 @@ function connectWS() {
|
|
|
1131
1201
|
ws.onclose = () => { setWS(false); setTimeout(connectWS, 2000); };
|
|
1132
1202
|
ws.onmessage = (e) => {
|
|
1133
1203
|
const { event, data } = JSON.parse(e.data);
|
|
1134
|
-
if (['task_update','task_added','task_started','task_complete','task_failed'].includes(event)) {
|
|
1204
|
+
if (['task_update','task_added','task_started','task_complete','task_failed','task_deleted','task_reordered'].includes(event)) {
|
|
1135
1205
|
loadBoard();
|
|
1136
1206
|
}
|
|
1137
1207
|
if (event === 'log') {
|
|
@@ -1250,7 +1320,11 @@ function cardHTML(task, position = null) {
|
|
|
1250
1320
|
<span class="tag ${task.type}">${task.type}</span>
|
|
1251
1321
|
${shortEpic ? `<span class="card-epic">${esc(shortEpic)}</span>` : ''}
|
|
1252
1322
|
</div>
|
|
1253
|
-
|
|
1323
|
+
<div class="card-actions" onclick="event.stopPropagation()">
|
|
1324
|
+
${isError ? `<button class="card-action-btn retry" onclick="openRetry('${task.id}')">${t('retryCard')}</button>` : ''}
|
|
1325
|
+
<button class="card-action-btn edit" onclick="openEditTask('${task.id}')" title="Edit">✎</button>
|
|
1326
|
+
<button class="card-action-btn del" onclick="deleteTask('${task.id}')" title="Delete">✕</button>
|
|
1327
|
+
</div>
|
|
1254
1328
|
</div>`;
|
|
1255
1329
|
}
|
|
1256
1330
|
|
|
@@ -1317,52 +1391,45 @@ async function onDrop(e, status) {
|
|
|
1317
1391
|
const insertBeforeId = col.dataset.insertBefore || '';
|
|
1318
1392
|
delete col.dataset.insertBefore;
|
|
1319
1393
|
|
|
1320
|
-
//
|
|
1321
|
-
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
|
|
1394
|
+
// Build the new ordered list of ids for this column
|
|
1395
|
+
// Start from the DOM — what's visually shown — plus insert dragged card
|
|
1396
|
+
const otherIds = [...col.querySelectorAll('.card')]
|
|
1397
|
+
.map(c => c.dataset.id)
|
|
1398
|
+
.filter(id => id && id !== draggedId);
|
|
1399
|
+
|
|
1400
|
+
let newOrderedIds;
|
|
1325
1401
|
if (!insertBeforeId) {
|
|
1326
|
-
//
|
|
1327
|
-
const maxOrder = tasks.length ? Math.max(...tasks.map(t => t.priority_order || 0)) : 0;
|
|
1328
|
-
newOrder = maxOrder + 1;
|
|
1402
|
+
newOrderedIds = [...otherIds, draggedId]; // dropped at end
|
|
1329
1403
|
} else {
|
|
1330
|
-
const
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1404
|
+
const insertIdx = otherIds.indexOf(insertBeforeId);
|
|
1405
|
+
if (insertIdx === -1) {
|
|
1406
|
+
newOrderedIds = [draggedId, ...otherIds];
|
|
1407
|
+
} else {
|
|
1408
|
+
newOrderedIds = [...otherIds.slice(0, insertIdx), draggedId, ...otherIds.slice(insertIdx)];
|
|
1409
|
+
}
|
|
1334
1410
|
}
|
|
1335
1411
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1412
|
+
// Update status first if moving to different column
|
|
1413
|
+
const draggedTask = allTasks().find(t => t.id === draggedId);
|
|
1414
|
+
if (draggedTask?.status !== status) {
|
|
1415
|
+
await fetch(`/api/tasks/${draggedId}`, {
|
|
1416
|
+
method: 'PATCH',
|
|
1417
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1418
|
+
body: JSON.stringify({ status }),
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// Persist the new order in one call
|
|
1423
|
+
await fetch('/api/tasks/reorder', {
|
|
1424
|
+
method: 'POST',
|
|
1338
1425
|
headers: { 'Content-Type': 'application/json' },
|
|
1339
|
-
body: JSON.stringify({
|
|
1426
|
+
body: JSON.stringify({ taskIds: newOrderedIds }),
|
|
1340
1427
|
});
|
|
1341
1428
|
|
|
1342
|
-
// Re-normalize priority_order for all tasks in this column after drop
|
|
1343
|
-
await normalizeColumnOrder(status);
|
|
1344
|
-
|
|
1345
1429
|
draggedId = null;
|
|
1346
1430
|
loadBoard();
|
|
1347
1431
|
}
|
|
1348
1432
|
|
|
1349
|
-
async function normalizeColumnOrder(status) {
|
|
1350
|
-
// Get all tasks in column sorted by current priority_order, renumber 1..N
|
|
1351
|
-
const tasks = allTasks()
|
|
1352
|
-
.filter(t => t.status === status)
|
|
1353
|
-
.sort((a, b) => (a.priority_order ?? 99) - (b.priority_order ?? 99));
|
|
1354
|
-
|
|
1355
|
-
for (let i = 0; i < tasks.length; i++) {
|
|
1356
|
-
if (tasks[i].priority_order !== i + 1) {
|
|
1357
|
-
await fetch(`/api/tasks/${tasks[i].id}`, {
|
|
1358
|
-
method: 'PATCH',
|
|
1359
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1360
|
-
body: JSON.stringify({ priority_order: i + 1 }),
|
|
1361
|
-
});
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
1433
|
// ── CARD SELECT ───────────────────────────────────────────────────────────────
|
|
1367
1434
|
async function selectCard(id) {
|
|
1368
1435
|
const task = allTasks().find(t => t.id === id);
|
|
@@ -1683,6 +1750,48 @@ function initTerminal() {
|
|
|
1683
1750
|
});
|
|
1684
1751
|
}
|
|
1685
1752
|
|
|
1753
|
+
// ── EDIT TASK ─────────────────────────────────────────────────────────────────
|
|
1754
|
+
function openEditTask(id) {
|
|
1755
|
+
const task = allTasks().find(t => t.id === id);
|
|
1756
|
+
if (!task) return;
|
|
1757
|
+
document.getElementById('e-id').value = id;
|
|
1758
|
+
document.getElementById('e-title').value = task.title || '';
|
|
1759
|
+
document.getElementById('e-desc').value = task.description || '';
|
|
1760
|
+
document.getElementById('e-priority').value = task.priority || 'medium';
|
|
1761
|
+
document.getElementById('e-type').value = task.type || 'feature';
|
|
1762
|
+
document.getElementById('editModal').style.display = 'flex';
|
|
1763
|
+
applyTranslations();
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
function closeEditTask() {
|
|
1767
|
+
document.getElementById('editModal').style.display = 'none';
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
async function submitEditTask() {
|
|
1771
|
+
const id = document.getElementById('e-id').value;
|
|
1772
|
+
const title = document.getElementById('e-title').value.trim();
|
|
1773
|
+
const description = document.getElementById('e-desc').value.trim();
|
|
1774
|
+
const priority = document.getElementById('e-priority').value;
|
|
1775
|
+
const type = document.getElementById('e-type').value;
|
|
1776
|
+
if (!title) return;
|
|
1777
|
+
|
|
1778
|
+
const priorityOrder = { high: 1, medium: 2, low: 3 };
|
|
1779
|
+
await fetch(`/api/tasks/${id}`, {
|
|
1780
|
+
method: 'PATCH',
|
|
1781
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1782
|
+
body: JSON.stringify({ title, description, priority, type, priority_order: priorityOrder[priority] }),
|
|
1783
|
+
});
|
|
1784
|
+
closeEditTask();
|
|
1785
|
+
loadBoard();
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
// ── DELETE TASK ───────────────────────────────────────────────────────────────
|
|
1789
|
+
async function deleteTask(id) {
|
|
1790
|
+
if (!confirm(currentLang === 'es' ? '¿Eliminar esta tarea?' : 'Delete this task?')) return;
|
|
1791
|
+
await fetch(`/api/tasks/${id}`, { method: 'DELETE' });
|
|
1792
|
+
loadBoard();
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1686
1795
|
// ── UTILS ─────────────────────────────────────────────────────────────────────
|
|
1687
1796
|
function esc(s) {
|
|
1688
1797
|
if (!s) return '';
|
|
@@ -1690,7 +1799,7 @@ function esc(s) {
|
|
|
1690
1799
|
}
|
|
1691
1800
|
|
|
1692
1801
|
document.addEventListener('keydown', e => {
|
|
1693
|
-
if (e.key === 'Escape') { closeModal(); closeRetry(); }
|
|
1802
|
+
if (e.key === 'Escape') { closeModal(); closeRetry(); closeEditTask(); }
|
|
1694
1803
|
if ((e.metaKey || e.ctrlKey) && e.key === 'n') { e.preventDefault(); openModal(); }
|
|
1695
1804
|
});
|
|
1696
1805
|
|
|
@@ -1724,6 +1833,7 @@ const STRINGS = {
|
|
|
1724
1833
|
expoIdle:'Expo not started. Click "Start Expo" to install dependencies and launch with tunnel.',
|
|
1725
1834
|
scanWith:'SCAN WITH EXPO GO',
|
|
1726
1835
|
retryCard:'↩ Retry',
|
|
1836
|
+
editTaskTitle:'Edit Task', saveTask:'Save Changes', deleteConfirm:'Delete this task?',
|
|
1727
1837
|
},
|
|
1728
1838
|
es: {
|
|
1729
1839
|
todo:'pendiente', running:'en curso', done:'listo', failed:'fallido',
|
|
@@ -1753,6 +1863,7 @@ const STRINGS = {
|
|
|
1753
1863
|
expoIdle:'Expo no iniciado. Hacé clic en "Iniciar Expo" para instalar dependencias y lanzar con tunnel.',
|
|
1754
1864
|
scanWith:'ESCANEAR CON EXPO GO',
|
|
1755
1865
|
retryCard:'↩ Reintentar',
|
|
1866
|
+
editTaskTitle:'Editar tarea', saveTask:'Guardar cambios', deleteConfirm:'¿Eliminar esta tarea?',
|
|
1756
1867
|
}
|
|
1757
1868
|
};
|
|
1758
1869
|
|
package/dashboard/server.js
CHANGED
|
@@ -369,6 +369,26 @@ app.patch("/api/tasks/:id", async (req, res) => {
|
|
|
369
369
|
res.json({ ok: true });
|
|
370
370
|
});
|
|
371
371
|
|
|
372
|
+
// DELETE a task
|
|
373
|
+
app.delete("/api/tasks/:id", async (req, res) => {
|
|
374
|
+
await supabase.from("cb_logs").delete().eq("task_id", req.params.id);
|
|
375
|
+
await supabase.from("cb_tasks").delete().eq("id", req.params.id);
|
|
376
|
+
broadcast("task_deleted", { id: req.params.id });
|
|
377
|
+
res.json({ ok: true });
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// POST reorder — receives ordered array of task ids for a column, renumbers them
|
|
381
|
+
app.post("/api/tasks/reorder", async (req, res) => {
|
|
382
|
+
const { taskIds } = req.body; // array of ids in new order
|
|
383
|
+
for (let i = 0; i < taskIds.length; i++) {
|
|
384
|
+
await supabase.from("cb_tasks")
|
|
385
|
+
.update({ priority_order: i + 1 })
|
|
386
|
+
.eq("id", taskIds[i]);
|
|
387
|
+
}
|
|
388
|
+
broadcast("task_reordered", { taskIds });
|
|
389
|
+
res.json({ ok: true });
|
|
390
|
+
});
|
|
391
|
+
|
|
372
392
|
app.get("/api/tasks/:id/logs", async (req, res) => {
|
|
373
393
|
const { data } = await supabase.from("cb_logs").select("*")
|
|
374
394
|
.eq("task_id", req.params.id).order("created_at");
|