@cccarv82/freya 1.0.56 → 1.0.58

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 CHANGED
@@ -155,22 +155,24 @@ body {
155
155
  color: var(--muted);
156
156
  }
157
157
 
158
- .statusPill {
159
- display: flex;
160
- align-items: center;
161
- gap: 8px;
162
- padding: 6px 10px;
163
- border: 1px solid var(--line);
164
- border-radius: 999px;
165
- background: var(--paper2);
166
- font-family: var(--mono);
167
- font-size: 12px;
168
- color: var(--muted);
169
- }
170
-
171
158
  .dot { width: 10px; height: 10px; border-radius: 6px; background: rgba(148,163,184,.6); }
172
159
  .dot.ok { background: rgba(16,185,129,.85); }
173
160
  .dot.err { background: rgba(239,68,68,.85); }
161
+ .dot.run { background: rgba(56,189,248,.9); }
162
+ .dot.plan { background: rgba(250,204,21,.9); }
163
+
164
+ .railStatus {
165
+ width: 16px;
166
+ height: 16px;
167
+ border-radius: 999px;
168
+ border: 1px solid var(--line2);
169
+ background: rgba(148,163,184,.6);
170
+ box-shadow: 0 0 0 4px rgba(0,0,0,.25);
171
+ }
172
+ .railStatus.ok { background: rgba(16,185,129,.9); }
173
+ .railStatus.err { background: rgba(239,68,68,.9); }
174
+ .railStatus.run { background: rgba(56,189,248,.9); }
175
+ .railStatus.plan { background: rgba(250,204,21,.9); }
174
176
 
175
177
  .sidePath {
176
178
  margin: 10px 6px 10px;
@@ -285,6 +287,52 @@ body {
285
287
  color: var(--muted);
286
288
  margin-bottom: 10px;
287
289
  }
290
+
291
+ .reportsHeader {
292
+ display: flex;
293
+ align-items: flex-end;
294
+ justify-content: space-between;
295
+ gap: 16px;
296
+ margin-bottom: 12px;
297
+ }
298
+
299
+ .reportsTitle { font-size: 18px; font-weight: 700; }
300
+ .reportsSubtitle { font-size: 12px; color: var(--faint); margin-top: 4px; }
301
+ .reportsActions { display: flex; gap: 10px; }
302
+
303
+ .reportsTools { margin-bottom: 12px; }
304
+ .reportsTools input { width: 100%; }
305
+
306
+ .reportsGrid {
307
+ display: grid;
308
+ gap: 14px;
309
+ }
310
+
311
+ .reportCard {
312
+ border: 1px solid var(--line);
313
+ border-radius: 16px;
314
+ padding: 14px;
315
+ background: var(--paper);
316
+ box-shadow: var(--shadow2);
317
+ display: grid;
318
+ gap: 12px;
319
+ }
320
+
321
+ .reportHead {
322
+ display: flex;
323
+ justify-content: space-between;
324
+ gap: 14px;
325
+ }
326
+
327
+ .reportName { font-weight: 700; }
328
+ .reportMeta { font-size: 12px; color: var(--faint); margin-top: 4px; }
329
+ .reportHeadActions { display: flex; gap: 8px; align-items: center; }
330
+
331
+ .reportBody { display: grid; gap: 10px; }
332
+ .reportPreview { border: 1px solid var(--line); border-radius: 12px; padding: 12px; background: rgba(0,0,0,.18); }
333
+ .reportRaw { display: none; }
334
+ .reportCard.raw .reportPreview { display: none; }
335
+ .reportCard.raw .reportRaw { display: block; }
288
336
  .centerHead { display: flex; justify-content: space-between; align-items: flex-end; gap: 18px; margin-bottom: 14px; }
289
337
  .statusLine { display:flex; align-items:center; justify-content:flex-end; gap: 12px; }
290
338
 
package/cli/web-ui.js CHANGED
@@ -8,6 +8,8 @@
8
8
  lastReportPath: null,
9
9
  lastText: '',
10
10
  reports: [],
11
+ reportTexts: {},
12
+ reportModes: {},
11
13
  selectedReport: null,
12
14
  lastPlan: '',
13
15
  lastApplied: null,
@@ -18,24 +20,18 @@
18
20
  chatLoaded: false
19
21
  };
