@polderlabs/bizar 4.5.1 → 4.7.0
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/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +0 -1
- package/bizar-dash/dist/assets/main-DAlLdW8I.css +1 -0
- package/bizar-dash/dist/assets/main-DGGq-iZI.js +361 -0
- package/bizar-dash/dist/assets/main-DGGq-iZI.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-O6ANdD4W.js → mobile-CWqPoGaT.js} +18 -19
- package/bizar-dash/dist/assets/mobile-CWqPoGaT.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-DQLFCjwJ.js → mobile-i4Uv9eW8.js} +1 -2
- package/bizar-dash/dist/assets/{mobile-DQLFCjwJ.js.map → mobile-i4Uv9eW8.js.map} +1 -1
- package/bizar-dash/dist/index.html +3 -3
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/bizar-dash/src/server/headroom.mjs +37 -35
- package/bizar-dash/src/server/logger.mjs +71 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +84 -37
- package/bizar-dash/src/server/metrics.mjs +193 -0
- package/bizar-dash/src/server/mods-loader.mjs +13 -5
- package/bizar-dash/src/server/providers-store.mjs +73 -1
- package/bizar-dash/src/server/routes/chat.mjs +41 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +7 -2
- package/bizar-dash/src/server/routes/memory.mjs +13 -4
- package/bizar-dash/src/server/routes/misc.mjs +2 -1
- package/bizar-dash/src/server/routes/overview.mjs +2 -1
- package/bizar-dash/src/server/schedules-runner.mjs +4 -4
- package/bizar-dash/src/server/server.mjs +101 -37
- package/bizar-dash/src/server/watcher.mjs +2 -2
- package/bizar-dash/src/web/App.tsx +40 -6
- package/bizar-dash/src/web/components/SearchModal.tsx +3 -2
- package/bizar-dash/src/web/components/Toast.tsx +1 -1
- package/bizar-dash/src/web/components/Topbar.tsx +1 -1
- package/bizar-dash/src/web/components/VirtualList.tsx +53 -0
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +17 -11
- package/bizar-dash/src/web/hooks/useI18n.ts +13 -0
- package/bizar-dash/src/web/lib/api.ts +12 -11
- package/bizar-dash/src/web/lib/i18n.ts +25 -0
- package/bizar-dash/src/web/locales/en.json +52 -0
- package/bizar-dash/src/web/main.tsx +5 -0
- package/bizar-dash/src/web/styles/chat.css +2 -0
- package/bizar-dash/src/web/styles/main.css +52 -23
- package/bizar-dash/src/web/styles/tasks.css +2 -0
- package/bizar-dash/src/web/views/Activity.tsx +24 -17
- package/bizar-dash/src/web/views/History.tsx +90 -75
- package/bizar-dash/src/web/views/Memory.tsx +4 -3
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +3 -2
- package/bizar-dash/src/web/views/Overview.tsx +15 -10
- package/bizar-dash/src/web/views/Settings.tsx +9 -2
- package/bizar-dash/src/web/views/Skills.tsx +3 -2
- package/bizar-dash/src/web/views/Tasks.tsx +4 -3
- package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +8 -6
- package/bizar-dash/tests/cli-bugfixes.test.mjs +151 -0
- package/bizar-dash/tests/cli-refactor.test.mjs +184 -0
- package/bizar-dash/tests/components/Button.test.tsx +41 -0
- package/bizar-dash/tests/components/Card.test.tsx +42 -0
- package/bizar-dash/tests/components/Modal.test.tsx +104 -0
- package/bizar-dash/tests/components/Spinner.test.tsx +32 -0
- package/bizar-dash/tests/components/StatusBadge.test.tsx +35 -0
- package/bizar-dash/tests/components/Toast.test.tsx +108 -0
- package/bizar-dash/tests/frontend-bugfixes.test.mjs +151 -0
- package/bizar-dash/tests/hooks/useModal.test.tsx +84 -0
- package/bizar-dash/tests/hooks/useToast.test.tsx +50 -0
- package/bizar-dash/tests/lib/i18n.test.ts +46 -0
- package/bizar-dash/tests/lib/utils.test.ts +194 -0
- package/bizar-dash/tests/logger.test.mjs +207 -0
- package/bizar-dash/tests/metrics.test.mjs +183 -0
- package/bizar-dash/tests/server-bugfixes.test.mjs +318 -0
- package/bizar-dash/tests/setup.ts +7 -0
- package/bizar-dash/vitest.config.ts +13 -0
- package/cli/artifact-cli.mjs +605 -0
- package/cli/artifact-render.mjs +621 -0
- package/cli/artifact-server.mjs +847 -0
- package/cli/artifact.mjs +38 -2089
- package/cli/bg.mjs +5 -13
- package/cli/bin.mjs +170 -1348
- package/cli/commands/artifact.mjs +20 -0
- package/cli/commands/dash.mjs +160 -0
- package/cli/commands/headroom.mjs +204 -0
- package/cli/commands/install.mjs +169 -0
- package/cli/commands/memory.mjs +25 -0
- package/cli/commands/minimax.mjs +265 -0
- package/cli/commands/mod.mjs +185 -0
- package/cli/commands/service.mjs +65 -0
- package/cli/commands/usage.mjs +109 -0
- package/cli/commands/util.mjs +306 -0
- package/cli/doctor.mjs +6 -16
- package/cli/memory.mjs +65 -21
- package/cli/provision.mjs +10 -15
- package/cli/service-controller.mjs +1 -11
- package/cli/service.mjs +1 -11
- package/cli/utils.mjs +41 -1
- package/install.sh +1 -2
- package/package.json +8 -3
- package/bizar-dash/dist/assets/main-eWZ4NlCL.css +0 -1
- package/bizar-dash/dist/assets/main-usWhlPWa.js +0 -362
- package/bizar-dash/dist/assets/main-usWhlPWa.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-O6ANdD4W.js.map +0 -1
|
@@ -596,7 +596,8 @@ export const modsLoader = {
|
|
|
596
596
|
const res = await fetch(modJsonUrl);
|
|
597
597
|
if (!res.ok) return null;
|
|
598
598
|
modJson = await res.json();
|
|
599
|
-
} catch {
|
|
599
|
+
} catch (err) {
|
|
600
|
+
console.warn('swallowed in fetchModMetadata:', err.message);
|
|
600
601
|
return null;
|
|
601
602
|
}
|
|
602
603
|
const id = modJson.id;
|
|
@@ -674,7 +675,8 @@ export const modsLoader = {
|
|
|
674
675
|
let entries;
|
|
675
676
|
try {
|
|
676
677
|
entries = readdirSync(p, { withFileTypes: true });
|
|
677
|
-
} catch {
|
|
678
|
+
} catch (err) {
|
|
679
|
+
console.warn('swallowed in mod walk:', err.message);
|
|
678
680
|
return;
|
|
679
681
|
}
|
|
680
682
|
for (const e of entries) {
|
|
@@ -913,20 +915,26 @@ export const modsLoader = {
|
|
|
913
915
|
try {
|
|
914
916
|
out.agents = readdirSync(OPENCODE_AGENTS_DIR)
|
|
915
917
|
.filter((f) => f.startsWith(prefix));
|
|
916
|
-
} catch {
|
|
918
|
+
} catch (err) {
|
|
919
|
+
console.warn('swallowed in agents readdir:', err.message);
|
|
920
|
+
}
|
|
917
921
|
}
|
|
918
922
|
if (existsSync(OPENCODE_COMMANDS_DIR)) {
|
|
919
923
|
try {
|
|
920
924
|
out.commands = readdirSync(OPENCODE_COMMANDS_DIR)
|
|
921
925
|
.filter((f) => f.startsWith(prefix));
|
|
922
|
-
} catch {
|
|
926
|
+
} catch (err) {
|
|
927
|
+
console.warn('swallowed in commands readdir:', err.message);
|
|
928
|
+
}
|
|
923
929
|
}
|
|
924
930
|
if (existsSync(OPENCODE_SKILLS_DIR)) {
|
|
925
931
|
try {
|
|
926
932
|
out.skills = readdirSync(OPENCODE_SKILLS_DIR, { withFileTypes: true })
|
|
927
933
|
.filter((e) => e.isDirectory() && e.name.startsWith(skillPrefix))
|
|
928
934
|
.map((e) => e.name);
|
|
929
|
-
} catch {
|
|
935
|
+
} catch (err) {
|
|
936
|
+
console.warn('swallowed in skills readdir:', err.message);
|
|
937
|
+
}
|
|
930
938
|
}
|
|
931
939
|
return out;
|
|
932
940
|
},
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
renameSync,
|
|
39
39
|
mkdirSync,
|
|
40
40
|
readdirSync,
|
|
41
|
+
statSync,
|
|
41
42
|
} from 'node:fs';
|
|
42
43
|
import { dirname, join } from 'node:path';
|
|
43
44
|
import { homedir } from 'node:os';
|
|
@@ -66,13 +67,80 @@ function atomicWriteJson(filePath, data) {
|
|
|
66
67
|
renameSync(tmp, filePath);
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
// v5.0.0 — Bug S1: 1-second debounced cache for opencode.json reads.
|
|
71
|
+
// `list()`/`listAll()` and other consumers call `loadConfig()` on every
|
|
72
|
+
// WS poll (every ~5s per client) and on every route hit. With N clients
|
|
73
|
+
// that becomes O(N) reads per minute. The cache collapses all reads
|
|
74
|
+
// within a 1-second window to a single read. Writes invalidate so
|
|
75
|
+
// subsequent reads see the new state.
|
|
76
|
+
//
|
|
77
|
+
// The cache lives on `globalThis` so it is shared across all module
|
|
78
|
+
// instances of providers-store.mjs — tests and code that re-imports
|
|
79
|
+
// the module (e.g. via `?cb=` cache-bust) must see the same cache,
|
|
80
|
+
// otherwise a write on one instance becomes invisible to another.
|
|
81
|
+
//
|
|
82
|
+
// The cache also tracks the file's mtime+size and invalidates if the
|
|
83
|
+
// on-disk file has been modified externally (e.g. an editor saving
|
|
84
|
+
// opencode.json, or a test setup writing a fresh file). This prevents
|
|
85
|
+
// stale reads when the file changes outside our write paths.
|
|
86
|
+
const OPENCODE_JSON_CACHE_GLOBAL_KEY = '__bizar_opencode_json_cache__';
|
|
87
|
+
function _opencodeCacheSlot() {
|
|
88
|
+
if (!globalThis[OPENCODE_JSON_CACHE_GLOBAL_KEY]) {
|
|
89
|
+
globalThis[OPENCODE_JSON_CACHE_GLOBAL_KEY] = { entry: null, at: 0 };
|
|
90
|
+
}
|
|
91
|
+
return globalThis[OPENCODE_JSON_CACHE_GLOBAL_KEY];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const OPENCODE_JSON_CACHE_TTL_MS = 1000;
|
|
95
|
+
|
|
96
|
+
function _fileStamp(filePath) {
|
|
97
|
+
// Best-effort stat — if stat fails (file missing, etc.), the caller
|
|
98
|
+
// will re-read and re-populate the cache.
|
|
99
|
+
try {
|
|
100
|
+
const st = statSync(filePath);
|
|
101
|
+
return `${st.size}:${st.mtimeMs}`;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readOpencodeJsonCached(filePath = OPENCODE_JSON) {
|
|
108
|
+
const slot = _opencodeCacheSlot();
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
if (slot.entry && slot.entry.filePath === filePath) {
|
|
111
|
+
const age = now - slot.at;
|
|
112
|
+
if (age < OPENCODE_JSON_CACHE_TTL_MS) {
|
|
113
|
+
// Fast path: still within TTL. Verify the file hasn't been
|
|
114
|
+
// modified externally (e.g. another process, an editor, or a
|
|
115
|
+
// test that writes the file directly). The stamp check is cheap
|
|
116
|
+
// and protects correctness when the file is touched outside
|
|
117
|
+
// our write paths.
|
|
118
|
+
const stamp = _fileStamp(filePath);
|
|
119
|
+
if (stamp === null || stamp === slot.entry.stamp) {
|
|
120
|
+
return slot.entry.data;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const data = safeReadJSON(filePath, {});
|
|
125
|
+
slot.entry = { filePath, data, stamp: _fileStamp(filePath) };
|
|
126
|
+
slot.at = now;
|
|
127
|
+
return data;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function invalidateOpencodeJsonCache() {
|
|
131
|
+
const slot = _opencodeCacheSlot();
|
|
132
|
+
slot.entry = null;
|
|
133
|
+
slot.at = 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
69
136
|
function loadConfig() {
|
|
70
|
-
return
|
|
137
|
+
return readOpencodeJsonCached(OPENCODE_JSON);
|
|
71
138
|
}
|
|
72
139
|
|
|
73
140
|
function saveConfig(data) {
|
|
74
141
|
mkdirSync(dirname(OPENCODE_JSON), { recursive: true });
|
|
75
142
|
atomicWriteJson(OPENCODE_JSON, data);
|
|
143
|
+
invalidateOpencodeJsonCache();
|
|
76
144
|
}
|
|
77
145
|
|
|
78
146
|
// v4.6.0 — expose the on-disk load/save helpers so route modules can
|
|
@@ -81,6 +149,10 @@ function saveConfig(data) {
|
|
|
81
149
|
// were not exported and config.mjs referenced them implicitly.
|
|
82
150
|
export { loadConfig, saveConfig };
|
|
83
151
|
|
|
152
|
+
// v5.0.0 — expose cache helpers for tests and other consumers that need
|
|
153
|
+
// to force a re-read (e.g. settings-store after a write).
|
|
154
|
+
export { readOpencodeJsonCached, invalidateOpencodeJsonCache, OPENCODE_JSON_CACHE_TTL_MS };
|
|
155
|
+
|
|
84
156
|
// ── v4.6.0 Backup-key rotation constants ────────────────────────────────────
|
|
85
157
|
//
|
|
86
158
|
// ERROR_COOLDOWN_THRESHOLD: after this many recorded errors on the same
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* - Upstream SSE error: clean up and return without crashing.
|
|
23
23
|
*/
|
|
24
24
|
import { Router } from 'express';
|
|
25
|
+
import { warn as logWarn } from '../logger.mjs';
|
|
25
26
|
import {
|
|
26
27
|
existsSync,
|
|
27
28
|
mkdirSync,
|
|
@@ -53,6 +54,38 @@ const SESSION_ID_RE = /^[A-Za-z0-9_-]{1,120}$/;
|
|
|
53
54
|
const MAX_CHAT_SUBSCRIPTIONS = 50;
|
|
54
55
|
let activeChatSubscriptions = 0;
|
|
55
56
|
|
|
57
|
+
/**
|
|
58
|
+
* v5.0.0 — Bug S3: per-chat-session backpressure cap on the SSE→WS
|
|
59
|
+
* forwarding pipeline. The upstream SSE can pump deltas faster than
|
|
60
|
+
* the WS broadcast can flush them if a connected WS client is slow.
|
|
61
|
+
* `safeSend()` in server.mjs already terminates slow WS clients based
|
|
62
|
+
* on the byte-level `bufferedAmount`, but we also need a defensive
|
|
63
|
+
* message-count cap so a single chat session can't accumulate an
|
|
64
|
+
* unbounded number of queued deltas in this process. When a session
|
|
65
|
+
* exceeds CHAT_DELTA_BUFFER_CAP, additional deltas are dropped with
|
|
66
|
+
* a warning log so operators see the issue.
|
|
67
|
+
*/
|
|
68
|
+
const CHAT_DELTA_BUFFER_CAP = 1000;
|
|
69
|
+
const chatDeltaCounts = new Map(); // chatSessionId -> count since idle
|
|
70
|
+
|
|
71
|
+
function noteChatDelta(chatSessionId) {
|
|
72
|
+
const cur = chatDeltaCounts.get(chatSessionId) || 0;
|
|
73
|
+
if (cur >= CHAT_DELTA_BUFFER_CAP) {
|
|
74
|
+
logWarn('dropped delta for session: per-session cap exceeded; client is too slow', {
|
|
75
|
+
module: 'chat',
|
|
76
|
+
sessionId: chatSessionId,
|
|
77
|
+
cap: CHAT_DELTA_BUFFER_CAP,
|
|
78
|
+
});
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
chatDeltaCounts.set(chatSessionId, cur + 1);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resetChatDeltaCount(chatSessionId) {
|
|
86
|
+
chatDeltaCounts.delete(chatSessionId);
|
|
87
|
+
}
|
|
88
|
+
|
|
56
89
|
/**
|
|
57
90
|
* @param {object} deps
|
|
58
91
|
* @param {object} deps.state
|
|
@@ -489,6 +522,11 @@ async function streamOpencodeSession({
|
|
|
489
522
|
// Forward text part deltas as chat:delta
|
|
490
523
|
const textDelta = extractTextDelta(envelope);
|
|
491
524
|
if (textDelta) {
|
|
525
|
+
// Bug S3 — drop the delta (with warning) if this session
|
|
526
|
+
// has hit the per-connection cap. The upstream SSE pump
|
|
527
|
+
// continues, but we stop forwarding to WS to avoid
|
|
528
|
+
// unbounded buffering here.
|
|
529
|
+
if (!noteChatDelta(chatSessionId)) return;
|
|
492
530
|
broadcast({
|
|
493
531
|
type: 'chat:delta',
|
|
494
532
|
sessionId: chatSessionId,
|
|
@@ -501,6 +539,9 @@ async function streamOpencodeSession({
|
|
|
501
539
|
onIdle(envelope) {
|
|
502
540
|
if (done) return;
|
|
503
541
|
done = true;
|
|
542
|
+
// Bug S3 — release the per-session delta counter on idle so
|
|
543
|
+
// the next prompt starts with a fresh budget.
|
|
544
|
+
resetChatDeltaCount(chatSessionId);
|
|
504
545
|
// Fetch the final message list and extract the assistant reply.
|
|
505
546
|
void (async () => {
|
|
506
547
|
try {
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
LIGHTRAG_DEFAULT_EMBEDDING,
|
|
28
28
|
} from '../memory-store.mjs';
|
|
29
29
|
import { resolveLightRAGConfig } from '../memory-lightrag.mjs';
|
|
30
|
+
import { warn as logWarn } from '../logger.mjs';
|
|
30
31
|
import { wrap } from './_shared.mjs';
|
|
31
32
|
|
|
32
33
|
/**
|
|
@@ -127,13 +128,17 @@ export function createLightragRouter({ projectRoot }) {
|
|
|
127
128
|
const content = readFileSync(logFile, 'utf8');
|
|
128
129
|
const lines = content.split('\n');
|
|
129
130
|
logTail.push(...lines.slice(-30));
|
|
130
|
-
} catch {
|
|
131
|
+
} catch (err) {
|
|
132
|
+
logWarn('swallowed in lightrag log tail', { module: 'lightrag', err: err.message });
|
|
133
|
+
}
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
let logSize = 0;
|
|
134
137
|
try {
|
|
135
138
|
if (existsSync(logFile)) logSize = statSync(logFile).size;
|
|
136
|
-
} catch {
|
|
139
|
+
} catch (err) {
|
|
140
|
+
logWarn('swallowed in lightrag log stat', { module: 'lightrag', err: err.message });
|
|
141
|
+
}
|
|
137
142
|
|
|
138
143
|
res.json({
|
|
139
144
|
running: alive,
|
|
@@ -14,6 +14,7 @@ import { join, dirname } from 'node:path';
|
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { existsSync, mkdirSync, readFileSync, statSync, readdirSync } from 'node:fs';
|
|
16
16
|
import { execFileSync } from 'node:child_process';
|
|
17
|
+
import { warn as logWarn } from '../logger.mjs';
|
|
17
18
|
|
|
18
19
|
const SERVER_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
20
|
const memoryStore = await import(`${SERVER_ROOT}/memory-store.mjs`).then((m) => m);
|
|
@@ -78,7 +79,9 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
78
79
|
try {
|
|
79
80
|
const data = JSON.parse(readFileSync(reindexMarker, 'utf8'));
|
|
80
81
|
lastSecretScan = data.attemptedAt || null;
|
|
81
|
-
} catch {
|
|
82
|
+
} catch (err) {
|
|
83
|
+
logWarn('swallowed in memory reindex marker read', { module: 'memory', err: err.message });
|
|
84
|
+
}
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
res.json({
|
|
@@ -486,7 +489,9 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
486
489
|
conflicts.push({ relPath: note.relPath, reason: 'git conflict markers' });
|
|
487
490
|
}
|
|
488
491
|
}
|
|
489
|
-
} catch {
|
|
492
|
+
} catch (err) {
|
|
493
|
+
logWarn('swallowed in memory conflict scan', { module: 'memory', err: err.message });
|
|
494
|
+
}
|
|
490
495
|
|
|
491
496
|
res.json({ conflicts });
|
|
492
497
|
}));
|
|
@@ -523,7 +528,9 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
523
528
|
try {
|
|
524
529
|
const { recordQuery } = await getMemoryLightrag();
|
|
525
530
|
recordQuery(projectRoot, Date.now() - startedAt);
|
|
526
|
-
} catch {
|
|
531
|
+
} catch (err) {
|
|
532
|
+
logWarn('swallowed in memory recordQuery', { module: 'memory', err: err.message });
|
|
533
|
+
}
|
|
527
534
|
res.json({ ok: true, q, lexical, semantic });
|
|
528
535
|
}));
|
|
529
536
|
|
|
@@ -605,7 +612,9 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
605
612
|
const content = readFileSync(logFile, 'utf8');
|
|
606
613
|
const lines = content.split('\n');
|
|
607
614
|
logTail.push(...lines.slice(-30));
|
|
608
|
-
} catch {
|
|
615
|
+
} catch (err) {
|
|
616
|
+
logWarn('swallowed in memory log tail', { module: 'memory', err: err.message });
|
|
617
|
+
}
|
|
609
618
|
}
|
|
610
619
|
|
|
611
620
|
let lastError = null;
|
|
@@ -18,6 +18,7 @@ import { searchStore } from '../search-store.mjs';
|
|
|
18
18
|
import { tailscaleStore } from '../tailscale-store.mjs';
|
|
19
19
|
import { updateStore } from '../update-store.mjs';
|
|
20
20
|
import { projectsStore } from '../projects-store.mjs';
|
|
21
|
+
import { error as logError } from '../logger.mjs';
|
|
21
22
|
import { wrap } from './_shared.mjs';
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -81,7 +82,7 @@ export function createMiscRouter({ state, broadcast }) {
|
|
|
81
82
|
updateStore.applyWithProgress({
|
|
82
83
|
packages,
|
|
83
84
|
broadcast,
|
|
84
|
-
}).catch((err) =>
|
|
85
|
+
}).catch((err) => logError('updates error', { module: 'updates', err: err?.message || String(err) }));
|
|
85
86
|
res.json({ started: true, packages });
|
|
86
87
|
});
|
|
87
88
|
|
|
@@ -15,6 +15,7 @@ import { agentsStore } from '../agents-store.mjs';
|
|
|
15
15
|
import { schedulesStore } from '../schedules-store.mjs';
|
|
16
16
|
import { providersStore, mcpsStore } from '../providers-store.mjs';
|
|
17
17
|
import { modsLoader } from '../mods-loader.mjs';
|
|
18
|
+
import { error as logError } from '../logger.mjs';
|
|
18
19
|
import {
|
|
19
20
|
OPENCODE_JSON,
|
|
20
21
|
readSettings,
|
|
@@ -56,7 +57,7 @@ export function createOverviewRouter({ state }) {
|
|
|
56
57
|
},
|
|
57
58
|
);
|
|
58
59
|
child.on('error', (err) => {
|
|
59
|
-
|
|
60
|
+
logError('restart spawn failed', { module: 'overview', err: err.message });
|
|
60
61
|
});
|
|
61
62
|
child.unref();
|
|
62
63
|
process.exit(0);
|
|
@@ -34,8 +34,8 @@ function logLine(line) {
|
|
|
34
34
|
try {
|
|
35
35
|
mkdirSync(LOG_DIR, { recursive: true });
|
|
36
36
|
appendFileSync(LOG_FILE, line + '\n', 'utf8');
|
|
37
|
-
} catch {
|
|
38
|
-
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.warn('swallowed in schedule log append:', err.message);
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -288,8 +288,8 @@ export const schedulesRunner = {
|
|
|
288
288
|
const projectIds = new Set(reg.projects.map((project) => project.id));
|
|
289
289
|
try {
|
|
290
290
|
if (schedulesStore.list('default').length > 0) projectIds.add('default');
|
|
291
|
-
} catch {
|
|
292
|
-
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.warn('swallowed in schedules due-list:', err.message);
|
|
293
293
|
}
|
|
294
294
|
let skipped = 0;
|
|
295
295
|
for (const projectId of projectIds) {
|
|
@@ -24,6 +24,7 @@ import { agentsStore } from './agents-store.mjs';
|
|
|
24
24
|
import { tasksStore } from './tasks-store.mjs';
|
|
25
25
|
import { schedulesStore } from './schedules-store.mjs';
|
|
26
26
|
import { providersStore, mcpsStore } from './providers-store.mjs';
|
|
27
|
+
import { readOpencodeJsonCached } from './providers-store.mjs';
|
|
27
28
|
import { homedir } from 'node:os';
|
|
28
29
|
import { startBgPoller, stopBgPoller } from './bg-poller.mjs';
|
|
29
30
|
import { startBgRetryLoop, stopBgRetryLoop } from './bg-retry.mjs';
|
|
@@ -38,6 +39,21 @@ import { buildAllowedRootsFromSettings, resolveSafePath } from './lib/path-safe.
|
|
|
38
39
|
import { V2EventBus } from './v2-event-bus.mjs';
|
|
39
40
|
import { loadOrCreateAuth, V2_DEFAULT_PORT } from './v2-auth-file.mjs';
|
|
40
41
|
import { createV2Router } from './routes-v2/index.mjs';
|
|
42
|
+
import { counter, gauge, render as renderMetrics } from './metrics.mjs';
|
|
43
|
+
|
|
44
|
+
// v4.7.0 — Prometheus-style HTTP metrics. Bound to the server-wide
|
|
45
|
+
// registry; render() emits the text exposition format consumed by
|
|
46
|
+
// `GET /metrics`. Counter is keyed by {method, route, status} where
|
|
47
|
+
// `route` is the matched Express pattern (e.g. `/api/tasks/:id`) to
|
|
48
|
+
// keep cardinality bounded; unmatched 404s collapse to `unmatched`.
|
|
49
|
+
const httpRequestsTotal = counter(
|
|
50
|
+
'http_requests_total',
|
|
51
|
+
'Count of HTTP requests handled by the dashboard server.',
|
|
52
|
+
);
|
|
53
|
+
const wsClientsGauge = gauge(
|
|
54
|
+
'ws_clients',
|
|
55
|
+
'Number of currently-connected WebSocket clients (snapshot + /ws/logs).',
|
|
56
|
+
);
|
|
41
57
|
|
|
42
58
|
let processHandlersInstalled = false;
|
|
43
59
|
let v2Bus = null;
|
|
@@ -160,6 +176,46 @@ export async function createServer({
|
|
|
160
176
|
next();
|
|
161
177
|
});
|
|
162
178
|
|
|
179
|
+
// v4.7.0 — Prometheus scrape endpoint. Mounted BEFORE the auth
|
|
180
|
+
// middleware and OUTSIDE /api/* so scrapers don't need a bearer
|
|
181
|
+
// token. Returns text/plain in the standard 0.0.4 exposition
|
|
182
|
+
// format so both Prometheus and `curl http://host/metrics` work.
|
|
183
|
+
app.get('/metrics', (_req, res) => {
|
|
184
|
+
res.set('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
|
|
185
|
+
res.send(renderMetrics());
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// v4.7.0 — Per-request counter. Uses the matched Express route
|
|
189
|
+
// pattern (e.g. `/api/tasks/:id`) when available; falls back to
|
|
190
|
+
// `unmatched` for 404s so the cardinality stays bounded. Hooked off
|
|
191
|
+
// `finish` so the final status code is captured even after error
|
|
192
|
+
// handlers run.
|
|
193
|
+
app.use((req, res, next) => {
|
|
194
|
+
res.on('finish', () => {
|
|
195
|
+
const route = req.route?.path ? `${req.baseUrl || ''}${req.route.path}` : 'unmatched';
|
|
196
|
+
httpRequestsTotal.inc({
|
|
197
|
+
method: req.method,
|
|
198
|
+
route,
|
|
199
|
+
status: String(res.statusCode),
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
next();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// v4.7.0 — Cache-Control headers for live JSON endpoints. The
|
|
206
|
+
// frontend pulls these every few seconds, so we use `no-cache` (which
|
|
207
|
+
// allows conditional revalidation) rather than `no-store` (which
|
|
208
|
+
// forbids it). Static assets already get `max-age=1y` via
|
|
209
|
+
// express.static above; this only fills the gap for the live API.
|
|
210
|
+
app.use('/api/settings', (req, res, next) => {
|
|
211
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
212
|
+
next();
|
|
213
|
+
});
|
|
214
|
+
app.use('/api/snapshot', (req, res, next) => {
|
|
215
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
216
|
+
next();
|
|
217
|
+
});
|
|
218
|
+
|
|
163
219
|
app.use(
|
|
164
220
|
(
|
|
165
221
|
err,
|
|
@@ -281,8 +337,8 @@ export async function createServer({
|
|
|
281
337
|
if (client.bufferedAmount > WS_BACKPRESSURE_LIMIT_BYTES) {
|
|
282
338
|
try {
|
|
283
339
|
client.terminate();
|
|
284
|
-
} catch {
|
|
285
|
-
|
|
340
|
+
} catch (err) {
|
|
341
|
+
console.warn('swallowed in terminate (backpressure):', err.message);
|
|
286
342
|
}
|
|
287
343
|
return false;
|
|
288
344
|
}
|
|
@@ -396,8 +452,8 @@ export async function createServer({
|
|
|
396
452
|
try {
|
|
397
453
|
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
|
|
398
454
|
socket.destroy();
|
|
399
|
-
} catch {
|
|
400
|
-
|
|
455
|
+
} catch (err) {
|
|
456
|
+
console.warn('swallowed in 403 destroy:', err.message);
|
|
401
457
|
}
|
|
402
458
|
return;
|
|
403
459
|
}
|
|
@@ -405,8 +461,8 @@ export async function createServer({
|
|
|
405
461
|
try {
|
|
406
462
|
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
407
463
|
socket.destroy();
|
|
408
|
-
} catch {
|
|
409
|
-
|
|
464
|
+
} catch (err) {
|
|
465
|
+
console.warn('swallowed in 401 destroy:', err.message);
|
|
410
466
|
}
|
|
411
467
|
return;
|
|
412
468
|
}
|
|
@@ -420,8 +476,8 @@ export async function createServer({
|
|
|
420
476
|
if (client.isAlive === false) {
|
|
421
477
|
try {
|
|
422
478
|
client.terminate();
|
|
423
|
-
} catch {
|
|
424
|
-
|
|
479
|
+
} catch (err) {
|
|
480
|
+
console.warn('swallowed in heartbeat terminate:', err.message);
|
|
425
481
|
}
|
|
426
482
|
return;
|
|
427
483
|
}
|
|
@@ -431,8 +487,8 @@ export async function createServer({
|
|
|
431
487
|
} catch {
|
|
432
488
|
try {
|
|
433
489
|
client.terminate();
|
|
434
|
-
} catch {
|
|
435
|
-
|
|
490
|
+
} catch (err) {
|
|
491
|
+
console.warn('swallowed in ping-fail terminate:', err.message);
|
|
436
492
|
}
|
|
437
493
|
}
|
|
438
494
|
});
|
|
@@ -518,9 +574,16 @@ export async function createServer({
|
|
|
518
574
|
|
|
519
575
|
wss.on('connection', (ws, req) => {
|
|
520
576
|
ws.isAlive = true;
|
|
577
|
+
wsClientsGauge.set(wss.clients.size);
|
|
521
578
|
ws.on('pong', () => {
|
|
522
579
|
ws.isAlive = true;
|
|
523
580
|
});
|
|
581
|
+
ws.on('close', () => {
|
|
582
|
+
wsClientsGauge.set(wss.clients.size);
|
|
583
|
+
});
|
|
584
|
+
ws.on('error', () => {
|
|
585
|
+
wsClientsGauge.set(wss.clients.size);
|
|
586
|
+
});
|
|
524
587
|
const path = req.url
|
|
525
588
|
? (() => {
|
|
526
589
|
try {
|
|
@@ -577,8 +640,8 @@ export async function createServer({
|
|
|
577
640
|
}
|
|
578
641
|
fileSize = newSize;
|
|
579
642
|
}
|
|
580
|
-
} catch {
|
|
581
|
-
|
|
643
|
+
} catch (err) {
|
|
644
|
+
console.warn('swallowed in sendLogChunk:', err.message);
|
|
582
645
|
}
|
|
583
646
|
}
|
|
584
647
|
|
|
@@ -617,8 +680,8 @@ export async function createServer({
|
|
|
617
680
|
data: buildSnapshotSafe(state, opencodeConfigDir),
|
|
618
681
|
}),
|
|
619
682
|
);
|
|
620
|
-
} catch {
|
|
621
|
-
|
|
683
|
+
} catch (err) {
|
|
684
|
+
console.warn('swallowed in ws snapshot send:', err.message);
|
|
622
685
|
}
|
|
623
686
|
|
|
624
687
|
ws.on('message', (raw) => {
|
|
@@ -678,34 +741,34 @@ export async function createServer({
|
|
|
678
741
|
function close() {
|
|
679
742
|
try {
|
|
680
743
|
stopBgPoller();
|
|
681
|
-
} catch {
|
|
682
|
-
|
|
744
|
+
} catch (err) {
|
|
745
|
+
console.warn('swallowed in stopBgPoller:', err.message);
|
|
683
746
|
}
|
|
684
747
|
try {
|
|
685
748
|
stopBgRetryLoop();
|
|
686
|
-
} catch {
|
|
687
|
-
|
|
749
|
+
} catch (err) {
|
|
750
|
+
console.warn('swallowed in stopBgRetryLoop:', err.message);
|
|
688
751
|
}
|
|
689
752
|
try {
|
|
690
753
|
watcher.stop();
|
|
691
|
-
} catch {
|
|
692
|
-
|
|
754
|
+
} catch (err) {
|
|
755
|
+
console.warn('swallowed in watcher.stop:', err.message);
|
|
693
756
|
}
|
|
694
757
|
try {
|
|
695
758
|
wss.clients.forEach((c) => c.terminate());
|
|
696
759
|
wss.close();
|
|
697
|
-
} catch {
|
|
698
|
-
|
|
760
|
+
} catch (err) {
|
|
761
|
+
console.warn('swallowed in wss.close:', err.message);
|
|
699
762
|
}
|
|
700
763
|
try {
|
|
701
764
|
clearInterval(heartbeatInterval);
|
|
702
|
-
} catch {
|
|
703
|
-
|
|
765
|
+
} catch (err) {
|
|
766
|
+
console.warn('swallowed in clearInterval:', err.message);
|
|
704
767
|
}
|
|
705
768
|
try {
|
|
706
769
|
server.close();
|
|
707
|
-
} catch {
|
|
708
|
-
|
|
770
|
+
} catch (err) {
|
|
771
|
+
console.warn('swallowed in server.close:', err.message);
|
|
709
772
|
}
|
|
710
773
|
currentBroadcast = () => {};
|
|
711
774
|
}
|
|
@@ -723,14 +786,15 @@ function buildSnapshotSafe(state, opencodeConfigDir) {
|
|
|
723
786
|
|
|
724
787
|
function buildSnapshot(state, opencodeConfigDir) {
|
|
725
788
|
const cfgFile = join(opencodeConfigDir, 'opencode.json');
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
789
|
+
// v5.0.0 — Bug S2: read opencode.json via the 1s-debounced cache in
|
|
790
|
+
// providers-store.mjs. WS clients connect on snapshot delivery; with N
|
|
791
|
+
// clients the readFileSync here was the main per-connection blocker.
|
|
792
|
+
// The cache collapses all reads within a 1-second window to a single
|
|
793
|
+
// disk hit. Writes (settings change, providers-store mutations)
|
|
794
|
+
// invalidate the cache, so the snapshot always reflects the latest
|
|
795
|
+
// state within at most 1s.
|
|
796
|
+
const exists = existsSync(cfgFile);
|
|
797
|
+
const cfg = readOpencodeJsonCached();
|
|
734
798
|
const activeProject = projectsStore.active();
|
|
735
799
|
return {
|
|
736
800
|
overview: state.getOverview(),
|
|
@@ -740,9 +804,9 @@ function buildSnapshot(state, opencodeConfigDir) {
|
|
|
740
804
|
activeProject,
|
|
741
805
|
config: {
|
|
742
806
|
path: cfgFile,
|
|
743
|
-
data: cfg,
|
|
744
|
-
raw: cfg ? JSON.stringify(cfg, null, 2) : '',
|
|
745
|
-
exists
|
|
807
|
+
data: exists ? cfg : null,
|
|
808
|
+
raw: exists && cfg ? JSON.stringify(cfg, null, 2) : '',
|
|
809
|
+
exists,
|
|
746
810
|
},
|
|
747
811
|
settings: readSettings(),
|
|
748
812
|
tasks: activeProject ? tasksStore.loadTasks(activeProject.id) : [],
|
|
@@ -69,8 +69,8 @@ export function createWatcher({ paths, onChange, options = {} }) {
|
|
|
69
69
|
async stop() {
|
|
70
70
|
try {
|
|
71
71
|
await watcher.close();
|
|
72
|
-
} catch {
|
|
73
|
-
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.warn('swallowed in watcher.close:', err.message);
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
76
|
/** Force a synthetic broadcast — useful after a self-mutation. */
|