@polderlabs/bizar 4.5.2 → 4.7.1
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-DHZmbnxQ.js +361 -0
- package/bizar-dash/dist/assets/main-DHZmbnxQ.js.map +1 -0
- package/bizar-dash/dist/assets/main-DX_Jh8Wc.css +1 -0
- package/bizar-dash/dist/assets/{mobile-lbH6szyX.js → mobile-BK8-ythT.js} +18 -18
- package/bizar-dash/dist/assets/mobile-BK8-ythT.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-BRhoDOUz.js → mobile-Chvf9u_B.js} +1 -1
- package/bizar-dash/dist/assets/{mobile-BRhoDOUz.js.map → mobile-Chvf9u_B.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/skills/publishing/SKILL.md +146 -0
- package/bizar-dash/src/server/api.mjs +8 -0
- package/bizar-dash/src/server/backup-store.mjs +525 -0
- package/bizar-dash/src/server/digest-store.mjs +558 -0
- package/bizar-dash/src/server/lib/rate-limit.mjs +122 -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/backup.mjs +112 -0
- package/bizar-dash/src/server/routes/chat.mjs +20 -3
- package/bizar-dash/src/server/routes/digests.mjs +82 -0
- 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/routes-v2/events.mjs +14 -0
- package/bizar-dash/src/server/schedules-runner.mjs +126 -0
- package/bizar-dash/src/server/server.mjs +79 -0
- package/bizar-dash/src/web/App.tsx +8 -1
- package/bizar-dash/src/web/components/BackupRestore.tsx +330 -0
- package/bizar-dash/src/web/components/SearchModal.tsx +6 -3
- 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/components/chat/Composer.tsx +2 -0
- 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/styles/main.css +70 -8
- package/bizar-dash/src/web/views/Activity.tsx +35 -18
- package/bizar-dash/src/web/views/Agents.tsx +57 -42
- package/bizar-dash/src/web/views/Artifacts.tsx +38 -25
- package/bizar-dash/src/web/views/Chat.tsx +8 -0
- package/bizar-dash/src/web/views/History.tsx +94 -76
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +21 -4
- package/bizar-dash/src/web/views/Mods.tsx +30 -17
- package/bizar-dash/src/web/views/Overview.tsx +19 -9
- package/bizar-dash/src/web/views/Providers.tsx +16 -16
- package/bizar-dash/src/web/views/Schedules.tsx +33 -15
- package/bizar-dash/src/web/views/Settings.tsx +97 -1745
- package/bizar-dash/src/web/views/Skills.tsx +4 -1
- package/bizar-dash/src/web/views/Tasks.tsx +11 -2
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +8 -2
- package/bizar-dash/src/web/views/memory/LightragPanel.tsx +3 -0
- package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +12 -4
- package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +3 -0
- package/bizar-dash/src/web/views/settings/ActivitySection.tsx +205 -0
- package/bizar-dash/src/web/views/settings/AgentSection.tsx +294 -0
- package/bizar-dash/src/web/views/settings/AuthSection.tsx +159 -0
- package/bizar-dash/src/web/views/settings/BackupSection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/GeneralSection.tsx +105 -0
- package/bizar-dash/src/web/views/settings/HeadroomSection.tsx +39 -0
- package/bizar-dash/src/web/views/settings/MemorySection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/NetworkSection.tsx +87 -0
- package/bizar-dash/src/web/views/settings/NotificationsSection.tsx +34 -0
- package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/SkillsSection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/SystemLlmSection.tsx +81 -0
- package/bizar-dash/src/web/views/settings/ThemeSection.tsx +168 -0
- package/bizar-dash/src/web/views/settings/UpdatesSection.tsx +256 -0
- package/bizar-dash/tests/a11y.test.tsx +206 -0
- package/bizar-dash/tests/backup-restore.test.mjs +217 -0
- package/bizar-dash/tests/backup-restore.test.tsx +123 -0
- package/bizar-dash/tests/backup-store.test.mjs +300 -0
- package/bizar-dash/tests/cli-bugfixes.test.mjs +4 -4
- package/bizar-dash/tests/cli-error-visibility.test.mjs +153 -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/digest-generation.test.mjs +191 -0
- package/bizar-dash/tests/digest-store.test.mjs +264 -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/rate-limit.test.mjs +298 -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 +221 -1350
- 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 +285 -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 +459 -0
- package/cli/digest.mjs +149 -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/main-DAlLdW8I.css +0 -1
- package/bizar-dash/dist/assets/mobile-lbH6szyX.js.map +0 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/routes/backup.mjs
|
|
3
|
+
*
|
|
4
|
+
* v4.8.0 — Backup/restore REST surface.
|
|
5
|
+
*
|
|
6
|
+
* Endpoints:
|
|
7
|
+
* GET /api/backup/list — list available backups
|
|
8
|
+
* POST /api/backup/create — create a new backup
|
|
9
|
+
* POST /api/backup/restore — restore from a backup
|
|
10
|
+
* POST /api/backup/verify — verify backup integrity
|
|
11
|
+
* DELETE /api/backup/:path — delete a backup
|
|
12
|
+
* GET /api/backup/manifest — get manifest for a backup
|
|
13
|
+
*/
|
|
14
|
+
import { Router } from 'express';
|
|
15
|
+
import { wrap } from './_shared.mjs';
|
|
16
|
+
import {
|
|
17
|
+
createBackup,
|
|
18
|
+
listBackups,
|
|
19
|
+
restoreBackup,
|
|
20
|
+
deleteBackup,
|
|
21
|
+
verifyBackup,
|
|
22
|
+
getManifest,
|
|
23
|
+
} from '../backup-store.mjs';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} deps
|
|
27
|
+
* @param {string} deps.projectRoot
|
|
28
|
+
* @returns {import('express').Router}
|
|
29
|
+
*/
|
|
30
|
+
export function createBackupRouter({ projectRoot }) {
|
|
31
|
+
const router = Router();
|
|
32
|
+
|
|
33
|
+
// GET /api/backup/list
|
|
34
|
+
router.get('/backup/list', wrap(async (_req, res) => {
|
|
35
|
+
const backups = await listBackups();
|
|
36
|
+
res.json({ ok: true, backups });
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
// POST /api/backup/create
|
|
40
|
+
router.post('/backup/create', wrap(async (req, res) => {
|
|
41
|
+
const { label, includeProject } = req.body || {};
|
|
42
|
+
const result = await createBackup({
|
|
43
|
+
label: label || null,
|
|
44
|
+
projectRoot: includeProject ? projectRoot : null,
|
|
45
|
+
});
|
|
46
|
+
res.json(result);
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
// POST /api/backup/restore
|
|
50
|
+
router.post('/backup/restore', wrap(async (req, res) => {
|
|
51
|
+
const { backupPath, dryRun, conflictStrategy, includeProject } = req.body || {};
|
|
52
|
+
if (!backupPath) {
|
|
53
|
+
const err = new Error('backupPath is required');
|
|
54
|
+
err.status = 400;
|
|
55
|
+
err.code = 'bad_request';
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
const result = await restoreBackup({
|
|
59
|
+
backupPath,
|
|
60
|
+
dryRun: Boolean(dryRun),
|
|
61
|
+
conflictStrategy: conflictStrategy || 'merge',
|
|
62
|
+
projectRoot: includeProject ? projectRoot : null,
|
|
63
|
+
});
|
|
64
|
+
res.json(result);
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
// POST /api/backup/verify
|
|
68
|
+
router.post('/backup/verify', wrap(async (req, res) => {
|
|
69
|
+
const { backupPath } = req.body || {};
|
|
70
|
+
if (!backupPath) {
|
|
71
|
+
const err = new Error('backupPath is required');
|
|
72
|
+
err.status = 400;
|
|
73
|
+
err.code = 'bad_request';
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
const result = await verifyBackup({ backupPath });
|
|
77
|
+
res.json(result);
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
// DELETE /api/backup/:path (path is url-encoded)
|
|
81
|
+
router.delete('/backup/:path', wrap(async (req, res) => {
|
|
82
|
+
const { path: encoded } = req.params;
|
|
83
|
+
const backupPath = decodeURIComponent(encoded);
|
|
84
|
+
const result = await deleteBackup({ backupPath });
|
|
85
|
+
if (!result.ok) {
|
|
86
|
+
const err = new Error(result.error || 'Delete failed');
|
|
87
|
+
err.status = 404;
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
res.json(result);
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
// GET /api/backup/manifest
|
|
94
|
+
router.get('/backup/manifest', wrap(async (req, res) => {
|
|
95
|
+
const { backupPath } = req.query || {};
|
|
96
|
+
if (!backupPath) {
|
|
97
|
+
const err = new Error('backupPath query param is required');
|
|
98
|
+
err.status = 400;
|
|
99
|
+
err.code = 'bad_request';
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
const result = await getManifest({ backupPath });
|
|
103
|
+
if (!result.ok) {
|
|
104
|
+
const err = new Error(result.error || 'Manifest not found');
|
|
105
|
+
err.status = 404;
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
res.json(result);
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
return router;
|
|
112
|
+
}
|
|
@@ -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,
|
|
@@ -43,6 +44,7 @@ import {
|
|
|
43
44
|
buildAuthHeader,
|
|
44
45
|
} from '../serve-info.mjs';
|
|
45
46
|
import { wrap } from './_shared.mjs';
|
|
47
|
+
import { createRateLimiter } from '../lib/rate-limit.mjs';
|
|
46
48
|
|
|
47
49
|
const SESSION_ID_RE = /^[A-Za-z0-9_-]{1,120}$/;
|
|
48
50
|
|
|
@@ -70,9 +72,11 @@ const chatDeltaCounts = new Map(); // chatSessionId -> count since idle
|
|
|
70
72
|
function noteChatDelta(chatSessionId) {
|
|
71
73
|
const cur = chatDeltaCounts.get(chatSessionId) || 0;
|
|
72
74
|
if (cur >= CHAT_DELTA_BUFFER_CAP) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
logWarn('dropped delta for session: per-session cap exceeded; client is too slow', {
|
|
76
|
+
module: 'chat',
|
|
77
|
+
sessionId: chatSessionId,
|
|
78
|
+
cap: CHAT_DELTA_BUFFER_CAP,
|
|
79
|
+
});
|
|
76
80
|
return false;
|
|
77
81
|
}
|
|
78
82
|
chatDeltaCounts.set(chatSessionId, cur + 1);
|
|
@@ -92,6 +96,19 @@ function resetChatDeltaCount(chatSessionId) {
|
|
|
92
96
|
export function createChatRouter({ state, broadcast }) {
|
|
93
97
|
const router = Router();
|
|
94
98
|
|
|
99
|
+
// v4.8.0 — Per-IP token bucket. Chat endpoints are the most expensive
|
|
100
|
+
// thing the dashboard does (each POST kicks off an SSE subscription +
|
|
101
|
+
// opencode prompt dispatch), so the default budget is conservative:
|
|
102
|
+
// 60 requests / minute / IP, refilling at 1 token per second.
|
|
103
|
+
// Operators can tune via BIZAR_RATE_LIMIT_CHAT_CAPACITY /
|
|
104
|
+
// BIZAR_RATE_LIMIT_CHAT_REFILL.
|
|
105
|
+
const chatLimiter = createRateLimiter({
|
|
106
|
+
capacity: parseInt(process.env.BIZAR_RATE_LIMIT_CHAT_CAPACITY || '60', 10),
|
|
107
|
+
refillPerSecond: parseFloat(process.env.BIZAR_RATE_LIMIT_CHAT_REFILL || '1'),
|
|
108
|
+
scope: 'chat',
|
|
109
|
+
});
|
|
110
|
+
router.use(chatLimiter);
|
|
111
|
+
|
|
95
112
|
router.get('/chat', wrap(async (req, res) => {
|
|
96
113
|
const sessionId = req.query.session ? String(req.query.session) : null;
|
|
97
114
|
const requestedLimit = req.query.limit ? Number(req.query.limit) : 200;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/routes/digests.mjs
|
|
3
|
+
*
|
|
4
|
+
* /api/digests — list digests
|
|
5
|
+
* /api/digests/generate (POST) — generate a digest
|
|
6
|
+
* /api/digests/:path (GET) — get a specific digest
|
|
7
|
+
* /api/digests/:path (DELETE) — delete a digest
|
|
8
|
+
*
|
|
9
|
+
* The `:path` parameter is URL-encoded so it survives Express routing.
|
|
10
|
+
*/
|
|
11
|
+
import { Router } from 'express';
|
|
12
|
+
import {
|
|
13
|
+
generateAndSave,
|
|
14
|
+
listDigests,
|
|
15
|
+
getDigest,
|
|
16
|
+
deleteDigest,
|
|
17
|
+
} from '../digest-store.mjs';
|
|
18
|
+
import { readActiveProjectId, wrap } from './_shared.mjs';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {object} deps
|
|
22
|
+
* @param {object} [deps.projectRoot]
|
|
23
|
+
* @returns {import('express').Router}
|
|
24
|
+
*/
|
|
25
|
+
export function createDigestsRouter({ projectRoot } = {}) {
|
|
26
|
+
const router = Router();
|
|
27
|
+
|
|
28
|
+
// GET /api/digests — list all digests
|
|
29
|
+
router.get('/digests', wrap(async (req, res) => {
|
|
30
|
+
const limit = parseInt(req.query.limit, 10) || 20;
|
|
31
|
+
const digests = await listDigests({ limit });
|
|
32
|
+
res.json({ digests });
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
// GET /api/digests/:path — get a single digest by path
|
|
36
|
+
// The path is double-encoded: first by the client, then by Express.
|
|
37
|
+
router.get('/digests/*', wrap(async (req, res) => {
|
|
38
|
+
// Reconstruct the path from the wildcard
|
|
39
|
+
const rawPath = req.params[0];
|
|
40
|
+
if (!rawPath) {
|
|
41
|
+
// This catches /api/digests with no trailing path (should be handled
|
|
42
|
+
// by the /digests route above, but Express may match this first if the
|
|
43
|
+
// wildcard is greedy — we handle the list case explicitly).
|
|
44
|
+
const digests = await listDigests({ limit: 20 });
|
|
45
|
+
res.json({ digests });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const filePath = decodeURIComponent(rawPath);
|
|
49
|
+
const digest = await getDigest(filePath);
|
|
50
|
+
if (!digest) {
|
|
51
|
+
res.status(404).json({ error: 'not_found', message: 'Digest not found' });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
res.json(digest);
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
// POST /api/digests/generate — generate a new digest
|
|
58
|
+
router.post('/digests/generate', wrap(async (req, res) => {
|
|
59
|
+
const { weekStart, weekEnd, dryRun } = req.body || {};
|
|
60
|
+
const root = req.body?.projectRoot || projectRoot || process.cwd();
|
|
61
|
+
const result = await generateAndSave({ weekStart, weekEnd, projectRoot: root, dryRun });
|
|
62
|
+
res.status(dryRun ? 200 : 201).json(result);
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
// DELETE /api/digests/* — delete a digest by path
|
|
66
|
+
router.delete('/digests/*', wrap(async (req, res) => {
|
|
67
|
+
const rawPath = req.params[0];
|
|
68
|
+
if (!rawPath) {
|
|
69
|
+
res.status(400).json({ error: 'bad_request', message: 'Digest path required' });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const filePath = decodeURIComponent(rawPath);
|
|
73
|
+
const result = await deleteDigest(filePath);
|
|
74
|
+
if (!result.ok) {
|
|
75
|
+
res.status(404).json({ error: 'not_found', message: result.error || 'Digest not found' });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
res.status(204).end();
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
return router;
|
|
82
|
+
}
|
|
@@ -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
|
|