@cccarv82/freya 1.0.5 → 1.0.7
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/index.js +4 -2
- package/cli/web.js +867 -118
- package/package.json +1 -1
package/cli/index.js
CHANGED
|
@@ -11,7 +11,7 @@ freya - F.R.E.Y.A. CLI
|
|
|
11
11
|
|
|
12
12
|
Usage:
|
|
13
13
|
freya init [dir] [--force] [--here|--in-place] [--force-data] [--force-logs]
|
|
14
|
-
freya web [--port <n>] [--dir <path>] [--no-open]
|
|
14
|
+
freya web [--port <n>] [--dir <path>] [--no-open] [--dev]
|
|
15
15
|
|
|
16
16
|
Defaults:
|
|
17
17
|
- If no [dir] is provided, creates ./freya
|
|
@@ -27,6 +27,7 @@ Examples:
|
|
|
27
27
|
|
|
28
28
|
freya web
|
|
29
29
|
freya web --dir ./freya --port 3872
|
|
30
|
+
freya web --dev # seeds demo data/logs for quick testing
|
|
30
31
|
`;
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -81,6 +82,7 @@ async function run(argv) {
|
|
|
81
82
|
const port = Number(kv['--port'] || 3872);
|
|
82
83
|
const dir = kv['--dir'] ? path.resolve(process.cwd(), kv['--dir']) : null;
|
|
83
84
|
const open = !flags.has('--no-open');
|
|
85
|
+
const dev = flags.has('--dev');
|
|
84
86
|
|
|
85
87
|
if (!Number.isFinite(port) || port <= 0) {
|
|
86
88
|
process.stderr.write('Invalid --port\n');
|
|
@@ -88,7 +90,7 @@ async function run(argv) {
|
|
|
88
90
|
return;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
await cmdWeb({ port, dir, open });
|
|
93
|
+
await cmdWeb({ port, dir, open, dev });
|
|
92
94
|
return;
|
|
93
95
|
}
|
|
94
96
|
|
package/cli/web.js
CHANGED
|
@@ -9,6 +9,10 @@ function guessNpmCmd() {
|
|
|
9
9
|
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
function guessNpxCmd() {
|
|
13
|
+
return process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
function guessOpenCmd() {
|
|
13
17
|
// Minimal cross-platform opener without extra deps
|
|
14
18
|
if (process.platform === 'win32') return { cmd: 'cmd', args: ['/c', 'start', ''] };
|
|
@@ -17,19 +21,33 @@ function guessOpenCmd() {
|
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
function exists(p) {
|
|
20
|
-
try {
|
|
24
|
+
try {
|
|
25
|
+
fs.accessSync(p);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
function newestFile(dir, prefix) {
|
|
24
33
|
if (!exists(dir)) return null;
|
|
25
|
-
const files = fs
|
|
34
|
+
const files = fs
|
|
35
|
+
.readdirSync(dir)
|
|
26
36
|
.filter((f) => f.startsWith(prefix) && f.endsWith('.md'))
|
|
27
37
|
.map((f) => ({ f, p: path.join(dir, f) }))
|
|
28
38
|
.filter((x) => {
|
|
29
|
-
try {
|
|
39
|
+
try {
|
|
40
|
+
return fs.statSync(x.p).isFile();
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
30
44
|
})
|
|
31
45
|
.sort((a, b) => {
|
|
32
|
-
try {
|
|
46
|
+
try {
|
|
47
|
+
return fs.statSync(b.p).mtimeMs - fs.statSync(a.p).mtimeMs;
|
|
48
|
+
} catch {
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
33
51
|
});
|
|
34
52
|
return files[0]?.p || null;
|
|
35
53
|
}
|
|
@@ -55,11 +73,29 @@ function readBody(req) {
|
|
|
55
73
|
|
|
56
74
|
function run(cmd, args, cwd) {
|
|
57
75
|
return new Promise((resolve) => {
|
|
58
|
-
|
|
76
|
+
let child;
|
|
77
|
+
try {
|
|
78
|
+
child = spawn(cmd, args, { cwd, shell: false, env: process.env });
|
|
79
|
+
} catch (e) {
|
|
80
|
+
return resolve({ code: 1, stdout: '', stderr: e.message || String(e) });
|
|
81
|
+
}
|
|
82
|
+
|
|
59
83
|
let stdout = '';
|
|
60
84
|
let stderr = '';
|
|
61
|
-
|
|
62
|
-
child.
|
|
85
|
+
|
|
86
|
+
child.stdout && child.stdout.on('data', (d) => {
|
|
87
|
+
stdout += d.toString();
|
|
88
|
+
});
|
|
89
|
+
child.stderr && child.stderr.on('data', (d) => {
|
|
90
|
+
stderr += d.toString();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Prevent unhandled error event (e.g., ENOENT on Windows when cmd not found)
|
|
94
|
+
child.on('error', (e) => {
|
|
95
|
+
stderr += `\n${e.message || String(e)}`;
|
|
96
|
+
resolve({ code: 1, stdout, stderr });
|
|
97
|
+
});
|
|
98
|
+
|
|
63
99
|
child.on('close', (code) => resolve({ code: code ?? 0, stdout, stderr }));
|
|
64
100
|
});
|
|
65
101
|
}
|
|
@@ -73,109 +109,652 @@ function openBrowser(url) {
|
|
|
73
109
|
}
|
|
74
110
|
}
|
|
75
111
|
|
|
112
|
+
async function pickDirectoryNative() {
|
|
113
|
+
if (process.platform === 'win32') {
|
|
114
|
+
// PowerShell FolderBrowserDialog
|
|
115
|
+
const ps = [
|
|
116
|
+
'-NoProfile',
|
|
117
|
+
'-Command',
|
|
118
|
+
[
|
|
119
|
+
'Add-Type -AssemblyName System.Windows.Forms;',
|
|
120
|
+
'$f = New-Object System.Windows.Forms.FolderBrowserDialog;',
|
|
121
|
+
"$f.Description = 'Select your FREYA workspace folder';",
|
|
122
|
+
'$f.ShowNewFolderButton = $true;',
|
|
123
|
+
'if ($f.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { Write-Output $f.SelectedPath }'
|
|
124
|
+
].join(' ')
|
|
125
|
+
];
|
|
126
|
+
const r = await run('powershell', ps, process.cwd());
|
|
127
|
+
if (r.code !== 0) throw new Error((r.stderr || r.stdout || '').trim() || 'Failed to open folder picker');
|
|
128
|
+
const out = (r.stdout || '').trim();
|
|
129
|
+
return out || null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (process.platform === 'darwin') {
|
|
133
|
+
const r = await run('osascript', ['-e', 'POSIX path of (choose folder with prompt "Select your FREYA workspace folder")'], process.cwd());
|
|
134
|
+
if (r.code !== 0) throw new Error((r.stderr || r.stdout || '').trim() || 'Failed to open folder picker');
|
|
135
|
+
const out = (r.stdout || '').trim();
|
|
136
|
+
return out || null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Linux: prefer zenity, then kdialog
|
|
140
|
+
if (exists('/usr/bin/zenity') || exists('/bin/zenity') || exists('/usr/local/bin/zenity')) {
|
|
141
|
+
const r = await run('zenity', ['--file-selection', '--directory', '--title=Select your FREYA workspace folder'], process.cwd());
|
|
142
|
+
if (r.code !== 0) return null;
|
|
143
|
+
return (r.stdout || '').trim() || null;
|
|
144
|
+
}
|
|
145
|
+
if (exists('/usr/bin/kdialog') || exists('/bin/kdialog') || exists('/usr/local/bin/kdialog')) {
|
|
146
|
+
const r = await run('kdialog', ['--getexistingdirectory', '.', 'Select your FREYA workspace folder'], process.cwd());
|
|
147
|
+
if (r.code !== 0) return null;
|
|
148
|
+
return (r.stdout || '').trim() || null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
76
154
|
function html() {
|
|
155
|
+
// Aesthetic: “clean workstation” — light-first UI inspired by modern productivity apps.
|
|
77
156
|
return `<!doctype html>
|
|
78
157
|
<html>
|
|
79
158
|
<head>
|
|
80
159
|
<meta charset="utf-8" />
|
|
81
160
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
82
|
-
<title>FREYA</title>
|
|
161
|
+
<title>FREYA Web</title>
|
|
83
162
|
<style>
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
163
|
+
/*
|
|
164
|
+
Design goals:
|
|
165
|
+
- Light theme by default (inspired by your reference screenshots)
|
|
166
|
+
- Dark mode toggle
|
|
167
|
+
- App-like layout: sidebar + main surface
|
|
168
|
+
- Clear onboarding and affordances
|
|
169
|
+
*/
|
|
170
|
+
|
|
171
|
+
:root {
|
|
172
|
+
--radius: 14px;
|
|
173
|
+
--shadow: 0 18px 55px rgba(16, 24, 40, .10);
|
|
174
|
+
--shadow2: 0 10px 20px rgba(16, 24, 40, .08);
|
|
175
|
+
--ring: 0 0 0 4px rgba(59, 130, 246, .18);
|
|
176
|
+
|
|
177
|
+
/* Light */
|
|
178
|
+
--bg: #f6f7fb;
|
|
179
|
+
--paper: #ffffff;
|
|
180
|
+
--paper2: #fbfbfd;
|
|
181
|
+
--line: rgba(16, 24, 40, .10);
|
|
182
|
+
--line2: rgba(16, 24, 40, .14);
|
|
183
|
+
--text: #0f172a;
|
|
184
|
+
--muted: rgba(15, 23, 42, .68);
|
|
185
|
+
--faint: rgba(15, 23, 42, .50);
|
|
186
|
+
|
|
187
|
+
--primary: #2563eb;
|
|
188
|
+
--primary2: #0ea5e9;
|
|
189
|
+
--accent: #f97316;
|
|
190
|
+
--ok: #16a34a;
|
|
191
|
+
--warn: #f59e0b;
|
|
192
|
+
--danger: #e11d48;
|
|
193
|
+
|
|
194
|
+
--chip: rgba(37, 99, 235, .08);
|
|
195
|
+
--chip2: rgba(249, 115, 22, .10);
|
|
196
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
197
|
+
--sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Arial;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
[data-theme="dark"] {
|
|
201
|
+
--bg: #0b1020;
|
|
202
|
+
--paper: rgba(255,255,255,.06);
|
|
203
|
+
--paper2: rgba(255,255,255,.04);
|
|
204
|
+
--line: rgba(255,255,255,.12);
|
|
205
|
+
--line2: rgba(255,255,255,.18);
|
|
206
|
+
--text: #e9f0ff;
|
|
207
|
+
--muted: rgba(233,240,255,.72);
|
|
208
|
+
--faint: rgba(233,240,255,.54);
|
|
209
|
+
|
|
210
|
+
--primary: #60a5fa;
|
|
211
|
+
--primary2: #22c55e;
|
|
212
|
+
--accent: #fb923c;
|
|
213
|
+
--chip: rgba(96, 165, 250, .14);
|
|
214
|
+
--chip2: rgba(251, 146, 60, .14);
|
|
215
|
+
|
|
216
|
+
--shadow: 0 30px 70px rgba(0,0,0,.55);
|
|
217
|
+
--shadow2: 0 18px 40px rgba(0,0,0,.35);
|
|
218
|
+
--ring: 0 0 0 4px rgba(96, 165, 250, .22);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
* { box-sizing: border-box; }
|
|
222
|
+
html, body { height: 100%; }
|
|
223
|
+
body {
|
|
224
|
+
margin: 0;
|
|
225
|
+
background:
|
|
226
|
+
radial-gradient(1200px 800px at 20% -10%, rgba(37,99,235,.12), transparent 55%),
|
|
227
|
+
radial-gradient(900px 600px at 92% 10%, rgba(249,115,22,.12), transparent 55%),
|
|
228
|
+
radial-gradient(1100px 700px at 70% 105%, rgba(14,165,233,.10), transparent 55%),
|
|
229
|
+
var(--bg);
|
|
230
|
+
color: var(--text);
|
|
231
|
+
font-family: var(--sans);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* subtle grain */
|
|
235
|
+
body:before {
|
|
236
|
+
content: "";
|
|
237
|
+
position: fixed;
|
|
238
|
+
inset: 0;
|
|
239
|
+
pointer-events: none;
|
|
240
|
+
background-image:
|
|
241
|
+
radial-gradient(circle at 15% 20%, rgba(255,255,255,.38), transparent 32%),
|
|
242
|
+
radial-gradient(circle at 80% 10%, rgba(255,255,255,.26), transparent 38%),
|
|
243
|
+
linear-gradient(transparent 0, transparent 3px, rgba(0,0,0,.02) 4px);
|
|
244
|
+
background-size: 900px 900px, 900px 900px, 100% 7px;
|
|
245
|
+
opacity: .08;
|
|
246
|
+
mix-blend-mode: overlay;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.app {
|
|
250
|
+
max-width: 1260px;
|
|
251
|
+
margin: 18px auto;
|
|
252
|
+
padding: 0 18px;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.frame {
|
|
256
|
+
display: grid;
|
|
257
|
+
grid-template-columns: 280px 1fr;
|
|
258
|
+
gap: 14px;
|
|
259
|
+
min-height: calc(100vh - 36px);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
@media (max-width: 980px) {
|
|
263
|
+
.frame { grid-template-columns: 1fr; }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.sidebar {
|
|
267
|
+
background: var(--paper);
|
|
268
|
+
border: 1px solid var(--line);
|
|
269
|
+
border-radius: var(--radius);
|
|
270
|
+
box-shadow: var(--shadow2);
|
|
271
|
+
padding: 14px;
|
|
272
|
+
position: sticky;
|
|
273
|
+
top: 18px;
|
|
274
|
+
height: fit-content;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.main {
|
|
278
|
+
background: var(--paper);
|
|
279
|
+
border: 1px solid var(--line);
|
|
280
|
+
border-radius: var(--radius);
|
|
281
|
+
box-shadow: var(--shadow);
|
|
282
|
+
overflow: hidden;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.topbar {
|
|
286
|
+
display: flex;
|
|
287
|
+
align-items: center;
|
|
288
|
+
justify-content: space-between;
|
|
289
|
+
padding: 14px 16px;
|
|
290
|
+
border-bottom: 1px solid var(--line);
|
|
291
|
+
background: linear-gradient(180deg, var(--paper2), var(--paper));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.brand {
|
|
295
|
+
display: flex;
|
|
296
|
+
align-items: center;
|
|
297
|
+
gap: 10px;
|
|
298
|
+
font-weight: 800;
|
|
299
|
+
letter-spacing: .08em;
|
|
300
|
+
text-transform: uppercase;
|
|
301
|
+
font-size: 12px;
|
|
302
|
+
color: var(--muted);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.spark {
|
|
306
|
+
width: 10px;
|
|
307
|
+
height: 10px;
|
|
308
|
+
border-radius: 4px;
|
|
309
|
+
background: linear-gradient(135deg, var(--accent), var(--primary));
|
|
310
|
+
box-shadow: 0 0 0 6px rgba(249,115,22,.12);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.actions {
|
|
314
|
+
display: flex;
|
|
315
|
+
align-items: center;
|
|
316
|
+
gap: 10px;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.chip {
|
|
320
|
+
font-family: var(--mono);
|
|
321
|
+
font-size: 12px;
|
|
322
|
+
padding: 7px 10px;
|
|
323
|
+
border-radius: 999px;
|
|
324
|
+
border: 1px solid var(--line);
|
|
325
|
+
background: rgba(255,255,255,.55);
|
|
326
|
+
color: var(--faint);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
[data-theme="dark"] .chip { background: rgba(0,0,0,.20); }
|
|
330
|
+
|
|
331
|
+
.toggle {
|
|
332
|
+
border: 1px solid var(--line);
|
|
333
|
+
border-radius: 999px;
|
|
334
|
+
background: var(--paper2);
|
|
335
|
+
padding: 7px 10px;
|
|
336
|
+
cursor: pointer;
|
|
337
|
+
color: var(--muted);
|
|
338
|
+
font-weight: 700;
|
|
339
|
+
font-size: 12px;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.section {
|
|
343
|
+
padding: 16px;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
h1 {
|
|
347
|
+
margin: 0;
|
|
348
|
+
font-size: 22px;
|
|
349
|
+
letter-spacing: -.02em;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.subtitle {
|
|
353
|
+
margin-top: 6px;
|
|
354
|
+
color: var(--muted);
|
|
355
|
+
font-size: 13px;
|
|
356
|
+
line-height: 1.4;
|
|
357
|
+
max-width: 860px;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.cards {
|
|
361
|
+
display: grid;
|
|
362
|
+
grid-template-columns: repeat(4, 1fr);
|
|
363
|
+
gap: 12px;
|
|
364
|
+
margin-top: 14px;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
@media (max-width: 1100px) { .cards { grid-template-columns: repeat(2, 1fr);} }
|
|
368
|
+
@media (max-width: 620px) { .cards { grid-template-columns: 1fr;} }
|
|
369
|
+
|
|
370
|
+
.card {
|
|
371
|
+
border: 1px solid var(--line);
|
|
372
|
+
background: var(--paper2);
|
|
373
|
+
border-radius: 14px;
|
|
374
|
+
padding: 12px;
|
|
375
|
+
display: grid;
|
|
376
|
+
gap: 6px;
|
|
377
|
+
cursor: pointer;
|
|
378
|
+
transition: transform .10s ease, border-color .16s ease, box-shadow .16s ease;
|
|
379
|
+
box-shadow: 0 1px 0 rgba(16,24,40,.04);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.card:hover {
|
|
383
|
+
transform: translateY(-1px);
|
|
384
|
+
border-color: var(--line2);
|
|
385
|
+
box-shadow: 0 10px 22px rgba(16,24,40,.10);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.icon {
|
|
389
|
+
width: 34px;
|
|
390
|
+
height: 34px;
|
|
391
|
+
border-radius: 12px;
|
|
392
|
+
display: grid;
|
|
393
|
+
place-items: center;
|
|
394
|
+
background: var(--chip);
|
|
395
|
+
border: 1px solid var(--line);
|
|
396
|
+
color: var(--primary);
|
|
397
|
+
font-weight: 900;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.icon.orange { background: var(--chip2); color: var(--accent); }
|
|
401
|
+
|
|
402
|
+
.title {
|
|
403
|
+
font-weight: 800;
|
|
404
|
+
font-size: 13px;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.desc {
|
|
408
|
+
color: var(--muted);
|
|
409
|
+
font-size: 12px;
|
|
410
|
+
line-height: 1.35;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.grid2 {
|
|
414
|
+
display: grid;
|
|
415
|
+
grid-template-columns: 1fr 1fr;
|
|
416
|
+
gap: 14px;
|
|
417
|
+
margin-top: 14px;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
@media (max-width: 980px) { .grid2 { grid-template-columns: 1fr; } }
|
|
421
|
+
|
|
422
|
+
.panel {
|
|
423
|
+
border: 1px solid var(--line);
|
|
424
|
+
background: var(--paper);
|
|
425
|
+
border-radius: 14px;
|
|
426
|
+
overflow: hidden;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.panelHead {
|
|
430
|
+
display: flex;
|
|
431
|
+
align-items: center;
|
|
432
|
+
justify-content: space-between;
|
|
433
|
+
padding: 12px 12px;
|
|
434
|
+
border-bottom: 1px solid var(--line);
|
|
435
|
+
background: linear-gradient(180deg, var(--paper2), var(--paper));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.panelHead b { font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
|
|
439
|
+
|
|
440
|
+
.panelBody { padding: 12px; }
|
|
441
|
+
|
|
442
|
+
label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
|
|
443
|
+
|
|
444
|
+
input {
|
|
445
|
+
width: 100%;
|
|
446
|
+
padding: 11px 12px;
|
|
447
|
+
border-radius: 12px;
|
|
448
|
+
border: 1px solid var(--line);
|
|
449
|
+
background: rgba(255,255,255,.72);
|
|
450
|
+
color: var(--text);
|
|
451
|
+
outline: none;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
[data-theme="dark"] input { background: rgba(0,0,0,.16); }
|
|
455
|
+
|
|
456
|
+
input:focus { box-shadow: var(--ring); border-color: rgba(37,99,235,.35); }
|
|
457
|
+
|
|
458
|
+
.row {
|
|
459
|
+
display: grid;
|
|
460
|
+
grid-template-columns: 1fr auto;
|
|
461
|
+
gap: 10px;
|
|
462
|
+
align-items: center;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.btn {
|
|
466
|
+
border: 1px solid var(--line);
|
|
467
|
+
border-radius: 12px;
|
|
468
|
+
background: var(--paper2);
|
|
469
|
+
color: var(--text);
|
|
470
|
+
padding: 10px 12px;
|
|
471
|
+
cursor: pointer;
|
|
472
|
+
font-weight: 800;
|
|
473
|
+
font-size: 12px;
|
|
474
|
+
transition: transform .10s ease, border-color .16s ease, box-shadow .16s ease;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.btn:hover { transform: translateY(-1px); border-color: var(--line2); box-shadow: 0 10px 22px rgba(16,24,40,.10); }
|
|
478
|
+
.btn:active { transform: translateY(0); }
|
|
479
|
+
|
|
480
|
+
.btn.primary {
|
|
481
|
+
background: linear-gradient(135deg, rgba(37,99,235,.14), rgba(14,165,233,.12));
|
|
482
|
+
border-color: rgba(37,99,235,.22);
|
|
483
|
+
color: var(--text);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.btn.orange {
|
|
487
|
+
background: linear-gradient(135deg, rgba(249,115,22,.16), rgba(37,99,235,.08));
|
|
488
|
+
border-color: rgba(249,115,22,.24);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.btn.danger {
|
|
492
|
+
background: rgba(225,29,72,.10);
|
|
493
|
+
border-color: rgba(225,29,72,.28);
|
|
494
|
+
color: var(--text);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.btn.small { padding: 9px 10px; font-weight: 800; }
|
|
498
|
+
|
|
499
|
+
.stack { display: flex; flex-wrap: wrap; gap: 10px; }
|
|
500
|
+
|
|
501
|
+
.help {
|
|
502
|
+
margin-top: 8px;
|
|
503
|
+
color: var(--faint);
|
|
504
|
+
font-size: 12px;
|
|
505
|
+
line-height: 1.35;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.log {
|
|
509
|
+
border: 1px solid var(--line);
|
|
510
|
+
background: rgba(255,255,255,.65);
|
|
511
|
+
border-radius: 14px;
|
|
512
|
+
padding: 12px;
|
|
513
|
+
font-family: var(--mono);
|
|
514
|
+
font-size: 12px;
|
|
515
|
+
line-height: 1.35;
|
|
516
|
+
white-space: pre-wrap;
|
|
517
|
+
max-height: 420px;
|
|
518
|
+
overflow: auto;
|
|
519
|
+
color: rgba(15,23,42,.92);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
[data-theme="dark"] .log { background: rgba(0,0,0,.20); color: rgba(233,240,255,.84); }
|
|
523
|
+
|
|
524
|
+
.statusRow { display:flex; align-items:center; justify-content: space-between; gap: 10px; }
|
|
525
|
+
|
|
526
|
+
.pill {
|
|
527
|
+
display: inline-flex;
|
|
528
|
+
align-items: center;
|
|
529
|
+
gap: 8px;
|
|
530
|
+
padding: 7px 10px;
|
|
531
|
+
border-radius: 999px;
|
|
532
|
+
border: 1px solid var(--line);
|
|
533
|
+
background: rgba(255,255,255,.55);
|
|
534
|
+
font-size: 12px;
|
|
535
|
+
color: var(--muted);
|
|
536
|
+
font-family: var(--mono);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
[data-theme="dark"] .pill { background: rgba(0,0,0,.18); }
|
|
540
|
+
|
|
541
|
+
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--warn); box-shadow: 0 0 0 5px rgba(245,158,11,.15); }
|
|
542
|
+
.dot.ok { background: var(--ok); box-shadow: 0 0 0 5px rgba(22,163,74,.12); }
|
|
543
|
+
.dot.err { background: var(--danger); box-shadow: 0 0 0 5px rgba(225,29,72,.14); }
|
|
544
|
+
|
|
545
|
+
.small {
|
|
546
|
+
font-size: 12px;
|
|
547
|
+
color: var(--faint);
|
|
548
|
+
font-family: var(--mono);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.sidebar h3 {
|
|
552
|
+
margin: 0;
|
|
553
|
+
font-size: 12px;
|
|
554
|
+
letter-spacing: .10em;
|
|
555
|
+
text-transform: uppercase;
|
|
556
|
+
color: var(--muted);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
.sideBlock { margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--line); }
|
|
560
|
+
|
|
561
|
+
.sidePath {
|
|
562
|
+
margin-top: 8px;
|
|
563
|
+
border: 1px solid var(--line);
|
|
564
|
+
background: var(--paper2);
|
|
565
|
+
border-radius: 12px;
|
|
566
|
+
padding: 10px;
|
|
567
|
+
font-family: var(--mono);
|
|
568
|
+
font-size: 12px;
|
|
569
|
+
color: var(--muted);
|
|
570
|
+
word-break: break-word;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.sideBtn { width: 100%; margin-top: 8px; }
|
|
574
|
+
|
|
575
|
+
.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); }
|
|
576
|
+
[data-theme="dark"] .k { background: rgba(0,0,0,.18); }
|
|
577
|
+
|
|
101
578
|
</style>
|
|
102
579
|
</head>
|
|
103
580
|
<body>
|
|
104
|
-
<
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
<div class="row">
|
|
112
|
-
<div>
|
|
113
|
-
<label>Workspace dir</label>
|
|
114
|
-
<input id="dir" placeholder="/path/to/freya (or ./freya)" />
|
|
115
|
-
<div class="small">Dica: a workspace é a pasta que contém <code>data/</code>, <code>logs/</code>, <code>scripts/</code>.</div>
|
|
116
|
-
</div>
|
|
117
|
-
<div class="btns">
|
|
118
|
-
<button onclick="doInit()">Init (preserva data/logs)</button>
|
|
119
|
-
<button class="secondary" onclick="doUpdate()">Update (init --here)</button>
|
|
120
|
-
<button class="secondary" onclick="doHealth()">Health</button>
|
|
121
|
-
<button class="secondary" onclick="doMigrate()">Migrate</button>
|
|
122
|
-
</div>
|
|
581
|
+
<div class="app">
|
|
582
|
+
<div class="frame">
|
|
583
|
+
|
|
584
|
+
<aside class="sidebar">
|
|
585
|
+
<div style="display:flex; align-items:center; justify-content: space-between; gap:10px;">
|
|
586
|
+
<h3>FREYA</h3>
|
|
587
|
+
<span class="pill"><span class="dot" id="dot"></span><span id="pill">idle</span></span>
|
|
123
588
|
</div>
|
|
124
589
|
|
|
125
|
-
<
|
|
590
|
+
<div class="sideBlock">
|
|
591
|
+
<h3>Workspace</h3>
|
|
592
|
+
<div class="sidePath" id="sidePath">./freya</div>
|
|
593
|
+
<button class="btn sideBtn" onclick="pickDir()">Select workspace…</button>
|
|
594
|
+
<button class="btn primary sideBtn" onclick="doInit()">Init workspace</button>
|
|
595
|
+
<button class="btn sideBtn" onclick="doUpdate()">Update (preserve data/logs)</button>
|
|
596
|
+
<button class="btn sideBtn" onclick="doHealth()">Health</button>
|
|
597
|
+
<button class="btn sideBtn" onclick="doMigrate()">Migrate</button>
|
|
598
|
+
<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>
|
|
599
|
+
</div>
|
|
126
600
|
|
|
127
|
-
<div class="
|
|
128
|
-
<
|
|
129
|
-
<button onclick="
|
|
130
|
-
<button onclick="
|
|
131
|
-
<
|
|
601
|
+
<div class="sideBlock">
|
|
602
|
+
<h3>Publish</h3>
|
|
603
|
+
<button class="btn sideBtn" onclick="publish('discord')">Publish → Discord</button>
|
|
604
|
+
<button class="btn sideBtn" onclick="publish('teams')">Publish → Teams</button>
|
|
605
|
+
<div class="help">Configure os webhooks no painel principal. O publish envia o último relatório gerado.</div>
|
|
132
606
|
</div>
|
|
133
607
|
|
|
134
|
-
<div
|
|
135
|
-
<
|
|
136
|
-
<
|
|
137
|
-
<div class="
|
|
608
|
+
<div class="sideBlock">
|
|
609
|
+
<h3>Atalhos</h3>
|
|
610
|
+
<div class="help"><span class="k">--dev</span> cria dados de exemplo para testar rápido.</div>
|
|
611
|
+
<div class="help"><span class="k">--port</span> muda a porta (default 3872).</div>
|
|
138
612
|
</div>
|
|
139
|
-
</
|
|
613
|
+
</aside>
|
|
140
614
|
|
|
141
|
-
<
|
|
142
|
-
<div class="
|
|
143
|
-
<div>
|
|
144
|
-
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
<div>
|
|
148
|
-
<label>Teams Webhook URL (optional)</label>
|
|
149
|
-
<input id="teams" placeholder="https://..." />
|
|
615
|
+
<main class="main">
|
|
616
|
+
<div class="topbar">
|
|
617
|
+
<div class="brand"><span class="spark"></span> Local-first status assistant</div>
|
|
618
|
+
<div class="actions">
|
|
619
|
+
<span class="chip" id="chipPort">127.0.0.1:3872</span>
|
|
620
|
+
<button class="toggle" id="themeToggle" onclick="toggleTheme()">Theme</button>
|
|
150
621
|
</div>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
622
|
+
</div>
|
|
623
|
+
|
|
624
|
+
<div class="section">
|
|
625
|
+
<h1>Morning, how can I help?</h1>
|
|
626
|
+
<div class="subtitle">Selecione uma workspace e gere relatórios (Executive / SM / Blockers / Daily). Você pode publicar no Discord/Teams com 1 clique.</div>
|
|
627
|
+
|
|
628
|
+
<div class="cards">
|
|
629
|
+
<div class="card" onclick="runReport('status')">
|
|
630
|
+
<div class="icon">E</div>
|
|
631
|
+
<div class="title">Executive report</div>
|
|
632
|
+
<div class="desc">Status pronto para stakeholders (entregas, projetos, blockers).</div>
|
|
633
|
+
</div>
|
|
634
|
+
<div class="card" onclick="runReport('sm-weekly')">
|
|
635
|
+
<div class="icon">S</div>
|
|
636
|
+
<div class="title">SM weekly</div>
|
|
637
|
+
<div class="desc">Resumo, wins, riscos e foco da próxima semana.</div>
|
|
638
|
+
</div>
|
|
639
|
+
<div class="card" onclick="runReport('blockers')">
|
|
640
|
+
<div class="icon orange">B</div>
|
|
641
|
+
<div class="title">Blockers</div>
|
|
642
|
+
<div class="desc">Lista priorizada por severidade + idade (pra destravar rápido).</div>
|
|
643
|
+
</div>
|
|
644
|
+
<div class="card" onclick="runReport('daily')">
|
|
645
|
+
<div class="icon">D</div>
|
|
646
|
+
<div class="title">Daily</div>
|
|
647
|
+
<div class="desc">Ontem / Hoje / Bloqueios — pronto pra standup.</div>
|
|
648
|
+
</div>
|
|
154
649
|
</div>
|
|
155
|
-
|
|
156
|
-
|
|
650
|
+
|
|
651
|
+
<div class="grid2">
|
|
652
|
+
<div class="panel">
|
|
653
|
+
<div class="panelHead"><b>Workspace & publish settings</b><span class="small" id="last"></span></div>
|
|
654
|
+
<div class="panelBody">
|
|
655
|
+
<label>Workspace dir</label>
|
|
656
|
+
<div class="row">
|
|
657
|
+
<input id="dir" placeholder="./freya" />
|
|
658
|
+
<button class="btn small" onclick="pickDir()">Browse</button>
|
|
659
|
+
</div>
|
|
660
|
+
<div class="help">Escolha a pasta que contém <code>data/</code>, <code>logs/</code> e <code>scripts/</code>.</div>
|
|
661
|
+
|
|
662
|
+
<div style="height:12px"></div>
|
|
663
|
+
|
|
664
|
+
<div class="stack">
|
|
665
|
+
<button class="btn primary" onclick="doInit()">Init</button>
|
|
666
|
+
<button class="btn" onclick="doUpdate()">Update</button>
|
|
667
|
+
<button class="btn" onclick="doHealth()">Health</button>
|
|
668
|
+
<button class="btn" onclick="doMigrate()">Migrate</button>
|
|
669
|
+
</div>
|
|
670
|
+
|
|
671
|
+
<div style="height:16px"></div>
|
|
672
|
+
|
|
673
|
+
<label>Discord webhook URL</label>
|
|
674
|
+
<input id="discord" placeholder="https://discord.com/api/webhooks/..." />
|
|
675
|
+
<div style="height:10px"></div>
|
|
676
|
+
|
|
677
|
+
<label>Teams webhook URL</label>
|
|
678
|
+
<input id="teams" placeholder="https://..." />
|
|
679
|
+
<div class="help">O publish usa incoming webhooks. (Depois a gente evolui para anexos/chunks.)</div>
|
|
680
|
+
|
|
681
|
+
<div style="height:10px"></div>
|
|
682
|
+
<div class="stack">
|
|
683
|
+
<button class="btn" onclick="publish('discord')">Publish last → Discord</button>
|
|
684
|
+
<button class="btn" onclick="publish('teams')">Publish last → Teams</button>
|
|
685
|
+
</div>
|
|
686
|
+
</div>
|
|
687
|
+
</div>
|
|
688
|
+
|
|
689
|
+
<div class="panel">
|
|
690
|
+
<div class="panelHead">
|
|
691
|
+
<b>Output</b>
|
|
692
|
+
<div class="stack">
|
|
693
|
+
<button class="btn small" onclick="copyOut()">Copy</button>
|
|
694
|
+
<button class="btn small" onclick="clearOut()">Clear</button>
|
|
695
|
+
</div>
|
|
696
|
+
</div>
|
|
697
|
+
<div class="panelBody">
|
|
698
|
+
<div class="log" id="out"></div>
|
|
699
|
+
<div class="help">Dica: quando um report gera arquivo, mostramos o conteúdo real do report aqui (melhor que stdout).</div>
|
|
700
|
+
</div>
|
|
701
|
+
</div>
|
|
702
|
+
|
|
157
703
|
</div>
|
|
158
704
|
</div>
|
|
159
|
-
|
|
705
|
+
|
|
706
|
+
</main>
|
|
160
707
|
|
|
161
708
|
</div>
|
|
162
709
|
</div>
|
|
163
710
|
|
|
164
711
|
<script>
|
|
165
712
|
const $ = (id) => document.getElementById(id);
|
|
166
|
-
const state = {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
713
|
+
const state = { lastReportPath: null, lastText: '' };
|
|
714
|
+
|
|
715
|
+
function applyTheme(theme) {
|
|
716
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
717
|
+
localStorage.setItem('freya.theme', theme);
|
|
718
|
+
$('themeToggle').textContent = theme === 'dark' ? 'Light' : 'Dark';
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function toggleTheme() {
|
|
722
|
+
const t = localStorage.getItem('freya.theme') || 'light';
|
|
723
|
+
applyTheme(t === 'dark' ? 'light' : 'dark');
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function setPill(kind, text) {
|
|
727
|
+
const dot = $('dot');
|
|
728
|
+
dot.classList.remove('ok','err');
|
|
729
|
+
if (kind === 'ok') dot.classList.add('ok');
|
|
730
|
+
if (kind === 'err') dot.classList.add('err');
|
|
731
|
+
$('pill').textContent = text;
|
|
732
|
+
$('status') && ($('status').textContent = text);
|
|
733
|
+
}
|
|
170
734
|
|
|
171
735
|
function setOut(text) {
|
|
172
|
-
state.lastText = text;
|
|
736
|
+
state.lastText = text || '';
|
|
173
737
|
$('out').textContent = text || '';
|
|
174
738
|
}
|
|
175
739
|
|
|
176
|
-
function
|
|
177
|
-
|
|
178
|
-
|
|
740
|
+
function clearOut() {
|
|
741
|
+
setOut('');
|
|
742
|
+
setPill('ok', 'idle');
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
async function copyOut() {
|
|
746
|
+
try {
|
|
747
|
+
await navigator.clipboard.writeText(state.lastText || '');
|
|
748
|
+
setPill('ok','copied');
|
|
749
|
+
setTimeout(() => setPill('ok','idle'), 800);
|
|
750
|
+
} catch (e) {
|
|
751
|
+
setPill('err','copy failed');
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function setLast(p) {
|
|
756
|
+
state.lastReportPath = p;
|
|
757
|
+
$('last').textContent = p ? ('Last report: ' + p) : '';
|
|
179
758
|
}
|
|
180
759
|
|
|
181
760
|
function saveLocal() {
|
|
@@ -185,13 +764,14 @@ function html() {
|
|
|
185
764
|
}
|
|
186
765
|
|
|
187
766
|
function loadLocal() {
|
|
188
|
-
$('dir').value = localStorage.getItem('freya.dir') || '';
|
|
767
|
+
$('dir').value = localStorage.getItem('freya.dir') || './freya';
|
|
189
768
|
$('discord').value = localStorage.getItem('freya.discord') || '';
|
|
190
769
|
$('teams').value = localStorage.getItem('freya.teams') || '';
|
|
770
|
+
$('sidePath').textContent = $('dir').value || './freya';
|
|
191
771
|
}
|
|
192
772
|
|
|
193
|
-
async function api(
|
|
194
|
-
const res = await fetch(
|
|
773
|
+
async function api(p, body) {
|
|
774
|
+
const res = await fetch(p, {
|
|
195
775
|
method: body ? 'POST' : 'GET',
|
|
196
776
|
headers: body ? { 'Content-Type': 'application/json' } : {},
|
|
197
777
|
body: body ? JSON.stringify(body) : undefined
|
|
@@ -206,63 +786,210 @@ function html() {
|
|
|
206
786
|
return d || './freya';
|
|
207
787
|
}
|
|
208
788
|
|
|
789
|
+
async function pickDir() {
|
|
790
|
+
try {
|
|
791
|
+
setPill('run','picker…');
|
|
792
|
+
const r = await api('/api/pick-dir', {});
|
|
793
|
+
if (r && r.dir) {
|
|
794
|
+
$('dir').value = r.dir;
|
|
795
|
+
$('sidePath').textContent = r.dir;
|
|
796
|
+
}
|
|
797
|
+
saveLocal();
|
|
798
|
+
setPill('ok','ready');
|
|
799
|
+
} catch (e) {
|
|
800
|
+
setPill('err','picker failed');
|
|
801
|
+
setOut(String(e && e.message ? e.message : e));
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
209
805
|
async function doInit() {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
806
|
+
try {
|
|
807
|
+
saveLocal();
|
|
808
|
+
$('sidePath').textContent = dirOrDefault();
|
|
809
|
+
setPill('run','init…');
|
|
810
|
+
setOut('');
|
|
811
|
+
const r = await api('/api/init', { dir: dirOrDefault() });
|
|
812
|
+
setOut(r.output);
|
|
813
|
+
setLast(null);
|
|
814
|
+
setPill('ok','init ok');
|
|
815
|
+
} catch (e) {
|
|
816
|
+
setPill('err','init failed');
|
|
817
|
+
setOut(String(e && e.message ? e.message : e));
|
|
818
|
+
}
|
|
215
819
|
}
|
|
216
820
|
|
|
217
821
|
async function doUpdate() {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
822
|
+
try {
|
|
823
|
+
saveLocal();
|
|
824
|
+
$('sidePath').textContent = dirOrDefault();
|
|
825
|
+
setPill('run','update…');
|
|
826
|
+
setOut('');
|
|
827
|
+
const r = await api('/api/update', { dir: dirOrDefault() });
|
|
828
|
+
setOut(r.output);
|
|
829
|
+
setLast(null);
|
|
830
|
+
setPill('ok','update ok');
|
|
831
|
+
} catch (e) {
|
|
832
|
+
setPill('err','update failed');
|
|
833
|
+
setOut(String(e && e.message ? e.message : e));
|
|
834
|
+
}
|
|
223
835
|
}
|
|
224
836
|
|
|
225
837
|
async function doHealth() {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
838
|
+
try {
|
|
839
|
+
saveLocal();
|
|
840
|
+
$('sidePath').textContent = dirOrDefault();
|
|
841
|
+
setPill('run','health…');
|
|
842
|
+
setOut('');
|
|
843
|
+
const r = await api('/api/health', { dir: dirOrDefault() });
|
|
844
|
+
setOut(r.output);
|
|
845
|
+
setLast(null);
|
|
846
|
+
setPill('ok','health ok');
|
|
847
|
+
} catch (e) {
|
|
848
|
+
setPill('err','health failed');
|
|
849
|
+
setOut(String(e && e.message ? e.message : e));
|
|
850
|
+
}
|
|
231
851
|
}
|
|
232
852
|
|
|
233
853
|
async function doMigrate() {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
854
|
+
try {
|
|
855
|
+
saveLocal();
|
|
856
|
+
$('sidePath').textContent = dirOrDefault();
|
|
857
|
+
setPill('run','migrate…');
|
|
858
|
+
setOut('');
|
|
859
|
+
const r = await api('/api/migrate', { dir: dirOrDefault() });
|
|
860
|
+
setOut(r.output);
|
|
861
|
+
setLast(null);
|
|
862
|
+
setPill('ok','migrate ok');
|
|
863
|
+
} catch (e) {
|
|
864
|
+
setPill('err','migrate failed');
|
|
865
|
+
setOut(String(e && e.message ? e.message : e));
|
|
866
|
+
}
|
|
239
867
|
}
|
|
240
868
|
|
|
241
869
|
async function runReport(name) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
870
|
+
try {
|
|
871
|
+
saveLocal();
|
|
872
|
+
$('sidePath').textContent = dirOrDefault();
|
|
873
|
+
setPill('run', name + '…');
|
|
874
|
+
setOut('');
|
|
875
|
+
const r = await api('/api/report', { dir: dirOrDefault(), script: name });
|
|
876
|
+
setOut(r.output);
|
|
877
|
+
setLast(r.reportPath || null);
|
|
878
|
+
if (r.reportText) state.lastText = r.reportText;
|
|
879
|
+
setPill('ok', name + ' ok');
|
|
880
|
+
} catch (e) {
|
|
881
|
+
setPill('err', name + ' failed');
|
|
882
|
+
setOut(String(e && e.message ? e.message : e));
|
|
883
|
+
}
|
|
247
884
|
}
|
|
248
885
|
|
|
249
886
|
async function publish(target) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
887
|
+
try {
|
|
888
|
+
saveLocal();
|
|
889
|
+
if (!state.lastText) throw new Error('Gere um relatório primeiro.');
|
|
890
|
+
const webhookUrl = target === 'discord' ? $('discord').value.trim() : $('teams').value.trim();
|
|
891
|
+
if (!webhookUrl) throw new Error('Configure o webhook antes.');
|
|
892
|
+
setPill('run','publish…');
|
|
893
|
+
await api('/api/publish', { webhookUrl, text: state.lastText });
|
|
894
|
+
setPill('ok','published');
|
|
895
|
+
} catch (e) {
|
|
896
|
+
setPill('err','publish failed');
|
|
897
|
+
setOut(String(e && e.message ? e.message : e));
|
|
898
|
+
}
|
|
256
899
|
}
|
|
257
900
|
|
|
901
|
+
// init
|
|
902
|
+
applyTheme(localStorage.getItem('freya.theme') || 'light');
|
|
903
|
+
$('chipPort').textContent = location.host;
|
|
258
904
|
loadLocal();
|
|
259
|
-
|
|
905
|
+
setPill('ok','idle');
|
|
260
906
|
</script>
|
|
261
907
|
</body>
|
|
262
908
|
</html>`;
|
|
263
909
|
}
|
|
264
910
|
|
|
265
|
-
|
|
911
|
+
function ensureDir(p) {
|
|
912
|
+
fs.mkdirSync(p, { recursive: true });
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function isoDate(d = new Date()) {
|
|
916
|
+
return d.toISOString().slice(0, 10);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function isoNow() {
|
|
920
|
+
return new Date().toISOString();
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function seedDevWorkspace(workspaceDir) {
|
|
924
|
+
// Only create if missing; never overwrite user content.
|
|
925
|
+
ensureDir(path.join(workspaceDir, 'data', 'tasks'));
|
|
926
|
+
ensureDir(path.join(workspaceDir, 'data', 'career'));
|
|
927
|
+
ensureDir(path.join(workspaceDir, 'data', 'blockers'));
|
|
928
|
+
ensureDir(path.join(workspaceDir, 'data', 'Clients', 'acme', 'rocket'));
|
|
929
|
+
ensureDir(path.join(workspaceDir, 'logs', 'daily'));
|
|
930
|
+
|
|
931
|
+
const taskLog = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
|
|
932
|
+
if (!exists(taskLog)) {
|
|
933
|
+
fs.writeFileSync(taskLog, JSON.stringify({
|
|
934
|
+
schemaVersion: 1,
|
|
935
|
+
tasks: [
|
|
936
|
+
{ id: 't-demo-1', description: 'Preparar update executivo', category: 'DO_NOW', status: 'PENDING', createdAt: isoNow(), priority: 'high' },
|
|
937
|
+
{ id: 't-demo-2', description: 'Revisar PR de integração Teams', category: 'SCHEDULE', status: 'PENDING', createdAt: isoNow(), priority: 'medium' },
|
|
938
|
+
{ id: 't-demo-3', description: 'Rodar retro e registrar aprendizados', category: 'DO_NOW', status: 'COMPLETED', createdAt: isoNow(), completedAt: isoNow(), priority: 'low' }
|
|
939
|
+
]
|
|
940
|
+
}, null, 2) + '\n', 'utf8');
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const careerLog = path.join(workspaceDir, 'data', 'career', 'career-log.json');
|
|
944
|
+
if (!exists(careerLog)) {
|
|
945
|
+
fs.writeFileSync(careerLog, JSON.stringify({
|
|
946
|
+
schemaVersion: 1,
|
|
947
|
+
entries: [
|
|
948
|
+
{ id: 'c-demo-1', date: isoDate(), type: 'Achievement', description: 'Publicou o CLI @cccarv82/freya com init/web', tags: ['shipping', 'tooling'], source: 'dev-seed' },
|
|
949
|
+
{ id: 'c-demo-2', date: isoDate(), type: 'Feedback', description: 'Feedback: UX do painel web está “muito promissor”', tags: ['product'], source: 'dev-seed' }
|
|
950
|
+
]
|
|
951
|
+
}, null, 2) + '\n', 'utf8');
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const blockerLog = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
|
|
955
|
+
if (!exists(blockerLog)) {
|
|
956
|
+
fs.writeFileSync(blockerLog, JSON.stringify({
|
|
957
|
+
schemaVersion: 1,
|
|
958
|
+
blockers: [
|
|
959
|
+
{ id: 'b-demo-1', title: 'Webhook do Teams falhando em ambientes com 2FA', description: 'Ajustar token / payload', createdAt: isoNow(), status: 'OPEN', severity: 'HIGH', nextAction: 'Validar payload e limites' },
|
|
960
|
+
{ id: 'b-demo-2', title: 'Definir template de status report por cliente', description: 'Padronizar headings', createdAt: isoNow(), status: 'MITIGATING', severity: 'MEDIUM' }
|
|
961
|
+
]
|
|
962
|
+
}, null, 2) + '\n', 'utf8');
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const projectStatus = path.join(workspaceDir, 'data', 'Clients', 'acme', 'rocket', 'status.json');
|
|
966
|
+
if (!exists(projectStatus)) {
|
|
967
|
+
fs.writeFileSync(projectStatus, JSON.stringify({
|
|
968
|
+
schemaVersion: 1,
|
|
969
|
+
client: 'Acme',
|
|
970
|
+
project: 'Rocket',
|
|
971
|
+
currentStatus: 'Green — progressing as planned',
|
|
972
|
+
lastUpdated: isoNow(),
|
|
973
|
+
active: true,
|
|
974
|
+
history: [
|
|
975
|
+
{ date: isoNow(), type: 'Status', content: 'Launched stage 1', source: 'dev-seed' },
|
|
976
|
+
{ date: isoNow(), type: 'Risk', content: 'Potential delay on vendor dependency', source: 'dev-seed' }
|
|
977
|
+
]
|
|
978
|
+
}, null, 2) + '\n', 'utf8');
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const dailyLog = path.join(workspaceDir, 'logs', 'daily', `${isoDate()}.md`);
|
|
982
|
+
if (!exists(dailyLog)) {
|
|
983
|
+
fs.writeFileSync(dailyLog, `# Daily Log ${isoDate()}\n\n## [09:15] Raw Input\nReunião com a Acme. Tudo verde, mas preciso alinhar com fornecedor.\n\n## [16:40] Raw Input\nTerminei o relatório SM e publiquei no Discord.\n`, 'utf8');
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
return {
|
|
987
|
+
seeded: true,
|
|
988
|
+
paths: { taskLog, careerLog, blockerLog, projectStatus, dailyLog }
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
async function cmdWeb({ port, dir, open, dev }) {
|
|
266
993
|
const host = '127.0.0.1';
|
|
267
994
|
|
|
268
995
|
const server = http.createServer(async (req, res) => {
|
|
@@ -282,16 +1009,21 @@ async function cmdWeb({ port, dir, open }) {
|
|
|
282
1009
|
|
|
283
1010
|
const workspaceDir = path.resolve(process.cwd(), payload.dir || dir || './freya');
|
|
284
1011
|
|
|
1012
|
+
if (req.url === '/api/pick-dir') {
|
|
1013
|
+
const picked = await pickDirectoryNative();
|
|
1014
|
+
return safeJson(res, 200, { dir: picked });
|
|
1015
|
+
}
|
|
1016
|
+
|
|
285
1017
|
if (req.url === '/api/init') {
|
|
286
1018
|
const pkg = '@cccarv82/freya';
|
|
287
|
-
const r = await run(
|
|
1019
|
+
const r = await run(guessNpxCmd(), [pkg, 'init', workspaceDir], process.cwd());
|
|
288
1020
|
return safeJson(res, r.code === 0 ? 200 : 400, { output: (r.stdout + r.stderr).trim() });
|
|
289
1021
|
}
|
|
290
1022
|
|
|
291
1023
|
if (req.url === '/api/update') {
|
|
292
1024
|
const pkg = '@cccarv82/freya';
|
|
293
1025
|
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
294
|
-
const r = await run(
|
|
1026
|
+
const r = await run(guessNpxCmd(), [pkg, 'init', '--here'], workspaceDir);
|
|
295
1027
|
return safeJson(res, r.code === 0 ? 200 : 400, { output: (r.stdout + r.stderr).trim() });
|
|
296
1028
|
}
|
|
297
1029
|
|
|
@@ -323,8 +1055,12 @@ async function cmdWeb({ port, dir, open }) {
|
|
|
323
1055
|
};
|
|
324
1056
|
const prefix = prefixMap[script] || null;
|
|
325
1057
|
const reportPath = prefix ? newestFile(reportsDir, prefix) : null;
|
|
1058
|
+
const reportText = reportPath && exists(reportPath) ? fs.readFileSync(reportPath, 'utf8') : null;
|
|
1059
|
+
|
|
1060
|
+
// Prefer showing the actual report content when available.
|
|
1061
|
+
const output = reportText ? reportText : out;
|
|
326
1062
|
|
|
327
|
-
return safeJson(res, r.code === 0 ? 200 : 400, { output
|
|
1063
|
+
return safeJson(res, r.code === 0 ? 200 : 400, { output, reportPath, reportText });
|
|
328
1064
|
}
|
|
329
1065
|
|
|
330
1066
|
if (req.url === '/api/publish') {
|
|
@@ -348,7 +1084,8 @@ async function cmdWeb({ port, dir, open }) {
|
|
|
348
1084
|
}
|
|
349
1085
|
};
|
|
350
1086
|
|
|
351
|
-
const
|
|
1087
|
+
const proto = u.protocol === 'https:' ? require('https') : require('http');
|
|
1088
|
+
const req2 = proto.request(options, (r2) => {
|
|
352
1089
|
const chunks = [];
|
|
353
1090
|
r2.on('data', (c) => chunks.push(c));
|
|
354
1091
|
r2.on('end', () => {
|
|
@@ -374,6 +1111,18 @@ async function cmdWeb({ port, dir, open }) {
|
|
|
374
1111
|
await new Promise((resolve) => server.listen(port, host, resolve));
|
|
375
1112
|
|
|
376
1113
|
const url = `http://${host}:${port}/`;
|
|
1114
|
+
|
|
1115
|
+
// Optional dev seed (safe: only creates files if missing)
|
|
1116
|
+
if (dev) {
|
|
1117
|
+
const target = dir ? path.resolve(process.cwd(), dir) : path.join(process.cwd(), 'freya');
|
|
1118
|
+
try {
|
|
1119
|
+
seedDevWorkspace(target);
|
|
1120
|
+
process.stdout.write(`Dev seed: created demo files in ${target}\n`);
|
|
1121
|
+
} catch (e) {
|
|
1122
|
+
process.stdout.write(`Dev seed failed: ${e.message || String(e)}\n`);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
377
1126
|
process.stdout.write(`FREYA web running at ${url}\n`);
|
|
378
1127
|
if (open) openBrowser(url);
|
|
379
1128
|
}
|