@polderlabs/bizar 4.9.0 → 5.0.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 (183) hide show
  1. package/bizar-dash/dist/assets/icons-Bo0iH9EC.js +701 -0
  2. package/bizar-dash/dist/assets/icons-Bo0iH9EC.js.map +1 -0
  3. package/bizar-dash/dist/assets/{index-DU61awG3.js → index-DmpSFPJY.js} +1 -1
  4. package/bizar-dash/dist/assets/{index-DU61awG3.js.map → index-DmpSFPJY.js.map} +1 -1
  5. package/bizar-dash/dist/assets/main-C1cpttnv.js +19 -0
  6. package/bizar-dash/dist/assets/main-C1cpttnv.js.map +1 -0
  7. package/bizar-dash/dist/assets/main-DTkNlLrw.css +1 -0
  8. package/bizar-dash/dist/assets/markdown-DIquRulQ.js +29 -0
  9. package/bizar-dash/dist/assets/markdown-DIquRulQ.js.map +1 -0
  10. package/bizar-dash/dist/assets/mobile-BtxQJftK.js +2 -0
  11. package/bizar-dash/dist/assets/mobile-BtxQJftK.js.map +1 -0
  12. package/bizar-dash/dist/assets/mobile-DD-FZrTC.js +1 -0
  13. package/bizar-dash/dist/assets/{mobile-CL5uUQEC.js.map → mobile-DD-FZrTC.js.map} +1 -1
  14. package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js +40 -0
  15. package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js.map +1 -0
  16. package/bizar-dash/dist/index.html +6 -3
  17. package/bizar-dash/dist/mobile.html +5 -2
  18. package/bizar-dash/node_modules/.package-lock.json +6 -0
  19. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  20. package/bizar-dash/package-lock.json +6 -0
  21. package/bizar-dash/skills/eval/SKILL.md +237 -0
  22. package/bizar-dash/src/server/api.mjs +35 -0
  23. package/bizar-dash/src/server/auth.mjs +155 -1
  24. package/bizar-dash/src/server/diagnostics-store.mjs +452 -2
  25. package/bizar-dash/src/server/eval-store.mjs +226 -0
  26. package/bizar-dash/src/server/eval.mjs +347 -0
  27. package/bizar-dash/src/server/memory-store.mjs +46 -0
  28. package/bizar-dash/src/server/ocr.mjs +55 -0
  29. package/bizar-dash/src/server/plugins/registry.mjs +363 -0
  30. package/bizar-dash/src/server/plugins/sandbox.mjs +655 -0
  31. package/bizar-dash/src/server/plugins/store.mjs +659 -0
  32. package/bizar-dash/src/server/providers-store.mjs +11 -4
  33. package/bizar-dash/src/server/routes/_shared.mjs +2 -2
  34. package/bizar-dash/src/server/routes/clipboard.mjs +173 -0
  35. package/bizar-dash/src/server/routes/doctor.mjs +71 -0
  36. package/bizar-dash/src/server/routes/env-vars.mjs +67 -1
  37. package/bizar-dash/src/server/routes/eval.mjs +147 -0
  38. package/bizar-dash/src/server/routes/memory.mjs +3 -0
  39. package/bizar-dash/src/server/routes/ocr.mjs +182 -0
  40. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +61 -42
  41. package/bizar-dash/src/server/routes/plugins.mjs +220 -0
  42. package/bizar-dash/src/server/routes/schedules.mjs +55 -0
  43. package/bizar-dash/src/server/routes/users.mjs +84 -0
  44. package/bizar-dash/src/server/routes/voice.mjs +131 -0
  45. package/bizar-dash/src/server/routes/workspaces.mjs +204 -0
  46. package/bizar-dash/src/server/serve-info.mjs +172 -0
  47. package/bizar-dash/src/server/voice-store.mjs +202 -0
  48. package/bizar-dash/src/server/voice-transcribe.mjs +72 -0
  49. package/bizar-dash/src/server/workspaces.mjs +626 -0
  50. package/bizar-dash/src/web/App.tsx +48 -11
  51. package/bizar-dash/src/web/components/AutosaveField.tsx +50 -0
  52. package/bizar-dash/src/web/components/DoctorPanel.tsx +160 -0
  53. package/bizar-dash/src/web/components/EnvVarManager.tsx +245 -62
  54. package/bizar-dash/src/web/components/InviteDialog.tsx +205 -0
  55. package/bizar-dash/src/web/components/ScheduleTemplateCard.tsx +71 -0
  56. package/bizar-dash/src/web/components/ScreenshotCapture.tsx +138 -0
  57. package/bizar-dash/src/web/components/ScreenshotOCR.tsx +42 -0
  58. package/bizar-dash/src/web/components/SettingsNav.tsx +101 -0
  59. package/bizar-dash/src/web/components/Sidebar.tsx +25 -1
  60. package/bizar-dash/src/web/components/StatusBadge.tsx +33 -2
  61. package/bizar-dash/src/web/components/Topbar.tsx +11 -0
  62. package/bizar-dash/src/web/components/VoiceNotesPanel.tsx +182 -0
  63. package/bizar-dash/src/web/components/VoiceRecorder.tsx +104 -0
  64. package/bizar-dash/src/web/components/WorkspaceSelector.tsx +158 -0
  65. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +64 -0
  66. package/bizar-dash/src/web/components/chat/Composer.tsx +1 -1
  67. package/bizar-dash/src/web/components/chat/useChat.ts +118 -1
  68. package/bizar-dash/src/web/hooks/useAutosave.ts +107 -0
  69. package/bizar-dash/src/web/lib/types.ts +134 -0
  70. package/bizar-dash/src/web/styles/chat.css +8 -5
  71. package/bizar-dash/src/web/styles/main.css +77 -2
  72. package/bizar-dash/src/web/styles/memory.css +82 -0
  73. package/bizar-dash/src/web/styles/settings.css +265 -0
  74. package/bizar-dash/src/web/views/Chat.tsx +15 -1
  75. package/bizar-dash/src/web/views/Doctor.tsx +317 -0
  76. package/bizar-dash/src/web/views/Memory.tsx +17 -2
  77. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +1 -0
  78. package/bizar-dash/src/web/views/Schedules.tsx +94 -12
  79. package/bizar-dash/src/web/views/Settings.tsx +78 -45
  80. package/bizar-dash/src/web/views/Workspace.tsx +294 -0
  81. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +114 -133
  82. package/bizar-dash/src/web/views/memory/FromScreenshotPanel.tsx +23 -0
  83. package/bizar-dash/src/web/views/memory/VaultFromClipboardPanel.tsx +101 -0
  84. package/bizar-dash/src/web/views/settings/AgentSection.tsx +23 -12
  85. package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +5 -6
  86. package/bizar-dash/src/web/views/settings/GeneralSection.tsx +38 -15
  87. package/bizar-dash/src/web/views/settings/MemorySection.tsx +92 -8
  88. package/bizar-dash/src/web/views/settings/WorkspacesSection.tsx +189 -0
  89. package/bizar-dash/tests/autosave.test.tsx +276 -0
  90. package/bizar-dash/tests/bundle-analysis.test.mjs +5 -2
  91. package/bizar-dash/tests/chat-composer.test.tsx +140 -0
  92. package/bizar-dash/tests/clipboard.test.mjs +147 -0
  93. package/bizar-dash/tests/components/doctor-panel.test.tsx +105 -0
  94. package/bizar-dash/tests/components/screenshot-ocr.test.tsx +75 -0
  95. package/bizar-dash/tests/components/workspace-selector.test.tsx +73 -0
  96. package/bizar-dash/tests/deploy-templates.test.mjs +100 -0
  97. package/bizar-dash/tests/diagnostics-store.test.mjs +206 -0
  98. package/bizar-dash/tests/env-vars-extended.test.mjs +190 -0
  99. package/bizar-dash/tests/eval/fixtures.test.mjs +141 -0
  100. package/bizar-dash/tests/eval/report.test.mjs +284 -0
  101. package/bizar-dash/tests/eval/runner.test.mjs +471 -0
  102. package/bizar-dash/tests/layout-spacing.test.mjs +101 -0
  103. package/bizar-dash/tests/memory-default-vault.test.mjs +98 -0
  104. package/bizar-dash/tests/minimax-bar.test.tsx +113 -0
  105. package/bizar-dash/tests/minimax-models.test.mjs +123 -0
  106. package/bizar-dash/tests/ocr.test.mjs +87 -0
  107. package/bizar-dash/tests/opencode-sessions-detail.test.mjs +158 -0
  108. package/bizar-dash/tests/plugins-registry.test.mjs +387 -0
  109. package/bizar-dash/tests/plugins-sandbox.test.mjs +374 -0
  110. package/bizar-dash/tests/plugins-store.test.mjs +455 -0
  111. package/bizar-dash/tests/routes-doctor.test.mjs +159 -0
  112. package/bizar-dash/tests/schedules-templates.test.mjs +144 -0
  113. package/bizar-dash/tests/settings-layout.test.tsx +129 -0
  114. package/bizar-dash/tests/settings-mode-wiring.test.tsx +151 -0
  115. package/bizar-dash/tests/settings-nav.test.tsx +126 -0
  116. package/bizar-dash/tests/users.test.mjs +108 -0
  117. package/bizar-dash/tests/voice-recorder.test.tsx +95 -0
  118. package/bizar-dash/tests/voice-store.test.mjs +148 -0
  119. package/bizar-dash/tests/voice-transcribe.test.mjs +87 -0
  120. package/bizar-dash/tests/workspaces.test.mjs +527 -0
  121. package/cli/bin.mjs +72 -2
  122. package/cli/commands/clip.mjs +146 -0
  123. package/cli/commands/deploy/cloudflare.mjs +250 -0
  124. package/cli/commands/deploy/docker.mjs +221 -0
  125. package/cli/commands/deploy/fly.mjs +161 -0
  126. package/cli/commands/deploy/vercel.mjs +225 -0
  127. package/cli/commands/deploy.mjs +240 -0
  128. package/cli/commands/eval.mjs +378 -0
  129. package/cli/commands/marketplace.mjs +64 -0
  130. package/cli/commands/minimax.mjs +5 -2
  131. package/cli/commands/ocr.mjs +165 -0
  132. package/cli/commands/plugin.mjs +358 -0
  133. package/cli/commands/voice.mjs +211 -0
  134. package/cli/commands/workspace.mjs +247 -0
  135. package/cli/tests/minimax-cli.test.mjs +79 -0
  136. package/config/agents/frigg.md +1 -1
  137. package/config/agents/heimdall.md +1 -1
  138. package/config/agents/mimir.md +1 -1
  139. package/config/agents/quick.md +1 -1
  140. package/config/agents/semble-search.md +1 -1
  141. package/config/agents/vor.md +1 -1
  142. package/config/opencode.json +28 -21
  143. package/config/opencode.json.template +8 -1
  144. package/package.json +12 -8
  145. package/plugins/bizar/index.ts +77 -0
  146. package/plugins/bizar/src/compaction.d.mts +48 -0
  147. package/plugins/bizar/src/compaction.mjs +192 -0
  148. package/plugins/bizar/tests/compaction.test.ts +264 -0
  149. package/templates/deploy/cloudflare/README.md +32 -0
  150. package/templates/deploy/cloudflare/functions-index.template.js +15 -0
  151. package/templates/deploy/cloudflare/wrangler.toml.template +9 -0
  152. package/templates/deploy/docker/.env.template +16 -0
  153. package/templates/deploy/docker/README.md +58 -0
  154. package/templates/deploy/docker/docker-compose.template.yml +23 -0
  155. package/templates/deploy/fly/README.md +35 -0
  156. package/templates/deploy/fly/fly.toml.template +28 -0
  157. package/templates/deploy/vercel/README.md +29 -0
  158. package/templates/deploy/vercel/api-index.template.js +18 -0
  159. package/templates/deploy/vercel/vercel.json.template +16 -0
  160. package/templates/eval-fixtures/README.md +58 -0
  161. package/templates/eval-fixtures/code-search-basic.json +28 -0
  162. package/templates/eval-fixtures/latency-bounds.json +16 -0
  163. package/templates/eval-fixtures/regression-suite.json +79 -0
  164. package/templates/eval-fixtures/response-format.json +30 -0
  165. package/templates/eval-fixtures/tool-call-correctness.json +24 -0
  166. package/templates/plugin-template/README.md +121 -0
  167. package/templates/plugin-template/index.js +66 -0
  168. package/templates/plugin-template/plugin.json +42 -0
  169. package/templates/plugin-template/tests/plugin.test.js +83 -0
  170. package/templates/schedules/daily-backup.json +12 -0
  171. package/templates/schedules/daily-cleanup.json +12 -0
  172. package/templates/schedules/hourly-health-check.json +12 -0
  173. package/templates/schedules/webhook-on-push.json +13 -0
  174. package/templates/schedules/weekly-digest.json +13 -0
  175. package/bizar-dash/dist/assets/main-DaC1Lc6q.js +0 -366
  176. package/bizar-dash/dist/assets/main-DaC1Lc6q.js.map +0 -1
  177. package/bizar-dash/dist/assets/main-DfmIfOUS.css +0 -1
  178. package/bizar-dash/dist/assets/mobile-CL5uUQEC.js +0 -1
  179. package/bizar-dash/dist/assets/mobile-D5WTWvuh.js +0 -338
  180. package/bizar-dash/dist/assets/mobile-D5WTWvuh.js.map +0 -1
  181. package/bizar-dash/src/web/views/settings/BackupSection.tsx +0 -16
  182. package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +0 -16
  183. package/bizar-dash/src/web/views/settings/SkillsSection.tsx +0 -16
