@cccarv82/freya 2.3.6 → 2.3.7

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.
Files changed (3) hide show
  1. package/cli/web-ui.js +167 -3
  2. package/cli/web.js +247 -33
  3. package/package.json +1 -1
package/cli/web-ui.js CHANGED
@@ -17,6 +17,8 @@
17
17
  autoApply: true,
18
18
  autoRunReports: false,
19
19
  prettyPublish: true,
20
+ timelineProject: '',
21
+ timelineTag: '',
20
22
  chatSessionId: null,
21
23
  chatLoaded: false
22
24
  };
@@ -705,8 +707,61 @@
705
707
  const filter = String(($('timelineFilter') && $('timelineFilter').value) || '').toLowerCase();
706
708
  const items = Array.isArray(state.timeline) ? state.timeline : [];
707
709
  const kind = state.timelineKind || 'all';
710
+ const projectSelect = $('timelineProject');
711
+ const tagSelect = $('timelineTag');
712
+ const getItemSlug = (it) => {
713
+ if (!it) return '';
714
+ if (it.slug) return String(it.slug);
715
+ if (it.kind === 'task' && it.content) return String(it.content);
716
+ return '';
717
+ };
718
+ const collectTimelineOptions = (list) => {
719
+ const slugs = new Set();
720
+ const tags = new Set();
721
+ for (const it of list) {
722
+ const slug = getItemSlug(it);
723
+ if (slug) slugs.add(slug);
724
+ const itTags = Array.isArray(it.tags) ? it.tags : [];
725
+ for (const tag of itTags) {
726
+ const cleaned = String(tag || '').trim();
727
+ if (cleaned) tags.add(cleaned);
728
+ }
729
+ }
730
+ return {
731
+ slugs: Array.from(slugs).sort((a, b) => a.localeCompare(b)),
732
+ tags: Array.from(tags).sort((a, b) => a.localeCompare(b))
733
+ };
734
+ };
735
+ const syncSelect = (selectEl, options, selected, placeholder) => {
736
+ if (!selectEl) return selected;
737
+ const nextSelected = options.includes(selected) ? selected : '';
738
+ selectEl.innerHTML = '';
739
+ const allOpt = document.createElement('option');
740
+ allOpt.value = '';
741
+ allOpt.textContent = placeholder;
742
+ selectEl.appendChild(allOpt);
743
+ for (const opt of options) {
744
+ const optionEl = document.createElement('option');
745
+ optionEl.value = opt;
746
+ optionEl.textContent = opt;
747
+ selectEl.appendChild(optionEl);
748
+ }
749
+ selectEl.value = nextSelected;
750
+ return nextSelected;
751
+ };
752
+ const options = collectTimelineOptions(items);
753
+ const selectedProject = syncSelect(projectSelect, options.slugs, state.timelineProject || '', 'Todos projetos');
754
+ const selectedTag = syncSelect(tagSelect, options.tags, state.timelineTag || '', 'Todas tags');
755
+ state.timelineProject = selectedProject;
756
+ state.timelineTag = selectedTag;
708
757
  const filtered = items.filter((i) => {
709
- const hay = [i.kind, i.title, i.content, (i.tags || []).join(' ')].join(' ').toLowerCase();
758
+ const slug = getItemSlug(i);
759
+ if (state.timelineProject && slug !== state.timelineProject) return false;
760
+ if (state.timelineTag) {
761
+ const itTags = Array.isArray(i.tags) ? i.tags.map((t) => String(t)) : [];
762
+ if (!itTags.includes(state.timelineTag)) return false;
763
+ }
764
+ const hay = [i.kind, i.title, i.content, i.slug, (i.tags || []).join(' ')].join(' ').toLowerCase();
710
765
  return !filter || hay.includes(filter);
711
766
  });
712
767
  el.innerHTML = '';
@@ -747,6 +802,16 @@
747
802
  renderTimeline();
748
803
  }
749
804
 
