@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.
- package/cli/web-ui.js +74 -0
- package/cli/web.js +125 -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
|
|
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
|
-
|
|
1423
|
-
|
|
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
|
}
|