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