20
22
 
21
- function applyTheme(theme) {
22
- document.documentElement.setAttribute('data-theme', theme);
23
- localStorage.setItem('freya.theme', theme);
24
- const t = $('themeToggle');
25
- if (t) t.textContent = theme === 'dark' ? 'Claro' : 'Escuro';
26
- }
27
-
28
- function toggleTheme() {
29
- const t = localStorage.getItem('freya.theme') || 'dark';
30
- applyTheme(t === 'dark' ? 'light' : 'dark');
23
+ function applyDarkTheme() {
24
+ document.documentElement.setAttribute('data-theme', 'dark');
31
25
  }
32
26
 
33
27
  function setPill(kind, text) {
34
28
  const dot = $('dot');
35
- if (!dot) return;
36
- dot.classList.remove('ok', 'err');
37
- if (kind === 'ok') dot.classList.add('ok');
38
- if (kind === 'err') dot.classList.add('err');
29
+ const rail = $('railStatus');
30
+ const classes = ['ok', 'err', 'run', 'plan'];
31
+ if (dot) dot.classList.remove(...classes);
32
+ if (rail) rail.classList.remove(...classes);
33
+ if (dot && classes.includes(kind)) dot.classList.add(kind);
34
+ if (rail && classes.includes(kind)) rail.classList.add(kind);
39
35
  const pill = $('pill');
40
36
  if (pill) pill.textContent = text;
41
37
  const status = $('status');
@@ -378,9 +374,13 @@
378
374
  const def = (window.__FREYA_DEFAULT_DIR && window.__FREYA_DEFAULT_DIR !== '__FREYA_DEFAULT_DIR__')
379
375
  ? window.__FREYA_DEFAULT_DIR
380
376
  : (localStorage.getItem('freya.dir') || './freya');
381
- $('dir').value = def;
382
- $('sidePath').textContent = def || './freya';
383
- localStorage.setItem('freya.dir', $('dir').value || './freya');
377
+ const dirEl = $('dir');
378
+ if (dirEl) {
379
+ dirEl.value = def;
380
+ localStorage.setItem('freya.dir', dirEl.value || './freya');
381
+ }
382
+ const side = $('sidePath');
383
+ if (side) side.textContent = def || './freya';
384
384
  }
385
385
 
386
386
  async function api(p, body) {
@@ -409,8 +409,9 @@
409
409
  }
410
410
 
411
411
  function dirOrDefault() {
412
- const d = $('dir').value.trim();
413
- return d || './freya';
412
+ const dEl = $('dir');
413
+ const d = dEl ? dEl.value.trim() : '';
414
+ return d || (localStorage.getItem('freya.dir') || './freya');
414
415
  }
415
416
 
416
417
  function fmtWhen(ms) {
@@ -492,6 +493,116 @@
492
493
  }
493
494
  }
494
495
 
