@polderlabs/bizar 4.5.2 → 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/main-DGGq-iZI.js +361 -0
- package/bizar-dash/dist/assets/main-DGGq-iZI.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-lbH6szyX.js → mobile-CWqPoGaT.js} +18 -18
- package/bizar-dash/dist/assets/mobile-CWqPoGaT.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-BRhoDOUz.js → mobile-i4Uv9eW8.js} +1 -1
- package/bizar-dash/dist/assets/{mobile-BRhoDOUz.js.map → mobile-i4Uv9eW8.js.map} +1 -1
- package/bizar-dash/dist/index.html +2 -2
- 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/logger.mjs +71 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +45 -6
- package/bizar-dash/src/server/metrics.mjs +193 -0
- package/bizar-dash/src/server/routes/chat.mjs +6 -3
- package/bizar-dash/src/server/routes/lightrag.mjs +3 -2
- package/bizar-dash/src/server/routes/memory.mjs +5 -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/server.mjs +62 -0
- package/bizar-dash/src/web/components/SearchModal.tsx +3 -2
- 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/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/views/Activity.tsx +24 -17
- package/bizar-dash/src/web/views/History.tsx +90 -75
- package/bizar-dash/src/web/views/Overview.tsx +11 -8
- package/bizar-dash/src/web/views/Settings.tsx +7 -1
- package/bizar-dash/tests/cli-bugfixes.test.mjs +4 -4
- 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/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 +2 -2
- 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 -2096
- package/cli/bg.mjs +5 -13
- package/cli/bin.mjs +149 -1361
- 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 +1 -13
- package/cli/provision.mjs +2 -13
- package/cli/service-controller.mjs +1 -11
- package/cli/service.mjs +1 -11
- package/cli/utils.mjs +41 -1
- package/package.json +6 -1
- package/bizar-dash/dist/assets/main-B4OfGAwz.js +0 -361
- package/bizar-dash/dist/assets/main-B4OfGAwz.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-lbH6szyX.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{am as r,j as o,r as e,an as n}from"./mobile-CWqPoGaT.js";const t=document.getElementById("root");if(!t)throw new Error("Root element #root not found");r(t).render(o.jsx(e.StrictMode,{children:o.jsx(n,{})}));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile-
|
|
1
|
+
{"version":3,"file":"mobile-i4Uv9eW8.js","sources":["../../src/web/mobile.tsx"],"sourcesContent":["// src/mobile.tsx — entry point for mobile dashboard.\nimport { StrictMode } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { MobileApp } from './MobileApp';\nimport './styles/mobile.css';\n\nconst root = document.getElementById('root');\nif (!root) throw new Error('Root element #root not found');\ncreateRoot(root).render(\n <StrictMode>\n <MobileApp />\n </StrictMode>,\n);\n"],"names":["root","createRoot","jsx","StrictMode","MobileApp"],"mappings":"gEAMA,MAAMA,EAAO,SAAS,eAAe,MAAM,EAC3C,GAAI,CAACA,EAAM,MAAM,IAAI,MAAM,8BAA8B,EACzDC,EAAWD,CAAI,EAAE,OACfE,EAAAA,IAACC,EAAAA,WAAA,CACC,SAAAD,EAAAA,IAACE,EAAA,CAAA,CAAU,CAAA,CACb,CACF"}
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
}
|
|
30
30
|
})();
|
|
31
31
|
</script>
|
|
32
|
-
<script type="module" crossorigin src="/assets/main-
|
|
33
|
-
<link rel="modulepreload" crossorigin href="/assets/mobile-
|
|
32
|
+
<script type="module" crossorigin src="/assets/main-DGGq-iZI.js"></script>
|
|
33
|
+
<link rel="modulepreload" crossorigin href="/assets/mobile-CWqPoGaT.js">
|
|
34
34
|
<link rel="stylesheet" crossorigin href="/assets/mobile-CsZQAswA.css">
|
|
35
35
|
<link rel="stylesheet" crossorigin href="/assets/main-DAlLdW8I.css">
|
|
36
36
|
</head>
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
13
13
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
14
14
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
|
15
|
-
<script type="module" crossorigin src="/assets/mobile-
|
|
16
|
-
<link rel="modulepreload" crossorigin href="/assets/mobile-
|
|
15
|
+
<script type="module" crossorigin src="/assets/mobile-i4Uv9eW8.js"></script>
|
|
16
|
+
<link rel="modulepreload" crossorigin href="/assets/mobile-CWqPoGaT.js">
|
|
17
17
|
<link rel="stylesheet" crossorigin href="/assets/mobile-CsZQAswA.css">
|
|
18
18
|
</head>
|
|
19
19
|
<body>
|
package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"4.1.9","results":[[":tests/lib/utils.test.ts",{"duration":20.207488999999953,"failed":false}],[":tests/components/Button.test.tsx",{"duration":65.99081000000001,"failed":false}],[":tests/components/Card.test.tsx",{"duration":24.894168999999977,"failed":false}],[":tests/hooks/useModal.test.tsx",{"duration":91.24809300000004,"failed":false}],[":tests/components/Toast.test.tsx",{"duration":112.40757200000002,"failed":false}],[":tests/components/Modal.test.tsx",{"duration":113.472669,"failed":false}],[":tests/lib/i18n.test.ts",{"duration":3.547411000000011,"failed":false}],[":tests/components/StatusBadge.test.tsx",{"duration":25.80152300000003,"failed":false}],[":tests/hooks/useToast.test.tsx",{"duration":74.72992599999998,"failed":false}],[":tests/components/Spinner.test.tsx",{"duration":43.51148800000004,"failed":false}]]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/logger.mjs
|
|
3
|
+
*
|
|
4
|
+
* v4.7.0 — Structured JSON logger with level filtering.
|
|
5
|
+
*
|
|
6
|
+
* Replaces ad-hoc `console.log('[module] message')` calls across the
|
|
7
|
+
* server with a single, level-gated logger that emits one JSON object
|
|
8
|
+
* per line. Output goes to the matching console.* stream (log/warn/
|
|
9
|
+
* error) so existing terminal capture keeps working, but the body is
|
|
10
|
+
* machine-parseable.
|
|
11
|
+
*
|
|
12
|
+
* Level selection: BIZAR_LOG_LEVEL env var (debug | info | warn | error).
|
|
13
|
+
* Default `info` matches the existing behaviour — everything but debug
|
|
14
|
+
* is visible. Set `debug` to surface verbose tracing during development.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* import { info, warn, error, child } from './logger.mjs';
|
|
18
|
+
* info('server started', { port: 4317 });
|
|
19
|
+
* warn('rate limited', { module: 'lightrag', err: e.message });
|
|
20
|
+
* const log = child({ module: 'overview' });
|
|
21
|
+
* log.info('snapshot built', { size: 1234 });
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
25
|
+
const ENV_LEVEL = process.env.BIZAR_LOG_LEVEL;
|
|
26
|
+
const currentLevel = LEVELS[ENV_LEVEL] !== undefined ? ENV_LEVEL : 'info';
|
|
27
|
+
|
|
28
|
+
function format(level, msg, ctx) {
|
|
29
|
+
const entry = {
|
|
30
|
+
ts: new Date().toISOString(),
|
|
31
|
+
level,
|
|
32
|
+
msg,
|
|
33
|
+
...(ctx && typeof ctx === 'object' ? ctx : {}),
|
|
34
|
+
};
|
|
35
|
+
return JSON.stringify(entry);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function shouldLog(level) {
|
|
39
|
+
return LEVELS[currentLevel] <= LEVELS[level];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function debug(msg, ctx) {
|
|
43
|
+
if (shouldLog('debug')) console.log(format('debug', msg, ctx));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function info(msg, ctx) {
|
|
47
|
+
if (shouldLog('info')) console.log(format('info', msg, ctx));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function warn(msg, ctx) {
|
|
51
|
+
if (shouldLog('warn')) console.warn(format('warn', msg, ctx));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function error(msg, ctx) {
|
|
55
|
+
if (shouldLog('error')) console.error(format('error', msg, ctx));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build a child logger that binds a context object to every log call.
|
|
60
|
+
* Useful for tagging a series of related entries with a module/session id.
|
|
61
|
+
*/
|
|
62
|
+
export function child(ctx) {
|
|
63
|
+
return {
|
|
64
|
+
debug: (m, c) => debug(m, { ...ctx, ...(c && typeof c === 'object' ? c : {}) }),
|
|
65
|
+
info: (m, c) => info(m, { ...ctx, ...(c && typeof c === 'object' ? c : {}) }),
|
|
66
|
+
warn: (m, c) => warn(m, { ...ctx, ...(c && typeof c === 'object' ? c : {}) }),
|
|
67
|
+
error: (m, c) => error(m, { ...ctx, ...(c && typeof c === 'object' ? c : {}) }),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export { currentLevel };
|
|
@@ -52,9 +52,32 @@ import https from 'node:https';
|
|
|
52
52
|
|
|
53
53
|
import { atomicWriteJson } from './routes/_shared.mjs';
|
|
54
54
|
import { parseFrontmatter } from './yaml.mjs';
|
|
55
|
+
import { warn as logWarn } from './logger.mjs';
|
|
55
56
|
|
|
56
57
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
57
58
|
|
|
59
|
+
/**
|
|
60
|
+
* v4.7.0 — Per-site log rate limiter for hot-path catches.
|
|
61
|
+
*
|
|
62
|
+
* Several empty catches below sit on stdout/stderr `data` handlers
|
|
63
|
+
* that can fire dozens of times per second when LightRAG is chatty
|
|
64
|
+
* (or the disk is slow). Without rate-limiting, a transient disk
|
|
65
|
+
* hiccup floods the log with thousands of identical warnings and
|
|
66
|
+
* hides the real problem underneath.
|
|
67
|
+
*
|
|
68
|
+
* Returns true the first time it's called for a key within `intervalMs`,
|
|
69
|
+
* then false until the window passes. Default 60s — matches the
|
|
70
|
+
* "if it's still broken, say it again a minute later" pattern.
|
|
71
|
+
*/
|
|
72
|
+
const _lastLog = new Map();
|
|
73
|
+
function _rateLimit(key, intervalMs = 60_000) {
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
const last = _lastLog.get(key) || 0;
|
|
76
|
+
if (now - last < intervalMs) return false;
|
|
77
|
+
_lastLog.set(key, now);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
58
81
|
function deepMerge(target, source) {
|
|
59
82
|
const out = { ...target };
|
|
60
83
|
if (source === null || source === undefined) return out;
|
|
@@ -444,11 +467,23 @@ export async function startServer(config, { logger } = {}) {
|
|
|
444
467
|
let fd = -1;
|
|
445
468
|
try {
|
|
446
469
|
fd = openSync(logFile, 'a');
|
|
447
|
-
child.stdout?.on('data', (d) => {
|
|
448
|
-
|
|
470
|
+
child.stdout?.on('data', (d) => {
|
|
471
|
+
try { writeSync(fd, d); } catch (err) {
|
|
472
|
+
if (_rateLimit('lightrag-write-stdout')) logWarn('lightrag stream write failed (stdout)', { module: 'lightrag', err: err?.message || String(err) });
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
child.stderr?.on('data', (d) => {
|
|
476
|
+
try { writeSync(fd, d); } catch (err) {
|
|
477
|
+
if (_rateLimit('lightrag-write-stderr')) logWarn('lightrag stream write failed (stderr)', { module: 'lightrag', err: err?.message || String(err) });
|
|
478
|
+
}
|
|
479
|
+
});
|
|
449
480
|
child.on('exit', (code, signal) => {
|
|
450
|
-
try { closeSync(fd); } catch {
|
|
451
|
-
|
|
481
|
+
try { closeSync(fd); } catch (err) {
|
|
482
|
+
if (_rateLimit('lightrag-close-fd')) logWarn('lightrag close fd failed', { module: 'lightrag', err: err?.message || String(err) });
|
|
483
|
+
}
|
|
484
|
+
try { unlinkSync(pidFile); } catch (err) {
|
|
485
|
+
if (_rateLimit('lightrag-exit-unlink')) logWarn('lightrag pid unlink failed', { module: 'lightrag', err: err?.message || String(err) });
|
|
486
|
+
}
|
|
452
487
|
log(`lightrag-server exited (code=${code}, signal=${signal})`);
|
|
453
488
|
});
|
|
454
489
|
} catch (openErr) {
|
|
@@ -483,7 +518,9 @@ export async function stopServer(config, { logger } = {}) {
|
|
|
483
518
|
const pidFile = join(config.workingDir, 'lightrag.pid');
|
|
484
519
|
const { pid, alive } = readPidAlive(pidFile);
|
|
485
520
|
if (!pid || !alive) {
|
|
486
|
-
try { unlinkSync(pidFile); } catch {
|
|
521
|
+
try { unlinkSync(pidFile); } catch (err) {
|
|
522
|
+
if (_rateLimit('lightrag-stop-notrunning-unlink')) logWarn('lightrag pid unlink failed (not running)', { module: 'lightrag', err: err?.message || String(err) });
|
|
523
|
+
}
|
|
487
524
|
return { ok: true, message: 'not running' };
|
|
488
525
|
}
|
|
489
526
|
log(`stopping lightrag-server (pid ${pid})`);
|
|
@@ -493,7 +530,9 @@ export async function stopServer(config, { logger } = {}) {
|
|
|
493
530
|
log(`escalating to SIGKILL (pid ${pid})`);
|
|
494
531
|
killPid(pid, 'SIGKILL');
|
|
495
532
|
}
|
|
496
|
-
try { unlinkSync(pidFile); } catch {
|
|
533
|
+
try { unlinkSync(pidFile); } catch (err) {
|
|
534
|
+
if (_rateLimit('lightrag-stop-unlink')) logWarn('lightrag pid unlink failed (stop)', { module: 'lightrag', err: err?.message || String(err) });
|
|
535
|
+
}
|
|
497
536
|
return { ok: true, message: 'stopped' };
|
|
498
537
|
}
|
|
499
538
|
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/metrics.mjs
|
|
3
|
+
*
|
|
4
|
+
* v4.7.0 — Tiny Prometheus-style metrics registry.
|
|
5
|
+
*
|
|
6
|
+
* Implements the three primitive metric types we actually use:
|
|
7
|
+
* - counter(name, help) → { inc(labels) }
|
|
8
|
+
* - gauge(name, help) → { set(value, labels) }
|
|
9
|
+
* - histogram(name, help, opts) → { observe(value, labels) }
|
|
10
|
+
*
|
|
11
|
+
* Storage is in-process: counters/gauges/histograms are kept in Maps
|
|
12
|
+
* keyed by metric name → label-set signature → values. Labels are
|
|
13
|
+
* JSON-stringified to produce a stable key (sorted-keys would be
|
|
14
|
+
* nicer but JSON.stringify of a literal object preserves insertion
|
|
15
|
+
* order, which is good enough for our use — every caller writes the
|
|
16
|
+
* same label shape for the same metric).
|
|
17
|
+
*
|
|
18
|
+
* `render()` returns the Prometheus text exposition format
|
|
19
|
+
* (Content-Type: text/plain; version=0.0.4) so it can be scraped by
|
|
20
|
+
* Prometheus or read by humans. The output includes the `# HELP` and
|
|
21
|
+
* `# TYPE` headers for every metric.
|
|
22
|
+
*
|
|
23
|
+
* `reset()` clears the registry — only intended for tests.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const counters = new Map();
|
|
27
|
+
const gauges = new Map();
|
|
28
|
+
const histograms = new Map();
|
|
29
|
+
|
|
30
|
+
// Default Prometheus-style histogram buckets in seconds (suitable for
|
|
31
|
+
// HTTP latency). Override per-metric via opts.buckets.
|
|
32
|
+
const DEFAULT_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10];
|
|
33
|
+
|
|
34
|
+
function labelsKey(labels) {
|
|
35
|
+
if (!labels || typeof labels !== 'object') return '{}';
|
|
36
|
+
const keys = Object.keys(labels);
|
|
37
|
+
if (keys.length === 0) return '{}';
|
|
38
|
+
const ordered = {};
|
|
39
|
+
for (const k of keys.sort()) ordered[k] = labels[k];
|
|
40
|
+
return JSON.stringify(ordered);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function labelsToString(labels) {
|
|
44
|
+
if (!labels || typeof labels !== 'object') return '';
|
|
45
|
+
const keys = Object.keys(labels);
|
|
46
|
+
if (keys.length === 0) return '';
|
|
47
|
+
return keys
|
|
48
|
+
.sort()
|
|
49
|
+
.map((k) => `${k}="${escapeLabelValue(String(labels[k]))}"`)
|
|
50
|
+
.join(',');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function escapeLabelValue(v) {
|
|
54
|
+
return v.replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/"/g, '\\"');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function counter(name, help) {
|
|
58
|
+
if (!name) throw new Error('counter(name, help): name is required');
|
|
59
|
+
return {
|
|
60
|
+
inc(labels = {}) {
|
|
61
|
+
const key = labelsKey(labels);
|
|
62
|
+
let entry = counters.get(name);
|
|
63
|
+
if (!entry) {
|
|
64
|
+
entry = { help: help || '', values: new Map() };
|
|
65
|
+
counters.set(name, entry);
|
|
66
|
+
}
|
|
67
|
+
const v = entry.values.get(key) || { labels, value: 0 };
|
|
68
|
+
v.value += 1;
|
|
69
|
+
entry.values.set(key, v);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function gauge(name, help) {
|
|
75
|
+
if (!name) throw new Error('gauge(name, help): name is required');
|
|
76
|
+
return {
|
|
77
|
+
set(value, labels = {}) {
|
|
78
|
+
const key = labelsKey(labels);
|
|
79
|
+
let entry = gauges.get(name);
|
|
80
|
+
if (!entry) {
|
|
81
|
+
entry = { help: help || '', values: new Map() };
|
|
82
|
+
gauges.set(name, entry);
|
|
83
|
+
}
|
|
84
|
+
entry.values.set(key, { labels, value: Number(value) });
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function histogram(name, help, opts = {}) {
|
|
90
|
+
if (!name) throw new Error('histogram(name, help, opts): name is required');
|
|
91
|
+
const buckets = Array.isArray(opts.buckets) && opts.buckets.length > 0
|
|
92
|
+
? [...opts.buckets].sort((a, b) => a - b)
|
|
93
|
+
: DEFAULT_BUCKETS;
|
|
94
|
+
return {
|
|
95
|
+
observe(value, labels = {}) {
|
|
96
|
+
const key = labelsKey(labels);
|
|
97
|
+
let entry = histograms.get(name);
|
|
98
|
+
if (!entry) {
|
|
99
|
+
entry = {
|
|
100
|
+
help: help || '',
|
|
101
|
+
buckets,
|
|
102
|
+
values: new Map(),
|
|
103
|
+
};
|
|
104
|
+
histograms.set(name, entry);
|
|
105
|
+
}
|
|
106
|
+
const v = entry.values.get(key) || {
|
|
107
|
+
labels,
|
|
108
|
+
count: 0,
|
|
109
|
+
sum: 0,
|
|
110
|
+
bucketCounts: buckets.map(() => 0),
|
|
111
|
+
};
|
|
112
|
+
v.count += 1;
|
|
113
|
+
v.sum += Number(value);
|
|
114
|
+
for (let i = 0; i < buckets.length; i += 1) {
|
|
115
|
+
if (Number(value) <= buckets[i]) v.bucketCounts[i] += 1;
|
|
116
|
+
}
|
|
117
|
+
entry.values.set(key, v);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function renderCounters() {
|
|
123
|
+
const lines = [];
|
|
124
|
+
for (const [name, entry] of counters) {
|
|
125
|
+
lines.push(`# HELP ${name} ${entry.help}`);
|
|
126
|
+
lines.push(`# TYPE ${name} counter`);
|
|
127
|
+
for (const v of entry.values.values()) {
|
|
128
|
+
const labels = labelsToString(v.labels);
|
|
129
|
+
lines.push(`${name}${labels ? '{' + labels + '}' : ''} ${v.value}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return lines;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function renderGauges() {
|
|
136
|
+
const lines = [];
|
|
137
|
+
for (const [name, entry] of gauges) {
|
|
138
|
+
lines.push(`# HELP ${name} ${entry.help}`);
|
|
139
|
+
lines.push(`# TYPE ${name} gauge`);
|
|
140
|
+
for (const v of entry.values.values()) {
|
|
141
|
+
const labels = labelsToString(v.labels);
|
|
142
|
+
lines.push(`${name}${labels ? '{' + labels + '}' : ''} ${v.value}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return lines;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function renderHistograms() {
|
|
149
|
+
const lines = [];
|
|
150
|
+
for (const [name, entry] of histograms) {
|
|
151
|
+
lines.push(`# HELP ${name} ${entry.help}`);
|
|
152
|
+
lines.push(`# TYPE ${name} histogram`);
|
|
153
|
+
for (const v of entry.values.values()) {
|
|
154
|
+
const baseLabels = labelsToString(v.labels);
|
|
155
|
+
for (let i = 0; i < entry.buckets.length; i += 1) {
|
|
156
|
+
const le = entry.buckets[i];
|
|
157
|
+
const bucketLabels = baseLabels
|
|
158
|
+
? `${baseLabels},le="${le}"`
|
|
159
|
+
: `le="${le}"`;
|
|
160
|
+
lines.push(`${name}_bucket{${bucketLabels}} ${v.bucketCounts[i]}`);
|
|
161
|
+
}
|
|
162
|
+
const infLabels = baseLabels ? `${baseLabels},le="+Inf"` : 'le="+Inf"';
|
|
163
|
+
lines.push(`${name}_bucket{${infLabels}} ${v.count}`);
|
|
164
|
+
const sumLabels = baseLabels;
|
|
165
|
+
lines.push(
|
|
166
|
+
`${name}_sum${sumLabels ? '{' + sumLabels + '}' : ''} ${v.sum}`,
|
|
167
|
+
);
|
|
168
|
+
lines.push(
|
|
169
|
+
`${name}_count${sumLabels ? '{' + sumLabels + '}' : ''} ${v.count}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return lines;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function render() {
|
|
177
|
+
const parts = [
|
|
178
|
+
...renderCounters(),
|
|
179
|
+
...renderGauges(),
|
|
180
|
+
...renderHistograms(),
|
|
181
|
+
];
|
|
182
|
+
return parts.length === 0 ? '' : `${parts.join('\n')}\n`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Reset every metric. Intended for tests — never call this from a
|
|
187
|
+
* long-running server.
|
|
188
|
+
*/
|
|
189
|
+
export function reset() {
|
|
190
|
+
counters.clear();
|
|
191
|
+
gauges.clear();
|
|
192
|
+
histograms.clear();
|
|
193
|
+
}
|
|
@@ -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,
|
|
@@ -70,9 +71,11 @@ const chatDeltaCounts = new Map(); // chatSessionId -> count since idle
|
|
|
70
71
|
function noteChatDelta(chatSessionId) {
|
|
71
72
|
const cur = chatDeltaCounts.get(chatSessionId) || 0;
|
|
72
73
|
if (cur >= CHAT_DELTA_BUFFER_CAP) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
});
|
|
76
79
|
return false;
|
|
77
80
|
}
|
|
78
81
|
chatDeltaCounts.set(chatSessionId, cur + 1);
|
|
@@ -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
|
/**
|
|
@@ -128,7 +129,7 @@ export function createLightragRouter({ projectRoot }) {
|
|
|
128
129
|
const lines = content.split('\n');
|
|
129
130
|
logTail.push(...lines.slice(-30));
|
|
130
131
|
} catch (err) {
|
|
131
|
-
|
|
132
|
+
logWarn('swallowed in lightrag log tail', { module: 'lightrag', err: err.message });
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
|
|
@@ -136,7 +137,7 @@ export function createLightragRouter({ projectRoot }) {
|
|
|
136
137
|
try {
|
|
137
138
|
if (existsSync(logFile)) logSize = statSync(logFile).size;
|
|
138
139
|
} catch (err) {
|
|
139
|
-
|
|
140
|
+
logWarn('swallowed in lightrag log stat', { module: 'lightrag', err: err.message });
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
res.json({
|
|
@@ -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);
|
|
@@ -79,7 +80,7 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
79
80
|
const data = JSON.parse(readFileSync(reindexMarker, 'utf8'));
|
|
80
81
|
lastSecretScan = data.attemptedAt || null;
|
|
81
82
|
} catch (err) {
|
|
82
|
-
|
|
83
|
+
logWarn('swallowed in memory reindex marker read', { module: 'memory', err: err.message });
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
@@ -489,7 +490,7 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
489
490
|
}
|
|
490
491
|
}
|
|
491
492
|
} catch (err) {
|
|
492
|
-
|
|
493
|
+
logWarn('swallowed in memory conflict scan', { module: 'memory', err: err.message });
|
|
493
494
|
}
|
|
494
495
|
|
|
495
496
|
res.json({ conflicts });
|
|
@@ -528,7 +529,7 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
528
529
|
const { recordQuery } = await getMemoryLightrag();
|
|
529
530
|
recordQuery(projectRoot, Date.now() - startedAt);
|
|
530
531
|
} catch (err) {
|
|
531
|
-
|
|
532
|
+
logWarn('swallowed in memory recordQuery', { module: 'memory', err: err.message });
|
|
532
533
|
}
|
|
533
534
|
res.json({ ok: true, q, lexical, semantic });
|
|
534
535
|
}));
|
|
@@ -612,7 +613,7 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
612
613
|
const lines = content.split('\n');
|
|
613
614
|
logTail.push(...lines.slice(-30));
|
|
614
615
|
} catch (err) {
|
|
615
|
-
|
|
616
|
+
logWarn('swallowed in memory log tail', { module: 'memory', err: err.message });
|
|
616
617
|
}
|
|
617
618
|
}
|
|
618
619
|
|
|
@@ -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);
|
|
@@ -39,6 +39,21 @@ import { buildAllowedRootsFromSettings, resolveSafePath } from './lib/path-safe.
|
|
|
39
39
|
import { V2EventBus } from './v2-event-bus.mjs';
|
|
40
40
|
import { loadOrCreateAuth, V2_DEFAULT_PORT } from './v2-auth-file.mjs';
|
|
41
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
|
+
);
|
|
42
57
|
|
|
43
58
|
let processHandlersInstalled = false;
|
|
44
59
|
let v2Bus = null;
|
|
@@ -161,6 +176,46 @@ export async function createServer({
|
|
|
161
176
|
next();
|
|
162
177
|
});
|
|
163
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
|
+
|
|
164
219
|
app.use(
|
|
165
220
|
(
|
|
166
221
|
err,
|
|
@@ -519,9 +574,16 @@ export async function createServer({
|
|
|
519
574
|
|
|
520
575
|
wss.on('connection', (ws, req) => {
|
|
521
576
|
ws.isAlive = true;
|
|
577
|
+
wsClientsGauge.set(wss.clients.size);
|
|
522
578
|
ws.on('pong', () => {
|
|
523
579
|
ws.isAlive = true;
|
|
524
580
|
});
|
|
581
|
+
ws.on('close', () => {
|
|
582
|
+
wsClientsGauge.set(wss.clients.size);
|
|
583
|
+
});
|
|
584
|
+
ws.on('error', () => {
|
|
585
|
+
wsClientsGauge.set(wss.clients.size);
|
|
586
|
+
});
|
|
525
587
|
const path = req.url
|
|
526
588
|
? (() => {
|
|
527
589
|
try {
|
|
@@ -128,8 +128,8 @@ export function SearchModal({ open, onClose, onSelect }: Props) {
|
|
|
128
128
|
return (
|
|
129
129
|
<div className="search-modal-backdrop" onClick={onClose}>
|
|
130
130
|
<div className="search-modal" role="dialog" aria-modal="true" aria-labelledby="search-modal-title" onClick={(e) => e.stopPropagation()}>
|
|
131
|
-
<div className="search-modal-head">
|
|
132
|
-
<Search size={14} />
|
|
131
|
+
<div className="search-modal-head" role="search">
|
|
132
|
+
<Search size={14} aria-hidden />
|
|
133
133
|
<span id="search-modal-title" className="sr-only">Search</span>
|
|
134
134
|
<input
|
|
135
135
|
ref={inputRef}
|
|
@@ -138,6 +138,7 @@ export function SearchModal({ open, onClose, onSelect }: Props) {
|
|
|
138
138
|
value={q}
|
|
139
139
|
onChange={(e) => setQ(e.target.value)}
|
|
140
140
|
onKeyDown={onKey}
|
|
141
|
+
aria-label="Search"
|
|
141
142
|
/>
|
|
142
143
|
<button
|
|
143
144
|
type="button"
|