@cccarv82/freya 1.0.57 → 1.0.59
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 +68 -13
- package/cli/web-ui.js +219 -26
- package/cli/web.js +108 -6
- package/package.json +1 -1
package/cli/web-ui.css
CHANGED
|
@@ -155,19 +155,6 @@ 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); }
|
|
@@ -300,6 +287,74 @@ body {
|
|
|
300
287
|
color: var(--muted);
|
|
301
288
|
margin-bottom: 10px;
|
|
302
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
|
+
cursor: pointer;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.reportName { font-weight: 700; }
|
|
329
|
+
.reportMeta { font-size: 12px; color: var(--faint); margin-top: 4px; display: flex; align-items: center; gap: 10px; }
|
|
330
|
+
.reportMetaText { display: inline-block; }
|
|
331
|
+
.reportHeadActions { display: flex; gap: 8px; align-items: center; }
|
|
332
|
+
|
|
333
|
+
.iconBtn {
|
|
334
|
+
border: 1px solid var(--line);
|
|
335
|
+
background: rgba(0,0,0,.25);
|
|
336
|
+
color: var(--muted);
|
|
337
|
+
border-radius: 10px;
|
|
338
|
+
padding: 2px 8px;
|
|
339
|
+
cursor: pointer;
|
|
340
|
+
font-size: 12px;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.reportBody { display: none; gap: 10px; }
|
|
344
|
+
.reportCard.expanded .reportBody { display: grid; }
|
|
345
|
+
|
|
346
|
+
.reportPreview {
|
|
347
|
+
border: 1px solid var(--line);
|
|
348
|
+
border-radius: 12px;
|
|
349
|
+
padding: 12px;
|
|
350
|
+
background: rgba(0,0,0,.18);
|
|
351
|
+
min-height: 120px;
|
|
352
|
+
outline: none;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.reportRaw { display: none; width: 100%; overflow: hidden; resize: none; }
|
|
356
|
+
.reportCard.raw .reportPreview { display: none; }
|
|
357
|
+
.reportCard.raw .reportRaw { display: block; }
|
|
303
358
|
.centerHead { display: flex; justify-content: space-between; align-items: flex-end; gap: 18px; margin-bottom: 14px; }
|
|
304
359
|
.statusLine { display:flex; align-items:center; justify-content:flex-end; gap: 12px; }
|
|
305
360
|
|
package/cli/web-ui.js
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
lastReportPath: null,
|
|
9
9
|
lastText: '',
|
|
10
10
|
reports: [],
|
|
11
|
+
reportTexts: {},
|
|
12
|
+
reportModes: {},
|
|
13
|
+
reportExpanded: {},
|
|
11
14
|
selectedReport: null,
|
|
12
15
|
lastPlan: '',
|
|
13
16
|
lastApplied: null,
|
|
@@ -18,16 +21,8 @@
|
|
|
18
21
|
chatLoaded: false
|
|
19
22
|
};
|
|
20
23
|
|
|
21
|
-
function
|
|
22
|
-
document.documentElement.setAttribute('data-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');
|
|
24
|
+
function applyDarkTheme() {
|
|
25
|
+
document.documentElement.setAttribute('data-theme', 'dark');
|
|
31
26
|
}
|
|
32
27
|
|
|
33
28
|
function setPill(kind, text) {
|
|
@@ -68,6 +63,20 @@
|
|
|
68
63
|
if (inList) { html += '</ul>'; inList = false; }
|
|
69
64
|
};
|
|
70
65
|
|
|
66
|
+
const inlineFormat = (text) => {
|
|
67
|
+
const esc = escapeHtml(text || '');
|
|
68
|
+
const codes = [];
|
|
69
|
+
let out = esc.replace(inlineCodeRe, (_, c) => {
|
|
70
|
+
const idx = codes.length;
|
|
71
|
+
codes.push(c);
|
|
72
|
+
return `@@CODE${idx}@@`;
|
|
73
|
+
});
|
|
74
|
+
out = out.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
75
|
+
out = out.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
76
|
+
out = out.replace(/@@CODE(\d+)@@/g, (_, i) => `<code class="md-inline">${codes[Number(i)]}</code>`);
|
|
77
|
+
return out;
|
|
78
|
+
};
|
|
79
|
+
|
|
71
80
|
for (const line of lines) {
|
|
72
81
|
if (line.trim().startsWith(FENCE)) {
|
|
73
82
|
if (!inCode) {
|
|
@@ -90,14 +99,14 @@
|
|
|
90
99
|
if (h) {
|
|
91
100
|
closeList();
|
|
92
101
|
const lvl = h[1].length;
|
|
93
|
-
html += '<h' + lvl + ' class="md-h' + lvl + '">' +
|
|
102
|
+
html += '<h' + lvl + ' class="md-h' + lvl + '">' + inlineFormat(h[2]) + '</h' + lvl + '>';
|
|
94
103
|
continue;
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
const li = line.match(/^[ \t]*[-*][ \t]+(.*)$/);
|
|
98
107
|
if (li) {
|
|
99
108
|
if (!inList) { html += '<ul class="md-ul">'; inList = true; }
|
|
100
|
-
const content =
|
|
109
|
+
const content = inlineFormat(li[1]);
|
|
101
110
|
html += '<li>' + content + '</li>';
|
|
102
111
|
continue;
|
|
103
112
|
}
|
|
@@ -109,7 +118,7 @@
|
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
closeList();
|
|
112
|
-
const p =
|
|
121
|
+
const p = inlineFormat(line);
|
|
113
122
|
html += '<p class="md-p">' + p + '</p>';
|
|
114
123
|
}
|
|
115
124
|
|
|
@@ -380,9 +389,13 @@
|
|
|
380
389
|
const def = (window.__FREYA_DEFAULT_DIR && window.__FREYA_DEFAULT_DIR !== '__FREYA_DEFAULT_DIR__')
|
|
381
390
|
? window.__FREYA_DEFAULT_DIR
|
|
382
391
|
: (localStorage.getItem('freya.dir') || './freya');
|
|
383
|
-
$('dir')
|
|
384
|
-
|
|
385
|
-
|
|
392
|
+
const dirEl = $('dir');
|
|
393
|
+
if (dirEl) {
|
|
394
|
+
dirEl.value = def;
|
|
395
|
+
localStorage.setItem('freya.dir', dirEl.value || './freya');
|
|
396
|
+
}
|
|
397
|
+
const side = $('sidePath');
|
|
398
|
+
if (side) side.textContent = def || './freya';
|
|
386
399
|
}
|
|
387
400
|
|
|
388
401
|
async function api(p, body) {
|
|
@@ -411,8 +424,9 @@
|
|
|
411
424
|
}
|
|
412
425
|
|
|
413
426
|
function dirOrDefault() {
|
|
414
|
-
const
|
|
415
|
-
|
|
427
|
+
const dEl = $('dir');
|
|
428
|
+
const d = dEl ? dEl.value.trim() : '';
|
|
429
|
+
return d || (localStorage.getItem('freya.dir') || './freya');
|
|
416
430
|
}
|
|
417
431
|
|
|
418
432
|
function fmtWhen(ms) {
|
|
@@ -494,6 +508,172 @@
|
|
|
494
508
|
}
|
|
495
509
|
}
|
|
496
510
|
|
|
511
|
+
function autoGrowTextarea(el) {
|
|
512
|
+
if (!el) return;
|
|
513
|
+
el.style.height = 'auto';
|
|
514
|
+
el.style.height = el.scrollHeight + 'px';
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function downloadReportPdf(item) {
|
|
518
|
+
const text = state.reportTexts[item.relPath] || '';
|
|
519
|
+
const html = `<!doctype html><html><head><meta charset="utf-8" /><title>${escapeHtml(item.name)}</title><style>body{font-family:Arial, sans-serif; padding:32px; color:#111;} h1,h2,h3{margin:16px 0 8px;} pre{background:#f5f5f5; padding:12px; border-radius:8px;}</style></head><body>${renderMarkdown(text)}</body></html>`;
|
|
520
|
+
const win = window.open('', '_blank');
|
|
521
|
+
if (!win) return;
|
|
522
|
+
win.document.write(html);
|
|
523
|
+
win.document.close();
|
|
524
|
+
win.focus();
|
|
525
|
+
win.print();
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function renderReportsPage() {
|
|
529
|
+
const grid = $('reportsGrid');
|
|
530
|
+
if (!grid) return;
|
|
531
|
+
const q = ($('reportsFilter') ? $('reportsFilter').value : '').trim().toLowerCase();
|
|
532
|
+
const list = (state.reports || []).filter((it) => {
|
|
533
|
+
if (!q) return true;
|
|
534
|
+
return (it.name + ' ' + it.kind).toLowerCase().includes(q);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
grid.innerHTML = '';
|
|
538
|
+
for (const item of list) {
|
|
539
|
+
const card = document.createElement('div');
|
|
540
|
+
const mode = state.reportModes[item.relPath] || 'preview';
|
|
541
|
+
const expanded = state.reportExpanded && state.reportExpanded[item.relPath];
|
|
542
|
+
card.className = 'reportCard' + (mode === 'raw' ? ' raw' : '') + (expanded ? ' expanded' : '');
|
|
543
|
+
|
|
544
|
+
const meta = fmtWhen(item.mtimeMs);
|
|
545
|
+
card.innerHTML =
|
|
546
|
+
'<div class="reportHead" data-action="expand">'
|
|
547
|
+
+ '<div>'
|
|
548
|
+
+ '<div class="reportName">' + escapeHtml(item.name) + '</div>'
|
|
549
|
+
+ '<div class="reportMeta">'
|
|
550
|
+
+ '<span class="reportMetaText">' + escapeHtml(item.relPath) + ' • ' + escapeHtml(meta) + '</span>'
|
|
551
|
+
+ '<button class="iconBtn" data-action="pdf" title="Baixar PDF">⬇</button>'
|
|
552
|
+
+ '</div>'
|
|
553
|
+
+ '</div>'
|
|
554
|
+
+ '<div class="reportHeadActions">'
|
|
555
|
+
+ '<button class="btn small" data-action="toggle">' + (mode === 'raw' ? 'Preview' : 'Markdown') + '</button>'
|
|
556
|
+
+ '<button class="btn small primary" data-action="save">Salvar</button>'
|
|
557
|
+
+ '</div>'
|
|
558
|
+
+ '</div>'
|
|
559
|
+
+ '<div class="reportBody">'
|
|
560
|
+
+ '<div class="reportPreview" contenteditable="true"></div>'
|
|
561
|
+
+ '<textarea class="reportRaw" rows="6"></textarea>'
|
|
562
|
+
+ '</div>';
|
|
563
|
+
|
|
564
|
+
const text = state.reportTexts[item.relPath] || '';
|
|
565
|
+
const preview = card.querySelector('.reportPreview');
|
|
566
|
+
if (preview) preview.innerHTML = renderMarkdown(text || '');
|
|
567
|
+
const raw = card.querySelector('.reportRaw');
|
|
568
|
+
if (raw) {
|
|
569
|
+
raw.value = text;
|
|
570
|
+
autoGrowTextarea(raw);
|
|
571
|
+
raw.addEventListener('input', () => {
|
|
572
|
+
state.reportTexts[item.relPath] = raw.value;
|
|
573
|
+
autoGrowTextarea(raw);
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (preview) {
|
|
578
|
+
preview.addEventListener('input', () => {
|
|
579
|
+
const val = preview.innerText || '';
|
|
580
|
+
state.reportTexts[item.relPath] = val;
|
|
581
|
+
if (raw) {
|
|
582
|
+
raw.value = val;
|
|
583
|
+
autoGrowTextarea(raw);
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const toggleBtn = card.querySelector('[data-action="toggle"]');
|
|
589
|
+
if (toggleBtn) {
|
|
590
|
+
toggleBtn.onclick = () => {
|
|
591
|
+
state.reportModes[item.relPath] = (state.reportModes[item.relPath] === 'raw') ? 'preview' : 'raw';
|
|
592
|
+
renderReportsPage();
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const saveBtn = card.querySelector('[data-action="save"]');
|
|
597
|
+
if (saveBtn) {
|
|
598
|
+
saveBtn.onclick = async () => {
|
|
599
|
+
try {
|
|
600
|
+
const content = (raw && typeof raw.value === 'string') ? raw.value : (state.reportTexts[item.relPath] || '');
|
|
601
|
+
setPill('run', 'salvando…');
|
|
602
|
+
await api('/api/reports/write', { dir: dirOrDefault(), relPath: item.relPath, text: content });
|
|
603
|
+
state.reportTexts[item.relPath] = content;
|
|
604
|
+
setPill('ok', 'salvo');
|
|
605
|
+
setTimeout(() => setPill('ok', 'pronto'), 800);
|
|
606
|
+
renderReportsPage();
|
|
607
|
+
} catch (e) {
|
|
608
|
+
setPill('err', 'falhou');
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const pdfBtn = card.querySelector('[data-action="pdf"]');
|
|
614
|
+
if (pdfBtn) {
|
|
615
|
+
pdfBtn.onclick = (ev) => {
|
|
616
|
+
ev.stopPropagation();
|
|
617
|
+
downloadReportPdf(item);
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const head = card.querySelector('[data-action="expand"]');
|
|
622
|
+
if (head) {
|
|
623
|
+
head.onclick = () => {
|
|
624
|
+
state.reportExpanded = state.reportExpanded || {};
|
|
625
|
+
state.reportExpanded[item.relPath] = !state.reportExpanded[item.relPath];
|
|
626
|
+
renderReportsPage();
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
grid.appendChild(card);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function refreshReportsPage() {
|
|
635
|
+
try {
|
|
636
|
+
setPill('run', 'carregando…');
|
|
637
|
+
const r = await api('/api/reports/list', { dir: dirOrDefault() });
|
|
638
|
+
state.reports = (r.reports || []);
|
|
639
|
+
state.reportTexts = {};
|
|
640
|
+
await Promise.all(state.reports.map(async (item) => {
|
|
641
|
+
try {
|
|
642
|
+
const rr = await api('/api/reports/read', { dir: dirOrDefault(), relPath: item.relPath });
|
|
643
|
+
state.reportTexts[item.relPath] = rr.text || '';
|
|
644
|
+
} catch {
|
|
645
|
+
state.reportTexts[item.relPath] = '';
|
|
646
|
+
}
|
|
647
|
+
}));
|
|
648
|
+
renderReportsPage();
|
|
649
|
+
setPill('ok', 'pronto');
|
|
650
|
+
} catch (e) {
|
|
651
|
+
setPill('err', 'falhou');
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function wireRailNav() {
|
|
656
|
+
const dash = $('railDashboard');
|
|
657
|
+
const rep = $('railReports');
|
|
658
|
+
if (dash) {
|
|
659
|
+
dash.onclick = () => {
|
|
660
|
+
const isReports = document.body && document.body.dataset && document.body.dataset.page === 'reports';
|
|
661
|
+
if (isReports) {
|
|
662
|
+
window.location.href = '/';
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const c = document.querySelector('.centerBody');
|
|
666
|
+
if (c) c.scrollTo({ top: 0, behavior: 'smooth' });
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
if (rep) {
|
|
670
|
+
rep.onclick = () => {
|
|
671
|
+
const isReports = document.body && document.body.dataset && document.body.dataset.page === 'reports';
|
|
672
|
+
if (!isReports) window.location.href = '/reports';
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
497
677
|
async function editTask(t) {
|
|
498
678
|
try {
|
|
499
679
|
const currentSlug = t.projectSlug ? String(t.projectSlug) : '';
|
|
@@ -990,10 +1170,11 @@
|
|
|
990
1170
|
}
|
|
991
1171
|
|
|
992
1172
|
// init
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1173
|
+
applyDarkTheme();
|
|
1174
|
+
const chipPort = $('chipPort');
|
|
1175
|
+
if (chipPort) chipPort.textContent = location.host;
|
|
996
1176
|
loadLocal();
|
|
1177
|
+
wireRailNav();
|
|
997
1178
|
|
|
998
1179
|
// Developer drawer (persist open/close)
|
|
999
1180
|
try {
|
|
@@ -1007,23 +1188,34 @@
|
|
|
1007
1188
|
}
|
|
1008
1189
|
} catch {}
|
|
1009
1190
|
|
|
1191
|
+
const isReportsPage = document.body && document.body.dataset && document.body.dataset.page === 'reports';
|
|
1192
|
+
|
|
1010
1193
|
// Load persisted settings from the workspace + bootstrap (auto-init + auto-health)
|
|
1011
1194
|
(async () => {
|
|
1012
1195
|
let defaults = null;
|
|
1013
1196
|
try {
|
|
1014
1197
|
defaults = await api('/api/defaults', { dir: dirOrDefault() });
|
|
1015
1198
|
if (defaults && defaults.workspaceDir) {
|
|
1016
|
-
$('dir')
|
|
1017
|
-
|
|
1199
|
+
const dirEl = $('dir');
|
|
1200
|
+
if (dirEl) dirEl.value = defaults.workspaceDir;
|
|
1201
|
+
const side = $('sidePath');
|
|
1202
|
+
if (side) side.textContent = defaults.workspaceDir;
|
|
1018
1203
|
}
|
|
1019
1204
|
if (defaults && defaults.settings) {
|
|
1020
|
-
$('discord')
|
|
1021
|
-
$('teams')
|
|
1205
|
+
const discord = $('discord');
|
|
1206
|
+
const teams = $('teams');
|
|
1207
|
+
if (discord) discord.value = defaults.settings.discordWebhookUrl || '';
|
|
1208
|
+
if (teams) teams.value = defaults.settings.teamsWebhookUrl || '';
|
|
1022
1209
|
}
|
|
1023
1210
|
} catch (e) {
|
|
1024
1211
|
// ignore
|
|
1025
1212
|
}
|
|
1026
1213
|
|
|
1214
|
+
if (isReportsPage) {
|
|
1215
|
+
await refreshReportsPage();
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1027
1219
|
// If workspace isn't initialized yet, auto-init (reduces clicks)
|
|
1028
1220
|
try {
|
|
1029
1221
|
if (defaults && defaults.workspaceOk === false) {
|
|
@@ -1060,12 +1252,13 @@
|
|
|
1060
1252
|
window.exportObsidian = exportObsidian;
|
|
1061
1253
|
window.rebuildIndex = rebuildIndex;
|
|
1062
1254
|
window.renderReportsList = renderReportsList;
|
|
1255
|
+
window.renderReportsPage = renderReportsPage;
|
|
1256
|
+
window.refreshReportsPage = refreshReportsPage;
|
|
1063
1257
|
window.copyOut = copyOut;
|
|
1064
1258
|
window.copyPath = copyPath;
|
|
1065
1259
|
window.openSelected = openSelected;
|
|
1066
1260
|
window.downloadSelected = downloadSelected;
|
|
1067
1261
|
window.clearOut = clearOut;
|
|
1068
|
-
window.toggleTheme = toggleTheme;
|
|
1069
1262
|
window.saveInbox = saveInbox;
|
|
1070
1263
|
window.saveAndPlan = saveAndPlan;
|
|
1071
1264
|
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,11 +906,8 @@ 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"
|
|
905
|
-
<button class="railBtn" type="button" title="Relatórios"
|
|
906
|
-
<button class="railBtn" type="button" title="Relatórios (atalho 2)">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
913
|
<div class="railStatus" id="railStatus" title="status"></div>
|
|
@@ -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>
|
|
@@ -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' });
|