@cccarv82/freya 2.2.0 → 2.3.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.
Files changed (3) hide show
  1. package/cli/web-ui.js +99 -0
  2. package/cli/web.js +227 -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
@@ -877,6 +877,11 @@ function reportsHtml(defaultDir) {
877
877
  return buildReportsHtml(safeDefault, APP_VERSION);
878
878
  }
879
879
 
880
+ function projectsHtml(defaultDir) {
881
+ const safeDefault = String(defaultDir || './freya').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
882
+ return buildProjectsHtml(safeDefault, APP_VERSION);
883
+ }
884
+
880
885
  function companionHtml(defaultDir) {
881
886
  const safeDefault = String(defaultDir || './freya').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
882
887
  return buildCompanionHtml(safeDefault, APP_VERSION);
@@ -906,6 +911,7 @@ function buildHtml(safeDefault, appVersion) {
906
911
  <button class="railBtn" id="railReports" type="button" title="Relatórios">R</button>
907
912
  <button class="railBtn" id="railCompanion" type="button" title="Companion">C</button>
908
913
  <button class="railBtn" id="railProjects" type="button" title="Projects">P</button>
914
+ <button class="railBtn" id="railTimeline" type="button" title="Timeline">T</button>
909
915
  </div>
910
916
  <div class="railBottom">
911
917
  <div class="railStatus" id="railStatus" title="status"></div>
@@ -1157,6 +1163,7 @@ function buildReportsHtml(safeDefault, appVersion) {
1157
1163
  <button class="railBtn active" id="railReports" type="button" title="Relatórios">R</button>
1158
1164
  <button class="railBtn" id="railCompanion" type="button" title="Companion">C</button>
1159
1165
  <button class="railBtn" id="railProjects" type="button" title="Projects">P</button>
1166
+ <button class="railBtn" id="railTimeline" type="button" title="Timeline">T</button>
1160
1167
  </div>
1161
1168
  <div class="railBottom">
1162
1169
  <div class="railStatus" id="railStatus" title="status"></div>
@@ -1235,6 +1242,7 @@ function buildProjectsHtml(safeDefault, appVersion) {
1235
1242
  <button class="railBtn" id="railReports" type="button" title="Relatorios">R</button>
1236
1243
  <button class="railBtn" id="railCompanion" type="button" title="Companion">C</button>
1237
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>
1238
1246
  </div>
1239
1247
  <div class="railBottom">
1240
1248
  <div class="railStatus" id="railStatus" title="status"></div>
@@ -1289,6 +1297,85 @@ function buildProjectsHtml(safeDefault, appVersion) {
1289
1297
  </html>`;
1290
1298
  }
1291
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
+
1292
1379
  function buildCompanionHtml(safeDefault, appVersion) {
1293
1380
  const safeVersion = escapeHtml(appVersion || 'unknown');
1294
1381
  return `<!doctype html>
@@ -1313,6 +1400,7 @@ function buildCompanionHtml(safeDefault, appVersion) {
1313
1400
  <button class="railBtn" id="railReports" type="button" title="Relatórios">R</button>
1314
1401
  <button class="railBtn active" id="railCompanion" type="button" title="Companion">C</button>
1315
1402
  <button class="railBtn" id="railProjects" type="button" title="Projects">P</button>
1403
+ <button class="railBtn" id="railTimeline" type="button" title="Timeline">T</button>
1316
1404
  </div>
1317
1405
  <div class="railBottom">
1318
1406
  <div class="railStatus" id="railStatus" title="status"></div>
@@ -1355,6 +1443,20 @@ function buildCompanionHtml(safeDefault, appVersion) {
1355
1443
 
1356
1444
  <section class="reportsGrid" id="healthChecklist"></section>
1357
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
+
1358
1460
  <section class="panel" style="margin-top:16px">
1359
1461
  <div class="panelHead"><b>Saida</b></div>
1360
1462
  <div class="panelBody">
@@ -1705,6 +1807,14 @@ async function cmdWeb({ port, dir, open, dev }) {
1705
1807
  return;
1706
1808
  }
1707
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
+
1708
1818
  if (req.method === 'GET' && req.url === '/app.css') {
1709
1819
  const css = fs.readFileSync(path.join(__dirname, 'web-ui.css'), 'utf8');
1710
1820
  res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8', 'Cache-Control': 'no-store' });
@@ -1824,6 +1934,79 @@ async function cmdWeb({ port, dir, open, dev }) {
1824
1934
  return safeJson(res, 200, { ok: true, projects: items });
1825
1935
  }
1826
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
+
1827
2010
  if (req.url === '/api/reports/list') {
1828
2011
  const reports = listReports(workspaceDir);
1829
2012
  return safeJson(res, 200, { reports });
@@ -1887,6 +2070,26 @@ if (req.url === '/api/reports/list') {
1887
2070
  return safeJson(res, 200, { ok: true, relPath: rel });
1888
2071
  }
1889
2072
 
2073
+ function autoLinkNotes(textInput) {
2074
+ try {
2075
+ const projectsDir = path.join(workspaceDir, 'docs', 'projects');
2076
+ if (!exists(projectsDir)) return '';
2077
+ const files = fs.readdirSync(projectsDir).filter((f) => f.endsWith('.md'));
2078
+ const links = [];
2079
+ for (const f of files) {
2080
+ const name = f.replace('.md', '');
2081
+ const slug = name.toLowerCase();
2082
+ if (textInput.toLowerCase().includes(slug)) links.push(`[[${name}]]`);
2083
+ }
2084
+ if (!links.length) return '';
2085
+ return `
2086
+
2087
+ Links: ${links.join(' ')}`;
2088
+ } catch {
2089
+ return '';
2090
+ }
2091
+ }
2092
+
1890
2093
  if (req.url === '/api/inbox/add') {
1891
2094
  const text = String(payload.text || '').trim();
1892
2095
  if (!text) return safeJson(res, 400, { error: 'Missing text' });
@@ -2562,7 +2765,30 @@ if (req.url === '/api/reports/list') {
2562
2765
  return safeJson(res, 200, { ok: true, items, stats: { pendingTasks, openBlockers, reportsToday, reportsTotal: reports.length } });
2563
2766
  }
2564
2767
 
2565
- if (req.url === '/api/blockers/summary') {
2768
+ if (req.url === '/api/incidents') {
2769
+ const p = path.join(workspaceDir, 'docs', 'reports', 'fidelizacao-incident-index.md');
2770
+ if (!exists(p)) return safeJson(res, 200, { ok: true, markdown: '' });
2771
+ const md = fs.readFileSync(p, 'utf8');
2772
+ return safeJson(res, 200, { ok: true, markdown: md });
2773
+ }
2774
+
2775
+ if (req.url === '/api/tasks/heatmap') {
2776
+ const file = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
2777
+ const doc = readJsonOrNull(file) || { tasks: [] };
2778
+ const tasks = Array.isArray(doc.tasks) ? doc.tasks : [];
2779
+ const map = {};
2780
+ for (const t of tasks) {
2781
+ const slug = t.projectSlug || 'unassigned';
2782
+ if (!map[slug]) map[slug] = { total: 0, pending: 0, completed: 0 };
2783
+ map[slug].total++;
2784
+ if (t.status === 'COMPLETED') map[slug].completed++; else map[slug].pending++;
2785
+ }
2786
+ const items = Object.entries(map).map(([slug, v]) => ({ slug, ...v }));
2787
+ items.sort((a, b) => b.total - a.total);
2788
+ return safeJson(res, 200, { ok: true, items });
2789
+ }
2790
+
2791
+ if (req.url === '/api/blockers/summary') {
2566
2792
  const file = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
2567
2793
  const doc = readJsonOrNull(file) || { schemaVersion: 1, blockers: [] };
2568
2794
  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.0",
3
+ "version": "2.3.0",
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",