@cccarv82/freya 2.3.7 → 2.3.9
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/README.md +15 -4
- package/cli/web-ui.css +1 -1
- package/cli/web-ui.js +72 -1
- package/cli/web.js +190 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,23 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
> **Sua Assistente de Produtividade Local-First no navegador.**
|
|
4
4
|
|
|
5
|
-
F.R.E.Y.A. é um sistema de agentes de IA projetado para organizar seu trabalho, gerenciar status de projetos, rastrear tarefas e registrar sua evolução de carreira, tudo através de uma interface
|
|
5
|
+
F.R.E.Y.A. é um sistema de agentes de IA projetado para organizar seu trabalho, gerenciar status de projetos, rastrear tarefas e registrar sua evolução de carreira, tudo através de uma interface web local no navegador.
|
|
6
6
|
|
|
7
7
|
## 🌟 Principais Recursos
|
|
8
8
|
|
|
9
|
+
* **Web local (apenas navegador):** Acesso 100% via UI web local, sem app desktop e sem cloud.
|
|
9
10
|
* **Ingestão Universal:** Registre updates, blockers e notas mentais em linguagem natural.
|
|
10
11
|
* **Gestão de Tarefas:** Crie, liste e conclua tarefas ("Lembre-me de fazer X", "Minhas tarefas", "Terminei X").
|
|
12
|
+
* **Timeline + Projetos:** Linha do tempo com filtros por tag, projeto e tipo, além de listagem de projetos.
|
|
13
|
+
* **Companion Panels:** Painel rápido com qualidade de log, resumo executivo, anomalias e resumo de risco.
|
|
14
|
+
* **Incident Radar:** Card de incidentes com status e ação de “marcar resolvido”.
|
|
15
|
+
* **Task Heatmap:** Visão por projeto com prioridade, contadores e link direto para status.
|
|
16
|
+
* **Relatórios Automatizados:** Gere resumos semanais, dailies, relatório de Scrum Master e relatórios executivos.
|
|
11
17
|
* **Oráculo:** Pergunte sobre o status de qualquer projeto ("Como está o projeto X?") e recupere logs diários ("O que anotei ontem?").
|
|
12
18
|
* **Career Coach:** Gere "Brag Sheets" automáticas para suas avaliações de desempenho.
|
|
13
|
-
* **
|
|
14
|
-
* **Blockers & Riscos:** Gere um relatório rápido de blockers priorizados por severidade.
|
|
19
|
+
* **Links Bidirecionais:** Auto-link entre notas, tarefas e projetos (compatível com Obsidian).
|
|
15
20
|
* **Saúde do Sistema:** Valide a integridade dos seus dados locais com um comando.
|
|
16
21
|
* **Git Automation:** Gere commits inteligentes automaticamente. A Freya analisa suas mudanças e escreve a mensagem para você.
|
|
17
22
|
* **Privacidade Total:** Seus dados (JSON e Markdown) ficam 100% locais na sua máquina.
|
|
18
23
|
|
|
19
24
|
## 📦 Instalação (Web UI)
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
FREYA web
|
|
27
|
+
|
|
28
|
+
A FREYA roda como um app web local. Basta iniciar o servidor e abrir o navegador.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx @cccarv82/freya@latest --no-open --port 3872
|
|
32
|
+
```
|
|
22
33
|
|
|
23
34
|
## 🚢 Publicação no npm (maintainers)
|
|
24
35
|
|
package/cli/web-ui.css
CHANGED
|
@@ -374,7 +374,7 @@ h1 { margin: 0; font-size: 22px; letter-spacing: -.02em; }
|
|
|
374
374
|
.midSpan { grid-column: auto; }
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
-
.panel { border: 1px solid var(--line); background: var(--paper); border-radius: var(--radius);
|
|
377
|
+
.panel { border: 1px solid var(--line); background: var(--paper); border-radius: var(--radius); box-shadow: 0 1px 0 rgba(16,24,40,.04); }
|
|
378
378
|
.panelHead { display: flex; align-items: center; justify-content: space-between; padding: 12px 12px; border-bottom: 1px solid var(--line); background: linear-gradient(180deg, var(--paper2), var(--paper)); }
|
|
379
379
|
.panelHead b { font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
|
|
380
380
|
.panelBody { padding: 12px; }
|
package/cli/web-ui.js
CHANGED
|
@@ -665,7 +665,6 @@
|
|
|
665
665
|
const items = Array.isArray(state.projects) ? state.projects : [];
|
|
666
666
|
const filtered = items.filter((p) => {
|
|
667
667
|
const hay = [p.client, p.program, p.stream, p.project, p.slug, (p.tags||[]).join(' ')].join(' ').toLowerCase();
|
|
668
|
-
if (kind !== 'all' && String(i.kind||'') !== kind) return false;
|
|
669
668
|
return !filter || hay.includes(filter);
|
|
670
669
|
});
|
|
671
670
|
el.innerHTML = '';
|
|
@@ -1268,6 +1267,74 @@
|
|
|
1268
1267
|
}
|
|
1269
1268
|
}
|
|
1270
1269
|
|
|
1270
|
+
async function refreshQualityScore() {
|
|
1271
|
+
const el = $('qualityScoreCard');
|
|
1272
|
+
if (el) el.innerHTML = '<div class="help">Carregando score...</div>';
|
|
1273
|
+
try {
|
|
1274
|
+
const r = await api('/api/quality/score', { dir: dirOrDefault() });
|
|
1275
|
+
if (!el) return;
|
|
1276
|
+
if (r && r.needsInit) {
|
|
1277
|
+
el.innerHTML = '<div class="help">Workspace não inicializado.</div>';
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
const score = (r && typeof r.score === 'number') ? r.score : null;
|
|
1281
|
+
const breakdown = (r && r.breakdown) ? r.breakdown : {};
|
|
1282
|
+
const threshold = 90;
|
|
1283
|
+
const status = (score !== null && score >= threshold) ? 'ok' : 'warn';
|
|
1284
|
+
|
|
1285
|
+
const line = (label, data, keyLabel) => {
|
|
1286
|
+
if (!data) return '';
|
|
1287
|
+
const pct = (typeof data.pct === 'number') ? `${data.pct}%` : 'n/a';
|
|
1288
|
+
const detail = keyLabel ? `${data[keyLabel] || 0}/${data.total || 0}` : `${data.total || 0}`;
|
|
1289
|
+
return `<div class=\"help\" style=\"margin-top:4px\"><b>${escapeHtml(label)}:</b> ${escapeHtml(pct)} (${escapeHtml(detail)})</div>`;
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
const html = `<div style=\"display:flex; justify-content:space-between; gap:10px; align-items:center\">`
|
|
1293
|
+
+ `<div style=\"min-width:0\"><div style=\"font-weight:800\">${score === null ? 'Sem score' : `Score: ${score}%`}</div>`
|
|
1294
|
+
+ `${line('Tasks com projectSlug', breakdown.tasks, 'withProjectSlug')}`
|
|
1295
|
+
+ `${line('Status com history', breakdown.status, 'withHistory')}`
|
|
1296
|
+
+ `${line('Blockers com projectSlug', breakdown.blockers, 'withProjectSlug')}`
|
|
1297
|
+
+ `</div>`
|
|
1298
|
+
+ `<div class=\"pill ${status}\">${status}</div>`
|
|
1299
|
+
+ `</div>`;
|
|
1300
|
+
el.innerHTML = html;
|
|
1301
|
+
} catch {
|
|
1302
|
+
if (el) el.innerHTML = '<div class="help">Falha ao carregar score.</div>';
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
async function refreshRiskSummary() {
|
|
1307
|
+
const el = $('riskSummary');
|
|
1308
|
+
if (el) el.innerHTML = '<div class="help">Carregando riscos...</div>';
|
|
1309
|
+
try {
|
|
1310
|
+
const r = await api('/api/risk/summary', { dir: dirOrDefault() });
|
|
1311
|
+
if (!el) return;
|
|
1312
|
+
if (r && r.needsInit) {
|
|
1313
|
+
el.innerHTML = '<div class="help">Workspace não inicializado.</div>';
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
const items = Array.isArray(r.items) ? r.items : [];
|
|
1317
|
+
if (!items.length) {
|
|
1318
|
+
el.innerHTML = '<div class="help">Sem riscos relevantes.</div>';
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
const rows = items.map((it) => {
|
|
1322
|
+
const age = (it.oldestBlockerDays != null) ? `${it.oldestBlockerDays}d` : 'n/a';
|
|
1323
|
+
return `<div class=\"rep\">`
|
|
1324
|
+
+ `<div style=\"display:flex; justify-content:space-between; gap:10px; align-items:center\">`
|
|
1325
|
+
+ `<div style=\"min-width:0\"><div style=\"font-weight:800\">${escapeHtml(it.slug || '')}</div>`
|
|
1326
|
+
+ `<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>`
|
|
1327
|
+
+ `</div>`
|
|
1328
|
+
+ `<div class=\"pill warn\">risco</div>`
|
|
1329
|
+
+ `</div>`
|
|
1330
|
+
+ `</div>`;
|
|
1331
|
+
}).join('');
|
|
1332
|
+
el.innerHTML = rows;
|
|
1333
|
+
} catch {
|
|
1334
|
+
if (el) el.innerHTML = '<div class="help">Falha ao carregar riscos.</div>';
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1271
1338
|
async function refreshExecutiveSummary() {
|
|
1272
1339
|
const el = $('executiveSummary');
|
|
1273
1340
|
if (el) el.textContent = 'Carregando resumo...';
|
|
@@ -1706,8 +1773,10 @@
|
|
|
1706
1773
|
|
|
1707
1774
|
if (isCompanionPage) {
|
|
1708
1775
|
await refreshHealthChecklist();
|
|
1776
|
+
await refreshQualityScore();
|
|
1709
1777
|
await refreshExecutiveSummary();
|
|
1710
1778
|
await refreshAnomalies();
|
|
1779
|
+
await refreshRiskSummary();
|
|
1711
1780
|
await refreshIncidents();
|
|
1712
1781
|
await refreshHeatmap();
|
|
1713
1782
|
return;
|
|
@@ -1759,8 +1828,10 @@
|
|
|
1759
1828
|
window.setTimelineKind = setTimelineKind;
|
|
1760
1829
|
window.refreshBlockersInsights = refreshBlockersInsights;
|
|
1761
1830
|
window.refreshHealthChecklist = refreshHealthChecklist;
|
|
1831
|
+
window.refreshQualityScore = refreshQualityScore;
|
|
1762
1832
|
window.refreshExecutiveSummary = refreshExecutiveSummary;
|
|
1763
1833
|
window.refreshAnomalies = refreshAnomalies;
|
|
1834
|
+
window.refreshRiskSummary = refreshRiskSummary;
|
|
1764
1835
|
window.copyOut = copyOut;
|
|
1765
1836
|
window.copyPath = copyPath;
|
|
1766
1837
|
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">
|
|
@@ -1941,7 +1961,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1941
1961
|
|
|
1942
1962
|
if (req.method === 'GET' && req.url === '/timeline') {
|
|
1943
1963
|
try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch {}
|
|
1944
|
-
const body =
|
|
1964
|
+
const body = buildTimelineHtml(dir || './freya', version);
|
|
1945
1965
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
1946
1966
|
res.end(body);
|
|
1947
1967
|
return;
|
|
@@ -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))
|
|
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))
|
|
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
|
|
2303
|
-
|
|
2304
|
-
|
|
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
|
|
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