@polderlabs/bizar 5.2.0 → 5.3.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/{MobileChat-TCputYzr.js → MobileChat-OidrxXR9.js} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-TCputYzr.js.map → MobileChat-OidrxXR9.js.map} +1 -1
- package/bizar-dash/dist/assets/{MobileSettings-BxcbL5XT.js → MobileSettings-B91gVFii.js} +1 -1
- package/bizar-dash/dist/assets/{MobileSettings-BxcbL5XT.js.map → MobileSettings-B91gVFii.js.map} +1 -1
- package/bizar-dash/dist/assets/{icons-DRDXfbBP.js → icons-Bd8piSb2.js} +18 -13
- package/bizar-dash/dist/assets/icons-Bd8piSb2.js.map +1 -0
- package/bizar-dash/dist/assets/{main-xFpWMd32.css → main-3G6mOhiK.css} +1 -1
- package/bizar-dash/dist/assets/main-6YUZY968.js +19 -0
- package/bizar-dash/dist/assets/main-6YUZY968.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-j3rOZK6v.js → mobile-CSsp30lr.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-j3rOZK6v.js.map → mobile-CSsp30lr.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile--17fkfrl.js → mobile-DtwK7DOV.js} +1 -1
- package/bizar-dash/dist/assets/{mobile--17fkfrl.js.map → mobile-DtwK7DOV.js.map} +1 -1
- package/bizar-dash/dist/assets/{useSlashCommands-BG-DhEck.js → useSlashCommands-DEkxEvOO.js} +2 -2
- package/bizar-dash/dist/assets/{useSlashCommands-BG-DhEck.js.map → useSlashCommands-DEkxEvOO.js.map} +1 -1
- package/bizar-dash/dist/index.html +5 -5
- package/bizar-dash/dist/mobile.html +3 -3
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/auth.mjs +14 -1
- package/bizar-dash/src/server/eval-store.mjs +55 -0
- package/bizar-dash/src/server/eval.mjs +63 -0
- package/bizar-dash/src/server/plugins/permission-audit.mjs +109 -0
- package/bizar-dash/src/server/plugins/sandbox.mjs +76 -2
- package/bizar-dash/src/server/plugins/store.mjs +4 -0
- package/bizar-dash/src/server/routes/eval.mjs +42 -2
- package/bizar-dash/src/server/routes/plugins.mjs +44 -4
- package/bizar-dash/src/server/routes/users.mjs +18 -0
- package/bizar-dash/src/server/schedules-runner.mjs +61 -0
- package/bizar-dash/src/web/components/EvalExportButton.tsx +27 -0
- package/bizar-dash/src/web/components/InstallConfirmDialog.tsx +62 -0
- package/bizar-dash/src/web/components/MarketplacePluginCard.tsx +51 -0
- package/bizar-dash/src/web/components/PluginCard.tsx +2 -2
- package/bizar-dash/src/web/components/PluginPermissions.tsx +1 -1
- package/bizar-dash/src/web/components/ScheduleTemplateCard.tsx +1 -0
- package/bizar-dash/src/web/components/Topbar.tsx +3 -0
- package/bizar-dash/src/web/components/VoiceNotesPanel.tsx +28 -0
- package/bizar-dash/src/web/styles/main.css +139 -0
- package/bizar-dash/src/web/views/Doctor.tsx +3 -3
- package/bizar-dash/src/web/views/Eval.tsx +225 -25
- package/bizar-dash/src/web/views/EvalReport.tsx +7 -3
- package/bizar-dash/src/web/views/Marketplace.tsx +135 -0
- package/bizar-dash/src/web/views/Plugins.tsx +9 -3
- package/bizar-dash/src/web/views/Schedules.tsx +1 -1
- package/bizar-dash/tests/a11y/components.test.tsx +149 -0
- package/bizar-dash/tests/a11y/forms.test.tsx +191 -0
- package/bizar-dash/tests/a11y/navigation.test.tsx +112 -0
- package/bizar-dash/tests/components/marketplace-plugin-card.test.tsx +144 -0
- package/bizar-dash/tests/eval-csv.test.mjs +67 -0
- package/bizar-dash/tests/eval-scheduled.test.mjs +84 -0
- package/bizar-dash/tests/plugins-permissions.test.mjs +525 -0
- package/cli/commands/dash.mjs +5 -0
- package/cli/commands/tailscale.mjs +8 -1
- package/package.json +1 -1
- package/bizar-dash/dist/assets/icons-DRDXfbBP.js.map +0 -1
- package/bizar-dash/dist/assets/main-D5Ditnrd.js +0 -19
- package/bizar-dash/dist/assets/main-D5Ditnrd.js.map +0 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/components/EvalExportButton.tsx — v5.3.0
|
|
2
|
+
// Export button for downloading eval run results as CSV.
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { Download } from 'lucide-react';
|
|
6
|
+
import { Button } from './Button';
|
|
7
|
+
import { api } from '../lib/api';
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
runId: string;
|
|
11
|
+
variant?: 'primary' | 'secondary' | 'ghost';
|
|
12
|
+
size?: 'sm' | 'md' | 'lg';
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function EvalExportButton({ runId, variant = 'secondary', size = 'sm' }: Props) {
|
|
16
|
+
const onExport = () => {
|
|
17
|
+
const token = api.getToken();
|
|
18
|
+
window.open(`/api/eval/runs/${encodeURIComponent(runId)}/export.csv?_t=${encodeURIComponent(token)}`, '_blank');
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Button variant={variant} size={size} onClick={onExport} title="Export results as CSV">
|
|
23
|
+
<Download size={14} aria-hidden />
|
|
24
|
+
Export CSV
|
|
25
|
+
</Button>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/web/components/InstallConfirmDialog.tsx — confirmation dialog for installing a marketplace plugin.
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { AlertTriangle } from 'lucide-react';
|
|
5
|
+
import { useModal } from './Modal';
|
|
6
|
+
import { Button } from './Button';
|
|
7
|
+
import { PluginPermissions } from './PluginPermissions';
|
|
8
|
+
import type { MarketplacePlugin } from './MarketplacePluginCard';
|
|
9
|
+
|
|
10
|
+
export type InstallConfirmDialogProps = {
|
|
11
|
+
plugin: MarketplacePlugin;
|
|
12
|
+
onConfirm: () => void | Promise<void>;
|
|
13
|
+
onCancel: () => void;
|
|
14
|
+
installing: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function InstallConfirmDialog({ plugin, onConfirm, onCancel, installing }: InstallConfirmDialogProps) {
|
|
18
|
+
const modal = useModal();
|
|
19
|
+
// Track the plugin id we've opened the modal for, so we don't re-open on every render.
|
|
20
|
+
const openedForRef = useRef<string | null>(null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!plugin) return;
|
|
24
|
+
// Only open if we haven't already opened for this plugin.
|
|
25
|
+
if (openedForRef.current === plugin.id) return;
|
|
26
|
+
openedForRef.current = plugin.id;
|
|
27
|
+
|
|
28
|
+
modal.open({
|
|
29
|
+
title: `Install ${plugin.name}?`,
|
|
30
|
+
width: 480,
|
|
31
|
+
onClose: () => {
|
|
32
|
+
openedForRef.current = null;
|
|
33
|
+
onCancel();
|
|
34
|
+
},
|
|
35
|
+
children: (
|
|
36
|
+
<div className="install-confirm">
|
|
37
|
+
<h3>{plugin.name} v{plugin.version}</h3>
|
|
38
|
+
{plugin.description && <p>{plugin.description}</p>}
|
|
39
|
+
|
|
40
|
+
<div className="install-warning">
|
|
41
|
+
<AlertTriangle size={16} />
|
|
42
|
+
<p>This plugin will run code on your machine. It requests these permissions:</p>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<PluginPermissions permissions={plugin.permissions ?? []} />
|
|
46
|
+
|
|
47
|
+
<div className="install-confirm-actions">
|
|
48
|
+
<Button onClick={onCancel} disabled={installing}>Cancel</Button>
|
|
49
|
+
<Button variant="primary" onClick={onConfirm} disabled={installing}>
|
|
50
|
+
{installing ? 'Installing...' : 'Install'}
|
|
51
|
+
</Button>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
),
|
|
55
|
+
});
|
|
56
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
57
|
+
}, [plugin, installing]);
|
|
58
|
+
|
|
59
|
+
// This component manages its own modal state via useModal.
|
|
60
|
+
// Render nothing in the React tree — the modal is rendered via portal.
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/web/components/MarketplacePluginCard.tsx — card for a plugin listed in the marketplace.
|
|
2
|
+
|
|
3
|
+
import { Card, CardTitle, CardMeta } from './Card';
|
|
4
|
+
import { Button } from './Button';
|
|
5
|
+
import { PluginPermissions } from './PluginPermissions';
|
|
6
|
+
|
|
7
|
+
export type MarketplacePlugin = {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
version: string;
|
|
11
|
+
author: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
category: string;
|
|
14
|
+
tags?: string[];
|
|
15
|
+
permissions?: string[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type MarketplacePluginCardProps = {
|
|
19
|
+
plugin: MarketplacePlugin;
|
|
20
|
+
onInstall: () => void;
|
|
21
|
+
onView: () => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function MarketplacePluginCard({ plugin, onInstall, onView }: MarketplacePluginCardProps) {
|
|
25
|
+
return (
|
|
26
|
+
<Card className="marketplace-plugin-card">
|
|
27
|
+
<div className="marketplace-plugin-head">
|
|
28
|
+
<div>
|
|
29
|
+
<CardTitle>{plugin.name}</CardTitle>
|
|
30
|
+
<CardMeta>v{plugin.version} · {plugin.category}</CardMeta>
|
|
31
|
+
</div>
|
|
32
|
+
<div className="marketplace-plugin-author">{plugin.author}</div>
|
|
33
|
+
</div>
|
|
34
|
+
{plugin.description && (
|
|
35
|
+
<p className="marketplace-plugin-description">{plugin.description}</p>
|
|
36
|
+
)}
|
|
37
|
+
{plugin.tags && plugin.tags.length > 0 && (
|
|
38
|
+
<div className="marketplace-plugin-tags">
|
|
39
|
+
{plugin.tags.map((tag) => (
|
|
40
|
+
<span key={tag} className="tag">{tag}</span>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
)}
|
|
44
|
+
<PluginPermissions permissions={plugin.permissions ?? []} />
|
|
45
|
+
<div className="marketplace-plugin-actions">
|
|
46
|
+
<Button onClick={onView}>Details</Button>
|
|
47
|
+
<Button variant="primary" onClick={onInstall}>Install</Button>
|
|
48
|
+
</div>
|
|
49
|
+
</Card>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -46,10 +46,10 @@ export function PluginCard({ plugin, onToggle, onUninstall, onConfigure }: Plugi
|
|
|
46
46
|
)}
|
|
47
47
|
<PluginPermissions permissions={plugin.permissions ?? []} />
|
|
48
48
|
<div className="plugin-card-actions">
|
|
49
|
-
<Button size="sm" onClick={() => onConfigure(plugin.id)}>
|
|
49
|
+
<Button size="sm" onClick={() => onConfigure(plugin.id)} aria-label={`Configure ${plugin.name}`}>
|
|
50
50
|
Configure
|
|
51
51
|
</Button>
|
|
52
|
-
<Button size="sm" variant="danger" onClick={() => onUninstall(plugin.id)}>
|
|
52
|
+
<Button size="sm" variant="danger" onClick={() => onUninstall(plugin.id)} aria-label={`Uninstall ${plugin.name}`}>
|
|
53
53
|
Uninstall
|
|
54
54
|
</Button>
|
|
55
55
|
</div>
|
|
@@ -16,7 +16,7 @@ export type PluginPermissionsProps = {
|
|
|
16
16
|
|
|
17
17
|
export function PluginPermissions({ permissions }: PluginPermissionsProps) {
|
|
18
18
|
return (
|
|
19
|
-
<div className="plugin-permissions">
|
|
19
|
+
<div className="plugin-permissions" aria-label="Required permissions">
|
|
20
20
|
{permissions.map((perm) => {
|
|
21
21
|
const info = PERMISSION_LABELS[perm] ?? { label: perm, icon: Shield, color: 'gray', description: '' };
|
|
22
22
|
const Icon = info.icon;
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
Brain,
|
|
26
26
|
Stethoscope,
|
|
27
27
|
ClipboardCheck,
|
|
28
|
+
Store,
|
|
28
29
|
type LucideIcon,
|
|
29
30
|
} from 'lucide-react';
|
|
30
31
|
import { cn } from '../lib/utils';
|
|
@@ -51,6 +52,8 @@ export const TABS: TabDef[] = [
|
|
|
51
52
|
{ id: 'skills', label: 'Skills', icon: Sparkles },
|
|
52
53
|
{ id: 'memory', label: 'Memory', icon: Brain },
|
|
53
54
|
{ id: 'mods', label: 'Mods', icon: Puzzle },
|
|
55
|
+
// v5.3.0 — Plugin marketplace browse + install UI.
|
|
56
|
+
{ id: 'marketplace', label: 'Marketplace', icon: Store },
|
|
54
57
|
{ id: 'schedules', label: 'Schedules', icon: Clock },
|
|
55
58
|
{ id: 'plugins', label: 'Plugins', icon: Puzzle },
|
|
56
59
|
{ id: 'history', label: 'History', icon: HistoryIcon },
|
|
@@ -120,6 +120,34 @@ export function VoiceNotesPanel({ vaultPath, refreshKey = 0 }: Props) {
|
|
|
120
120
|
|
|
121
121
|
useEffect(() => {
|
|
122
122
|
loadNotes();
|
|
123
|
+
|
|
124
|
+
// v5.2.0 — Listen for transcription worker updates
|
|
125
|
+
const handleMessage = (e: MessageEvent) => {
|
|
126
|
+
try {
|
|
127
|
+
const msg = JSON.parse(e.data);
|
|
128
|
+
if (msg.type === 'voice:updated' && msg.noteId) {
|
|
129
|
+
// Update the matching note's transcript in place
|
|
130
|
+
setNotes((prev) => prev.map((n) =>
|
|
131
|
+
n.id === msg.noteId
|
|
132
|
+
? { ...n, transcript: msg.patch?.transcript ?? n.transcript }
|
|
133
|
+
: n
|
|
134
|
+
));
|
|
135
|
+
}
|
|
136
|
+
} catch { /* ignore parse errors */ }
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Connect to WS for real-time transcript updates
|
|
140
|
+
const wsUrl = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`;
|
|
141
|
+
let ws: WebSocket | null = null;
|
|
142
|
+
try {
|
|
143
|
+
ws = new WebSocket(wsUrl);
|
|
144
|
+
ws.addEventListener('message', handleMessage);
|
|
145
|
+
} catch { /* ignore */ }
|
|
146
|
+
|
|
147
|
+
return () => {
|
|
148
|
+
ws?.removeEventListener('message', handleMessage);
|
|
149
|
+
ws?.close();
|
|
150
|
+
};
|
|
123
151
|
}, [vaultPath, refreshKey]);
|
|
124
152
|
|
|
125
153
|
const handleDelete = async (id: string) => {
|
|
@@ -9162,6 +9162,120 @@ select:focus-visible,
|
|
|
9162
9162
|
margin-top: var(--space-2);
|
|
9163
9163
|
}
|
|
9164
9164
|
|
|
9165
|
+
/* ─── v5.3.0 — Plugin marketplace ──────────────────────────────── */
|
|
9166
|
+
.marketplace-grid {
|
|
9167
|
+
display: grid;
|
|
9168
|
+
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
9169
|
+
gap: 16px;
|
|
9170
|
+
margin-top: 24px;
|
|
9171
|
+
}
|
|
9172
|
+
|
|
9173
|
+
.marketplace-plugin-card {
|
|
9174
|
+
display: flex;
|
|
9175
|
+
flex-direction: column;
|
|
9176
|
+
}
|
|
9177
|
+
|
|
9178
|
+
.marketplace-plugin-head {
|
|
9179
|
+
display: flex;
|
|
9180
|
+
align-items: flex-start;
|
|
9181
|
+
justify-content: space-between;
|
|
9182
|
+
gap: var(--space-3);
|
|
9183
|
+
}
|
|
9184
|
+
|
|
9185
|
+
.marketplace-plugin-description {
|
|
9186
|
+
color: var(--text-dim);
|
|
9187
|
+
margin: var(--space-2) 0;
|
|
9188
|
+
font-size: 13px;
|
|
9189
|
+
line-height: 1.5;
|
|
9190
|
+
}
|
|
9191
|
+
|
|
9192
|
+
.marketplace-plugin-tags {
|
|
9193
|
+
display: flex;
|
|
9194
|
+
flex-wrap: wrap;
|
|
9195
|
+
gap: 4px;
|
|
9196
|
+
margin: 8px 0;
|
|
9197
|
+
}
|
|
9198
|
+
|
|
9199
|
+
.marketplace-plugin-tags .tag {
|
|
9200
|
+
background: var(--surface-3, rgba(255, 255, 255, 0.05));
|
|
9201
|
+
color: var(--text-dim, #8b949e);
|
|
9202
|
+
padding: 2px 6px;
|
|
9203
|
+
border-radius: 8px;
|
|
9204
|
+
font-size: 10px;
|
|
9205
|
+
}
|
|
9206
|
+
|
|
9207
|
+
.marketplace-plugin-actions {
|
|
9208
|
+
display: flex;
|
|
9209
|
+
gap: var(--space-2);
|
|
9210
|
+
flex-wrap: wrap;
|
|
9211
|
+
margin-top: auto;
|
|
9212
|
+
padding-top: var(--space-2);
|
|
9213
|
+
}
|
|
9214
|
+
|
|
9215
|
+
.marketplace-plugin-author {
|
|
9216
|
+
font-size: 12px;
|
|
9217
|
+
color: var(--text-dim);
|
|
9218
|
+
}
|
|
9219
|
+
|
|
9220
|
+
.marketplace-search-row {
|
|
9221
|
+
margin-bottom: 16px;
|
|
9222
|
+
}
|
|
9223
|
+
|
|
9224
|
+
.category-chips {
|
|
9225
|
+
display: flex;
|
|
9226
|
+
gap: 8px;
|
|
9227
|
+
margin: 16px 0;
|
|
9228
|
+
flex-wrap: wrap;
|
|
9229
|
+
}
|
|
9230
|
+
|
|
9231
|
+
.category-chips button {
|
|
9232
|
+
padding: 6px 12px;
|
|
9233
|
+
border: 1px solid var(--border, #30363d);
|
|
9234
|
+
border-radius: 14px;
|
|
9235
|
+
background: transparent;
|
|
9236
|
+
color: var(--text-dim);
|
|
9237
|
+
cursor: pointer;
|
|
9238
|
+
font-size: 13px;
|
|
9239
|
+
}
|
|
9240
|
+
|
|
9241
|
+
.category-chips button:hover {
|
|
9242
|
+
background: var(--surface-2);
|
|
9243
|
+
}
|
|
9244
|
+
|
|
9245
|
+
.category-chips button.active {
|
|
9246
|
+
background: var(--accent, #8b5cf6);
|
|
9247
|
+
color: white;
|
|
9248
|
+
border-color: var(--accent, #8b5cf6);
|
|
9249
|
+
}
|
|
9250
|
+
|
|
9251
|
+
.install-confirm h3 {
|
|
9252
|
+
margin: 0 0 var(--space-2);
|
|
9253
|
+
font-size: 16px;
|
|
9254
|
+
}
|
|
9255
|
+
|
|
9256
|
+
.install-warning {
|
|
9257
|
+
display: flex;
|
|
9258
|
+
gap: 8px;
|
|
9259
|
+
background: rgba(245, 158, 11, 0.1);
|
|
9260
|
+
padding: 12px;
|
|
9261
|
+
border-radius: 6px;
|
|
9262
|
+
margin: 12px 0;
|
|
9263
|
+
color: #d29922;
|
|
9264
|
+
font-size: 13px;
|
|
9265
|
+
}
|
|
9266
|
+
|
|
9267
|
+
.install-warning p {
|
|
9268
|
+
margin: 0;
|
|
9269
|
+
color: var(--text);
|
|
9270
|
+
}
|
|
9271
|
+
|
|
9272
|
+
.install-confirm-actions {
|
|
9273
|
+
display: flex;
|
|
9274
|
+
gap: var(--space-2);
|
|
9275
|
+
justify-content: flex-end;
|
|
9276
|
+
margin-top: var(--space-4);
|
|
9277
|
+
}
|
|
9278
|
+
|
|
9165
9279
|
/* ─── Plugin permissions ──────────────────────────────────────────── */
|
|
9166
9280
|
.plugin-permissions {
|
|
9167
9281
|
display: flex;
|
|
@@ -9610,3 +9724,28 @@ select:focus-visible,
|
|
|
9610
9724
|
display: flex;
|
|
9611
9725
|
flex-direction: column;
|
|
9612
9726
|
}
|
|
9727
|
+
|
|
9728
|
+
/* ─── High-contrast mode (prefers-contrast: more) ──────────────────── */
|
|
9729
|
+
@media (prefers-contrast: more) {
|
|
9730
|
+
:root {
|
|
9731
|
+
--text-dim: var(--text);
|
|
9732
|
+
--text-muted: var(--text);
|
|
9733
|
+
}
|
|
9734
|
+
|
|
9735
|
+
[data-theme='light'] {
|
|
9736
|
+
--text-dim: #374151;
|
|
9737
|
+
--text-muted: #374151;
|
|
9738
|
+
}
|
|
9739
|
+
|
|
9740
|
+
:focus-visible,
|
|
9741
|
+
button:focus-visible,
|
|
9742
|
+
[role='button']:focus-visible,
|
|
9743
|
+
a:focus-visible,
|
|
9744
|
+
input:focus-visible,
|
|
9745
|
+
textarea:focus-visible,
|
|
9746
|
+
select:focus-visible,
|
|
9747
|
+
[tabindex]:focus-visible {
|
|
9748
|
+
outline-width: 3px;
|
|
9749
|
+
outline-style: solid;
|
|
9750
|
+
}
|
|
9751
|
+
}
|
|
@@ -208,7 +208,7 @@ function DoctorInner({
|
|
|
208
208
|
</div>
|
|
209
209
|
<div className="doctor-header-meta muted">
|
|
210
210
|
<Clock size={12} aria-hidden />
|
|
211
|
-
<span>
|
|
211
|
+
<span aria-live="polite" aria-atomic="true">
|
|
212
212
|
v{data.bizarVersion} · {data.platform}/{data.arch} ·{' '}
|
|
213
213
|
up {Math.floor(data.uptime)}s
|
|
214
214
|
{lastFetched ? ` · refreshed ${formatTime(new Date(lastFetched))}` : ''}
|
|
@@ -266,7 +266,7 @@ function DoctorInner({
|
|
|
266
266
|
</CardTitle>
|
|
267
267
|
<CardMeta>Run a check, view logs, or export the snapshot.</CardMeta>
|
|
268
268
|
<div className="doctor-actions-row">
|
|
269
|
-
<Button variant="primary" onClick={fetchSnapshot} disabled={refreshing}>
|
|
269
|
+
<Button variant="primary" onClick={fetchSnapshot} disabled={refreshing} aria-label="Refresh diagnostics">
|
|
270
270
|
{refreshing ? <Spinner size="sm" /> : <RefreshCw size={12} />}
|
|
271
271
|
{refreshing ? 'Running…' : 'Run Health Check Now'}
|
|
272
272
|
</Button>
|
|
@@ -279,7 +279,7 @@ function DoctorInner({
|
|
|
279
279
|
<Button
|
|
280
280
|
variant="ghost"
|
|
281
281
|
onClick={() => setAutoRefresh((v) => !v)}
|
|
282
|
-
|
|
282
|
+
aria-label={autoRefresh ? 'Pause auto-refresh' : 'Resume auto-refresh'}
|
|
283
283
|
>
|
|
284
284
|
{autoRefresh ? 'Pause auto-refresh' : 'Resume auto-refresh'}
|
|
285
285
|
</Button>
|