496
+ function renderReportsPage() {
497
+ const grid = $('reportsGrid');
498
+ if (!grid) return;
499
+ const q = ($('reportsFilter') ? $('reportsFilter').value : '').trim().toLowerCase();
500
+ const list = (state.reports || []).filter((it) => {
501
+ if (!q) return true;
502
+ return (it.name + ' ' + it.kind).toLowerCase().includes(q);
503
+ });
504
+
505
+ grid.innerHTML = '';
506
+ for (const item of list) {
507
+ const card = document.createElement('div');
508
+ const mode = state.reportModes[item.relPath] || 'preview';
509
+ card.className = 'reportCard' + (mode === 'raw' ? ' raw' : '');
510
+
511
+ const meta = fmtWhen(item.mtimeMs);
512
+ card.innerHTML =
513
+ '<div class="reportHead">'
514
+ + '<div>'
515
+ + '<div class="reportName">' + escapeHtml(item.name) + '</div>'
516
+ + '<div class="reportMeta">' + escapeHtml(item.relPath) + ' • ' + escapeHtml(meta) + '</div>'
517
+ + '</div>'
518
+ + '<div class="reportHeadActions">'
519
+ + '<button class="btn small" data-action="toggle">' + (mode === 'raw' ? 'Preview' : 'Markdown') + '</button>'
520
+ + '<button class="btn small primary" data-action="save">Salvar</button>'
521
+ + '</div>'
522
+ + '</div>'
523
+ + '<div class="reportBody">'
524
+ + '<div class="reportPreview"></div>'
525
+ + '<textarea class="reportRaw" rows="12"></textarea>'
526
+ + '</div>';
527
+
528
+ const text = state.reportTexts[item.relPath] || '';
529
+ const preview = card.querySelector('.reportPreview');
530
+ if (preview) preview.innerHTML = renderMarkdown(text || '');
531
+ const raw = card.querySelector('.reportRaw');
532
+ if (raw) raw.value = text;
533
+
534
+ const toggleBtn = card.querySelector('[data-action="toggle"]');
535
+ if (toggleBtn) {
536
+ toggleBtn.onclick = () => {
537
+ state.reportModes[item.relPath] = (state.reportModes[item.relPath] === 'raw') ? 'preview' : 'raw';
538
+ renderReportsPage();
539
+ };
540
+ }
541
+
542
+ const saveBtn = card.querySelector('[data-action="save"]');
543
+ if (saveBtn) {
544
+ saveBtn.onclick = async () => {
545
+ try {
546
+ const content = (raw && typeof raw.value === 'string') ? raw.value : '';
547
+ setPill('run', 'salvando…');
548
+ await api('/api/reports/write', { dir: dirOrDefault(), relPath: item.relPath, text: content });
549
+ state.reportTexts[item.relPath] = content;
550
+ setPill('ok', 'salvo');
551
+ setTimeout(() => setPill('ok', 'pronto'), 800);
552
+ renderReportsPage();
553
+ } catch (e) {
554
+ setPill('err', 'falhou');
555
+ }
556
+ };
557
+ }
558
+
559
+ grid.appendChild(card);
560
+ }
561
+ }
562
+
563
+ async function refreshReportsPage() {
564
+ try {
565
+ setPill('run', 'carregando…');
566
+ const r = await api('/api/reports/list', { dir: dirOrDefault() });
567
+ state.reports = (r.reports || []);
568
+ state.reportTexts = {};
569
+ await Promise.all(state.reports.map(async (item) => {
570
+ try {
571
+ const rr = await api('/api/reports/read', { dir: dirOrDefault(), relPath: item.relPath });
572
+ state.reportTexts[item.relPath] = rr.text || '';
573
+ } catch {
574
+ state.reportTexts[item.relPath] = '';
575
+ }
576
+ }));
577
+ renderReportsPage();
578
+ setPill('ok', 'pronto');
579
+ } catch (e) {
580
+ setPill('err', 'falhou');
581
+ }
582
+ }
583
+
584
+ function wireRailNav() {
585
+ const dash = $('railDashboard');
586
+ const rep = $('railReports');
587
+ if (dash) {
588
+ dash.onclick = () => {
589
+ const isReports = document.body && document.body.dataset && document.body.dataset.page === 'reports';
590
+ if (isReports) {
591
+ window.location.href = '/';
592
+ return;
593
+ }
594
+ const c = document.querySelector('.centerBody');
595
+ if (c) c.scrollTo({ top: 0, behavior: 'smooth' });
596
+ };
597
+ }
598
+ if (rep) {
599
+ rep.onclick = () => {
600
+ const isReports = document.body && document.body.dataset && document.body.dataset.page === 'reports';
601
+ if (!isReports) window.location.href = '/reports';
602
+ };
603
+ }
604
+ }
605
+
495
606
  async function editTask(t) {
496
607
  try {
497
608
  const currentSlug = t.projectSlug ? String(t.projectSlug) : '';
@@ -988,10 +1099,11 @@
988
1099
  }
989
1100
 
990
1101
  // init
991
- applyTheme('dark');
992
- try { localStorage.setItem('freya.theme', 'dark'); } catch (err) {}
993
- $('chipPort').textContent = location.host;
1102
+ applyDarkTheme();
1103
+ const chipPort = $('chipPort');
1104
+ if (chipPort) chipPort.textContent = location.host;
994
1105
  loadLocal();
1106
+ wireRailNav();
995
1107
 
996
1108
  // Developer drawer (persist open/close)
997
1109
  try {
@@ -1005,23 +1117,34 @@
1005
1117
  }
1006
1118
  } catch {}
