@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.
- package/cli/web-ui.js +93 -3
- package/cli/web.js +138 -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)
|
|
549
|
-
|
|
550
|
-
|
|
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
|
|
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
|
-
|
|
1410
|
-
|
|
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
|
}
|