805
+ function setTimelineProject(project) {
806
+ state.timelineProject = String(project || '');
807
+ renderTimeline();
808
+ }
809
+
810
+ function setTimelineTag(tag) {
811
+ state.timelineTag = String(tag || '');
812
+ renderTimeline();
813
+ }
814
+
750
815
  async function refreshTimeline() {
751
816
  try {
752
817
  const r = await api('/api/timeline', { dir: dirOrDefault() });
@@ -779,15 +844,54 @@
779
844
  if (current) cards.push(current);
780
845
  el.innerHTML = '';
781
846
  if (!cards.length) { el.innerHTML = renderMarkdown(md); return; }
782
- for (const c of cards) {
847
+ for (let idx = 0; idx < cards.length; idx++) {
848
+ const c = cards[idx];
783
849
  const card = document.createElement('div');
784
850
  card.className = 'reportCard';
785
851
  const dateLine = c.body.find((b)=> b.toLowerCase().includes('data'));
786
852
  const impactLine = c.body.find((b)=> b.toLowerCase().includes('descricao') || b.toLowerCase().includes('impacto'));
853
+ const statusLine = c.body.find((b)=> /^status\s*:/i.test(b));
854
+ const statusRaw = statusLine ? statusLine.split(':').slice(1).join(':').trim().toLowerCase() : '';
855
+ let statusKey = '';
856
+ if (['open', 'aberto', 'aberta'].includes(statusRaw)) statusKey = 'open';
857
+ else if (['mitigating', 'mitigando', 'mitigacao', 'mitigação'].includes(statusRaw)) statusKey = 'mitigating';
858
+ else if (['resolved', 'resolvido', 'resolvida', 'closed', 'fechado', 'fechada'].includes(statusRaw)) statusKey = 'resolved';
859
+
787
860
  card.innerHTML = '<div class="reportTitle">' + escapeHtml(c.title) + '</div>'
788
861
  + (dateLine ? ('<div class="reportMeta">' + escapeHtml(dateLine) + '</div>') : '')
789
862
  + (impactLine ? ('<div class="help" style="margin-top:4px">' + escapeHtml(impactLine) + '</div>') : '')
790
- + c.body.filter((b)=> b!==dateLine && b!==impactLine).map((b) => '<div class="help" style="margin-top:4px">' + escapeHtml(b) + '</div>').join('');
863
+ + c.body.filter((b)=> b!==dateLine && b!==impactLine && b!==statusLine).map((b) => '<div class="help" style="margin-top:4px">' + escapeHtml(b) + '</div>').join('');
864
+
865
+ if (statusKey) {
866
+ const actions = document.createElement('div');
867
+ actions.className = 'reportActions';
868
+ actions.style.display = 'flex';
869
+ actions.style.gap = '8px';
870
+ actions.style.marginTop = '8px';
871
+ actions.style.flexWrap = 'wrap';
872
+
873
+ const label = statusKey === 'open' ? 'aberto' : (statusKey === 'mitigating' ? 'mitigando' : 'resolvido');
874
+ const pillClass = statusKey === 'resolved' ? 'ok' : (statusKey === 'mitigating' ? 'info' : 'warn');
875
+ const pill = document.createElement('span');
876
+ pill.className = 'pill ' + pillClass;
877
+ pill.textContent = label;
878
+ actions.appendChild(pill);
879
+
880
+ if (statusKey !== 'resolved') {
881
+ const btn = document.createElement('button');
882
+ btn.className = 'btn small';
883
+ btn.type = 'button';
884
+ btn.textContent = 'Marcar resolvido';
885
+ btn.onclick = async () => {
886
+ await api('/api/incidents/resolve', { dir: dirOrDefault(), title: c.title, index: idx });
887
+ await refreshIncidents();
888
+ };
889
+ actions.appendChild(btn);
890
+ }
891
+
892
+ card.appendChild(actions);
893
+ }
894
+
791
895
  el.appendChild(card);
792
896
  }
793
897
  }
@@ -814,10 +918,20 @@
814
918
  for (const it of items) {
815
919
  const row = document.createElement('div');
816
920
  row.className = 'rep';
921
+ const priority = String(it.priority || '').toLowerCase();
922
+ const pill = priority ? ('<span class="pill ' + (priority === 'high' ? 'warn' : (priority === 'medium' ? 'info' : '')) + '">' + escapeHtml(priority) + '</span>') : '';
923
+ const action = it.linkRel ? ('<button class="btn small" type="button" data-link="' + escapeHtml(it.linkRel) + '">Abrir status</button>') : '';
817
924
  row.innerHTML = '<div style="display:flex; justify-content:space-between; gap:10px; align-items:center">'
818
925
  + '<div style="min-width:0"><div style="font-weight:800">' + escapeHtml(it.slug || 'unassigned') + '</div>'
819
926
  + '<div class="help" style="margin-top:4px">Total: ' + escapeHtml(String(it.total)) + ' · Pendentes: ' + escapeHtml(String(it.pending)) + ' · Concluidas: ' + escapeHtml(String(it.completed)) + '</div></div>'
927
+ + '<div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap">' + pill + action + '</div>'
820
928
  + '</div>';
929
+ const btn = row.querySelector('button[data-link]');
930
+ if (btn) {
931
+ btn.onclick = async () => {
932
+ await api('/api/reports/open', { dir: dirOrDefault(), relPath: btn.getAttribute('data-link') });
933
+ };
934
+ }
821
935
  el.appendChild(row);
822
936
  }
823
937
  if (!items.length) {
@@ -1154,6 +1268,52 @@
1154
1268
  }
1155
1269
  }
1156
1270
 
1271
+ async function refreshExecutiveSummary() {
1272
+ const el = $('executiveSummary');
1273
+ if (el) el.textContent = 'Carregando resumo...';
1274
+ try {
1275
+ const r = await api('/api/summary/executive', { dir: dirOrDefault() });
1276
+ if (!el) return;
1277
+ el.textContent = r.summary || 'Sem resumo disponível.';
1278
+ } catch {
1279
+ if (el) el.textContent = 'Falha ao carregar resumo.';
1280
+ }
1281
+ }
1282
+
1283
+ async function refreshAnomalies() {
1284
+ const el = $('anomaliesBox');
1285
+ if (el) el.innerHTML = '<div class="help">Carregando anomalias...</div>';
1286
+ try {
1287
+ const r = await api('/api/anomalies', { dir: dirOrDefault() });
1288
+ if (!el) return;
1289
+ const anomalies = (r && r.anomalies) ? r.anomalies : {};
1290
+ const tasksMissing = anomalies.tasksMissingProject || { count: 0, samples: [] };
1291
+ const statusMissing = anomalies.statusMissingHistory || { count: 0, samples: [] };
1292
+
1293
+ const rows = [];
1294
+ const pushRow = (label, data) => {
1295
+ const status = data.count > 0 ? 'warn' : 'ok';
1296
+ const samples = (data.samples || []).slice(0, 5).map((s) => `<div class=\"help\" style=\"margin-top:4px\">${escapeHtml(s)}</div>`).join('');
1297
+ rows.push(
1298
+ `<div class=\"rep\">`
1299
+ + `<div style=\"display:flex; justify-content:space-between; gap:10px; align-items:center\">`
1300
+ + `<div style=\"min-width:0\"><div style=\"font-weight:800\">${escapeHtml(label)}</div>`
1301
+ + `<div class=\"help\" style=\"margin-top:4px\">${data.count} ocorrência(s)</div>`
1302
+ + `${samples}</div>`
1303
+ + `<div class=\"pill ${status}\">${status}</div>`
1304
+ + `</div>`
1305
+ + `</div>`
1306
+ );
1307
+ };
1308
+
1309
+ pushRow('Tarefas sem projectSlug', tasksMissing);
1310
+ pushRow('Status sem history', statusMissing);
1311
+ el.innerHTML = rows.join('') || '<div class="help">Sem anomalias.</div>';
1312
+ } catch {
1313
+ if (el) el.innerHTML = '<div class="help">Falha ao carregar anomalias.</div>';
1314
+ }
1315
+ }
1316
+
1157
1317
  async function doHealth() {
1158
1318
  try {
1159
1319
  saveLocal();
@@ -1546,6 +1706,8 @@
1546
1706
 
1547
1707
  if (isCompanionPage) {
1548
1708
  await refreshHealthChecklist();
1709
+ await refreshExecutiveSummary();
1710
+ await refreshAnomalies();
1549
1711
  await refreshIncidents();
1550
1712
  await refreshHeatmap();
1551
1713
  return;
@@ -1597,6 +1759,8 @@
1597
1759
  window.setTimelineKind = setTimelineKind;
1598
1760
  window.refreshBlockersInsights = refreshBlockersInsights;
1599
1761
  window.refreshHealthChecklist = refreshHealthChecklist;
1762
+ window.refreshExecutiveSummary = refreshExecutiveSummary;
1763
+ window.refreshAnomalies = refreshAnomalies;
1600
1764
  window.copyOut = copyOut;
1601
1765
  window.copyPath = copyPath;
1602
1766
  window.openSelected = openSelected;
package/cli/web.js CHANGED
@@ -255,6 +255,57 @@ function splitForDiscord(text, limit = 1900) {
255
255
  return parts;
256
256
  }
257
257
 
258
+ function parseIncidentMarkdown(md) {
259
+ const lines = String(md || '').split(/\r?\n/);
260
+ const items = [];
261
+ let current = null;
262
+ for (const line of lines) {
263
+ if (line.startsWith('- **')) {
264
+ if (current) items.push(current);
265
+ current = { title: line.replace('- **', '').replace('**', '').trim(), body: [] };
266
+ continue;
267
+ }
268
+ if (current && line.trim().startsWith('- ')) {
269
+ current.body.push(line.trim().replace(/^- /, ''));
270
+ }
271
+ }
272
+ if (current) items.push(current);
273
+ return items;
274
+ }
275
+
276
+ function serializeIncidentMarkdown(items) {
277
+ return (items || []).map((it) => {
278
+ const body = Array.isArray(it.body) ? it.body : [];
279
+ const lines = ['- **' + (it.title || 'Incidente') + '**'];
280
+ for (const b of body) lines.push('- ' + b);
281
+ return lines.join('\n');
282
+ }).join('\n');
283
+ }
284
+
285
+ function resolveIncidentInMarkdown(md, title, index) {
286
+ const items = parseIncidentMarkdown(md);
287
+ const matches = items.map((it, i) => ({ it, i })).filter((x) => x.it.title === title);
288
+ if (!matches.length) return null;
289
+ const pick = typeof index === 'number' && Number.isInteger(index) ? matches[index] : matches[0];
290
+ if (!pick) return null;
291
+ const entry = pick.it;
292
+ const body = Array.isArray(entry.body) ? entry.body : [];
293
+ const statusIdx = body.findIndex((b) => /^status\s*:/i.test(b));
294
+ if (statusIdx >= 0) body[statusIdx] = 'Status: resolved';
295
+ else body.push('Status: resolved');
296
+ entry.body = body;
297
+ return serializeIncidentMarkdown(items);
298
+ }
299
+
300
+ function normalizePriority(value) {
301
+ const raw = String(value || '').trim().toLowerCase();
302
+ if (!raw) return '';
303
+ if (['high', 'alta', 'alto', 'critical', 'critico', 'crítico'].includes(raw)) return 'high';
304
+ if (['medium', 'media', 'médio', 'medio'].includes(raw)) return 'medium';
305
+ if (['low', 'baixa', 'baixo'].includes(raw)) return 'low';
306
+ return '';
307
+ }
308
+
258
309
  function postJson(url, bodyObj) {
259
310
  return new Promise((resolve, reject) => {
260
311
  const u = new URL(url);
@@ -1360,6 +1411,11 @@ function buildTimelineHtml(safeDefault, appVersion) {
1360
1411
  <input id=\"timelineFilter\" placeholder=\"filtrar (tag, projeto, tipo)\" oninput=\"renderTimeline()\" />
1361
1412
  </section>
1362
1413
 
1414
+ <section class=\"reportsTools\" style=\"display:grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px;\">
1415
+ <select id=\"timelineProject\" onchange=\"setTimelineProject(this.value)\"></select>
1416
+ <select id=\"timelineTag\" onchange=\"setTimelineTag(this.value)\"></select>
1417
+ </section>
1418
+
1363
1419
  <section class=\"reportsGrid\" id=\"timelineGrid\"></section>
1364
1420
  </div>
1365
1421
  </main>
@@ -1443,6 +1499,26 @@ function buildCompanionHtml(safeDefault, appVersion) {
1443
1499
 
1444
1500
  <section class="reportsGrid" id="healthChecklist"></section>
1445
1501
 
1502
+ <section class="panel" style="margin-top:16px">
1503
+ <div class="panelHead" style="display:flex; align-items:center; justify-content:space-between; gap:10px">
1504
+ <b>Resumo Executivo</b>
1505
+ <button class="btn small" type="button" onclick="refreshExecutiveSummary()">Atualizar</button>
1506
+ </div>
1507
+ <div class="panelBody">
1508
+ <div id="executiveSummary" class="help"></div>
1509
+ </div>
1510
+ </section>
1511
+
1512
+ <section class="panel" style="margin-top:16px">
1513
+ <div class="panelHead" style="display:flex; align-items:center; justify-content:space-between; gap:10px">
1514
+ <b>Anomalias</b>
1515
+ <button class="btn small" type="button" onclick="refreshAnomalies()">Atualizar</button>
1516
+ </div>
1517
+ <div class="panelBody">
1518
+ <div id="anomaliesBox"></div>
1519
+ </div>
1520
+ </section>
1521
+
1446
1522
  <section class="panel" style="margin-top:16px">
1447
1523
  <div class="panelHead"><b>Incident Radar</b></div>
1448
1524
  <div class="panelBody">
@@ -1621,6 +1697,62 @@ function readJsonOrNull(p) {
1621
1697
  }
1622
1698
  }
1623
1699
 
1700
+ function truncateText(text, maxLen) {
1701
+ const str = String(text || '').trim();
1702
+ if (str.length <= maxLen) return str;
1703
+ return str.slice(0, Math.max(0, maxLen - 3)).trimEnd() + '...';
1704
+ }
1705
+
1706
+ function getTimelineItems(workspaceDir) {
1707
+ const items = [];
1708
+ const dailyDir = path.join(workspaceDir, 'logs', 'daily');
1709
+ if (exists(dailyDir)) {
1710
+ const files = fs.readdirSync(dailyDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f));
1711
+ for (const f of files) {
1712
+ const date = f.replace('.md', '');
1713
+ const full = path.join(dailyDir, f);
1714
+ const body = fs.readFileSync(full, 'utf8');
1715
+ items.push({ kind: 'daily', date, title: `Daily ${date}`, content: body.slice(0, 500) });
1716
+ }
1717
+ }
1718
+ const base = path.join(workspaceDir, 'data', 'Clients');
1719
+ if (exists(base)) {
1720
+ const stack = [base];
1721
+ while (stack.length) {
1722
+ const dirp = stack.pop();
1723
+ const entries = fs.readdirSync(dirp, { withFileTypes: true });
1724
+ for (const ent of entries) {
1725
+ const full = path.join(dirp, ent.name);
1726
+ if (ent.isDirectory()) stack.push(full);
1727
+ else if (ent.isFile() && ent.name === 'status.json') {
1728
+ const doc = readJsonOrNull(full) || {};
1729
+ const slug = path.relative(base, path.dirname(full)).replace(/\\/g, '/');
1730
+ const hist = Array.isArray(doc.history) ? doc.history : [];
1731
+ for (const h of hist) {
1732
+ items.push({
1733
+ kind: 'status',
1734
+ date: h.date || '',
1735
+ title: `${doc.project || slug} (${h.type || 'Status'})`,
1736
+ content: h.content || '',
1737
+ tags: h.tags || [],
1738
+ slug
1739
+ });
1740
+ }
1741
+ }
1742
+ }
1743
+ }
1744
+ }
1745
+ const taskFile = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
1746
+ const taskDoc = readJsonOrNull(taskFile) || { tasks: [] };
1747
+ const tasks = Array.isArray(taskDoc.tasks) ? taskDoc.tasks : [];
1748
+ for (const t of tasks) {
1749
+ if (t.createdAt) items.push({ kind: 'task', date: String(t.createdAt).slice(0, 10), title: `Task criada: ${t.description || t.id}`, content: t.projectSlug || '' });
1750
+ if (t.completedAt) items.push({ kind: 'task', date: String(t.completedAt).slice(0, 10), title: `Task concluida: ${t.description || t.id}`, content: t.projectSlug || '' });
1751
+ }
1752
+ items.sort((a, b) => String(b.date || '').localeCompare(String(a.date || '')));
1753
+ return items;
1754
+ }
1755
+
1624
1756
  function writeJson(p, obj) {
1625
1757
  ensureDir(path.dirname(p));
1626
1758
  fs.writeFileSync(p, JSON.stringify(obj, null, 2) + '\n', 'utf8');
@@ -1935,17 +2067,72 @@ async function cmdWeb({ port, dir, open, dev }) {
1935
2067
  }
1936
2068
 
1937
2069
  if (req.url === '/api/timeline') {
1938
- const items = [];
1939
- const dailyDir = path.join(workspaceDir, 'logs', 'daily');
1940
- if (exists(dailyDir)) {
1941
- const files = fs.readdirSync(dailyDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f));
1942
- for (const f of files) {
1943
- const date = f.replace('.md', '');
1944
- const full = path.join(dailyDir, f);
1945
- const body = fs.readFileSync(full, 'utf8');
1946
- items.push({ kind: 'daily', date, title: `Daily ${date}`, content: body.slice(0, 500) });
2070
+ const items = getTimelineItems(workspaceDir);
2071
+ return safeJson(res, 200, { ok: true, items });
2072
+ }
2073
+
2074
+ if (req.url === '/api/summary/executive') {
2075
+ const items = getTimelineItems(workspaceDir);
2076
+ const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
2077
+ const recent = items.filter((it) => {
2078
+ const ts = Date.parse(it.date || '');
2079
+ return Number.isFinite(ts) && ts >= cutoff;
2080
+ });
2081
+ const counts = { status: 0, taskCreated: 0, taskCompleted: 0, daily: 0 };
2082
+ for (const it of recent) {
2083
+ if (it.kind === 'status') counts.status++;
2084
+ else if (it.kind === 'daily') counts.daily++;
2085
+ else if (it.kind === 'task' && String(it.title || '').toLowerCase().includes('concluida')) counts.taskCompleted++;
2086
+ else if (it.kind === 'task') counts.taskCreated++;
2087
+ }
2088
+
2089
+ const blockerFile = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
2090
+ const blockerDoc = readJsonOrNull(blockerFile) || { blockers: [] };
2091
+ const openBlockers = (blockerDoc.blockers || []).filter((b) => b && String(b.status || '').trim() === 'OPEN');
2092
+
2093
+ let summary = '';
2094
+ if (!recent.length) {
2095
+ summary = 'Sem eventos recentes na timeline nos últimos 7 dias.';
2096
+ } else {
2097
+ const parts = [];
2098
+ if (counts.taskCompleted) parts.push(`${counts.taskCompleted} tarefa(s) concluida(s)`);
2099
+ if (counts.taskCreated) parts.push(`${counts.taskCreated} tarefa(s) criada(s)`);
2100
+ if (counts.status) parts.push(`${counts.status} update(s) de status`);
2101
+ if (!parts.length && counts.daily) parts.push(`${counts.daily} daily(s)`);
2102
+ const head = parts.length ? parts.join(', ') : `${recent.length} eventos`;
2103
+ summary = `Resumo: ${head} nos últimos 7 dias.`;
2104
+ }
2105
+
2106
+ const blockersSentence = openBlockers.length
2107
+ ? `Há ${openBlockers.length} blocker(s) aberto(s).`
2108
+ : 'Sem blockers abertos.';
2109
+
2110
+ summary = `${summary} ${blockersSentence}`.trim();
2111
+ summary = truncateText(summary, 260);
2112
+ return safeJson(res, 200, { ok: true, summary, stats: { recent: recent.length, openBlockers: openBlockers.length, ...counts } });
2113
+ }
2114
+
2115
+ if (req.url === '/api/anomalies') {
2116
+ const anomalies = {
2117
+ tasksMissingProject: { count: 0, samples: [] },
2118
+ statusMissingHistory: { count: 0, samples: [] }
2119
+ };
2120
+
2121
+ const taskFile = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
2122
+ if (exists(taskFile)) {
2123
+ const taskDoc = readJsonOrNull(taskFile) || { tasks: [] };
2124
+ const tasks = Array.isArray(taskDoc.tasks) ? taskDoc.tasks : [];
2125
+ for (const t of tasks) {
2126
+ const slug = String(t.projectSlug || '').trim();
2127
+ if (!slug) {
2128
+ anomalies.tasksMissingProject.count++;
2129
+ if (anomalies.tasksMissingProject.samples.length < 5) {
2130
+ anomalies.tasksMissingProject.samples.push(`data/tasks/task-log.json::${t.id || t.description || 'task'}`);
2131
+ }
2132
+ }
1947
2133
  }
1948
2134
  }
2135
+
1949
2136
  const base = path.join(workspaceDir, 'data', 'Clients');
1950
2137
  if (exists(base)) {
1951
2138
  const stack = [base];
@@ -1957,31 +2144,31 @@ if (req.url === '/api/timeline') {
1957
2144
  if (ent.isDirectory()) stack.push(full);
1958
2145
  else if (ent.isFile() && ent.name === 'status.json') {
1959
2146
  const doc = readJsonOrNull(full) || {};
1960
- const slug = path.relative(base, path.dirname(full)).replace(/\\/g, '/');
1961
- const hist = Array.isArray(doc.history) ? doc.history : [];
1962
- for (const h of hist) {
1963
- items.push({
1964
- kind: 'status',
1965
- date: h.date || '',
1966
- title: `${doc.project || slug} (${h.type || 'Status'})`,
1967
- content: h.content || '',
1968
- tags: h.tags || [],
1969
- slug
1970
- });
2147
+ if (!Array.isArray(doc.history)) {
2148
+ anomalies.statusMissingHistory.count++;
2149
+ if (anomalies.statusMissingHistory.samples.length < 5) {
2150
+ anomalies.statusMissingHistory.samples.push(path.relative(workspaceDir, full).replace(/\\/g, '/'));
2151
+ }
1971
2152
  }
1972
2153
  }
1973
2154
  }
1974
2155
  }
1975
2156
  }
1976
- const taskFile = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
1977
- const taskDoc = readJsonOrNull(taskFile) || { tasks: [] };
1978
- const tasks = Array.isArray(taskDoc.tasks) ? taskDoc.tasks : [];
1979
- for (const t of tasks) {
1980
- if (t.createdAt) items.push({ kind: 'task', date: String(t.createdAt).slice(0, 10), title: `Task criada: ${t.description || t.id}`, content: t.projectSlug || '' });
1981
- if (t.completedAt) items.push({ kind: 'task', date: String(t.completedAt).slice(0, 10), title: `Task concluida: ${t.description || t.id}`, content: t.projectSlug || '' });
1982
- }
1983
- items.sort((a, b) => String(b.date || '').localeCompare(String(a.date || '')));
1984
- return safeJson(res, 200, { ok: true, items });
2157
+
2158
+ return safeJson(res, 200, { ok: true, anomalies });
2159
+ }
2160
+
2161
+ if (req.url === '/api/incidents/resolve') {
2162
+ const title = payload.title;
2163
+ const index = Number.isInteger(payload.index) ? payload.index : null;
2164
+ if (!title) return safeJson(res, 400, { error: 'Missing title' });
2165
+ const p = path.join(workspaceDir, 'docs', 'reports', 'fidelizacao-incident-index.md');
2166
+ if (!exists(p)) return safeJson(res, 404, { error: 'Incident index not found' });
2167
+ const md = fs.readFileSync(p, 'utf8');
2168
+ const updated = resolveIncidentInMarkdown(md, title, index);
2169
+ if (!updated) return safeJson(res, 404, { error: 'Incident not found' });
2170
+ fs.writeFileSync(p, updated, 'utf8');
2171
+ return safeJson(res, 200, { ok: true });
1985
2172
  }
1986
2173
 
1987
2174
  if (req.url === '/api/incidents') {
@@ -1996,13 +2183,20 @@ if (req.url === '/api/timeline') {
1996
2183
  const doc = readJsonOrNull(file) || { tasks: [] };
1997
2184
  const tasks = Array.isArray(doc.tasks) ? doc.tasks : [];
1998
2185
  const map = {};
2186
+ const priorityRank = { high: 3, medium: 2, low: 1, '': 0 };
1999
2187
  for (const t of tasks) {
2000
2188
  const slug = t.projectSlug || 'unassigned';
2001
- if (!map[slug]) map[slug] = { total: 0, pending: 0, completed: 0 };
2189
+ if (!map[slug]) map[slug] = { total: 0, pending: 0, completed: 0, priority: '' };
2002
2190
  map[slug].total++;
2003
2191
  if (t.status === 'COMPLETED') map[slug].completed++; else map[slug].pending++;
2192
+ const p = normalizePriority(t.priority || t.severity);
2193
+ if (priorityRank[p] > priorityRank[map[slug].priority || '']) map[slug].priority = p;
2004
2194
  }
2005
- const items = Object.entries(map).map(([slug, v]) => ({ slug, ...v }));
2195
+ const items = Object.entries(map).map(([slug, v]) => {
2196
+ const statusPath = path.join(workspaceDir, 'data', 'Clients', slug, 'status.json');
2197
+ const linkRel = exists(statusPath) ? path.relative(workspaceDir, statusPath).replace(/\\/g, '/') : '';
2198
+ return { slug, ...v, linkRel };
2199
+ });
2006
2200
  items.sort((a, b) => b.total - a.total);
2007
2201
  return safeJson(res, 200, { ok: true, items });
2008
2202
  }
@@ -2788,6 +2982,19 @@ if (req.url === '/api/reports/list') {
2788
2982
  return safeJson(res, 200, { ok: true, items, stats: { pendingTasks, openBlockers, reportsToday, reportsTotal: reports.length } });
2789
2983
  }
2790
2984
 
2985
+ if (req.url === '/api/incidents/resolve') {
2986
+ const title = payload.title;
2987
+ const index = Number.isInteger(payload.index) ? payload.index : null;
2988
+ if (!title) return safeJson(res, 400, { error: 'Missing title' });
2989
+ const p = path.join(workspaceDir, 'docs', 'reports', 'fidelizacao-incident-index.md');
2990
+ if (!exists(p)) return safeJson(res, 404, { error: 'Incident index not found' });
2991
+ const md = fs.readFileSync(p, 'utf8');
2992
+ const updated = resolveIncidentInMarkdown(md, title, index);
2993
+ if (!updated) return safeJson(res, 404, { error: 'Incident not found' });
2994
+ fs.writeFileSync(p, updated, 'utf8');
2995
+ return safeJson(res, 200, { ok: true });
2996
+ }
2997
+
2791
2998
  if (req.url === '/api/incidents') {
2792
2999
  const p = path.join(workspaceDir, 'docs', 'reports', 'fidelizacao-incident-index.md');
2793
3000
  if (!exists(p)) return safeJson(res, 200, { ok: true, markdown: '' });
@@ -2800,13 +3007,20 @@ if (req.url === '/api/reports/list') {
2800
3007
  const doc = readJsonOrNull(file) || { tasks: [] };
2801
3008
  const tasks = Array.isArray(doc.tasks) ? doc.tasks : [];
2802
3009
  const map = {};
3010
+ const priorityRank = { high: 3, medium: 2, low: 1, '': 0 };
2803
3011
  for (const t of tasks) {
2804
3012
  const slug = t.projectSlug || 'unassigned';
2805
- if (!map[slug]) map[slug] = { total: 0, pending: 0, completed: 0 };
3013
+ if (!map[slug]) map[slug] = { total: 0, pending: 0, completed: 0, priority: '' };
2806
3014
  map[slug].total++;
2807
3015
  if (t.status === 'COMPLETED') map[slug].completed++; else map[slug].pending++;
3016
+ const p = normalizePriority(t.priority || t.severity);
3017
+ if (priorityRank[p] > priorityRank[map[slug].priority || '']) map[slug].priority = p;
2808
3018
  }
2809
- const items = Object.entries(map).map(([slug, v]) => ({ slug, ...v }));
3019
+ const items = Object.entries(map).map(([slug, v]) => {
3020
+ const statusPath = path.join(workspaceDir, 'data', 'Clients', slug, 'status.json');
3021
+ const linkRel = exists(statusPath) ? path.relative(workspaceDir, statusPath).replace(/\\/g, '/') : '';
3022
+ return { slug, ...v, linkRel };
3023
+ });
2810
3024
  items.sort((a, b) => b.total - a.total);
2811
3025
  return safeJson(res, 200, { ok: true, items });
2812
3026
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "2.3.6",
3
+ "version": "2.3.7",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js && node scripts/validate-structure.js",