@cccarv82/freya 1.0.11 → 1.0.12
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.js +329 -15
- package/package.json +1 -1
package/cli/web.js
CHANGED
|
@@ -59,6 +59,67 @@ function newestFile(dir, prefix) {
|
|
|
59
59
|
return files[0]?.p || null;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function settingsPath(workspaceDir) {
|
|
63
|
+
return path.join(workspaceDir, 'data', 'settings', 'settings.json');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readSettings(workspaceDir) {
|
|
67
|
+
const p = settingsPath(workspaceDir);
|
|
68
|
+
try {
|
|
69
|
+
if (!exists(p)) return { discordWebhookUrl: '', teamsWebhookUrl: '' };
|
|
70
|
+
const json = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
71
|
+
return {
|
|
72
|
+
discordWebhookUrl: json.discordWebhookUrl || '',
|
|
73
|
+
teamsWebhookUrl: json.teamsWebhookUrl || ''
|
|
74
|
+
};
|
|
75
|
+
} catch {
|
|
76
|
+
return { discordWebhookUrl: '', teamsWebhookUrl: '' };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function writeSettings(workspaceDir, settings) {
|
|
81
|
+
const p = settingsPath(workspaceDir);
|
|
82
|
+
ensureDir(path.dirname(p));
|
|
83
|
+
const out = {
|
|
84
|
+
schemaVersion: 1,
|
|
85
|
+
updatedAt: new Date().toISOString(),
|
|
86
|
+
discordWebhookUrl: settings.discordWebhookUrl || '',
|
|
87
|
+
teamsWebhookUrl: settings.teamsWebhookUrl || ''
|
|
88
|
+
};
|
|
89
|
+
fs.writeFileSync(p, JSON.stringify(out, null, 2) + '\n', 'utf8');
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function listReports(workspaceDir) {
|
|
94
|
+
const dir = path.join(workspaceDir, 'docs', 'reports');
|
|
95
|
+
if (!exists(dir)) return [];
|
|
96
|
+
|
|
97
|
+
const files = fs.readdirSync(dir)
|
|
98
|
+
.filter((f) => f.endsWith('.md'))
|
|
99
|
+
.map((name) => {
|
|
100
|
+
const full = path.join(dir, name);
|
|
101
|
+
const st = fs.statSync(full);
|
|
102
|
+
return { name, full, mtimeMs: st.mtimeMs, size: st.size };
|
|
103
|
+
})
|
|
104
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
105
|
+
|
|
106
|
+
function kind(name) {
|
|
107
|
+
if (name.startsWith('executive-')) return 'executive';
|
|
108
|
+
if (name.startsWith('sm-weekly-')) return 'sm-weekly';
|
|
109
|
+
if (name.startsWith('blockers-')) return 'blockers';
|
|
110
|
+
if (name.startsWith('daily-')) return 'daily';
|
|
111
|
+
return 'other';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return files.map((f) => ({
|
|
115
|
+
kind: kind(f.name),
|
|
116
|
+
name: f.name,
|
|
117
|
+
relPath: path.relative(workspaceDir, f.full).replace(/\\/g, '/'),
|
|
118
|
+
mtimeMs: f.mtimeMs,
|
|
119
|
+
size: f.size
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
|
|
62
123
|
function safeJson(res, code, obj) {
|
|
63
124
|
const body = JSON.stringify(obj);
|
|
64
125
|
res.writeHead(code, {
|
|
@@ -607,6 +668,32 @@ function html(defaultDir) {
|
|
|
607
668
|
|
|
608
669
|
.sideBtn { width: 100%; margin-top: 8px; }
|
|
609
670
|
|
|
671
|
+
.rep {
|
|
672
|
+
width: 100%;
|
|
673
|
+
text-align: left;
|
|
674
|
+
border: 1px solid var(--line);
|
|
675
|
+
border-radius: 12px;
|
|
676
|
+
background: var(--paper2);
|
|
677
|
+
padding: 10px 12px;
|
|
678
|
+
cursor: pointer;
|
|
679
|
+
font-family: var(--mono);
|
|
680
|
+
font-size: 12px;
|
|
681
|
+
color: var(--muted);
|
|
682
|
+
}
|
|
683
|
+
.rep:hover { border-color: var(--line2); box-shadow: 0 10px 22px rgba(16,24,40,.10); }
|
|
684
|
+
.repActive { border-color: rgba(59,130,246,.55); box-shadow: 0 0 0 4px rgba(59,130,246,.12); }
|
|
685
|
+
|
|
686
|
+
.md-h1{ font-size: 20px; margin: 10px 0 6px; }
|
|
687
|
+
.md-h2{ font-size: 16px; margin: 10px 0 6px; }
|
|
688
|
+
.md-h3{ font-size: 14px; margin: 10px 0 6px; }
|
|
689
|
+
.md-p{ margin: 6px 0; color: var(--muted); line-height: 1.5; }
|
|
690
|
+
.md-ul{ margin: 6px 0 6px 18px; color: var(--muted); }
|
|
691
|
+
.md-inline{ font-family: var(--mono); font-size: 12px; padding: 2px 6px; border: 1px solid var(--line); border-radius: 8px; background: rgba(255,255,255,.55); }
|
|
692
|
+
[data-theme="dark"] .md-inline{ background: rgba(0,0,0,.18); }
|
|
693
|
+
.md-code{ background: rgba(0,0,0,.05); border: 1px solid var(--line); border-radius: 14px; padding: 12px; overflow:auto; }
|
|
694
|
+
[data-theme="dark"] .md-code{ background: rgba(0,0,0,.22); }
|
|
695
|
+
.md-sp{ height: 10px; }
|
|
696
|
+
|
|
610
697
|
.k { display: inline-block; padding: 2px 6px; border: 1px solid var(--line); border-radius: 8px; background: rgba(255,255,255,.65); font-family: var(--mono); font-size: 12px; color: var(--muted); }
|
|
611
698
|
[data-theme="dark"] .k { background: rgba(0,0,0,.18); }
|
|
612
699
|
|
|
@@ -689,35 +776,52 @@ function html(defaultDir) {
|
|
|
689
776
|
|
|
690
777
|
<div style="height:12px"></div>
|
|
691
778
|
|
|
692
|
-
<div style="height:16px"></div>
|
|
693
|
-
|
|
694
779
|
<label>Discord webhook URL</label>
|
|
695
780
|
<input id="discord" placeholder="https://discord.com/api/webhooks/..." />
|
|
696
781
|
<div style="height:10px"></div>
|
|
697
782
|
|
|
698
783
|
<label>Teams webhook URL</label>
|
|
699
784
|
<input id="teams" placeholder="https://..." />
|
|
700
|
-
<div class="help">
|
|
785
|
+
<div class="help">Os webhooks ficam salvos na workspace em <code>data/settings/settings.json</code>.</div>
|
|
701
786
|
|
|
702
787
|
<div style="height:10px"></div>
|
|
703
788
|
<div class="stack">
|
|
704
|
-
<button class="btn" onclick="
|
|
705
|
-
<button class="btn" onclick="publish('
|
|
789
|
+
<button class="btn" onclick="saveSettings()">Save settings</button>
|
|
790
|
+
<button class="btn" onclick="publish('discord')">Publish selected → Discord</button>
|
|
791
|
+
<button class="btn" onclick="publish('teams')">Publish selected → Teams</button>
|
|
706
792
|
</div>
|
|
793
|
+
|
|
794
|
+
<div style="height:14px"></div>
|
|
795
|
+
|
|
796
|
+
<div class="help"><b>Dica:</b> clique em um relatório em <i>Reports</i> para ver o preview e habilitar publish/copy.</div>
|
|
707
797
|
</div>
|
|
708
798
|
</div>
|
|
709
799
|
|
|
710
800
|
<div class="panel">
|
|
711
801
|
<div class="panelHead">
|
|
712
|
-
<b>
|
|
802
|
+
<b>Reports</b>
|
|
803
|
+
<div class="stack">
|
|
804
|
+
<button class="btn small" onclick="refreshReports()">Refresh</button>
|
|
805
|
+
</div>
|
|
806
|
+
</div>
|
|
807
|
+
<div class="panelBody">
|
|
808
|
+
<input id="reportsFilter" placeholder="filter (ex: daily, executive, 2026-01-29)" style="width:100%; margin-bottom:10px" oninput="renderReportsList()" />
|
|
809
|
+
<div id="reportsList" style="display:grid; gap:8px"></div>
|
|
810
|
+
<div class="help">Últimos relatórios em <code>docs/reports</code>. Clique para abrir preview.</div>
|
|
811
|
+
</div>
|
|
812
|
+
</div>
|
|
813
|
+
|
|
814
|
+
<div class="panel">
|
|
815
|
+
<div class="panelHead">
|
|
816
|
+
<b>Preview</b>
|
|
713
817
|
<div class="stack">
|
|
714
818
|
<button class="btn small" onclick="copyOut()">Copy</button>
|
|
715
819
|
<button class="btn small" onclick="clearOut()">Clear</button>
|
|
716
820
|
</div>
|
|
717
821
|
</div>
|
|
718
822
|
<div class="panelBody">
|
|
719
|
-
<div class="log"
|
|
720
|
-
<div class="help">
|
|
823
|
+
<div id="reportPreview" class="log md" style="font-family: var(--sans);"></div>
|
|
824
|
+
<div class="help">O preview renderiza Markdown básico (headers, listas, code). O botão Copy copia o conteúdo completo.</div>
|
|
721
825
|
</div>
|
|
722
826
|
</div>
|
|
723
827
|
|
|
@@ -732,7 +836,7 @@ function html(defaultDir) {
|
|
|
732
836
|
<script>
|
|
733
837
|
window.__FREYA_DEFAULT_DIR = "${safeDefault}";
|
|
734
838
|
const $ = (id) => document.getElementById(id);
|
|
735
|
-
const state = { lastReportPath: null, lastText: '' };
|
|
839
|
+
const state = { lastReportPath: null, lastText: '', reports: [], selectedReport: null };
|
|
736
840
|
|
|
737
841
|
function applyTheme(theme) {
|
|
738
842
|
document.documentElement.setAttribute('data-theme', theme);
|
|
@@ -754,13 +858,89 @@ function html(defaultDir) {
|
|
|
754
858
|
$('status') && ($('status').textContent = text);
|
|
755
859
|
}
|
|
756
860
|
|
|
861
|
+
function escapeHtml(str) {
|
|
862
|
+
return String(str)
|
|
863
|
+
.replace(/&/g, '&')
|
|
864
|
+
.replace(/</g, '<')
|
|
865
|
+
.replace(/>/g, '>')
|
|
866
|
+
.replace(/\"/g, '"')
|
|
867
|
+
.replace(/'/g, ''');
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function renderMarkdown(md) {
|
|
871
|
+
const lines = String(md || '').split(/\r?\n/);
|
|
872
|
+
let html = '';
|
|
873
|
+
let inCode = false;
|
|
874
|
+
let inList = false;
|
|
875
|
+
|
|
876
|
+
const BT = String.fromCharCode(96); // backtick
|
|
877
|
+
const FENCE = BT + BT + BT;
|
|
878
|
+
const inlineCodeRe = /\x60([^\x60]+)\x60/g;
|
|
879
|
+
|
|
880
|
+
const closeList = () => {
|
|
881
|
+
if (inList) { html += '</ul>'; inList = false; }
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
for (const line of lines) {
|
|
885
|
+
if (line.trim().startsWith(FENCE)) {
|
|
886
|
+
if (!inCode) {
|
|
887
|
+
closeList();
|
|
888
|
+
inCode = true;
|
|
889
|
+
html += '<pre class="md-code"><code>';
|
|
890
|
+
} else {
|
|
891
|
+
inCode = false;
|
|
892
|
+
html += '</code></pre>';
|
|
893
|
+
}
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (inCode) {
|
|
898
|
+
html += escapeHtml(line) + '\n';
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const h = line.match(/^(#{1,3})\s+(.*)$/);
|
|
903
|
+
if (h) {
|
|
904
|
+
closeList();
|
|
905
|
+
const lvl = h[1].length;
|
|
906
|
+
html += '<h' + lvl + ' class="md-h' + lvl + '">' + escapeHtml(h[2]) + '</h' + lvl + '>';
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const li = line.match(/^\s*[-*]\s+(.*)$/);
|
|
911
|
+
if (li) {
|
|
912
|
+
if (!inList) { html += '<ul class="md-ul">'; inList = true; }
|
|
913
|
+
const content = escapeHtml(li[1]).replace(inlineCodeRe, '<code class="md-inline">$1</code>');
|
|
914
|
+
html += '<li>' + content + '</li>';
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (line.trim() === '') {
|
|
919
|
+
closeList();
|
|
920
|
+
html += '<div class="md-sp"></div>';
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
closeList();
|
|
925
|
+
const p = escapeHtml(line).replace(inlineCodeRe, '<code class="md-inline">$1</code>');
|
|
926
|
+
html += '<p class="md-p">' + p + '</p>';
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
closeList();
|
|
930
|
+
if (inCode) html += '</code></pre>';
|
|
931
|
+
return html;
|
|
932
|
+
}
|
|
933
|
+
|
|
757
934
|
function setOut(text) {
|
|
758
935
|
state.lastText = text || '';
|
|
759
|
-
$('
|
|
936
|
+
const el = $('reportPreview');
|
|
937
|
+
if (el) el.innerHTML = renderMarkdown(state.lastText);
|
|
760
938
|
}
|
|
761
939
|
|
|
762
940
|
function clearOut() {
|
|
763
|
-
|
|
941
|
+
state.lastText = '';
|
|
942
|
+
const el = $('reportPreview');
|
|
943
|
+
if (el) el.innerHTML = '';
|
|
764
944
|
setPill('ok', 'idle');
|
|
765
945
|
}
|
|
766
946
|
|
|
@@ -781,14 +961,10 @@ function html(defaultDir) {
|
|
|
781
961
|
|
|
782
962
|
function saveLocal() {
|
|
783
963
|
localStorage.setItem('freya.dir', $('dir').value);
|
|
784
|
-
localStorage.setItem('freya.discord', $('discord').value);
|
|
785
|
-
localStorage.setItem('freya.teams', $('teams').value);
|
|
786
964
|
}
|
|
787
965
|
|
|
788
966
|
function loadLocal() {
|
|
789
967
|
$('dir').value = (window.__FREYA_DEFAULT_DIR && window.__FREYA_DEFAULT_DIR !== '__FREYA_DEFAULT_DIR__') ? window.__FREYA_DEFAULT_DIR : (localStorage.getItem('freya.dir') || './freya');
|
|
790
|
-
$('discord').value = localStorage.getItem('freya.discord') || '';
|
|
791
|
-
$('teams').value = localStorage.getItem('freya.teams') || '';
|
|
792
968
|
$('sidePath').textContent = $('dir').value || './freya';
|
|
793
969
|
// Always persist the current run's default dir to avoid stale values
|
|
794
970
|
localStorage.setItem('freya.dir', $('dir').value || './freya');
|
|
@@ -810,6 +986,78 @@ function html(defaultDir) {
|
|
|
810
986
|
return d || './freya';
|
|
811
987
|
}
|
|
812
988
|
|
|
989
|
+
function fmtWhen(ms) {
|
|
990
|
+
try {
|
|
991
|
+
const d = new Date(ms);
|
|
992
|
+
const yy = String(d.getFullYear());
|
|
993
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
994
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
995
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
996
|
+
const mi = String(d.getMinutes()).padStart(2, '0');
|
|
997
|
+
return yy + '-' + mm + '-' + dd + ' ' + hh + ':' + mi;
|
|
998
|
+
} catch {
|
|
999
|
+
return '';
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
async function selectReport(item) {
|
|
1004
|
+
const rr = await api('/api/reports/read', { dir: dirOrDefault(), relPath: item.relPath });
|
|
1005
|
+
state.selectedReport = item;
|
|
1006
|
+
setLast(item.name);
|
|
1007
|
+
setOut(rr.text || '');
|
|
1008
|
+
renderReportsList();
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function renderReportsList() {
|
|
1012
|
+
const list = $('reportsList');
|
|
1013
|
+
if (!list) return;
|
|
1014
|
+
const q = ($('reportsFilter') ? $('reportsFilter').value : '').trim().toLowerCase();
|
|
1015
|
+
const filtered = (state.reports || []).filter((it) => {
|
|
1016
|
+
if (!q) return true;
|
|
1017
|
+
return (it.name + ' ' + it.kind).toLowerCase().includes(q);
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
list.innerHTML = '';
|
|
1021
|
+
for (const item of filtered) {
|
|
1022
|
+
const btn = document.createElement('button');
|
|
1023
|
+
btn.className = 'rep' + (state.selectedReport && state.selectedReport.relPath === item.relPath ? ' repActive' : '');
|
|
1024
|
+
btn.type = 'button';
|
|
1025
|
+
const meta = fmtWhen(item.mtimeMs);
|
|
1026
|
+
btn.innerHTML =
|
|
1027
|
+
'<div style="display:flex; gap:10px; align-items:center; justify-content:space-between">'
|
|
1028
|
+
+ '<div style="min-width:0">'
|
|
1029
|
+
+ '<div><span style="font-weight:800">' + escapeHtml(item.kind) + '</span> <span style="opacity:.7">—</span> ' + escapeHtml(item.name) + '</div>'
|
|
1030
|
+
+ '<div style="opacity:.65; font-size:11px; margin-top:4px">' + escapeHtml(item.relPath) + '</div>'
|
|
1031
|
+
+ '</div>'
|
|
1032
|
+
+ '<div style="opacity:.7; font-size:11px; white-space:nowrap">' + escapeHtml(meta) + '</div>'
|
|
1033
|
+
+ '</div>';
|
|
1034
|
+
|
|
1035
|
+
btn.onclick = async () => {
|
|
1036
|
+
try {
|
|
1037
|
+
await selectReport(item);
|
|
1038
|
+
} catch (e) {
|
|
1039
|
+
setPill('err', 'open failed');
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
list.appendChild(btn);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
async function refreshReports() {
|
|
1047
|
+
try {
|
|
1048
|
+
const r = await api('/api/reports/list', { dir: dirOrDefault() });
|
|
1049
|
+
state.reports = (r.reports || []).slice(0, 50);
|
|
1050
|
+
renderReportsList();
|
|
1051
|
+
|
|
1052
|
+
// Auto-select latest if nothing selected yet
|
|
1053
|
+
if (!state.selectedReport && state.reports && state.reports[0]) {
|
|
1054
|
+
await selectReport(state.reports[0]);
|
|
1055
|
+
}
|
|
1056
|
+
} catch (e) {
|
|
1057
|
+
// ignore
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
813
1061
|
async function pickDir() {
|
|
814
1062
|
try {
|
|
815
1063
|
setPill('run','picker…');
|
|
@@ -835,6 +1083,7 @@ function html(defaultDir) {
|
|
|
835
1083
|
const r = await api('/api/init', { dir: dirOrDefault() });
|
|
836
1084
|
setOut(r.output);
|
|
837
1085
|
setLast(null);
|
|
1086
|
+
await refreshReports();
|
|
838
1087
|
setPill('ok','init ok');
|
|
839
1088
|
} catch (e) {
|
|
840
1089
|
setPill('err','init failed');
|
|
@@ -851,6 +1100,7 @@ function html(defaultDir) {
|
|
|
851
1100
|
const r = await api('/api/update', { dir: dirOrDefault() });
|
|
852
1101
|
setOut(r.output);
|
|
853
1102
|
setLast(null);
|
|
1103
|
+
await refreshReports();
|
|
854
1104
|
setPill('ok','update ok');
|
|
855
1105
|
} catch (e) {
|
|
856
1106
|
setPill('err','update failed');
|
|
@@ -900,6 +1150,7 @@ function html(defaultDir) {
|
|
|
900
1150
|
setOut(r.output);
|
|
901
1151
|
setLast(r.reportPath || null);
|
|
902
1152
|
if (r.reportText) state.lastText = r.reportText;
|
|
1153
|
+
await refreshReports();
|
|
903
1154
|
setPill('ok', name + ' ok');
|
|
904
1155
|
} catch (e) {
|
|
905
1156
|
setPill('err', name + ' failed');
|
|
@@ -907,6 +1158,24 @@ function html(defaultDir) {
|
|
|
907
1158
|
}
|
|
908
1159
|
}
|
|
909
1160
|
|
|
1161
|
+
async function saveSettings() {
|
|
1162
|
+
try {
|
|
1163
|
+
saveLocal();
|
|
1164
|
+
setPill('run','saving…');
|
|
1165
|
+
await api('/api/settings/save', {
|
|
1166
|
+
dir: dirOrDefault(),
|
|
1167
|
+
settings: {
|
|
1168
|
+
discordWebhookUrl: $('discord').value.trim(),
|
|
1169
|
+
teamsWebhookUrl: $('teams').value.trim()
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
setPill('ok','saved');
|
|
1173
|
+
setTimeout(() => setPill('ok','idle'), 800);
|
|
1174
|
+
} catch (e) {
|
|
1175
|
+
setPill('err','save failed');
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
910
1179
|
async function publish(target) {
|
|
911
1180
|
try {
|
|
912
1181
|
saveLocal();
|
|
@@ -926,6 +1195,25 @@ function html(defaultDir) {
|
|
|
926
1195
|
applyTheme(localStorage.getItem('freya.theme') || 'light');
|
|
927
1196
|
$('chipPort').textContent = location.host;
|
|
928
1197
|
loadLocal();
|
|
1198
|
+
|
|
1199
|
+
// Load persisted settings from the workspace
|
|
1200
|
+
(async () => {
|
|
1201
|
+
try {
|
|
1202
|
+
const r = await api('/api/defaults', { dir: dirOrDefault() });
|
|
1203
|
+
if (r && r.workspaceDir) {
|
|
1204
|
+
$('dir').value = r.workspaceDir;
|
|
1205
|
+
$('sidePath').textContent = r.workspaceDir;
|
|
1206
|
+
}
|
|
1207
|
+
if (r && r.settings) {
|
|
1208
|
+
$('discord').value = r.settings.discordWebhookUrl || '';
|
|
1209
|
+
$('teams').value = r.settings.teamsWebhookUrl || '';
|
|
1210
|
+
}
|
|
1211
|
+
} catch (e) {
|
|
1212
|
+
// ignore
|
|
1213
|
+
}
|
|
1214
|
+
refreshReports();
|
|
1215
|
+
})();
|
|
1216
|
+
|
|
929
1217
|
setPill('ok','idle');
|
|
930
1218
|
</script>
|
|
931
1219
|
</body>
|
|
@@ -1127,6 +1415,32 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1127
1415
|
return safeJson(res, 200, { dir: picked });
|
|
1128
1416
|
}
|
|
1129
1417
|
|
|
1418
|
+
if (req.url === '/api/defaults') {
|
|
1419
|
+
const settings = readSettings(workspaceDir);
|
|
1420
|
+
const reports = listReports(workspaceDir).slice(0, 20);
|
|
1421
|
+
return safeJson(res, 200, { workspaceDir, settings, reports });
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
if (req.url === '/api/settings/save') {
|
|
1425
|
+
const incoming = payload.settings || {};
|
|
1426
|
+
const saved = writeSettings(workspaceDir, incoming);
|
|
1427
|
+
return safeJson(res, 200, { ok: true, settings: { discordWebhookUrl: saved.discordWebhookUrl, teamsWebhookUrl: saved.teamsWebhookUrl } });
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
if (req.url === '/api/reports/list') {
|
|
1431
|
+
const reports = listReports(workspaceDir);
|
|
1432
|
+
return safeJson(res, 200, { reports });
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
if (req.url === '/api/reports/read') {
|
|
1436
|
+
const rel = payload.relPath;
|
|
1437
|
+
if (!rel) return safeJson(res, 400, { error: 'Missing relPath' });
|
|
1438
|
+
const full = path.join(workspaceDir, rel);
|
|
1439
|
+
if (!exists(full)) return safeJson(res, 404, { error: 'Report not found' });
|
|
1440
|
+
const text = fs.readFileSync(full, 'utf8');
|
|
1441
|
+
return safeJson(res, 200, { relPath: rel, text });
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1130
1444
|
if (req.url === '/api/init') {
|
|
1131
1445
|
const pkg = '@cccarv82/freya';
|
|
1132
1446
|
const r = await run(guessNpxCmd(), [guessNpxYesFlag(), pkg, 'init', workspaceDir], process.cwd());
|