@cccarv82/freya 2.3.9 → 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 +325 -27
- 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',
|
|
@@ -938,6 +938,11 @@ function companionHtml(defaultDir) {
|
|
|
938
938
|
return buildCompanionHtml(safeDefault, APP_VERSION);
|
|
939
939
|
}
|
|
940
940
|
|
|
941
|
+
function timelineHtml(defaultDir) {
|
|
942
|
+
const safeDefault = String(defaultDir || './freya').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
943
|
+
return buildTimelineHtml(safeDefault, APP_VERSION);
|
|
944
|
+
}
|
|
945
|
+
|
|
941
946
|
function buildHtml(safeDefault, appVersion) {
|
|
942
947
|
const safeVersion = escapeHtml(appVersion || 'unknown');
|
|
943
948
|
return `<!doctype html>
|
|
@@ -961,8 +966,9 @@ function buildHtml(safeDefault, appVersion) {
|
|
|
961
966
|
<button class="railBtn active" id="railDashboard" type="button" title="Dashboard">D</button>
|
|
962
967
|
<button class="railBtn" id="railReports" type="button" title="Relatórios">R</button>
|
|
963
968
|
<button class="railBtn" id="railCompanion" type="button" title="Companion">C</button>
|
|
964
|
-
|
|
965
|
-
|
|
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>
|
|
966
972
|
</div>
|
|
967
973
|
<div class="railBottom">
|
|
968
974
|
<div class="railStatus" id="railStatus" title="status"></div>
|
|
@@ -1213,8 +1219,9 @@ function buildReportsHtml(safeDefault, appVersion) {
|
|
|
1213
1219
|
<button class="railBtn" id="railDashboard" type="button" title="Dashboard">D</button>
|
|
1214
1220
|
<button class="railBtn active" id="railReports" type="button" title="Relatórios">R</button>
|
|
1215
1221
|
<button class="railBtn" id="railCompanion" type="button" title="Companion">C</button>
|
|
1216
|
-
|
|
1217
|
-
|
|
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>
|
|
1218
1225
|
</div>
|
|
1219
1226
|
<div class="railBottom">
|
|
1220
1227
|
<div class="railStatus" id="railStatus" title="status"></div>
|
|
@@ -1293,7 +1300,8 @@ function buildProjectsHtml(safeDefault, appVersion) {
|
|
|
1293
1300
|
<button class="railBtn" id="railReports" type="button" title="Relatorios">R</button>
|
|
1294
1301
|
<button class="railBtn" id="railCompanion" type="button" title="Companion">C</button>
|
|
1295
1302
|
<button class="railBtn active" id="railProjects" type="button" title="Projects">P</button>
|
|
1296
|
-
|
|
1303
|
+
<button class="railBtn" id="railTimeline" type="button" title="Timeline">T</button>
|
|
1304
|
+
<button class="railBtn" id="railGraph" type="button" title="Grafo">G</button>
|
|
1297
1305
|
</div>
|
|
1298
1306
|
<div class="railBottom">
|
|
1299
1307
|
<div class="railStatus" id="railStatus" title="status"></div>
|
|
@@ -1432,6 +1440,94 @@ function buildTimelineHtml(safeDefault, appVersion) {
|
|
|
1432
1440
|
</html>`;
|
|
1433
1441
|
}
|
|
1434
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
|
+
|
|
1435
1531
|
function buildCompanionHtml(safeDefault, appVersion) {
|
|
1436
1532
|
const safeVersion = escapeHtml(appVersion || 'unknown');
|
|
1437
1533
|
return `<!doctype html>
|
|
@@ -1455,8 +1551,9 @@ function buildCompanionHtml(safeDefault, appVersion) {
|
|
|
1455
1551
|
<button class="railBtn" id="railDashboard" type="button" title="Dashboard">D</button>
|
|
1456
1552
|
<button class="railBtn" id="railReports" type="button" title="Relatórios">R</button>
|
|
1457
1553
|
<button class="railBtn active" id="railCompanion" type="button" title="Companion">C</button>
|
|
1458
|
-
|
|
1459
|
-
|
|
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>
|
|
1460
1557
|
</div>
|
|
1461
1558
|
<div class="railBottom">
|
|
1462
1559
|
<div class="railStatus" id="railStatus" title="status"></div>
|
|
@@ -1531,11 +1628,11 @@ function buildCompanionHtml(safeDefault, appVersion) {
|
|
|
1531
1628
|
|
|
1532
1629
|
<section class="panel" style="margin-top:16px">
|
|
1533
1630
|
<div class="panelHead" style="display:flex; align-items:center; justify-content:space-between; gap:10px">
|
|
1534
|
-
<b>
|
|
1535
|
-
<button class="btn small" type="button" onclick="
|
|
1631
|
+
<b>Radar de Risco</b>
|
|
1632
|
+
<button class="btn small" type="button" onclick="refreshRiskRadar()">Atualizar</button>
|
|
1536
1633
|
</div>
|
|
1537
1634
|
<div class="panelBody">
|
|
1538
|
-
<div id="
|
|
1635
|
+
<div id="riskRadarBox"></div>
|
|
1539
1636
|
</div>
|
|
1540
1637
|
</section>
|
|
1541
1638
|
|
|
@@ -1928,7 +2025,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1928
2025
|
if (!req.url) return safeJson(res, 404, { error: 'Not found' });
|
|
1929
2026
|
|
|
1930
2027
|
if (req.method === 'GET' && req.url === '/') {
|
|
1931
|
-
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch {}
|
|
2028
|
+
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
1932
2029
|
const body = html(dir || './freya');
|
|
1933
2030
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
1934
2031
|
res.end(body);
|
|
@@ -1936,7 +2033,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1936
2033
|
}
|
|
1937
2034
|
|
|
1938
2035
|
if (req.method === 'GET' && req.url === '/reports') {
|
|
1939
|
-
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch {}
|
|
2036
|
+
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
1940
2037
|
const body = reportsHtml(dir || './freya');
|
|
1941
2038
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
1942
2039
|
res.end(body);
|
|
@@ -1944,7 +2041,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1944
2041
|
}
|
|
1945
2042
|
|
|
1946
2043
|
if (req.method === 'GET' && req.url === '/companion') {
|
|
1947
|
-
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch {}
|
|
2044
|
+
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
1948
2045
|
const body = companionHtml(dir || './freya');
|
|
1949
2046
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
1950
2047
|
res.end(body);
|
|
@@ -1952,16 +2049,24 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1952
2049
|
}
|
|
1953
2050
|
|
|
1954
2051
|
if (req.method === 'GET' && req.url === '/projects') {
|
|
1955
|
-
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch {}
|
|
2052
|
+
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
1956
2053
|
const body = projectsHtml(dir || './freya');
|
|
1957
2054
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
1958
2055
|
res.end(body);
|
|
1959
2056
|
return;
|
|
1960
2057
|
}
|
|
1961
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
|
+
|
|
1962
2067
|
if (req.method === 'GET' && req.url === '/timeline') {
|
|
1963
|
-
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch {}
|
|
1964
|
-
const body =
|
|
2068
|
+
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
2069
|
+
const body = timelineHtml(dir || './freya');
|
|
1965
2070
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
1966
2071
|
res.end(body);
|
|
1967
2072
|
return;
|
|
@@ -1981,6 +2086,13 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1981
2086
|
return;
|
|
1982
2087
|
}
|
|
1983
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
|
+
|
|
1984
2096
|
if (req.method === 'GET' && req.url === '/favicon.ico') {
|
|
1985
2097
|
res.writeHead(204, { 'Cache-Control': 'no-store' });
|
|
1986
2098
|
res.end();
|
|
@@ -2009,7 +2121,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2009
2121
|
url: req.url,
|
|
2010
2122
|
payload: summarizePayload(payload)
|
|
2011
2123
|
});
|
|
2012
|
-
} catch {}
|
|
2124
|
+
} catch { }
|
|
2013
2125
|
|
|
2014
2126
|
if (req.url === '/api/pick-dir') {
|
|
2015
2127
|
const picked = await pickDirectoryNative();
|
|
@@ -2082,11 +2194,96 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2082
2194
|
}
|
|
2083
2195
|
}
|
|
2084
2196
|
}
|
|
2085
|
-
items.sort((a,b)=> String(b.lastUpdated||'').localeCompare(String(a.lastUpdated||'')));
|
|
2197
|
+
items.sort((a, b) => String(b.lastUpdated || '').localeCompare(String(a.lastUpdated || '')));
|
|
2086
2198
|
return safeJson(res, 200, { ok: true, projects: items });
|
|
2087
2199
|
}
|
|
2088
2200
|
|
|
2089
|
-
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') {
|
|
2090
2287
|
const items = getTimelineItems(workspaceDir);
|
|
2091
2288
|
return safeJson(res, 200, { ok: true, items });
|
|
2092
2289
|
}
|
|
@@ -2308,6 +2505,107 @@ if (req.url === '/api/timeline') {
|
|
|
2308
2505
|
return safeJson(res, 200, { ok: true, anomalies });
|
|
2309
2506
|
}
|
|
2310
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
|
+
|
|
2311
2609
|
if (req.url === '/api/incidents/resolve') {
|
|
2312
2610
|
const title = payload.title;
|
|
2313
2611
|
const index = Number.isInteger(payload.index) ? payload.index : null;
|
|
@@ -2351,7 +2649,7 @@ if (req.url === '/api/timeline') {
|
|
|
2351
2649
|
return safeJson(res, 200, { ok: true, items });
|
|
2352
2650
|
}
|
|
2353
2651
|
|
|
2354
|
-
if (req.url === '/api/reports/list') {
|
|
2652
|
+
if (req.url === '/api/reports/list') {
|
|
2355
2653
|
const reports = listReports(workspaceDir);
|
|
2356
2654
|
return safeJson(res, 200, { reports });
|
|
2357
2655
|
}
|
|
@@ -3019,7 +3317,7 @@ if (req.url === '/api/reports/list') {
|
|
|
3019
3317
|
const obj = JSON.parse(line);
|
|
3020
3318
|
if (!obj || typeof obj !== 'object') continue;
|
|
3021
3319
|
items.push(obj);
|
|
3022
|
-
} catch {}
|
|
3320
|
+
} catch { }
|
|
3023
3321
|
}
|
|
3024
3322
|
|
|
3025
3323
|
const outDir = path.join(workspaceDir, 'docs', 'chat');
|
|
@@ -3156,7 +3454,7 @@ if (req.url === '/api/reports/list') {
|
|
|
3156
3454
|
const reportsToday = reports.filter((r) => r && String(r.name || '').includes(today)).length;
|
|
3157
3455
|
|
|
3158
3456
|
const items = [
|
|
3159
|
-
|
|
3457
|
+
{ label: 'Blockers abertos', status: openBlockers > 0 ? 'warn' : 'ok', detail: `${openBlockers} aberto(s)` },
|
|
3160
3458
|
{ label: 'Tarefas DO_NOW pendentes', status: pendingTasks > 0 ? 'warn' : 'ok', detail: `${pendingTasks} pendente(s)` },
|
|
3161
3459
|
{ label: 'Relatorios de hoje', status: reportsToday > 0 ? 'ok' : 'warn', detail: `${reportsToday} gerado(s)` }
|
|
3162
3460
|
];
|
|
@@ -3207,7 +3505,7 @@ if (req.url === '/api/reports/list') {
|
|
|
3207
3505
|
return safeJson(res, 200, { ok: true, items });
|
|
3208
3506
|
}
|
|
3209
3507
|
|
|
3210
|
-
if (req.url === '/api/blockers/summary') {
|
|
3508
|
+
if (req.url === '/api/blockers/summary') {
|
|
3211
3509
|
const file = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
|
|
3212
3510
|
const doc = readJsonOrNull(file) || { schemaVersion: 1, blockers: [] };
|
|
3213
3511
|
const blockers = Array.isArray(doc.blockers) ? doc.blockers : [];
|
package/package.json
CHANGED