@cccarv82/freya 2.3.10 → 2.3.11
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/vis-network.min.js +34 -0
- package/cli/web-ui.js +102 -37
- package/cli/web.js +319 -26
- package/package.json +1 -1
package/cli/web.js
CHANGED
|
@@ -467,7 +467,7 @@ function redactSecrets(text) {
|
|
|
467
467
|
|
|
468
468
|
function debugLogPath(workspaceDir) {
|
|
469
469
|
const dir = path.join(workspaceDir, '.debuglogs');
|
|
470
|
-
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
470
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch { }
|
|
471
471
|
return path.join(dir, 'debug.jsonl');
|
|
472
472
|
}
|
|
473
473
|
|
|
@@ -511,7 +511,7 @@ function rotateDebugLog(filePath, maxBytes = 5 * 1024 * 1024) {
|
|
|
511
511
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
512
512
|
const rotated = path.join(dir, 'debug-' + stamp + '.jsonl');
|
|
513
513
|
fs.renameSync(filePath, rotated);
|
|
514
|
-
} catch {}
|
|
514
|
+
} catch { }
|
|
515
515
|
}
|
|
516
516
|
|
|
517
517
|
function writeDebugEvent(workspaceDir, event) {
|
|
@@ -521,7 +521,7 @@ function writeDebugEvent(workspaceDir, event) {
|
|
|
521
521
|
rotateDebugLog(filePath);
|
|
522
522
|
const line = JSON.stringify({ ts: new Date().toISOString(), ...event });
|
|
523
523
|
fs.appendFileSync(filePath, line + '\n', 'utf8');
|
|
524
|
-
} catch {}
|
|
524
|
+
} catch { }
|
|
525
525
|
}
|
|
526
526
|
|
|
527
527
|
async function publishRobust(webhookUrl, text, opts = {}) {
|
|
@@ -593,7 +593,7 @@ function safeJson(res, code, obj) {
|
|
|
593
593
|
error
|
|
594
594
|
});
|
|
595
595
|
}
|
|
596
|
-
} catch {}
|
|
596
|
+
} catch { }
|
|
597
597
|
|
|
598
598
|
res.writeHead(code, {
|
|
599
599
|
'Content-Type': 'application/json; charset=utf-8',
|
|
@@ -966,8 +966,9 @@ function buildHtml(safeDefault, appVersion) {
|
|
|
966
966
|
<button class="railBtn active" id="railDashboard" type="button" title="Dashboard">D</button>
|
|
967
967
|
<button class="railBtn" id="railReports" type="button" title="Relatórios">R</button>
|
|
968
968
|
<button class="railBtn" id="railCompanion" type="button" title="Companion">C</button>
|
|
969
|
-
|
|
970
|
-
|
|
969
|
+
<button class="railBtn" id="railProjects" type="button" title="Projects">P</button>
|
|
970
|
+
<button class="railBtn" id="railTimeline" type="button" title="Timeline">T</button>
|
|
971
|
+
<button class="railBtn" id="railGraph" type="button" title="Grafo">G</button>
|
|
971
972
|
</div>
|
|
972
973
|
<div class="railBottom">
|
|
973
974
|
<div class="railStatus" id="railStatus" title="status"></div>
|
|
@@ -1218,8 +1219,9 @@ function buildReportsHtml(safeDefault, appVersion) {
|
|
|
1218
1219
|
<button class="railBtn" id="railDashboard" type="button" title="Dashboard">D</button>
|
|
1219
1220
|
<button class="railBtn active" id="railReports" type="button" title="Relatórios">R</button>
|
|
1220
1221
|
<button class="railBtn" id="railCompanion" type="button" title="Companion">C</button>
|
|
1221
|
-
|
|
1222
|
-
|
|
1222
|
+
<button class="railBtn" id="railProjects" type="button" title="Projects">P</button>
|
|
1223
|
+
<button class="railBtn" id="railTimeline" type="button" title="Timeline">T</button>
|
|
1224
|
+
<button class="railBtn" id="railGraph" type="button" title="Grafo">G</button>
|
|
1223
1225
|
</div>
|
|
1224
1226
|
<div class="railBottom">
|
|
1225
1227
|
<div class="railStatus" id="railStatus" title="status"></div>
|
|
@@ -1298,7 +1300,8 @@ function buildProjectsHtml(safeDefault, appVersion) {
|
|
|
1298
1300
|
<button class="railBtn" id="railReports" type="button" title="Relatorios">R</button>
|
|
1299
1301
|
<button class="railBtn" id="railCompanion" type="button" title="Companion">C</button>
|
|
1300
1302
|
<button class="railBtn active" id="railProjects" type="button" title="Projects">P</button>
|
|
1301
|
-
|
|
1303
|
+
<button class="railBtn" id="railTimeline" type="button" title="Timeline">T</button>
|
|
1304
|
+
<button class="railBtn" id="railGraph" type="button" title="Grafo">G</button>
|
|
1302
1305
|
</div>
|
|
1303
1306
|
<div class="railBottom">
|
|
1304
1307
|
<div class="railStatus" id="railStatus" title="status"></div>
|
|
@@ -1437,6 +1440,94 @@ function buildTimelineHtml(safeDefault, appVersion) {
|
|
|
1437
1440
|
</html>`;
|
|
1438
1441
|
}
|
|
1439
1442
|
|
|
1443
|
+
function buildGraphHtml(safeDefault, appVersion) {
|
|
1444
|
+
const safeVersion = escapeHtml(appVersion || 'unknown');
|
|
1445
|
+
return `<!doctype html>
|
|
1446
|
+
<html>
|
|
1447
|
+
<head>
|
|
1448
|
+
<meta charset="utf-8" />
|
|
1449
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1450
|
+
<title>Connections Graph</title>
|
|
1451
|
+
<link rel="stylesheet" href="/app.css" />
|
|
1452
|
+
<script src="/vis-network.min.js"></script>
|
|
1453
|
+
<style>
|
|
1454
|
+
#networkGraph {
|
|
1455
|
+
width: 100%;
|
|
1456
|
+
height: 600px;
|
|
1457
|
+
border: 1px solid var(--border);
|
|
1458
|
+
border-radius: 6px;
|
|
1459
|
+
background: var(--bg2);
|
|
1460
|
+
}
|
|
1461
|
+
</style>
|
|
1462
|
+
</head>
|
|
1463
|
+
<body data-page="graph">
|
|
1464
|
+
<div class="app">
|
|
1465
|
+
<div class="frame">
|
|
1466
|
+
<div class="shell">
|
|
1467
|
+
|
|
1468
|
+
<aside class="rail">
|
|
1469
|
+
<div class="railTop">
|
|
1470
|
+
<div class="railLogo">F</div>
|
|
1471
|
+
</div>
|
|
1472
|
+
<div class="railNav">
|
|
1473
|
+
<button class="railBtn" id="railDashboard" type="button" title="Dashboard">D</button>
|
|
1474
|
+
<button class="railBtn" id="railReports" type="button" title="Relatorios">R</button>
|
|
1475
|
+
<button class="railBtn" id="railCompanion" type="button" title="Companion">C</button>
|
|
1476
|
+
<button class="railBtn" id="railProjects" type="button" title="Projects">P</button>
|
|
1477
|
+
<button class="railBtn" id="railTimeline" type="button" title="Timeline">T</button>
|
|
1478
|
+
<button class="railBtn active" id="railGraph" type="button" title="Grafo">G</button>
|
|
1479
|
+
</div>
|
|
1480
|
+
<div class="railBottom">
|
|
1481
|
+
<div class="railStatus" id="railStatus" title="status"></div>
|
|
1482
|
+
</div>
|
|
1483
|
+
</aside>
|
|
1484
|
+
|
|
1485
|
+
<main class="center reportsPage" id="graphPage">
|
|
1486
|
+
<div class="topbar">
|
|
1487
|
+
<div class="brandLine">
|
|
1488
|
+
<span class="spark"></span>
|
|
1489
|
+
<div class="brandStack">
|
|
1490
|
+
<div class="brand">FREYA</div>
|
|
1491
|
+
<div class="brandSub">Connections Graph</div>
|
|
1492
|
+
</div>
|
|
1493
|
+
</div>
|
|
1494
|
+
<div class="topActions">
|
|
1495
|
+
<span class="chip" id="chipVersion">v${safeVersion}</span>
|
|
1496
|
+
<span class="chip" id="chipPort">127.0.0.1:3872</span>
|
|
1497
|
+
</div>
|
|
1498
|
+
</div>
|
|
1499
|
+
|
|
1500
|
+
<div class="centerBody">
|
|
1501
|
+
<input id="dir" type="hidden" />
|
|
1502
|
+
|
|
1503
|
+
<section class="reportsHeader">
|
|
1504
|
+
<div>
|
|
1505
|
+
<div class="reportsTitle">Grafo de Conexões</div>
|
|
1506
|
+
<div class="reportsSubtitle">Visualização de dependências entre projetos e tags.</div>
|
|
1507
|
+
</div>
|
|
1508
|
+
<div class="reportsActions">
|
|
1509
|
+
<button class="btn small" type="button" onclick="refreshGraph()">Atualizar</button>
|
|
1510
|
+
</div>
|
|
1511
|
+
</section>
|
|
1512
|
+
|
|
1513
|
+
<section class="reportsGrid">
|
|
1514
|
+
<div id="networkGraph"></div>
|
|
1515
|
+
</section>
|
|
1516
|
+
</div>
|
|
1517
|
+
</main>
|
|
1518
|
+
|
|
1519
|
+
</div>
|
|
1520
|
+
</div>
|
|
1521
|
+
</div>
|
|
1522
|
+
|
|
1523
|
+
<script>
|
|
1524
|
+
window.__FREYA_DEFAULT_DIR = "${safeDefault}";
|
|
1525
|
+
</script>
|
|
1526
|
+
<script src="/app.js"></script>
|
|
1527
|
+
</body>
|
|
1528
|
+
</html>`;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1440
1531
|
function buildCompanionHtml(safeDefault, appVersion) {
|
|
1441
1532
|
const safeVersion = escapeHtml(appVersion || 'unknown');
|
|
1442
1533
|
return `<!doctype html>
|
|
@@ -1460,8 +1551,9 @@ function buildCompanionHtml(safeDefault, appVersion) {
|
|
|
1460
1551
|
<button class="railBtn" id="railDashboard" type="button" title="Dashboard">D</button>
|
|
1461
1552
|
<button class="railBtn" id="railReports" type="button" title="Relatórios">R</button>
|
|
1462
1553
|
<button class="railBtn active" id="railCompanion" type="button" title="Companion">C</button>
|
|
1463
|
-
|
|
1464
|
-
|
|
1554
|
+
<button class="railBtn" id="railProjects" type="button" title="Projects">P</button>
|
|
1555
|
+
<button class="railBtn" id="railTimeline" type="button" title="Timeline">T</button>
|
|
1556
|
+
<button class="railBtn" id="railGraph" type="button" title="Grafo">G</button>
|
|
1465
1557
|
</div>
|
|
1466
1558
|
<div class="railBottom">
|
|
1467
1559
|
<div class="railStatus" id="railStatus" title="status"></div>
|
|
@@ -1536,11 +1628,11 @@ function buildCompanionHtml(safeDefault, appVersion) {
|
|
|
1536
1628
|
|
|
1537
1629
|
<section class="panel" style="margin-top:16px">
|
|
1538
1630
|
<div class="panelHead" style="display:flex; align-items:center; justify-content:space-between; gap:10px">
|
|
1539
|
-
<b>
|
|
1540
|
-
<button class="btn small" type="button" onclick="
|
|
1631
|
+
<b>Radar de Risco</b>
|
|
1632
|
+
<button class="btn small" type="button" onclick="refreshRiskRadar()">Atualizar</button>
|
|
1541
1633
|
</div>
|
|
1542
1634
|
<div class="panelBody">
|
|
1543
|
-
<div id="
|
|
1635
|
+
<div id="riskRadarBox"></div>
|
|
1544
1636
|
</div>
|
|
1545
1637
|
</section>
|
|
1546
1638
|
|
|
@@ -1933,7 +2025,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1933
2025
|
if (!req.url) return safeJson(res, 404, { error: 'Not found' });
|
|
1934
2026
|
|
|
1935
2027
|
if (req.method === 'GET' && req.url === '/') {
|
|
1936
|
-
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch {}
|
|
2028
|
+
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
1937
2029
|
const body = html(dir || './freya');
|
|
1938
2030
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
1939
2031
|
res.end(body);
|
|
@@ -1941,7 +2033,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1941
2033
|
}
|
|
1942
2034
|
|
|
1943
2035
|
if (req.method === 'GET' && req.url === '/reports') {
|
|
1944
|
-
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch {}
|
|
2036
|
+
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
1945
2037
|
const body = reportsHtml(dir || './freya');
|
|
1946
2038
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
1947
2039
|
res.end(body);
|
|
@@ -1949,7 +2041,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1949
2041
|
}
|
|
1950
2042
|
|
|
1951
2043
|
if (req.method === 'GET' && req.url === '/companion') {
|
|
1952
|
-
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch {}
|
|
2044
|
+
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
1953
2045
|
const body = companionHtml(dir || './freya');
|
|
1954
2046
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
1955
2047
|
res.end(body);
|
|
@@ -1957,15 +2049,23 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1957
2049
|
}
|
|
1958
2050
|
|
|
1959
2051
|
if (req.method === 'GET' && req.url === '/projects') {
|
|
1960
|
-
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch {}
|
|
2052
|
+
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
1961
2053
|
const body = projectsHtml(dir || './freya');
|
|
1962
2054
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
1963
2055
|
res.end(body);
|
|
1964
2056
|
return;
|
|
1965
2057
|
}
|
|
1966
2058
|
|
|
2059
|
+
if (req.method === 'GET' && req.url === '/graph') {
|
|
2060
|
+
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
2061
|
+
const body = buildGraphHtml(dir || './freya', APP_VERSION);
|
|
2062
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
2063
|
+
res.end(body);
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
1967
2067
|
if (req.method === 'GET' && req.url === '/timeline') {
|
|
1968
|
-
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch {}
|
|
2068
|
+
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
1969
2069
|
const body = timelineHtml(dir || './freya');
|
|
1970
2070
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
1971
2071
|
res.end(body);
|
|
@@ -1986,6 +2086,13 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1986
2086
|
return;
|
|
1987
2087
|
}
|
|
1988
2088
|
|
|
2089
|
+
if (req.method === 'GET' && req.url === '/vis-network.min.js') {
|
|
2090
|
+
const js = fs.readFileSync(path.join(__dirname, 'vis-network.min.js'), 'utf8');
|
|
2091
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8', 'Cache-Control': 'max-age=31536000' });
|
|
2092
|
+
res.end(js);
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
1989
2096
|
if (req.method === 'GET' && req.url === '/favicon.ico') {
|
|
1990
2097
|
res.writeHead(204, { 'Cache-Control': 'no-store' });
|
|
1991
2098
|
res.end();
|
|
@@ -2014,7 +2121,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2014
2121
|
url: req.url,
|
|
2015
2122
|
payload: summarizePayload(payload)
|
|
2016
2123
|
});
|
|
2017
|
-
} catch {}
|
|
2124
|
+
} catch { }
|
|
2018
2125
|
|
|
2019
2126
|
if (req.url === '/api/pick-dir') {
|
|
2020
2127
|
const picked = await pickDirectoryNative();
|
|
@@ -2087,11 +2194,96 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2087
2194
|
}
|
|
2088
2195
|
}
|
|
2089
2196
|
}
|
|
2090
|
-
items.sort((a,b)=> String(b.lastUpdated||'').localeCompare(String(a.lastUpdated||'')));
|
|
2197
|
+
items.sort((a, b) => String(b.lastUpdated || '').localeCompare(String(a.lastUpdated || '')));
|
|
2091
2198
|
return safeJson(res, 200, { ok: true, projects: items });
|
|
2092
2199
|
}
|
|
2093
2200
|
|
|
2094
|
-
if (req.url === '/api/
|
|
2201
|
+
if (req.url === '/api/graph/data') {
|
|
2202
|
+
const nodes = [];
|
|
2203
|
+
const edges = [];
|
|
2204
|
+
const nodeIds = new Set();
|
|
2205
|
+
let idCounter = 1;
|
|
2206
|
+
|
|
2207
|
+
const addNode = (id, label, group) => {
|
|
2208
|
+
if (!nodeIds.has(id)) {
|
|
2209
|
+
nodes.push({ id, label: truncateText(label, 30), group });
|
|
2210
|
+
nodeIds.add(id);
|
|
2211
|
+
}
|
|
2212
|
+
};
|
|
2213
|
+
|
|
2214
|
+
// 1. Load Projects
|
|
2215
|
+
const base = path.join(workspaceDir, 'data', 'Clients');
|
|
2216
|
+
if (exists(base)) {
|
|
2217
|
+
const stack = [base];
|
|
2218
|
+
while (stack.length) {
|
|
2219
|
+
const dirp = stack.pop();
|
|
2220
|
+
const entries = fs.readdirSync(dirp, { withFileTypes: true });
|
|
2221
|
+
for (const ent of entries) {
|
|
2222
|
+
const full = path.join(dirp, ent.name);
|
|
2223
|
+
if (ent.isDirectory()) stack.push(full);
|
|
2224
|
+
else if (ent.isFile() && ent.name === 'status.json') {
|
|
2225
|
+
const doc = readJsonOrNull(full) || {};
|
|
2226
|
+
const slug = path.relative(base, path.dirname(full)).replace(/\\/g, '/');
|
|
2227
|
+
addNode(slug, slug, 'project');
|
|
2228
|
+
|
|
2229
|
+
// Link tags
|
|
2230
|
+
const tags = Array.isArray(doc.tags) ? doc.tags : [];
|
|
2231
|
+
for (const t of tags) {
|
|
2232
|
+
const tid = 'tag:' + String(t).trim().toLowerCase();
|
|
2233
|
+
addNode(tid, String(t).trim(), 'tag');
|
|
2234
|
+
edges.push({ from: slug, to: tid });
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
// 2. Load Tasks
|
|
2242
|
+
const taskFile = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
|
|
2243
|
+
const taskDoc = readJsonOrNull(taskFile) || { tasks: [] };
|
|
2244
|
+
const tasks = Array.isArray(taskDoc.tasks) ? taskDoc.tasks : [];
|
|
2245
|
+
|
|
2246
|
+
for (const t of tasks) {
|
|
2247
|
+
// Only show pending high/medium tasks for clarity, or unassigned ones.
|
|
2248
|
+
if (t.status === 'COMPLETED') continue;
|
|
2249
|
+
|
|
2250
|
+
const tid = 'task:' + t.id;
|
|
2251
|
+
addNode(tid, t.title || 'Tarefa', 'task');
|
|
2252
|
+
|
|
2253
|
+
const slug = t.projectSlug || null;
|
|
2254
|
+
if (slug && nodeIds.has(slug)) {
|
|
2255
|
+
edges.push({ from: tid, to: slug });
|
|
2256
|
+
} else {
|
|
2257
|
+
// Connect to an "Unassigned" node
|
|
2258
|
+
addNode('unassigned', 'Sin Asignar', 'unassigned');
|
|
2259
|
+
edges.push({ from: tid, to: 'unassigned' });
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
// 3. Load Blockers
|
|
2264
|
+
const blockerFile = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
|
|
2265
|
+
const blockerDoc = readJsonOrNull(blockerFile) || { blockers: [] };
|
|
2266
|
+
const blockers = Array.isArray(blockerDoc.blockers) ? blockerDoc.blockers : [];
|
|
2267
|
+
|
|
2268
|
+
for (const b of blockers) {
|
|
2269
|
+
if (String(b.status || '').toUpperCase() !== 'OPEN') continue;
|
|
2270
|
+
|
|
2271
|
+
const bid = 'blocker:' + b.id;
|
|
2272
|
+
addNode(bid, 'BLQ: ' + (b.title || 'Blocker'), 'blocker');
|
|
2273
|
+
|
|
2274
|
+
const slug = b.projectSlug || null;
|
|
2275
|
+
if (slug && nodeIds.has(slug)) {
|
|
2276
|
+
edges.push({ from: bid, to: slug });
|
|
2277
|
+
} else {
|
|
2278
|
+
addNode('unassigned_blockers', 'Bloqueios Soltos', 'unassigned');
|
|
2279
|
+
edges.push({ from: bid, to: 'unassigned_blockers' });
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
return safeJson(res, 200, { ok: true, nodes, edges });
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
if (req.url === '/api/timeline') {
|
|
2095
2287
|
const items = getTimelineItems(workspaceDir);
|
|
2096
2288
|
return safeJson(res, 200, { ok: true, items });
|
|
2097
2289
|
}
|
|
@@ -2313,6 +2505,107 @@ if (req.url === '/api/timeline') {
|
|
|
2313
2505
|
return safeJson(res, 200, { ok: true, anomalies });
|
|
2314
2506
|
}
|
|
2315
2507
|
|
|
2508
|
+
if (req.url === '/api/companion/risk-radar') {
|
|
2509
|
+
if (!looksLikeFreyaWorkspace(workspaceDir)) {
|
|
2510
|
+
return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized' });
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
const now = Date.now();
|
|
2514
|
+
const items = [];
|
|
2515
|
+
|
|
2516
|
+
// 1. Inactive Projects (Stale)
|
|
2517
|
+
const base = path.join(workspaceDir, 'data', 'Clients');
|
|
2518
|
+
if (exists(base)) {
|
|
2519
|
+
const stack = [base];
|
|
2520
|
+
while (stack.length) {
|
|
2521
|
+
const dirp = stack.pop();
|
|
2522
|
+
const entries = fs.readdirSync(dirp, { withFileTypes: true });
|
|
2523
|
+
for (const ent of entries) {
|
|
2524
|
+
const full = path.join(dirp, ent.name);
|
|
2525
|
+
if (ent.isDirectory()) stack.push(full);
|
|
2526
|
+
else if (ent.isFile() && ent.name === 'status.json') {
|
|
2527
|
+
const doc = readJsonOrNull(full) || {};
|
|
2528
|
+
const slug = path.relative(base, path.dirname(full)).replace(/\\/g, '/');
|
|
2529
|
+
if (doc.active !== false) {
|
|
2530
|
+
const lastUpdated = doc.lastUpdated ? Date.parse(doc.lastUpdated) : null;
|
|
2531
|
+
if (lastUpdated) {
|
|
2532
|
+
const ageDays = Math.floor((now - lastUpdated) / (24 * 60 * 60 * 1000));
|
|
2533
|
+
if (ageDays > 14) { // 14 days without an update is a risk
|
|
2534
|
+
items.push({
|
|
2535
|
+
type: 'stale_project',
|
|
2536
|
+
severity: ageDays > 30 ? 'high' : 'medium',
|
|
2537
|
+
slug: slug,
|
|
2538
|
+
message: `Projeto Inativo (${ageDays} dias sem update)`
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
// 2. Blockers concentration
|
|
2549
|
+
const blockerFile = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
|
|
2550
|
+
if (exists(blockerFile)) {
|
|
2551
|
+
const blockerDoc = readJsonOrNull(blockerFile) || { blockers: [] };
|
|
2552
|
+
const blockers = Array.isArray(blockerDoc.blockers) ? blockerDoc.blockers : [];
|
|
2553
|
+
const blockersByProject = {};
|
|
2554
|
+
for (const b of blockers) {
|
|
2555
|
+
if (String(b.status || '').toUpperCase() === 'OPEN') {
|
|
2556
|
+
const slug = String(b.projectSlug || '').trim();
|
|
2557
|
+
if (slug) {
|
|
2558
|
+
blockersByProject[slug] = (blockersByProject[slug] || 0) + 1;
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
for (const [slug, count] of Object.entries(blockersByProject)) {
|
|
2563
|
+
if (count >= 3) {
|
|
2564
|
+
items.push({
|
|
2565
|
+
type: 'blocker_concentration',
|
|
2566
|
+
severity: count > 5 ? 'high' : 'medium',
|
|
2567
|
+
slug: slug,
|
|
2568
|
+
message: `Concentração de Bloqueios (${count} abertos)`
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
// 3. Task Overload
|
|
2575
|
+
const taskFile = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
|
|
2576
|
+
if (exists(taskFile)) {
|
|
2577
|
+
const taskDoc = readJsonOrNull(taskFile) || { tasks: [] };
|
|
2578
|
+
const tasks = Array.isArray(taskDoc.tasks) ? taskDoc.tasks : [];
|
|
2579
|
+
const tasksByProject = {};
|
|
2580
|
+
for (const t of tasks) {
|
|
2581
|
+
if (t.status !== 'COMPLETED' && String(t.priority || '').toUpperCase() === 'HIGH') {
|
|
2582
|
+
const slug = String(t.projectSlug || '').trim();
|
|
2583
|
+
if (slug) {
|
|
2584
|
+
tasksByProject[slug] = (tasksByProject[slug] || 0) + 1;
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
for (const [slug, count] of Object.entries(tasksByProject)) {
|
|
2589
|
+
if (count >= 5) {
|
|
2590
|
+
items.push({
|
|
2591
|
+
type: 'task_overload',
|
|
2592
|
+
severity: 'high',
|
|
2593
|
+
slug: slug,
|
|
2594
|
+
message: `Sobrecarga Crítica (${count} tarefas High-priority pendentes)`
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
items.sort((a, b) => {
|
|
2601
|
+
const sA = a.severity === 'high' ? 2 : 1;
|
|
2602
|
+
const sB = b.severity === 'high' ? 2 : 1;
|
|
2603
|
+
return sB - sA;
|
|
2604
|
+
});
|
|
2605
|
+
|
|
2606
|
+
return safeJson(res, 200, { ok: true, items });
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2316
2609
|
if (req.url === '/api/incidents/resolve') {
|
|
2317
2610
|
const title = payload.title;
|
|
2318
2611
|
const index = Number.isInteger(payload.index) ? payload.index : null;
|
|
@@ -2356,7 +2649,7 @@ if (req.url === '/api/timeline') {
|
|
|
2356
2649
|
return safeJson(res, 200, { ok: true, items });
|
|
2357
2650
|
}
|
|
2358
2651
|
|
|
2359
|
-
if (req.url === '/api/reports/list') {
|
|
2652
|
+
if (req.url === '/api/reports/list') {
|
|
2360
2653
|
const reports = listReports(workspaceDir);
|
|
2361
2654
|
return safeJson(res, 200, { reports });
|
|
2362
2655
|
}
|
|
@@ -3024,7 +3317,7 @@ if (req.url === '/api/reports/list') {
|
|
|
3024
3317
|
const obj = JSON.parse(line);
|
|
3025
3318
|
if (!obj || typeof obj !== 'object') continue;
|
|
3026
3319
|
items.push(obj);
|
|
3027
|
-
} catch {}
|
|
3320
|
+
} catch { }
|
|
3028
3321
|
}
|
|
3029
3322
|
|
|
3030
3323
|
const outDir = path.join(workspaceDir, 'docs', 'chat');
|
|
@@ -3161,7 +3454,7 @@ if (req.url === '/api/reports/list') {
|
|
|
3161
3454
|
const reportsToday = reports.filter((r) => r && String(r.name || '').includes(today)).length;
|
|
3162
3455
|
|
|
3163
3456
|
const items = [
|
|
3164
|
-
|
|
3457
|
+
{ label: 'Blockers abertos', status: openBlockers > 0 ? 'warn' : 'ok', detail: `${openBlockers} aberto(s)` },
|
|
3165
3458
|
{ label: 'Tarefas DO_NOW pendentes', status: pendingTasks > 0 ? 'warn' : 'ok', detail: `${pendingTasks} pendente(s)` },
|
|
3166
3459
|
{ label: 'Relatorios de hoje', status: reportsToday > 0 ? 'ok' : 'warn', detail: `${reportsToday} gerado(s)` }
|
|
3167
3460
|
];
|
|
@@ -3212,7 +3505,7 @@ if (req.url === '/api/reports/list') {
|
|
|
3212
3505
|
return safeJson(res, 200, { ok: true, items });
|
|
3213
3506
|
}
|
|
3214
3507
|
|
|
3215
|
-
if (req.url === '/api/blockers/summary') {
|
|
3508
|
+
if (req.url === '/api/blockers/summary') {
|
|
3216
3509
|
const file = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
|
|
3217
3510
|
const doc = readJsonOrNull(file) || { schemaVersion: 1, blockers: [] };
|
|
3218
3511
|
const blockers = Array.isArray(doc.blockers) ? doc.blockers : [];
|
package/package.json
CHANGED