@cccarv82/freya 2.1.7 → 2.2.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 +56 -0
  2. package/cli/web.js +124 -1
  3. package/package.json +1 -1
package/cli/web-ui.js CHANGED
@@ -656,6 +656,48 @@
656
656
  grid.appendChild(card); }
657
657
  }
658
658
 
659
+ function renderProjects() {
660
+ const el = $('projectsGrid');
661
+ if (!el) return;
662
+ const filter = String(($('projectsFilter') && $('projectsFilter').value) || '').toLowerCase();
663
+ const items = Array.isArray(state.projects) ? state.projects : [];
664
+ const filtered = items.filter((p) => {
665
+ const hay = [p.client, p.program, p.stream, p.project, p.slug, (p.tags||[]).join(' ')].join(' ').toLowerCase();
666
+ return !filter || hay.includes(filter);
667
+ });
668
+ el.innerHTML = '';
669
+ for (const p of filtered) {
670
+ const card = document.createElement('div');
671
+ card.className = 'reportCard';
672
+ card.innerHTML = '<div class="reportHead">'
673
+ + '<div><div class="reportTitle">' + escapeHtml(p.project || p.slug || 'Projeto') + '</div>'
674
+ + '<div class="reportMeta">' + escapeHtml([p.client, p.program, p.stream].filter(Boolean).join(' · ')) + '</div></div>'
675
+ + '<div class="reportActions">' + (p.active ? '<span class="pill ok">ativo</span>' : '<span class="pill warn">inativo</span>') + '</div>'
676
+ + '</div>'
677
+ + '<div class="help" style="margin-top:8px">' + escapeHtml(p.currentStatus || 'Sem status') + '</div>'
678
+ + '<div class="reportMeta" style="margin-top:8px">Última atualização: ' + escapeHtml(p.lastUpdated || '—') + '</div>'
679
+ + '<div class="reportMeta">Eventos: ' + escapeHtml(String(p.historyCount || 0)) + '</div>';
680
+ el.appendChild(card);
681
+ }
682
+ if (!filtered.length) {
683
+ const empty = document.createElement('div');
684
+ empty.className = 'help';
685
+ empty.textContent = 'Nenhum projeto encontrado.';
686
+ el.appendChild(empty);
687
+ }
688
+ }
689
+
690
+ async function refreshProjects() {
691
+ try {
692
+ const r = await api('/api/projects/list', { dir: dirOrDefault() });
693
+ state.projects = r.projects || [];
694
+ renderProjects();
695
+ } catch (e) {
696
+ const el = $('projectsGrid');
697
+ if (el) el.textContent = 'Falha ao carregar projetos.';
698
+ }
699
+ }
700
+
659
701
  async function refreshReportsPage() {
660
702
  try {
661
703
  setPill('run', 'carregando…');
@@ -680,6 +722,7 @@
680
722
  function wireRailNav() {
681
723
  const dash = $('railDashboard');
682
724
  const rep = $('railReports');
725
+ const proj = $('railProjects');
683
726
  const health = $('railCompanion');
684
727
  if (dash) {
685
728
  dash.onclick = () => {
@@ -698,6 +741,12 @@
698
741
  if (!isReports) window.location.href = '/reports';
699
742
  };
700
743
  }
744
+ if (proj) {
745
+ proj.onclick = () => {
746
+ const isProjects = document.body && document.body.dataset && document.body.dataset.page === 'projects';
747
+ if (!isProjects) window.location.href = '/projects';
748
+ };
749
+ }
701
750
  if (health) {
702
751
  health.onclick = () => {
703
752
  const isHealth = document.body && document.body.dataset && document.body.dataset.page === 'companion';
@@ -1314,6 +1363,7 @@
1314
1363
  } catch {}
1315
1364
 
1316
1365
  const isReportsPage = document.body && document.body.dataset && document.body.dataset.page === 'reports';
1366
+ const isProjectsPage = document.body && document.body.dataset && document.body.dataset.page === 'projects';
1317
1367
  const isCompanionPage = document.body && document.body.dataset && document.body.dataset.page === 'companion';
1318
1368
 
1319
1369
  // Load persisted settings from the workspace + bootstrap (auto-init + auto-health)
@@ -1342,6 +1392,11 @@
1342
1392
  return;
1343
1393
  }
1344
1394
 
1395
+ if (isProjectsPage) {
1396
+ await refreshProjects();
1397
+ return;
1398
+ }
1399
+
1345
1400
  if (isCompanionPage) {
1346
1401
  await refreshHealthChecklist();
1347
1402
  return;
@@ -1385,6 +1440,7 @@
1385
1440
  window.renderReportsList = renderReportsList;
1386
1441
  window.renderReportsPage = renderReportsPage;
1387
1442
  window.refreshReportsPage = refreshReportsPage;
1443
+ window.refreshProjects = refreshProjects;
1388
1444
  window.refreshBlockersInsights = refreshBlockersInsights;
1389
1445
  window.refreshHealthChecklist = refreshHealthChecklist;
1390
1446
  window.copyOut = copyOut;
package/cli/web.js CHANGED
@@ -905,6 +905,7 @@ function buildHtml(safeDefault, appVersion) {
905
905
  <button class="railBtn active" id="railDashboard" type="button" title="Dashboard">D</button>
906
906
  <button class="railBtn" id="railReports" type="button" title="Relatórios">R</button>
907
907
  <button class="railBtn" id="railCompanion" type="button" title="Companion">C</button>
908
+ <button class="railBtn" id="railProjects" type="button" title="Projects">P</button>
908
909
  </div>
909
910
  <div class="railBottom">
910
911
  <div class="railStatus" id="railStatus" title="status"></div>
@@ -1155,6 +1156,7 @@ function buildReportsHtml(safeDefault, appVersion) {
1155
1156
  <button class="railBtn" id="railDashboard" type="button" title="Dashboard">D</button>
1156
1157
  <button class="railBtn active" id="railReports" type="button" title="Relatórios">R</button>
1157
1158
  <button class="railBtn" id="railCompanion" type="button" title="Companion">C</button>
1159
+ <button class="railBtn" id="railProjects" type="button" title="Projects">P</button>
1158
1160
  </div>
1159
1161
  <div class="railBottom">
1160
1162
  <div class="railStatus" id="railStatus" title="status"></div>
@@ -1209,6 +1211,84 @@ function buildReportsHtml(safeDefault, appVersion) {
1209
1211
  </html>`
1210
1212
  }
1211
1213
 
1214
+ function buildProjectsHtml(safeDefault, appVersion) {
1215
+ const safeVersion = escapeHtml(appVersion || 'unknown');
1216
+ return `<!doctype html>
1217
+ <html>
1218
+ <head>
1219
+ <meta charset="utf-8" />
1220
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1221
+ <title>Projects</title>
1222
+ <link rel="stylesheet" href="/app.css" />
1223
+ </head>
1224
+ <body data-page="projects">
1225
+ <div class="app">
1226
+ <div class="frame">
1227
+ <div class="shell">
1228
+
1229
+ <aside class="rail">
1230
+ <div class="railTop">
1231
+ <div class="railLogo">F</div>
1232
+ </div>
1233
+ <div class="railNav">
1234
+ <button class="railBtn" id="railDashboard" type="button" title="Dashboard">D</button>
1235
+ <button class="railBtn" id="railReports" type="button" title="Relatorios">R</button>
1236
+ <button class="railBtn" id="railCompanion" type="button" title="Companion">C</button>
1237
+ <button class="railBtn active" id="railProjects" type="button" title="Projects">P</button>
1238
+ </div>
1239
+ <div class="railBottom">
1240
+ <div class="railStatus" id="railStatus" title="status"></div>
1241
+ </div>
1242
+ </aside>
1243
+
1244
+ <main class="center reportsPage" id="projectsPage">
1245
+ <div class="topbar">
1246
+ <div class="brandLine">
1247
+ <span class="spark"></span>
1248
+ <div class="brandStack">
1249
+ <div class="brand">FREYA</div>
1250
+ <div class="brandSub">Projects</div>
1251
+ </div>
1252
+ </div>
1253
+ <div class="topActions">
1254
+ <span class="chip" id="chipVersion">v${safeVersion}</span>
1255
+ <span class="chip" id="chipPort">127.0.0.1:3872</span>
1256
+ </div>
1257
+ </div>
1258
+
1259
+ <div class="centerBody">
1260
+ <input id="dir" type="hidden" />
1261
+
1262
+ <section class="reportsHeader">
1263
+ <div>
1264
+ <div class="reportsTitle">Projects</div>
1265
+ <div class="reportsSubtitle">Status por projeto com ultima atualizacao e riscos.</div>
1266
+ </div>
1267
+ <div class="reportsActions">
1268
+ <button class="btn small" type="button" onclick="refreshProjects()">Atualizar</button>
1269
+ </div>
1270
+ </section>
1271
+
1272
+ <section class="reportsTools">
1273
+ <input id="projectsFilter" placeholder="filtrar (cliente, projeto, tag)" oninput="renderProjects()" />
1274
+ </section>
1275
+
1276
+ <section class="reportsGrid" id="projectsGrid"></section>
1277
+ </div>
1278
+ </main>
1279
+
1280
+ </div>
1281
+ </div>
1282
+ </div>
1283
+
1284
+ <script>
1285
+ window.__FREYA_DEFAULT_DIR = "${safeDefault}";
1286
+ </script>
1287
+ <script src="/app.js"></script>
1288
+ </body>
1289
+ </html>`;
1290
+ }
1291
+
1212
1292
  function buildCompanionHtml(safeDefault, appVersion) {
1213
1293
  const safeVersion = escapeHtml(appVersion || 'unknown');
1214
1294
  return `<!doctype html>
@@ -1232,6 +1312,7 @@ function buildCompanionHtml(safeDefault, appVersion) {
1232
1312
  <button class="railBtn" id="railDashboard" type="button" title="Dashboard">D</button>
1233
1313
  <button class="railBtn" id="railReports" type="button" title="Relatórios">R</button>
1234
1314
  <button class="railBtn active" id="railCompanion" type="button" title="Companion">C</button>
1315
+ <button class="railBtn" id="railProjects" type="button" title="Projects">P</button>
1235
1316
  </div>
1236
1317
  <div class="railBottom">
1237
1318
  <div class="railStatus" id="railStatus" title="status"></div>
@@ -1616,6 +1697,14 @@ async function cmdWeb({ port, dir, open, dev }) {
1616
1697
  return;
1617
1698
  }
1618
1699
 
1700
+ if (req.method === 'GET' && req.url === '/projects') {
1701
+ try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch {}
1702
+ const body = projectsHtml(dir || './freya');
1703
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
1704
+ res.end(body);
1705
+ return;
1706
+ }
1707
+
1619
1708
  if (req.method === 'GET' && req.url === '/app.css') {
1620
1709
  const css = fs.readFileSync(path.join(__dirname, 'web-ui.css'), 'utf8');
1621
1710
  res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8', 'Cache-Control': 'no-store' });
@@ -1701,7 +1790,41 @@ async function cmdWeb({ port, dir, open, dev }) {
1701
1790
  return safeJson(res, 200, { ok: true, map: out });
1702
1791
  }
1703
1792
 
1704
- if (req.url === '/api/reports/list') {
1793
+ if (req.url === '/api/projects/list') {
1794
+ const base = path.join(workspaceDir, 'data', 'Clients');
1795
+ const items = [];
1796
+ if (exists(base)) {
1797
+ const stack = [base];
1798
+ while (stack.length) {
1799
+ const dirp = stack.pop();
1800
+ const entries = fs.readdirSync(dirp, { withFileTypes: true });
1801
+ for (const ent of entries) {
1802
+ const full = path.join(dirp, ent.name);
1803
+ if (ent.isDirectory()) stack.push(full);
1804
+ else if (ent.isFile() && ent.name === 'status.json') {
1805
+ const doc = readJsonOrNull(full) || {};
1806
+ const rel = path.relative(base, path.dirname(full)).replace(/\\/g, '/');
1807
+ items.push({
1808
+ slug: rel,
1809
+ client: doc.client || null,
1810
+ program: doc.program || null,
1811
+ stream: doc.stream || null,
1812
+ project: doc.project || null,
1813
+ active: doc.active !== false,
1814
+ currentStatus: doc.currentStatus || '',
1815
+ lastUpdated: doc.lastUpdated || '',
1816
+ tags: Array.isArray(doc.tags) ? doc.tags : [],
1817
+ historyCount: Array.isArray(doc.history) ? doc.history.length : 0
1818
+ });
1819
+ }
1820
+ }
1821
+ }
1822
+ }
1823
+ items.sort((a,b)=> String(b.lastUpdated||'').localeCompare(String(a.lastUpdated||'')));
1824
+ return safeJson(res, 200, { ok: true, projects: items });
1825
+ }
1826
+
1827
+ if (req.url === '/api/reports/list') {
1705
1828
  const reports = listReports(workspaceDir);
1706
1829
  return safeJson(res, 200, { reports });
1707
1830
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "2.1.7",
3
+ "version": "2.2.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",