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.
@@ -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
- ${isError ? `<button class="card-retry-btn" onclick="event.stopPropagation();openRetry('${task.id}')">${t('retryCard')} Edit</button>` : ''}
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
- // Calculate new priority_order based on drop position
1321
- const colKey = { todo: 'todo', in_progress: 'prog', done: 'done', error: 'err' }[status] || status;
1322
- const tasks = allTasks().filter(t => t.status === status && t.id !== draggedId);
1323
-
1324
- let newOrder;
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
- // Dropped at end
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 targetTask = tasks.find(t => t.id === insertBeforeId);
1331
- const targetOrder = targetTask?.priority_order ?? 1;
1332
- // Shift tasks after insertion point
1333
- newOrder = targetOrder - 0.5;
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
- await fetch(`/api/tasks/${draggedId}`, {
1337
- method: 'PATCH',
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({ status, priority_order: newOrder }),
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
 
@@ -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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeboard",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "AI engineering team — from PRD to working mobile app, autonomously",
5
5
  "type": "module",
6
6
  "bin": {