@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.
@@ -64,11 +64,8 @@ function runNpmInstall(workspaceDir) {
64
64
  env: { ...process.env }
65
65
  };
66
66
 
67
- if (process.platform === 'win32') {
68
- execSync('npm install --no-audit --no-fund', opts);
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
- // We'll execute via cmd.exe on Windows for reliability.
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
- req.on('data', (c) => chunks.push(c));
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: process.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: process.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
- await ready;
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
- if (!safeFull.startsWith(safeReportsDir + path.sep)) {
2884
- return safeJson(res, 400, { error: 'Invalid report path' });
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
- // Execute everything inside a transaction for the apply process
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
- if (req.url === '/api/incidents/resolve') {
3656
- const title = payload.title;
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
- const r = await run(npmCmd, ['run', script], workspaceDir);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "2.7.1",
3
+ "version": "2.8.3",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js && node scripts/validate-structure.js",
@@ -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
- const REPORT_DIR = path.join(__dirname, '../docs/reports');
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
- const DATA_DIR = path.join(__dirname, '../data');
8
- const LOGS_DIR = path.join(__dirname, '../logs/daily');
9
- const REPORT_DIR = path.join(__dirname, '../docs/reports');
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.getCreatedAt(t) || Date.parse(t.completedAt || t.completed_at || ''); // Use native since no safeParse helper here
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 DATA_DIR = path.join(__dirname, '../data');
18
- const LOGS_DIR = path.join(__dirname, '../logs/daily');
19
- const OUTPUT_DIR = path.join(__dirname, '../docs/reports');
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
- const DATA_DIR = path.join(__dirname, '../data');
9
- const LOGS_DIR = path.join(__dirname, '../logs/daily');
10
- const REPORT_DIR = path.join(__dirname, '../docs/reports');
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.forEach(e => {
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`;