@cccarv82/freya 2.7.1 → 2.8.3
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/auto-update.js +2 -5
- package/cli/web-ui.js +16 -0
- package/cli/web.js +132 -88
- package/package.json +1 -1
- package/scripts/generate-blockers-report.js +6 -1
- package/scripts/generate-daily-summary.js +10 -4
- package/scripts/generate-executive-report.js +8 -4
- package/scripts/generate-sm-weekly-report.js +10 -4
- package/scripts/generate-weekly-report.js +86 -100
- package/scripts/lib/DataLayer.js +48 -2
- package/scripts/lib/DataManager.js +62 -5
- package/scripts/lib/index-utils.js +4 -2
- package/scripts/migrate-data.js +8 -1
- package/scripts/validate-data.js +11 -3
- package/templates/base/scripts/generate-blockers-report.js +6 -1
- package/templates/base/scripts/generate-daily-summary.js +10 -4
- package/templates/base/scripts/generate-executive-report.js +9 -5
- package/templates/base/scripts/generate-sm-weekly-report.js +10 -4
- package/templates/base/scripts/generate-weekly-report.js +86 -100
- package/templates/base/scripts/lib/DataLayer.js +51 -5
- package/templates/base/scripts/lib/DataManager.js +76 -19
- package/templates/base/scripts/lib/index-utils.js +4 -2
- package/templates/base/scripts/migrate-data.js +8 -1
- package/templates/base/scripts/validate-data.js +20 -12
package/cli/auto-update.js
CHANGED
|
@@ -64,11 +64,8 @@ function runNpmInstall(workspaceDir) {
|
|
|
64
64
|
env: { ...process.env }
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
} else {
|
|
70
|
-
execSync('npm install --no-audit --no-fund', opts);
|
|
71
|
-
}
|
|
67
|
+
// BUG-16: both branches were identical; collapsed to one line
|
|
68
|
+
execSync('npm install --no-audit --no-fund', opts);
|
|
72
69
|
console.log('[FREYA] Dependencies installed successfully.');
|
|
73
70
|
return true;
|
|
74
71
|
} catch (err) {
|
package/cli/web-ui.js
CHANGED
|
@@ -1872,6 +1872,22 @@
|
|
|
1872
1872
|
if (isCompanionPage) {
|
|
1873
1873
|
refreshHealthChecklist();
|
|
1874
1874
|
}
|
|
1875
|
+
// On Dashboard: reveal the reportPreviewPanel so the user can see the output.
|
|
1876
|
+
// The panel is hidden by default (display:none) and only shown after a report runs.
|
|
1877
|
+
const previewPanel = $('reportPreviewPanel');
|
|
1878
|
+
if (previewPanel) {
|
|
1879
|
+
previewPanel.style.display = '';
|
|
1880
|
+
const titleEl = $('reportPreviewTitle');
|
|
1881
|
+
const labels = {
|
|
1882
|
+
status: 'Relatório Executivo',
|
|
1883
|
+
'sm-weekly': 'SM Weekly Report',
|
|
1884
|
+
blockers: 'Relatório de Bloqueios',
|
|
1885
|
+
daily: 'Daily Summary',
|
|
1886
|
+
report: 'Relatório Semanal'
|
|
1887
|
+
};
|
|
1888
|
+
if (titleEl) titleEl.textContent = labels[name] || 'Relatório Gerado';
|
|
1889
|
+
previewPanel.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1890
|
+
}
|
|
1875
1891
|
setPill('ok', name + ' ok');
|
|
1876
1892
|
} catch (e) {
|
|
1877
1893
|
setPill('err', name + ' failed');
|
package/cli/web.js
CHANGED
|
@@ -8,7 +8,7 @@ const { spawn } = require('child_process');
|
|
|
8
8
|
const { searchWorkspace } = require('../scripts/lib/search-utils');
|
|
9
9
|
const { searchIndex } = require('../scripts/lib/index-utils');
|
|
10
10
|
const { initWorkspace } = require('./init');
|
|
11
|
-
const { defaultInstance: dl, ready } = require('../scripts/lib/DataLayer');
|
|
11
|
+
const { defaultInstance: dl, ready, configure: configureDataLayer } = require('../scripts/lib/DataLayer');
|
|
12
12
|
const DataManager = require('../scripts/lib/DataManager');
|
|
13
13
|
|
|
14
14
|
function readAppVersion() {
|
|
@@ -40,8 +40,22 @@ const CHAT_ID_PATTERNS = [
|
|
|
40
40
|
];
|
|
41
41
|
|
|
42
42
|
function guessNpmCmd() {
|
|
43
|
-
//
|
|
44
|
-
return process.platform === 'win32' ? 'npm' : 'npm';
|
|
43
|
+
// On Windows, use npm.cmd so child_process.spawn can locate the executable without shell.
|
|
44
|
+
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// --- Module-level helpers used across multiple request handlers ---
|
|
48
|
+
|
|
49
|
+
function sha1(text) {
|
|
50
|
+
return crypto.createHash('sha1').update(String(text || ''), 'utf8').digest('hex');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeWhitespace(t) {
|
|
54
|
+
return String(t || '').replace(/\s+/g, ' ').trim();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeTextForKey(t) {
|
|
58
|
+
return normalizeWhitespace(t).toLowerCase();
|
|
45
59
|
}
|
|
46
60
|
|
|
47
61
|
function guessOpenCmd() {
|
|
@@ -250,6 +264,8 @@ function splitForDiscord(text, limit = 1900) {
|
|
|
250
264
|
const cut2 = window.lastIndexOf(NL);
|
|
251
265
|
if (cut > 400) end = i + cut;
|
|
252
266
|
else if (cut2 > 600) end = i + cut2;
|
|
267
|
+
// BUG-24: guarantee forward progress to prevent infinite loop
|
|
268
|
+
if (end <= i) end = i + 1;
|
|
253
269
|
const chunk = t.slice(i, end).trim();
|
|
254
270
|
if (chunk) parts.push(chunk);
|
|
255
271
|
i = end;
|
|
@@ -635,26 +651,38 @@ function normalizeWorkspaceDir(inputDir) {
|
|
|
635
651
|
return d;
|
|
636
652
|
}
|
|
637
653
|
|
|
638
|
-
function readBody(req) {
|
|
654
|
+
function readBody(req, maxBytes = 4 * 1024 * 1024) {
|
|
655
|
+
// BUG-34: enforce a request body size limit to prevent memory exhaustion
|
|
639
656
|
return new Promise((resolve, reject) => {
|
|
640
657
|
const chunks = [];
|
|
641
|
-
|
|
658
|
+
let total = 0;
|
|
659
|
+
req.on('data', chunk => {
|
|
660
|
+
total += chunk.length;
|
|
661
|
+
if (total > maxBytes) {
|
|
662
|
+
req.destroy();
|
|
663
|
+
reject(new Error('Request body too large'));
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
chunks.push(chunk);
|
|
667
|
+
});
|
|
642
668
|
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
643
669
|
req.on('error', reject);
|
|
644
670
|
});
|
|
645
671
|
}
|
|
646
672
|
|
|
647
|
-
function run(cmd, args, cwd) {
|
|
673
|
+
function run(cmd, args, cwd, extraEnv) {
|
|
648
674
|
return new Promise((resolve) => {
|
|
649
675
|
let child;
|
|
650
676
|
|
|
677
|
+
const env = extraEnv ? { ...process.env, ...extraEnv } : process.env;
|
|
678
|
+
|
|
651
679
|
try {
|
|
652
680
|
// On Windows, reliably execute CLI tools through cmd.exe.
|
|
653
681
|
if (process.platform === 'win32' && (cmd === 'npx' || cmd === 'npm')) {
|
|
654
682
|
const comspec = process.env.ComSpec || 'cmd.exe';
|
|
655
|
-
child = spawn(comspec, ['/d', '/s', '/c', cmd, ...args], { cwd, shell: false, env
|
|
683
|
+
child = spawn(comspec, ['/d', '/s', '/c', cmd, ...args], { cwd, shell: false, env });
|
|
656
684
|
} else {
|
|
657
|
-
child = spawn(cmd, args, { cwd, shell: false, env
|
|
685
|
+
child = spawn(cmd, args, { cwd, shell: false, env });
|
|
658
686
|
}
|
|
659
687
|
} catch (e) {
|
|
660
688
|
return resolve({ code: 1, stdout: '', stderr: e.message || String(e) });
|
|
@@ -1244,7 +1272,18 @@ function buildHtml(safeDefault, appVersion) {
|
|
|
1244
1272
|
</div>
|
|
1245
1273
|
</section>
|
|
1246
1274
|
|
|
1247
|
-
|
|
1275
|
+
<!-- Report Preview Panel — populated by runReport() via setOut() -->
|
|
1276
|
+
<section class="panel" id="reportPreviewPanel" style="display:none; margin-bottom: 16px;">
|
|
1277
|
+
<div class="panelHead" style="background: linear-gradient(90deg, var(--paper2), var(--paper)); border-left: 4px solid var(--accent);">
|
|
1278
|
+
<b style="color: var(--text); font-size: 14px;" id="reportPreviewTitle">Relatório Gerado</b>
|
|
1279
|
+
<div class="stack">
|
|
1280
|
+
<button class="btn small" type="button" onclick="document.getElementById('reportPreviewPanel').style.display='none'">Fechar</button>
|
|
1281
|
+
</div>
|
|
1282
|
+
</div>
|
|
1283
|
+
<div class="panelBody panelScroll" style="max-height: 520px; overflow-y: auto;">
|
|
1284
|
+
<div id="reportPreview" class="log md" style="font-family: var(--sans); padding: 8px 0;"></div>
|
|
1285
|
+
</div>
|
|
1286
|
+
</section>
|
|
1248
1287
|
|
|
1249
1288
|
<div class="centerHead">
|
|
1250
1289
|
<div>
|
|
@@ -1306,7 +1345,14 @@ function buildHtml(safeDefault, appVersion) {
|
|
|
1306
1345
|
<label style="display:flex; align-items:center; gap:10px; user-select:none; margin: 6px 0 12px 0">
|
|
1307
1346
|
<input id="prettyPublish" type="checkbox" checked style="width:auto" onchange="togglePrettyPublish()" />
|
|
1308
1347
|
Publicação bonita (cards/embeds)
|
|
1348
|
+
</label>
|
|
1309
1349
|
|
|
1350
|
+
</div>
|
|
1351
|
+
</div>
|
|
1352
|
+
</div>
|
|
1353
|
+
</div>
|
|
1354
|
+
</div>
|
|
1355
|
+
</main>
|
|
1310
1356
|
</div>
|
|
1311
1357
|
</div>
|
|
1312
1358
|
</div>
|
|
@@ -2080,13 +2126,30 @@ function getTimelineItems(workspaceDir) {
|
|
|
2080
2126
|
}
|
|
2081
2127
|
}
|
|
2082
2128
|
}
|
|
2129
|
+
|
|
2130
|
+
// BUG-33: Prefer SQLite tasks (primary source) over legacy task-log.json
|
|
2131
|
+
const seenIds = new Set();
|
|
2132
|
+
if (dl.db) {
|
|
2133
|
+
try {
|
|
2134
|
+
const sqliteTasks = dl.db.prepare('SELECT id, description, project_slug, created_at, completed_at FROM tasks').all();
|
|
2135
|
+
for (const t of sqliteTasks) {
|
|
2136
|
+
seenIds.add(t.id);
|
|
2137
|
+
if (t.created_at) items.push({ kind: 'task', date: String(t.created_at).slice(0, 10), title: `Task criada: ${t.description || t.id}`, content: t.project_slug || '' });
|
|
2138
|
+
if (t.completed_at) items.push({ kind: 'task', date: String(t.completed_at).slice(0, 10), title: `Task concluida: ${t.description || t.id}`, content: t.project_slug || '' });
|
|
2139
|
+
}
|
|
2140
|
+
} catch { /* db may not be ready yet, fall through to legacy */ }
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
// Legacy JSON fallback for tasks not yet in SQLite
|
|
2083
2144
|
const taskFile = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
|
|
2084
2145
|
const taskDoc = readJsonOrNull(taskFile) || { tasks: [] };
|
|
2085
2146
|
const tasks = Array.isArray(taskDoc.tasks) ? taskDoc.tasks : [];
|
|
2086
2147
|
for (const t of tasks) {
|
|
2148
|
+
if (seenIds.has(t.id)) continue; // already from SQLite
|
|
2087
2149
|
if (t.createdAt) items.push({ kind: 'task', date: String(t.createdAt).slice(0, 10), title: `Task criada: ${t.description || t.id}`, content: t.projectSlug || '' });
|
|
2088
2150
|
if (t.completedAt) items.push({ kind: 'task', date: String(t.completedAt).slice(0, 10), title: `Task concluida: ${t.description || t.id}`, content: t.projectSlug || '' });
|
|
2089
2151
|
}
|
|
2152
|
+
|
|
2090
2153
|
items.sort((a, b) => String(b.date || '').localeCompare(String(a.date || '')));
|
|
2091
2154
|
return items;
|
|
2092
2155
|
}
|
|
@@ -2237,12 +2300,24 @@ function seedDevWorkspace(workspaceDir) {
|
|
|
2237
2300
|
}
|
|
2238
2301
|
|
|
2239
2302
|
async function cmdWeb({ port, dir, open, dev }) {
|
|
2240
|
-
|
|
2303
|
+
// Determine workspace directory first, before any DB access.
|
|
2304
|
+
const wsDir = normalizeWorkspaceDir(dir || './freya');
|
|
2305
|
+
|
|
2306
|
+
// Configure the DataLayer singleton to point at the workspace's SQLite file
|
|
2307
|
+
// BEFORE awaiting `ready`. `ready` was started with the default path at
|
|
2308
|
+
// module-load time; configureDataLayer closes that connection and reopens
|
|
2309
|
+
// against the correct file so no code ever uses the wrong database.
|
|
2310
|
+
try {
|
|
2311
|
+
await configureDataLayer(wsDir);
|
|
2312
|
+
} catch (e) {
|
|
2313
|
+
console.error('[FREYA] Warning: Failed to configure DataLayer for workspace:', e.message || String(e));
|
|
2314
|
+
// Non-fatal: fall back to default DB path.
|
|
2315
|
+
await ready;
|
|
2316
|
+
}
|
|
2241
2317
|
|
|
2242
2318
|
// Auto-update workspace scripts/deps if Freya version changed
|
|
2243
2319
|
try {
|
|
2244
2320
|
const { autoUpdate } = require('./auto-update');
|
|
2245
|
-
const wsDir = normalizeWorkspaceDir(dir || './freya');
|
|
2246
2321
|
await autoUpdate(wsDir);
|
|
2247
2322
|
} catch { /* non-fatal */ }
|
|
2248
2323
|
|
|
@@ -2254,9 +2329,12 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2254
2329
|
try {
|
|
2255
2330
|
if (!req.url) return safeJson(res, 404, { error: 'Not found' });
|
|
2256
2331
|
|
|
2332
|
+
// BUG-29: helper to replace hardcoded port placeholder with the actual listen port
|
|
2333
|
+
const injectPort = (body) => body.replace(/127\.0\.0\.1:3872/g, `127.0.0.1:${port}`);
|
|
2334
|
+
|
|
2257
2335
|
if (req.method === 'GET' && req.url === '/') {
|
|
2258
2336
|
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
2259
|
-
const body = html(dir || './freya');
|
|
2337
|
+
const body = injectPort(html(dir || './freya'));
|
|
2260
2338
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
2261
2339
|
res.end(body);
|
|
2262
2340
|
return;
|
|
@@ -2264,7 +2342,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2264
2342
|
|
|
2265
2343
|
if (req.method === 'GET' && req.url === '/reports') {
|
|
2266
2344
|
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
2267
|
-
const body = reportsHtml(dir || './freya');
|
|
2345
|
+
const body = injectPort(reportsHtml(dir || './freya'));
|
|
2268
2346
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
2269
2347
|
res.end(body);
|
|
2270
2348
|
return;
|
|
@@ -2272,7 +2350,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2272
2350
|
|
|
2273
2351
|
if (req.method === 'GET' && req.url === '/companion') {
|
|
2274
2352
|
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
2275
|
-
const body = companionHtml(dir || './freya');
|
|
2353
|
+
const body = injectPort(companionHtml(dir || './freya'));
|
|
2276
2354
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
2277
2355
|
res.end(body);
|
|
2278
2356
|
return;
|
|
@@ -2280,7 +2358,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2280
2358
|
|
|
2281
2359
|
if (req.method === 'GET' && req.url === '/projects') {
|
|
2282
2360
|
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
2283
|
-
const body = projectsHtml(dir || './freya');
|
|
2361
|
+
const body = injectPort(projectsHtml(dir || './freya'));
|
|
2284
2362
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
2285
2363
|
res.end(body);
|
|
2286
2364
|
return;
|
|
@@ -2288,7 +2366,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2288
2366
|
|
|
2289
2367
|
if (req.method === 'GET' && req.url === '/graph') {
|
|
2290
2368
|
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
2291
|
-
const body = buildGraphHtml(dir || './freya', APP_VERSION);
|
|
2369
|
+
const body = injectPort(buildGraphHtml(dir || './freya', APP_VERSION));
|
|
2292
2370
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
2293
2371
|
res.end(body);
|
|
2294
2372
|
return;
|
|
@@ -2296,7 +2374,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2296
2374
|
|
|
2297
2375
|
if (req.method === 'GET' && req.url === '/timeline') {
|
|
2298
2376
|
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
2299
|
-
const body = timelineHtml(dir || './freya');
|
|
2377
|
+
const body = injectPort(timelineHtml(dir || './freya'));
|
|
2300
2378
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
2301
2379
|
res.end(body);
|
|
2302
2380
|
return;
|
|
@@ -2304,7 +2382,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2304
2382
|
|
|
2305
2383
|
if (req.method === 'GET' && req.url === '/settings') {
|
|
2306
2384
|
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
2307
|
-
const body = settingsHtml(dir || './freya');
|
|
2385
|
+
const body = injectPort(settingsHtml(dir || './freya'));
|
|
2308
2386
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
2309
2387
|
res.end(body);
|
|
2310
2388
|
return;
|
|
@@ -2312,7 +2390,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2312
2390
|
|
|
2313
2391
|
if (req.method === 'GET' && req.url === '/docs') {
|
|
2314
2392
|
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
2315
|
-
const body = docsHtml(dir || './freya');
|
|
2393
|
+
const body = injectPort(docsHtml(dir || './freya'));
|
|
2316
2394
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
2317
2395
|
res.end(body);
|
|
2318
2396
|
return;
|
|
@@ -2880,8 +2958,10 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2880
2958
|
const reportsDir = path.join(workspaceDir, 'docs', 'reports');
|
|
2881
2959
|
const safeReportsDir = path.resolve(reportsDir);
|
|
2882
2960
|
const safeFull = path.resolve(full);
|
|
2883
|
-
|
|
2884
|
-
|
|
2961
|
+
// BUG-07: use path.relative for traversal check (startsWith can be fooled on some OS)
|
|
2962
|
+
const rel2 = path.relative(safeReportsDir, safeFull);
|
|
2963
|
+
if (!rel2 || rel2.startsWith('..') || path.isAbsolute(rel2)) {
|
|
2964
|
+
return safeJson(res, 400, { error: 'Caminho de relatório inválido' });
|
|
2885
2965
|
}
|
|
2886
2966
|
|
|
2887
2967
|
const text = fs.readFileSync(safeFull, 'utf8');
|
|
@@ -3045,8 +3125,10 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3045
3125
|
|
|
3046
3126
|
// Best-effort: if Copilot CLI isn't available, return 200 with an explanatory plan
|
|
3047
3127
|
// so the UI can show actionable next steps instead of hard-failing.
|
|
3128
|
+
// BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
|
|
3129
|
+
const agentEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
|
|
3048
3130
|
try {
|
|
3049
|
-
const r = await run(cmd, ['-s', '--no-color', '--stream', 'off', '-p', prompt, '--allow-all-tools'], workspaceDir);
|
|
3131
|
+
const r = await run(cmd, ['-s', '--no-color', '--stream', 'off', '-p', prompt, '--allow-all-tools'], workspaceDir, agentEnv);
|
|
3050
3132
|
const out = (r.stdout + r.stderr).trim();
|
|
3051
3133
|
if (r.code !== 0) {
|
|
3052
3134
|
return safeJson(res, 200, {
|
|
@@ -3190,23 +3272,6 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3190
3272
|
if (!Array.isArray(actions) || actions.length === 0) {
|
|
3191
3273
|
return safeJson(res, 400, { error: 'Plan has no actions[]' });
|
|
3192
3274
|
}
|
|
3193
|
-
function normalizeWhitespace(t) {
|
|
3194
|
-
return String(t || '').replace(/\s+/g, ' ').trim();
|
|
3195
|
-
}
|
|
3196
|
-
|
|
3197
|
-
function normalizeTextForKey(t) {
|
|
3198
|
-
return normalizeWhitespace(t).toLowerCase();
|
|
3199
|
-
}
|
|
3200
|
-
|
|
3201
|
-
function sha1(text) {
|
|
3202
|
-
return crypto.createHash('sha1').update(String(text || ''), 'utf8').digest('hex');
|
|
3203
|
-
}
|
|
3204
|
-
|
|
3205
|
-
const recentTasks = dl.db.prepare("SELECT description FROM tasks WHERE created_at >= datetime('now', '-1 day')").all();
|
|
3206
|
-
const existingTaskKeys24h = new Set(recentTasks.map(t => sha1(normalizeTextForKey(t.description))));
|
|
3207
|
-
|
|
3208
|
-
const recentBlockers = dl.db.prepare("SELECT title FROM blockers WHERE created_at >= datetime('now', '-1 day')").all();
|
|
3209
|
-
const existingBlockerKeys24h = new Set(recentBlockers.map(b => sha1(normalizeTextForKey(b.title))));
|
|
3210
3275
|
|
|
3211
3276
|
const now = new Date().toISOString();
|
|
3212
3277
|
const applyMode = String(payload.mode || 'all').trim();
|
|
@@ -3241,8 +3306,14 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3241
3306
|
const insertTask = dl.db.prepare(`INSERT INTO tasks (id, project_slug, description, category, status, metadata) VALUES (?, ?, ?, ?, ?, ?)`);
|
|
3242
3307
|
const insertBlocker = dl.db.prepare(`INSERT INTO blockers (id, project_slug, title, severity, status, owner, next_action, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
3243
3308
|
|
|
3244
|
-
//
|
|
3309
|
+
// BUG-31: Move deduplication queries INSIDE the transaction to eliminate TOCTOU race
|
|
3245
3310
|
const applyTx = dl.db.transaction((actionsToApply) => {
|
|
3311
|
+
// Query for existing keys inside the transaction for atomicity
|
|
3312
|
+
const recentTasks = dl.db.prepare("SELECT description FROM tasks WHERE created_at >= datetime('now', '-1 day')").all();
|
|
3313
|
+
const existingTaskKeys24h = new Set(recentTasks.map(t => sha1(normalizeTextForKey(t.description))));
|
|
3314
|
+
const recentBlockers = dl.db.prepare("SELECT title FROM blockers WHERE created_at >= datetime('now', '-1 day')").all();
|
|
3315
|
+
const existingBlockerKeys24h = new Set(recentBlockers.map(b => sha1(normalizeTextForKey(b.title))));
|
|
3316
|
+
|
|
3246
3317
|
for (const a of actionsToApply) {
|
|
3247
3318
|
if (!a || typeof a !== 'object') continue;
|
|
3248
3319
|
const type = String(a.type || '').trim();
|
|
@@ -3370,18 +3441,21 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3370
3441
|
}
|
|
3371
3442
|
|
|
3372
3443
|
const npmCmd = guessNpmCmd();
|
|
3444
|
+
// Pass workspace dir to all child npm scripts so their DataLayer instances
|
|
3445
|
+
// resolve the same SQLite file as the web server process.
|
|
3446
|
+
const workspaceEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
|
|
3373
3447
|
|
|
3374
3448
|
if (req.url === '/api/health') {
|
|
3375
3449
|
if (!looksLikeFreyaWorkspace(workspaceDir)) {
|
|
3376
3450
|
return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized' });
|
|
3377
3451
|
}
|
|
3378
|
-
const r = await run(npmCmd, ['run', 'health'], workspaceDir);
|
|
3452
|
+
const r = await run(npmCmd, ['run', 'health'], workspaceDir, workspaceEnv);
|
|
3379
3453
|
const output = (r.stdout + r.stderr).trim();
|
|
3380
3454
|
return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { output } : { error: output || 'health failed', output });
|
|
3381
3455
|
}
|
|
3382
3456
|
|
|
3383
3457
|
if (req.url === '/api/migrate') {
|
|
3384
|
-
const r = await run(npmCmd, ['run', 'migrate'], workspaceDir);
|
|
3458
|
+
const r = await run(npmCmd, ['run', 'migrate'], workspaceDir, workspaceEnv);
|
|
3385
3459
|
const output = (r.stdout + r.stderr).trim();
|
|
3386
3460
|
return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { output } : { error: output || 'migrate failed', output });
|
|
3387
3461
|
}
|
|
@@ -3389,13 +3463,13 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3389
3463
|
|
|
3390
3464
|
|
|
3391
3465
|
if (req.url === '/api/obsidian/export') {
|
|
3392
|
-
const r = await run(npmCmd, ['run', 'export-obsidian'], workspaceDir);
|
|
3466
|
+
const r = await run(npmCmd, ['run', 'export-obsidian'], workspaceDir, workspaceEnv);
|
|
3393
3467
|
const out = (r.stdout + r.stderr).trim();
|
|
3394
3468
|
return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { ok: true, output: out } : { error: out || 'export failed', output: out });
|
|
3395
3469
|
}
|
|
3396
3470
|
|
|
3397
3471
|
if (req.url === '/api/index/rebuild') {
|
|
3398
|
-
const r = await run(npmCmd, ['run', 'build-index'], workspaceDir);
|
|
3472
|
+
const r = await run(npmCmd, ['run', 'build-index'], workspaceDir, workspaceEnv);
|
|
3399
3473
|
const out = (r.stdout + r.stderr).trim();
|
|
3400
3474
|
return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { ok: true, output: out } : { error: out || 'index rebuild failed', output: out });
|
|
3401
3475
|
}
|
|
@@ -3435,9 +3509,11 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3435
3509
|
|
|
3436
3510
|
const cmd = process.env.COPILOT_CMD || 'copilot';
|
|
3437
3511
|
|
|
3512
|
+
// BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
|
|
3513
|
+
const oracleEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
|
|
3438
3514
|
try {
|
|
3439
3515
|
// Removed --allow-all-tools and --add-dir to force reliance on RAG context
|
|
3440
|
-
const r = await run(cmd, ['-s', '--no-color', '--stream', 'off', '-p', prompt], workspaceDir);
|
|
3516
|
+
const r = await run(cmd, ['-s', '--no-color', '--stream', 'off', '-p', prompt], workspaceDir, oracleEnv);
|
|
3441
3517
|
const out = (r.stdout + r.stderr).trim();
|
|
3442
3518
|
if (r.code !== 0) {
|
|
3443
3519
|
return safeJson(res, 200, { ok: false, answer: 'Falha na busca do agente Oracle:\n' + (out || 'Exit code != 0'), sessionId });
|
|
@@ -3652,48 +3728,8 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3652
3728
|
return safeJson(res, 200, { ok: true, items, stats: { pendingTasks, openBlockers, reportsToday, reportsTotal: reports.length } });
|
|
3653
3729
|
}
|
|
3654
3730
|
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
const index = Number.isInteger(payload.index) ? payload.index : null;
|
|
3658
|
-
if (!title) return safeJson(res, 400, { error: 'Missing title' });
|
|
3659
|
-
const p = path.join(workspaceDir, 'docs', 'reports', 'fidelizacao-incident-index.md');
|
|
3660
|
-
if (!exists(p)) return safeJson(res, 404, { error: 'Incident index not found' });
|
|
3661
|
-
const md = fs.readFileSync(p, 'utf8');
|
|
3662
|
-
const updated = resolveIncidentInMarkdown(md, title, index);
|
|
3663
|
-
if (!updated) return safeJson(res, 404, { error: 'Incident not found' });
|
|
3664
|
-
fs.writeFileSync(p, updated, 'utf8');
|
|
3665
|
-
return safeJson(res, 200, { ok: true });
|
|
3666
|
-
}
|
|
3667
|
-
|
|
3668
|
-
if (req.url === '/api/incidents') {
|
|
3669
|
-
const p = path.join(workspaceDir, 'docs', 'reports', 'fidelizacao-incident-index.md');
|
|
3670
|
-
if (!exists(p)) return safeJson(res, 200, { ok: true, markdown: '' });
|
|
3671
|
-
const md = fs.readFileSync(p, 'utf8');
|
|
3672
|
-
return safeJson(res, 200, { ok: true, markdown: md });
|
|
3673
|
-
}
|
|
3674
|
-
|
|
3675
|
-
if (req.url === '/api/tasks/heatmap') {
|
|
3676
|
-
const file = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
|
|
3677
|
-
const doc = readJsonOrNull(file) || { tasks: [] };
|
|
3678
|
-
const tasks = Array.isArray(doc.tasks) ? doc.tasks : [];
|
|
3679
|
-
const map = {};
|
|
3680
|
-
const priorityRank = { high: 3, medium: 2, low: 1, '': 0 };
|
|
3681
|
-
for (const t of tasks) {
|
|
3682
|
-
const slug = t.projectSlug || 'unassigned';
|
|
3683
|
-
if (!map[slug]) map[slug] = { total: 0, pending: 0, completed: 0, priority: '' };
|
|
3684
|
-
map[slug].total++;
|
|
3685
|
-
if (t.status === 'COMPLETED') map[slug].completed++; else map[slug].pending++;
|
|
3686
|
-
const p = normalizePriority(t.priority || t.severity);
|
|
3687
|
-
if (priorityRank[p] > priorityRank[map[slug].priority || '']) map[slug].priority = p;
|
|
3688
|
-
}
|
|
3689
|
-
const items = Object.entries(map).map(([slug, v]) => {
|
|
3690
|
-
const statusPath = path.join(workspaceDir, 'data', 'Clients', slug, 'status.json');
|
|
3691
|
-
const linkRel = exists(statusPath) ? path.relative(workspaceDir, statusPath).replace(/\\/g, '/') : '';
|
|
3692
|
-
return { slug, ...v, linkRel };
|
|
3693
|
-
});
|
|
3694
|
-
items.sort((a, b) => b.total - a.total);
|
|
3695
|
-
return safeJson(res, 200, { ok: true, items });
|
|
3696
|
-
}
|
|
3731
|
+
// BUG-12: duplicate handlers for /api/incidents/resolve, /api/incidents,
|
|
3732
|
+
// and /api/tasks/heatmap were removed here — originals remain earlier in the file.
|
|
3697
3733
|
|
|
3698
3734
|
if (req.url === '/api/blockers/summary') {
|
|
3699
3735
|
const open = dl.db.prepare(`
|
|
@@ -3808,7 +3844,15 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3808
3844
|
const script = payload.script;
|
|
3809
3845
|
if (!script) return safeJson(res, 400, { error: 'Missing script' });
|
|
3810
3846
|
|
|
3811
|
-
|
|
3847
|
+
// BUG-15: Whitelist allowed report scripts to prevent arbitrary npm run execution
|
|
3848
|
+
const ALLOWED_REPORT_SCRIPTS = new Set(['blockers', 'sm-weekly', 'status', 'daily', 'report', 'build-index', 'update-index', 'export-obsidian']);
|
|
3849
|
+
if (!ALLOWED_REPORT_SCRIPTS.has(script)) {
|
|
3850
|
+
return safeJson(res, 400, { error: 'Script não permitido: ' + script });
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
// Pass FREYA_WORKSPACE_DIR so the workspace's DataLayer uses the same
|
|
3854
|
+
// SQLite file as the web server process (fixes the two-database split).
|
|
3855
|
+
const r = await run(npmCmd, ['run', script], workspaceDir, workspaceEnv);
|
|
3812
3856
|
const out = (r.stdout + r.stderr).trim();
|
|
3813
3857
|
|
|
3814
3858
|
const reportsDir = path.join(workspaceDir, 'docs', 'reports');
|
package/package.json
CHANGED
|
@@ -5,7 +5,12 @@ const { toIsoDate, safeParseToMs, isWithinRange } = require('./lib/date-utils');
|
|
|
5
5
|
|
|
6
6
|
const DataManager = require('./lib/DataManager');
|
|
7
7
|
const { ready } = require('./lib/DataLayer');
|
|
8
|
-
|
|
8
|
+
// BUG-30: use FREYA_WORKSPACE_DIR instead of __dirname
|
|
9
|
+
const WORKSPACE_DIR = process.env.FREYA_WORKSPACE_DIR
|
|
10
|
+
? path.resolve(process.env.FREYA_WORKSPACE_DIR)
|
|
11
|
+
: path.join(__dirname, '..'); // fallback: scripts/ is one level below repo root
|
|
12
|
+
|
|
13
|
+
const REPORT_DIR = path.join(WORKSPACE_DIR, 'docs', 'reports');
|
|
9
14
|
|
|
10
15
|
const SEVERITY_ORDER = {
|
|
11
16
|
CRITICAL: 0,
|
|
@@ -4,9 +4,14 @@ const path = require('path');
|
|
|
4
4
|
const DataManager = require('./lib/DataManager');
|
|
5
5
|
const { ready } = require('./lib/DataLayer');
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
7
|
+
// BUG-30: use FREYA_WORKSPACE_DIR instead of __dirname
|
|
8
|
+
const WORKSPACE_DIR = process.env.FREYA_WORKSPACE_DIR
|
|
9
|
+
? path.resolve(process.env.FREYA_WORKSPACE_DIR)
|
|
10
|
+
: path.join(__dirname, '..'); // fallback: scripts/ is one level below repo root
|
|
11
|
+
|
|
12
|
+
const DATA_DIR = path.join(WORKSPACE_DIR, 'data');
|
|
13
|
+
const LOGS_DIR = path.join(WORKSPACE_DIR, 'logs', 'daily');
|
|
14
|
+
const REPORT_DIR = path.join(WORKSPACE_DIR, 'docs', 'reports');
|
|
10
15
|
|
|
11
16
|
const dm = new DataManager(DATA_DIR, LOGS_DIR);
|
|
12
17
|
|
|
@@ -24,9 +29,10 @@ async function generateDailySummary() {
|
|
|
24
29
|
let summary = "";
|
|
25
30
|
|
|
26
31
|
// 1. Ontem (Completed < 24h)
|
|
32
|
+
// BUG-09: use completedAt (not createdAt) to decide if a task appears in "yesterday"
|
|
27
33
|
const completedRecently = tasks.filter(t => {
|
|
28
34
|
if (t.status !== 'COMPLETED') return false;
|
|
29
|
-
const ms = dm.
|
|
35
|
+
const ms = dm.getResolvedAt(t);
|
|
30
36
|
if (!Number.isFinite(ms)) return false;
|
|
31
37
|
return ms >= ms24h && ms <= now.getTime();
|
|
32
38
|
});
|
|
@@ -13,10 +13,14 @@ const { toIsoDate, safeParseToMs } = require('./lib/date-utils');
|
|
|
13
13
|
const DataManager = require('./lib/DataManager');
|
|
14
14
|
const { ready } = require('./lib/DataLayer');
|
|
15
15
|
|
|
16
|
-
// --- Configuration ---
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
// --- Configuration (BUG-30: use FREYA_WORKSPACE_DIR instead of __dirname) ---
|
|
17
|
+
const WORKSPACE_DIR = process.env.FREYA_WORKSPACE_DIR
|
|
18
|
+
? path.resolve(process.env.FREYA_WORKSPACE_DIR)
|
|
19
|
+
: path.join(__dirname, '..'); // fallback: scripts/ is one level below repo root
|
|
20
|
+
|
|
21
|
+
const DATA_DIR = path.join(WORKSPACE_DIR, 'data');
|
|
22
|
+
const LOGS_DIR = path.join(WORKSPACE_DIR, 'logs', 'daily');
|
|
23
|
+
const OUTPUT_DIR = path.join(WORKSPACE_DIR, 'docs', 'reports');
|
|
20
24
|
|
|
21
25
|
const dm = new DataManager(DATA_DIR, LOGS_DIR);
|
|
22
26
|
|
|
@@ -5,9 +5,14 @@ const { toIsoDate, safeParseToMs } = require('./lib/date-utils');
|
|
|
5
5
|
const DataManager = require('./lib/DataManager');
|
|
6
6
|
const { ready } = require('./lib/DataLayer');
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
8
|
+
// BUG-30: use FREYA_WORKSPACE_DIR instead of __dirname
|
|
9
|
+
const WORKSPACE_DIR = process.env.FREYA_WORKSPACE_DIR
|
|
10
|
+
? path.resolve(process.env.FREYA_WORKSPACE_DIR)
|
|
11
|
+
: path.join(__dirname, '..'); // fallback: scripts/ is one level below repo root
|
|
12
|
+
|
|
13
|
+
const DATA_DIR = path.join(WORKSPACE_DIR, 'data');
|
|
14
|
+
const LOGS_DIR = path.join(WORKSPACE_DIR, 'logs', 'daily');
|
|
15
|
+
const REPORT_DIR = path.join(WORKSPACE_DIR, 'docs', 'reports');
|
|
11
16
|
|
|
12
17
|
const dm = new DataManager(DATA_DIR, LOGS_DIR);
|
|
13
18
|
|
|
@@ -109,7 +114,8 @@ async function generate() {
|
|
|
109
114
|
projectsWithUpdates.forEach(p => {
|
|
110
115
|
md += `### ${p.client} / ${p.project}\n`;
|
|
111
116
|
if (p.currentStatus) md += `**Status:** ${p.currentStatus}\n`;
|
|
112
|
-
p.recent
|
|
117
|
+
const projectEvents = p.events || p.recent || [];
|
|
118
|
+
projectEvents.forEach(e => {
|
|
113
119
|
md += `- [${e.date || e.timestamp}] ${e.content || ''}\n`;
|
|
114
120
|
});
|
|
115
121
|
md += `\n`;
|