@cccarv82/freya 1.0.27 → 1.0.29

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 +137 -9
  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>
@@ -974,8 +1000,8 @@ async function cmdWeb({ port, dir, open, dev }) {
974
1000
  const schema = {
975
1001
  actions: [
976
1002
  { type: 'append_daily_log', text: '<string>' },
977
- { type: 'create_task', description: '<string>', priority: 'HIGH|MEDIUM|LOW', category: 'DO_NOW|SCHEDULE|DELEGATE|IGNORE' },
978
- { type: 'create_blocker', title: '<string>', severity: 'CRITICAL|HIGH|MEDIUM|LOW', notes: '<string>' },
1003
+ { type: 'create_task', description: '<string>', priority: 'HIGH|MEDIUM|LOW', category: 'DO_NOW|SCHEDULE|DELEGATE|IGNORE', projectSlug: '<string optional>' },
1004
+ { type: 'create_blocker', title: '<string>', severity: 'CRITICAL|HIGH|MEDIUM|LOW', notes: '<string>', projectSlug: '<string optional>' },
979
1005
  { type: 'suggest_report', name: 'daily|status|sm-weekly|blockers' },
980
1006
  { type: 'oracle_query', query: '<string>' }
981
1007
  ]
@@ -1056,7 +1082,8 @@ async function cmdWeb({ port, dir, open, dev }) {
1056
1082
  const priorityRaw = String(a.priority || '').trim().toLowerCase();
1057
1083
  const priority = (priorityRaw === 'high' || priorityRaw === 'medium' || priorityRaw === 'low') ? priorityRaw : undefined;
1058
1084
  if (!description) { preview.errors.push('Task missing description'); continue; }
1059
- preview.tasks.push({ description, category: validTaskCats.has(category) ? category : 'DO_NOW', priority });
1085
+ const projectSlug = String(a.projectSlug || '').trim();
1086
+ preview.tasks.push({ description, category: validTaskCats.has(category) ? category : 'DO_NOW', priority, projectSlug: projectSlug || undefined });
1060
1087
  continue;
1061
1088
  }
1062
1089
 
@@ -1072,7 +1099,8 @@ async function cmdWeb({ port, dir, open, dev }) {
1072
1099
  else severity = 'MEDIUM';
1073
1100
  }
1074
1101
  if (!title) { preview.errors.push('Blocker missing title'); continue; }
1075
- preview.blockers.push({ title, notes, severity });
1102
+ const projectSlug = String(a.projectSlug || '').trim();
1103
+ preview.blockers.push({ title, notes, severity, projectSlug: projectSlug || undefined });
1076
1104
  continue;
1077
1105
  }
1078
1106
 
@@ -1228,7 +1256,8 @@ async function cmdWeb({ port, dir, open, dev }) {
1228
1256
  if (applyMode !== 'all' && applyMode !== 'tasks') continue;
1229
1257
  const description = normalizeWhitespace(a.description);
1230
1258
  if (!description) continue;
1231
- const key = sha1(normalizeTextForKey(description));
1259
+ const projectSlug = String(a.projectSlug || '').trim();
1260
+ const key = sha1(normalizeTextForKey((projectSlug ? projectSlug + ' ' : '') + description));
1232
1261
  if (existingTaskKeys24h.has(key)) { applied.tasksSkipped++; continue; }
1233
1262
  const category = validTaskCats.has(String(a.category || '').trim()) ? String(a.category).trim() : 'DO_NOW';
1234
1263
  const priority = normPriority(a.priority);
@@ -1239,6 +1268,7 @@ async function cmdWeb({ port, dir, open, dev }) {
1239
1268
  status: 'PENDING',
1240
1269
  createdAt: now,
1241
1270
  };
1271
+ if (projectSlug) task.projectSlug = projectSlug;
1242
1272
  if (priority) task.priority = priority;
1243
1273
  taskLog.tasks.push(task);
1244
1274
  applied.tasks++;
@@ -1248,7 +1278,8 @@ async function cmdWeb({ port, dir, open, dev }) {
1248
1278
  if (type === 'create_blocker') {
1249
1279
  if (applyMode !== 'all' && applyMode !== 'blockers') continue;
1250
1280
  const title = normalizeWhitespace(a.title);
1251
- const key = sha1(normalizeTextForKey(title));
1281
+ const projectSlug = String(a.projectSlug || '').trim();
1282
+ const key = sha1(normalizeTextForKey((projectSlug ? projectSlug + ' ' : '') + title));
1252
1283
  if (existingBlockerKeys24h.has(key)) { applied.blockersSkipped++; continue; }
1253
1284
  const notes = normalizeWhitespace(a.notes);
1254
1285
  if (!title) continue;
@@ -1261,6 +1292,7 @@ async function cmdWeb({ port, dir, open, dev }) {
1261
1292
  status: 'OPEN',
1262
1293
  severity,
1263
1294
  };
1295
+ if (projectSlug) blocker.projectSlug = projectSlug;
1264
1296
  blockerLog.blockers.push(blocker);
1265
1297
  applied.blockers++;
1266
1298
  continue;
@@ -1351,6 +1383,93 @@ async function cmdWeb({ port, dir, open, dev }) {
1351
1383
  return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { output } : { error: output || 'migrate failed', output });
1352
1384
  }
1353
1385
 
1386
+
1387
+ if (req.url === '/api/tasks/list') {
1388
+ const limit = Math.max(1, Math.min(50, Number(payload.limit || 10)));
1389
+ const cat = payload.category ? String(payload.category).trim() : null;
1390
+ const status = payload.status ? String(payload.status).trim() : null;
1391
+
1392
+ const file = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
1393
+ const doc = readJsonOrNull(file) || { schemaVersion: 1, tasks: [] };
1394
+ const tasks = Array.isArray(doc.tasks) ? doc.tasks.slice() : [];
1395
+
1396
+ const filtered = tasks
1397
+ .filter((t) => {
1398
+ if (!t || typeof t !== 'object') return false;
1399
+ if (cat && String(t.category || '').trim() !== cat) return false;
1400
+ if (status && String(t.status || '').trim() !== status) return false;
1401
+ return true;
1402
+ })
1403
+ .sort((a, b) => {
1404
+ const pa = String(a.priority || '').toLowerCase();
1405
+ const pb = String(b.priority || '').toLowerCase();
1406
+ const rank = (p) => (p === 'high' ? 0 : p === 'medium' ? 1 : p === 'low' ? 2 : 3);
1407
+ const ra = rank(pa), rb = rank(pb);
1408
+ if (ra !== rb) return ra - rb;
1409
+ return String(b.createdAt || '').localeCompare(String(a.createdAt || ''));
1410
+ })
1411
+ .slice(0, limit);
1412
+
1413
+ return safeJson(res, 200, { ok: true, tasks: filtered });
1414
+ }
1415
+
1416
+ if (req.url === '/api/tasks/complete') {
1417
+ const id = String(payload.id || '').trim();
1418
+ if (!id) return safeJson(res, 400, { error: 'Missing id' });
1419
+
1420
+ const file = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
1421
+ const doc = readJsonOrNull(file) || { schemaVersion: 1, tasks: [] };
1422
+ const tasks = Array.isArray(doc.tasks) ? doc.tasks : [];
1423
+
1424
+ const now = isoNow();
1425
+ let updated = null;
1426
+ for (const t of tasks) {
1427
+ if (t && t.id === id) {
1428
+ t.status = 'COMPLETED';
1429
+ t.completedAt = now;
1430
+ updated = t;
1431
+ break;
1432
+ }
1433
+ }
1434
+
1435
+ if (!updated) return safeJson(res, 404, { error: 'Task not found' });
1436
+ writeJson(file, doc);
1437
+ return safeJson(res, 200, { ok: true, task: updated });
1438
+ }
1439
+
1440
+ if (req.url === '/api/blockers/list') {
1441
+ const limit = Math.max(1, Math.min(50, Number(payload.limit || 10)));
1442
+ const status = payload.status ? String(payload.status).trim() : 'OPEN';
1443
+
1444
+ const file = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
1445
+ const doc = readJsonOrNull(file) || { schemaVersion: 1, blockers: [] };
1446
+ const blockers = Array.isArray(doc.blockers) ? doc.blockers.slice() : [];
1447
+
1448
+ const sevRank = (s) => {
1449
+ const v = String(s || '').toUpperCase();
1450
+ if (v === 'CRITICAL') return 0;
1451
+ if (v === 'HIGH') return 1;
1452
+ if (v === 'MEDIUM') return 2;
1453
+ if (v === 'LOW') return 3;
1454
+ return 9;
1455
+ };
1456
+
1457
+ const filtered = blockers
1458
+ .filter((b) => {
1459
+ if (!b || typeof b !== 'object') return false;
1460
+ if (status && String(b.status || '').trim() !== status) return false;
1461
+ return true;
1462
+ })
1463
+ .sort((a, b) => {
1464
+ const ra = sevRank(a.severity);
1465
+ const rb = sevRank(b.severity);
1466
+ if (ra !== rb) return ra - rb;
1467
+ return String(a.createdAt || '').localeCompare(String(b.createdAt || ''));
1468
+ })
1469
+ .slice(0, limit);
1470
+
1471
+ return safeJson(res, 200, { ok: true, blockers: filtered });
1472
+ }
1354
1473
  if (req.url === '/api/report') {
1355
1474
  const script = payload.script;
1356
1475
  if (!script) return safeJson(res, 400, { error: 'Missing script' });
@@ -1415,12 +1534,21 @@ async function cmdWeb({ port, dir, open, dev }) {
1415
1534
 
1416
1535
  const url = `http://${host}:${port}/`;
1417
1536
 
1418
- // Optional dev seed (safe: only creates files if missing)
1537
+ // Optional dev seed
1538
+ // Safety rules:
1539
+ // - only seed when workspace is empty OR already initialized as a Freya workspace
1540
+ // - never overwrite non-dev user content
1419
1541
  if (dev) {
1420
1542
  const target = dir ? path.resolve(process.cwd(), dir) : path.join(process.cwd(), 'freya');
1421
1543
  try {
1422
- seedDevWorkspace(target);
1423
- process.stdout.write(`Dev seed: created demo files in ${target}\n`);
1544
+ const targetOk = looksLikeFreyaWorkspace(target);
1545
+ const empty = looksEmptyWorkspace(target);
1546
+ if (!targetOk && !empty) {
1547
+ process.stdout.write(`Dev seed: skipped (workspace not empty and not initialized) -> ${target}\n`);
1548
+ } else {
1549
+ seedDevWorkspace(target);
1550
+ process.stdout.write(`Dev seed: created demo files in ${target}\n`);
1551
+ }
1424
1552
  } catch (e) {
1425
1553
  process.stdout.write(`Dev seed failed: ${e.message || String(e)}\n`);
1426
1554
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "1.0.27",
3
+ "version": "1.0.29",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js",