@cccarv82/freya 1.0.14 → 1.0.16
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 +225 -0
- package/cli/web-ui.js +573 -0
- package/cli/web.js +563 -974
- package/package.json +2 -2
package/cli/web-ui.js
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
/* FREYA web UI script (served as static asset)
|
|
2
|
+
Keep this file plain JS to avoid escaping issues in inline template literals.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
(function () {
|
|
6
|
+
const $ = (id) => document.getElementById(id);
|
|
7
|
+
const state = { lastReportPath: null, lastText: '', reports: [], selectedReport: null, lastPlan: '', lastApplied: null };
|
|
8
|
+
|
|
9
|
+
function applyTheme(theme) {
|
|
10
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
11
|
+
localStorage.setItem('freya.theme', theme);
|
|
12
|
+
const t = $('themeToggle');
|
|
13
|
+
if (t) t.textContent = theme === 'dark' ? 'Light' : 'Dark';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function toggleTheme() {
|
|
17
|
+
const t = localStorage.getItem('freya.theme') || 'light';
|
|
18
|
+
applyTheme(t === 'dark' ? 'light' : 'dark');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function setPill(kind, text) {
|
|
22
|
+
const dot = $('dot');
|
|
23
|
+
if (!dot) return;
|
|
24
|
+
dot.classList.remove('ok', 'err');
|
|
25
|
+
if (kind === 'ok') dot.classList.add('ok');
|
|
26
|
+
if (kind === 'err') dot.classList.add('err');
|
|
27
|
+
const pill = $('pill');
|
|
28
|
+
if (pill) pill.textContent = text;
|
|
29
|
+
const status = $('status');
|
|
30
|
+
if (status) status.textContent = text;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function escapeHtml(str) {
|
|
34
|
+
return String(str)
|
|
35
|
+
.replace(/&/g, '&')
|
|
36
|
+
.replace(/</g, '<')
|
|
37
|
+
.replace(/>/g, '>')
|
|
38
|
+
.replace(/\"/g, '"')
|
|
39
|
+
.replace(/'/g, ''');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renderMarkdown(md) {
|
|
43
|
+
const lines = String(md || '').split(/\r?\n/);
|
|
44
|
+
let html = '';
|
|
45
|
+
let inCode = false;
|
|
46
|
+
let inList = false;
|
|
47
|
+
|
|
48
|
+
const NL = String.fromCharCode(10);
|
|
49
|
+
const BT = String.fromCharCode(96);
|
|
50
|
+
const FENCE = BT + BT + BT;
|
|
51
|
+
const inlineCodeRe = /\x60([^\x60]+)\x60/g;
|
|
52
|
+
|
|
53
|
+
const closeList = () => {
|
|
54
|
+
if (inList) { html += '</ul>'; inList = false; }
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
if (line.trim().startsWith(FENCE)) {
|
|
59
|
+
if (!inCode) {
|
|
60
|
+
closeList();
|
|
61
|
+
inCode = true;
|
|
62
|
+
html += '<pre class="md-code"><code>';
|
|
63
|
+
} else {
|
|
64
|
+
inCode = false;
|
|
65
|
+
html += '</code></pre>';
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (inCode) {
|
|
71
|
+
html += escapeHtml(line) + NL;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const h = line.match(/^(#{1,3})[ \t]+(.*)$/);
|
|
76
|
+
if (h) {
|
|
77
|
+
closeList();
|
|
78
|
+
const lvl = h[1].length;
|
|
79
|
+
html += '<h' + lvl + ' class="md-h' + lvl + '">' + escapeHtml(h[2]) + '</h' + lvl + '>';
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const li = line.match(/^[ \t]*[-*][ \t]+(.*)$/);
|
|
84
|
+
if (li) {
|
|
85
|
+
if (!inList) { html += '<ul class="md-ul">'; inList = true; }
|
|
86
|
+
const content = escapeHtml(li[1]).replace(inlineCodeRe, '<code class="md-inline">$1</code>');
|
|
87
|
+
html += '<li>' + content + '</li>';
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (line.trim() === '') {
|
|
92
|
+
closeList();
|
|
93
|
+
html += '<div class="md-sp"></div>';
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
closeList();
|
|
98
|
+
const p = escapeHtml(line).replace(inlineCodeRe, '<code class="md-inline">$1</code>');
|
|
99
|
+
html += '<p class="md-p">' + p + '</p>';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
closeList();
|
|
103
|
+
if (inCode) html += '</code></pre>';
|
|
104
|
+
return html;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function setOut(text) {
|
|
108
|
+
state.lastText = text || '';
|
|
109
|
+
const el = $('reportPreview');
|
|
110
|
+
if (el) el.innerHTML = renderMarkdown(state.lastText);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function clearOut() {
|
|
114
|
+
state.lastText = '';
|
|
115
|
+
const el = $('reportPreview');
|
|
116
|
+
if (el) el.innerHTML = '';
|
|
117
|
+
setPill('ok', 'idle');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function copyOut() {
|
|
121
|
+
try {
|
|
122
|
+
await navigator.clipboard.writeText(state.lastText || '');
|
|
123
|
+
setPill('ok', 'copied');
|
|
124
|
+
setTimeout(() => setPill('ok', 'idle'), 800);
|
|
125
|
+
} catch (e) {
|
|
126
|
+
setPill('err', 'copy failed');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function copyPath() {
|
|
131
|
+
try {
|
|
132
|
+
if (!state.selectedReport) {
|
|
133
|
+
setPill('err', 'no report');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const r = await api('/api/reports/resolve', { dir: dirOrDefault(), relPath: state.selectedReport.relPath });
|
|
137
|
+
await navigator.clipboard.writeText(r.fullPath || '');
|
|
138
|
+
setPill('ok', 'path copied');
|
|
139
|
+
setTimeout(() => setPill('ok', 'idle'), 800);
|
|
140
|
+
} catch {
|
|
141
|
+
setPill('err', 'copy path failed');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function openSelected() {
|
|
146
|
+
try {
|
|
147
|
+
if (!state.selectedReport) {
|
|
148
|
+
setPill('err', 'no report');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
setPill('run', 'opening…');
|
|
152
|
+
await api('/api/reports/open', { dir: dirOrDefault(), relPath: state.selectedReport.relPath });
|
|
153
|
+
setPill('ok', 'opened');
|
|
154
|
+
setTimeout(() => setPill('ok', 'idle'), 800);
|
|
155
|
+
} catch {
|
|
156
|
+
setPill('err', 'open failed');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function downloadSelected() {
|
|
161
|
+
try {
|
|
162
|
+
if (!state.selectedReport || !state.lastText) {
|
|
163
|
+
setPill('err', 'no report');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const blob = new Blob([state.lastText], { type: 'text/markdown;charset=utf-8' });
|
|
167
|
+
const url = URL.createObjectURL(blob);
|
|
168
|
+
const a = document.createElement('a');
|
|
169
|
+
a.href = url;
|
|
170
|
+
a.download = state.selectedReport.name || 'report.md';
|
|
171
|
+
document.body.appendChild(a);
|
|
172
|
+
a.click();
|
|
173
|
+
a.remove();
|
|
174
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
175
|
+
setPill('ok', 'downloaded');
|
|
176
|
+
setTimeout(() => setPill('ok', 'idle'), 800);
|
|
177
|
+
} catch {
|
|
178
|
+
setPill('err', 'download failed');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function setLast(p) {
|
|
183
|
+
state.lastReportPath = p;
|
|
184
|
+
const el = $('last');
|
|
185
|
+
if (el) el.textContent = p ? ('Last report: ' + p) : '';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function saveLocal() {
|
|
189
|
+
localStorage.setItem('freya.dir', $('dir').value);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function loadLocal() {
|
|
193
|
+
const def = (window.__FREYA_DEFAULT_DIR && window.__FREYA_DEFAULT_DIR !== '__FREYA_DEFAULT_DIR__')
|
|
194
|
+
? window.__FREYA_DEFAULT_DIR
|
|
195
|
+
: (localStorage.getItem('freya.dir') || './freya');
|
|
196
|
+
$('dir').value = def;
|
|
197
|
+
$('sidePath').textContent = def || './freya';
|
|
198
|
+
localStorage.setItem('freya.dir', $('dir').value || './freya');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function api(p, body) {
|
|
202
|
+
const res = await fetch(p, {
|
|
203
|
+
method: body ? 'POST' : 'GET',
|
|
204
|
+
headers: body ? { 'Content-Type': 'application/json' } : {},
|
|
205
|
+
body: body ? JSON.stringify(body) : undefined
|
|
206
|
+
});
|
|
207
|
+
const json = await res.json();
|
|
208
|
+
if (!res.ok) throw new Error(json.error || 'Request failed');
|
|
209
|
+
return json;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function dirOrDefault() {
|
|
213
|
+
const d = $('dir').value.trim();
|
|
214
|
+
return d || './freya';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function fmtWhen(ms) {
|
|
218
|
+
try {
|
|
219
|
+
const d = new Date(ms);
|
|
220
|
+
const yy = String(d.getFullYear());
|
|
221
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
222
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
223
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
224
|
+
const mi = String(d.getMinutes()).padStart(2, '0');
|
|
225
|
+
return yy + '-' + mm + '-' + dd + ' ' + hh + ':' + mi;
|
|
226
|
+
} catch {
|
|
227
|
+
return '';
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function selectReport(item) {
|
|
232
|
+
const rr = await api('/api/reports/read', { dir: dirOrDefault(), relPath: item.relPath });
|
|
233
|
+
state.selectedReport = item;
|
|
234
|
+
setLast(item.name);
|
|
235
|
+
setOut(rr.text || '');
|
|
236
|
+
renderReportsList();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function renderReportsList() {
|
|
240
|
+
const list = $('reportsList');
|
|
241
|
+
if (!list) return;
|
|
242
|
+
const q = ($('reportsFilter') ? $('reportsFilter').value : '').trim().toLowerCase();
|
|
243
|
+
const filtered = (state.reports || []).filter((it) => {
|
|
244
|
+
if (!q) return true;
|
|
245
|
+
return (it.name + ' ' + it.kind).toLowerCase().includes(q);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
list.innerHTML = '';
|
|
249
|
+
for (const item of filtered) {
|
|
250
|
+
const btn = document.createElement('button');
|
|
251
|
+
btn.className = 'rep' + (state.selectedReport && state.selectedReport.relPath === item.relPath ? ' repActive' : '');
|
|
252
|
+
btn.type = 'button';
|
|
253
|
+
const meta = fmtWhen(item.mtimeMs);
|
|
254
|
+
btn.innerHTML =
|
|
255
|
+
'<div style="display:flex; gap:10px; align-items:center; justify-content:space-between">'
|
|
256
|
+
+ '<div style="min-width:0">'
|
|
257
|
+
+ '<div><span style="font-weight:800">' + escapeHtml(item.kind) + '</span> <span style="opacity:.7">—</span> ' + escapeHtml(item.name) + '</div>'
|
|
258
|
+
+ '<div style="opacity:.65; font-size:11px; margin-top:4px">' + escapeHtml(item.relPath) + '</div>'
|
|
259
|
+
+ '</div>'
|
|
260
|
+
+ '<div style="opacity:.7; font-size:11px; white-space:nowrap">' + escapeHtml(meta) + '</div>'
|
|
261
|
+
+ '</div>';
|
|
262
|
+
|
|
263
|
+
btn.onclick = async () => {
|
|
264
|
+
try {
|
|
265
|
+
await selectReport(item);
|
|
266
|
+
} catch (e) {
|
|
267
|
+
setPill('err', 'open failed');
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
list.appendChild(btn);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function refreshReports() {
|
|
275
|
+
try {
|
|
276
|
+
const r = await api('/api/reports/list', { dir: dirOrDefault() });
|
|
277
|
+
state.reports = (r.reports || []).slice(0, 50);
|
|
278
|
+
renderReportsList();
|
|
279
|
+
if (!state.selectedReport && state.reports && state.reports[0]) {
|
|
280
|
+
await selectReport(state.reports[0]);
|
|
281
|
+
}
|
|
282
|
+
} catch (e) {
|
|
283
|
+
// ignore
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function pickDir() {
|
|
288
|
+
try {
|
|
289
|
+
setPill('run', 'picker…');
|
|
290
|
+
const r = await api('/api/pick-dir', {});
|
|
291
|
+
if (r && r.dir) {
|
|
292
|
+
$('dir').value = r.dir;
|
|
293
|
+
$('sidePath').textContent = r.dir;
|
|
294
|
+
}
|
|
295
|
+
saveLocal();
|
|
296
|
+
setPill('ok', 'ready');
|
|
297
|
+
} catch (e) {
|
|
298
|
+
setPill('err', 'picker failed');
|
|
299
|
+
setOut(String(e && e.message ? e.message : e));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function doInit() {
|
|
304
|
+
try {
|
|
305
|
+
saveLocal();
|
|
306
|
+
$('sidePath').textContent = dirOrDefault();
|
|
307
|
+
setPill('run', 'init…');
|
|
308
|
+
setOut('');
|
|
309
|
+
const r = await api('/api/init', { dir: dirOrDefault() });
|
|
310
|
+
setOut(r.output);
|
|
311
|
+
setLast(null);
|
|
312
|
+
await refreshReports();
|
|
313
|
+
setPill('ok', 'init ok');
|
|
314
|
+
} catch (e) {
|
|
315
|
+
setPill('err', 'init failed');
|
|
316
|
+
setOut(String(e && e.message ? e.message : e));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function doUpdate() {
|
|
321
|
+
try {
|
|
322
|
+
saveLocal();
|
|
323
|
+
$('sidePath').textContent = dirOrDefault();
|
|
324
|
+
setPill('run', 'update…');
|
|
325
|
+
setOut('');
|
|
326
|
+
const r = await api('/api/update', { dir: dirOrDefault() });
|
|
327
|
+
setOut(r.output);
|
|
328
|
+
setLast(null);
|
|
329
|
+
await refreshReports();
|
|
330
|
+
setPill('ok', 'update ok');
|
|
331
|
+
} catch (e) {
|
|
332
|
+
setPill('err', 'update failed');
|
|
333
|
+
setOut(String(e && e.message ? e.message : e));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function doHealth() {
|
|
338
|
+
try {
|
|
339
|
+
saveLocal();
|
|
340
|
+
$('sidePath').textContent = dirOrDefault();
|
|
341
|
+
setPill('run', 'health…');
|
|
342
|
+
setOut('');
|
|
343
|
+
const r = await api('/api/health', { dir: dirOrDefault() });
|
|
344
|
+
setOut(r.output);
|
|
345
|
+
setLast(null);
|
|
346
|
+
setPill('ok', 'health ok');
|
|
347
|
+
} catch (e) {
|
|
348
|
+
setPill('err', 'health failed');
|
|
349
|
+
setOut(String(e && e.message ? e.message : e));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function doMigrate() {
|
|
354
|
+
try {
|
|
355
|
+
saveLocal();
|
|
356
|
+
$('sidePath').textContent = dirOrDefault();
|
|
357
|
+
setPill('run', 'migrate…');
|
|
358
|
+
setOut('');
|
|
359
|
+
const r = await api('/api/migrate', { dir: dirOrDefault() });
|
|
360
|
+
setOut(r.output);
|
|
361
|
+
setLast(null);
|
|
362
|
+
setPill('ok', 'migrate ok');
|
|
363
|
+
} catch (e) {
|
|
364
|
+
setPill('err', 'migrate failed');
|
|
365
|
+
setOut(String(e && e.message ? e.message : e));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function runReport(name) {
|
|
370
|
+
try {
|
|
371
|
+
saveLocal();
|
|
372
|
+
$('sidePath').textContent = dirOrDefault();
|
|
373
|
+
setPill('run', name + '…');
|
|
374
|
+
setOut('');
|
|
375
|
+
const r = await api('/api/report', { dir: dirOrDefault(), script: name });
|
|
376
|
+
setOut(r.output);
|
|
377
|
+
setLast(r.reportPath || null);
|
|
378
|
+
if (r.reportText) state.lastText = r.reportText;
|
|
379
|
+
await refreshReports();
|
|
380
|
+
setPill('ok', name + ' ok');
|
|
381
|
+
} catch (e) {
|
|
382
|
+
setPill('err', name + ' failed');
|
|
383
|
+
setOut(String(e && e.message ? e.message : e));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function saveSettings() {
|
|
388
|
+
try {
|
|
389
|
+
saveLocal();
|
|
390
|
+
setPill('run', 'saving…');
|
|
391
|
+
await api('/api/settings/save', {
|
|
392
|
+
dir: dirOrDefault(),
|
|
393
|
+
settings: {
|
|
394
|
+
discordWebhookUrl: $('discord').value.trim(),
|
|
395
|
+
teamsWebhookUrl: $('teams').value.trim()
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
setPill('ok', 'saved');
|
|
399
|
+
setTimeout(() => setPill('ok', 'idle'), 800);
|
|
400
|
+
} catch (e) {
|
|
401
|
+
setPill('err', 'save failed');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function publish(target) {
|
|
406
|
+
try {
|
|
407
|
+
saveLocal();
|
|
408
|
+
if (!state.lastText) throw new Error('Gere um relatório primeiro.');
|
|
409
|
+
const webhookUrl = target === 'discord' ? $('discord').value.trim() : $('teams').value.trim();
|
|
410
|
+
if (!webhookUrl) throw new Error('Configure o webhook antes.');
|
|
411
|
+
setPill('run', 'publish…');
|
|
412
|
+
await api('/api/publish', { webhookUrl, text: state.lastText, mode: 'chunks' });
|
|
413
|
+
setPill('ok', 'published');
|
|
414
|
+
} catch (e) {
|
|
415
|
+
setPill('err', 'publish failed');
|
|
416
|
+
setOut(String(e && e.message ? e.message : e));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function saveInbox() {
|
|
421
|
+
try {
|
|
422
|
+
const ta = $('inboxText');
|
|
423
|
+
if (!ta) return;
|
|
424
|
+
const text = (ta.value || '').trim();
|
|
425
|
+
if (!text) {
|
|
426
|
+
setPill('err', 'empty');
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
setPill('run', 'saving…');
|
|
430
|
+
await api('/api/inbox/add', { dir: dirOrDefault(), text });
|
|
431
|
+
ta.value = '';
|
|
432
|
+
setPill('ok', 'saved');
|
|
433
|
+
setTimeout(() => setPill('ok', 'idle'), 800);
|
|
434
|
+
} catch (e) {
|
|
435
|
+
setPill('err', 'save failed');
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function saveAndPlan() {
|
|
440
|
+
try {
|
|
441
|
+
const ta = $('inboxText');
|
|
442
|
+
if (!ta) return;
|
|
443
|
+
const text = (ta.value || '').trim();
|
|
444
|
+
if (!text) {
|
|
445
|
+
setPill('err', 'empty');
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
setPill('run', 'saving…');
|
|
450
|
+
await api('/api/inbox/add', { dir: dirOrDefault(), text });
|
|
451
|
+
|
|
452
|
+
setPill('run', 'planning…');
|
|
453
|
+
const r = await api('/api/agents/plan', { dir: dirOrDefault(), text });
|
|
454
|
+
|
|
455
|
+
state.lastPlan = r.plan || '';
|
|
456
|
+
|
|
457
|
+
// Show plan output in Preview panel
|
|
458
|
+
setOut('## Agent Plan (draft)\n\n' + (r.plan || ''));
|
|
459
|
+
ta.value = '';
|
|
460
|
+
|
|
461
|
+
setPill('ok', 'planned');
|
|
462
|
+
setTimeout(() => setPill('ok', 'idle'), 800);
|
|
463
|
+
} catch (e) {
|
|
464
|
+
setPill('err', 'plan failed');
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function runSuggestedReports() {
|
|
469
|
+
try {
|
|
470
|
+
const suggested = state.lastApplied && Array.isArray(state.lastApplied.reportsSuggested)
|
|
471
|
+
? state.lastApplied.reportsSuggested
|
|
472
|
+
: [];
|
|
473
|
+
if (!suggested.length) {
|
|
474
|
+
setPill('err', 'no suggestions');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Dedup + allowlist
|
|
479
|
+
const allow = new Set(['daily', 'status', 'sm-weekly', 'blockers']);
|
|
480
|
+
const uniq = Array.from(new Set(suggested.map((s) => String(s).trim()))).filter((s) => allow.has(s));
|
|
481
|
+
if (!uniq.length) {
|
|
482
|
+
setPill('err', 'no valid');
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
setPill('run', 'running…');
|
|
487
|
+
let out = '## Ran suggested reports\n\n';
|
|
488
|
+
for (const name of uniq) {
|
|
489
|
+
const r = await api('/api/report', { dir: dirOrDefault(), script: name === 'status' ? 'status' : name });
|
|
490
|
+
out += `### ${name}\n` + (r.reportPath ? `- file: ${r.reportPath}\n` : '') + '\n';
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
setOut(out);
|
|
494
|
+
await refreshReports();
|
|
495
|
+
setPill('ok', 'done');
|
|
496
|
+
setTimeout(() => setPill('ok', 'idle'), 800);
|
|
497
|
+
} catch (e) {
|
|
498
|
+
setPill('err', 'run failed');
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function applyPlan() {
|
|
503
|
+
try {
|
|
504
|
+
if (!state.lastPlan) {
|
|
505
|
+
setPill('err', 'no plan');
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
setPill('run', 'applying…');
|
|
509
|
+
const r = await api('/api/agents/apply', { dir: dirOrDefault(), plan: state.lastPlan });
|
|
510
|
+
state.lastApplied = r.applied || null;
|
|
511
|
+
const summary = r.applied || {};
|
|
512
|
+
|
|
513
|
+
let msg = '## Apply result\n\n' + JSON.stringify(summary, null, 2);
|
|
514
|
+
if (summary && Array.isArray(summary.reportsSuggested) && summary.reportsSuggested.length) {
|
|
515
|
+
msg += '\n\n## Suggested reports\n- ' + summary.reportsSuggested.join('\n- ');
|
|
516
|
+
msg += '\n\nUse: **Run suggested reports** (sidebar)';
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
setOut(msg);
|
|
520
|
+
setPill('ok', 'applied');
|
|
521
|
+
setTimeout(() => setPill('ok', 'idle'), 800);
|
|
522
|
+
} catch (e) {
|
|
523
|
+
setPill('err', 'apply failed');
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// init
|
|
528
|
+
applyTheme(localStorage.getItem('freya.theme') || 'light');
|
|
529
|
+
$('chipPort').textContent = location.host;
|
|
530
|
+
loadLocal();
|
|
531
|
+
|
|
532
|
+
// Load persisted settings from the workspace
|
|
533
|
+
(async () => {
|
|
534
|
+
try {
|
|
535
|
+
const r = await api('/api/defaults', { dir: dirOrDefault() });
|
|
536
|
+
if (r && r.workspaceDir) {
|
|
537
|
+
$('dir').value = r.workspaceDir;
|
|
538
|
+
$('sidePath').textContent = r.workspaceDir;
|
|
539
|
+
}
|
|
540
|
+
if (r && r.settings) {
|
|
541
|
+
$('discord').value = r.settings.discordWebhookUrl || '';
|
|
542
|
+
$('teams').value = r.settings.teamsWebhookUrl || '';
|
|
543
|
+
}
|
|
544
|
+
} catch (e) {
|
|
545
|
+
// ignore
|
|
546
|
+
}
|
|
547
|
+
refreshReports();
|
|
548
|
+
})();
|
|
549
|
+
|
|
550
|
+
setPill('ok', 'idle');
|
|
551
|
+
|
|
552
|
+
// Expose handlers for inline onclick
|
|
553
|
+
window.doInit = doInit;
|
|
554
|
+
window.doUpdate = doUpdate;
|
|
555
|
+
window.doHealth = doHealth;
|
|
556
|
+
window.doMigrate = doMigrate;
|
|
557
|
+
window.pickDir = pickDir;
|
|
558
|
+
window.runReport = runReport;
|
|
559
|
+
window.publish = publish;
|
|
560
|
+
window.saveSettings = saveSettings;
|
|
561
|
+
window.refreshReports = refreshReports;
|
|
562
|
+
window.renderReportsList = renderReportsList;
|
|
563
|
+
window.copyOut = copyOut;
|
|
564
|
+
window.copyPath = copyPath;
|
|
565
|
+
window.openSelected = openSelected;
|
|
566
|
+
window.downloadSelected = downloadSelected;
|
|
567
|
+
window.clearOut = clearOut;
|
|
568
|
+
window.toggleTheme = toggleTheme;
|
|
569
|
+
window.saveInbox = saveInbox;
|
|
570
|
+
window.saveAndPlan = saveAndPlan;
|
|
571
|
+
window.applyPlan = applyPlan;
|
|
572
|
+
window.runSuggestedReports = runSuggestedReports;
|
|
573
|
+
})();
|