@cccarv82/freya 1.0.26 → 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 +93 -3
  2. package/cli/web.js +138 -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…');
@@ -545,9 +618,25 @@
545
618
  setOut(header + (r.plan || ''));
546
619
  ta.value = '';
547
620
 
548
- if (r.ok === false) setPill('err', 'planner off');
549
- else setPill('ok', 'planned');
550
- setTimeout(() => setPill('ok', 'idle'), 800);
621
+ if (r.ok === false) {
622
+ setPill('err', 'planner off');
623
+ setTimeout(() => setPill('ok', 'idle'), 800);
624
+ return;
625
+ }
626
+
627
+ if (state.autoApply) {
628
+ setPill('run', 'applying…');
629
+ await applyPlan();
630
+ const a = state.lastApplied || {};
631
+ setPill('ok', `applied (${a.tasks || 0}t, ${a.blockers || 0}b)`);
632
+ if (state.autoRunReports) {
633
+ await runSuggestedReports();
634
+ }
635
+ } else {
636
+ setPill('ok', 'planned');
637
+ }
638
+
639
+ setTimeout(() => setPill('ok', 'idle'), 1200);
551
640
  } catch (e) {
552
641
  setPill('err', 'plan failed');
553
642
  }
@@ -664,6 +753,7 @@
664
753
  window.publish = publish;
665
754
  window.saveSettings = saveSettings;
666
755
  window.refreshReports = refreshReports;
756
+ window.refreshToday = refreshToday;
667
757
  window.renderReportsList = renderReportsList;
668
758
  window.copyOut = copyOut;
669
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>
@@ -1279,6 +1305,19 @@ async function cmdWeb({ port, dir, open, dev }) {
1279
1305
  }
1280
1306
  }
1281
1307
 
1308
+ // Auto-suggest reports when planner didn't include any
1309
+ // (keeps UX consistent: if you created a blocker, at least suggest blockers)
1310
+ if (!applied.reportsSuggested.length) {
1311
+ const sug = [];
1312
+ sug.push('daily');
1313
+ if (applied.blockers > 0) sug.push('blockers');
1314
+ if ((applied.tasks > 0 || applied.blockers > 0) && applyMode !== 'blockers') sug.push('status');
1315
+ applied.reportsSuggested = Array.from(new Set(sug));
1316
+ } else {
1317
+ // Dedup
1318
+ applied.reportsSuggested = Array.from(new Set(applied.reportsSuggested.map((s) => String(s).trim()).filter(Boolean)));
1319
+ }
1320
+
1282
1321
  // Persist
1283
1322
  writeJson(taskFile, taskLog);
1284
1323
  writeJson(blockerFile, blockerLog);
@@ -1338,6 +1377,93 @@ async function cmdWeb({ port, dir, open, dev }) {
1338
1377
  return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { output } : { error: output || 'migrate failed', output });
1339
1378
  }
1340
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
+ }
1341
1467
  if (req.url === '/api/report') {
1342
1468
  const script = payload.script;
1343
1469
  if (!script) return safeJson(res, 400, { error: 'Missing script' });
@@ -1402,12 +1528,21 @@ async function cmdWeb({ port, dir, open, dev }) {
1402
1528
 
1403
1529
  const url = `http://${host}:${port}/`;
1404
1530
 
1405
- // 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
1406
1535
  if (dev) {
1407
1536
  const target = dir ? path.resolve(process.cwd(), dir) : path.join(process.cwd(), 'freya');
1408
1537
  try {
1409
- seedDevWorkspace(target);
1410
- 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
+ }
1411
1546
  } catch (e) {
1412
1547
  process.stdout.write(`Dev seed failed: ${e.message || String(e)}\n`);
1413
1548
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "1.0.26",
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",