@cccarv82/freya 2.7.0 → 2.8.2
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.js +120 -87
- 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 +95 -7
- 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 +98 -10
- 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.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) });
|
|
@@ -1306,7 +1334,14 @@ function buildHtml(safeDefault, appVersion) {
|
|
|
1306
1334
|
<label style="display:flex; align-items:center; gap:10px; user-select:none; margin: 6px 0 12px 0">
|
|
1307
1335
|
<input id="prettyPublish" type="checkbox" checked style="width:auto" onchange="togglePrettyPublish()" />
|
|
1308
1336
|
Publicação bonita (cards/embeds)
|
|
1337
|
+
</label>
|
|
1309
1338
|
|
|
1339
|
+
</div>
|
|
1340
|
+
</div>
|
|
1341
|
+
</div>
|
|
1342
|
+
</div>
|
|
1343
|
+
</div>
|
|
1344
|
+
</main>
|
|
1310
1345
|
</div>
|
|
1311
1346
|
</div>
|
|
1312
1347
|
</div>
|
|
@@ -2080,13 +2115,30 @@ function getTimelineItems(workspaceDir) {
|
|
|
2080
2115
|
}
|
|
2081
2116
|
}
|
|
2082
2117
|
}
|
|
2118
|
+
|
|
2119
|
+
// BUG-33: Prefer SQLite tasks (primary source) over legacy task-log.json
|
|
2120
|
+
const seenIds = new Set();
|
|
2121
|
+
if (dl.db) {
|
|
2122
|
+
try {
|
|
2123
|
+
const sqliteTasks = dl.db.prepare('SELECT id, description, project_slug, created_at, completed_at FROM tasks').all();
|
|
2124
|
+
for (const t of sqliteTasks) {
|
|
2125
|
+
seenIds.add(t.id);
|
|
2126
|
+
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 || '' });
|
|
2127
|
+
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 || '' });
|
|
2128
|
+
}
|
|
2129
|
+
} catch { /* db may not be ready yet, fall through to legacy */ }
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
// Legacy JSON fallback for tasks not yet in SQLite
|
|
2083
2133
|
const taskFile = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
|
|
2084
2134
|
const taskDoc = readJsonOrNull(taskFile) || { tasks: [] };
|
|
2085
2135
|
const tasks = Array.isArray(taskDoc.tasks) ? taskDoc.tasks : [];
|
|
2086
2136
|
for (const t of tasks) {
|
|
2137
|
+
if (seenIds.has(t.id)) continue; // already from SQLite
|
|
2087
2138
|
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
2139
|
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
2140
|
}
|
|
2141
|
+
|
|
2090
2142
|
items.sort((a, b) => String(b.date || '').localeCompare(String(a.date || '')));
|
|
2091
2143
|
return items;
|
|
2092
2144
|
}
|
|
@@ -2237,12 +2289,24 @@ function seedDevWorkspace(workspaceDir) {
|
|
|
2237
2289
|
}
|
|
2238
2290
|
|
|
2239
2291
|
async function cmdWeb({ port, dir, open, dev }) {
|
|
2240
|
-
|
|
2292
|
+
// Determine workspace directory first, before any DB access.
|
|
2293
|
+
const wsDir = normalizeWorkspaceDir(dir || './freya');
|
|
2294
|
+
|
|
2295
|
+
// Configure the DataLayer singleton to point at the workspace's SQLite file
|
|
2296
|
+
// BEFORE awaiting `ready`. `ready` was started with the default path at
|
|
2297
|
+
// module-load time; configureDataLayer closes that connection and reopens
|
|
2298
|
+
// against the correct file so no code ever uses the wrong database.
|
|
2299
|
+
try {
|
|
2300
|
+
await configureDataLayer(wsDir);
|
|
2301
|
+
} catch (e) {
|
|
2302
|
+
console.error('[FREYA] Warning: Failed to configure DataLayer for workspace:', e.message || String(e));
|
|
2303
|
+
// Non-fatal: fall back to default DB path.
|
|
2304
|
+
await ready;
|
|
2305
|
+
}
|
|
2241
2306
|
|
|
2242
2307
|
// Auto-update workspace scripts/deps if Freya version changed
|
|
2243
2308
|
try {
|
|
2244
2309
|
const { autoUpdate } = require('./auto-update');
|
|
2245
|
-
const wsDir = normalizeWorkspaceDir(dir || './freya');
|
|
2246
2310
|
await autoUpdate(wsDir);
|
|
2247
2311
|
} catch { /* non-fatal */ }
|
|
2248
2312
|
|
|
@@ -2254,9 +2318,12 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2254
2318
|
try {
|
|
2255
2319
|
if (!req.url) return safeJson(res, 404, { error: 'Not found' });
|
|
2256
2320
|
|
|
2321
|
+
// BUG-29: helper to replace hardcoded port placeholder with the actual listen port
|
|
2322
|
+
const injectPort = (body) => body.replace(/127\.0\.0\.1:3872/g, `127.0.0.1:${port}`);
|
|
2323
|
+
|
|
2257
2324
|
if (req.method === 'GET' && req.url === '/') {
|
|
2258
2325
|
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
2259
|
-
const body = html(dir || './freya');
|
|
2326
|
+
const body = injectPort(html(dir || './freya'));
|
|
2260
2327
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
2261
2328
|
res.end(body);
|
|
2262
2329
|
return;
|
|
@@ -2264,7 +2331,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2264
2331
|
|
|
2265
2332
|
if (req.method === 'GET' && req.url === '/reports') {
|
|
2266
2333
|
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
2267
|
-
const body = reportsHtml(dir || './freya');
|
|
2334
|
+
const body = injectPort(reportsHtml(dir || './freya'));
|
|
2268
2335
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
2269
2336
|
res.end(body);
|
|
2270
2337
|
return;
|
|
@@ -2272,7 +2339,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2272
2339
|
|
|
2273
2340
|
if (req.method === 'GET' && req.url === '/companion') {
|
|
2274
2341
|
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
2275
|
-
const body = companionHtml(dir || './freya');
|
|
2342
|
+
const body = injectPort(companionHtml(dir || './freya'));
|
|
2276
2343
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
2277
2344
|
res.end(body);
|
|
2278
2345
|
return;
|
|
@@ -2280,7 +2347,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2280
2347
|
|
|
2281
2348
|
if (req.method === 'GET' && req.url === '/projects') {
|
|
2282
2349
|
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
2283
|
-
const body = projectsHtml(dir || './freya');
|
|
2350
|
+
const body = injectPort(projectsHtml(dir || './freya'));
|
|
2284
2351
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
2285
2352
|
res.end(body);
|
|
2286
2353
|
return;
|
|
@@ -2288,7 +2355,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2288
2355
|
|
|
2289
2356
|
if (req.method === 'GET' && req.url === '/graph') {
|
|
2290
2357
|
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
2291
|
-
const body = buildGraphHtml(dir || './freya', APP_VERSION);
|
|
2358
|
+
const body = injectPort(buildGraphHtml(dir || './freya', APP_VERSION));
|
|
2292
2359
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
2293
2360
|
res.end(body);
|
|
2294
2361
|
return;
|
|
@@ -2296,7 +2363,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2296
2363
|
|
|
2297
2364
|
if (req.method === 'GET' && req.url === '/timeline') {
|
|
2298
2365
|
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
2299
|
-
const body = timelineHtml(dir || './freya');
|
|
2366
|
+
const body = injectPort(timelineHtml(dir || './freya'));
|
|
2300
2367
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
2301
2368
|
res.end(body);
|
|
2302
2369
|
return;
|
|
@@ -2304,7 +2371,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2304
2371
|
|
|
2305
2372
|
if (req.method === 'GET' && req.url === '/settings') {
|
|
2306
2373
|
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
2307
|
-
const body = settingsHtml(dir || './freya');
|
|
2374
|
+
const body = injectPort(settingsHtml(dir || './freya'));
|
|
2308
2375
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
2309
2376
|
res.end(body);
|
|
2310
2377
|
return;
|
|
@@ -2312,7 +2379,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2312
2379
|
|
|
2313
2380
|
if (req.method === 'GET' && req.url === '/docs') {
|
|
2314
2381
|
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
|
|
2315
|
-
const body = docsHtml(dir || './freya');
|
|
2382
|
+
const body = injectPort(docsHtml(dir || './freya'));
|
|
2316
2383
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
2317
2384
|
res.end(body);
|
|
2318
2385
|
return;
|
|
@@ -2880,8 +2947,10 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2880
2947
|
const reportsDir = path.join(workspaceDir, 'docs', 'reports');
|
|
2881
2948
|
const safeReportsDir = path.resolve(reportsDir);
|
|
2882
2949
|
const safeFull = path.resolve(full);
|
|
2883
|
-
|
|
2884
|
-
|
|
2950
|
+
// BUG-07: use path.relative for traversal check (startsWith can be fooled on some OS)
|
|
2951
|
+
const rel2 = path.relative(safeReportsDir, safeFull);
|
|
2952
|
+
if (!rel2 || rel2.startsWith('..') || path.isAbsolute(rel2)) {
|
|
2953
|
+
return safeJson(res, 400, { error: 'Caminho de relatório inválido' });
|
|
2885
2954
|
}
|
|
2886
2955
|
|
|
2887
2956
|
const text = fs.readFileSync(safeFull, 'utf8');
|
|
@@ -3045,8 +3114,10 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3045
3114
|
|
|
3046
3115
|
// Best-effort: if Copilot CLI isn't available, return 200 with an explanatory plan
|
|
3047
3116
|
// so the UI can show actionable next steps instead of hard-failing.
|
|
3117
|
+
// BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
|
|
3118
|
+
const agentEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
|
|
3048
3119
|
try {
|
|
3049
|
-
const r = await run(cmd, ['-s', '--no-color', '--stream', 'off', '-p', prompt, '--allow-all-tools'], workspaceDir);
|
|
3120
|
+
const r = await run(cmd, ['-s', '--no-color', '--stream', 'off', '-p', prompt, '--allow-all-tools'], workspaceDir, agentEnv);
|
|
3050
3121
|
const out = (r.stdout + r.stderr).trim();
|
|
3051
3122
|
if (r.code !== 0) {
|
|
3052
3123
|
return safeJson(res, 200, {
|
|
@@ -3190,23 +3261,6 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3190
3261
|
if (!Array.isArray(actions) || actions.length === 0) {
|
|
3191
3262
|
return safeJson(res, 400, { error: 'Plan has no actions[]' });
|
|
3192
3263
|
}
|
|
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
3264
|
|
|
3211
3265
|
const now = new Date().toISOString();
|
|
3212
3266
|
const applyMode = String(payload.mode || 'all').trim();
|
|
@@ -3241,8 +3295,14 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3241
3295
|
const insertTask = dl.db.prepare(`INSERT INTO tasks (id, project_slug, description, category, status, metadata) VALUES (?, ?, ?, ?, ?, ?)`);
|
|
3242
3296
|
const insertBlocker = dl.db.prepare(`INSERT INTO blockers (id, project_slug, title, severity, status, owner, next_action, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
3243
3297
|
|
|
3244
|
-
//
|
|
3298
|
+
// BUG-31: Move deduplication queries INSIDE the transaction to eliminate TOCTOU race
|
|
3245
3299
|
const applyTx = dl.db.transaction((actionsToApply) => {
|
|
3300
|
+
// Query for existing keys inside the transaction for atomicity
|
|
3301
|
+
const recentTasks = dl.db.prepare("SELECT description FROM tasks WHERE created_at >= datetime('now', '-1 day')").all();
|
|
3302
|
+
const existingTaskKeys24h = new Set(recentTasks.map(t => sha1(normalizeTextForKey(t.description))));
|
|
3303
|
+
const recentBlockers = dl.db.prepare("SELECT title FROM blockers WHERE created_at >= datetime('now', '-1 day')").all();
|
|
3304
|
+
const existingBlockerKeys24h = new Set(recentBlockers.map(b => sha1(normalizeTextForKey(b.title))));
|
|
3305
|
+
|
|
3246
3306
|
for (const a of actionsToApply) {
|
|
3247
3307
|
if (!a || typeof a !== 'object') continue;
|
|
3248
3308
|
const type = String(a.type || '').trim();
|
|
@@ -3370,18 +3430,21 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3370
3430
|
}
|
|
3371
3431
|
|
|
3372
3432
|
const npmCmd = guessNpmCmd();
|
|
3433
|
+
// Pass workspace dir to all child npm scripts so their DataLayer instances
|
|
3434
|
+
// resolve the same SQLite file as the web server process.
|
|
3435
|
+
const workspaceEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
|
|
3373
3436
|
|
|
3374
3437
|
if (req.url === '/api/health') {
|
|
3375
3438
|
if (!looksLikeFreyaWorkspace(workspaceDir)) {
|
|
3376
3439
|
return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized' });
|
|
3377
3440
|
}
|
|
3378
|
-
const r = await run(npmCmd, ['run', 'health'], workspaceDir);
|
|
3441
|
+
const r = await run(npmCmd, ['run', 'health'], workspaceDir, workspaceEnv);
|
|
3379
3442
|
const output = (r.stdout + r.stderr).trim();
|
|
3380
3443
|
return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { output } : { error: output || 'health failed', output });
|
|
3381
3444
|
}
|
|
3382
3445
|
|
|
3383
3446
|
if (req.url === '/api/migrate') {
|
|
3384
|
-
const r = await run(npmCmd, ['run', 'migrate'], workspaceDir);
|
|
3447
|
+
const r = await run(npmCmd, ['run', 'migrate'], workspaceDir, workspaceEnv);
|
|
3385
3448
|
const output = (r.stdout + r.stderr).trim();
|
|
3386
3449
|
return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { output } : { error: output || 'migrate failed', output });
|
|
3387
3450
|
}
|
|
@@ -3389,13 +3452,13 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3389
3452
|
|
|
3390
3453
|
|
|
3391
3454
|
if (req.url === '/api/obsidian/export') {
|
|
3392
|
-
const r = await run(npmCmd, ['run', 'export-obsidian'], workspaceDir);
|
|
3455
|
+
const r = await run(npmCmd, ['run', 'export-obsidian'], workspaceDir, workspaceEnv);
|
|
3393
3456
|
const out = (r.stdout + r.stderr).trim();
|
|
3394
3457
|
return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { ok: true, output: out } : { error: out || 'export failed', output: out });
|
|
3395
3458
|
}
|
|
3396
3459
|
|
|
3397
3460
|
if (req.url === '/api/index/rebuild') {
|
|
3398
|
-
const r = await run(npmCmd, ['run', 'build-index'], workspaceDir);
|
|
3461
|
+
const r = await run(npmCmd, ['run', 'build-index'], workspaceDir, workspaceEnv);
|
|
3399
3462
|
const out = (r.stdout + r.stderr).trim();
|
|
3400
3463
|
return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { ok: true, output: out } : { error: out || 'index rebuild failed', output: out });
|
|
3401
3464
|
}
|
|
@@ -3435,9 +3498,11 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3435
3498
|
|
|
3436
3499
|
const cmd = process.env.COPILOT_CMD || 'copilot';
|
|
3437
3500
|
|
|
3501
|
+
// BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
|
|
3502
|
+
const oracleEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
|
|
3438
3503
|
try {
|
|
3439
3504
|
// 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);
|
|
3505
|
+
const r = await run(cmd, ['-s', '--no-color', '--stream', 'off', '-p', prompt], workspaceDir, oracleEnv);
|
|
3441
3506
|
const out = (r.stdout + r.stderr).trim();
|
|
3442
3507
|
if (r.code !== 0) {
|
|
3443
3508
|
return safeJson(res, 200, { ok: false, answer: 'Falha na busca do agente Oracle:\n' + (out || 'Exit code != 0'), sessionId });
|
|
@@ -3652,48 +3717,8 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3652
3717
|
return safeJson(res, 200, { ok: true, items, stats: { pendingTasks, openBlockers, reportsToday, reportsTotal: reports.length } });
|
|
3653
3718
|
}
|
|
3654
3719
|
|
|
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
|
-
}
|
|
3720
|
+
// BUG-12: duplicate handlers for /api/incidents/resolve, /api/incidents,
|
|
3721
|
+
// and /api/tasks/heatmap were removed here — originals remain earlier in the file.
|
|
3697
3722
|
|
|
3698
3723
|
if (req.url === '/api/blockers/summary') {
|
|
3699
3724
|
const open = dl.db.prepare(`
|
|
@@ -3808,7 +3833,15 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3808
3833
|
const script = payload.script;
|
|
3809
3834
|
if (!script) return safeJson(res, 400, { error: 'Missing script' });
|
|
3810
3835
|
|
|
3811
|
-
|
|
3836
|
+
// BUG-15: Whitelist allowed report scripts to prevent arbitrary npm run execution
|
|
3837
|
+
const ALLOWED_REPORT_SCRIPTS = new Set(['blockers', 'sm-weekly', 'status', 'daily', 'report', 'build-index', 'update-index', 'export-obsidian']);
|
|
3838
|
+
if (!ALLOWED_REPORT_SCRIPTS.has(script)) {
|
|
3839
|
+
return safeJson(res, 400, { error: 'Script não permitido: ' + script });
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
// Pass FREYA_WORKSPACE_DIR so the workspace's DataLayer uses the same
|
|
3843
|
+
// SQLite file as the web server process (fixes the two-database split).
|
|
3844
|
+
const r = await run(npmCmd, ['run', script], workspaceDir, workspaceEnv);
|
|
3812
3845
|
const out = (r.stdout + r.stderr).trim();
|
|
3813
3846
|
|
|
3814
3847
|
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`;
|