@cccarv82/freya 1.0.10 → 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
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, {
|
|
@@ -185,8 +246,9 @@ async function pickDirectoryNative() {
|
|
|
185
246
|
return null;
|
|
186
247
|
}
|
|
187
248
|
|
|
188
|
-
function html() {
|
|
249
|
+
function html(defaultDir) {
|
|
189
250
|
// Aesthetic: “clean workstation” — light-first UI inspired by modern productivity apps.
|
|
251
|
+
const safeDefault = String(defaultDir || './freya').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
190
252
|
return `<!doctype html>
|
|
191
253
|
<html>
|
|
192
254
|
<head>
|
|
@@ -606,6 +668,32 @@ function html() {
|
|
|
606
668
|
|
|
607
669
|
.sideBtn { width: 100%; margin-top: 8px; }
|
|
608
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
|
+
|
|
609
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); }
|
|
610
698
|
[data-theme="dark"] .k { background: rgba(0,0,0,.18); }
|
|
611
699
|
|
|
@@ -632,13 +720,6 @@ function html() {
|
|
|
632
720
|
<div class="help">Dica: se você já tem uma workspace antiga, use <b>Update</b>. Por padrão, <b>data/</b> e <b>logs/</b> não são sobrescritos.</div>
|
|
633
721
|
</div>
|
|
634
722
|
|
|
635
|
-
<div class="sideBlock">
|
|
636
|
-
<h3>Publish</h3>
|
|
637
|
-
<button class="btn sideBtn" onclick="publish('discord')">Publish → Discord</button>
|
|
638
|
-
<button class="btn sideBtn" onclick="publish('teams')">Publish → Teams</button>
|
|
639
|
-
<div class="help">Configure os webhooks no painel principal. O publish envia o último relatório gerado.</div>
|
|
640
|
-
</div>
|
|
641
|
-
|
|
642
723
|
<div class="sideBlock">
|
|
643
724
|
<h3>Atalhos</h3>
|
|
644
725
|
<div class="help"><span class="k">--dev</span> cria dados de exemplo para testar rápido.</div>
|
|
@@ -695,42 +776,52 @@ function html() {
|
|
|
695
776
|
|
|
696
777
|
<div style="height:12px"></div>
|
|
697
778
|
|
|
698
|
-
<div class="stack">
|
|
699
|
-
<button class="btn primary" onclick="doInit()">Init</button>
|
|
700
|
-
<button class="btn" onclick="doUpdate()">Update</button>
|
|
701
|
-
<button class="btn" onclick="doHealth()">Health</button>
|
|
702
|
-
<button class="btn" onclick="doMigrate()">Migrate</button>
|
|
703
|
-
</div>
|
|
704
|
-
|
|
705
|
-
<div style="height:16px"></div>
|
|
706
|
-
|
|
707
779
|
<label>Discord webhook URL</label>
|
|
708
780
|
<input id="discord" placeholder="https://discord.com/api/webhooks/..." />
|
|
709
781
|
<div style="height:10px"></div>
|
|
710
782
|
|
|
711
783
|
<label>Teams webhook URL</label>
|
|
712
784
|
<input id="teams" placeholder="https://..." />
|
|
713
|
-
<div class="help">
|
|
785
|
+
<div class="help">Os webhooks ficam salvos na workspace em <code>data/settings/settings.json</code>.</div>
|
|
714
786
|
|
|
715
787
|
<div style="height:10px"></div>
|
|
716
788
|
<div class="stack">
|
|
717
|
-
<button class="btn" onclick="
|
|
718
|
-
<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>
|
|
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>
|
|
797
|
+
</div>
|
|
798
|
+
</div>
|
|
799
|
+
|
|
800
|
+
<div class="panel">
|
|
801
|
+
<div class="panelHead">
|
|
802
|
+
<b>Reports</b>
|
|
803
|
+
<div class="stack">
|
|
804
|
+
<button class="btn small" onclick="refreshReports()">Refresh</button>
|
|
719
805
|
</div>
|
|
720
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>
|
|
721
812
|
</div>
|
|
722
813
|
|
|
723
814
|
<div class="panel">
|
|
724
815
|
<div class="panelHead">
|
|
725
|
-
<b>
|
|
816
|
+
<b>Preview</b>
|
|
726
817
|
<div class="stack">
|
|
727
818
|
<button class="btn small" onclick="copyOut()">Copy</button>
|
|
728
819
|
<button class="btn small" onclick="clearOut()">Clear</button>
|
|
729
820
|
</div>
|
|
730
821
|
</div>
|
|
731
822
|
<div class="panelBody">
|
|
732
|
-
<div class="log"
|
|
733
|
-
<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>
|
|
734
825
|
</div>
|
|
735
826
|
</div>
|
|
736
827
|
|
|
@@ -743,8 +834,9 @@ function html() {
|
|
|
743
834
|
</div>
|
|
744
835
|
|
|
745
836
|
<script>
|
|
837
|
+
window.__FREYA_DEFAULT_DIR = "${safeDefault}";
|
|
746
838
|
const $ = (id) => document.getElementById(id);
|
|
747
|
-
const state = { lastReportPath: null, lastText: '' };
|
|
839
|
+
const state = { lastReportPath: null, lastText: '', reports: [], selectedReport: null };
|
|
748
840
|
|
|
749
841
|
function applyTheme(theme) {
|
|
750
842
|
document.documentElement.setAttribute('data-theme', theme);
|
|
@@ -766,13 +858,89 @@ function html() {
|
|
|
766
858
|
$('status') && ($('status').textContent = text);
|
|
767
859
|
}
|
|
768
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
|
+
|
|
769
934
|
function setOut(text) {
|
|
770
935
|
state.lastText = text || '';
|
|
771
|
-
$('
|
|
936
|
+
const el = $('reportPreview');
|
|
937
|
+
if (el) el.innerHTML = renderMarkdown(state.lastText);
|
|
772
938
|
}
|
|
773
939
|
|
|
774
940
|
function clearOut() {
|
|
775
|
-
|
|
941
|
+
state.lastText = '';
|
|
942
|
+
const el = $('reportPreview');
|
|
943
|
+
if (el) el.innerHTML = '';
|
|
776
944
|
setPill('ok', 'idle');
|
|
777
945
|
}
|
|
778
946
|
|
|
@@ -793,15 +961,13 @@ function html() {
|
|
|
793
961
|
|
|
794
962
|
function saveLocal() {
|
|
795
963
|
localStorage.setItem('freya.dir', $('dir').value);
|
|
796
|
-
localStorage.setItem('freya.discord', $('discord').value);
|
|
797
|
-
localStorage.setItem('freya.teams', $('teams').value);
|
|
798
964
|
}
|
|
799
965
|
|
|
800
966
|
function loadLocal() {
|
|
801
|
-
$('dir').value = localStorage.getItem('freya.dir') || './freya';
|
|
802
|
-
$('discord').value = localStorage.getItem('freya.discord') || '';
|
|
803
|
-
$('teams').value = localStorage.getItem('freya.teams') || '';
|
|
967
|
+
$('dir').value = (window.__FREYA_DEFAULT_DIR && window.__FREYA_DEFAULT_DIR !== '__FREYA_DEFAULT_DIR__') ? window.__FREYA_DEFAULT_DIR : (localStorage.getItem('freya.dir') || './freya');
|
|
804
968
|
$('sidePath').textContent = $('dir').value || './freya';
|
|
969
|
+
// Always persist the current run's default dir to avoid stale values
|
|
970
|
+
localStorage.setItem('freya.dir', $('dir').value || './freya');
|
|
805
971
|
}
|
|
806
972
|
|
|
807
973
|
async function api(p, body) {
|
|
@@ -820,6 +986,78 @@ function html() {
|
|
|
820
986
|
return d || './freya';
|
|
821
987
|
}
|
|
822
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
|
+
|
|
823
1061
|
async function pickDir() {
|
|
824
1062
|
try {
|
|
825
1063
|
setPill('run','picker…');
|
|
@@ -845,6 +1083,7 @@ function html() {
|
|
|
845
1083
|
const r = await api('/api/init', { dir: dirOrDefault() });
|
|
846
1084
|
setOut(r.output);
|
|
847
1085
|
setLast(null);
|
|
1086
|
+
await refreshReports();
|
|
848
1087
|
setPill('ok','init ok');
|
|
849
1088
|
} catch (e) {
|
|
850
1089
|
setPill('err','init failed');
|
|
@@ -861,6 +1100,7 @@ function html() {
|
|
|
861
1100
|
const r = await api('/api/update', { dir: dirOrDefault() });
|
|
862
1101
|
setOut(r.output);
|
|
863
1102
|
setLast(null);
|
|
1103
|
+
await refreshReports();
|
|
864
1104
|
setPill('ok','update ok');
|
|
865
1105
|
} catch (e) {
|
|
866
1106
|
setPill('err','update failed');
|
|
@@ -910,6 +1150,7 @@ function html() {
|
|
|
910
1150
|
setOut(r.output);
|
|
911
1151
|
setLast(r.reportPath || null);
|
|
912
1152
|
if (r.reportText) state.lastText = r.reportText;
|
|
1153
|
+
await refreshReports();
|
|
913
1154
|
setPill('ok', name + ' ok');
|
|
914
1155
|
} catch (e) {
|
|
915
1156
|
setPill('err', name + ' failed');
|
|
@@ -917,6 +1158,24 @@ function html() {
|
|
|
917
1158
|
}
|
|
918
1159
|
}
|
|
919
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
|
+
|
|
920
1179
|
async function publish(target) {
|
|
921
1180
|
try {
|
|
922
1181
|
saveLocal();
|
|
@@ -936,6 +1195,25 @@ function html() {
|
|
|
936
1195
|
applyTheme(localStorage.getItem('freya.theme') || 'light');
|
|
937
1196
|
$('chipPort').textContent = location.host;
|
|
938
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
|
+
|
|
939
1217
|
setPill('ok','idle');
|
|
940
1218
|
</script>
|
|
941
1219
|
</body>
|
|
@@ -1119,7 +1397,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1119
1397
|
if (!req.url) return safeJson(res, 404, { error: 'Not found' });
|
|
1120
1398
|
|
|
1121
1399
|
if (req.method === 'GET' && req.url === '/') {
|
|
1122
|
-
const body = html();
|
|
1400
|
+
const body = html(dir || './freya');
|
|
1123
1401
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
1124
1402
|
res.end(body);
|
|
1125
1403
|
return;
|
|
@@ -1137,6 +1415,32 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1137
1415
|
return safeJson(res, 200, { dir: picked });
|
|
1138
1416
|
}
|
|
1139
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
|
+
|
|
1140
1444
|
if (req.url === '/api/init') {
|
|
1141
1445
|
const pkg = '@cccarv82/freya';
|
|
1142
1446
|
const r = await run(guessNpxCmd(), [guessNpxYesFlag(), pkg, 'init', workspaceDir], process.cwd());
|
|
@@ -1178,7 +1482,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1178
1482
|
blockers: 'blockers-',
|
|
1179
1483
|
'sm-weekly': 'sm-weekly-',
|
|
1180
1484
|
status: 'executive-',
|
|
1181
|
-
daily:
|
|
1485
|
+
daily: 'daily-'
|
|
1182
1486
|
};
|
|
1183
1487
|
const prefix = prefixMap[script] || null;
|
|
1184
1488
|
const reportPath = prefix ? newestFile(reportsDir, prefix) : null;
|
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@ const { safeReadJson, quarantineCorruptedFile } = require('./lib/fs-utils');
|
|
|
6
6
|
|
|
7
7
|
const TASK_LOG_PATH = path.join(__dirname, '../data/tasks/task-log.json');
|
|
8
8
|
const BLOCKERS_LOG_PATH = path.join(__dirname, '../data/blockers/blocker-log.json');
|
|
9
|
+
const REPORT_DIR = path.join(__dirname, '../docs/reports');
|
|
9
10
|
|
|
10
11
|
// --- Helper Logic ---
|
|
11
12
|
const now = new Date();
|
|
@@ -88,6 +89,16 @@ function generateDailySummary() {
|
|
|
88
89
|
|
|
89
90
|
console.log(summary);
|
|
90
91
|
|
|
92
|
+
// Write report file for UI (optional, but helps preview/history)
|
|
93
|
+
try {
|
|
94
|
+
fs.mkdirSync(REPORT_DIR, { recursive: true });
|
|
95
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
96
|
+
const outPath = path.join(REPORT_DIR, `daily-${date}.md`);
|
|
97
|
+
fs.writeFileSync(outPath, `# Daily Summary — ${date}\n\n${summary}\n`, 'utf8');
|
|
98
|
+
} catch (e) {
|
|
99
|
+
// non-fatal
|
|
100
|
+
}
|
|
101
|
+
|
|
91
102
|
} catch (err) {
|
|
92
103
|
console.error("Error generating daily:", err.message);
|
|
93
104
|
}
|
|
@@ -218,7 +218,7 @@ function generateReport(period) {
|
|
|
218
218
|
|
|
219
219
|
// Save
|
|
220
220
|
ensureDir(OUTPUT_DIR);
|
|
221
|
-
const filename = `
|
|
221
|
+
const filename = `executive-${period}-${dateStr}.md`;
|
|
222
222
|
const outputPath = path.join(OUTPUT_DIR, filename);
|
|
223
223
|
fs.writeFileSync(outputPath, md, 'utf8');
|
|
224
224
|
|