@@ -0,0 +1,317 @@
1
+ // src/views/Doctor.tsx — v6.0.0 Doctor page.
2
+ //
3
+ // Full-page Doctor view: shows system health, service health, counts,
4
+ // recent errors, and action buttons. Refreshes every 30 seconds via
5
+ // the cheap `/api/doctor/health` endpoint and re-fetches the full
6
+ // snapshot on demand (initial mount, manual Refresh, or after a
7
+ // single-check POST so the operator sees the immediate result).
8
+ //
9
+ // The page deliberately leans on the existing Card component and the
10
+ // StatusBadge variants added in v6.0.0 (`ok`/`warn`/`fail`). Layout
11
+ // is a single column on narrow viewports, two columns on desktop —
12
+ // matching the rest of the dashboard's grid.
13
+
14
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
15
+ import {
16
+ CheckCircle2,
17
+ AlertTriangle,
18
+ AlertOctagon,
19
+ Download,
20
+ FileText,
21
+ RefreshCw,
22
+ PlayCircle,
23
+ Stethoscope,
24
+ Clock,
25
+ } from 'lucide-react';
26
+ import { Card, CardTitle, CardMeta } from '../components/Card';
27
+ import { Button } from '../components/Button';
28
+ import { DoctorPanel } from '../components/DoctorPanel';
29
+ import { StatusBadge } from '../components/StatusBadge';
30
+ import { Spinner } from '../components/Spinner';
31
+ import { useToast } from '../components/Toast';
32
+ import { api } from '../lib/api';
33
+ import { cn, formatTime } from '../lib/utils';
34
+ import type { DoctorSnapshot, DoctorCheck, Settings, Snapshot } from '../lib/types';
35
+
36
+ const REFRESH_INTERVAL_MS = 30_000;
37
+
38
+ type Props = {
39
+ snapshot: Snapshot;
40
+ settings: Settings;
41
+ activeTab: string;
42
+ setActiveTab: (id: string) => void;
43
+ refreshSnapshot: () => Promise<void>;
44
+ };
45
+
46
+ function DoctorInner({
47
+ setActiveTab,
48
+ }: Props) {
49
+ const toast = useToast();
50
+ const [data, setData] = useState<DoctorSnapshot | null>(null);
51
+ const [loading, setLoading] = useState(true);
52
+ const [refreshing, setRefreshing] = useState(false);
53
+ const [lastFetched, setLastFetched] = useState<number | null>(null);
54
+ const [autoRefresh, setAutoRefresh] = useState(true);
55
+ const cancelledRef = useRef(false);
56
+
57
+ const fetchSnapshot = useCallback(async () => {
58
+ setRefreshing(true);
59
+ try {
60
+ const r = await api.get<DoctorSnapshot>('/doctor');
61
+ if (!cancelledRef.current) {
62
+ setData(r);
63
+ setLastFetched(Date.now());
64
+ setLoading(false);
65
+ }
66
+ } catch (err) {
67
+ if (!cancelledRef.current) {
68
+ setLoading(false);
69
+ toast.error(`Doctor refresh failed: ${(err as Error).message}`);
70
+ }
71
+ } finally {
72
+ if (!cancelledRef.current) setRefreshing(false);
73
+ }
74
+ }, [toast]);
75
+
76
+ // Initial fetch on mount
77
+ useEffect(() => {
78
+ cancelledRef.current = false;
79
+ fetchSnapshot();
80
+ return () => { cancelledRef.current = true; };
81
+ }, [fetchSnapshot]);
82
+
83
+ // 30s auto-refresh via the cheap health endpoint so the page
84
+ // stays current without burning a full snapshot every tick. We
85
+ // only trigger a full snapshot fetch if status changed.
86
+ useEffect(() => {
87
+ if (!autoRefresh) return;
88
+ const tick = async () => {
89
+ try {
90
+ const r = await api.get<{ status: string; issues: DoctorCheck[] }>('/doctor/health');
91
+ setData((cur) => {
92
+ if (!cur) return cur;
93
+ if (cur.health.status !== r.status || cur.health.issues.length !== r.issues.length) {
94
+ // Status changed — re-fetch the full snapshot
95
+ fetchSnapshot();
96
+ }
97
+ return cur;
98
+ });
99
+ } catch { /* non-fatal */ }
100
+ };
101
+ const id = setInterval(tick, REFRESH_INTERVAL_MS);
102
+ return () => clearInterval(id);
103
+ }, [autoRefresh, fetchSnapshot]);
104
+
105
+ const onRunCheck = async (checkName: string) => {
106
+ try {
107
+ const r = await api.post<DoctorCheck>('/doctor/check', { checkName });
108
+ toast.info(`${r.name}: ${r.message}`, 4000);
109
+ // Re-fetch so the panel reflects the fresh result
110
+ await fetchSnapshot();
111
+ } catch (err) {
112
+ const apiErr = err as { status?: number; data?: { message?: string } };
113
+ toast.error(`Check failed: ${apiErr.data?.message ?? (err as Error).message}`);
114
+ }
115
+ };
116
+
117
+ const onExportJson = () => {
118
+ if (!data) return;
119
+ try {
120
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
121
+ const url = URL.createObjectURL(blob);
122
+ const a = document.createElement('a');
123
+ a.href = url;
124
+ a.download = `doctor-snapshot-${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
125
+ document.body.appendChild(a);
126
+ a.click();
127
+ document.body.removeChild(a);
128
+ URL.revokeObjectURL(url);
129
+ } catch (err) {
130
+ toast.error(`Export failed: ${(err as Error).message}`);
131
+ }
132
+ };
133
+
134
+ const onViewLogs = () => {
135
+ // Logs live on the Settings → Activity Log surface (already
136
+ // implemented). Set the tab and let the user browse.
137
+ setActiveTab('settings');
138
+ };
139
+
140
+ const counts = data?.counts;
141
+ const countsInfo = useMemo(() => {
142
+ if (!counts) return [];
143
+ return [
144
+ { label: 'Tasks', value: counts.tasks },
145
+ { label: 'Schedules', value: counts.schedules },
146
+ { label: 'Mods', value: counts.mods },
147
+ { label: 'Providers', value: counts.providers },
148
+ { label: 'MCPs', value: counts.mcps },
149
+ { label: 'Agents', value: counts.agents },
150
+ { label: 'Projects', value: counts.projects },
151
+ { label: 'Workspaces', value: counts.workspaces },
152
+ { label: 'Voice notes', value: counts.voiceNotes },
153
+ { label: 'Eval runs', value: counts.evalRuns },
154
+ { label: 'Backups', value: counts.backups },
155
+ ];
156
+ }, [counts]);
157
+
158
+ if (loading) {
159
+ return (
160
+ <div className="view view-doctor view-loading">
161
+ <Spinner size="lg" />
162
+ <p>Running health checks…</p>
163
+ </div>
164
+ );
165
+ }
166
+
167
+ if (!data) {
168
+ return (
169
+ <div className="view view-doctor">
170
+ <Card>
171
+ <CardTitle>
172
+ <AlertOctagon size={14} /> Doctor unavailable
173
+ </CardTitle>
174
+ <CardMeta>Could not reach /api/doctor.</CardMeta>
175
+ <div className="doctor-fallback-actions">
176
+ <Button variant="primary" onClick={fetchSnapshot}>
177
+ <RefreshCw size={12} /> Retry
178
+ </Button>
179
+ </div>
180
+ </Card>
181
+ </div>
182
+ );
183
+ }
184
+
185
+ const overall = data.health.status;
186
+ const OverallIcon =
187
+ overall === 'ok' ? CheckCircle2 : overall === 'warn' ? AlertTriangle : AlertOctagon;
188
+ const overallLabel =
189
+ overall === 'ok' ? 'Healthy' : overall === 'warn' ? 'Warning' : 'Failed';
190
+
191
+ return (
192
+ <div className="view view-doctor" data-overall={overall}>
193
+ <div className="doctor-header">
194
+ <div className="doctor-header-row">
195
+ <div className="doctor-header-title">
196
+ <Stethoscope size={20} aria-hidden />
197
+ <h1>Doctor</h1>
198
+ </div>
199
+ <div className="doctor-header-status">
200
+ <span className={cn('doctor-overall-pill', `doctor-overall-${overall}`)}>
201
+ <OverallIcon size={14} aria-hidden />
202
+ <span className="doctor-overall-label">{overallLabel}</span>
203
+ <StatusBadge kind={overall} dot>
204
+ {data.health.issues.length} issue{data.health.issues.length === 1 ? '' : 's'}
205
+ </StatusBadge>
206
+ </span>
207
+ </div>
208
+ </div>
209
+ <div className="doctor-header-meta muted">
210
+ <Clock size={12} aria-hidden />
211
+ <span>
212
+ v{data.bizarVersion} · {data.platform}/{data.arch} ·{' '}
213
+ up {Math.floor(data.uptime)}s
214
+ {lastFetched ? ` · refreshed ${formatTime(new Date(lastFetched))}` : ''}
215
+ </span>
216
+ </div>
217
+ </div>
218
+
219
+ <div className="doctor-grid">
220
+ <DoctorPanel
221
+ category="system"
222
+ checks={data.checks.system}
223
+ meta={`${data.platform}/${data.arch}`}
224
+ />
225
+ <DoctorPanel
226
+ category="services"
227
+ checks={data.checks.services}
228
+ meta={
229
+ data.services.opencode?.reachable
230
+ ? `opencode on ${data.services.opencode.port ?? '?'}`
231
+ : 'opencode not connected'
232
+ }
233
+ />
234
+ <DoctorPanel
235
+ category="counts"
236
+ checks={[]}
237
+ info={countsInfo}
238
+ meta={counts?.activeProject ? `active: ${counts.activeProject}` : 'no active project'}
239
+ />
240
+ <Card className="doctor-errors-card">
241
+ <CardTitle>
242
+ <AlertOctagon size={14} aria-hidden />
243
+ <span>Recent errors (last hour)</span>
244
+ <span className="card-meta-inline muted">
245
+ {data.recentErrors.length} line{data.recentErrors.length === 1 ? '' : 's'}
246
+ </span>
247
+ </CardTitle>
248
+ {data.recentErrors.length === 0 ? (
249
+ <div className="muted doctor-errors-empty">No errors in the last hour. Nice.</div>
250
+ ) : (
251
+ <ul className="doctor-errors-list" role="list">
252
+ {data.recentErrors.map((e, idx) => (
253
+ <li key={`${e.tsMs ?? idx}-${idx}`} className="doctor-error-row">
254
+ <span className="doctor-error-ts mono">
255
+ {e.ts ? formatTime(new Date(e.ts)) : '??:??:??'}
256
+ </span>
257
+ <span className="doctor-error-line mono">{e.line}</span>
258
+ </li>
259
+ ))}
260
+ </ul>
261
+ )}
262
+ </Card>
263
+ <Card className="doctor-actions-card">
264
+ <CardTitle>
265
+ <PlayCircle size={14} aria-hidden /> Actions
266
+ </CardTitle>
267
+ <CardMeta>Run a check, view logs, or export the snapshot.</CardMeta>
268
+ <div className="doctor-actions-row">
269
+ <Button variant="primary" onClick={fetchSnapshot} disabled={refreshing}>
270
+ {refreshing ? <Spinner size="sm" /> : <RefreshCw size={12} />}
271
+ {refreshing ? 'Running…' : 'Run Health Check Now'}
272
+ </Button>
273
+ <Button variant="secondary" onClick={onViewLogs}>
274
+ <FileText size={12} /> View Full Logs
275
+ </Button>
276
+ <Button variant="secondary" onClick={onExportJson}>
277
+ <Download size={12} /> Export Diagnostics as JSON
278
+ </Button>
279
+ <Button
280
+ variant="ghost"
281
+ onClick={() => setAutoRefresh((v) => !v)}
282
+ title="Toggle auto-refresh"
283
+ >
284
+ {autoRefresh ? 'Pause auto-refresh' : 'Resume auto-refresh'}
285
+ </Button>
286
+ </div>
287
+ {data.checks.services.some((c) => c.status !== 'ok') ||
288
+ data.checks.config.some((c) => c.status !== 'ok') ? (
289
+ <div className="doctor-actions-rerun">
290
+ <span className="muted">Rerun a single check:</span>
291
+ <div className="doctor-actions-rerun-row">
292
+ {[
293
+ ...data.checks.services,
294
+ ...data.checks.config,
295
+ ]
296
+ .filter((c) => c.status !== 'ok')
297
+ .map((c) => (
298
+ <Button
299
+ key={c.name}
300
+ variant="ghost"
301
+ size="sm"
302
+ onClick={() => onRunCheck(c.name)}
303
+ title={`Run check: ${c.name}`}
304
+ >
305
+ <PlayCircle size={10} /> {c.name}
306
+ </Button>
307
+ ))}
308
+ </div>
309
+ </div>
310
+ ) : null}
311
+ </Card>
312
+ </div>
313
+ </div>
314
+ );
315
+ }
316
+
317
+ export const Doctor = React.memo(DoctorInner);
@@ -10,11 +10,14 @@
10
10
  import React, { useCallback, useState } from 'react';
11
11
  import {
12
12
  Brain,
13
+ Clipboard,
13
14
  FileText,
14
15
  GitBranch,
15
16
  LayoutDashboard,
17
+ Mic,
16
18
  Network,
17
19
  RefreshCw,
20
+ Scan,
18
21
  Search as SearchIcon,
19
22
  Sliders,
20
23
  } from 'lucide-react';
@@ -28,8 +31,11 @@ import { GitSyncPanel } from './memory/GitSyncPanel';
28
31
  import { SemanticSearchPanel } from './memory/SemanticSearchPanel';
29
32
  import { ConfigPanel } from './memory/ConfigPanel';
30
33
  import { MemoryGraphPanel } from './memory/MemoryGraphPanel';
34
+ import { FromScreenshotPanel } from './memory/FromScreenshotPanel';
35
+ import { VaultFromClipboardPanel } from './memory/VaultFromClipboardPanel';
36
+ import { VoiceNotesPanel } from '../components/VoiceNotesPanel';
31
37
 
32
- type SubPanel = 'overview' | 'lightrag' | 'obsidian' | 'git' | 'semantic' | 'config' | 'graph';
38
+ type SubPanel = 'overview' | 'lightrag' | 'obsidian' | 'git' | 'semantic' | 'config' | 'graph' | 'webclip' | 'screenshot' | 'voice';
33
39
 
34
40
  type Props = {
35
41
  snapshot: unknown;
@@ -51,6 +57,9 @@ const SOURCES: Array<{
51
57
  { id: 'semantic', label: 'Semantic Search', icon: SearchIcon },
52
58
  { id: 'config', label: 'Config', icon: Sliders },
53
59
  { id: 'graph', label: 'Memory Graph', icon: Network },
60
+ { id: 'webclip', label: 'Web Clip', icon: Clipboard },
61
+ { id: 'screenshot', label: 'Screenshot OCR', icon: Scan },
62
+ { id: 'voice', label: 'Voice Notes', icon: Mic },
54
63
  ];
55
64
 
56
65
  function MemoryInner(_props: Props) {
@@ -83,6 +92,12 @@ function MemoryInner(_props: Props) {
83
92
  return <ConfigPanel refreshKey={refreshKey} />;
84
93
  case 'graph':
85
94
  return <MemoryGraphPanel refreshKey={refreshKey} />;
95
+ case 'webclip':
96
+ return <VaultFromClipboardPanel refreshKey={refreshKey} />;
97
+ case 'screenshot':
98
+ return <FromScreenshotPanel refreshKey={refreshKey} />;
99
+ case 'voice':
100
+ return <VoiceNotesPanel refreshKey={refreshKey} />;
86
101
  default:
87
102
  return <Card>Unknown panel: {active}</Card>;
88
103
  }
@@ -97,7 +112,7 @@ function MemoryInner(_props: Props) {
97
112
  <Brain size={18} /> Memory
98
113
  </h2>
99
114
  <p className="view-subtitle">
100
- LightRAG, Obsidian vault, git sync, and semantic search — all in one place.
115
+ LightRAG, Obsidian vault, git sync, semantic search, web clips, and screenshot OCR — all in one place.
101
116
  </p>
102
117
  </div>
103
118
  <div className="view-actions">
@@ -784,6 +784,7 @@ function QuotaBar({ icon, label, remainingPct, consumedPct, used, total, resetIn
784
784
  <div className="minimax-quota-head">
785
785
  <span className="minimax-quota-label">{icon} {label}</span>
786
786
  <span className="minimax-quota-remaining">{remainingPct}% remaining</span>
787
+ <span className="minimax-quota-consumed">· {consumedPct}% used</span>
787
788
  </div>
788
789
  <div className="minimax-bar">
789
790
  <div className="minimax-bar-fill" style={{ width: `${consumedPct}%` }} aria-label={`${consumedPct}% consumed`} />
@@ -13,6 +13,7 @@ import {
13
13
  RefreshCw,
14
14
  History,
15
15
  ChevronDown,
16
+ LayoutTemplate,
16
17
  } from 'lucide-react';
17
18
  import { Card, CardTitle, CardMeta } from '../components/Card';
18
19
  import { Button } from '../components/Button';
@@ -23,6 +24,8 @@ import { useModal } from '../components/Modal';
23
24
  import { api } from '../lib/api';
24
25
  import { formatRelative } from '../lib/utils';
25
26
  import type { Schedule, Settings, Snapshot } from '../lib/types';
27
+ import { ScheduleTemplateCard } from '../components/ScheduleTemplateCard';
28
+ import type { ScheduleTemplate } from '../components/ScheduleTemplateCard';
26
29
 
27
30
  type Props = {
28
31
  snapshot: Snapshot;
@@ -221,6 +224,9 @@ export function Schedules({ snapshot, refreshSnapshot }: Props) {
221
224
  const modal = useModal();
222
225
  const [schedules, setSchedules] = useState<Schedule[]>(snapshot.schedules || []);
223
226
  const [loading, setLoading] = useState(!snapshot.schedules);
227
+ const [activeSection, setActiveSection] = useState<'schedules' | 'templates'>('schedules');
228
+ const [templates, setTemplates] = useState<ScheduleTemplate[]>([]);
229
+ const [templatesLoading, setTemplatesLoading] = useState(false);
224
230
 
225
231
  const reload = async () => {
226
232
  try {
@@ -235,6 +241,18 @@ export function Schedules({ snapshot, refreshSnapshot }: Props) {
235
241
  }
236
242
  };
237
243
 
244
+ const loadTemplates = async () => {
245
+ setTemplatesLoading(true);
246
+ try {
247
+ const r = await api.get<{ templates: ScheduleTemplate[] }>('/schedules/templates');
248
+ setTemplates(r.templates || []);
249
+ } catch (err) {
250
+ toast.error(`Templates load failed: ${(err as Error).message}`);
251
+ } finally {
252
+ setTemplatesLoading(false);
253
+ }
254
+ };
255
+
238
256
  useEffect(() => {
239
257
  if (snapshot.schedules?.length || snapshot.schedules) {
240
258
  setSchedules(snapshot.schedules || []);
@@ -245,6 +263,12 @@ export function Schedules({ snapshot, refreshSnapshot }: Props) {
245
263
  // eslint-disable-next-line react-hooks/exhaustive-deps
246
264
  }, [snapshot.schedules]);
247
265
 
266
+ useEffect(() => {
267
+ if (activeSection === 'templates' && templates.length === 0) {
268
+ loadTemplates();
269
+ }
270
+ }, [activeSection]);
271
+
248
272
  const openEditor = (initial?: Schedule) => {
249
273
  modal.open({
250
274
  title: initial ? `Edit schedule: ${initial.name}` : 'New schedule',
@@ -294,6 +318,20 @@ export function Schedules({ snapshot, refreshSnapshot }: Props) {
294
318
  }
295
319
  };
296
320
 
321
+ const onUseTemplate = async (template: ScheduleTemplate) => {
322
+ try {
323
+ const saved = await api.post<Schedule>('/schedules/from-template', {
324
+ templateId: template.id,
325
+ });
326
+ setSchedules((cur) => [...cur, saved]);
327
+ setActiveSection('schedules');
328
+ toast.success(`Schedule "${saved.name}" created from template.`);
329
+ await refreshSnapshot();
330
+ } catch (err) {
331
+ toast.error(`Failed to create schedule: ${(err as Error).message}`);
332
+ }
333
+ };
334
+
297
335
  if (loading) {
298
336
  return (
299
337
  <div className="view-loading"><Spinner size="lg" /></div>
@@ -304,25 +342,69 @@ export function Schedules({ snapshot, refreshSnapshot }: Props) {
304
342
  <div className="view view-schedules">
305
343
  <header className="view-header">
306
344
  <div className="view-header-text">
307
- <h2 className="view-title">
308
- <Clock size={18} /> Schedules ({schedules.length})
309
- </h2>
345
+ <div className="view-title-row">
346
+ <h2 className="view-title">
347
+ <Clock size={18} /> Schedules
348
+ </h2>
349
+ <div className="view-section-tabs">
350
+ <button
351
+ type="button"
352
+ className={`tab-btn ${activeSection === 'schedules' ? 'tab-btn-active' : ''}`}
353
+ onClick={() => setActiveSection('schedules')}
354
+ >
355
+ Schedules ({schedules.length})
356
+ </button>
357
+ <button
358
+ type="button"
359
+ className={`tab-btn ${activeSection === 'templates' ? 'tab-btn-active' : ''}`}
360
+ onClick={() => setActiveSection('templates')}
361
+ >
362
+ <LayoutTemplate size={14} /> Templates
363
+ </button>
364
+ </div>
365
+ </div>
310
366
  <p className="view-subtitle">
311
- Recurring tasks for the active project: <strong>{snapshot.activeProject?.name || '(none)'}</strong>.
312
- Service daemon runs them at the right time.
367
+ {activeSection === 'schedules'
368
+ ? <>Recurring tasks for <strong>{snapshot.activeProject?.name || '(none)'}</strong>. Service daemon runs them at the right time.</>
369
+ : 'Example schedules you can use as a starting point.'}
313
370
  </p>
314
371
  </div>
315
372
  <div className="view-actions">
316
- <Button variant="secondary" size="sm" onClick={reload}>
317
- <RefreshCw size={14} /> Refresh
318
- </Button>
319
- <Button variant="primary" size="sm" onClick={() => openEditor()}>
320
- <Plus size={14} /> New schedule
321
- </Button>
373
+ {activeSection === 'schedules' && (
374
+ <>
375
+ <Button variant="secondary" size="sm" onClick={reload}>
376
+ <RefreshCw size={14} /> Refresh
377
+ </Button>
378
+ <Button variant="primary" size="sm" onClick={() => openEditor()}>
379
+ <Plus size={14} /> New schedule
380
+ </Button>
381
+ </>
382
+ )}
383
+ {activeSection === 'templates' && (
384
+ <Button variant="secondary" size="sm" onClick={loadTemplates} disabled={templatesLoading}>
385
+ <RefreshCw size={14} /> Refresh
386
+ </Button>
387
+ )}
322
388
  </div>
323
389
  </header>
324
390
 
325
- {schedules.length === 0 ? (
391
+ {activeSection === 'templates' ? (
392
+ templatesLoading ? (
393
+ <div className="view-loading"><Spinner size="lg" /></div>
394
+ ) : templates.length === 0 ? (
395
+ <EmptyState
396
+ icon={<LayoutTemplate size={32} />}
397
+ title="No templates"
398
+ message="No schedule templates found. Templates are loaded from templates/schedules/."
399
+ />
400
+ ) : (
401
+ <div className="schedule-template-grid">
402
+ {templates.map((t) => (
403
+ <ScheduleTemplateCard key={t.id} template={t} onUse={onUseTemplate} />
404
+ ))}
405
+ </div>
406
+ )
407
+ ) : schedules.length === 0 ? (
326
408
  <EmptyState
327
409
  icon={<Clock size={32} />}
328
410
  title="No schedules"