@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.
- package/bizar-dash/dist/assets/icons-Bo0iH9EC.js +701 -0
- package/bizar-dash/dist/assets/icons-Bo0iH9EC.js.map +1 -0
- package/bizar-dash/dist/assets/{index-DU61awG3.js → index-DmpSFPJY.js} +1 -1
- package/bizar-dash/dist/assets/{index-DU61awG3.js.map → index-DmpSFPJY.js.map} +1 -1
- package/bizar-dash/dist/assets/main-C1cpttnv.js +19 -0
- package/bizar-dash/dist/assets/main-C1cpttnv.js.map +1 -0
- package/bizar-dash/dist/assets/main-DTkNlLrw.css +1 -0
- package/bizar-dash/dist/assets/markdown-DIquRulQ.js +29 -0
- package/bizar-dash/dist/assets/markdown-DIquRulQ.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-BtxQJftK.js +2 -0
- package/bizar-dash/dist/assets/mobile-BtxQJftK.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-DD-FZrTC.js +1 -0
- package/bizar-dash/dist/assets/{mobile-CL5uUQEC.js.map → mobile-DD-FZrTC.js.map} +1 -1
- package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js +40 -0
- package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.js.map +1 -0
- package/bizar-dash/dist/index.html +6 -3
- package/bizar-dash/dist/mobile.html +5 -2
- package/bizar-dash/node_modules/.package-lock.json +6 -0
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/package-lock.json +6 -0
- package/bizar-dash/skills/eval/SKILL.md +237 -0
- package/bizar-dash/src/server/api.mjs +35 -0
- package/bizar-dash/src/server/auth.mjs +155 -1
- package/bizar-dash/src/server/diagnostics-store.mjs +452 -2
- package/bizar-dash/src/server/eval-store.mjs +226 -0
- package/bizar-dash/src/server/eval.mjs +347 -0
- package/bizar-dash/src/server/memory-store.mjs +46 -0
- package/bizar-dash/src/server/ocr.mjs +55 -0
- package/bizar-dash/src/server/plugins/registry.mjs +363 -0
- package/bizar-dash/src/server/plugins/sandbox.mjs +655 -0
- package/bizar-dash/src/server/plugins/store.mjs +659 -0
- package/bizar-dash/src/server/providers-store.mjs +11 -4
- package/bizar-dash/src/server/routes/_shared.mjs +2 -2
- package/bizar-dash/src/server/routes/clipboard.mjs +173 -0
- package/bizar-dash/src/server/routes/doctor.mjs +71 -0
- package/bizar-dash/src/server/routes/env-vars.mjs +67 -1
- package/bizar-dash/src/server/routes/eval.mjs +147 -0
- package/bizar-dash/src/server/routes/memory.mjs +3 -0
- package/bizar-dash/src/server/routes/ocr.mjs +182 -0
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +61 -42
- package/bizar-dash/src/server/routes/plugins.mjs +220 -0
- package/bizar-dash/src/server/routes/schedules.mjs +55 -0
- package/bizar-dash/src/server/routes/users.mjs +84 -0
- package/bizar-dash/src/server/routes/voice.mjs +131 -0
- package/bizar-dash/src/server/routes/workspaces.mjs +204 -0
- package/bizar-dash/src/server/serve-info.mjs +172 -0
- package/bizar-dash/src/server/voice-store.mjs +202 -0
- package/bizar-dash/src/server/voice-transcribe.mjs +72 -0
- package/bizar-dash/src/server/workspaces.mjs +626 -0
- package/bizar-dash/src/web/App.tsx +48 -11
- package/bizar-dash/src/web/components/AutosaveField.tsx +50 -0
- package/bizar-dash/src/web/components/DoctorPanel.tsx +160 -0
- package/bizar-dash/src/web/components/EnvVarManager.tsx +245 -62
- package/bizar-dash/src/web/components/InviteDialog.tsx +205 -0
- package/bizar-dash/src/web/components/ScheduleTemplateCard.tsx +71 -0
- package/bizar-dash/src/web/components/ScreenshotCapture.tsx +138 -0
- package/bizar-dash/src/web/components/ScreenshotOCR.tsx +42 -0
- package/bizar-dash/src/web/components/SettingsNav.tsx +101 -0
- package/bizar-dash/src/web/components/Sidebar.tsx +25 -1
- package/bizar-dash/src/web/components/StatusBadge.tsx +33 -2
- package/bizar-dash/src/web/components/Topbar.tsx +11 -0
- package/bizar-dash/src/web/components/VoiceNotesPanel.tsx +182 -0
- package/bizar-dash/src/web/components/VoiceRecorder.tsx +104 -0
- package/bizar-dash/src/web/components/WorkspaceSelector.tsx +158 -0
- package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +64 -0
- package/bizar-dash/src/web/components/chat/Composer.tsx +1 -1
- package/bizar-dash/src/web/components/chat/useChat.ts +118 -1
- package/bizar-dash/src/web/hooks/useAutosave.ts +107 -0
- package/bizar-dash/src/web/lib/types.ts +134 -0
- package/bizar-dash/src/web/styles/chat.css +8 -5
- package/bizar-dash/src/web/styles/main.css +77 -2
- package/bizar-dash/src/web/styles/memory.css +82 -0
- package/bizar-dash/src/web/styles/settings.css +265 -0
- package/bizar-dash/src/web/views/Chat.tsx +15 -1
- package/bizar-dash/src/web/views/Doctor.tsx +317 -0
- package/bizar-dash/src/web/views/Memory.tsx +17 -2
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +1 -0
- package/bizar-dash/src/web/views/Schedules.tsx +94 -12
- package/bizar-dash/src/web/views/Settings.tsx +78 -45
- package/bizar-dash/src/web/views/Workspace.tsx +294 -0
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +114 -133
- package/bizar-dash/src/web/views/memory/FromScreenshotPanel.tsx +23 -0
- package/bizar-dash/src/web/views/memory/VaultFromClipboardPanel.tsx +101 -0
- package/bizar-dash/src/web/views/settings/AgentSection.tsx +23 -12
- package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +5 -6
- package/bizar-dash/src/web/views/settings/GeneralSection.tsx +38 -15
- package/bizar-dash/src/web/views/settings/MemorySection.tsx +92 -8
- package/bizar-dash/src/web/views/settings/WorkspacesSection.tsx +189 -0
- package/bizar-dash/tests/autosave.test.tsx +276 -0
- package/bizar-dash/tests/bundle-analysis.test.mjs +5 -2
- package/bizar-dash/tests/chat-composer.test.tsx +140 -0
- package/bizar-dash/tests/clipboard.test.mjs +147 -0
- package/bizar-dash/tests/components/doctor-panel.test.tsx +105 -0
- package/bizar-dash/tests/components/screenshot-ocr.test.tsx +75 -0
- package/bizar-dash/tests/components/workspace-selector.test.tsx +73 -0
- package/bizar-dash/tests/deploy-templates.test.mjs +100 -0
- package/bizar-dash/tests/diagnostics-store.test.mjs +206 -0
- package/bizar-dash/tests/env-vars-extended.test.mjs +190 -0
- package/bizar-dash/tests/eval/fixtures.test.mjs +141 -0
- package/bizar-dash/tests/eval/report.test.mjs +284 -0
- package/bizar-dash/tests/eval/runner.test.mjs +471 -0
- package/bizar-dash/tests/layout-spacing.test.mjs +101 -0
- package/bizar-dash/tests/memory-default-vault.test.mjs +98 -0
- package/bizar-dash/tests/minimax-bar.test.tsx +113 -0
- package/bizar-dash/tests/minimax-models.test.mjs +123 -0
- package/bizar-dash/tests/ocr.test.mjs +87 -0
- package/bizar-dash/tests/opencode-sessions-detail.test.mjs +158 -0
- package/bizar-dash/tests/plugins-registry.test.mjs +387 -0
- package/bizar-dash/tests/plugins-sandbox.test.mjs +374 -0
- package/bizar-dash/tests/plugins-store.test.mjs +455 -0
- package/bizar-dash/tests/routes-doctor.test.mjs +159 -0
- package/bizar-dash/tests/schedules-templates.test.mjs +144 -0
- package/bizar-dash/tests/settings-layout.test.tsx +129 -0
- package/bizar-dash/tests/settings-mode-wiring.test.tsx +151 -0
- package/bizar-dash/tests/settings-nav.test.tsx +126 -0
- package/bizar-dash/tests/users.test.mjs +108 -0
- package/bizar-dash/tests/voice-recorder.test.tsx +95 -0
- package/bizar-dash/tests/voice-store.test.mjs +148 -0
- package/bizar-dash/tests/voice-transcribe.test.mjs +87 -0
- package/bizar-dash/tests/workspaces.test.mjs +527 -0
- package/cli/bin.mjs +72 -2
- package/cli/commands/clip.mjs +146 -0
- package/cli/commands/deploy/cloudflare.mjs +250 -0
- package/cli/commands/deploy/docker.mjs +221 -0
- package/cli/commands/deploy/fly.mjs +161 -0
- package/cli/commands/deploy/vercel.mjs +225 -0
- package/cli/commands/deploy.mjs +240 -0
- package/cli/commands/eval.mjs +378 -0
- package/cli/commands/marketplace.mjs +64 -0
- package/cli/commands/minimax.mjs +5 -2
- package/cli/commands/ocr.mjs +165 -0
- package/cli/commands/plugin.mjs +358 -0
- package/cli/commands/voice.mjs +211 -0
- package/cli/commands/workspace.mjs +247 -0
- package/cli/tests/minimax-cli.test.mjs +79 -0
- package/config/agents/frigg.md +1 -1
- package/config/agents/heimdall.md +1 -1
- package/config/agents/mimir.md +1 -1
- package/config/agents/quick.md +1 -1
- package/config/agents/semble-search.md +1 -1
- package/config/agents/vor.md +1 -1
- package/config/opencode.json +28 -21
- package/config/opencode.json.template +8 -1
- package/package.json +12 -8
- package/plugins/bizar/index.ts +77 -0
- package/plugins/bizar/src/compaction.d.mts +48 -0
- package/plugins/bizar/src/compaction.mjs +192 -0
- package/plugins/bizar/tests/compaction.test.ts +264 -0
- package/templates/deploy/cloudflare/README.md +32 -0
- package/templates/deploy/cloudflare/functions-index.template.js +15 -0
- package/templates/deploy/cloudflare/wrangler.toml.template +9 -0
- package/templates/deploy/docker/.env.template +16 -0
- package/templates/deploy/docker/README.md +58 -0
- package/templates/deploy/docker/docker-compose.template.yml +23 -0
- package/templates/deploy/fly/README.md +35 -0
- package/templates/deploy/fly/fly.toml.template +28 -0
- package/templates/deploy/vercel/README.md +29 -0
- package/templates/deploy/vercel/api-index.template.js +18 -0
- package/templates/deploy/vercel/vercel.json.template +16 -0
- package/templates/eval-fixtures/README.md +58 -0
- package/templates/eval-fixtures/code-search-basic.json +28 -0
- package/templates/eval-fixtures/latency-bounds.json +16 -0
- package/templates/eval-fixtures/regression-suite.json +79 -0
- package/templates/eval-fixtures/response-format.json +30 -0
- package/templates/eval-fixtures/tool-call-correctness.json +24 -0
- package/templates/plugin-template/README.md +121 -0
- package/templates/plugin-template/index.js +66 -0
- package/templates/plugin-template/plugin.json +42 -0
- package/templates/plugin-template/tests/plugin.test.js +83 -0
- package/templates/schedules/daily-backup.json +12 -0
- package/templates/schedules/daily-cleanup.json +12 -0
- package/templates/schedules/hourly-health-check.json +12 -0
- package/templates/schedules/webhook-on-push.json +13 -0
- package/templates/schedules/weekly-digest.json +13 -0
- package/bizar-dash/dist/assets/main-DaC1Lc6q.js +0 -366
- package/bizar-dash/dist/assets/main-DaC1Lc6q.js.map +0 -1
- package/bizar-dash/dist/assets/main-DfmIfOUS.css +0 -1
- package/bizar-dash/dist/assets/mobile-CL5uUQEC.js +0 -1
- package/bizar-dash/dist/assets/mobile-D5WTWvuh.js +0 -338
- package/bizar-dash/dist/assets/mobile-D5WTWvuh.js.map +0 -1
- package/bizar-dash/src/web/views/settings/BackupSection.tsx +0 -16
- package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +0 -16
- 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,
|
|
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
|
-
<
|
|
308
|
-
<
|
|
309
|
-
|
|
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
|
-
|
|
312
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
{
|
|
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"
|