1007
1119
 
1120
+ const isReportsPage = document.body && document.body.dataset && document.body.dataset.page === 'reports';
1121
+
1008
1122
  // Load persisted settings from the workspace + bootstrap (auto-init + auto-health)
1009
1123
  (async () => {
1010
1124
  let defaults = null;
1011
1125
  try {
1012
1126
  defaults = await api('/api/defaults', { dir: dirOrDefault() });
1013
1127
  if (defaults && defaults.workspaceDir) {
1014
- $('dir').value = defaults.workspaceDir;
1015
- $('sidePath').textContent = defaults.workspaceDir;
1128
+ const dirEl = $('dir');
1129
+ if (dirEl) dirEl.value = defaults.workspaceDir;
1130
+ const side = $('sidePath');
1131
+ if (side) side.textContent = defaults.workspaceDir;
1016
1132
  }
1017
1133
  if (defaults && defaults.settings) {
1018
- $('discord').value = defaults.settings.discordWebhookUrl || '';
1019
- $('teams').value = defaults.settings.teamsWebhookUrl || '';
1134
+ const discord = $('discord');
1135
+ const teams = $('teams');
1136
+ if (discord) discord.value = defaults.settings.discordWebhookUrl || '';
1137
+ if (teams) teams.value = defaults.settings.teamsWebhookUrl || '';
1020
1138
  }
1021
1139
  } catch (e) {
1022
1140
  // ignore
1023
1141
  }
1024
1142
 
1143
+ if (isReportsPage) {
1144
+ await refreshReportsPage();
1145
+ return;
1146
+ }
1147
+
1025
1148
  // If workspace isn't initialized yet, auto-init (reduces clicks)
1026
1149
  try {
1027
1150
  if (defaults && defaults.workspaceOk === false) {
@@ -1058,12 +1181,13 @@
1058
1181
  window.exportObsidian = exportObsidian;
1059
1182
  window.rebuildIndex = rebuildIndex;
1060
1183
  window.renderReportsList = renderReportsList;
1184
+ window.renderReportsPage = renderReportsPage;
1185
+ window.refreshReportsPage = refreshReportsPage;
1061
1186
  window.copyOut = copyOut;
1062
1187
  window.copyPath = copyPath;
1063
1188
  window.openSelected = openSelected;
1064
1189
  window.downloadSelected = downloadSelected;
1065
1190
  window.clearOut = clearOut;
1066
- window.toggleTheme = toggleTheme;
1067
1191
  window.saveInbox = saveInbox;
1068
1192
  window.saveAndPlan = saveAndPlan;
1069
1193
  window.toggleAutoApply = toggleAutoApply;
package/cli/web.js CHANGED
@@ -881,6 +881,11 @@ function html(defaultDir) {
881
881
  return buildHtml(safeDefault, APP_VERSION);
882
882
  }
883
883
 
884
+ function reportsHtml(defaultDir) {
885
+ const safeDefault = String(defaultDir || './freya').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
886
+ return buildReportsHtml(safeDefault, APP_VERSION);
887
+ }
888
+
884
889
  function buildHtml(safeDefault, appVersion) {
885
890
  const safeVersion = escapeHtml(appVersion || 'unknown');
886
891
  return `<!doctype html>
@@ -901,14 +906,11 @@ function buildHtml(safeDefault, appVersion) {
901
906
  <div class="railLogo">F</div>
902
907
  </div>
903
908
  <div class="railNav">
904
- <button class="railBtn active" type="button" title="Dashboard">H</button>
905
- <button class="railBtn" type="button" title="Workspace">W</button>
906
- <button class="railBtn" type="button" title="Relatórios">R</button>
907
- <button class="railBtn" type="button" title="Preview">P</button>
908
- <button class="railBtn" type="button" title="Conversa">C</button>
909
+ <button class="railBtn active" id="railDashboard" type="button" title="Dashboard">D</button>
910
+ <button class="railBtn" id="railReports" type="button" title="Relatórios">R</button>
909
911
  </div>
910
912
  <div class="railBottom">
911
- <button class="railBtn railToggle" id="themeToggle" type="button" onclick="toggleTheme()">Claro</button>
913
+ <div class="railStatus" id="railStatus" title="status"></div>
912
914
  </div>
913
915
  </aside>
914
916
 
@@ -920,7 +922,6 @@ function buildHtml(safeDefault, appVersion) {
920
922
  <div class="brand">FREYA</div>
921
923
  <div class="brandSub">Assistente de status local-first</div>
922
924
  </div>
923
- <div class="statusPill"><span class="dot" id="dot"></span><span id="pill">pronto</span></div>
924
925
  </div>
925
926
  <div class="topActions">
926
927
  <span class="chip" id="chipVersion">v${safeVersion}</span>
@@ -954,7 +955,7 @@ function buildHtml(safeDefault, appVersion) {
954
955
  </div>
955
956
  </section>
956
957
 
957
- <section class="utilityGrid">
958
+ <section class="utilityGrid" id="reportsSection">
958
959
  <div class="utilityCard">
959
960
  <div class="utilityHead">Área de trabalho</div>
960
961
  <div class="sidePath" id="sidePath">./freya</div>
@@ -1130,6 +1131,82 @@ function buildHtml(safeDefault, appVersion) {
1130
1131
  </html>`;
1131
1132
  }
1132
1133
 
1134
+ function buildReportsHtml(safeDefault, appVersion) {
1135
+ const safeVersion = escapeHtml(appVersion || 'unknown');
1136
+ return `<!doctype html>
1137
+ <html>
1138
+ <head>
1139
+ <meta charset="utf-8" />
1140
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1141
+ <title>FREYA Reports</title>
1142
+ <link rel="stylesheet" href="/app.css" />
1143
+ </head>
1144
+ <body data-page="reports">
1145
+ <div class="app">
1146
+ <div class="frame">
1147
+ <div class="shell">
1148
+
1149
+ <aside class="rail">
1150
+ <div class="railTop">
1151
+ <div class="railLogo">F</div>
1152
+ </div>
1153
+ <div class="railNav">
1154
+ <button class="railBtn" id="railDashboard" type="button" title="Dashboard">D</button>
1155
+ <button class="railBtn active" id="railReports" type="button" title="Relatórios">R</button>
1156
+ </div>
1157
+ <div class="railBottom">
1158
+ <div class="railStatus" id="railStatus" title="status"></div>
1159
+ </div>
1160
+ </aside>
1161
+
1162
+ <main class="center reportsPage" id="reportsPage">
1163
+ <div class="topbar">
1164
+ <div class="brandLine">
1165
+ <span class="spark"></span>
1166
+ <div class="brandStack">
1167
+ <div class="brand">FREYA</div>
1168
+ <div class="brandSub">Relatórios</div>
1169
+ </div>
1170
+ </div>
1171
+ <div class="topActions">
1172
+ <span class="chip" id="chipVersion">v${safeVersion}</span>
1173
+ <span class="chip" id="chipPort">127.0.0.1:3872</span>
1174
+ </div>
1175
+ </div>
1176
+
1177
+ <div class="centerBody">
1178
+ <input id="dir" type="hidden" />
1179
+
1180
+ <section class="reportsHeader">
1181
+ <div>
1182
+ <div class="reportsTitle">Relatórios</div>
1183
+ <div class="reportsSubtitle">Edite e refine seus relatórios com preview em Markdown.</div>
1184
+ </div>
1185
+ <div class="reportsActions">
1186
+ <button class="btn small" type="button" onclick="refreshReportsPage()">Atualizar</button>
1187
+ </div>
1188
+ </section>
1189
+
1190
+ <section class="reportsTools">
1191
+ <input id="reportsFilter" placeholder="filtrar (ex: daily, executive, 2026-01-29)" oninput="renderReportsPage()" />
1192
+ </section>
1193
+
1194
+ <section class="reportsGrid" id="reportsGrid"></section>
1195
+ </div>
1196
+ </main>
1197
+
1198
+ </div>
1199
+ </div>
1200
+ </div>
1201
+
1202
+ <script>
1203
+ window.__FREYA_DEFAULT_DIR = "${safeDefault}";
1204
+ </script>
1205
+ <script src="/app.js"></script>
1206
+ </body>
1207
+ </html>`;
1208
+ }
1209
+
1133
1210
  function ensureDir(p) {
1134
1211
  fs.mkdirSync(p, { recursive: true });
1135
1212
  }
@@ -1316,6 +1393,14 @@ async function cmdWeb({ port, dir, open, dev }) {
1316
1393
  return;
1317
1394
  }
1318
1395
 
1396
+ if (req.method === 'GET' && req.url === '/reports') {
1397
+ try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch {}
1398
+ const body = reportsHtml(dir || './freya');
1399
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
1400
+ res.end(body);
1401
+ return;
1402
+ }
1403
+
1319
1404
  if (req.method === 'GET' && req.url === '/app.css') {
1320
1405
  const css = fs.readFileSync(path.join(__dirname, 'web-ui.css'), 'utf8');
1321
1406
  res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8', 'Cache-Control': 'no-store' });
@@ -1412,6 +1497,23 @@ async function cmdWeb({ port, dir, open, dev }) {
1412
1497
  return safeJson(res, 200, { relPath: rel, fullPath: full });
1413
1498
  }
1414
1499
 
1500
+ if (req.url === '/api/reports/write') {
1501
+ const rel = payload.relPath;
1502
+ const text = payload.text;
1503
+ if (!rel) return safeJson(res, 400, { error: 'Missing relPath' });
1504
+ if (typeof text !== 'string') return safeJson(res, 400, { error: 'Missing text' });
1505
+ const reportsDir = path.join(workspaceDir, 'docs', 'reports');
1506
+ const full = path.join(workspaceDir, rel);
1507
+ const safeReportsDir = path.resolve(reportsDir);
1508
+ const safeFull = path.resolve(full);
1509
+ if (!safeFull.startsWith(safeReportsDir + path.sep)) {
1510
+ return safeJson(res, 400, { error: 'Invalid report path' });
1511
+ }
1512
+ if (!exists(safeFull)) return safeJson(res, 404, { error: 'Report not found' });
1513
+ fs.writeFileSync(safeFull, text, 'utf8');
1514
+ return safeJson(res, 200, { ok: true, relPath: rel });
1515
+ }
1516
+
1415
1517
  if (req.url === '/api/inbox/add') {
1416
1518
  const text = String(payload.text || '').trim();
1417
1519
  if (!text) return safeJson(res, 400, { error: 'Missing text' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "1.0.56",
3
+ "version": "1.0.58",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js",