@cccarv82/freya 2.3.7 → 2.3.8

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.
Files changed (3) hide show
  1. package/cli/web-ui.js +72 -0
  2. package/cli/web.js +189 -7
  3. package/package.json +1 -1
package/cli/web-ui.js CHANGED
@@ -1268,6 +1268,74 @@
1268
1268
  }
1269
1269
  }
1270
1270
 
1271
+ async function refreshQualityScore() {
1272
+ const el = $('qualityScoreCard');
1273
+ if (el) el.innerHTML = '<div class="help">Carregando score...</div>';
1274
+ try {
1275
+ const r = await api('/api/quality/score', { dir: dirOrDefault() });
1276
+ if (!el) return;
1277
+ if (r && r.needsInit) {
1278
+ el.innerHTML = '<div class="help">Workspace não inicializado.</div>';
1279
+ return;
1280
+ }
1281
+ const score = (r && typeof r.score === 'number') ? r.score : null;
1282
+ const breakdown = (r && r.breakdown) ? r.breakdown : {};
1283
+ const threshold = 90;
1284
+ const status = (score !== null && score >= threshold) ? 'ok' : 'warn';
1285
+
1286
+ const line = (label, data, keyLabel) => {
1287
+ if (!data) return '';
1288
+ const pct = (typeof data.pct === 'number') ? `${data.pct}%` : 'n/a';
1289
+ const detail = keyLabel ? `${data[keyLabel] || 0}/${data.total || 0}` : `${data.total || 0}`;
1290
+ return `<div class=\"help\" style=\"margin-top:4px\"><b>${escapeHtml(label)}:</b> ${escapeHtml(pct)} (${escapeHtml(detail)})</div>`;
1291
+ };
1292
+
1293
+ const html = `<div style=\"display:flex; justify-content:space-between; gap:10px; align-items:center\">`
1294
+ + `<div style=\"min-width:0\"><div style=\"font-weight:800\">${score === null ? 'Sem score' : `Score: ${score}%`}</div>`
1295
+ + `${line('Tasks com projectSlug', breakdown.tasks, 'withProjectSlug')}`
1296
+ + `${line('Status com history', breakdown.status, 'withHistory')}`
1297
+ + `${line('Blockers com projectSlug', breakdown.blockers, 'withProjectSlug')}`
1298
+ + `</div>`
1299
+ + `<div class=\"pill ${status}\">${status}</div>`
1300
+ + `</div>`;
1301
+ el.innerHTML = html;
1302
+ } catch {
1303
+ if (el) el.innerHTML = '<div class="help">Falha ao carregar score.</div>';
1304
+ }
1305
+ }
1306
+
1307
+ async function refreshRiskSummary() {
1308
+ const el = $('riskSummary');
1309
+ if (el) el.innerHTML = '<div class="help">Carregando riscos...</div>';
1310
+ try {
1311
+ const r = await api('/api/risk/summary', { dir: dirOrDefault() });
1312
+ if (!el) return;
1313
+ if (r && r.needsInit) {
1314
+ el.innerHTML = '<div class="help">Workspace não inicializado.</div>';
1315
+ return;
1316
+ }
1317
+ const items = Array.isArray(r.items) ? r.items : [];
1318
+ if (!items.length) {
1319
+ el.innerHTML = '<div class="help">Sem riscos relevantes.</div>';
1320
+ return;
1321
+ }
1322
+ const rows = items.map((it) => {
1323
+ const age = (it.oldestBlockerDays != null) ? `${it.oldestBlockerDays}d` : 'n/a';
1324
+ return `<div class=\"rep\">`
1325
+ + `<div style=\"display:flex; justify-content:space-between; gap:10px; align-items:center\">`
1326
+ + `<div style=\"min-width:0\"><div style=\"font-weight:800\">${escapeHtml(it.slug || '')}</div>`
1327
+ + `<div class=\"help\" style=\"margin-top:4px\">Pendentes: ${escapeHtml(String(it.pendingTasks || 0))} · Blockers 7d+: ${escapeHtml(String(it.oldBlockers || 0))} · Mais antigo: ${escapeHtml(age)}</div>`
1328
+ + `</div>`
1329
+ + `<div class=\"pill warn\">risco</div>`
1330
+ + `</div>`
1331
+ + `</div>`;
1332
+ }).join('');
1333
+ el.innerHTML = rows;
1334
+ } catch {
1335
+ if (el) el.innerHTML = '<div class="help">Falha ao carregar riscos.</div>';
1336
+ }
1337
+ }
1338
+
1271
1339
  async function refreshExecutiveSummary() {
1272
1340
  const el = $('executiveSummary');
1273
1341
  if (el) el.textContent = 'Carregando resumo...';
@@ -1706,8 +1774,10 @@
1706
1774
 
1707
1775
  if (isCompanionPage) {
1708
1776
  await refreshHealthChecklist();
1777
+ await refreshQualityScore();
1709
1778
  await refreshExecutiveSummary();
1710
1779
  await refreshAnomalies();
1780
+ await refreshRiskSummary();
1711
1781
  await refreshIncidents();
1712
1782
  await refreshHeatmap();
1713
1783
  return;
@@ -1759,8 +1829,10 @@
1759
1829
  window.setTimelineKind = setTimelineKind;
1760
1830
  window.refreshBlockersInsights = refreshBlockersInsights;
1761
1831
  window.refreshHealthChecklist = refreshHealthChecklist;
1832
+ window.refreshQualityScore = refreshQualityScore;
1762
1833
  window.refreshExecutiveSummary = refreshExecutiveSummary;
1763
1834
  window.refreshAnomalies = refreshAnomalies;
1835
+ window.refreshRiskSummary = refreshRiskSummary;
1764
1836
  window.copyOut = copyOut;
1765
1837
  window.copyPath = copyPath;
1766
1838
  window.openSelected = openSelected;
package/cli/web.js CHANGED
@@ -1499,6 +1499,16 @@ function buildCompanionHtml(safeDefault, appVersion) {
1499
1499
 
1500
1500
  <section class="reportsGrid" id="healthChecklist"></section>
1501
1501
 
1502
+ <section class="panel" style="margin-top:16px">
1503
+ <div class="panelHead" style="display:flex; align-items:center; justify-content:space-between; gap:10px">
1504
+ <b>Qualidade de Log</b>
1505
+ <button class="btn small" type="button" onclick="refreshQualityScore()">Atualizar</button>
1506
+ </div>
1507
+ <div class="panelBody">
1508
+ <div id="qualityScoreCard"></div>
1509
+ </div>
1510
+ </section>
1511
+
1502
1512
  <section class="panel" style="margin-top:16px">
1503
1513
  <div class="panelHead" style="display:flex; align-items:center; justify-content:space-between; gap:10px">
1504
1514
  <b>Resumo Executivo</b>
@@ -1519,6 +1529,16 @@ function buildCompanionHtml(safeDefault, appVersion) {
1519
1529
  </div>
1520
1530
  </section>
1521
1531
 
1532
+ <section class="panel" style="margin-top:16px">
1533
+ <div class="panelHead" style="display:flex; align-items:center; justify-content:space-between; gap:10px">
1534
+ <b>Resumo de Risco</b>
1535
+ <button class="btn small" type="button" onclick="refreshRiskSummary()">Atualizar</button>
1536
+ </div>
1537
+ <div class="panelBody">
1538
+ <div id="riskSummary"></div>
1539
+ </div>
1540
+ </section>
1541
+
1522
1542
  <section class="panel" style="margin-top:16px">
1523
1543
  <div class="panelHead"><b>Incident Radar</b></div>
1524
1544
  <div class="panelBody">
@@ -2112,6 +2132,136 @@ if (req.url === '/api/timeline') {
2112
2132
  return safeJson(res, 200, { ok: true, summary, stats: { recent: recent.length, openBlockers: openBlockers.length, ...counts } });
2113
2133
  }
2114
2134
 
2135
+ if (req.url === '/api/quality/score') {
2136
+ if (!looksLikeFreyaWorkspace(workspaceDir)) {
2137
+ return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized' });
2138
+ }
2139
+
2140
+ const pct = (count, total) => (total > 0 ? Math.round((count / total) * 1000) / 10 : null);
2141
+
2142
+ // Tasks with projectSlug
2143
+ let tasksTotal = 0;
2144
+ let tasksWithProject = 0;
2145
+ const taskFile = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
2146
+ if (exists(taskFile)) {
2147
+ const taskDoc = readJsonOrNull(taskFile) || { tasks: [] };
2148
+ const tasks = Array.isArray(taskDoc.tasks) ? taskDoc.tasks : [];
2149
+ tasksTotal = tasks.length;
2150
+ for (const t of tasks) {
2151
+ const slug = String(t && t.projectSlug ? t.projectSlug : '').trim();
2152
+ if (slug) tasksWithProject++;
2153
+ }
2154
+ }
2155
+
2156
+ // Status files with history array
2157
+ let statusTotal = 0;
2158
+ let statusWithHistory = 0;
2159
+ const base = path.join(workspaceDir, 'data', 'Clients');
2160
+ if (exists(base)) {
2161
+ const stack = [base];
2162
+ while (stack.length) {
2163
+ const dirp = stack.pop();
2164
+ const entries = fs.readdirSync(dirp, { withFileTypes: true });
2165
+ for (const ent of entries) {
2166
+ const full = path.join(dirp, ent.name);
2167
+ if (ent.isDirectory()) stack.push(full);
2168
+ else if (ent.isFile() && ent.name === 'status.json') {
2169
+ statusTotal++;
2170
+ const doc = readJsonOrNull(full) || {};
2171
+ if (Array.isArray(doc.history)) statusWithHistory++;
2172
+ }
2173
+ }
2174
+ }
2175
+ }
2176
+
2177
+ // Blockers with projectSlug
2178
+ let blockersTotal = 0;
2179
+ let blockersWithProject = 0;
2180
+ const blockersFile = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
2181
+ if (exists(blockersFile)) {
2182
+ const blockersDoc = readJsonOrNull(blockersFile) || { blockers: [] };
2183
+ const blockers = Array.isArray(blockersDoc.blockers) ? blockersDoc.blockers : [];
2184
+ blockersTotal = blockers.length;
2185
+ for (const b of blockers) {
2186
+ const slug = String(b && b.projectSlug ? b.projectSlug : '').trim();
2187
+ if (slug) blockersWithProject++;
2188
+ }
2189
+ }
2190
+
2191
+ const breakdown = {
2192
+ tasks: { total: tasksTotal, withProjectSlug: tasksWithProject, pct: pct(tasksWithProject, tasksTotal) },
2193
+ status: { total: statusTotal, withHistory: statusWithHistory, pct: pct(statusWithHistory, statusTotal) },
2194
+ blockers: { total: blockersTotal, withProjectSlug: blockersWithProject, pct: pct(blockersWithProject, blockersTotal) }
2195
+ };
2196
+
2197
+ const scoreParts = [breakdown.tasks.pct, breakdown.status.pct, breakdown.blockers.pct].filter((v) => typeof v === 'number');
2198
+ const score = scoreParts.length ? Math.round((scoreParts.reduce((a, b) => a + b, 0) / scoreParts.length) * 10) / 10 : null;
2199
+
2200
+ return safeJson(res, 200, { ok: true, score, breakdown });
2201
+ }
2202
+
2203
+ if (req.url === '/api/risk/summary') {
2204
+ if (!looksLikeFreyaWorkspace(workspaceDir)) {
2205
+ return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized' });
2206
+ }
2207
+
2208
+ const pendingThreshold = 5;
2209
+ const daysThreshold = 7;
2210
+ const now = Date.now();
2211
+
2212
+ const pendingByProject = {};
2213
+ const taskFile = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
2214
+ if (exists(taskFile)) {
2215
+ const taskDoc = readJsonOrNull(taskFile) || { tasks: [] };
2216
+ const tasks = Array.isArray(taskDoc.tasks) ? taskDoc.tasks : [];
2217
+ for (const t of tasks) {
2218
+ if (!t || t.status === 'COMPLETED') continue;
2219
+ const slug = String(t.projectSlug || '').trim();
2220
+ if (!slug) continue;
2221
+ pendingByProject[slug] = (pendingByProject[slug] || 0) + 1;
2222
+ }
2223
+ }
2224
+
2225
+ const blockersByProject = {};
2226
+ const oldestByProject = {};
2227
+ const blockerFile = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
2228
+ if (exists(blockerFile)) {
2229
+ const blockerDoc = readJsonOrNull(blockerFile) || { blockers: [] };
2230
+ const blockers = Array.isArray(blockerDoc.blockers) ? blockerDoc.blockers : [];
2231
+ for (const b of blockers) {
2232
+ if (!b) continue;
2233
+ const status = String(b.status || '').toUpperCase();
2234
+ if (status !== 'OPEN' && status !== 'MITIGATING') continue;
2235
+ const slug = String(b.projectSlug || '').trim();
2236
+ if (!slug) continue;
2237
+ const createdAt = b.createdAt ? Date.parse(b.createdAt) : null;
2238
+ if (!createdAt) continue;
2239
+ const ageDays = Math.floor((now - createdAt) / (24 * 60 * 60 * 1000));
2240
+ if (ageDays < daysThreshold) continue;
2241
+ blockersByProject[slug] = (blockersByProject[slug] || 0) + 1;
2242
+ if (oldestByProject[slug] == null || ageDays > oldestByProject[slug]) oldestByProject[slug] = ageDays;
2243
+ }
2244
+ }
2245
+
2246
+ const projects = new Set([...Object.keys(pendingByProject), ...Object.keys(blockersByProject)]);
2247
+ const items = [];
2248
+ for (const slug of projects) {
2249
+ const pending = pendingByProject[slug] || 0;
2250
+ const oldBlockers = blockersByProject[slug] || 0;
2251
+ const oldestDays = oldestByProject[slug] || null;
2252
+ if (pending <= pendingThreshold && oldBlockers === 0) continue;
2253
+ items.push({ slug, pendingTasks: pending, oldBlockers, oldestBlockerDays: oldestDays });
2254
+ }
2255
+
2256
+ items.sort((a, b) => {
2257
+ if (b.oldBlockers !== a.oldBlockers) return b.oldBlockers - a.oldBlockers;
2258
+ if (b.pendingTasks !== a.pendingTasks) return b.pendingTasks - a.pendingTasks;
2259
+ return (b.oldestBlockerDays || 0) - (a.oldestBlockerDays || 0);
2260
+ });
2261
+
2262
+ return safeJson(res, 200, { ok: true, items: items.slice(0, 5), threshold: { pending: pendingThreshold, days: daysThreshold } });
2263
+ }
2264
+
2115
2265
  if (req.url === '/api/anomalies') {
2116
2266
  const anomalies = {
2117
2267
  tasksMissingProject: { count: 0, samples: [] },
@@ -2269,6 +2419,7 @@ if (req.url === '/api/reports/list') {
2269
2419
  const lc = textInput.toLowerCase();
2270
2420
  const projectsDir = path.join(workspaceDir, 'docs', 'projects');
2271
2421
  const links = [];
2422
+ const slugs = [];
2272
2423
 
2273
2424
  if (exists(projectsDir)) {
2274
2425
  const files = fs.readdirSync(projectsDir).filter((f) => f.endsWith('.md'));
@@ -2278,7 +2429,10 @@ if (req.url === '/api/reports/list') {
2278
2429
  const txt = fs.readFileSync(full, 'utf8');
2279
2430
  const m = txt.match(/DataPath:\s*data\/Clients\/(.+?)\//i);
2280
2431
  const slug = m ? m[1].toLowerCase() : name.toLowerCase();
2281
- if (lc.includes(slug)) links.push('[[' + name + ']]');
2432
+ if (lc.includes(slug)) {
2433
+ links.push('[[' + name + ']]');
2434
+ slugs.push(slug);
2435
+ }
2282
2436
  }
2283
2437
  }
2284
2438
 
@@ -2293,17 +2447,21 @@ if (req.url === '/api/reports/list') {
2293
2447
  if (ent.isDirectory()) stack.push(full);
2294
2448
  else if (ent.isFile() && ent.name === 'status.json') {
2295
2449
  const slug = path.relative(base, path.dirname(full)).replace(/\\/g, '/').toLowerCase();
2296
- if (lc.includes(slug)) links.push('[[' + slug + ']]');
2450
+ if (lc.includes(slug)) {
2451
+ links.push('[[' + slug + ']]');
2452
+ slugs.push(slug);
2453
+ }
2297
2454
  }
2298
2455
  }
2299
2456
  }
2300
2457
  }
2301
2458
 
2302
- const uniq = Array.from(new Set(links));
2303
- if (!uniq.length) return '';
2304
- return '\n\nLinks: ' + uniq.join(' ');
2459
+ const uniqLinks = Array.from(new Set(links));
2460
+ const uniqSlugs = Array.from(new Set(slugs));
2461
+ const linksText = uniqLinks.length ? ('\n\nLinks: ' + uniqLinks.join(' ')) : '';
2462
+ return { linksText, slugs: uniqSlugs };
2305
2463
  } catch {
2306
- return '';
2464
+ return { linksText: '', slugs: [] };
2307
2465
  }
2308
2466
  }
2309
2467
 
@@ -2319,9 +2477,33 @@ if (req.url === '/api/reports/list') {
2319
2477
  const hh = String(stamp.getHours()).padStart(2, '0');
2320
2478
  const mm = String(stamp.getMinutes()).padStart(2, '0');
2321
2479
 
2322
- const block = `\n\n## [${hh}:${mm}] Raw Input\n${text}\n`;
2480
+ const linkInfo = autoLinkNotes(text);
2481
+ const linksText = linkInfo && linkInfo.linksText ? linkInfo.linksText : '';
2482
+ const slugs = linkInfo && Array.isArray(linkInfo.slugs) ? linkInfo.slugs : [];
2483
+
2484
+ const block = `\n\n## [${hh}:${mm}] Raw Input\n${text}${linksText}\n`;
2323
2485
  fs.appendFileSync(file, block, 'utf8');
2324
2486
 
2487
+ if (slugs.length) {
2488
+ const logRel = path.relative(workspaceDir, file).replace(/\\/g, '/');
2489
+ const stampText = `${d} ${hh}:${mm}`;
2490
+ for (const slug of slugs) {
2491
+ const statusPath = path.join(workspaceDir, 'data', 'Clients', slug, 'status.json');
2492
+ if (!exists(statusPath)) continue;
2493
+ const doc = readJsonOrNull(statusPath) || { history: [] };
2494
+ if (!Array.isArray(doc.history)) doc.history = [];
2495
+ const already = doc.history.some((h) => h && (String(h.source || '').includes(logRel) || String(h.content || '').includes(logRel)));
2496
+ if (already) continue;
2497
+ doc.history.push({
2498
+ date: isoNow(),
2499
+ type: 'Log',
2500
+ content: `Log entry ${stampText} (${logRel})`,
2501
+ source: logRel
2502
+ });
2503
+ writeJson(statusPath, doc);
2504
+ }
2505
+ }
2506
+
2325
2507
  return safeJson(res, 200, { ok: true, file: path.relative(workspaceDir, file).replace(/\\/g, '/'), appended: true });
2326
2508
  }
2327
2509
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "2.3.7",
3
+ "version": "2.3.8",
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",