@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.
Files changed (123) hide show
  1. package/bizar-dash/dist/assets/main-DHZmbnxQ.js +361 -0
  2. package/bizar-dash/dist/assets/main-DHZmbnxQ.js.map +1 -0
  3. package/bizar-dash/dist/assets/main-DX_Jh8Wc.css +1 -0
  4. package/bizar-dash/dist/assets/{mobile-lbH6szyX.js → mobile-BK8-ythT.js} +18 -18
  5. package/bizar-dash/dist/assets/mobile-BK8-ythT.js.map +1 -0
  6. package/bizar-dash/dist/assets/{mobile-BRhoDOUz.js → mobile-Chvf9u_B.js} +1 -1
  7. package/bizar-dash/dist/assets/{mobile-BRhoDOUz.js.map → mobile-Chvf9u_B.js.map} +1 -1
  8. package/bizar-dash/dist/index.html +3 -3
  9. package/bizar-dash/dist/mobile.html +2 -2
  10. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  11. package/bizar-dash/skills/publishing/SKILL.md +146 -0
  12. package/bizar-dash/src/server/api.mjs +8 -0
  13. package/bizar-dash/src/server/backup-store.mjs +525 -0
  14. package/bizar-dash/src/server/digest-store.mjs +558 -0
  15. package/bizar-dash/src/server/lib/rate-limit.mjs +122 -0
  16. package/bizar-dash/src/server/logger.mjs +71 -0
  17. package/bizar-dash/src/server/memory-lightrag.mjs +45 -6
  18. package/bizar-dash/src/server/metrics.mjs +193 -0
  19. package/bizar-dash/src/server/routes/backup.mjs +112 -0
  20. package/bizar-dash/src/server/routes/chat.mjs +20 -3
  21. package/bizar-dash/src/server/routes/digests.mjs +82 -0
  22. package/bizar-dash/src/server/routes/lightrag.mjs +3 -2
  23. package/bizar-dash/src/server/routes/memory.mjs +5 -4
  24. package/bizar-dash/src/server/routes/misc.mjs +2 -1
  25. package/bizar-dash/src/server/routes/overview.mjs +2 -1
  26. package/bizar-dash/src/server/routes-v2/events.mjs +14 -0
  27. package/bizar-dash/src/server/schedules-runner.mjs +126 -0
  28. package/bizar-dash/src/server/server.mjs +79 -0
  29. package/bizar-dash/src/web/App.tsx +8 -1
  30. package/bizar-dash/src/web/components/BackupRestore.tsx +330 -0
  31. package/bizar-dash/src/web/components/SearchModal.tsx +6 -3
  32. package/bizar-dash/src/web/components/VirtualList.tsx +53 -0
  33. package/bizar-dash/src/web/components/chat/ChatThread.tsx +17 -11
  34. package/bizar-dash/src/web/components/chat/Composer.tsx +2 -0
  35. package/bizar-dash/src/web/hooks/useI18n.ts +13 -0
  36. package/bizar-dash/src/web/lib/i18n.ts +25 -0
  37. package/bizar-dash/src/web/locales/en.json +52 -0
  38. package/bizar-dash/src/web/main.tsx +5 -0
  39. package/bizar-dash/src/web/styles/main.css +70 -8
  40. package/bizar-dash/src/web/views/Activity.tsx +35 -18
  41. package/bizar-dash/src/web/views/Agents.tsx +57 -42
  42. package/bizar-dash/src/web/views/Artifacts.tsx +38 -25
  43. package/bizar-dash/src/web/views/Chat.tsx +8 -0
  44. package/bizar-dash/src/web/views/History.tsx +94 -76
  45. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +21 -4
  46. package/bizar-dash/src/web/views/Mods.tsx +30 -17
  47. package/bizar-dash/src/web/views/Overview.tsx +19 -9
  48. package/bizar-dash/src/web/views/Providers.tsx +16 -16
  49. package/bizar-dash/src/web/views/Schedules.tsx +33 -15
  50. package/bizar-dash/src/web/views/Settings.tsx +97 -1745
  51. package/bizar-dash/src/web/views/Skills.tsx +4 -1
  52. package/bizar-dash/src/web/views/Tasks.tsx +11 -2
  53. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +8 -2
  54. package/bizar-dash/src/web/views/memory/LightragPanel.tsx +3 -0
  55. package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +12 -4
  56. package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +3 -0
  57. package/bizar-dash/src/web/views/settings/ActivitySection.tsx +205 -0
  58. package/bizar-dash/src/web/views/settings/AgentSection.tsx +294 -0
  59. package/bizar-dash/src/web/views/settings/AuthSection.tsx +159 -0
  60. package/bizar-dash/src/web/views/settings/BackupSection.tsx +16 -0
  61. package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +16 -0
  62. package/bizar-dash/src/web/views/settings/GeneralSection.tsx +105 -0
  63. package/bizar-dash/src/web/views/settings/HeadroomSection.tsx +39 -0
  64. package/bizar-dash/src/web/views/settings/MemorySection.tsx +16 -0
  65. package/bizar-dash/src/web/views/settings/NetworkSection.tsx +87 -0
  66. package/bizar-dash/src/web/views/settings/NotificationsSection.tsx +34 -0
  67. package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +16 -0
  68. package/bizar-dash/src/web/views/settings/SkillsSection.tsx +16 -0
  69. package/bizar-dash/src/web/views/settings/SystemLlmSection.tsx +81 -0
  70. package/bizar-dash/src/web/views/settings/ThemeSection.tsx +168 -0
  71. package/bizar-dash/src/web/views/settings/UpdatesSection.tsx +256 -0
  72. package/bizar-dash/tests/a11y.test.tsx +206 -0
  73. package/bizar-dash/tests/backup-restore.test.mjs +217 -0
  74. package/bizar-dash/tests/backup-restore.test.tsx +123 -0
  75. package/bizar-dash/tests/backup-store.test.mjs +300 -0
  76. package/bizar-dash/tests/cli-bugfixes.test.mjs +4 -4
  77. package/bizar-dash/tests/cli-error-visibility.test.mjs +153 -0
  78. package/bizar-dash/tests/cli-refactor.test.mjs +184 -0
  79. package/bizar-dash/tests/components/Button.test.tsx +41 -0
  80. package/bizar-dash/tests/components/Card.test.tsx +42 -0
  81. package/bizar-dash/tests/components/Modal.test.tsx +104 -0
  82. package/bizar-dash/tests/components/Spinner.test.tsx +32 -0
  83. package/bizar-dash/tests/components/StatusBadge.test.tsx +35 -0
  84. package/bizar-dash/tests/components/Toast.test.tsx +108 -0
  85. package/bizar-dash/tests/digest-generation.test.mjs +191 -0
  86. package/bizar-dash/tests/digest-store.test.mjs +264 -0
  87. package/bizar-dash/tests/hooks/useModal.test.tsx +84 -0
  88. package/bizar-dash/tests/hooks/useToast.test.tsx +50 -0
  89. package/bizar-dash/tests/lib/i18n.test.ts +46 -0
  90. package/bizar-dash/tests/lib/utils.test.ts +194 -0
  91. package/bizar-dash/tests/logger.test.mjs +207 -0
  92. package/bizar-dash/tests/metrics.test.mjs +183 -0
  93. package/bizar-dash/tests/rate-limit.test.mjs +298 -0
  94. package/bizar-dash/tests/server-bugfixes.test.mjs +2 -2
  95. package/bizar-dash/tests/setup.ts +7 -0
  96. package/bizar-dash/vitest.config.ts +13 -0
  97. package/cli/artifact-cli.mjs +605 -0
  98. package/cli/artifact-render.mjs +621 -0
  99. package/cli/artifact-server.mjs +847 -0
  100. package/cli/artifact.mjs +38 -2096
  101. package/cli/bg.mjs +5 -13
  102. package/cli/bin.mjs +221 -1350
  103. package/cli/commands/artifact.mjs +20 -0
  104. package/cli/commands/dash.mjs +160 -0
  105. package/cli/commands/headroom.mjs +204 -0
  106. package/cli/commands/install.mjs +169 -0
  107. package/cli/commands/memory.mjs +25 -0
  108. package/cli/commands/minimax.mjs +285 -0
  109. package/cli/commands/mod.mjs +185 -0
  110. package/cli/commands/service.mjs +65 -0
  111. package/cli/commands/usage.mjs +109 -0
  112. package/cli/commands/util.mjs +459 -0
  113. package/cli/digest.mjs +149 -0
  114. package/cli/doctor.mjs +1 -13
  115. package/cli/provision.mjs +2 -13
  116. package/cli/service-controller.mjs +1 -11
  117. package/cli/service.mjs +1 -11
  118. package/cli/utils.mjs +41 -1
  119. package/package.json +6 -1
  120. package/bizar-dash/dist/assets/main-B4OfGAwz.js +0 -361
  121. package/bizar-dash/dist/assets/main-B4OfGAwz.js.map +0 -1
  122. package/bizar-dash/dist/assets/main-DAlLdW8I.css +0 -1
  123. 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) => { try { writeSync(fd, d); } catch {} });
448
- child.stderr?.on('data', (d) => { try { writeSync(fd, d); } catch {} });
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
- try { unlinkSync(pidFile); } catch {}
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
- console.warn(
74
- `[chat] dropped delta for session ${chatSessionId}: per-session cap (${CHAT_DELTA_BUFFER_CAP}) exceeded; client is too slow`,
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
- console.warn('swallowed in lightrag log tail:', err.message);
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
- console.warn('swallowed in lightrag log stat:', err.message);
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
- console.warn('swallowed in memory reindex marker read:', err.message);
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
- console.warn('swallowed in memory conflict scan:', err.message);
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
- console.warn('swallowed in memory recordQuery:', err.message);
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
- console.warn('swallowed in memory log tail:', err.message);
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) => console.error('[updates] error:', 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