@cccarv82/freya 2.2.1 → 2.3.1

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 +99 -0
  2. package/cli/web.js +245 -1
  3. package/package.json +1 -1
package/cli/web-ui.js CHANGED
@@ -698,6 +698,87 @@
698
698
  }
699
699
  }
700
700
 
701
+ function renderTimeline() {
702
+ const el = $('timelineGrid');
703
+ if (!el) return;
704
+ const filter = String(($('timelineFilter') && $('timelineFilter').value) || '').toLowerCase();
705
+ const items = Array.isArray(state.timeline) ? state.timeline : [];
706
+ const filtered = items.filter((i) => {
707
+ const hay = [i.kind, i.title, i.content, (i.tags || []).join(' ')].join(' ').toLowerCase();
708
+ return !filter || hay.includes(filter);
709
+ });
710
+ el.innerHTML = '';
711
+ for (const it of filtered) {
712
+ const card = document.createElement('div');
713
+ card.className = 'reportCard';
714
+ card.innerHTML = '<div class="reportHead">'
715
+ + '<div><div class="reportTitle">' + escapeHtml(it.title || 'Evento') + '</div>'
716
+ + '<div class="reportMeta">' + escapeHtml(it.date || '') + ' · ' + escapeHtml(it.kind || '') + '</div></div>'
717
+ + '</div>'
718
+ + '<div class="help" style="margin-top:8px">' + escapeHtml(it.content || '') + '</div>';
719
+ el.appendChild(card);
720
+ }
721
+ if (!filtered.length) {
722
+ const empty = document.createElement('div');
723
+ empty.className = 'help';
724
+ empty.textContent = 'Nenhum evento encontrado.';
725
+ el.appendChild(empty);
726
+ }
727
+ }
728
+
729
+ async function refreshTimeline() {
730
+ try {
731
+ const r = await api('/api/timeline', { dir: dirOrDefault() });
732
+ state.timeline = r.items || [];
733
+ renderTimeline();
734
+ } catch (e) {
735
+ const el = $('timelineGrid');
736
+ if (el) el.textContent = 'Falha ao carregar timeline.';
737
+ }
738
+ }
739
+
740
+ async function refreshIncidents() {
741
+ try {
742
+ const r = await api('/api/incidents', { dir: dirOrDefault() });
743
+ const el = $('incidentsBox');
744
+ if (el) {
745
+ const md = r.markdown || '';
746
+ el.innerHTML = md ? renderMarkdown(md) : '<div class="help">Nenhum incidente registrado.</div>';
747
+ }
748
+ } catch {
749
+ const el = $('incidentsBox');
750
+ if (el) el.textContent = 'Falha ao carregar incidentes.';
751
+ }
752
+ }
753
+
754
+ async function refreshHeatmap() {
755
+ try {
756
+ const r = await api('/api/tasks/heatmap', { dir: dirOrDefault() });
757
+ const el = $('heatmapGrid');
758
+ if (!el) return;
759
+ el.innerHTML = '';
760
+ const items = r.items || [];
761
+ for (const it of items) {
762
+ const row = document.createElement('div');
763
+ row.className = 'rep';
764
+ row.innerHTML = '<div style="display:flex; justify-content:space-between; gap:10px; align-items:center">'
765
+ + '<div style="min-width:0"><div style="font-weight:800">' + escapeHtml(it.slug || 'unassigned') + '</div>'
766
+ + '<div class="help" style="margin-top:4px">Total: ' + escapeHtml(String(it.total)) + ' · Pendentes: ' + escapeHtml(String(it.pending)) + ' · Concluidas: ' + escapeHtml(String(it.completed)) + '</div></div>'
767
+ + '</div>';
768
+ el.appendChild(row);
769
+ }
770
+ if (!items.length) {
771
+ const empty = document.createElement('div');
772
+ empty.className = 'help';
773
+ empty.textContent = 'Sem dados de tasks.';
774
+ el.appendChild(empty);
775
+ }
776
+ } catch {
777
+ const el = $('heatmapGrid');
778
+ if (el) el.textContent = 'Falha ao carregar heatmap.';
779
+ }
780
+ }
781
+
701
782
  async function refreshReportsPage() {
702
783
  try {
703
784
  setPill('run', 'carregando…');
@@ -723,6 +804,7 @@
723
804
  const dash = $('railDashboard');
724
805
  const rep = $('railReports');
725
806
  const proj = $('railProjects');
807
+ const tl = $('railTimeline');
726
808
  const health = $('railCompanion');
727
809
  if (dash) {
728
810
  dash.onclick = () => {
@@ -747,6 +829,12 @@
747
829
  if (!isProjects) window.location.href = '/projects';
748
830
  };
749
831
  }
832
+ if (tl) {
833
+ tl.onclick = () => {
834
+ const isTimeline = document.body && document.body.dataset && document.body.dataset.page === 'timeline';
835
+ if (!isTimeline) window.location.href = '/timeline';
836
+ };
837
+ }
750
838
  if (health) {
751
839
  health.onclick = () => {
752
840
  const isHealth = document.body && document.body.dataset && document.body.dataset.page === 'companion';
@@ -1364,6 +1452,7 @@
1364
1452
 
1365
1453
  const isReportsPage = document.body && document.body.dataset && document.body.dataset.page === 'reports';
1366
1454
  const isProjectsPage = document.body && document.body.dataset && document.body.dataset.page === 'projects';
1455
+ const isTimelinePage = document.body && document.body.dataset && document.body.dataset.page === 'timeline';
1367
1456
  const isCompanionPage = document.body && document.body.dataset && document.body.dataset.page === 'companion';
1368
1457
 
1369
1458
  // Load persisted settings from the workspace + bootstrap (auto-init + auto-health)
@@ -1397,8 +1486,15 @@
1397
1486
  return;
1398
1487
  }
1399
1488
 
1489
+ if (isTimelinePage) {
1490
+ await refreshTimeline();
1491
+ return;
1492
+ }
1493
+
1400
1494
  if (isCompanionPage) {
1401
1495
  await refreshHealthChecklist();
1496
+ await refreshIncidents();
1497
+ await refreshHeatmap();
1402
1498
  return;
1403
1499
  }
1404
1500
 
@@ -1441,6 +1537,9 @@
1441
1537
  window.renderReportsPage = renderReportsPage;
1442
1538
  window.refreshReportsPage = refreshReportsPage;
1443
1539
  window.refreshProjects = refreshProjects;
1540
+ window.refreshTimeline = refreshTimeline;
1541
+ window.refreshIncidents = refreshIncidents;
1542
+ window.refreshHeatmap = refreshHeatmap;
1444
1543
  window.refreshBlockersInsights = refreshBlockersInsights;
1445
1544
  window.refreshHealthChecklist = refreshHealthChecklist;
1446
1545
  window.copyOut = copyOut;
package/cli/web.js CHANGED
@@ -911,6 +911,7 @@ function buildHtml(safeDefault, appVersion) {
911
911
  <button class="railBtn" id="railReports" type="button" title="Relatórios">R</button>
912
912
  <button class="railBtn" id="railCompanion" type="button" title="Companion">C</button>
913
913
  <button class="railBtn" id="railProjects" type="button" title="Projects">P</button>
914
+ <button class="railBtn" id="railTimeline" type="button" title="Timeline">T</button>
914
915
  </div>
915
916
  <div class="railBottom">
916
917
  <div class="railStatus" id="railStatus" title="status"></div>
@@ -1162,6 +1163,7 @@ function buildReportsHtml(safeDefault, appVersion) {
1162
1163
  <button class="railBtn active" id="railReports" type="button" title="Relatórios">R</button>
1163
1164
  <button class="railBtn" id="railCompanion" type="button" title="Companion">C</button>
1164
1165
  <button class="railBtn" id="railProjects" type="button" title="Projects">P</button>
1166
+ <button class="railBtn" id="railTimeline" type="button" title="Timeline">T</button>
1165
1167
  </div>
1166
1168
  <div class="railBottom">
1167
1169
  <div class="railStatus" id="railStatus" title="status"></div>
@@ -1240,6 +1242,7 @@ function buildProjectsHtml(safeDefault, appVersion) {
1240
1242
  <button class="railBtn" id="railReports" type="button" title="Relatorios">R</button>
1241
1243
  <button class="railBtn" id="railCompanion" type="button" title="Companion">C</button>
1242
1244
  <button class="railBtn active" id="railProjects" type="button" title="Projects">P</button>
1245
+ <button class="railBtn" id="railTimeline" type="button" title="Timeline">T</button>
1243
1246
  </div>
1244
1247
  <div class="railBottom">
1245
1248
  <div class="railStatus" id="railStatus" title="status"></div>
@@ -1294,6 +1297,85 @@ function buildProjectsHtml(safeDefault, appVersion) {
1294
1297
  </html>`;
1295
1298
  }
1296
1299
 
1300
+ function buildTimelineHtml(safeDefault, appVersion) {
1301
+ const safeVersion = escapeHtml(appVersion || 'unknown');
1302
+ return `<!doctype html>
1303
+ <html>
1304
+ <head>
1305
+ <meta charset=\"utf-8\" />
1306
+ <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
1307
+ <title>Timeline</title>
1308
+ <link rel=\"stylesheet\" href=\"/app.css\" />
1309
+ </head>
1310
+ <body data-page=\"timeline\">
1311
+ <div class=\"app\">
1312
+ <div class=\"frame\">
1313
+ <div class=\"shell\">
1314
+
1315
+ <aside class=\"rail\">
1316
+ <div class=\"railTop\">
1317
+ <div class=\"railLogo\">F</div>
1318
+ </div>
1319
+ <div class=\"railNav\">
1320
+ <button class=\"railBtn\" id=\"railDashboard\" type=\"button\" title=\"Dashboard\">D</button>
1321
+ <button class=\"railBtn\" id=\"railReports\" type=\"button\" title=\"Relatorios\">R</button>
1322
+ <button class=\"railBtn\" id=\"railCompanion\" type=\"button\" title=\"Companion\">C</button>
1323
+ <button class=\"railBtn\" id=\"railProjects\" type=\"button\" title=\"Projects\">P</button>
1324
+ <button class=\"railBtn active\" id=\"railTimeline\" type=\"button\" title=\"Timeline\">T</button>
1325
+ </div>
1326
+ <div class=\"railBottom\">
1327
+ <div class=\"railStatus\" id=\"railStatus\" title=\"status\"></div>
1328
+ </div>
1329
+ </aside>
1330
+
1331
+ <main class=\"center reportsPage\" id=\"timelinePage\">
1332
+ <div class=\"topbar\">
1333
+ <div class=\"brandLine\">
1334
+ <span class=\"spark\"></span>
1335
+ <div class=\"brandStack\">
1336
+ <div class=\"brand\">FREYA</div>
1337
+ <div class=\"brandSub\">Timeline</div>
1338
+ </div>
1339
+ </div>
1340
+ <div class=\"topActions\">
1341
+ <span class=\"chip\" id=\"chipVersion\">v${safeVersion}</span>
1342
+ <span class=\"chip\" id=\"chipPort\">127.0.0.1:3872</span>
1343
+ </div>
1344
+ </div>
1345
+
1346
+ <div class=\"centerBody\">
1347
+ <input id=\"dir\" type=\"hidden\" />
1348
+
1349
+ <section class=\"reportsHeader\">
1350
+ <div>
1351
+ <div class=\"reportsTitle\">Timeline</div>
1352
+ <div class=\"reportsSubtitle\">Eventos do dia a dia, status e tarefas em ordem cronologica.</div>
1353
+ </div>
1354
+ <div class=\"reportsActions\">
1355
+ <button class=\"btn small\" type=\"button\" onclick=\"refreshTimeline()\">Atualizar</button>
1356
+ </div>
1357
+ </section>
1358
+
1359
+ <section class=\"reportsTools\">
1360
+ <input id=\"timelineFilter\" placeholder=\"filtrar (tag, projeto, tipo)\" oninput=\"renderTimeline()\" />
1361
+ </section>
1362
+
1363
+ <section class=\"reportsGrid\" id=\"timelineGrid\"></section>
1364
+ </div>
1365
+ </main>
1366
+
1367
+ </div>
1368
+ </div>
1369
+ </div>
1370
+
1371
+ <script>
1372
+ window.__FREYA_DEFAULT_DIR = \"${safeDefault}\";
1373
+ </script>
1374
+ <script src=\"/app.js\"></script>
1375
+ </body>
1376
+ </html>`;
1377
+ }
1378
+
1297
1379
  function buildCompanionHtml(safeDefault, appVersion) {
1298
1380
  const safeVersion = escapeHtml(appVersion || 'unknown');
1299
1381
  return `<!doctype html>
@@ -1318,6 +1400,7 @@ function buildCompanionHtml(safeDefault, appVersion) {
1318
1400
  <button class="railBtn" id="railReports" type="button" title="Relatórios">R</button>
1319
1401
  <button class="railBtn active" id="railCompanion" type="button" title="Companion">C</button>
1320
1402
  <button class="railBtn" id="railProjects" type="button" title="Projects">P</button>
1403
+ <button class="railBtn" id="railTimeline" type="button" title="Timeline">T</button>
1321
1404
  </div>
1322
1405
  <div class="railBottom">
1323
1406
  <div class="railStatus" id="railStatus" title="status"></div>
@@ -1360,6 +1443,20 @@ function buildCompanionHtml(safeDefault, appVersion) {
1360
1443
 
1361
1444
  <section class="reportsGrid" id="healthChecklist"></section>
1362
1445
 
1446
+ <section class="panel" style="margin-top:16px">
1447
+ <div class="panelHead"><b>Incident Radar</b></div>
1448
+ <div class="panelBody">
1449
+ <div id="incidentsBox" class="log md" style="font-family: var(--sans);"></div>
1450
+ </div>
1451
+ </section>
1452
+
1453
+ <section class="panel" style="margin-top:16px">
1454
+ <div class="panelHead"><b>Task Heatmap</b></div>
1455
+ <div class="panelBody">
1456
+ <div id="heatmapGrid"></div>
1457
+ </div>
1458
+ </section>
1459
+
1363
1460
  <section class="panel" style="margin-top:16px">
1364
1461
  <div class="panelHead"><b>Saida</b></div>
1365
1462
  <div class="panelBody">
@@ -1710,6 +1807,14 @@ async function cmdWeb({ port, dir, open, dev }) {
1710
1807
  return;
1711
1808
  }
1712
1809
 
1810
+ if (req.method === 'GET' && req.url === '/timeline') {
1811
+ try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch {}
1812
+ const body = timelineHtml(dir || './freya');
1813
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
1814
+ res.end(body);
1815
+ return;
1816
+ }
1817
+
1713
1818
  if (req.method === 'GET' && req.url === '/app.css') {
1714
1819
  const css = fs.readFileSync(path.join(__dirname, 'web-ui.css'), 'utf8');
1715
1820
  res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8', 'Cache-Control': 'no-store' });
@@ -1829,6 +1934,79 @@ async function cmdWeb({ port, dir, open, dev }) {
1829
1934
  return safeJson(res, 200, { ok: true, projects: items });
1830
1935
  }
1831
1936
 
1937
+ 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) });
1947
+ }
1948
+ }
1949
+ const base = path.join(workspaceDir, 'data', 'Clients');
1950
+ if (exists(base)) {
1951
+ const stack = [base];
1952
+ while (stack.length) {
1953
+ const dirp = stack.pop();
1954
+ const entries = fs.readdirSync(dirp, { withFileTypes: true });
1955
+ for (const ent of entries) {
1956
+ const full = path.join(dirp, ent.name);
1957
+ if (ent.isDirectory()) stack.push(full);
1958
+ else if (ent.isFile() && ent.name === 'status.json') {
1959
+ 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
+ });
1971
+ }
1972
+ }
1973
+ }
1974
+ }
1975
+ }
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 });
1985
+ }
1986
+
1987
+ if (req.url === '/api/incidents') {
1988
+ const p = path.join(workspaceDir, 'docs', 'reports', 'fidelizacao-incident-index.md');
1989
+ if (!exists(p)) return safeJson(res, 200, { ok: true, markdown: '' });
1990
+ const md = fs.readFileSync(p, 'utf8');
1991
+ return safeJson(res, 200, { ok: true, markdown: md });
1992
+ }
1993
+
1994
+ if (req.url === '/api/tasks/heatmap') {
1995
+ const file = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
1996
+ const doc = readJsonOrNull(file) || { tasks: [] };
1997
+ const tasks = Array.isArray(doc.tasks) ? doc.tasks : [];
1998
+ const map = {};
1999
+ for (const t of tasks) {
2000
+ const slug = t.projectSlug || 'unassigned';
2001
+ if (!map[slug]) map[slug] = { total: 0, pending: 0, completed: 0 };
2002
+ map[slug].total++;
2003
+ if (t.status === 'COMPLETED') map[slug].completed++; else map[slug].pending++;
2004
+ }
2005
+ const items = Object.entries(map).map(([slug, v]) => ({ slug, ...v }));
2006
+ items.sort((a, b) => b.total - a.total);
2007
+ return safeJson(res, 200, { ok: true, items });
2008
+ }
2009
+
1832
2010
  if (req.url === '/api/reports/list') {
1833
2011
  const reports = listReports(workspaceDir);
1834
2012
  return safeJson(res, 200, { reports });
@@ -1892,6 +2070,49 @@ if (req.url === '/api/reports/list') {
1892
2070
  return safeJson(res, 200, { ok: true, relPath: rel });
1893
2071
  }
1894
2072
 
2073
+ function autoLinkNotes(textInput) {
2074
+ try {
2075
+ const lc = textInput.toLowerCase();
2076
+ const projectsDir = path.join(workspaceDir, 'docs', 'projects');
2077
+ const links = [];
2078
+
2079
+ if (exists(projectsDir)) {
2080
+ const files = fs.readdirSync(projectsDir).filter((f) => f.endsWith('.md'));
2081
+ for (const f of files) {
2082
+ const name = f.replace('.md', '');
2083
+ const full = path.join(projectsDir, f);
2084
+ const txt = fs.readFileSync(full, 'utf8');
2085
+ const m = txt.match(/DataPath:\s*data\/Clients\/(.+?)\//i);
2086
+ const slug = m ? m[1].toLowerCase() : name.toLowerCase();
2087
+ if (lc.includes(slug)) links.push('[[' + name + ']]');
2088
+ }
2089
+ }
2090
+
2091
+ const base = path.join(workspaceDir, 'data', 'Clients');
2092
+ if (exists(base)) {
2093
+ const stack = [base];
2094
+ while (stack.length) {
2095
+ const dirp = stack.pop();
2096
+ const entries = fs.readdirSync(dirp, { withFileTypes: true });
2097
+ for (const ent of entries) {
2098
+ const full = path.join(dirp, ent.name);
2099
+ if (ent.isDirectory()) stack.push(full);
2100
+ else if (ent.isFile() && ent.name === 'status.json') {
2101
+ const slug = path.relative(base, path.dirname(full)).replace(/\\/g, '/').toLowerCase();
2102
+ if (lc.includes(slug)) links.push('[[' + slug + ']]');
2103
+ }
2104
+ }
2105
+ }
2106
+ }
2107
+
2108
+ const uniq = Array.from(new Set(links));
2109
+ if (!uniq.length) return '';
2110
+ return '\n\nLinks: ' + uniq.join(' ');
2111
+ } catch {
2112
+ return '';
2113
+ }
2114
+ }
2115
+
1895
2116
  if (req.url === '/api/inbox/add') {
1896
2117
  const text = String(payload.text || '').trim();
1897
2118
  if (!text) return safeJson(res, 400, { error: 'Missing text' });
@@ -2567,7 +2788,30 @@ if (req.url === '/api/reports/list') {
2567
2788
  return safeJson(res, 200, { ok: true, items, stats: { pendingTasks, openBlockers, reportsToday, reportsTotal: reports.length } });
2568
2789
  }
2569
2790
 
2570
- if (req.url === '/api/blockers/summary') {
2791
+ if (req.url === '/api/incidents') {
2792
+ const p = path.join(workspaceDir, 'docs', 'reports', 'fidelizacao-incident-index.md');
2793
+ if (!exists(p)) return safeJson(res, 200, { ok: true, markdown: '' });
2794
+ const md = fs.readFileSync(p, 'utf8');
2795
+ return safeJson(res, 200, { ok: true, markdown: md });
2796
+ }
2797
+
2798
+ if (req.url === '/api/tasks/heatmap') {
2799
+ const file = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
2800
+ const doc = readJsonOrNull(file) || { tasks: [] };
2801
+ const tasks = Array.isArray(doc.tasks) ? doc.tasks : [];
2802
+ const map = {};
2803
+ for (const t of tasks) {
2804
+ const slug = t.projectSlug || 'unassigned';
2805
+ if (!map[slug]) map[slug] = { total: 0, pending: 0, completed: 0 };
2806
+ map[slug].total++;
2807
+ if (t.status === 'COMPLETED') map[slug].completed++; else map[slug].pending++;
2808
+ }
2809
+ const items = Object.entries(map).map(([slug, v]) => ({ slug, ...v }));
2810
+ items.sort((a, b) => b.total - a.total);
2811
+ return safeJson(res, 200, { ok: true, items });
2812
+ }
2813
+
2814
+ if (req.url === '/api/blockers/summary') {
2571
2815
  const file = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
2572
2816
  const doc = readJsonOrNull(file) || { schemaVersion: 1, blockers: [] };
2573
2817
  const blockers = Array.isArray(doc.blockers) ? doc.blockers : [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "2.2.1",
3
+ "version": "2.3.1",
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",