claudeboard 2.11.0 → 2.13.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/agents/expo-health.js +4 -9
- package/dashboard/index.html +204 -42
- package/dashboard/server.js +20 -0
- package/package.json +1 -1
package/agents/expo-health.js
CHANGED
|
@@ -6,6 +6,7 @@ import fs from "fs";
|
|
|
6
6
|
import path from "path";
|
|
7
7
|
import { createRequire } from "module";
|
|
8
8
|
import { createConnection } from "net";
|
|
9
|
+
import { spawn as _spawn, execSync } from "child_process";
|
|
9
10
|
|
|
10
11
|
const require = createRequire(import.meta.url);
|
|
11
12
|
const MAX_FIX_ATTEMPTS = 5;
|
|
@@ -149,14 +150,13 @@ async function tryStartExpo(projectPath, port) {
|
|
|
149
150
|
done(false, `Expo did not become ready within 60s.\n${output.slice(-800)}`), 60000);
|
|
150
151
|
|
|
151
152
|
try {
|
|
152
|
-
const { spawn } = require("child_process");
|
|
153
153
|
// Use iOS simulator if available, fallback to web
|
|
154
|
-
const useIOS = process.env.CLAUDEBOARD_IOS === "1" ||
|
|
154
|
+
const useIOS = process.env.CLAUDEBOARD_IOS === "1" || isSimulatorAvailableSync();
|
|
155
155
|
const expoArgs = useIOS
|
|
156
156
|
? ["expo", "start", "--ios", "--port", String(port)]
|
|
157
157
|
: ["expo", "start", "--web", "--port", String(port)];
|
|
158
158
|
|
|
159
|
-
proc =
|
|
159
|
+
proc = _spawn("npx", expoArgs, {
|
|
160
160
|
cwd: projectPath,
|
|
161
161
|
env: (() => { const e = { ...process.env, CI: "1", EXPO_NO_INTERACTIVE: "1", EXPO_NO_DOTENV: "0" }; delete e.ANTHROPIC_API_KEY; return e; })(),
|
|
162
162
|
stdio: "pipe",
|
|
@@ -320,13 +320,10 @@ function getFatalError(output) {
|
|
|
320
320
|
}
|
|
321
321
|
|
|
322
322
|
// ── iOS Simulator detection ───────────────────────────────────────────────────
|
|
323
|
-
|
|
323
|
+
function isSimulatorAvailableSync() {
|
|
324
324
|
try {
|
|
325
|
-
const { execSync } = await import("child_process");
|
|
326
325
|
const output = execSync("xcrun simctl list devices booted 2>&1", { encoding: "utf8" });
|
|
327
|
-
// Check if any device is already booted
|
|
328
326
|
if (output.includes("Booted")) return true;
|
|
329
|
-
// Check if any iOS runtime is available
|
|
330
327
|
const runtimes = execSync("xcrun simctl list runtimes 2>&1", { encoding: "utf8" });
|
|
331
328
|
return runtimes.includes("iOS") && !runtimes.includes("unavailable");
|
|
332
329
|
} catch {
|
|
@@ -337,7 +334,6 @@ async function isSimulatorAvailable() {
|
|
|
337
334
|
// Take a screenshot of the iOS simulator
|
|
338
335
|
export async function screenshotSimulator(outputPath) {
|
|
339
336
|
try {
|
|
340
|
-
const { execSync } = await import("child_process");
|
|
341
337
|
execSync(`xcrun simctl io booted screenshot "${outputPath}"`, { stdio: "pipe" });
|
|
342
338
|
const data = fs.readFileSync(outputPath);
|
|
343
339
|
return { success: true, base64: data.toString("base64"), path: outputPath };
|
|
@@ -349,7 +345,6 @@ export async function screenshotSimulator(outputPath) {
|
|
|
349
345
|
// Tap on the iOS simulator at given coordinates
|
|
350
346
|
export async function tapSimulator(x, y) {
|
|
351
347
|
try {
|
|
352
|
-
const { execSync } = await import("child_process");
|
|
353
348
|
execSync(`xcrun simctl io booted tap ${x} ${y}`, { stdio: "pipe" });
|
|
354
349
|
return true;
|
|
355
350
|
} catch { return false; }
|
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,13 +1201,23 @@ 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
|
-
|
|
1204
|
+
|
|
1205
|
+
// Task state changes that require a board reload (status flipped, new task, deleted)
|
|
1206
|
+
if (['task_added','task_started','task_complete','task_failed','task_deleted'].includes(event)) {
|
|
1135
1207
|
loadBoard();
|
|
1136
1208
|
}
|
|
1209
|
+
// Reorder: just re-render from current board data — no network call needed
|
|
1210
|
+
if (event === 'task_reordered') {
|
|
1211
|
+
renderKanban();
|
|
1212
|
+
updateStats();
|
|
1213
|
+
}
|
|
1214
|
+
// Single task field update: patch in-memory and re-render (no full reload)
|
|
1215
|
+
if (event === 'task_update') {
|
|
1216
|
+
patchTaskInMemory(data);
|
|
1217
|
+
}
|
|
1137
1218
|
if (event === 'log') {
|
|
1138
1219
|
board.logs.unshift(data);
|
|
1139
1220
|
if (activeTab === 'activity') renderLogs();
|
|
1140
|
-
// If on detail tab, don't touch activityPane — it'll refresh next time they switch
|
|
1141
1221
|
}
|
|
1142
1222
|
if (event === 'expo_status') {
|
|
1143
1223
|
setExpoStatus(data.status, data.url);
|
|
@@ -1151,6 +1231,12 @@ function connectWS() {
|
|
|
1151
1231
|
function setWS(on) {
|
|
1152
1232
|
document.getElementById('wsDot').className = 'ws-dot' + (on ? ' on' : '');
|
|
1153
1233
|
document.getElementById('wsLabel').textContent = on ? t('live') : t('reconnecting');
|
|
1234
|
+
// Poll only when disconnected
|
|
1235
|
+
if (on) {
|
|
1236
|
+
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
|
|
1237
|
+
} else {
|
|
1238
|
+
if (!pollInterval) pollInterval = setInterval(loadBoard, 10000);
|
|
1239
|
+
}
|
|
1154
1240
|
}
|
|
1155
1241
|
|
|
1156
1242
|
// ── DATA ─────────────────────────────────────────────────────────────────────
|
|
@@ -1250,13 +1336,45 @@ function cardHTML(task, position = null) {
|
|
|
1250
1336
|
<span class="tag ${task.type}">${task.type}</span>
|
|
1251
1337
|
${shortEpic ? `<span class="card-epic">${esc(shortEpic)}</span>` : ''}
|
|
1252
1338
|
</div>
|
|
1253
|
-
|
|
1339
|
+
<div class="card-actions" onclick="event.stopPropagation()">
|
|
1340
|
+
${isError ? `<button class="card-action-btn retry" onclick="openRetry('${task.id}')">${t('retryCard')}</button>` : ''}
|
|
1341
|
+
<button class="card-action-btn edit" onclick="openEditTask('${task.id}')" title="Edit">✎</button>
|
|
1342
|
+
<button class="card-action-btn del" onclick="deleteTask('${task.id}')" title="Delete">✕</button>
|
|
1343
|
+
</div>
|
|
1254
1344
|
</div>`;
|
|
1255
1345
|
}
|
|
1256
1346
|
|
|
1257
|
-
// ──
|
|
1347
|
+
// ── IN-MEMORY PATCH (avoid full reload for minor updates) ─────────────────────
|
|
1348
|
+
function patchTaskInMemory(patch) {
|
|
1349
|
+
if (!board.epics) return;
|
|
1350
|
+
for (const epic of board.epics) {
|
|
1351
|
+
const idx = (epic.cb_tasks || []).findIndex(t => t.id === patch.id);
|
|
1352
|
+
if (idx !== -1) {
|
|
1353
|
+
epic.cb_tasks[idx] = { ...epic.cb_tasks[idx], ...patch };
|
|
1354
|
+
renderKanban();
|
|
1355
|
+
updateStats();
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
// Task not found in memory yet (new orphan etc), do full reload
|
|
1360
|
+
loadBoard();
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// ── DRAG STATE ────────────────────────────────────────────────────────────────
|
|
1364
|
+
// We track order in JS so we don't depend on DOM state during drag
|
|
1365
|
+
let dragColumnOrder = {}; // { status: [id, id, ...] } — snapshot at dragstart
|
|
1258
1366
|
function onDragStart(e, id) {
|
|
1259
1367
|
draggedId = id;
|
|
1368
|
+
|
|
1369
|
+
// Snapshot current order of all columns from memory (reliable — not DOM-dependent)
|
|
1370
|
+
dragColumnOrder = {};
|
|
1371
|
+
for (const status of ['todo', 'in_progress', 'done', 'error']) {
|
|
1372
|
+
dragColumnOrder[status] = allTasks()
|
|
1373
|
+
.filter(t => t.status === status)
|
|
1374
|
+
.sort((a, b) => (a.priority_order ?? 99) - (b.priority_order ?? 99))
|
|
1375
|
+
.map(t => t.id);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1260
1378
|
setTimeout(() => {
|
|
1261
1379
|
const el = document.querySelector(`.card[data-id="${id}"]`);
|
|
1262
1380
|
if (el) el.classList.add('dragging');
|
|
@@ -1317,50 +1435,49 @@ async function onDrop(e, status) {
|
|
|
1317
1435
|
const insertBeforeId = col.dataset.insertBefore || '';
|
|
1318
1436
|
delete col.dataset.insertBefore;
|
|
1319
1437
|
|
|
1320
|
-
//
|
|
1321
|
-
const
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
let
|
|
1438
|
+
// Use the JS snapshot (taken at dragstart) — not DOM which has .dragging gaps
|
|
1439
|
+
const sourceStatus = allTasks().find(t => t.id === draggedId)?.status || status;
|
|
1440
|
+
const columnIds = (dragColumnOrder[status] || []).filter(id => id !== draggedId);
|
|
1441
|
+
|
|
1442
|
+
let newOrderedIds;
|
|
1325
1443
|
if (!insertBeforeId) {
|
|
1326
|
-
|
|
1327
|
-
const maxOrder = tasks.length ? Math.max(...tasks.map(t => t.priority_order || 0)) : 0;
|
|
1328
|
-
newOrder = maxOrder + 1;
|
|
1444
|
+
newOrderedIds = [...columnIds, draggedId];
|
|
1329
1445
|
} else {
|
|
1330
|
-
const
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1446
|
+
const insertIdx = columnIds.indexOf(insertBeforeId);
|
|
1447
|
+
if (insertIdx === -1) {
|
|
1448
|
+
newOrderedIds = [draggedId, ...columnIds];
|
|
1449
|
+
} else {
|
|
1450
|
+
newOrderedIds = [...columnIds.slice(0, insertIdx), draggedId, ...columnIds.slice(insertIdx)];
|
|
1451
|
+
}
|
|
1334
1452
|
}
|
|
1335
1453
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1454
|
+
// Optimistic update in memory so re-render is instant
|
|
1455
|
+
const task = allTasks().find(t => t.id === draggedId);
|
|
1456
|
+
if (task) task.status = status;
|
|
1457
|
+
newOrderedIds.forEach((id, i) => {
|
|
1458
|
+
const t = allTasks().find(t => t.id === id);
|
|
1459
|
+
if (t) t.priority_order = i + 1;
|
|
1340
1460
|
});
|
|
1461
|
+
renderKanban();
|
|
1462
|
+
updateStats();
|
|
1341
1463
|
|
|
1342
|
-
//
|
|
1343
|
-
|
|
1464
|
+
// Update status if column changed
|
|
1465
|
+
if (sourceStatus !== status) {
|
|
1466
|
+
await fetch(`/api/tasks/${draggedId}`, {
|
|
1467
|
+
method: 'PATCH',
|
|
1468
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1469
|
+
body: JSON.stringify({ status }),
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1344
1472
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1473
|
+
// Persist new order
|
|
1474
|
+
await fetch('/api/tasks/reorder', {
|
|
1475
|
+
method: 'POST',
|
|
1476
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1477
|
+
body: JSON.stringify({ taskIds: newOrderedIds }),
|
|
1478
|
+
});
|
|
1348
1479
|
|
|
1349
|
-
|
|
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
|
-
}
|
|
1480
|
+
draggedId = null;
|
|
1364
1481
|
}
|
|
1365
1482
|
|
|
1366
1483
|
// ── CARD SELECT ───────────────────────────────────────────────────────────────
|
|
@@ -1683,6 +1800,48 @@ function initTerminal() {
|
|
|
1683
1800
|
});
|
|
1684
1801
|
}
|
|
1685
1802
|
|
|
1803
|
+
// ── EDIT TASK ─────────────────────────────────────────────────────────────────
|
|
1804
|
+
function openEditTask(id) {
|
|
1805
|
+
const task = allTasks().find(t => t.id === id);
|
|
1806
|
+
if (!task) return;
|
|
1807
|
+
document.getElementById('e-id').value = id;
|
|
1808
|
+
document.getElementById('e-title').value = task.title || '';
|
|
1809
|
+
document.getElementById('e-desc').value = task.description || '';
|
|
1810
|
+
document.getElementById('e-priority').value = task.priority || 'medium';
|
|
1811
|
+
document.getElementById('e-type').value = task.type || 'feature';
|
|
1812
|
+
document.getElementById('editModal').style.display = 'flex';
|
|
1813
|
+
applyTranslations();
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
function closeEditTask() {
|
|
1817
|
+
document.getElementById('editModal').style.display = 'none';
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
async function submitEditTask() {
|
|
1821
|
+
const id = document.getElementById('e-id').value;
|
|
1822
|
+
const title = document.getElementById('e-title').value.trim();
|
|
1823
|
+
const description = document.getElementById('e-desc').value.trim();
|
|
1824
|
+
const priority = document.getElementById('e-priority').value;
|
|
1825
|
+
const type = document.getElementById('e-type').value;
|
|
1826
|
+
if (!title) return;
|
|
1827
|
+
|
|
1828
|
+
const priorityOrder = { high: 1, medium: 2, low: 3 };
|
|
1829
|
+
await fetch(`/api/tasks/${id}`, {
|
|
1830
|
+
method: 'PATCH',
|
|
1831
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1832
|
+
body: JSON.stringify({ title, description, priority, type, priority_order: priorityOrder[priority] }),
|
|
1833
|
+
});
|
|
1834
|
+
closeEditTask();
|
|
1835
|
+
loadBoard();
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// ── DELETE TASK ───────────────────────────────────────────────────────────────
|
|
1839
|
+
async function deleteTask(id) {
|
|
1840
|
+
if (!confirm(currentLang === 'es' ? '¿Eliminar esta tarea?' : 'Delete this task?')) return;
|
|
1841
|
+
await fetch(`/api/tasks/${id}`, { method: 'DELETE' });
|
|
1842
|
+
loadBoard();
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1686
1845
|
// ── UTILS ─────────────────────────────────────────────────────────────────────
|
|
1687
1846
|
function esc(s) {
|
|
1688
1847
|
if (!s) return '';
|
|
@@ -1690,7 +1849,7 @@ function esc(s) {
|
|
|
1690
1849
|
}
|
|
1691
1850
|
|
|
1692
1851
|
document.addEventListener('keydown', e => {
|
|
1693
|
-
if (e.key === 'Escape') { closeModal(); closeRetry(); }
|
|
1852
|
+
if (e.key === 'Escape') { closeModal(); closeRetry(); closeEditTask(); }
|
|
1694
1853
|
if ((e.metaKey || e.ctrlKey) && e.key === 'n') { e.preventDefault(); openModal(); }
|
|
1695
1854
|
});
|
|
1696
1855
|
|
|
@@ -1724,6 +1883,7 @@ const STRINGS = {
|
|
|
1724
1883
|
expoIdle:'Expo not started. Click "Start Expo" to install dependencies and launch with tunnel.',
|
|
1725
1884
|
scanWith:'SCAN WITH EXPO GO',
|
|
1726
1885
|
retryCard:'↩ Retry',
|
|
1886
|
+
editTaskTitle:'Edit Task', saveTask:'Save Changes', deleteConfirm:'Delete this task?',
|
|
1727
1887
|
},
|
|
1728
1888
|
es: {
|
|
1729
1889
|
todo:'pendiente', running:'en curso', done:'listo', failed:'fallido',
|
|
@@ -1753,6 +1913,7 @@ const STRINGS = {
|
|
|
1753
1913
|
expoIdle:'Expo no iniciado. Hacé clic en "Iniciar Expo" para instalar dependencias y lanzar con tunnel.',
|
|
1754
1914
|
scanWith:'ESCANEAR CON EXPO GO',
|
|
1755
1915
|
retryCard:'↩ Reintentar',
|
|
1916
|
+
editTaskTitle:'Editar tarea', saveTask:'Guardar cambios', deleteConfirm:'¿Eliminar esta tarea?',
|
|
1756
1917
|
}
|
|
1757
1918
|
};
|
|
1758
1919
|
|
|
@@ -1790,8 +1951,9 @@ function applyTranslations() {
|
|
|
1790
1951
|
|
|
1791
1952
|
// ── INIT ──────────────────────────────────────────────────────────────────────
|
|
1792
1953
|
loadBoard();
|
|
1793
|
-
setInterval(loadBoard, 8000);
|
|
1794
1954
|
connectWS();
|
|
1955
|
+
// Only poll when WS is disconnected (fallback)
|
|
1956
|
+
let pollInterval = null;
|
|
1795
1957
|
|
|
1796
1958
|
// Load expo status
|
|
1797
1959
|
fetch('/api/expo/status').then(r => r.json()).then(d => setExpoStatus(d.status, d.url));
|
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");
|