claudeboard 2.12.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 +69 -18
- 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
|
@@ -1201,13 +1201,23 @@ function connectWS() {
|
|
|
1201
1201
|
ws.onclose = () => { setWS(false); setTimeout(connectWS, 2000); };
|
|
1202
1202
|
ws.onmessage = (e) => {
|
|
1203
1203
|
const { event, data } = JSON.parse(e.data);
|
|
1204
|
-
|
|
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)) {
|
|
1205
1207
|
loadBoard();
|
|
1206
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
|
+
}
|
|
1207
1218
|
if (event === 'log') {
|
|
1208
1219
|
board.logs.unshift(data);
|
|
1209
1220
|
if (activeTab === 'activity') renderLogs();
|
|
1210
|
-
// If on detail tab, don't touch activityPane — it'll refresh next time they switch
|
|
1211
1221
|
}
|
|
1212
1222
|
if (event === 'expo_status') {
|
|
1213
1223
|
setExpoStatus(data.status, data.url);
|
|
@@ -1221,6 +1231,12 @@ function connectWS() {
|
|
|
1221
1231
|
function setWS(on) {
|
|
1222
1232
|
document.getElementById('wsDot').className = 'ws-dot' + (on ? ' on' : '');
|
|
1223
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
|
+
}
|
|
1224
1240
|
}
|
|
1225
1241
|
|
|
1226
1242
|
// ── DATA ─────────────────────────────────────────────────────────────────────
|
|
@@ -1328,9 +1344,37 @@ function cardHTML(task, position = null) {
|
|
|
1328
1344
|
</div>`;
|
|
1329
1345
|
}
|
|
1330
1346
|
|
|
1331
|
-
// ──
|
|
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
|
|
1332
1366
|
function onDragStart(e, id) {
|
|
1333
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
|
+
|
|
1334
1378
|
setTimeout(() => {
|
|
1335
1379
|
const el = document.querySelector(`.card[data-id="${id}"]`);
|
|
1336
1380
|
if (el) el.classList.add('dragging');
|
|
@@ -1391,27 +1435,34 @@ async function onDrop(e, status) {
|
|
|
1391
1435
|
const insertBeforeId = col.dataset.insertBefore || '';
|
|
1392
1436
|
delete col.dataset.insertBefore;
|
|
1393
1437
|
|
|
1394
|
-
//
|
|
1395
|
-
|
|
1396
|
-
const
|
|
1397
|
-
.map(c => c.dataset.id)
|
|
1398
|
-
.filter(id => id && id !== draggedId);
|
|
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);
|
|
1399
1441
|
|
|
1400
1442
|
let newOrderedIds;
|
|
1401
1443
|
if (!insertBeforeId) {
|
|
1402
|
-
newOrderedIds = [...
|
|
1444
|
+
newOrderedIds = [...columnIds, draggedId];
|
|
1403
1445
|
} else {
|
|
1404
|
-
const insertIdx =
|
|
1446
|
+
const insertIdx = columnIds.indexOf(insertBeforeId);
|
|
1405
1447
|
if (insertIdx === -1) {
|
|
1406
|
-
newOrderedIds = [draggedId, ...
|
|
1448
|
+
newOrderedIds = [draggedId, ...columnIds];
|
|
1407
1449
|
} else {
|
|
1408
|
-
newOrderedIds = [...
|
|
1450
|
+
newOrderedIds = [...columnIds.slice(0, insertIdx), draggedId, ...columnIds.slice(insertIdx)];
|
|
1409
1451
|
}
|
|
1410
1452
|
}
|
|
1411
1453
|
|
|
1412
|
-
//
|
|
1413
|
-
const
|
|
1414
|
-
if (
|
|
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;
|
|
1460
|
+
});
|
|
1461
|
+
renderKanban();
|
|
1462
|
+
updateStats();
|
|
1463
|
+
|
|
1464
|
+
// Update status if column changed
|
|
1465
|
+
if (sourceStatus !== status) {
|
|
1415
1466
|
await fetch(`/api/tasks/${draggedId}`, {
|
|
1416
1467
|
method: 'PATCH',
|
|
1417
1468
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1419,7 +1470,7 @@ async function onDrop(e, status) {
|
|
|
1419
1470
|
});
|
|
1420
1471
|
}
|
|
1421
1472
|
|
|
1422
|
-
// Persist
|
|
1473
|
+
// Persist new order
|
|
1423
1474
|
await fetch('/api/tasks/reorder', {
|
|
1424
1475
|
method: 'POST',
|
|
1425
1476
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1427,7 +1478,6 @@ async function onDrop(e, status) {
|
|
|
1427
1478
|
});
|
|
1428
1479
|
|
|
1429
1480
|
draggedId = null;
|
|
1430
|
-
loadBoard();
|
|
1431
1481
|
}
|
|
1432
1482
|
|
|
1433
1483
|
// ── CARD SELECT ───────────────────────────────────────────────────────────────
|
|
@@ -1901,8 +1951,9 @@ function applyTranslations() {
|
|
|
1901
1951
|
|
|
1902
1952
|
// ── INIT ──────────────────────────────────────────────────────────────────────
|
|
1903
1953
|
loadBoard();
|
|
1904
|
-
setInterval(loadBoard, 8000);
|
|
1905
1954
|
connectWS();
|
|
1955
|
+
// Only poll when WS is disconnected (fallback)
|
|
1956
|
+
let pollInterval = null;
|
|
1906
1957
|
|
|
1907
1958
|
// Load expo status
|
|
1908
1959
|
fetch('/api/expo/status').then(r => r.json()).then(d => setExpoStatus(d.status, d.url));
|