@cccarv82/freya 2.0.0 → 2.1.0
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.css +5 -0
- package/cli/web-ui.js +96 -0
- package/cli/web.js +190 -2
- package/package.json +1 -1
- package/scripts/generate-blockers-report.js +2 -1
- package/scripts/generate-daily-summary.js +2 -1
- package/scripts/generate-executive-report.js +2 -1
- package/scripts/generate-sm-weekly-report.js +2 -1
- package/scripts/generate-weekly-report.js +9 -1
- package/templates/base/scripts/generate-blockers-report.js +2 -1
- package/templates/base/scripts/generate-daily-summary.js +2 -1
- package/templates/base/scripts/generate-executive-report.js +2 -1
- package/templates/base/scripts/generate-sm-weekly-report.js +2 -1
- package/templates/base/scripts/generate-weekly-report.js +9 -1
package/cli/web-ui.css
CHANGED
|
@@ -240,6 +240,11 @@ body {
|
|
|
240
240
|
.topActions { display: flex; align-items: center; gap: 10px; }
|
|
241
241
|
.chip { font-family: var(--mono); font-size: 12px; padding: 7px 10px; border-radius: 999px; border: 1px solid var(--line); background: rgba(0,0,0,.22); color: var(--faint); }
|
|
242
242
|
|
|
243
|
+
.pill { font-family: var(--mono); font-size: 11px; padding: 4px 8px; border-radius: 999px; border: 1px solid var(--line); background: var(--paper2); color: var(--muted); text-transform: uppercase; letter-spacing: .08em; }
|
|
244
|
+
.pill.ok { background: rgba(16, 185, 129, .12); color: #065f46; border-color: rgba(16, 185, 129, .28); }
|
|
245
|
+
.pill.warn { background: rgba(245, 158, 11, .18); color: #92400e; border-color: rgba(245, 158, 11, .35); }
|
|
246
|
+
.pill.info { background: rgba(59, 130, 246, .14); color: #1e3a8a; border-color: rgba(59, 130, 246, .35); }
|
|
247
|
+
|
|
243
248
|
.centerBody {
|
|
244
249
|
padding: 16px 20px 20px;
|
|
245
250
|
overflow: auto;
|
package/cli/web-ui.js
CHANGED
|
@@ -680,6 +680,7 @@
|
|
|
680
680
|
function wireRailNav() {
|
|
681
681
|
const dash = $('railDashboard');
|
|
682
682
|
const rep = $('railReports');
|
|
683
|
+
const health = $('railHealth');
|
|
683
684
|
if (dash) {
|
|
684
685
|
dash.onclick = () => {
|
|
685
686
|
const isReports = document.body && document.body.dataset && document.body.dataset.page === 'reports';
|
|
@@ -697,6 +698,12 @@
|
|
|
697
698
|
if (!isReports) window.location.href = '/reports';
|
|
698
699
|
};
|
|
699
700
|
}
|
|
701
|
+
if (health) {
|
|
702
|
+
health.onclick = () => {
|
|
703
|
+
const isHealth = document.body && document.body.dataset && document.body.dataset.page === 'health';
|
|
704
|
+
if (!isHealth) window.location.href = '/health';
|
|
705
|
+
};
|
|
706
|
+
}
|
|
700
707
|
}
|
|
701
708
|
|
|
702
709
|
async function editTask(t) {
|
|
@@ -819,11 +826,42 @@
|
|
|
819
826
|
]);
|
|
820
827
|
renderTasks((t && t.tasks) || []);
|
|
821
828
|
renderBlockers((b && b.blockers) || []);
|
|
829
|
+
refreshBlockersInsights();
|
|
822
830
|
} catch (e) {
|
|
823
831
|
// keep silent in background refresh
|
|
824
832
|
}
|
|
825
833
|
}
|
|
826
834
|
|
|
835
|
+
function renderBlockersInsights(payload) {
|
|
836
|
+
const el = $('blockersInsights');
|
|
837
|
+
if (!el) return;
|
|
838
|
+
if (!payload || !payload.summary) {
|
|
839
|
+
el.textContent = 'Sem insights no momento.';
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const lines = [];
|
|
843
|
+
lines.push('<div class="help" style="margin-bottom:6px"><b>Resumo:</b> ' + escapeHtml(payload.summary) + '</div>');
|
|
844
|
+
if (payload.suggestions && payload.suggestions.length) {
|
|
845
|
+
lines.push('<div class="help"><b>Proximos passos:</b></div>');
|
|
846
|
+
lines.push('<ul style="margin:6px 0 0 18px; padding:0;">' + payload.suggestions.map((s) => '<li class="help">' + escapeHtml(s) + '</li>').join('') + '</ul>');
|
|
847
|
+
}
|
|
848
|
+
if (payload.top && payload.top.length) {
|
|
849
|
+
lines.push('<div class="help" style="margin-top:8px"><b>Top blockers:</b></div>');
|
|
850
|
+
lines.push('<ul style="margin:6px 0 0 18px; padding:0;">' + payload.top.map((b) => '<li class="help">' + escapeHtml(String(b.severity || '')) + ' - ' + escapeHtml(String(b.title || '')) + '</li>').join('') + '</ul>');
|
|
851
|
+
}
|
|
852
|
+
el.innerHTML = lines.join('');
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
async function refreshBlockersInsights() {
|
|
856
|
+
try {
|
|
857
|
+
const r = await api('/api/blockers/summary', { dir: dirOrDefault() });
|
|
858
|
+
renderBlockersInsights(r);
|
|
859
|
+
} catch (e) {
|
|
860
|
+
const el = $('blockersInsights');
|
|
861
|
+
if (el) el.textContent = 'Falha ao carregar insights.';
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
827
865
|
async function pickDir() {
|
|
828
866
|
try {
|
|
829
867
|
setPill('run', 'picker…');
|
|
@@ -879,6 +917,50 @@
|
|
|
879
917
|
}
|
|
880
918
|
}
|
|
881
919
|
|
|
920
|
+
function renderHealthChecklist(items) {
|
|
921
|
+
const el = $('healthChecklist');
|
|
922
|
+
if (!el) return;
|
|
923
|
+
el.innerHTML = '';
|
|
924
|
+
const list = Array.isArray(items) ? items : [];
|
|
925
|
+
if (!list.length) {
|
|
926
|
+
const empty = document.createElement('div');
|
|
927
|
+
empty.className = 'help';
|
|
928
|
+
empty.textContent = 'Sem checks disponiveis.';
|
|
929
|
+
el.appendChild(empty);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
for (const it of list) {
|
|
933
|
+
const row = document.createElement('div');
|
|
934
|
+
row.className = 'rep';
|
|
935
|
+
const status = String(it.status || 'info');
|
|
936
|
+
row.innerHTML = '<div style="display:flex; justify-content:space-between; gap:10px; align-items:center">'
|
|
937
|
+
+ '<div style="min-width:0"><div style="font-weight:800">' + escapeHtml(it.label || '') + '</div>'
|
|
938
|
+
+ '<div class="help" style="margin-top:4px">' + escapeHtml(it.detail || '') + '</div></div>'
|
|
939
|
+
+ '<div class="pill ' + escapeHtml(status) + '">' + escapeHtml(status) + '</div>'
|
|
940
|
+
+ '</div>';
|
|
941
|
+
el.appendChild(row);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
async function refreshHealthChecklist() {
|
|
946
|
+
try {
|
|
947
|
+
setPill('run', 'checklist…');
|
|
948
|
+
const r = await api('/api/health/checklist', { dir: dirOrDefault() });
|
|
949
|
+
if (r && r.needsInit) {
|
|
950
|
+
setOut(r.error || 'Workspace not initialized');
|
|
951
|
+
setPill('plan', 'needs init');
|
|
952
|
+
renderHealthChecklist([]);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
renderHealthChecklist(r.items || []);
|
|
956
|
+
setPill('ok', 'pronto');
|
|
957
|
+
} catch (e) {
|
|
958
|
+
setPill('err', 'checklist failed');
|
|
959
|
+
const el = $('healthChecklist');
|
|
960
|
+
if (el) el.textContent = 'Falha ao carregar checklist.';
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
882
964
|
async function doHealth() {
|
|
883
965
|
try {
|
|
884
966
|
saveLocal();
|
|
@@ -886,6 +968,12 @@
|
|
|
886
968
|
setPill('run', 'health…');
|
|
887
969
|
setOut('');
|
|
888
970
|
const r = await api('/api/health', { dir: dirOrDefault() });
|
|
971
|
+
if (r && r.needsInit) {
|
|
972
|
+
setOut(r.error || 'Workspace not initialized');
|
|
973
|
+
setLast(null);
|
|
974
|
+
setPill('plan', 'needs init');
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
889
977
|
setOut(r.output);
|
|
890
978
|
setLast(null);
|
|
891
979
|
setPill('ok', 'health ok');
|
|
@@ -1214,6 +1302,7 @@
|
|
|
1214
1302
|
} catch {}
|
|
1215
1303
|
|
|
1216
1304
|
const isReportsPage = document.body && document.body.dataset && document.body.dataset.page === 'reports';
|
|
1305
|
+
const isHealthPage = document.body && document.body.dataset && document.body.dataset.page === 'health';
|
|
1217
1306
|
|
|
1218
1307
|
// Load persisted settings from the workspace + bootstrap (auto-init + auto-health)
|
|
1219
1308
|
(async () => {
|
|
@@ -1241,6 +1330,11 @@
|
|
|
1241
1330
|
return;
|
|
1242
1331
|
}
|
|
1243
1332
|
|
|
1333
|
+
if (isHealthPage) {
|
|
1334
|
+
await refreshHealthChecklist();
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1244
1338
|
// If workspace isn't initialized yet, auto-init (reduces clicks)
|
|
1245
1339
|
try {
|
|
1246
1340
|
if (defaults && defaults.workspaceOk === false) {
|
|
@@ -1279,6 +1373,8 @@
|
|
|
1279
1373
|
window.renderReportsList = renderReportsList;
|
|
1280
1374
|
window.renderReportsPage = renderReportsPage;
|
|
1281
1375
|
window.refreshReportsPage = refreshReportsPage;
|
|
1376
|
+
window.refreshBlockersInsights = refreshBlockersInsights;
|
|
1377
|
+
window.refreshHealthChecklist = refreshHealthChecklist;
|
|
1282
1378
|
window.copyOut = copyOut;
|
|
1283
1379
|
window.copyPath = copyPath;
|
|
1284
1380
|
window.openSelected = openSelected;
|
package/cli/web.js
CHANGED
|
@@ -877,6 +877,11 @@ function reportsHtml(defaultDir) {
|
|
|
877
877
|
return buildReportsHtml(safeDefault, APP_VERSION);
|
|
878
878
|
}
|
|
879
879
|
|
|
880
|
+
function healthHtml(defaultDir) {
|
|
881
|
+
const safeDefault = String(defaultDir || './freya').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
882
|
+
return buildHealthHtml(safeDefault, APP_VERSION);
|
|
883
|
+
}
|
|
884
|
+
|
|
880
885
|
function buildHtml(safeDefault, appVersion) {
|
|
881
886
|
const safeVersion = escapeHtml(appVersion || 'unknown');
|
|
882
887
|
return `<!doctype html>
|
|
@@ -899,6 +904,7 @@ function buildHtml(safeDefault, appVersion) {
|
|
|
899
904
|
<div class="railNav">
|
|
900
905
|
<button class="railBtn active" id="railDashboard" type="button" title="Dashboard">D</button>
|
|
901
906
|
<button class="railBtn" id="railReports" type="button" title="Relatórios">R</button>
|
|
907
|
+
<button class="railBtn" id="railHealth" type="button" title="Saude">H</button>
|
|
902
908
|
</div>
|
|
903
909
|
<div class="railBottom">
|
|
904
910
|
<div class="railStatus" id="railStatus" title="status"></div>
|
|
@@ -999,6 +1005,10 @@ function buildHtml(safeDefault, appVersion) {
|
|
|
999
1005
|
<div style="height:12px"></div>
|
|
1000
1006
|
<div class="small" style="margin-bottom:8px; opacity:.8">Bloqueios abertos</div>
|
|
1001
1007
|
<div id="blockersList" style="display:grid; gap:8px"></div>
|
|
1008
|
+
<div style="height:12px"></div>
|
|
1009
|
+
<div class="small" style="margin-bottom:8px; opacity:.8">Insights de blockers</div>
|
|
1010
|
+
<div id="blockersInsights" class="help"></div>
|
|
1011
|
+
<div style="margin-top:8px"><button class="btn small" type="button" onclick="refreshBlockersInsights()">Atualizar insights</button></div>
|
|
1002
1012
|
</div>
|
|
1003
1013
|
</section>
|
|
1004
1014
|
|
|
@@ -1144,6 +1154,7 @@ function buildReportsHtml(safeDefault, appVersion) {
|
|
|
1144
1154
|
<div class="railNav">
|
|
1145
1155
|
<button class="railBtn" id="railDashboard" type="button" title="Dashboard">D</button>
|
|
1146
1156
|
<button class="railBtn active" id="railReports" type="button" title="Relatórios">R</button>
|
|
1157
|
+
<button class="railBtn" id="railHealth" type="button" title="Saude">H</button>
|
|
1147
1158
|
</div>
|
|
1148
1159
|
<div class="railBottom">
|
|
1149
1160
|
<div class="railStatus" id="railStatus" title="status"></div>
|
|
@@ -1195,6 +1206,87 @@ function buildReportsHtml(safeDefault, appVersion) {
|
|
|
1195
1206
|
</script>
|
|
1196
1207
|
<script src="/app.js"></script>
|
|
1197
1208
|
</body>
|
|
1209
|
+
</html>`
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function buildHealthHtml(safeDefault, appVersion) {
|
|
1213
|
+
const safeVersion = escapeHtml(appVersion || 'unknown');
|
|
1214
|
+
return `<!doctype html>
|
|
1215
|
+
<html>
|
|
1216
|
+
<head>
|
|
1217
|
+
<meta charset="utf-8" />
|
|
1218
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1219
|
+
<title>FREYA Health</title>
|
|
1220
|
+
<link rel="stylesheet" href="/app.css" />
|
|
1221
|
+
</head>
|
|
1222
|
+
<body data-page="health">
|
|
1223
|
+
<div class="app">
|
|
1224
|
+
<div class="frame">
|
|
1225
|
+
<div class="shell">
|
|
1226
|
+
|
|
1227
|
+
<aside class="rail">
|
|
1228
|
+
<div class="railTop">
|
|
1229
|
+
<div class="railLogo">F</div>
|
|
1230
|
+
</div>
|
|
1231
|
+
<div class="railNav">
|
|
1232
|
+
<button class="railBtn" id="railDashboard" type="button" title="Dashboard">D</button>
|
|
1233
|
+
<button class="railBtn" id="railReports" type="button" title="Relatórios">R</button>
|
|
1234
|
+
<button class="railBtn active" id="railHealth" type="button" title="Saude">H</button>
|
|
1235
|
+
</div>
|
|
1236
|
+
<div class="railBottom">
|
|
1237
|
+
<div class="railStatus" id="railStatus" title="status"></div>
|
|
1238
|
+
</div>
|
|
1239
|
+
</aside>
|
|
1240
|
+
|
|
1241
|
+
<main class="center reportsPage" id="healthPage">
|
|
1242
|
+
<div class="topbar">
|
|
1243
|
+
<div class="brandLine">
|
|
1244
|
+
<span class="spark"></span>
|
|
1245
|
+
<div class="brandStack">
|
|
1246
|
+
<div class="brand">FREYA</div>
|
|
1247
|
+
<div class="brandSub">Checklist de Saude</div>
|
|
1248
|
+
</div>
|
|
1249
|
+
</div>
|
|
1250
|
+
<div class="topActions">
|
|
1251
|
+
<span class="chip" id="chipVersion">v${safeVersion}</span>
|
|
1252
|
+
<span class="chip" id="chipPort">127.0.0.1:3872</span>
|
|
1253
|
+
</div>
|
|
1254
|
+
</div>
|
|
1255
|
+
|
|
1256
|
+
<div class="centerBody">
|
|
1257
|
+
<input id="dir" type="hidden" />
|
|
1258
|
+
|
|
1259
|
+
<section class="reportsHeader">
|
|
1260
|
+
<div>
|
|
1261
|
+
<div class="reportsTitle">Checklist de Saude</div>
|
|
1262
|
+
<div class="reportsSubtitle">Verificacoes rapidas antes de reunioes e rituais.</div>
|
|
1263
|
+
</div>
|
|
1264
|
+
<div class="reportsActions">
|
|
1265
|
+
<button class="btn small" type="button" onclick="refreshHealthChecklist()">Atualizar</button>
|
|
1266
|
+
<button class="btn small" type="button" onclick="doHealth()">Rodar health</button>
|
|
1267
|
+
</div>
|
|
1268
|
+
</section>
|
|
1269
|
+
|
|
1270
|
+
<section class="reportsTools" style="grid-template-columns: repeat(auto-fit,minmax(180px,1fr));">
|
|
1271
|
+
<button class="btn" type="button" onclick="runReport('daily')">Gerar Daily</button>
|
|
1272
|
+
<button class="btn" type="button" onclick="runReport('blockers')">Gerar Blockers</button>
|
|
1273
|
+
<button class="btn" type="button" onclick="runReport('status')">Gerar Status</button>
|
|
1274
|
+
<button class="btn" type="button" onclick="runReport('sm-weekly')">Gerar SM Weekly</button>
|
|
1275
|
+
</section>
|
|
1276
|
+
|
|
1277
|
+
<section class="reportsGrid" id="healthChecklist"></section>
|
|
1278
|
+
</div>
|
|
1279
|
+
</main>
|
|
1280
|
+
|
|
1281
|
+
</div>
|
|
1282
|
+
</div>
|
|
1283
|
+
</div>
|
|
1284
|
+
|
|
1285
|
+
<script>
|
|
1286
|
+
window.__FREYA_DEFAULT_DIR = "${safeDefault}";
|
|
1287
|
+
</script>
|
|
1288
|
+
<script src="/app.js"></script>
|
|
1289
|
+
</body>
|
|
1198
1290
|
</html>`;
|
|
1199
1291
|
}
|
|
1200
1292
|
|
|
@@ -1511,6 +1603,14 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1511
1603
|
return;
|
|
1512
1604
|
}
|
|
1513
1605
|
|
|
1606
|
+
if (req.method === 'GET' && req.url === '/health') {
|
|
1607
|
+
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch {}
|
|
1608
|
+
const body = healthHtml(dir || './freya');
|
|
1609
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
1610
|
+
res.end(body);
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1514
1614
|
if (req.method === 'GET' && req.url === '/app.css') {
|
|
1515
1615
|
const css = fs.readFileSync(path.join(__dirname, 'web-ui.css'), 'utf8');
|
|
1516
1616
|
res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
@@ -1525,12 +1625,23 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1525
1625
|
return;
|
|
1526
1626
|
}
|
|
1527
1627
|
|
|
1628
|
+
if (req.method === 'GET' && req.url === '/favicon.ico') {
|
|
1629
|
+
res.writeHead(204, { 'Cache-Control': 'no-store' });
|
|
1630
|
+
res.end();
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1528
1634
|
if (req.url.startsWith('/api/')) {
|
|
1529
1635
|
const raw = await readBody(req);
|
|
1530
1636
|
const payload = raw ? JSON.parse(raw) : {};
|
|
1531
1637
|
|
|
1532
1638
|
const requestedDir = payload.dir || dir || './freya';
|
|
1533
|
-
|
|
1639
|
+
let workspaceDir;
|
|
1640
|
+
try {
|
|
1641
|
+
workspaceDir = normalizeWorkspaceDir(requestedDir);
|
|
1642
|
+
} catch {
|
|
1643
|
+
workspaceDir = path.resolve(process.cwd(), requestedDir);
|
|
1644
|
+
}
|
|
1534
1645
|
|
|
1535
1646
|
// debug logging (always on)
|
|
1536
1647
|
try {
|
|
@@ -2035,7 +2146,15 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2035
2146
|
|
|
2036
2147
|
if (req.url === '/api/init') {
|
|
2037
2148
|
try {
|
|
2038
|
-
const
|
|
2149
|
+
const requestedInitDir = String(payload.dir || '').trim() || './freya';
|
|
2150
|
+
let initDir;
|
|
2151
|
+
try {
|
|
2152
|
+
initDir = normalizeWorkspaceDir(requestedInitDir);
|
|
2153
|
+
} catch {
|
|
2154
|
+
initDir = path.resolve(process.cwd(), requestedInitDir);
|
|
2155
|
+
}
|
|
2156
|
+
res.__freyaDebug.workspaceDir = initDir;
|
|
2157
|
+
const { output } = await initWorkspace({ targetDir: initDir, force: false, forceData: false, forceLogs: false });
|
|
2039
2158
|
return safeJson(res, 200, { output: String(output || '').trim() });
|
|
2040
2159
|
} catch (e) {
|
|
2041
2160
|
const message = e && e.message ? e.message : String(e);
|
|
@@ -2056,6 +2175,9 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2056
2175
|
const npmCmd = guessNpmCmd();
|
|
2057
2176
|
|
|
2058
2177
|
if (req.url === '/api/health') {
|
|
2178
|
+
if (!looksLikeFreyaWorkspace(workspaceDir)) {
|
|
2179
|
+
return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized' });
|
|
2180
|
+
}
|
|
2059
2181
|
const r = await run(npmCmd, ['run', 'health'], workspaceDir);
|
|
2060
2182
|
const output = (r.stdout + r.stderr).trim();
|
|
2061
2183
|
return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { output } : { error: output || 'health failed', output });
|
|
@@ -2282,6 +2404,72 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2282
2404
|
return safeJson(res, 200, { ok: true, task: updated });
|
|
2283
2405
|
}
|
|
2284
2406
|
|
|
2407
|
+
if (req.url === '/api/health/checklist') {
|
|
2408
|
+
if (!looksLikeFreyaWorkspace(workspaceDir)) {
|
|
2409
|
+
return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized', items: [] });
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
const tasksFile = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
|
|
2413
|
+
const blockersFile = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
|
|
2414
|
+
const tasksDoc = readJsonOrNull(tasksFile) || { schemaVersion: 1, tasks: [] };
|
|
2415
|
+
const blockersDoc = readJsonOrNull(blockersFile) || { schemaVersion: 1, blockers: [] };
|
|
2416
|
+
|
|
2417
|
+
const pendingTasks = (tasksDoc.tasks || []).filter((t) => t && t.status === 'PENDING' && t.category === 'DO_NOW').length;
|
|
2418
|
+
const openBlockers = (blockersDoc.blockers || []).filter((b) => b && String(b.status || '').trim() === 'OPEN').length;
|
|
2419
|
+
|
|
2420
|
+
const today = isoDate();
|
|
2421
|
+
const reports = listReports(workspaceDir);
|
|
2422
|
+
const reportsToday = reports.filter((r) => r && String(r.name || '').includes(today)).length;
|
|
2423
|
+
|
|
2424
|
+
const items = [
|
|
2425
|
+
{ label: 'Workspace inicializada', status: 'ok', detail: 'ok' },
|
|
2426
|
+
{ label: 'Blockers abertos', status: openBlockers > 0 ? 'warn' : 'ok', detail: `${openBlockers} aberto(s)` },
|
|
2427
|
+
{ label: 'Tarefas DO_NOW pendentes', status: pendingTasks > 0 ? 'warn' : 'ok', detail: `${pendingTasks} pendente(s)` },
|
|
2428
|
+
{ label: 'Relatorios de hoje', status: reportsToday > 0 ? 'ok' : 'warn', detail: `${reportsToday} gerado(s)` }
|
|
2429
|
+
];
|
|
2430
|
+
|
|
2431
|
+
return safeJson(res, 200, { ok: true, items, stats: { pendingTasks, openBlockers, reportsToday, reportsTotal: reports.length } });
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
if (req.url === '/api/blockers/summary') {
|
|
2435
|
+
const file = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
|
|
2436
|
+
const doc = readJsonOrNull(file) || { schemaVersion: 1, blockers: [] };
|
|
2437
|
+
const blockers = Array.isArray(doc.blockers) ? doc.blockers : [];
|
|
2438
|
+
|
|
2439
|
+
const open = blockers.filter((b) => b && String(b.status || '').trim() === 'OPEN');
|
|
2440
|
+
const sevCount = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0, OTHER: 0 };
|
|
2441
|
+
const now = Date.now();
|
|
2442
|
+
let older7 = 0;
|
|
2443
|
+
let missingSlug = 0;
|
|
2444
|
+
|
|
2445
|
+
for (const b of open) {
|
|
2446
|
+
const sev = String(b.severity || '').toUpperCase();
|
|
2447
|
+
if (sevCount[sev] !== undefined) sevCount[sev]++; else sevCount.OTHER++;
|
|
2448
|
+
if (!b.projectSlug) missingSlug++;
|
|
2449
|
+
const created = b.createdAt ? Date.parse(b.createdAt) : null;
|
|
2450
|
+
if (created && now - created > 7 * 24 * 60 * 60 * 1000) older7++;
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
const top = open
|
|
2454
|
+
.sort((a, b) => String(a.createdAt || '').localeCompare(String(b.createdAt || '')))
|
|
2455
|
+
.slice(0, 5)
|
|
2456
|
+
.map((b) => ({
|
|
2457
|
+
id: b.id,
|
|
2458
|
+
title: b.title || b.description || 'Sem titulo',
|
|
2459
|
+
severity: b.severity || 'UNKNOWN',
|
|
2460
|
+
projectSlug: b.projectSlug || null
|
|
2461
|
+
}));
|
|
2462
|
+
|
|
2463
|
+
const suggestions = [];
|
|
2464
|
+
if (sevCount.CRITICAL > 0) suggestions.push('Escalar blockers CRITICAL agora e definir dono/ETA.');
|
|
2465
|
+
if (sevCount.HIGH > 0) suggestions.push('Agendar alinhamento rapido para blockers HIGH.');
|
|
2466
|
+
if (missingSlug > 0) suggestions.push('Adicionar projectSlug nos blockers sem contexto.');
|
|
2467
|
+
if (older7 > 0) suggestions.push('Revisar blockers com mais de 7 dias e decidir: resolver ou descartar.');
|
|
2468
|
+
if (!suggestions.length) suggestions.push('Nenhuma acao urgente. Monitorar diariamente.');
|
|
2469
|
+
|
|
2470
|
+
const summary = `CRITICAL: ${sevCount.CRITICAL}, HIGH: ${sevCount.HIGH}, MEDIUM: ${sevCount.MEDIUM}, LOW: ${sevCount.LOW}, TOTAL: ${open.length}`;
|
|
2471
|
+
return safeJson(res, 200, { ok: true, summary, counts: sevCount, suggestions, top });
|
|
2472
|
+
}
|
|
2285
2473
|
if (req.url === '/api/blockers/list') {
|
|
2286
2474
|
const limit = Math.max(1, Math.min(50, Number(payload.limit || 10)));
|
|
2287
2475
|
const status = payload.status ? String(payload.status).trim() : 'OPEN';
|
package/package.json
CHANGED
|
@@ -113,6 +113,7 @@ function generateReport() {
|
|
|
113
113
|
const now = new Date();
|
|
114
114
|
const nowMs = now.getTime();
|
|
115
115
|
const reportDate = toIsoDate(now);
|
|
116
|
+
const reportTime = (() => { const d = new Date(); const hh = String(d.getHours()).padStart(2,'0'); const mm = String(d.getMinutes()).padStart(2,'0'); const ss = String(d.getSeconds()).padStart(2,'0'); return `${hh}${mm}${ss}`; })();
|
|
116
117
|
const blockers = loadBlockers();
|
|
117
118
|
|
|
118
119
|
const statusCounts = new Map();
|
|
@@ -207,7 +208,7 @@ function generateReport() {
|
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
ensureReportDir();
|
|
210
|
-
const outputPath = path.join(REPORT_DIR, `blockers-${reportDate}.md`);
|
|
211
|
+
const outputPath = path.join(REPORT_DIR, `blockers-${reportDate}-${reportTime}.md`);
|
|
211
212
|
fs.writeFileSync(outputPath, report);
|
|
212
213
|
console.log(report);
|
|
213
214
|
}
|
|
@@ -93,7 +93,8 @@ function generateDailySummary() {
|
|
|
93
93
|
try {
|
|
94
94
|
fs.mkdirSync(REPORT_DIR, { recursive: true });
|
|
95
95
|
const date = new Date().toISOString().slice(0, 10);
|
|
96
|
-
const
|
|
96
|
+
const time = new Date().toTimeString().slice(0, 8).replace(/:/g, '');
|
|
97
|
+
const outPath = path.join(REPORT_DIR, `daily-${date}-${time}.md`);
|
|
97
98
|
fs.writeFileSync(outPath, `# Daily Summary — ${date}\n\n${summary}\n`, 'utf8');
|
|
98
99
|
} catch (e) {
|
|
99
100
|
// non-fatal
|
|
@@ -407,7 +407,8 @@ function generateReport(period) {
|
|
|
407
407
|
|
|
408
408
|
// Save
|
|
409
409
|
ensureDir(OUTPUT_DIR);
|
|
410
|
-
const
|
|
410
|
+
const timeStr = new Date().toTimeString().slice(0, 8).replace(/:/g, '');
|
|
411
|
+
const filename = `executive-${period}-${dateStr}-${timeStr}.md`;
|
|
411
412
|
const outputPath = path.join(OUTPUT_DIR, filename);
|
|
412
413
|
fs.writeFileSync(outputPath, md, 'utf8');
|
|
413
414
|
|
|
@@ -67,6 +67,7 @@ function generate() {
|
|
|
67
67
|
start.setDate(now.getDate() - 7);
|
|
68
68
|
|
|
69
69
|
const reportDate = toIsoDate(now);
|
|
70
|
+
const reportTime = (() => { const d = new Date(); const hh = String(d.getHours()).padStart(2,'0'); const mm = String(d.getMinutes()).padStart(2,'0'); const ss = String(d.getSeconds()).padStart(2,'0'); return `${hh}${mm}${ss}`; })();
|
|
70
71
|
|
|
71
72
|
const taskLog = readJsonOrQuarantine(TASKS_FILE, { schemaVersion: 1, tasks: [] });
|
|
72
73
|
const blockersLog = readJsonOrQuarantine(BLOCKERS_FILE, { schemaVersion: 1, blockers: [] });
|
|
@@ -198,7 +199,7 @@ function generate() {
|
|
|
198
199
|
}
|
|
199
200
|
}
|
|
200
201
|
|
|
201
|
-
const outPath = path.join(REPORT_DIR, `sm-weekly-${reportDate}.md`);
|
|
202
|
+
const outPath = path.join(REPORT_DIR, `sm-weekly-${reportDate}-${reportTime}.md`);
|
|
202
203
|
fs.writeFileSync(outPath, md, 'utf8');
|
|
203
204
|
console.log(md);
|
|
204
205
|
console.log(`\nSaved: ${outPath}`);
|
|
@@ -24,6 +24,13 @@ function getFormattedDate() {
|
|
|
24
24
|
return toIsoDate(now);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function getFormattedTime() {
|
|
28
|
+
const hh = String(now.getHours()).padStart(2, '0');
|
|
29
|
+
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
30
|
+
const ss = String(now.getSeconds()).padStart(2, '0');
|
|
31
|
+
return `${hh}${mm}${ss}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
// --- File Walking ---
|
|
28
35
|
function walk(dir, fileList = []) {
|
|
29
36
|
const files = fs.readdirSync(dir);
|
|
@@ -69,6 +76,7 @@ function generateWeeklyReport() {
|
|
|
69
76
|
|
|
70
77
|
// 2. Generate Content
|
|
71
78
|
const reportDate = getFormattedDate();
|
|
79
|
+
const reportTime = getFormattedTime();
|
|
72
80
|
let report = `# Weekly Report - ${reportDate}\n\n`;
|
|
73
81
|
|
|
74
82
|
// Projects
|
|
@@ -122,7 +130,7 @@ function generateWeeklyReport() {
|
|
|
122
130
|
}
|
|
123
131
|
|
|
124
132
|
// 3. Save and Output
|
|
125
|
-
const outputPath = path.join(REPORT_DIR, `weekly-${reportDate}.md`);
|
|
133
|
+
const outputPath = path.join(REPORT_DIR, `weekly-${reportDate}-${reportTime}.md`);
|
|
126
134
|
fs.writeFileSync(outputPath, report);
|
|
127
135
|
|
|
128
136
|
console.log(`✅ Report generated at: ${outputPath}`);
|
|
@@ -113,6 +113,7 @@ function generateReport() {
|
|
|
113
113
|
const now = new Date();
|
|
114
114
|
const nowMs = now.getTime();
|
|
115
115
|
const reportDate = toIsoDate(now);
|
|
116
|
+
const reportTime = (() => { const d = new Date(); const hh = String(d.getHours()).padStart(2,'0'); const mm = String(d.getMinutes()).padStart(2,'0'); const ss = String(d.getSeconds()).padStart(2,'0'); return `${hh}${mm}${ss}`; })();
|
|
116
117
|
const blockers = loadBlockers();
|
|
117
118
|
|
|
118
119
|
const statusCounts = new Map();
|
|
@@ -207,7 +208,7 @@ function generateReport() {
|
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
ensureReportDir();
|
|
210
|
-
const outputPath = path.join(REPORT_DIR, `blockers-${reportDate}.md`);
|
|
211
|
+
const outputPath = path.join(REPORT_DIR, `blockers-${reportDate}-${reportTime}.md`);
|
|
211
212
|
fs.writeFileSync(outputPath, report);
|
|
212
213
|
console.log(report);
|
|
213
214
|
}
|
|
@@ -93,7 +93,8 @@ function generateDailySummary() {
|
|
|
93
93
|
try {
|
|
94
94
|
fs.mkdirSync(REPORT_DIR, { recursive: true });
|
|
95
95
|
const date = new Date().toISOString().slice(0, 10);
|
|
96
|
-
const
|
|
96
|
+
const time = new Date().toTimeString().slice(0, 8).replace(/:/g, '');
|
|
97
|
+
const outPath = path.join(REPORT_DIR, `daily-${date}-${time}.md`);
|
|
97
98
|
fs.writeFileSync(outPath, `# Daily Summary — ${date}\n\n${summary}\n`, 'utf8');
|
|
98
99
|
} catch (e) {
|
|
99
100
|
// non-fatal
|
|
@@ -407,7 +407,8 @@ function generateReport(period) {
|
|
|
407
407
|
|
|
408
408
|
// Save
|
|
409
409
|
ensureDir(OUTPUT_DIR);
|
|
410
|
-
const
|
|
410
|
+
const timeStr = new Date().toTimeString().slice(0, 8).replace(/:/g, '');
|
|
411
|
+
const filename = `executive-${period}-${dateStr}-${timeStr}.md`;
|
|
411
412
|
const outputPath = path.join(OUTPUT_DIR, filename);
|
|
412
413
|
fs.writeFileSync(outputPath, md, 'utf8');
|
|
413
414
|
|
|
@@ -67,6 +67,7 @@ function generate() {
|
|
|
67
67
|
start.setDate(now.getDate() - 7);
|
|
68
68
|
|
|
69
69
|
const reportDate = toIsoDate(now);
|
|
70
|
+
const reportTime = (() => { const d = new Date(); const hh = String(d.getHours()).padStart(2,'0'); const mm = String(d.getMinutes()).padStart(2,'0'); const ss = String(d.getSeconds()).padStart(2,'0'); return `${hh}${mm}${ss}`; })();
|
|
70
71
|
|
|
71
72
|
const taskLog = readJsonOrQuarantine(TASKS_FILE, { schemaVersion: 1, tasks: [] });
|
|
72
73
|
const blockersLog = readJsonOrQuarantine(BLOCKERS_FILE, { schemaVersion: 1, blockers: [] });
|
|
@@ -198,7 +199,7 @@ function generate() {
|
|
|
198
199
|
}
|
|
199
200
|
}
|
|
200
201
|
|
|
201
|
-
const outPath = path.join(REPORT_DIR, `sm-weekly-${reportDate}.md`);
|
|
202
|
+
const outPath = path.join(REPORT_DIR, `sm-weekly-${reportDate}-${reportTime}.md`);
|
|
202
203
|
fs.writeFileSync(outPath, md, 'utf8');
|
|
203
204
|
console.log(md);
|
|
204
205
|
console.log(`\nSaved: ${outPath}`);
|
|
@@ -24,6 +24,13 @@ function getFormattedDate() {
|
|
|
24
24
|
return toIsoDate(now);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function getFormattedTime() {
|
|
28
|
+
const hh = String(now.getHours()).padStart(2, '0');
|
|
29
|
+
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
30
|
+
const ss = String(now.getSeconds()).padStart(2, '0');
|
|
31
|
+
return `${hh}${mm}${ss}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
// --- File Walking ---
|
|
28
35
|
function walk(dir, fileList = []) {
|
|
29
36
|
const files = fs.readdirSync(dir);
|
|
@@ -69,6 +76,7 @@ function generateWeeklyReport() {
|
|
|
69
76
|
|
|
70
77
|
// 2. Generate Content
|
|
71
78
|
const reportDate = getFormattedDate();
|
|
79
|
+
const reportTime = getFormattedTime();
|
|
72
80
|
let report = `# Weekly Report - ${reportDate}\n\n`;
|
|
73
81
|
|
|
74
82
|
// Projects
|
|
@@ -122,7 +130,7 @@ function generateWeeklyReport() {
|
|
|
122
130
|
}
|
|
123
131
|
|
|
124
132
|
// 3. Save and Output
|
|
125
|
-
const outputPath = path.join(REPORT_DIR, `weekly-${reportDate}.md`);
|
|
133
|
+
const outputPath = path.join(REPORT_DIR, `weekly-${reportDate}-${reportTime}.md`);
|
|
126
134
|
fs.writeFileSync(outputPath, report);
|
|
127
135
|
|
|
128
136
|
console.log(`✅ Report generated at: ${outputPath}`);
|