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.
@@ -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" || await isSimulatorAvailable();
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 = spawn("npx", expoArgs, {
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
- async function isSimulatorAvailable() {
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; }
@@ -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
- if (['task_update','task_added','task_started','task_complete','task_failed','task_deleted','task_reordered'].includes(event)) {
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
- // ── DRAG & DROP ───────────────────────────────────────────────────────────────
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
- // 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);
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 = [...otherIds, draggedId]; // dropped at end
1444
+ newOrderedIds = [...columnIds, draggedId];
1403
1445
  } else {
1404
- const insertIdx = otherIds.indexOf(insertBeforeId);
1446
+ const insertIdx = columnIds.indexOf(insertBeforeId);
1405
1447
  if (insertIdx === -1) {
1406
- newOrderedIds = [draggedId, ...otherIds];
1448
+ newOrderedIds = [draggedId, ...columnIds];
1407
1449
  } else {
1408
- newOrderedIds = [...otherIds.slice(0, insertIdx), draggedId, ...otherIds.slice(insertIdx)];
1450
+ newOrderedIds = [...columnIds.slice(0, insertIdx), draggedId, ...columnIds.slice(insertIdx)];
1409
1451
  }
1410
1452
  }
1411
1453
 
1412
- // Update status first if moving to different column
1413
- const draggedTask = allTasks().find(t => t.id === draggedId);
1414
- if (draggedTask?.status !== status) {
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 the new order in one call
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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeboard",
3
- "version": "2.12.0",
3
+ "version": "2.13.0",
4
4
  "description": "AI engineering team — from PRD to working mobile app, autonomously",
5
5
  "type": "module",
6
6
  "bin": {