@cccarv82/freya 1.0.27 → 1.0.28

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.
Files changed (3) hide show
  1. package/cli/web-ui.js +74 -0
  2. package/cli/web.js +125 -3
  3. package/package.json +1 -1
package/cli/web-ui.js CHANGED
@@ -337,6 +337,79 @@
337
337
  }
338
338
  }
339
339
 
340
+ function renderTasks(list) {
341
+ const el = $('tasksList');
342
+ if (!el) return;
343
+ el.innerHTML = '';
344
+ for (const t of list || []) {
345
+ const row = document.createElement('div');
346
+ row.className = 'rep';
347
+ const pri = (t.priority || '').toUpperCase();
348
+ row.innerHTML = '<div style="display:flex; justify-content:space-between; gap:10px; align-items:center">'
349
+ + '<div style="min-width:0"><div style="font-weight:700">' + escapeHtml(t.description || '') + '</div>'
350
+ + '<div style="opacity:.7; font-size:11px; margin-top:4px">' + escapeHtml(String(t.category || '')) + (pri ? (' · ' + escapeHtml(pri)) : '') + '</div></div>'
351
+ + '<button class="btn small" type="button">Complete</button>'
352
+ + '</div>';
353
+ const btn = row.querySelector('button');
354
+ btn.onclick = async () => {
355
+ try {
356
+ setPill('run', 'completing…');
357
+ await api('/api/tasks/complete', { dir: dirOrDefault(), id: t.id });
358
+ await refreshToday();
359
+ setPill('ok', 'completed');
360
+ setTimeout(() => setPill('ok', 'idle'), 800);
361
+ } catch (e) {
362
+ setPill('err', 'complete failed');
363
+ setOut(String(e && e.message ? e.message : e));
364
+ }
365
+ };
366
+ el.appendChild(row);
367
+ }
368
+ if (!el.childElementCount) {
369
+ const empty = document.createElement('div');
370
+ empty.className = 'help';
371
+ empty.textContent = 'No DO_NOW tasks.';
372
+ el.appendChild(empty);
373
+ }
374
+ }
375
+
376
+ function renderBlockers(list) {
377
+ const el = $('blockersList');
378
+ if (!el) return;
379
+ el.innerHTML = '';
380
+ for (const b of list || []) {
381
+ const row = document.createElement('div');
382
+ row.className = 'rep';
383
+ const sev = String(b.severity || '').toUpperCase();
384
+ row.innerHTML = '<div style="display:flex; justify-content:space-between; gap:10px; align-items:center">'
385
+ + '<div style="min-width:0"><div style="font-weight:800">' + escapeHtml(sev) + '</div>'
386
+ + '<div style="margin-top:4px">' + escapeHtml(b.title || '') + '</div>'
387
+ + '</div>'
388
+ + '<div style="opacity:.7; font-size:11px; white-space:nowrap">' + escapeHtml(fmtWhen(new Date(b.createdAt || Date.now()).getTime())) + '</div>'
389
+ + '</div>';
390
+ el.appendChild(row);
391
+ }
392
+ if (!el.childElementCount) {
393
+ const empty = document.createElement('div');
394
+ empty.className = 'help';
395
+ empty.textContent = 'No OPEN blockers.';
396
+ el.appendChild(empty);
397
+ }
398
+ }
399
+
400
+ async function refreshToday() {
401
+ try {
402
+ const [t, b] = await Promise.all([
403
+ api('/api/tasks/list', { dir: dirOrDefault(), category: 'DO_NOW', status: 'PENDING', limit: 5 }),
404
+ api('/api/blockers/list', { dir: dirOrDefault(), status: 'OPEN', limit: 5 })
405
+ ]);
406
+ renderTasks((t && t.tasks) || []);
407
+ renderBlockers((b && b.blockers) || []);
408
+ } catch (e) {
409
+ // keep silent in background refresh
410
+ }
411
+ }
412
+
340
413
  async function pickDir() {
341
414
  try {
342
415
  setPill('run', 'picker…');
@@ -680,6 +753,7 @@
680
753
  window.publish = publish;
681
754
  window.saveSettings = saveSettings;
682
755
  window.refreshReports = refreshReports;
756
+ window.refreshToday = refreshToday;
683
757
  window.renderReportsList = renderReportsList;
684
758
  window.copyOut = copyOut;
685
759
  window.copyPath = copyPath;
package/cli/web.js CHANGED
@@ -368,6 +368,16 @@ function safeJson(res, code, obj) {
368
368
  res.end(body);
369
369
  }
370
370
 
371
+ function looksEmptyWorkspace(dir) {
372
+ try {
373
+ if (!exists(dir)) return true;
374
+ const entries = fs.readdirSync(dir).filter((n) => !['.debuglogs', '.DS_Store'].includes(n));
375
+ return entries.length === 0;
376
+ } catch {
377
+ return true;
378
+ }
379
+ }
380
+
371
381
  function looksLikeFreyaWorkspace(dir) {
372
382
  // minimal check: has scripts/validate-data.js and data/
373
383
  return (
@@ -638,6 +648,22 @@ function buildHtml(safeDefault) {
638
648
  </div>
639
649
  </div>
640
650
 
651
+ <div class="panel">
652
+ <div class="panelHead">
653
+ <b>Today</b>
654
+ <div class="stack">
655
+ <button class="btn small" onclick="refreshToday()">Refresh</button>
656
+ </div>
657
+ </div>
658
+ <div class="panelBody">
659
+ <div class="small" style="margin-bottom:8px; opacity:.8">Do Now</div>
660
+ <div id="tasksList" style="display:grid; gap:8px"></div>
661
+ <div style="height:12px"></div>
662
+ <div class="small" style="margin-bottom:8px; opacity:.8">Open blockers</div>
663
+ <div id="blockersList" style="display:grid; gap:8px"></div>
664
+ </div>
665
+ </div>
666
+
641
667
  <div class="panel">
642
668
  <div class="panelHead">
643
669
  <b>Preview</b>
@@ -1351,6 +1377,93 @@ async function cmdWeb({ port, dir, open, dev }) {
1351
1377
  return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { output } : { error: output || 'migrate failed', output });
1352
1378
  }
1353
1379
 
1380
+
1381
+ if (req.url === '/api/tasks/list') {
1382
+ const limit = Math.max(1, Math.min(50, Number(payload.limit || 10)));
1383
+ const cat = payload.category ? String(payload.category).trim() : null;
1384
+ const status = payload.status ? String(payload.status).trim() : null;
1385
+
1386
+ const file = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
1387
+ const doc = readJsonOrNull(file) || { schemaVersion: 1, tasks: [] };
1388
+ const tasks = Array.isArray(doc.tasks) ? doc.tasks.slice() : [];
1389
+
1390
+ const filtered = tasks
1391
+ .filter((t) => {
1392
+ if (!t || typeof t !== 'object') return false;
1393
+ if (cat && String(t.category || '').trim() !== cat) return false;
1394
+ if (status && String(t.status || '').trim() !== status) return false;
1395
+ return true;
1396
+ })
1397
+ .sort((a, b) => {
1398
+ const pa = String(a.priority || '').toLowerCase();
1399
+ const pb = String(b.priority || '').toLowerCase();
1400
+ const rank = (p) => (p === 'high' ? 0 : p === 'medium' ? 1 : p === 'low' ? 2 : 3);
1401
+ const ra = rank(pa), rb = rank(pb);
1402
+ if (ra !== rb) return ra - rb;
1403
+ return String(b.createdAt || '').localeCompare(String(a.createdAt || ''));
1404
+ })
1405
+ .slice(0, limit);
1406
+
1407
+ return safeJson(res, 200, { ok: true, tasks: filtered });
1408
+ }
1409
+
1410
+ if (req.url === '/api/tasks/complete') {
1411
+ const id = String(payload.id || '').trim();
1412
+ if (!id) return safeJson(res, 400, { error: 'Missing id' });
1413
+
1414
+ const file = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
1415
+ const doc = readJsonOrNull(file) || { schemaVersion: 1, tasks: [] };
1416
+ const tasks = Array.isArray(doc.tasks) ? doc.tasks : [];
1417
+
1418
+ const now = isoNow();
1419
+ let updated = null;
1420
+ for (const t of tasks) {
1421
+ if (t && t.id === id) {
1422
+ t.status = 'COMPLETED';
1423
+ t.completedAt = now;
1424
+ updated = t;
1425
+ break;
1426
+ }
1427
+ }
1428
+
1429
+ if (!updated) return safeJson(res, 404, { error: 'Task not found' });
1430
+ writeJson(file, doc);
1431
+ return safeJson(res, 200, { ok: true, task: updated });
1432
+ }
1433
+
1434
+ if (req.url === '/api/blockers/list') {
1435
+ const limit = Math.max(1, Math.min(50, Number(payload.limit || 10)));
1436
+ const status = payload.status ? String(payload.status).trim() : 'OPEN';
1437
+
1438
+ const file = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
1439
+ const doc = readJsonOrNull(file) || { schemaVersion: 1, blockers: [] };
1440
+ const blockers = Array.isArray(doc.blockers) ? doc.blockers.slice() : [];
1441
+
1442
+ const sevRank = (s) => {
1443
+ const v = String(s || '').toUpperCase();
1444
+ if (v === 'CRITICAL') return 0;
1445
+ if (v === 'HIGH') return 1;
1446
+ if (v === 'MEDIUM') return 2;
1447
+ if (v === 'LOW') return 3;
1448
+ return 9;
1449
+ };
1450
+
1451
+ const filtered = blockers
1452
+ .filter((b) => {
1453
+ if (!b || typeof b !== 'object') return false;
1454
+ if (status && String(b.status || '').trim() !== status) return false;
1455
+ return true;
1456
+ })
1457
+ .sort((a, b) => {
1458
+ const ra = sevRank(a.severity);
1459
+ const rb = sevRank(b.severity);
1460
+ if (ra !== rb) return ra - rb;
1461
+ return String(a.createdAt || '').localeCompare(String(b.createdAt || ''));
1462
+ })
1463
+ .slice(0, limit);
1464
+
1465
+ return safeJson(res, 200, { ok: true, blockers: filtered });
1466
+ }
1354
1467
  if (req.url === '/api/report') {
1355
1468
  const script = payload.script;
1356
1469
  if (!script) return safeJson(res, 400, { error: 'Missing script' });
@@ -1415,12 +1528,21 @@ async function cmdWeb({ port, dir, open, dev }) {
1415
1528
 
1416
1529
  const url = `http://${host}:${port}/`;
1417
1530
 
1418
- // Optional dev seed (safe: only creates files if missing)
1531
+ // Optional dev seed
1532
+ // Safety rules:
1533
+ // - only seed when workspace is empty OR already initialized as a Freya workspace
1534
+ // - never overwrite non-dev user content
1419
1535
  if (dev) {
1420
1536
  const target = dir ? path.resolve(process.cwd(), dir) : path.join(process.cwd(), 'freya');
1421
1537
  try {
1422
- seedDevWorkspace(target);
1423
- process.stdout.write(`Dev seed: created demo files in ${target}\n`);
1538
+ const targetOk = looksLikeFreyaWorkspace(target);
1539
+ const empty = looksEmptyWorkspace(target);
1540
+ if (!targetOk && !empty) {
1541
+ process.stdout.write(`Dev seed: skipped (workspace not empty and not initialized) -> ${target}\n`);
1542
+ } else {
1543
+ seedDevWorkspace(target);
1544
+ process.stdout.write(`Dev seed: created demo files in ${target}\n`);
1545
+ }
1424
1546
  } catch (e) {
1425
1547
  process.stdout.write(`Dev seed failed: ${e.message || String(e)}\n`);
1426
1548
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "1.0.27",
3
+ "version": "1.0.28",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js",