@cccarv82/freya 2.7.1 → 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.
@@ -1,23 +1,34 @@
1
+ /**
2
+ * generate-weekly-report.js
3
+ * Generates a weekly Markdown report aggregating Tasks, Blockers, Career entries,
4
+ * and Project Updates from the SQLite database.
5
+ *
6
+ * Usage: node scripts/generate-weekly-report.js
7
+ */
8
+
1
9
  const fs = require('fs');
2
10
  const path = require('path');
3
11
 
4
- const { toIsoDate, isWithinRange } = require('./lib/date-utils');
12
+ const { toIsoDate, safeParseToMs } = require('./lib/date-utils');
13
+ const DataManager = require('./lib/DataManager');
14
+ const { ready } = require('./lib/DataLayer');
5
15
 
6
- const DATA_DIR = path.join(__dirname, '../data');
7
- const REPORT_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
8
20
 
9
- // Ensure output dir exists
10
- if (!fs.existsSync(REPORT_DIR)) {
11
- fs.mkdirSync(REPORT_DIR, { recursive: true });
12
- }
21
+ const REPORT_DIR = path.join(WORKSPACE_DIR, 'docs', 'reports');
13
22
 
14
23
  // --- Date Logic ---
15
24
  const now = new Date();
16
25
  const oneDay = 24 * 60 * 60 * 1000;
17
26
 
18
27
  function isWithinWeek(dateStr) {
19
- const sevenDaysAgo = new Date(now.getTime() - (7 * oneDay));
20
- return isWithinRange(dateStr, sevenDaysAgo, now);
28
+ const ms = safeParseToMs(dateStr);
29
+ if (!Number.isFinite(ms)) return false;
30
+ const sevenDaysAgo = now.getTime() - (7 * oneDay);
31
+ return ms >= sevenDaysAgo && ms <= now.getTime();
21
32
  }
22
33
 
