@cccarv82/freya 2.14.0 → 2.15.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 +341 -25
- package/cli/web-ui.js +460 -156
- package/cli/web.js +713 -236
- package/package.json +3 -3
- package/scripts/lib/DataLayer.js +10 -1
- package/scripts/lib/DataManager.js +1 -0
- package/scripts/lib/Embedder.js +5 -1
- package/templates/base/scripts/lib/DataLayer.js +11 -2
- package/templates/base/scripts/lib/DataManager.js +15 -14
- package/templates/base/scripts/lib/Embedder.js +5 -1
package/cli/web.js
CHANGED
|
@@ -624,7 +624,7 @@ function safeJson(res, code, obj) {
|
|
|
624
624
|
function looksEmptyWorkspace(dir) {
|
|
625
625
|
try {
|
|
626
626
|
if (!exists(dir)) return true;
|
|
627
|
-
const entries = fs.readdirSync(dir).filter((n) => !['.debuglogs', '.DS_Store'].includes(n));
|
|
627
|
+
const entries = fs.readdirSync(dir).filter((n) => !['.debuglogs', '.DS_Store', 'data', 'logs'].includes(n));
|
|
628
628
|
return entries.length === 0;
|
|
629
629
|
} catch {
|
|
630
630
|
return true;
|
|
@@ -983,6 +983,11 @@ function docsHtml(defaultDir) {
|
|
|
983
983
|
return buildDocsHtml(safeDefault, APP_VERSION);
|
|
984
984
|
}
|
|
985
985
|
|
|
986
|
+
function kanbanHtml(defaultDir) {
|
|
987
|
+
const safeDefault = String(defaultDir || './freya').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
988
|
+
return buildKanbanHtml(safeDefault, APP_VERSION);
|
|
989
|
+
}
|
|
990
|
+
|
|
986
991
|
function buildDocsHtml(safeDefault, appVersion) {
|
|
987
992
|
const safeVersion = escapeHtml(appVersion || 'unknown');
|
|
988
993
|
return `<!doctype html>
|
|
@@ -1035,6 +1040,9 @@ function buildDocsHtml(safeDefault, appVersion) {
|
|
|
1035
1040
|
<button class="railBtn" id="railCompanion" type="button" title="Companion">
|
|
1036
1041
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>
|
|
1037
1042
|
</button>
|
|
1043
|
+
<button class="railBtn" id="railKanban" type="button" title="Kanban">
|
|
1044
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="5" height="18" rx="1"></rect><rect x="10" y="3" width="5" height="12" rx="1"></rect><rect x="17" y="3" width="5" height="15" rx="1"></rect></svg>
|
|
1045
|
+
</button>
|
|
1038
1046
|
<button class="railBtn" id="railProjects" type="button" title="Projects">
|
|
1039
1047
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
|
|
1040
1048
|
</button>
|
|
@@ -1174,6 +1182,9 @@ function buildHtml(safeDefault, appVersion) {
|
|
|
1174
1182
|
<button class="railBtn" id="railCompanion" type="button" title="Companion">
|
|
1175
1183
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>
|
|
1176
1184
|
</button>
|
|
1185
|
+
<button class="railBtn" id="railKanban" type="button" title="Kanban">
|
|
1186
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="5" height="18" rx="1"></rect><rect x="10" y="3" width="5" height="12" rx="1"></rect><rect x="17" y="3" width="5" height="15" rx="1"></rect></svg>
|
|
1187
|
+
</button>
|
|
1177
1188
|
<button class="railBtn" id="railProjects" type="button" title="Projects">
|
|
1178
1189
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
|
|
1179
1190
|
</button>
|
|
@@ -1209,7 +1220,7 @@ function buildHtml(safeDefault, appVersion) {
|
|
|
1209
1220
|
<div class="centerBody">
|
|
1210
1221
|
<!-- Unified Input Panel -->
|
|
1211
1222
|
<div class="promptShell">
|
|
1212
|
-
<div class="promptBar" style="border-radius: 16px; display: flex; flex-direction: column; overflow: hidden;">
|
|
1223
|
+
<div class="promptBar" style="border-radius: 16px; display: flex; flex-direction: column; overflow: hidden; flex: 1;">
|
|
1213
1224
|
<!-- Header -->
|
|
1214
1225
|
<div class="promptMeta" style="flex-shrink:0;">
|
|
1215
1226
|
<div class="promptTitle" style="display: flex; align-items: center; gap: 8px;">
|
|
@@ -1223,7 +1234,7 @@ function buildHtml(safeDefault, appVersion) {
|
|
|
1223
1234
|
</div>
|
|
1224
1235
|
|
|
1225
1236
|
<!-- Textarea -->
|
|
1226
|
-
<textarea id="inboxText" aria-label="Entrada de texto para processar ou perguntar" placeholder="Cole updates, decisões, blockers... ou faça uma pergunta à Freya. ▸ Salvar & Processar → extrai tarefas e blockers do texto ▸ Perguntar → consulta o histórico via busca semântica (RAG)" style="resize:none; min-height:
|
|
1237
|
+
<textarea id="inboxText" aria-label="Entrada de texto para processar ou perguntar" placeholder="Cole updates, decisões, blockers... ou faça uma pergunta à Freya. ▸ Salvar & Processar → extrai tarefas e blockers do texto ▸ Perguntar → consulta o histórico via busca semântica (RAG)" style="resize:none; min-height: 200px; flex: 1; border-radius: 0; border-left: none; border-right: none; border-top: none; border-bottom: 1px solid var(--border); padding: 14px 16px; font-size: 13px; line-height: 1.6;"
|
|
1227
1238
|
onkeydown="if((event.metaKey||event.ctrlKey)&&event.key==='Enter'){event.preventDefault();window.saveAndPlan();}"></textarea>
|
|
1228
1239
|
|
|
1229
1240
|
<!-- Actions bar -->
|
|
@@ -1257,7 +1268,7 @@ function buildHtml(safeDefault, appVersion) {
|
|
|
1257
1268
|
</div>
|
|
1258
1269
|
|
|
1259
1270
|
<!-- Chat thread: responses appear here after actions -->
|
|
1260
|
-
<div id="chatThread" style="max-height:
|
|
1271
|
+
<div id="chatThread" style="flex: 0 1 auto; min-height: 0; max-height: 40vh; overflow-y:auto; overflow-x:hidden; padding:0 12px; display:none; flex-direction:column; gap:8px; border-top: 1px solid var(--border);"></div>
|
|
1261
1272
|
</div>
|
|
1262
1273
|
</div>
|
|
1263
1274
|
|
|
@@ -1317,6 +1328,39 @@ function buildHtml(safeDefault, appVersion) {
|
|
|
1317
1328
|
</div>
|
|
1318
1329
|
</div>
|
|
1319
1330
|
|
|
1331
|
+
<!-- Quick-add modal (Ctrl+K) -->
|
|
1332
|
+
<div id="quickAddOverlay" class="qa-overlay" style="display:none;">
|
|
1333
|
+
<div class="qa-modal">
|
|
1334
|
+
<div class="qa-header">
|
|
1335
|
+
<span style="font-weight:700; font-size:14px;">Nova Task</span>
|
|
1336
|
+
<button class="btn small" type="button" onclick="window.closeQuickAdd()" style="padding:2px 8px;">×</button>
|
|
1337
|
+
</div>
|
|
1338
|
+
<textarea id="qaDesc" class="qa-input" placeholder="Descri\u00e7\u00e3o da task..." rows="3"></textarea>
|
|
1339
|
+
<div class="qa-row">
|
|
1340
|
+
<select id="qaCat" class="qa-select">
|
|
1341
|
+
<option value="DO_NOW">DO_NOW</option>
|
|
1342
|
+
<option value="SCHEDULE">SCHEDULE</option>
|
|
1343
|
+
<option value="DELEGATE">DELEGATE</option>
|
|
1344
|
+
</select>
|
|
1345
|
+
<select id="qaPriority" class="qa-select">
|
|
1346
|
+
<option value="">Prioridade</option>
|
|
1347
|
+
<option value="critical">Cr\u00edtica</option>
|
|
1348
|
+
<option value="high">Alta</option>
|
|
1349
|
+
<option value="medium">M\u00e9dia</option>
|
|
1350
|
+
<option value="low">Baixa</option>
|
|
1351
|
+
</select>
|
|
1352
|
+
</div>
|
|
1353
|
+
<div class="qa-row">
|
|
1354
|
+
<input id="qaSlug" class="qa-input" placeholder="Projeto (slug)" style="flex:1;" />
|
|
1355
|
+
<input id="qaDue" type="date" class="qa-input" style="width:160px;" />
|
|
1356
|
+
</div>
|
|
1357
|
+
<div class="qa-row" style="justify-content:flex-end;">
|
|
1358
|
+
<button class="btn small" type="button" onclick="window.closeQuickAdd()">Cancelar</button>
|
|
1359
|
+
<button class="btn primary small" type="button" onclick="window.submitQuickAdd()">Criar Task</button>
|
|
1360
|
+
</div>
|
|
1361
|
+
</div>
|
|
1362
|
+
</div>
|
|
1363
|
+
|
|
1320
1364
|
<script>
|
|
1321
1365
|
window.__FREYA_DEFAULT_DIR = "${safeDefault}";
|
|
1322
1366
|
</script>
|
|
@@ -1354,6 +1398,9 @@ function buildReportsHtml(safeDefault, appVersion) {
|
|
|
1354
1398
|
<button class="railBtn" id="railCompanion" type="button" title="Companion">
|
|
1355
1399
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>
|
|
1356
1400
|
</button>
|
|
1401
|
+
<button class="railBtn" id="railKanban" type="button" title="Kanban">
|
|
1402
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="5" height="18" rx="1"></rect><rect x="10" y="3" width="5" height="12" rx="1"></rect><rect x="17" y="3" width="5" height="15" rx="1"></rect></svg>
|
|
1403
|
+
</button>
|
|
1357
1404
|
<button class="railBtn" id="railProjects" type="button" title="Projects">
|
|
1358
1405
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
|
|
1359
1406
|
</button>
|
|
@@ -1467,6 +1514,9 @@ function buildProjectsHtml(safeDefault, appVersion) {
|
|
|
1467
1514
|
<button class="railBtn" id="railCompanion" type="button" title="Companion">
|
|
1468
1515
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>
|
|
1469
1516
|
</button>
|
|
1517
|
+
<button class="railBtn" id="railKanban" type="button" title="Kanban">
|
|
1518
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="5" height="18" rx="1"></rect><rect x="10" y="3" width="5" height="12" rx="1"></rect><rect x="17" y="3" width="5" height="15" rx="1"></rect></svg>
|
|
1519
|
+
</button>
|
|
1470
1520
|
<button class="railBtn active" id="railProjects" type="button" title="Projects">
|
|
1471
1521
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
|
|
1472
1522
|
</button>
|
|
@@ -1560,6 +1610,9 @@ function buildTimelineHtml(safeDefault, appVersion) {
|
|
|
1560
1610
|
<button class=\"railBtn\" id=\"railCompanion\" type=\"button\" title=\"Companion\">
|
|
1561
1611
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>
|
|
1562
1612
|
</button>
|
|
1613
|
+
<button class=\"railBtn\" id=\"railKanban\" type=\"button\" title=\"Kanban\">
|
|
1614
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="5" height="18" rx="1"></rect><rect x="10" y="3" width="5" height="12" rx="1"></rect><rect x="17" y="3" width="5" height="15" rx="1"></rect></svg>
|
|
1615
|
+
</button>
|
|
1563
1616
|
<button class=\"railBtn\" id=\"railProjects\" type=\"button\" title=\"Projects\">
|
|
1564
1617
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
|
|
1565
1618
|
</button>
|
|
@@ -1669,6 +1722,9 @@ function buildGraphHtml(safeDefault, appVersion) {
|
|
|
1669
1722
|
<button class="railBtn" id="railCompanion" type="button" title="Companion">
|
|
1670
1723
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>
|
|
1671
1724
|
</button>
|
|
1725
|
+
<button class="railBtn" id="railKanban" type="button" title="Kanban">
|
|
1726
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="5" height="18" rx="1"></rect><rect x="10" y="3" width="5" height="12" rx="1"></rect><rect x="17" y="3" width="5" height="15" rx="1"></rect></svg>
|
|
1727
|
+
</button>
|
|
1672
1728
|
<button class="railBtn" id="railProjects" type="button" title="Projects">
|
|
1673
1729
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
|
|
1674
1730
|
</button>
|
|
@@ -1760,6 +1816,9 @@ function buildCompanionHtml(safeDefault, appVersion) {
|
|
|
1760
1816
|
<button class="railBtn active" id="railCompanion" type="button" title="Companion">
|
|
1761
1817
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>
|
|
1762
1818
|
</button>
|
|
1819
|
+
<button class="railBtn" id="railKanban" type="button" title="Kanban">
|
|
1820
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="5" height="18" rx="1"></rect><rect x="10" y="3" width="5" height="12" rx="1"></rect><rect x="17" y="3" width="5" height="15" rx="1"></rect></svg>
|
|
1821
|
+
</button>
|
|
1763
1822
|
<button class="railBtn" id="railProjects" type="button" title="Projects">
|
|
1764
1823
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
|
|
1765
1824
|
</button>
|
|
@@ -1797,40 +1856,55 @@ function buildCompanionHtml(safeDefault, appVersion) {
|
|
|
1797
1856
|
<section class="reportsHeader">
|
|
1798
1857
|
<div>
|
|
1799
1858
|
<div class="reportsTitle">Scrum Master Companion</div>
|
|
1800
|
-
<div class="reportsSubtitle">
|
|
1859
|
+
<div class="reportsSubtitle">Acompanhamento de projetos e frentes em tempo real.</div>
|
|
1801
1860
|
</div>
|
|
1802
1861
|
<div class="reportsActions">
|
|
1803
|
-
<button class="btn small" type="button" onclick="refreshCompanionDash()"
|
|
1862
|
+
<button class="btn small" type="button" onclick="refreshCompanionDash()">Atualizar Dashboard</button>
|
|
1804
1863
|
</div>
|
|
1805
1864
|
</section>
|
|
1806
1865
|
|
|
1807
|
-
|
|
1866
|
+
<button class="btn small" type="button" onclick="window.location.href='/reports'" style="margin-bottom: 16px;">
|
|
1867
|
+
📊 Ver / Gerar Relatórios →
|
|
1868
|
+
</button>
|
|
1869
|
+
|
|
1870
|
+
<!-- PROJECTS OVERVIEW -->
|
|
1808
1871
|
<div class="companionTabs">
|
|
1809
|
-
<button class="tab active" data-filter="all" onclick="filterCompanionView('all')">○ Todos</button>
|
|
1810
|
-
<button class="tab" data-filter="alerts" onclick="filterCompanionView('alerts')"
|
|
1811
|
-
<button class="tab" data-filter="risk" onclick="filterCompanionView('risk')"
|
|
1872
|
+
<button class="tab active" data-filter="all" onclick="window.filterCompanionView('all')">○ Todos</button>
|
|
1873
|
+
<button class="tab" data-filter="alerts" onclick="window.filterCompanionView('alerts')">● Alertas</button>
|
|
1874
|
+
<button class="tab" data-filter="risk" onclick="window.filterCompanionView('risk')">⚠ Risco Alto</button>
|
|
1812
1875
|
</div>
|
|
1813
1876
|
|
|
1814
1877
|
<!-- CONSOLIDATED VIEW -->
|
|
1815
|
-
<div id="
|
|
1878
|
+
<div id="consolidatedViewBox" style="display: none;">
|
|
1879
|
+
<section class="panel">
|
|
1880
|
+
<div class="panelHead"><b>Visão Consolidada</b></div>
|
|
1881
|
+
<div class="panelBody">
|
|
1882
|
+
<div class="consolidatedView" id="consolidatedView" style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px;"></div>
|
|
1883
|
+
</div>
|
|
1884
|
+
</section>
|
|
1885
|
+
</div>
|
|
1816
1886
|
|
|
1817
1887
|
<!-- PROJECT CARDS GRID -->
|
|
1818
|
-
<div id="
|
|
1888
|
+
<div id="projectCardsBox" style="display: none;">
|
|
1889
|
+
<div class="projectCardsGrid" id="projectCardsGrid"></div>
|
|
1890
|
+
</div>
|
|
1819
1891
|
|
|
1820
1892
|
<!-- STREAM BREAKDOWN -->
|
|
1821
|
-
<
|
|
1822
|
-
<
|
|
1823
|
-
|
|
1824
|
-
<div
|
|
1825
|
-
|
|
1826
|
-
|
|
1893
|
+
<div id="streamBreakdownBox" style="display: none;">
|
|
1894
|
+
<section class="panel">
|
|
1895
|
+
<div class="panelHead"><b>Detalhamento por Stream</b></div>
|
|
1896
|
+
<div class="panelBody">
|
|
1897
|
+
<div id="streamBreakdown"></div>
|
|
1898
|
+
</div>
|
|
1899
|
+
</section>
|
|
1900
|
+
</div>
|
|
1827
1901
|
|
|
1828
|
-
<!-- ALERTS
|
|
1829
|
-
<div id="
|
|
1902
|
+
<!-- ALERTS VIEW -->
|
|
1903
|
+
<div id="alertsViewBox" style="display: none;">
|
|
1830
1904
|
<section class="panel">
|
|
1831
|
-
<div class="panelHead"><b
|
|
1905
|
+
<div class="panelHead"><b>Alertas Prioritários</b></div>
|
|
1832
1906
|
<div class="panelBody">
|
|
1833
|
-
<div id="
|
|
1907
|
+
<div id="alertsView"></div>
|
|
1834
1908
|
</div>
|
|
1835
1909
|
</section>
|
|
1836
1910
|
</div>
|
|
@@ -1998,18 +2072,6 @@ function truncateText(text, maxLen) {
|
|
|
1998
2072
|
return str.slice(0, Math.max(0, maxLen - 3)).trimEnd() + '...';
|
|
1999
2073
|
}
|
|
2000
2074
|
|
|
2001
|
-
function formatAgo(timestampMs) {
|
|
2002
|
-
const ms = typeof timestampMs === 'number' ? timestampMs : (typeof timestampMs === 'string' ? Date.parse(timestampMs) : 0);
|
|
2003
|
-
if (!Number.isFinite(ms)) return 'unknown';
|
|
2004
|
-
const now = Date.now();
|
|
2005
|
-
const diffMs = now - ms;
|
|
2006
|
-
if (diffMs < 60000) return 'agora';
|
|
2007
|
-
if (diffMs < 3600000) return `${Math.floor(diffMs / 60000)}m`;
|
|
2008
|
-
if (diffMs < 86400000) return `${Math.floor(diffMs / 3600000)}h`;
|
|
2009
|
-
if (diffMs < 604800000) return `${Math.floor(diffMs / 86400000)}d`;
|
|
2010
|
-
return `${Math.floor(diffMs / 604800000)}w`;
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
2075
|
function getTimelineItems(workspaceDir) {
|
|
2014
2076
|
const items = [];
|
|
2015
2077
|
const dailyDir = path.join(workspaceDir, 'logs', 'daily');
|
|
@@ -2319,6 +2381,14 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2319
2381
|
return;
|
|
2320
2382
|
}
|
|
2321
2383
|
|
|
2384
|
+
if (req.method === 'GET' && req.url === '/kanban') {
|
|
2385
|
+
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
2386
|
+
const body = injectPort(kanbanHtml(dir || './freya'));
|
|
2387
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
2388
|
+
res.end(body);
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2322
2392
|
if (req.method === 'GET' && req.url === '/app.css') {
|
|
2323
2393
|
const css = fs.readFileSync(path.join(__dirname, 'web-ui.css'), 'utf8');
|
|
2324
2394
|
res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
@@ -2807,6 +2877,310 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2807
2877
|
return safeJson(res, 200, { ok: true, items });
|
|
2808
2878
|
}
|
|
2809
2879
|
|
|
2880
|
+
// /api/companion/projects-summary: Consolidate project KPIs
|
|
2881
|
+
if (req.url === '/api/companion/projects-summary') {
|
|
2882
|
+
if (!looksLikeFreyaWorkspace(workspaceDir)) {
|
|
2883
|
+
return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized' });
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
const now = Date.now();
|
|
2887
|
+
const projectMap = {}; // projectSlug -> { name, totalTasks, completedTasks, pendingTasks, blockers, streams, lastUpdateAgo, status }
|
|
2888
|
+
|
|
2889
|
+
// 1. Load tasks
|
|
2890
|
+
const taskFile = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
|
|
2891
|
+
if (exists(taskFile)) {
|
|
2892
|
+
const taskDoc = readJsonOrNull(taskFile) || { tasks: [] };
|
|
2893
|
+
const tasks = Array.isArray(taskDoc.tasks) ? taskDoc.tasks : [];
|
|
2894
|
+
|
|
2895
|
+
for (const t of tasks) {
|
|
2896
|
+
const slug = String(t.projectSlug || '').trim();
|
|
2897
|
+
if (!slug) continue;
|
|
2898
|
+
|
|
2899
|
+
if (!projectMap[slug]) {
|
|
2900
|
+
projectMap[slug] = {
|
|
2901
|
+
slug, name: slug, totalTasks: 0, completedTasks: 0, pendingTasks: 0,
|
|
2902
|
+
openBlockers: 0, blockersBySeverity: { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 },
|
|
2903
|
+
streams: new Set(), lastUpdateMs: now, status: 'ON_TRACK'
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
projectMap[slug].totalTasks++;
|
|
2908
|
+
if (t.status === 'COMPLETED') {
|
|
2909
|
+
projectMap[slug].completedTasks++;
|
|
2910
|
+
} else {
|
|
2911
|
+
projectMap[slug].pendingTasks++;
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
const streamSlug = String(t.streamSlug || '').trim();
|
|
2915
|
+
if (streamSlug) {
|
|
2916
|
+
projectMap[slug].streams.add(streamSlug);
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
if (t.createdAt) {
|
|
2920
|
+
const taskTime = Date.parse(t.createdAt);
|
|
2921
|
+
if (taskTime > projectMap[slug].lastUpdateMs) {
|
|
2922
|
+
projectMap[slug].lastUpdateMs = taskTime;
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
// 2. Load blockers
|
|
2929
|
+
const blockerFile = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
|
|
2930
|
+
if (exists(blockerFile)) {
|
|
2931
|
+
const blockerDoc = readJsonOrNull(blockerFile) || { blockers: [] };
|
|
2932
|
+
const blockers = Array.isArray(blockerDoc.blockers) ? blockerDoc.blockers : [];
|
|
2933
|
+
|
|
2934
|
+
for (const b of blockers) {
|
|
2935
|
+
const slug = String(b.projectSlug || '').trim();
|
|
2936
|
+
if (!slug) continue;
|
|
2937
|
+
|
|
2938
|
+
if (!projectMap[slug]) {
|
|
2939
|
+
projectMap[slug] = {
|
|
2940
|
+
slug, name: slug, totalTasks: 0, completedTasks: 0, pendingTasks: 0,
|
|
2941
|
+
openBlockers: 0, blockersBySeverity: { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 },
|
|
2942
|
+
streams: new Set(), lastUpdateMs: now, status: 'ON_TRACK'
|
|
2943
|
+
};
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
if (String(b.status || '').toUpperCase() === 'OPEN') {
|
|
2947
|
+
projectMap[slug].openBlockers++;
|
|
2948
|
+
const sev = String(b.severity || 'LOW').toUpperCase();
|
|
2949
|
+
if (projectMap[slug].blockersBySeverity[sev] !== undefined) {
|
|
2950
|
+
projectMap[slug].blockersBySeverity[sev]++;
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
if (b.createdAt) {
|
|
2955
|
+
const blockerTime = Date.parse(b.createdAt);
|
|
2956
|
+
if (blockerTime > projectMap[slug].lastUpdateMs) {
|
|
2957
|
+
projectMap[slug].lastUpdateMs = blockerTime;
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
// Helper: format time ago
|
|
2964
|
+
const formatAgo = (ms) => {
|
|
2965
|
+
const age = now - ms;
|
|
2966
|
+
const secs = Math.floor(age / 1000);
|
|
2967
|
+
if (secs < 60) return 'just now';
|
|
2968
|
+
const mins = Math.floor(secs / 60);
|
|
2969
|
+
if (mins < 60) return mins + 'm ago';
|
|
2970
|
+
const hours = Math.floor(mins / 60);
|
|
2971
|
+
if (hours < 24) return hours + 'h ago';
|
|
2972
|
+
const days = Math.floor(hours / 24);
|
|
2973
|
+
return days + 'd ago';
|
|
2974
|
+
};
|
|
2975
|
+
|
|
2976
|
+
// Calculate status and completion rate for each project
|
|
2977
|
+
const projects = [];
|
|
2978
|
+
for (const [slug, proj] of Object.entries(projectMap)) {
|
|
2979
|
+
const completionRate = proj.totalTasks > 0 ? Math.round((proj.completedTasks / proj.totalTasks) * 100) : 0;
|
|
2980
|
+
let status = 'ON_TRACK';
|
|
2981
|
+
|
|
2982
|
+
if (proj.blockersBySeverity.CRITICAL > 0) status = 'AT_RISK';
|
|
2983
|
+
if (proj.blockersBySeverity.HIGH >= 3) status = 'AT_RISK';
|
|
2984
|
+
if (proj.pendingTasks > 15) status = 'AT_RISK';
|
|
2985
|
+
|
|
2986
|
+
const ageDays = Math.floor((now - proj.lastUpdateMs) / (24 * 60 * 60 * 1000));
|
|
2987
|
+
if (ageDays > 14) status = 'IDLE';
|
|
2988
|
+
|
|
2989
|
+
projects.push({
|
|
2990
|
+
slug: proj.slug,
|
|
2991
|
+
name: proj.name,
|
|
2992
|
+
totalTasks: proj.totalTasks,
|
|
2993
|
+
completedTasks: proj.completedTasks,
|
|
2994
|
+
pendingTasks: proj.pendingTasks,
|
|
2995
|
+
completionRate,
|
|
2996
|
+
openBlockers: proj.openBlockers,
|
|
2997
|
+
blockersBySeverity: proj.blockersBySeverity,
|
|
2998
|
+
streams: Array.from(proj.streams),
|
|
2999
|
+
lastUpdateAgo: formatAgo(proj.lastUpdateMs),
|
|
3000
|
+
status
|
|
3001
|
+
});
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
return safeJson(res, 200, { ok: true, projects });
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
// /api/companion/streams-breakdown: Task/blocker breakdown by stream within each project
|
|
3008
|
+
if (req.url === '/api/companion/streams-breakdown') {
|
|
3009
|
+
if (!looksLikeFreyaWorkspace(workspaceDir)) {
|
|
3010
|
+
return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized' });
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
const breakdownMap = {}; // projectSlug -> { projectName, streams: [...] }
|
|
3014
|
+
|
|
3015
|
+
// 1. Load tasks grouped by project & stream
|
|
3016
|
+
const taskFile = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
|
|
3017
|
+
if (exists(taskFile)) {
|
|
3018
|
+
const taskDoc = readJsonOrNull(taskFile) || { tasks: [] };
|
|
3019
|
+
const tasks = Array.isArray(taskDoc.tasks) ? taskDoc.tasks : [];
|
|
3020
|
+
|
|
3021
|
+
for (const t of tasks) {
|
|
3022
|
+
const pSlug = String(t.projectSlug || '').trim();
|
|
3023
|
+
const sSlug = String(t.streamSlug || 'default').trim();
|
|
3024
|
+
if (!pSlug) continue;
|
|
3025
|
+
|
|
3026
|
+
if (!breakdownMap[pSlug]) {
|
|
3027
|
+
breakdownMap[pSlug] = { projectName: pSlug, streams: {} };
|
|
3028
|
+
}
|
|
3029
|
+
if (!breakdownMap[pSlug].streams[sSlug]) {
|
|
3030
|
+
breakdownMap[pSlug].streams[sSlug] = {
|
|
3031
|
+
streamName: sSlug, totalTasks: 0, completedTasks: 0, pendingTasks: 0,
|
|
3032
|
+
blockersCount: 0, blockersBySeverity: { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }
|
|
3033
|
+
};
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
breakdownMap[pSlug].streams[sSlug].totalTasks++;
|
|
3037
|
+
if (t.status === 'COMPLETED') {
|
|
3038
|
+
breakdownMap[pSlug].streams[sSlug].completedTasks++;
|
|
3039
|
+
} else {
|
|
3040
|
+
breakdownMap[pSlug].streams[sSlug].pendingTasks++;
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
// 2. Load blockers grouped by project & stream
|
|
3046
|
+
const blockerFile = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
|
|
3047
|
+
if (exists(blockerFile)) {
|
|
3048
|
+
const blockerDoc = readJsonOrNull(blockerFile) || { blockers: [] };
|
|
3049
|
+
const blockers = Array.isArray(blockerDoc.blockers) ? blockerDoc.blockers : [];
|
|
3050
|
+
|
|
3051
|
+
for (const b of blockers) {
|
|
3052
|
+
const pSlug = String(b.projectSlug || '').trim();
|
|
3053
|
+
const sSlug = String(b.streamSlug || 'default').trim();
|
|
3054
|
+
if (!pSlug) continue;
|
|
3055
|
+
|
|
3056
|
+
if (!breakdownMap[pSlug]) {
|
|
3057
|
+
breakdownMap[pSlug] = { projectName: pSlug, streams: {} };
|
|
3058
|
+
}
|
|
3059
|
+
if (!breakdownMap[pSlug].streams[sSlug]) {
|
|
3060
|
+
breakdownMap[pSlug].streams[sSlug] = {
|
|
3061
|
+
streamName: sSlug, totalTasks: 0, completedTasks: 0, pendingTasks: 0,
|
|
3062
|
+
blockersCount: 0, blockersBySeverity: { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
if (String(b.status || '').toUpperCase() === 'OPEN') {
|
|
3067
|
+
breakdownMap[pSlug].streams[sSlug].blockersCount++;
|
|
3068
|
+
const sev = String(b.severity || 'LOW').toUpperCase();
|
|
3069
|
+
if (breakdownMap[pSlug].streams[sSlug].blockersBySeverity[sev] !== undefined) {
|
|
3070
|
+
breakdownMap[pSlug].streams[sSlug].blockersBySeverity[sev]++;
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
// Convert to array format
|
|
3077
|
+
const breakdown = [];
|
|
3078
|
+
for (const [pSlug, pData] of Object.entries(breakdownMap)) {
|
|
3079
|
+
const streams = [];
|
|
3080
|
+
for (const [sSlug, sData] of Object.entries(pData.streams)) {
|
|
3081
|
+
streams.push({
|
|
3082
|
+
streamName: sData.streamName,
|
|
3083
|
+
totalTasks: sData.totalTasks,
|
|
3084
|
+
completedTasks: sData.completedTasks,
|
|
3085
|
+
pendingTasks: sData.pendingTasks,
|
|
3086
|
+
blockersCount: sData.blockersCount,
|
|
3087
|
+
blockersBySeverity: sData.blockersBySeverity
|
|
3088
|
+
});
|
|
3089
|
+
}
|
|
3090
|
+
breakdown.push({
|
|
3091
|
+
projectSlug: pSlug,
|
|
3092
|
+
projectName: pData.projectName,
|
|
3093
|
+
streams: streams.sort((a, b) => b.blockersCount - a.blockersCount)
|
|
3094
|
+
});
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
return safeJson(res, 200, { ok: true, breakdown });
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
// /api/companion/alerts: Prioritized alerts by severity
|
|
3101
|
+
if (req.url === '/api/companion/alerts') {
|
|
3102
|
+
if (!looksLikeFreyaWorkspace(workspaceDir)) {
|
|
3103
|
+
return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized' });
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
const now = Date.now();
|
|
3107
|
+
const alerts = [];
|
|
3108
|
+
|
|
3109
|
+
// 1. Check for old blockers
|
|
3110
|
+
const blockerFile = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
|
|
3111
|
+
if (exists(blockerFile)) {
|
|
3112
|
+
const blockerDoc = readJsonOrNull(blockerFile) || { blockers: [] };
|
|
3113
|
+
const blockers = Array.isArray(blockerDoc.blockers) ? blockerDoc.blockers : [];
|
|
3114
|
+
|
|
3115
|
+
for (const b of blockers) {
|
|
3116
|
+
if (String(b.status || '').toUpperCase() === 'OPEN') {
|
|
3117
|
+
const createdTime = b.createdAt ? Date.parse(b.createdAt) : now;
|
|
3118
|
+
const ageDays = Math.floor((now - createdTime) / (24 * 60 * 60 * 1000));
|
|
3119
|
+
|
|
3120
|
+
let severity = 'MEDIUM';
|
|
3121
|
+
if (String(b.severity || '').toUpperCase() === 'CRITICAL') severity = 'CRITICAL';
|
|
3122
|
+
else if (ageDays > 7) severity = 'HIGH';
|
|
3123
|
+
|
|
3124
|
+
alerts.push({
|
|
3125
|
+
severity,
|
|
3126
|
+
type: 'old_blocker',
|
|
3127
|
+
projectSlug: String(b.projectSlug || '').trim(),
|
|
3128
|
+
streamName: String(b.streamSlug || '').trim(),
|
|
3129
|
+
message: `Bloqueio: ${b.title} (${ageDays} dias)`,
|
|
3130
|
+
age: ageDays,
|
|
3131
|
+
createdAt: b.createdAt
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
// 2. Check for stale projects
|
|
3138
|
+
const base = path.join(workspaceDir, 'data', 'Clients');
|
|
3139
|
+
if (exists(base)) {
|
|
3140
|
+
const stack = [base];
|
|
3141
|
+
while (stack.length) {
|
|
3142
|
+
const dirp = stack.pop();
|
|
3143
|
+
const entries = fs.readdirSync(dirp, { withFileTypes: true });
|
|
3144
|
+
for (const ent of entries) {
|
|
3145
|
+
const full = path.join(dirp, ent.name);
|
|
3146
|
+
if (ent.isDirectory()) stack.push(full);
|
|
3147
|
+
else if (ent.isFile() && ent.name === 'status.json') {
|
|
3148
|
+
const doc = readJsonOrNull(full) || {};
|
|
3149
|
+
const slug = path.relative(base, path.dirname(full)).replace(/\\/g, '/');
|
|
3150
|
+
if (doc.active !== false) {
|
|
3151
|
+
const lastUpdated = doc.lastUpdated ? Date.parse(doc.lastUpdated) : null;
|
|
3152
|
+
if (lastUpdated) {
|
|
3153
|
+
const ageDays = Math.floor((now - lastUpdated) / (24 * 60 * 60 * 1000));
|
|
3154
|
+
if (ageDays > 14) {
|
|
3155
|
+
alerts.push({
|
|
3156
|
+
severity: ageDays > 30 ? 'CRITICAL' : 'HIGH',
|
|
3157
|
+
type: 'stale_project',
|
|
3158
|
+
projectSlug: slug,
|
|
3159
|
+
streamName: '',
|
|
3160
|
+
message: `Projeto inativo por ${ageDays} dias`,
|
|
3161
|
+
age: ageDays,
|
|
3162
|
+
createdAt: doc.lastUpdated
|
|
3163
|
+
});
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
// Sort by severity (CRITICAL > HIGH > MEDIUM) then by age
|
|
3173
|
+
const severityOrder = { CRITICAL: 3, HIGH: 2, MEDIUM: 1 };
|
|
3174
|
+
alerts.sort((a, b) => {
|
|
3175
|
+
const sA = severityOrder[a.severity] || 0;
|
|
3176
|
+
const sB = severityOrder[b.severity] || 0;
|
|
3177
|
+
if (sA !== sB) return sB - sA;
|
|
3178
|
+
return (b.age || 0) - (a.age || 0);
|
|
3179
|
+
});
|
|
3180
|
+
|
|
3181
|
+
return safeJson(res, 200, { ok: true, alerts });
|
|
3182
|
+
}
|
|
3183
|
+
|
|
2810
3184
|
if (req.url === '/api/incidents/resolve') {
|
|
2811
3185
|
const title = payload.title;
|
|
2812
3186
|
const index = Number.isInteger(payload.index) ? payload.index : null;
|
|
@@ -3417,15 +3791,19 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3417
3791
|
return `\n\n---\nFILE: ${rel}\n---\n` + fs.readFileSync(p, 'utf8');
|
|
3418
3792
|
}).join('');
|
|
3419
3793
|
|
|
3420
|
-
// V2 RAG Context
|
|
3794
|
+
// V2 RAG Context (graceful fallback if embedder/sharp not available)
|
|
3421
3795
|
const dm = new DataManager(workspaceDir, path.join(workspaceDir, 'logs'));
|
|
3422
|
-
const ragResults = await dm.semanticSearch(query, 12);
|
|
3423
3796
|
let ragContext = '';
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
ragContext
|
|
3797
|
+
try {
|
|
3798
|
+
const ragResults = await dm.semanticSearch(query, 12);
|
|
3799
|
+
if (ragResults.length > 0) {
|
|
3800
|
+
ragContext = '\n\n[MEMÓRIA DE LONGO PRAZO RECUPERADA (RAG VIA SQLITE)]\n';
|
|
3801
|
+
for (const r of ragResults) {
|
|
3802
|
+
ragContext += `\n---\nFONTE: ${r.reference_type} -> ID: ${r.reference_id} (Score: ${r.score.toFixed(3)})\nCONTEÚDO:\n${r.text_chunk}\n`;
|
|
3803
|
+
}
|
|
3428
3804
|
}
|
|
3805
|
+
} catch (ragErr) {
|
|
3806
|
+
console.error('[oracle] RAG search failed (embedder/sharp unavailable), continuing without context:', ragErr.message);
|
|
3429
3807
|
}
|
|
3430
3808
|
|
|
3431
3809
|
const prompt = `Você é o agente Oracle do sistema F.R.E.Y.A.\n\nSiga estritamente os arquivos de regras abaixo.\nResponda de forma analítica e consultiva.\n${ragContext}\n\nREGRAS:${rulesText}\n\nCONSULTA DO USUÁRIO:\n${query}\n`;
|
|
@@ -3575,6 +3953,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3575
3953
|
status: t.status,
|
|
3576
3954
|
createdAt: t.created_at,
|
|
3577
3955
|
completedAt: t.completed_at,
|
|
3956
|
+
dueDate: t.due_date || null,
|
|
3578
3957
|
projectSlug: t.project_slug,
|
|
3579
3958
|
priority: meta.priority,
|
|
3580
3959
|
streamSlug: meta.streamSlug,
|
|
@@ -3627,6 +4006,33 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3627
4006
|
queryUpdates.push('category = ?');
|
|
3628
4007
|
params.push(patch.category.trim());
|
|
3629
4008
|
}
|
|
4009
|
+
if (typeof patch.dueDate === 'string') {
|
|
4010
|
+
queryUpdates.push('due_date = ?');
|
|
4011
|
+
params.push(patch.dueDate.trim() || null);
|
|
4012
|
+
}
|
|
4013
|
+
if (patch.dueDate === null) {
|
|
4014
|
+
queryUpdates.push('due_date = NULL');
|
|
4015
|
+
}
|
|
4016
|
+
if (typeof patch.status === 'string') {
|
|
4017
|
+
queryUpdates.push('status = ?');
|
|
4018
|
+
params.push(patch.status.trim());
|
|
4019
|
+
if (patch.status === 'COMPLETED') {
|
|
4020
|
+
queryUpdates.push('completed_at = ?');
|
|
4021
|
+
params.push(new Date().toISOString());
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
if (typeof patch.description === 'string') {
|
|
4025
|
+
queryUpdates.push('description = ?');
|
|
4026
|
+
params.push(patch.description.trim());
|
|
4027
|
+
}
|
|
4028
|
+
if (typeof patch.priority === 'string') {
|
|
4029
|
+
const row = dl.db.prepare('SELECT metadata FROM tasks WHERE id = ?').get(id);
|
|
4030
|
+
let meta = {};
|
|
4031
|
+
try { meta = row && row.metadata ? JSON.parse(row.metadata) : {}; } catch { meta = {}; }
|
|
4032
|
+
meta.priority = patch.priority.trim().toLowerCase();
|
|
4033
|
+
queryUpdates.push('metadata = ?');
|
|
4034
|
+
params.push(JSON.stringify(meta));
|
|
4035
|
+
}
|
|
3630
4036
|
|
|
3631
4037
|
if (queryUpdates.length === 0) return safeJson(res, 200, { ok: true, task: { id } });
|
|
3632
4038
|
|
|
@@ -3666,202 +4072,6 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3666
4072
|
return safeJson(res, 200, { ok: true, items, stats: { pendingTasks, openBlockers, reportsToday, reportsTotal: reports.length } });
|
|
3667
4073
|
}
|
|
3668
4074
|
|
|
3669
|
-
if (req.url === '/api/companion/projects-summary') {
|
|
3670
|
-
if (!looksLikeFreyaWorkspace(workspaceDir)) {
|
|
3671
|
-
return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized', projects: [] });
|
|
3672
|
-
}
|
|
3673
|
-
|
|
3674
|
-
const now = Date.now();
|
|
3675
|
-
const projects = {};
|
|
3676
|
-
|
|
3677
|
-
// Aggregate tasks by projectSlug
|
|
3678
|
-
const tasksRaw = dl.db.prepare('SELECT * FROM tasks WHERE status IN ("PENDING", "COMPLETED")').all();
|
|
3679
|
-
const blockersRaw = dl.db.prepare('SELECT * FROM blockers WHERE status IN ("OPEN", "MITIGATING")').all();
|
|
3680
|
-
|
|
3681
|
-
// Build project summary
|
|
3682
|
-
for (const t of tasksRaw) {
|
|
3683
|
-
const slug = String(t.project_slug || 'unassigned').trim();
|
|
3684
|
-
if (!projects[slug]) {
|
|
3685
|
-
projects[slug] = { slug, totalTasks: 0, completedTasks: 0, pendingTasks: 0, streams: new Set(), blockersCount: 0, blockersBySeverity: { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }, oldestBlockerDays: null, lastUpdateMs: null };
|
|
3686
|
-
}
|
|
3687
|
-
projects[slug].totalTasks++;
|
|
3688
|
-
if (String(t.status).toUpperCase() === 'COMPLETED') projects[slug].completedTasks++;
|
|
3689
|
-
else projects[slug].pendingTasks++;
|
|
3690
|
-
if (t.completed_at) projects[slug].lastUpdateMs = Math.max(projects[slug].lastUpdateMs || 0, Date.parse(t.completed_at));
|
|
3691
|
-
if (t.created_at) projects[slug].lastUpdateMs = Math.max(projects[slug].lastUpdateMs || 0, Date.parse(t.created_at));
|
|
3692
|
-
const meta = t.metadata ? JSON.parse(String(t.metadata || '{}')) : {};
|
|
3693
|
-
if (meta.streamSlug) projects[slug].streams.add(String(meta.streamSlug));
|
|
3694
|
-
}
|
|
3695
|
-
|
|
3696
|
-
// Add blockers to projects
|
|
3697
|
-
for (const b of blockersRaw) {
|
|
3698
|
-
const slug = String(b.project_slug || 'unassigned').trim();
|
|
3699
|
-
if (!projects[slug]) {
|
|
3700
|
-
projects[slug] = { slug, totalTasks: 0, completedTasks: 0, pendingTasks: 0, streams: new Set(), blockersCount: 0, blockersBySeverity: { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }, oldestBlockerDays: null, lastUpdateMs: null };
|
|
3701
|
-
}
|
|
3702
|
-
projects[slug].blockersCount++;
|
|
3703
|
-
const sev = String(b.severity || 'LOW').toUpperCase();
|
|
3704
|
-
if (projects[slug].blockersBySeverity[sev] !== undefined) projects[slug].blockersBySeverity[sev]++;
|
|
3705
|
-
const createdMs = b.created_at ? Date.parse(b.created_at) : null;
|
|
3706
|
-
if (createdMs) {
|
|
3707
|
-
projects[slug].lastUpdateMs = Math.max(projects[slug].lastUpdateMs || 0, createdMs);
|
|
3708
|
-
const ageDays = Math.floor((now - createdMs) / (24 * 60 * 60 * 1000));
|
|
3709
|
-
if (projects[slug].oldestBlockerDays === null || ageDays > projects[slug].oldestBlockerDays) {
|
|
3710
|
-
projects[slug].oldestBlockerDays = ageDays;
|
|
3711
|
-
}
|
|
3712
|
-
}
|
|
3713
|
-
}
|
|
3714
|
-
|
|
3715
|
-
// Convert to array and calculate status + velocity
|
|
3716
|
-
const projectsArray = Object.values(projects).map(p => {
|
|
3717
|
-
const completedThisWeek = dl.db.prepare('SELECT count(*) as count FROM tasks WHERE project_slug = ? AND status = "COMPLETED" AND completed_at > ?').get(p.slug, new Date(now - 7 * 24 * 60 * 60 * 1000).toISOString());
|
|
3718
|
-
const velocityThisWeek = completedThisWeek ? (completedThisWeek.count / 7).toFixed(1) : '0';
|
|
3719
|
-
const lastUpdateAgo = p.lastUpdateMs ? formatAgo(p.lastUpdateMs) : 'nunca';
|
|
3720
|
-
const hasOldBlockers = p.oldestBlockerDays && p.oldestBlockerDays > 7;
|
|
3721
|
-
const hasHighSeverity = p.blockersBySeverity.CRITICAL > 0 || p.blockersBySeverity.HIGH > 0;
|
|
3722
|
-
const isIdle = p.lastUpdateMs && (now - p.lastUpdateMs) > 7 * 24 * 60 * 60 * 1000;
|
|
3723
|
-
const status = isIdle ? 'IDLE' : (hasHighSeverity || hasOldBlockers ? 'AT_RISK' : 'ON_TRACK');
|
|
3724
|
-
return {
|
|
3725
|
-
slug: p.slug,
|
|
3726
|
-
name: p.slug.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
|
|
3727
|
-
totalTasks: p.totalTasks,
|
|
3728
|
-
completedTasks: p.completedTasks,
|
|
3729
|
-
pendingTasks: p.pendingTasks,
|
|
3730
|
-
blockersCount: p.blockersCount,
|
|
3731
|
-
blockersBySeverity: p.blockersBySeverity,
|
|
3732
|
-
oldestBlockerDays: p.oldestBlockerDays,
|
|
3733
|
-
velocityThisWeek: parseFloat(velocityThisWeek),
|
|
3734
|
-
lastUpdateAgo: lastUpdateAgo,
|
|
3735
|
-
status: status,
|
|
3736
|
-
streams: Array.from(p.streams)
|
|
3737
|
-
};
|
|
3738
|
-
});
|
|
3739
|
-
|
|
3740
|
-
return safeJson(res, 200, { ok: true, projects: projectsArray });
|
|
3741
|
-
}
|
|
3742
|
-
|
|
3743
|
-
if (req.url === '/api/companion/streams-breakdown') {
|
|
3744
|
-
if (!looksLikeFreyaWorkspace(workspaceDir)) {
|
|
3745
|
-
return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized', items: [] });
|
|
3746
|
-
}
|
|
3747
|
-
|
|
3748
|
-
const tasks = dl.db.prepare('SELECT * FROM tasks').all();
|
|
3749
|
-
const blockers = dl.db.prepare('SELECT * FROM blockers').all();
|
|
3750
|
-
|
|
3751
|
-
const breakdown = {};
|
|
3752
|
-
for (const t of tasks) {
|
|
3753
|
-
const projectSlug = String(t.project_slug || 'unassigned');
|
|
3754
|
-
if (!breakdown[projectSlug]) breakdown[projectSlug] = {};
|
|
3755
|
-
const meta = t.metadata ? JSON.parse(String(t.metadata || '{}')) : {};
|
|
3756
|
-
const streamName = String(meta.streamSlug || 'Default');
|
|
3757
|
-
if (!breakdown[projectSlug][streamName]) {
|
|
3758
|
-
breakdown[projectSlug][streamName] = { totalTasks: 0, completedTasks: 0, pendingTasks: 0, blockersCount: 0, blockersHighestSeverity: null };
|
|
3759
|
-
}
|
|
3760
|
-
breakdown[projectSlug][streamName].totalTasks++;
|
|
3761
|
-
if (String(t.status).toUpperCase() === 'COMPLETED') breakdown[projectSlug][streamName].completedTasks++;
|
|
3762
|
-
else breakdown[projectSlug][streamName].pendingTasks++;
|
|
3763
|
-
}
|
|
3764
|
-
|
|
3765
|
-
for (const b of blockers) {
|
|
3766
|
-
if (String(b.status).toUpperCase() !== 'OPEN' && String(b.status).toUpperCase() !== 'MITIGATING') continue;
|
|
3767
|
-
const projectSlug = String(b.project_slug || 'unassigned');
|
|
3768
|
-
if (!breakdown[projectSlug]) breakdown[projectSlug] = {};
|
|
3769
|
-
const meta = b.metadata ? JSON.parse(String(b.metadata || '{}')) : {};
|
|
3770
|
-
const streamName = String(meta.streamSlug || 'Default');
|
|
3771
|
-
if (!breakdown[projectSlug][streamName]) {
|
|
3772
|
-
breakdown[projectSlug][streamName] = { totalTasks: 0, completedTasks: 0, pendingTasks: 0, blockersCount: 0, blockersHighestSeverity: null };
|
|
3773
|
-
}
|
|
3774
|
-
breakdown[projectSlug][streamName].blockersCount++;
|
|
3775
|
-
const sev = String(b.severity || 'LOW').toUpperCase();
|
|
3776
|
-
const sevOrder = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
|
|
3777
|
-
const sevVal = sevOrder[sev] !== undefined ? sevOrder[sev] : 999;
|
|
3778
|
-
const currentVal = breakdown[projectSlug][streamName].blockersHighestSeverity ? sevOrder[breakdown[projectSlug][streamName].blockersHighestSeverity] : 999;
|
|
3779
|
-
if (sevVal < currentVal) breakdown[projectSlug][streamName].blockersHighestSeverity = sev;
|
|
3780
|
-
}
|
|
3781
|
-
|
|
3782
|
-
const items = Object.entries(breakdown).map(([projectSlug, streams]) => ({
|
|
3783
|
-
projectSlug,
|
|
3784
|
-
projectName: projectSlug.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
|
|
3785
|
-
streams: Object.entries(streams).map(([streamName, data]) => ({ streamName, ...data }))
|
|
3786
|
-
}));
|
|
3787
|
-
|
|
3788
|
-
return safeJson(res, 200, { ok: true, items });
|
|
3789
|
-
}
|
|
3790
|
-
|
|
3791
|
-
if (req.url === '/api/companion/alerts') {
|
|
3792
|
-
if (!looksLikeFreyaWorkspace(workspaceDir)) {
|
|
3793
|
-
return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized', alerts: [] });
|
|
3794
|
-
}
|
|
3795
|
-
|
|
3796
|
-
const now = Date.now();
|
|
3797
|
-
const alerts = [];
|
|
3798
|
-
|
|
3799
|
-
const blockers = dl.db.prepare('SELECT * FROM blockers WHERE status IN ("OPEN", "MITIGATING")').all();
|
|
3800
|
-
const tasks = dl.db.prepare('SELECT * FROM tasks WHERE status = "PENDING"').all();
|
|
3801
|
-
|
|
3802
|
-
// Old blockers
|
|
3803
|
-
for (const b of blockers) {
|
|
3804
|
-
const createdMs = b.created_at ? Date.parse(b.created_at) : null;
|
|
3805
|
-
if (createdMs) {
|
|
3806
|
-
const ageDays = Math.floor((now - createdMs) / (24 * 60 * 60 * 1000));
|
|
3807
|
-
if (ageDays > 3) {
|
|
3808
|
-
const sev = String(b.severity || '').toUpperCase();
|
|
3809
|
-
alerts.push({
|
|
3810
|
-
severity: sev === 'CRITICAL' ? 'CRITICAL' : sev === 'HIGH' ? 'HIGH' : 'MEDIUM',
|
|
3811
|
-
type: 'old_blocker',
|
|
3812
|
-
projectSlug: String(b.project_slug || ''),
|
|
3813
|
-
streamName: 'blocker: ' + (b.title || ''),
|
|
3814
|
-
message: `Blocker aberto há ${ageDays} dias: ${b.title || 'Sem titulo'}`,
|
|
3815
|
-
ageDays
|
|
3816
|
-
});
|
|
3817
|
-
}
|
|
3818
|
-
}
|
|
3819
|
-
}
|
|
3820
|
-
|
|
3821
|
-
// Stale projects (7+ days without update)
|
|
3822
|
-
const projects = {};
|
|
3823
|
-
for (const t of tasks) {
|
|
3824
|
-
const slug = String(t.project_slug || '');
|
|
3825
|
-
if (slug) {
|
|
3826
|
-
const createdMs = t.created_at ? Date.parse(t.created_at) : null;
|
|
3827
|
-
if (createdMs && (!projects[slug] || createdMs > projects[slug])) projects[slug] = createdMs;
|
|
3828
|
-
}
|
|
3829
|
-
}
|
|
3830
|
-
for (const b of blockers) {
|
|
3831
|
-
const slug = String(b.project_slug || '');
|
|
3832
|
-
if (slug) {
|
|
3833
|
-
const createdMs = b.created_at ? Date.parse(b.created_at) : null;
|
|
3834
|
-
if (createdMs && (!projects[slug] || createdMs > projects[slug])) projects[slug] = createdMs;
|
|
3835
|
-
}
|
|
3836
|
-
}
|
|
3837
|
-
for (const [slug, lastMs] of Object.entries(projects)) {
|
|
3838
|
-
if (lastMs) {
|
|
3839
|
-
const ageDays = Math.floor((now - lastMs) / (24 * 60 * 60 * 1000));
|
|
3840
|
-
if (ageDays > 7) {
|
|
3841
|
-
alerts.push({
|
|
3842
|
-
severity: ageDays > 14 ? 'HIGH' : 'MEDIUM',
|
|
3843
|
-
type: 'stale_project',
|
|
3844
|
-
projectSlug: slug,
|
|
3845
|
-
streamName: '',
|
|
3846
|
-
message: `Projeto ${slug} sem updates há ${ageDays} dias`,
|
|
3847
|
-
ageDays
|
|
3848
|
-
});
|
|
3849
|
-
}
|
|
3850
|
-
}
|
|
3851
|
-
}
|
|
3852
|
-
|
|
3853
|
-
// Sort by severity + age
|
|
3854
|
-
alerts.sort((a, b) => {
|
|
3855
|
-
const sevOrder = { CRITICAL: 0, HIGH: 1, MEDIUM: 2 };
|
|
3856
|
-
const aVal = sevOrder[a.severity] || 99;
|
|
3857
|
-
const bVal = sevOrder[b.severity] || 99;
|
|
3858
|
-
if (aVal !== bVal) return aVal - bVal;
|
|
3859
|
-
return (b.ageDays || 0) - (a.ageDays || 0);
|
|
3860
|
-
});
|
|
3861
|
-
|
|
3862
|
-
return safeJson(res, 200, { ok: true, alerts: alerts.slice(0, 10) });
|
|
3863
|
-
}
|
|
3864
|
-
|
|
3865
4075
|
// BUG-12: duplicate handlers for /api/incidents/resolve, /api/incidents,
|
|
3866
4076
|
// and /api/tasks/heatmap were removed here — originals remain earlier in the file.
|
|
3867
4077
|
|
|
@@ -3974,6 +4184,100 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3974
4184
|
return safeJson(res, 200, { ok: true, blocker: { id, ...patch } });
|
|
3975
4185
|
}
|
|
3976
4186
|
|
|
4187
|
+
// --- Quick-add: create a single task directly ---
|
|
4188
|
+
if (req.url === '/api/tasks/create') {
|
|
4189
|
+
const desc = String(payload.description || '').trim();
|
|
4190
|
+
if (!desc) return safeJson(res, 400, { error: 'Missing description' });
|
|
4191
|
+
|
|
4192
|
+
const cat = String(payload.category || 'DO_NOW').trim().toUpperCase();
|
|
4193
|
+
const validCats = new Set(['DO_NOW', 'SCHEDULE', 'DELEGATE', 'IGNORE']);
|
|
4194
|
+
if (!validCats.has(cat)) return safeJson(res, 400, { error: 'Invalid category' });
|
|
4195
|
+
|
|
4196
|
+
const slug = typeof payload.projectSlug === 'string' ? payload.projectSlug.trim() || null : null;
|
|
4197
|
+
const dueDate = typeof payload.dueDate === 'string' ? payload.dueDate.trim() || null : null;
|
|
4198
|
+
const pri = typeof payload.priority === 'string' ? payload.priority.trim().toLowerCase() : undefined;
|
|
4199
|
+
const meta = {};
|
|
4200
|
+
if (pri) meta.priority = pri;
|
|
4201
|
+
|
|
4202
|
+
const id = `task-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
|
4203
|
+
dl.db.prepare('INSERT INTO tasks (id, project_slug, description, category, status, due_date, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)')
|
|
4204
|
+
.run(id, slug, desc, cat, 'PENDING', dueDate, Object.keys(meta).length ? JSON.stringify(meta) : null);
|
|
4205
|
+
|
|
4206
|
+
return safeJson(res, 200, { ok: true, task: { id, description: desc, category: cat, status: 'PENDING', projectSlug: slug, dueDate, priority: pri } });
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
// --- Delta summary: what changed since yesterday ---
|
|
4210
|
+
if (req.url === '/api/summary/delta') {
|
|
4211
|
+
const now = new Date();
|
|
4212
|
+
const yesterday = new Date(now);
|
|
4213
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
4214
|
+
const yIso = yesterday.toISOString();
|
|
4215
|
+
|
|
4216
|
+
const completed = dl.db.prepare("SELECT count(*) as c FROM tasks WHERE status = 'COMPLETED' AND completed_at >= ?").get(yIso);
|
|
4217
|
+
const newTasks = dl.db.prepare("SELECT count(*) as c FROM tasks WHERE created_at >= ?").get(yIso);
|
|
4218
|
+
const resolvedBlockers = dl.db.prepare("SELECT count(*) as c FROM blockers WHERE resolved_at >= ?").get(yIso);
|
|
4219
|
+
const newBlockers = dl.db.prepare("SELECT count(*) as c FROM blockers WHERE created_at >= ?").get(yIso);
|
|
4220
|
+
const overdue = dl.db.prepare("SELECT count(*) as c FROM tasks WHERE status = 'PENDING' AND due_date IS NOT NULL AND due_date < ?")
|
|
4221
|
+
.get(now.toISOString().slice(0, 10));
|
|
4222
|
+
|
|
4223
|
+
return safeJson(res, 200, {
|
|
4224
|
+
ok: true,
|
|
4225
|
+
delta: {
|
|
4226
|
+
completedTasks: completed ? completed.c : 0,
|
|
4227
|
+
newTasks: newTasks ? newTasks.c : 0,
|
|
4228
|
+
resolvedBlockers: resolvedBlockers ? resolvedBlockers.c : 0,
|
|
4229
|
+
newBlockers: newBlockers ? newBlockers.c : 0,
|
|
4230
|
+
overdueTasks: overdue ? overdue.c : 0
|
|
4231
|
+
}
|
|
4232
|
+
});
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
// --- Kanban: all active tasks for board view ---
|
|
4236
|
+
if (req.url === '/api/tasks/kanban') {
|
|
4237
|
+
const rawTasks = dl.db.prepare(`
|
|
4238
|
+
SELECT * FROM tasks WHERE status != 'ARCHIVED'
|
|
4239
|
+
ORDER BY
|
|
4240
|
+
CASE JSON_EXTRACT(metadata, '$.priority')
|
|
4241
|
+
WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4
|
|
4242
|
+
END ASC,
|
|
4243
|
+
created_at DESC
|
|
4244
|
+
`).all();
|
|
4245
|
+
|
|
4246
|
+
const tasks = rawTasks.map(t => {
|
|
4247
|
+
let meta = {};
|
|
4248
|
+
try { meta = t.metadata ? JSON.parse(t.metadata) : {}; } catch { meta = {}; }
|
|
4249
|
+
return {
|
|
4250
|
+
id: t.id,
|
|
4251
|
+
description: t.description,
|
|
4252
|
+
category: t.category,
|
|
4253
|
+
status: t.status,
|
|
4254
|
+
createdAt: t.created_at,
|
|
4255
|
+
completedAt: t.completed_at,
|
|
4256
|
+
dueDate: t.due_date || null,
|
|
4257
|
+
projectSlug: t.project_slug,
|
|
4258
|
+
priority: meta.priority,
|
|
4259
|
+
streamSlug: meta.streamSlug
|
|
4260
|
+
};
|
|
4261
|
+
});
|
|
4262
|
+
|
|
4263
|
+
const openBlockers = dl.db.prepare(`
|
|
4264
|
+
SELECT * FROM blockers WHERE status IN ('OPEN', 'MITIGATING')
|
|
4265
|
+
ORDER BY
|
|
4266
|
+
CASE severity WHEN 'CRITICAL' THEN 0 WHEN 'HIGH' THEN 1 WHEN 'MEDIUM' THEN 2 WHEN 'LOW' THEN 3 ELSE 9 END ASC,
|
|
4267
|
+
created_at ASC
|
|
4268
|
+
`).all().map(b => ({
|
|
4269
|
+
id: b.id,
|
|
4270
|
+
title: b.title,
|
|
4271
|
+
severity: b.severity,
|
|
4272
|
+
status: b.status,
|
|
4273
|
+
projectSlug: b.project_slug,
|
|
4274
|
+
owner: b.owner,
|
|
4275
|
+
createdAt: b.created_at
|
|
4276
|
+
}));
|
|
4277
|
+
|
|
4278
|
+
return safeJson(res, 200, { ok: true, tasks, blockers: openBlockers });
|
|
4279
|
+
}
|
|
4280
|
+
|
|
3977
4281
|
if (req.url === '/api/report') {
|
|
3978
4282
|
const script = payload.script;
|
|
3979
4283
|
if (!script) return safeJson(res, 400, { error: 'Missing script' });
|
|
@@ -4100,6 +4404,9 @@ function buildSettingsHtml(safeDefault, appVersion) {
|
|
|
4100
4404
|
<button class="railBtn" id="railCompanion" type="button" title="Companion">
|
|
4101
4405
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>
|
|
4102
4406
|
</button>
|
|
4407
|
+
<button class="railBtn" id="railKanban" type="button" title="Kanban">
|
|
4408
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="5" height="18" rx="1"></rect><rect x="10" y="3" width="5" height="12" rx="1"></rect><rect x="17" y="3" width="5" height="15" rx="1"></rect></svg>
|
|
4409
|
+
</button>
|
|
4103
4410
|
<button class="railBtn" id="railProjects" type="button" title="Projects">
|
|
4104
4411
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
|
|
4105
4412
|
</button>
|
|
@@ -4215,5 +4522,175 @@ function buildSettingsHtml(safeDefault, appVersion) {
|
|
|
4215
4522
|
</html>`;
|
|
4216
4523
|
}
|
|
4217
4524
|
|
|
4525
|
+
function buildKanbanHtml(safeDefault, appVersion) {
|
|
4526
|
+
const safeVersion = escapeHtml(appVersion || 'unknown');
|
|
4527
|
+
const kanbanSvg = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="5" height="18" rx="1"></rect><rect x="10" y="3" width="5" height="12" rx="1"></rect><rect x="17" y="3" width="5" height="15" rx="1"></rect></svg>';
|
|
4528
|
+
return `<!doctype html>
|
|
4529
|
+
<html>
|
|
4530
|
+
<head>
|
|
4531
|
+
<meta charset="utf-8" />
|
|
4532
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
4533
|
+
<title>FREYA Kanban</title>
|
|
4534
|
+
<link rel="stylesheet" href="/app.css" />
|
|
4535
|
+
</head>
|
|
4536
|
+
<body data-page="kanban">
|
|
4537
|
+
<div class="app">
|
|
4538
|
+
<div class="frame">
|
|
4539
|
+
<div class="shell">
|
|
4540
|
+
|
|
4541
|
+
<aside class="rail" role="navigation" aria-label="Menu principal">
|
|
4542
|
+
<div class="railTop">
|
|
4543
|
+
<div class="railLogo">F</div>
|
|
4544
|
+
</div>
|
|
4545
|
+
<div class="railNav">
|
|
4546
|
+
<button class="railBtn" id="railDashboard" type="button" title="Dashboard">
|
|
4547
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="5"></rect><rect x="14" y="12" width="7" height="9"></rect><rect x="3" y="16" width="7" height="5"></rect></svg>
|
|
4548
|
+
</button>
|
|
4549
|
+
<button class="railBtn" id="railReports" type="button" title="Relat\\u00f3rios">
|
|
4550
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
|
|
4551
|
+
</button>
|
|
4552
|
+
<button class="railBtn" id="railCompanion" type="button" title="Companion">
|
|
4553
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>
|
|
4554
|
+
</button>
|
|
4555
|
+
<button class="railBtn active" id="railKanban" type="button" title="Kanban">
|
|
4556
|
+
${kanbanSvg}
|
|
4557
|
+
</button>
|
|
4558
|
+
<button class="railBtn" id="railProjects" type="button" title="Projects">
|
|
4559
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
|
|
4560
|
+
</button>
|
|
4561
|
+
<button class="railBtn" id="railTimeline" type="button" title="Timeline">
|
|
4562
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
|
|
4563
|
+
</button>
|
|
4564
|
+
<button class="railBtn" id="railGraph" type="button" title="Grafo">
|
|
4565
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"></circle><circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="19" r="3"></circle><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line></svg>
|
|
4566
|
+
</button>
|
|
4567
|
+
</div>
|
|
4568
|
+
<div class="railBottom">
|
|
4569
|
+
<button id="railSettings" class="railBtn" title="Configura\\u00e7\\u00f5es" onclick="if (document.body.dataset.page !== 'settings') window.location.href='/settings';">
|
|
4570
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
|
4571
|
+
</button>
|
|
4572
|
+
</div>
|
|
4573
|
+
</aside>
|
|
4574
|
+
|
|
4575
|
+
<main class="center" role="main">
|
|
4576
|
+
<div class="topbar">
|
|
4577
|
+
<div class="brandLine">
|
|
4578
|
+
<span class="spark"></span>
|
|
4579
|
+
<div class="brandStack">
|
|
4580
|
+
<div class="brand">FREYA</div>
|
|
4581
|
+
<div class="brandSub">Kanban Board — Vis\\u00e3o SM</div>
|
|
4582
|
+
</div>
|
|
4583
|
+
</div>
|
|
4584
|
+
<div class="topActions">
|
|
4585
|
+
<span class="chip clickable" onclick="window.location.href='/docs'">Docs</span>
|
|
4586
|
+
<span class="chip" id="chipVersion">v${safeVersion}</span>
|
|
4587
|
+
</div>
|
|
4588
|
+
</div>
|
|
4589
|
+
|
|
4590
|
+
<div class="centerBody">
|
|
4591
|
+
<!-- Kanban toolbar -->
|
|
4592
|
+
<div class="kanban-toolbar" style="display:flex; justify-content:space-between; align-items:center; padding:0 0 16px; gap:12px; flex-wrap:wrap;">
|
|
4593
|
+
<div style="display:flex; gap:8px; align-items:center;">
|
|
4594
|
+
<select id="kanbanFilterProject" class="kanban-filter" onchange="window.filterKanban()">
|
|
4595
|
+
<option value="">Todos os projetos</option>
|
|
4596
|
+
</select>
|
|
4597
|
+
<button class="btn small" type="button" onclick="window.loadKanban()">
|
|
4598
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:-2px;margin-right:4px"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>
|
|
4599
|
+
Atualizar
|
|
4600
|
+
</button>
|
|
4601
|
+
</div>
|
|
4602
|
+
<button class="btn primary small" type="button" onclick="window.openQuickAdd()">+ Nova Task</button>
|
|
4603
|
+
</div>
|
|
4604
|
+
|
|
4605
|
+
<!-- Delta banner -->
|
|
4606
|
+
<div id="kanbanDelta" style="display:none;"></div>
|
|
4607
|
+
|
|
4608
|
+
<!-- Kanban columns -->
|
|
4609
|
+
<div id="kanbanBoard" class="kanban-board">
|
|
4610
|
+
<div class="kanban-col" data-category="DO_NOW">
|
|
4611
|
+
<div class="kanban-col-head do-now">
|
|
4612
|
+
<span class="kanban-col-title">DO NOW</span>
|
|
4613
|
+
<span class="kanban-col-count" id="countDoNow">0</span>
|
|
4614
|
+
</div>
|
|
4615
|
+
<div class="kanban-col-body" id="colDoNow"></div>
|
|
4616
|
+
</div>
|
|
4617
|
+
<div class="kanban-col" data-category="SCHEDULE">
|
|
4618
|
+
<div class="kanban-col-head schedule">
|
|
4619
|
+
<span class="kanban-col-title">SCHEDULE</span>
|
|
4620
|
+
<span class="kanban-col-count" id="countSchedule">0</span>
|
|
4621
|
+
</div>
|
|
4622
|
+
<div class="kanban-col-body" id="colSchedule"></div>
|
|
4623
|
+
</div>
|
|
4624
|
+
<div class="kanban-col" data-category="DELEGATE">
|
|
4625
|
+
<div class="kanban-col-head delegate">
|
|
4626
|
+
<span class="kanban-col-title">DELEGATE</span>
|
|
4627
|
+
<span class="kanban-col-count" id="countDelegate">0</span>
|
|
4628
|
+
</div>
|
|
4629
|
+
<div class="kanban-col-body" id="colDelegate"></div>
|
|
4630
|
+
</div>
|
|
4631
|
+
<div class="kanban-col" data-category="COMPLETED">
|
|
4632
|
+
<div class="kanban-col-head done">
|
|
4633
|
+
<span class="kanban-col-title">DONE (7d)</span>
|
|
4634
|
+
<span class="kanban-col-count" id="countDone">0</span>
|
|
4635
|
+
</div>
|
|
4636
|
+
<div class="kanban-col-body" id="colDone"></div>
|
|
4637
|
+
</div>
|
|
4638
|
+
</div>
|
|
4639
|
+
|
|
4640
|
+
<!-- Blockers strip below kanban -->
|
|
4641
|
+
<div id="kanbanBlockers" style="margin-top:20px; display:none;">
|
|
4642
|
+
<div style="font-size:13px; font-weight:700; color:var(--text); margin-bottom:8px; display:flex; align-items:center; gap:6px;">
|
|
4643
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
|
4644
|
+
Blockers Ativos
|
|
4645
|
+
</div>
|
|
4646
|
+
<div id="kanbanBlockersList" class="kanban-blockers-list"></div>
|
|
4647
|
+
</div>
|
|
4648
|
+
</div>
|
|
4649
|
+
</main>
|
|
4650
|
+
</div>
|
|
4651
|
+
</div>
|
|
4652
|
+
</div>
|
|
4653
|
+
|
|
4654
|
+
<!-- Quick-add modal -->
|
|
4655
|
+
<div id="quickAddOverlay" class="qa-overlay" style="display:none;">
|
|
4656
|
+
<div class="qa-modal">
|
|
4657
|
+
<div class="qa-header">
|
|
4658
|
+
<span style="font-weight:700; font-size:14px;">Nova Task</span>
|
|
4659
|
+
<button class="btn small" type="button" onclick="window.closeQuickAdd()" style="padding:2px 8px;">×</button>
|
|
4660
|
+
</div>
|
|
4661
|
+
<textarea id="qaDesc" class="qa-input" placeholder="Descri\\u00e7\\u00e3o da task..." rows="3"></textarea>
|
|
4662
|
+
<div class="qa-row">
|
|
4663
|
+
<select id="qaCat" class="qa-select">
|
|
4664
|
+
<option value="DO_NOW">DO_NOW</option>
|
|
4665
|
+
<option value="SCHEDULE">SCHEDULE</option>
|
|
4666
|
+
<option value="DELEGATE">DELEGATE</option>
|
|
4667
|
+
</select>
|
|
4668
|
+
<select id="qaPriority" class="qa-select">
|
|
4669
|
+
<option value="">Prioridade</option>
|
|
4670
|
+
<option value="critical">Cr\\u00edtica</option>
|
|
4671
|
+
<option value="high">Alta</option>
|
|
4672
|
+
<option value="medium">M\\u00e9dia</option>
|
|
4673
|
+
<option value="low">Baixa</option>
|
|
4674
|
+
</select>
|
|
4675
|
+
</div>
|
|
4676
|
+
<div class="qa-row">
|
|
4677
|
+
<input id="qaSlug" class="qa-input" placeholder="Projeto (slug)" style="flex:1;" />
|
|
4678
|
+
<input id="qaDue" type="date" class="qa-input" style="width:160px;" />
|
|
4679
|
+
</div>
|
|
4680
|
+
<div class="qa-row" style="justify-content:flex-end;">
|
|
4681
|
+
<button class="btn small" type="button" onclick="window.closeQuickAdd()">Cancelar</button>
|
|
4682
|
+
<button class="btn primary small" type="button" onclick="window.submitQuickAdd()">Criar Task</button>
|
|
4683
|
+
</div>
|
|
4684
|
+
</div>
|
|
4685
|
+
</div>
|
|
4686
|
+
|
|
4687
|
+
<script>
|
|
4688
|
+
window.__FREYA_DEFAULT_DIR = "${safeDefault}";
|
|
4689
|
+
</script>
|
|
4690
|
+
<script src="/app.js"></script>
|
|
4691
|
+
</body>
|
|
4692
|
+
</html>`;
|
|
4693
|
+
}
|
|
4694
|
+
|
|
4218
4695
|
module.exports = { cmdWeb };
|
|
4219
4696
|
|