23
34
  function getFormattedDate() {
@@ -31,112 +42,87 @@ function getFormattedTime() {
31
42
  return `${hh}${mm}${ss}`;
32
43
  }
33
44
 
34
- // --- File Walking ---
35
- function walk(dir, fileList = []) {
36
- const files = fs.readdirSync(dir);
37
- files.forEach(file => {
38
- const filePath = path.join(dir, file);
39
- const stat = fs.statSync(filePath);
40
- if (stat.isDirectory()) {
41
- walk(filePath, fileList);
42
- } else {
43
- if (path.extname(file) === '.json') {
44
- fileList.push(filePath);
45
- }
46
- }
47
- });
48
- return fileList;
49
- }
45
+ // --- Report Generation ---
46
+ async function generateWeeklyReport() {
47
+ await ready;
48
+
49
+ const start = new Date(now.getTime() - 7 * oneDay);
50
+ const end = now;
51
+
52
+ const dm = new DataManager();
53
+
54
+ // Fetch data from SQLite
55
+ const { completed: completedTasks } = dm.getTasks(start, end);
56
+ const { open: openBlockers, resolvedRecent } = dm.getBlockers(start, end);
57
+ const projectUpdates = dm.getProjectUpdates(start, end);
58
+ const careerEntries = dm.getCareerEntries ? dm.getCareerEntries(start, end) : [];
59
+
60
+ // Ensure output dir exists
61
+ if (!fs.existsSync(REPORT_DIR)) {
62
+ fs.mkdirSync(REPORT_DIR, { recursive: true });
63
+ }
50
64
 
51
- // --- Aggregation ---
52
- function generateWeeklyReport() {
53
- const files = walk(DATA_DIR);
54
-
55
- const projects = [];
56
- let taskLog = { schemaVersion: 1, tasks: [] };
57
- let careerLog = { entries: [] };
58
-
59
- // 1. Collect Data
60
- files.forEach(file => {
61
- try {
62
- const content = fs.readFileSync(file, 'utf8');
63
- const json = JSON.parse(content);
64
-
65
- if (file.endsWith('task-log.json')) {
66
- taskLog = json;
67
- } else if (file.endsWith('career-log.json')) {
68
- careerLog = json;
69
- } else if (file.endsWith('status.json')) {
70
- projects.push(json);
71
- }
72
- } catch (err) {
73
- console.error(`Error reading ${file}: ${err.message}`);
74
- }
75
- });
76
-
77
- // 2. Generate Content
78
65
  const reportDate = getFormattedDate();
79
66
  const reportTime = getFormattedTime();
80
67
  let report = `# Weekly Report - ${reportDate}\n\n`;
81
68
 
82
69
  // Projects
83
- report += "## 🚀 Project Updates\n";
84
- let hasProjectUpdates = false;
85
- projects.forEach(p => {
86
- if (p.history && Array.isArray(p.history)) {
87
- const recentUpdates = p.history.filter(h => isWithinWeek(h.date));
88
- if (recentUpdates.length > 0) {
89
- hasProjectUpdates = true;
90
- report += `### ${p.client} - ${p.project}\n`;
91
- recentUpdates.forEach(u => {
92
- const dateStr = u.date ? u.date.split('T')[0] : 'Unknown Date';
93
- report += `- **${dateStr}**: ${u.content}\n`;
94
- });
95
- report += "\n";
96
- }
97
- }
98
- });
99
- if (!hasProjectUpdates) report += "No project updates recorded this week.\n\n";
100
-
101
- // Tasks
102
- report += "## ✅ Completed Tasks\n";
103
- if (taskLog.tasks && Array.isArray(taskLog.tasks)) {
104
- const recentTasks = taskLog.tasks.filter(t => t.status === "COMPLETED" && isWithinWeek(t.completedAt));
105
- if (recentTasks.length > 0) {
106
- recentTasks.forEach(t => {
107
- report += `- ${t.description}\n`;
70
+ report += '## Project Updates\n';
71
+ if (projectUpdates.length > 0) {
72
+ projectUpdates.forEach(p => {
73
+ report += `### ${p.client || 'Unknown'} - ${p.project || p.slug || 'Unknown'}\n`;
74
+ const events = Array.isArray(p.events) ? p.events : [];
75
+ events.forEach(e => {
76
+ const dateStr = e.date ? String(e.date).slice(0, 10) : 'Unknown Date';
77
+ report += `- **${dateStr}**: ${e.content || ''}\n`;
108
78
  });
109
- } else {
110
- report += "No tasks completed this week.\n";
111
- }
79
+ report += '\n';
80
+ });
112
81
  } else {
113
- report += "No task log found.\n";
82
+ report += 'No project updates recorded this week.\n\n';
114
83
  }
115
- report += "\n";
116
-
117
- // Career
118
- report += "## 🌟 Career Highlights\n";
119
- if (careerLog.entries && Array.isArray(careerLog.entries)) {
120
- const recentCareer = careerLog.entries.filter(e => isWithinWeek(e.date));
121
- if (recentCareer.length > 0) {
122
- recentCareer.forEach(e => {
123
- report += `- **[${e.type}]**: ${e.description}\n`;
124
- });
125
- } else {
126
- report += "No career updates this week.\n";
127
- }
84
+
85
+ // Completed Tasks
86
+ report += '## Completed Tasks\n';
87
+ if (completedTasks.length > 0) {
88
+ completedTasks.forEach(t => {
89
+ const projectTag = t.projectSlug || t.project_slug ? `[${t.projectSlug || t.project_slug}] ` : '';
90
+ report += `- ${projectTag}${t.description}\n`;
91
+ });
128
92
  } else {
129
- report += "No career log found.\n";
93
+ report += 'No tasks completed this week.\n';
94
+ }
95
+ report += '\n';
96
+
97
+ // Open Blockers
98
+ report += '## Open Blockers\n';
99
+ if (openBlockers.length > 0) {
100
+ openBlockers.forEach(b => {
101
+ const sev = b.severity ? `[${b.severity}] ` : '';
102
+ report += `- ${sev}${b.title}\n`;
103
+ });
104
+ } else {
105
+ report += 'No open blockers.\n';
106
+ }
107
+ report += '\n';
108
+
109
+ // Career entries (if DataManager supports it)
110
+ if (Array.isArray(careerEntries) && careerEntries.length > 0) {
111
+ report += '## Career Highlights\n';
112
+ careerEntries.forEach(e => {
113
+ report += `- **[${e.type || 'Note'}]**: ${e.description || e.content || ''}\n`;
114
+ });
115
+ report += '\n';
130
116
  }
131
117
 
132
118
  // 3. Save and Output
133
119
  const outputPath = path.join(REPORT_DIR, `weekly-${reportDate}-${reportTime}.md`);
134
120
  fs.writeFileSync(outputPath, report);
135
-
136
- console.log(`✅ Report generated at: ${outputPath}`);
137
- console.log("---------------------------------------------------");
121
+
122
+ console.log(`Report generated at: ${outputPath}`);
123
+ console.log('---------------------------------------------------');
138
124
  console.log(report);
139
- console.log("---------------------------------------------------");
125
+ console.log('---------------------------------------------------');
140
126
  }
141
127
 
142
- generateWeeklyReport();
128
+ generateWeeklyReport().catch(err => { console.error(err); process.exit(1); });
@@ -165,6 +165,15 @@ class SqlJsDatabase {
165
165
  };
166
166
  }
167
167
 
168
+ /**
169
+ * Public save — explicitly flush the in-memory database to disk.
170
+ * Use this when you need to ensure data is persisted before spawning a
171
+ * child process that will read the same SQLite file (e.g. in tests).
172
+ */
173
+ save() {
174
+ this._save();
175
+ }
176
+
168
177
  /**
169
178
  * Close the database.
170
179
  */
@@ -181,7 +190,13 @@ class SqlJsDatabase {
181
190
  class DataLayer {
182
191
  constructor(dbPath = null) {
183
192
  if (!dbPath) {
184
- const dataDir = path.join(__dirname, '..', '..', 'data');
193
+ // Prefer FREYA_WORKSPACE_DIR env var so that the web server and report
194
+ // child processes always share the same SQLite file regardless of where
195
+ // each DataLayer.js lives on disk.
196
+ const workspaceDir = process.env.FREYA_WORKSPACE_DIR
197
+ ? path.resolve(process.env.FREYA_WORKSPACE_DIR)
198
+ : path.join(__dirname, '..', '..');
199
+ const dataDir = path.join(workspaceDir, 'data');
185
200
  if (!fs.existsSync(dataDir)) {
186
201
  fs.mkdirSync(dataDir, { recursive: true });
187
202
  }
@@ -332,4 +347,35 @@ const ready = defaultInstance.init().catch(err => {
332
347
  process.exit(1);
333
348
  });
334
349
 
335
- module.exports = { defaultInstance, DataLayer, ready };
350
+ /**
351
+ * Configure the singleton to use a specific workspace directory.
352
+ * Call this before performing any DB operations against the target workspace.
353
+ * Closes the current in-memory DB (if open) and reinitializes from the new path.
354
+ *
355
+ * This is used by the web server to ensure it uses the same SQLite file as
356
+ * report child processes (both pointed at {workspaceDir}/data/freya.sqlite).
357
+ *
358
+ * @param {string} workspaceDir Absolute path to the Freya workspace directory.
359
+ * @returns {Promise<void>} Resolves when the new DB is ready.
360
+ */
361
+ async function configure(workspaceDir) {
362
+ const newDbPath = path.join(path.resolve(workspaceDir), 'data', 'freya.sqlite');
363
+ if (defaultInstance._dbPath === newDbPath && defaultInstance.db) {
364
+ // Already pointing at the right file.
365
+ return;
366
+ }
367
+ // Close existing connection if open.
368
+ if (defaultInstance.db) {
369
+ try { defaultInstance.db.close(); } catch { /* ignore */ }
370
+ defaultInstance.db = null;
371
+ }
372
+ // Set the new path and reinitialize.
373
+ defaultInstance._dbPath = newDbPath;
374
+ const dataDir = path.dirname(newDbPath);
375
+ if (!fs.existsSync(dataDir)) {
376
+ fs.mkdirSync(dataDir, { recursive: true });
377
+ }
378
+ await defaultInstance.init();
379
+ }
380
+
381
+ module.exports = { defaultInstance, DataLayer, ready, configure };
@@ -25,7 +25,13 @@ class DataManager {
25
25
  // --- Tasks ---
26
26
  getTasksRaw() {
27
27
  return dl.db.prepare('SELECT * FROM tasks').all().map(t => {
28
- const meta = t.metadata ? JSON.parse(t.metadata) : {};
28
+ // BUG-17: JSON.parse can throw on corrupted metadata; default to {}
29
+ let meta = {};
30
+ try {
31
+ meta = t.metadata ? JSON.parse(t.metadata) : {};
32
+ } catch {
33
+ meta = {};
34
+ }
29
35
  return {
30
36
  id: t.id,
31
37
  projectSlug: t.project_slug,
@@ -64,7 +70,13 @@ class DataManager {
64
70
  // --- Blockers ---
65
71
  getBlockersRaw() {
66
72
  return dl.db.prepare('SELECT * FROM blockers').all().map(b => {
67
- const meta = b.metadata ? JSON.parse(b.metadata) : {};
73
+ // BUG-17: JSON.parse can throw on corrupted metadata; default to {}
74
+ let meta = {};
75
+ try {
76
+ meta = b.metadata ? JSON.parse(b.metadata) : {};
77
+ } catch {
78
+ meta = {};
79
+ }
68
80
  return {
69
81
  id: b.id,
70
82
  projectSlug: b.project_slug,
@@ -160,9 +172,17 @@ class DataManager {
160
172
 
161
173
  // --- Daily Logs ---
162
174
  getDailyLogs(start, end) {
163
- function toIso(d) { return (d instanceof Date ? d : new Date(d)).toISOString().slice(0, 10); }
164
- const startIso = formatDate ? formatDate(start) : toIso(start);
165
- const endIso = formatDate ? formatDate(end) : toIso(end);
175
+ // BUG-41: use local date parts instead of toISOString().slice(0,10) to avoid
176
+ // timezone drift (UTC midnight may differ from local midnight)
177
+ function toLocalDate(d) {
178
+ const dt = d instanceof Date ? d : new Date(d);
179
+ const yyyy = dt.getFullYear();
180
+ const mm = String(dt.getMonth() + 1).padStart(2, '0');
181
+ const dd = String(dt.getDate()).padStart(2, '0');
182
+ return `${yyyy}-${mm}-${dd}`;
183
+ }
184
+ const startIso = formatDate ? formatDate(start) : toLocalDate(start);
185
+ const endIso = formatDate ? formatDate(end) : toLocalDate(end);
166
186
 
167
187
  return dl.db.prepare(`
168
188
  SELECT date, raw_markdown as content FROM daily_logs
@@ -171,6 +191,43 @@ class DataManager {
171
191
  `).all(startIso, endIso);
172
192
  }
173
193
 
194
+ // --- Timestamp helpers ---
195
+ // These accept a record (task or blocker) and return milliseconds since epoch, or NaN.
196
+
197
+ getCreatedAt(record) {
198
+ const { safeParseToMs } = require('./date-utils');
199
+ const candidates = [
200
+ record.createdAt,
201
+ record.created_at,
202
+ record.openedAt,
203
+ record.opened_at,
204
+ record.date,
205
+ record.loggedAt,
206
+ ];
207
+ for (const value of candidates) {
208
+ const ms = safeParseToMs(value);
209
+ if (Number.isFinite(ms)) return ms;
210
+ }
211
+ return NaN;
212
+ }
213
+
214
+ getResolvedAt(record) {
215
+ const { safeParseToMs } = require('./date-utils');
216
+ const candidates = [
217
+ record.resolvedAt,
218
+ record.resolved_at,
219
+ record.closedAt,
220
+ record.closed_at,
221
+ record.completedAt,
222
+ record.completed_at,
223
+ ];
224
+ for (const value of candidates) {
225
+ const ms = safeParseToMs(value);
226
+ if (Number.isFinite(ms)) return ms;
227
+ }
228
+ return NaN;
229
+ }
230
+
174
231
  // --- RAG (Vector Search) ---
175
232
  async semanticSearch(query, topK = 10) {
176
233
  const queryVector = await defaultEmbedder.embedText(query);
@@ -8,7 +8,8 @@ const ID_PATTERNS = [
8
8
  ];
9
9
 
10
10
  const TEXT_EXTS = new Set(['.md', '.txt', '.log', '.json', '.yaml', '.yml']);
11
- const TOKEN_RE = /[A-Za-z0-9_-]{3,}/g;
11
+ // BUG-46: TOKEN_RE was a module-level global with /g flag — its lastIndex would
12
+ // persist between calls causing random skipped tokens. Moved inside the function.
12
13
 
13
14
  const DEFAULT_MAX_SIZE = 2 * 1024 * 1024;
14
15
  const DEFAULT_TOKEN_LIMIT = 500;
@@ -142,6 +143,8 @@ function extractIdMatches(text) {
142
143
  }
143
144
 
144
145
  function extractKeywordIndexMap(textLower, tokenLimit) {
146
+ // BUG-46: create a fresh regex each call — module-level /g regex retains lastIndex
147
+ const TOKEN_RE = /[A-Za-z0-9_-]{3,}/g;
145
148
  const map = new Map();
146
149
  let m;
147
150
  while ((m = TOKEN_RE.exec(textLower)) !== null) {
@@ -151,7 +154,6 @@ function extractKeywordIndexMap(textLower, tokenLimit) {
151
154
  if (map.size >= tokenLimit) break;
152
155
  }
153
156
  }
154
- TOKEN_RE.lastIndex = 0;
155
157
  return map;
156
158
  }
157
159
 
@@ -3,9 +3,16 @@ const path = require('path');
3
3
 
4
4
  const { safeReadJson, quarantineCorruptedFile } = require('./lib/fs-utils');
5
5
 
6
+ // BUG-43: support FREYA_WORKSPACE_DIR (used by web server) in addition to DATA_DIR
7
+ const WORKSPACE_DIR = process.env.FREYA_WORKSPACE_DIR
8
+ ? path.resolve(process.env.FREYA_WORKSPACE_DIR)
9
+ : null;
10
+
6
11
  const DATA_DIR = process.env.DATA_DIR
7
12
  ? path.resolve(process.env.DATA_DIR)
8
- : path.join(__dirname, '../data');
13
+ : WORKSPACE_DIR
14
+ ? path.join(WORKSPACE_DIR, 'data')
15
+ : path.join(__dirname, '../data');
9
16
 
10
17
  const KNOWN_FILES = [
11
18
  { relPath: path.join('tasks', 'task-log.json'), label: 'tasks/task-log.json' },
@@ -4,7 +4,12 @@ const path = require('path');
4
4
  const { safeReadJson, quarantineCorruptedFile } = require('./lib/fs-utils');
5
5
  const SCHEMA = require('./lib/schema');
6
6
 
7
- const DATA_DIR = path.join(__dirname, '../data');
7
+ // BUG-30: use FREYA_WORKSPACE_DIR so the web server uses the correct workspace
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');
8
13
 
9
14
  // --- Validation Helpers ---
10
15
 
@@ -173,8 +178,11 @@ function validateData() {
173
178
  // Route validation based on filename/path
174
179
  if (file.endsWith('career-log.json')) {
175
180
  fileErrors = validateCareerLog(json, relativePath);
176
- } else if (file.endsWith('task-log.json') || file.endsWith('status.json') || file.endsWith('blocker-log.json')) {
177
- // Obsoleted by SQLite, ignore
181
+ } else if (file.endsWith('task-log.json') || file.endsWith('blocker-log.json')) {
182
+ // BUG-06: These are legacy JSON files superseded by SQLite storage.
183
+ // They are no longer the source of truth; validation is skipped to avoid
184
+ // false positives on empty/stub files left behind after migration.
185
+ console.log(`ℹ️ [${relativePath}] Legacy file (superseded by SQLite). Skipping schema validation.`);
178
186
  } else {
179
187
  // Optional: warn about unknown files, or ignore
180
188
  // console.warn(`⚠️ [${relativePath}] Unknown JSON file type. Skipping schema validation.`);
@@ -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 workspace 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 workspace 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
  });
@@ -2,7 +2,7 @@
2
2
  * generate-executive-report.js
3
3
  * Generates a professional Markdown status report (Daily/Weekly)
4
4
  * aggregating Tasks, Project Updates, and Daily Logs.
5
- *
5
+ *
6
6
  * Usage: node scripts/generate-executive-report.js --period [daily|weekly]
7
7
  */
8
8
 
@@ -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 workspace 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 workspace 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`;