@polderlabs/bizar 4.4.13 → 4.5.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/CHANGELOG.md +37 -276
- package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +2 -0
- package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js.map +1 -0
- package/bizar-dash/dist/assets/main-eWZ4NlCL.css +1 -0
- package/bizar-dash/dist/assets/main-usWhlPWa.js +362 -0
- package/bizar-dash/dist/assets/main-usWhlPWa.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js → mobile-DQLFCjwJ.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js.map → mobile-DQLFCjwJ.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-O6ANdD4W.js +352 -0
- package/bizar-dash/dist/assets/mobile-O6ANdD4W.js.map +1 -0
- package/bizar-dash/dist/index.html +3 -3
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/skills/agent-baseline/SKILL.md +80 -0
- package/bizar-dash/skills/bizar/SKILL.md +116 -0
- package/bizar-dash/skills/chat/SKILL.md +74 -0
- package/bizar-dash/skills/headroom/SKILL.md +94 -0
- package/bizar-dash/skills/lightrag/SKILL.md +86 -0
- package/bizar-dash/skills/minimax/SKILL.md +80 -0
- package/bizar-dash/skills/obsidian/SKILL.md +68 -0
- package/bizar-dash/skills/providers/SKILL.md +75 -0
- package/bizar-dash/skills/sdk/SKILL.md +138 -0
- package/bizar-dash/skills/self-improvement/SKILL.md +53 -0
- package/bizar-dash/skills/skills-cli/SKILL.md +94 -0
- package/bizar-dash/skills/usage/SKILL.md +62 -0
- package/bizar-dash/src/server/api.mjs +18 -0
- package/bizar-dash/src/server/headroom.mjs +645 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +272 -2
- package/bizar-dash/src/server/memory-obsidian.mjs +230 -0
- package/bizar-dash/src/server/memory-store.mjs +189 -0
- package/bizar-dash/src/server/minimax-usage-store.mjs +372 -0
- package/bizar-dash/src/server/minimax.mjs +196 -5
- package/bizar-dash/src/server/providers-store.mjs +956 -0
- package/bizar-dash/src/server/routes/_shared.mjs +17 -0
- package/bizar-dash/src/server/routes/config.mjs +52 -1
- package/bizar-dash/src/server/routes/env-vars.mjs +165 -0
- package/bizar-dash/src/server/routes/headroom.mjs +126 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +154 -0
- package/bizar-dash/src/server/routes/memory.mjs +668 -1
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +14 -29
- package/bizar-dash/src/server/routes/opencode-sessions.mjs +205 -3
- package/bizar-dash/src/server/routes/providers.mjs +266 -5
- package/bizar-dash/src/server/routes/skills.mjs +32 -43
- package/bizar-dash/src/server/routes/update.mjs +340 -0
- package/bizar-dash/src/server/routes/usage.mjs +136 -0
- package/bizar-dash/src/server/serve-info.mjs +135 -4
- package/bizar-dash/src/server/server.mjs +20 -0
- package/bizar-dash/src/server/skills-store.mjs +152 -262
- package/bizar-dash/src/web/App.tsx +120 -29
- package/bizar-dash/src/web/components/EnvVarManager.tsx +247 -0
- package/bizar-dash/src/web/components/HeadroomSettings.tsx +418 -0
- package/bizar-dash/src/web/components/HeadroomStatus.tsx +158 -0
- package/bizar-dash/src/web/components/SettingsSearch.tsx +213 -0
- package/bizar-dash/src/web/components/Topbar.tsx +2 -1
- package/bizar-dash/src/web/components/UsageChart.tsx +250 -0
- package/bizar-dash/src/web/components/UsageTable.tsx +90 -0
- package/bizar-dash/src/web/components/chat/ChatComposer.tsx +21 -25
- package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +199 -37
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +29 -17
- package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +7 -1
- package/bizar-dash/src/web/components/chat/InfoPanel.tsx +71 -6
- package/bizar-dash/src/web/components/chat/useChat.ts +751 -257
- package/bizar-dash/src/web/lib/api.ts +43 -0
- package/bizar-dash/src/web/lib/types.ts +16 -0
- package/bizar-dash/src/web/main.tsx +2 -0
- package/bizar-dash/src/web/mobile/views/MobileChat.tsx +110 -35
- package/bizar-dash/src/web/styles/chat.css +135 -1
- package/bizar-dash/src/web/styles/main.css +46 -0
- package/bizar-dash/src/web/styles/memory.css +955 -0
- package/bizar-dash/src/web/styles/minimax-usage.css +335 -0
- package/bizar-dash/src/web/styles/settings.css +418 -0
- package/bizar-dash/src/web/styles/skills.css +302 -0
- package/bizar-dash/src/web/styles/tasks.css +288 -0
- package/bizar-dash/src/web/views/Chat.tsx +276 -48
- package/bizar-dash/src/web/views/Config.tsx +3 -2065
- package/bizar-dash/src/web/views/Memory.tsx +140 -0
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +476 -461
- package/bizar-dash/src/web/views/Overview.tsx +3 -0
- package/bizar-dash/src/web/views/Settings.tsx +36 -0
- package/bizar-dash/src/web/views/Skills.tsx +208 -260
- package/bizar-dash/src/web/views/Tasks.tsx +348 -1119
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +289 -0
- package/bizar-dash/src/web/views/memory/GitSyncPanel.tsx +220 -0
- package/bizar-dash/src/web/views/memory/LightragPanel.tsx +307 -0
- package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +354 -0
- package/bizar-dash/src/web/views/memory/MemoryStatusCard.tsx +160 -0
- package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +642 -0
- package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +194 -0
- package/bizar-dash/tests/chat-session-create.test.mjs +391 -0
- package/bizar-dash/tests/chat-session-stream.test.mjs +308 -0
- package/bizar-dash/tests/env-vars-store.test.mjs +216 -0
- package/bizar-dash/tests/headroom-install.test.mjs +173 -0
- package/bizar-dash/tests/headroom-settings.test.mjs +126 -0
- package/bizar-dash/tests/headroom-status.test.mjs +117 -0
- package/bizar-dash/tests/lightrag-defaults.node.test.mjs +118 -0
- package/bizar-dash/tests/memory-lightrag-extended.test.mjs +162 -0
- package/bizar-dash/tests/memory-obsidian.test.mjs +269 -0
- package/bizar-dash/tests/memory-tab.test.mjs +322 -0
- package/bizar-dash/tests/minimax-chat-usage.test.mjs +178 -0
- package/bizar-dash/tests/minimax-usage-store.node.test.mjs +293 -0
- package/bizar-dash/tests/mod-upgrade.node.test.mjs +1 -1
- package/bizar-dash/tests/opencode-sessions-detail.test.mjs +12 -9
- package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +479 -3
- package/bizar-dash/tests/providers-store-search.node.test.mjs +166 -0
- package/bizar-dash/tests/skills-list.test.mjs +232 -0
- package/bizar-dash/tests/skills-search.test.mjs +222 -0
- package/bizar-dash/tests/submit-feedback.test.mjs +6 -6
- package/bizar-dash/tests/tasks-create.test.mjs +187 -0
- package/bizar-dash/tests/update-check.test.mjs +127 -0
- package/bizar-dash/tests/update-run.test.mjs +266 -0
- package/cli/bin.mjs +247 -1
- package/cli/provision.mjs +118 -4
- package/config/agents/_shared/SKILLS.md +109 -0
- package/package.json +1 -1
- package/bizar-dash/dist/assets/main-BB5mJurD.js +0 -352
- package/bizar-dash/dist/assets/main-BB5mJurD.js.map +0 -1
- package/bizar-dash/dist/assets/main-BsnQLXdh.css +0 -1
- package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js +0 -354
- package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js.map +0 -1
|
@@ -7,8 +7,8 @@ import { ModalProvider, useModal } from './components/Modal';
|
|
|
7
7
|
import { ToastProvider, useToast } from './components/Toast';
|
|
8
8
|
import { SearchModal } from './components/SearchModal';
|
|
9
9
|
import { Notifications } from './components/Notifications';
|
|
10
|
+
import { api, ApiError } from './lib/api';
|
|
10
11
|
import { CommandDialog, type DialogDescriptor } from './components/CommandDialog';
|
|
11
|
-
import { api } from './lib/api';
|
|
12
12
|
import { Ws } from './lib/ws';
|
|
13
13
|
import {
|
|
14
14
|
applyTheme,
|
|
@@ -34,6 +34,7 @@ import { Mods } from './views/Mods';
|
|
|
34
34
|
import { ModView, type ModView as ModViewType } from './views/ModView';
|
|
35
35
|
import { Schedules } from './views/Schedules';
|
|
36
36
|
import { Skills } from './views/Skills';
|
|
37
|
+
import { Memory } from './views/Memory';
|
|
37
38
|
import { History } from './views/History';
|
|
38
39
|
import { MiniMaxUsage } from './views/MiniMaxUsage';
|
|
39
40
|
import { BackgroundAgents } from './views/BackgroundAgents';
|
|
@@ -67,6 +68,7 @@ const VIEW_MAP: Record<string, (p: ViewProps) => React.ReactNode> = {
|
|
|
67
68
|
chat: Chat,
|
|
68
69
|
agents: Agents,
|
|
69
70
|
artifacts: Artifacts,
|
|
71
|
+
memory: Memory,
|
|
70
72
|
tasks: Tasks,
|
|
71
73
|
activity: Activity,
|
|
72
74
|
background: BackgroundAgents,
|
|
@@ -79,7 +81,7 @@ const VIEW_MAP: Record<string, (p: ViewProps) => React.ReactNode> = {
|
|
|
79
81
|
minimax: MiniMaxUsage,
|
|
80
82
|
};
|
|
81
83
|
|
|
82
|
-
const VERSION = '
|
|
84
|
+
const VERSION = 'v4.5.0';
|
|
83
85
|
|
|
84
86
|
/**
|
|
85
87
|
* Render the active view. If `activeTab` matches a built-in tab id,
|
|
@@ -113,6 +115,67 @@ function renderActiveView(
|
|
|
113
115
|
return <V {...viewProps} />;
|
|
114
116
|
}
|
|
115
117
|
|
|
118
|
+
// v3.6.2 — Inline token-entry form shown on the boot-error screen when
|
|
119
|
+
// the server returns 401 (reverse-proxy / Tailscale Serve scenario).
|
|
120
|
+
// The user pastes the token, it is saved to localStorage, and boot is
|
|
121
|
+
// retried. The form closes on Escape or on successful save.
|
|
122
|
+
function TokenEntryForm({ onSaved }: { onSaved: () => void }) {
|
|
123
|
+
const [value, setValue] = useState('');
|
|
124
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
125
|
+
|
|
126
|
+
// Auto-focus the input on mount
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
inputRef.current?.focus();
|
|
129
|
+
}, []);
|
|
130
|
+
|
|
131
|
+
const submit = () => {
|
|
132
|
+
const tok = value.trim();
|
|
133
|
+
if (!tok) return;
|
|
134
|
+
api.setToken(tok);
|
|
135
|
+
onSaved();
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
139
|
+
if (e.key === 'Escape') {
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
onSaved();
|
|
142
|
+
}
|
|
143
|
+
if (e.key === 'Enter') {
|
|
144
|
+
e.preventDefault();
|
|
145
|
+
submit();
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<form
|
|
151
|
+
className="token-entry-form"
|
|
152
|
+
onSubmit={(e) => { e.preventDefault(); submit(); }}
|
|
153
|
+
>
|
|
154
|
+
<div className="token-entry-row">
|
|
155
|
+
<input
|
|
156
|
+
ref={inputRef}
|
|
157
|
+
type="password"
|
|
158
|
+
className="input mono"
|
|
159
|
+
value={value}
|
|
160
|
+
onChange={(e) => setValue(e.target.value)}
|
|
161
|
+
placeholder="Paste auth token"
|
|
162
|
+
spellCheck={false}
|
|
163
|
+
autoComplete="off"
|
|
164
|
+
onKeyDown={handleKeyDown}
|
|
165
|
+
/>
|
|
166
|
+
<Button type="submit" variant="primary" size="sm" disabled={!value.trim()}>
|
|
167
|
+
Save & retry
|
|
168
|
+
</Button>
|
|
169
|
+
</div>
|
|
170
|
+
<p className="token-entry-hint">
|
|
171
|
+
Where do I find this token? Check the file{' '}
|
|
172
|
+
<code>~/.config/bizar/dashboard-secret</code> on the machine
|
|
173
|
+
running the dashboard, or look for it in the server output.
|
|
174
|
+
</p>
|
|
175
|
+
</form>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
116
179
|
export function App() {
|
|
117
180
|
return (
|
|
118
181
|
<ToastProvider>
|
|
@@ -133,6 +196,7 @@ function Shell() {
|
|
|
133
196
|
const [settings, setSettings] = useState<Settings | null>(null);
|
|
134
197
|
const [wsStatus, setWsStatus] = useState<WsStatus>('connecting');
|
|
135
198
|
const [bootError, setBootError] = useState<string | null>(null);
|
|
199
|
+
const [authPromptOpen, setAuthPromptOpen] = useState(false);
|
|
136
200
|
const [searchOpen, setSearchOpen] = useState(false);
|
|
137
201
|
const [stuckAgents, setStuckAgents] = useState<{ name: string }[]>([]);
|
|
138
202
|
const [stuckBannerDismissed, setStuckBannerDismissed] = useState(false);
|
|
@@ -196,9 +260,11 @@ function Shell() {
|
|
|
196
260
|
return () => mql.removeEventListener('change', handler);
|
|
197
261
|
}, [settings?.theme?.mode]);
|
|
198
262
|
|
|
199
|
-
//
|
|
200
|
-
|
|
263
|
+
// v3.6.2 — Boot logic extracted to retryBoot so it can be re-invoked
|
|
264
|
+
// after the user pastes a token in the inline recovery form.
|
|
265
|
+
const retryBoot = useCallback(async () => {
|
|
201
266
|
let cancelled = false;
|
|
267
|
+
|
|
202
268
|
const loadBootData = () =>
|
|
203
269
|
Promise.all([
|
|
204
270
|
api.get<Snapshot>('/snapshot').catch(() => null),
|
|
@@ -216,42 +282,59 @@ function Shell() {
|
|
|
216
282
|
if (stuck?.stuck) setStuckAgents(stuck.stuck);
|
|
217
283
|
};
|
|
218
284
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (cancelled) return;
|
|
285
|
+
try {
|
|
286
|
+
const auth = await api.probeAuthStatus();
|
|
287
|
+
const [snap, set, stuck] = await loadBootData();
|
|
288
|
+
if (cancelled) return;
|
|
224
289
|
|
|
225
|
-
|
|
226
|
-
|
|
290
|
+
applyBootData(snap, set, stuck);
|
|
291
|
+
if (snap || set) {
|
|
292
|
+
setBootError(null);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
227
295
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
296
|
+
if (!auth.loopback) {
|
|
297
|
+
setBootError('Dashboard server unreachable.');
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
232
300
|
|
|
233
|
-
|
|
234
|
-
|
|
301
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
302
|
+
if (cancelled) return;
|
|
235
303
|
|
|
236
|
-
|
|
237
|
-
|
|
304
|
+
const [retrySnap, retrySet, retryStuck] = await loadBootData();
|
|
305
|
+
if (cancelled) return;
|
|
238
306
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
307
|
+
applyBootData(retrySnap, retrySet, retryStuck);
|
|
308
|
+
if (!retrySnap && !retrySet) {
|
|
309
|
+
setBootError('Dashboard server unreachable.');
|
|
310
|
+
}
|
|
311
|
+
} catch (err) {
|
|
312
|
+
if (cancelled) return;
|
|
313
|
+
const msg = (err as Error)?.message ?? 'unknown error';
|
|
314
|
+
// v3.6.2 — If we got a 401, the user needs to supply a token.
|
|
315
|
+
// Surface the form inline instead of just the error message.
|
|
316
|
+
if (err instanceof ApiError && err.status === 401) {
|
|
246
317
|
setBootError(msg);
|
|
247
|
-
|
|
318
|
+
setAuthPromptOpen(true);
|
|
319
|
+
toast.error(`Auth required: ${msg}`);
|
|
320
|
+
return;
|
|
248
321
|
}
|
|
249
|
-
|
|
322
|
+
setBootError(msg);
|
|
323
|
+
toast.error(`Failed to load: ${msg}`);
|
|
324
|
+
}
|
|
325
|
+
}, [toast]);
|
|
250
326
|
|
|
327
|
+
// Initial fetch — runs once on mount
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
let cancelled = false;
|
|
330
|
+
const id = setTimeout(() => {
|
|
331
|
+
if (!cancelled) retryBoot();
|
|
332
|
+
}, 0);
|
|
251
333
|
return () => {
|
|
252
334
|
cancelled = true;
|
|
335
|
+
clearTimeout(id);
|
|
253
336
|
};
|
|
254
|
-
}, [
|
|
337
|
+
}, [retryBoot]);
|
|
255
338
|
|
|
256
339
|
// Apply defaultTab ONCE on mount only — must NOT re-fire on toast changes
|
|
257
340
|
useEffect(() => {
|
|
@@ -695,6 +778,14 @@ function Shell() {
|
|
|
695
778
|
<div className="boot-error">
|
|
696
779
|
<h2>Dashboard unavailable</h2>
|
|
697
780
|
<p>{bootError}</p>
|
|
781
|
+
{authPromptOpen && (
|
|
782
|
+
<TokenEntryForm
|
|
783
|
+
onSaved={() => {
|
|
784
|
+
setAuthPromptOpen(false);
|
|
785
|
+
retryBoot();
|
|
786
|
+
}}
|
|
787
|
+
/>
|
|
788
|
+
)}
|
|
698
789
|
<p className="boot-error-hint">
|
|
699
790
|
Make sure the Bizar dashboard server is running. Try{' '}
|
|
700
791
|
<code>bizar-dash start</code> in your terminal.
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// src/web/components/EnvVarManager.tsx — manage BIZAR_* env vars from ~/.config/bizar/env.json
|
|
2
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { Plus, Pencil, Trash2, Eye, EyeOff, RefreshCw, KeyRound, CheckCircle, X } from 'lucide-react';
|
|
4
|
+
import { Button } from './Button';
|
|
5
|
+
import { useToast } from './Toast';
|
|
6
|
+
import { api } from '../lib/api';
|
|
7
|
+
import { cn } from '../lib/utils';
|
|
8
|
+
|
|
9
|
+
export type EnvVarEntry = {
|
|
10
|
+
name: string;
|
|
11
|
+
value: string; // masked on GET, full on edit
|
|
12
|
+
createdAt: string;
|
|
13
|
+
source: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type Props = {
|
|
17
|
+
onError?: (msg: string) => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function EnvVarManager({ onError }: Props) {
|
|
21
|
+
const toast = useToast();
|
|
22
|
+
const [vars, setVars] = useState<EnvVarEntry[]>([]);
|
|
23
|
+
const [loading, setLoading] = useState(false);
|
|
24
|
+
const [saving, setSaving] = useState(false);
|
|
25
|
+
// Which rows have the value revealed (edit mode shows real value)
|
|
26
|
+
const [revealed, setRevealed] = useState<Set<string>>(new Set());
|
|
27
|
+
// Currently editing name (null = none)
|
|
28
|
+
const [editing, setEditing] = useState<string | null>(null);
|
|
29
|
+
const [editValue, setEditValue] = useState('');
|
|
30
|
+
// New var form
|
|
31
|
+
const [showAdd, setShowAdd] = useState(false);
|
|
32
|
+
const [newName, setNewName] = useState('');
|
|
33
|
+
const [newValue, setNewValue] = useState('');
|
|
34
|
+
|
|
35
|
+
const load = useCallback(async () => {
|
|
36
|
+
setLoading(true);
|
|
37
|
+
try {
|
|
38
|
+
const r = await api.get<EnvVarEntry[]>('/env-vars');
|
|
39
|
+
setVars(r);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
toast.error(`Load failed: ${(err as Error).message}`);
|
|
42
|
+
onError?.((err as Error).message);
|
|
43
|
+
} finally {
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}
|
|
46
|
+
}, [toast, onError]);
|
|
47
|
+
|
|
48
|
+
useEffect(() => { load(); }, [load]);
|
|
49
|
+
|
|
50
|
+
const toggleReveal = (name: string) => {
|
|
51
|
+
setRevealed((prev) => {
|
|
52
|
+
const next = new Set(prev);
|
|
53
|
+
if (next.has(name)) next.delete(name);
|
|
54
|
+
else next.add(name);
|
|
55
|
+
return next;
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleAdd = async () => {
|
|
60
|
+
const name = newName.trim().toUpperCase();
|
|
61
|
+
const value = newValue.trim();
|
|
62
|
+
if (!name || !/^BIZAR_[A-Z0-9_]+$/.test(name)) {
|
|
63
|
+
toast.warning('Name must match BIZAR_<NAME> (e.g. BIZAR_MINIMAX_KEY)');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!value) {
|
|
67
|
+
toast.warning('Value is required.');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (vars.some((v) => v.name === name)) {
|
|
71
|
+
toast.warning(`${name} already exists.`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
setSaving(true);
|
|
75
|
+
try {
|
|
76
|
+
await api.post('/env-vars', { name, value });
|
|
77
|
+
toast.success(`${name} created.`);
|
|
78
|
+
setShowAdd(false);
|
|
79
|
+
setNewName('');
|
|
80
|
+
setNewValue('');
|
|
81
|
+
await load();
|
|
82
|
+
} catch (err) {
|
|
83
|
+
toast.error(`Create failed: ${(err as Error).message}`);
|
|
84
|
+
} finally {
|
|
85
|
+
setSaving(false);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const startEdit = (v: EnvVarEntry) => {
|
|
90
|
+
setEditing(v.name);
|
|
91
|
+
setEditValue(v.value === v.value.replace(/\*/g, '') ? v.value : '');
|
|
92
|
+
setRevealed((prev) => { const next = new Set(prev); next.add(v.name); return next; });
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const handleUpdate = async (name: string) => {
|
|
96
|
+
if (!editValue.trim()) {
|
|
97
|
+
toast.warning('Value cannot be empty.');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
setSaving(true);
|
|
101
|
+
try {
|
|
102
|
+
await api.put(`/env-vars/${encodeURIComponent(name)}`, { value: editValue.trim() });
|
|
103
|
+
toast.success(`${name} updated.`);
|
|
104
|
+
setEditing(null);
|
|
105
|
+
await load();
|
|
106
|
+
} catch (err) {
|
|
107
|
+
toast.error(`Update failed: ${(err as Error).message}`);
|
|
108
|
+
} finally {
|
|
109
|
+
setSaving(false);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleDelete = async (name: string) => {
|
|
114
|
+
if (!confirm(`Delete ${name}? This cannot be undone.`)) return;
|
|
115
|
+
try {
|
|
116
|
+
await api.del(`/env-vars/${encodeURIComponent(name)}`);
|
|
117
|
+
toast.success(`${name} deleted.`);
|
|
118
|
+
await load();
|
|
119
|
+
} catch (err) {
|
|
120
|
+
toast.error(`Delete failed: ${(err as Error).message}`);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (loading) {
|
|
125
|
+
return <div className="muted" style={{ padding: '16px 0', fontSize: 13 }}>Loading env vars…</div>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div className="env-var-manager">
|
|
130
|
+
<div className="env-var-toolbar">
|
|
131
|
+
<Button variant="primary" size="sm" onClick={() => setShowAdd(true)}>
|
|
132
|
+
<Plus size={12} /> New variable
|
|
133
|
+
</Button>
|
|
134
|
+
<Button variant="ghost" size="sm" onClick={load} title="Reload">
|
|
135
|
+
<RefreshCw size={12} />
|
|
136
|
+
</Button>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{vars.length === 0 && !showAdd && (
|
|
140
|
+
<div className="env-var-empty">
|
|
141
|
+
<KeyRound size={20} />
|
|
142
|
+
<span>No BIZAR_* env vars yet.</span>
|
|
143
|
+
<span style={{ fontSize: 11 }}>Create one to store API keys securely.</span>
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{/* New var form */}
|
|
148
|
+
{showAdd && (
|
|
149
|
+
<div className="env-var-add-form">
|
|
150
|
+
<div className="env-var-add-row">
|
|
151
|
+
<input
|
|
152
|
+
className="input mono"
|
|
153
|
+
style={{ width: 240 }}
|
|
154
|
+
placeholder="BIZAR_NAME"
|
|
155
|
+
value={newName}
|
|
156
|
+
onChange={(e) => setNewName(e.target.value.toUpperCase())}
|
|
157
|
+
onKeyDown={(e) => { if (e.key === 'Enter') handleAdd(); if (e.key === 'Escape') setShowAdd(false); }}
|
|
158
|
+
autoFocus
|
|
159
|
+
/>
|
|
160
|
+
<input
|
|
161
|
+
className="input mono"
|
|
162
|
+
type="password"
|
|
163
|
+
placeholder="value (API key, token, etc.)"
|
|
164
|
+
value={newValue}
|
|
165
|
+
onChange={(e) => setNewValue(e.target.value)}
|
|
166
|
+
onKeyDown={(e) => { if (e.key === 'Enter') handleAdd(); if (e.key === 'Escape') setShowAdd(false); }}
|
|
167
|
+
/>
|
|
168
|
+
<Button variant="primary" size="sm" onClick={handleAdd} disabled={saving}>
|
|
169
|
+
<CheckCircle size={12} /> Create
|
|
170
|
+
</Button>
|
|
171
|
+
<Button variant="ghost" size="sm" onClick={() => { setShowAdd(false); setNewName(''); setNewValue(''); }}>
|
|
172
|
+
<X size={12} />
|
|
173
|
+
</Button>
|
|
174
|
+
</div>
|
|
175
|
+
<p style={{ fontSize: 11, color: 'var(--text-dim)', margin: 0 }}>
|
|
176
|
+
Names must match <code>BIZAR_<var>NAME</var></code> (uppercase letters, digits, underscores).
|
|
177
|
+
</p>
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
|
|
181
|
+
{/* Var list */}
|
|
182
|
+
{vars.length > 0 && (
|
|
183
|
+
<div className="env-var-list">
|
|
184
|
+
{vars.map((v) => {
|
|
185
|
+
const isEditing = editing === v.name;
|
|
186
|
+
const isRevealed = revealed.has(v.name);
|
|
187
|
+
return (
|
|
188
|
+
<div key={v.name} className={cn('env-var-row', isEditing && 'env-var-row-editing')}>
|
|
189
|
+
<span className="env-var-name mono">{v.name}</span>
|
|
190
|
+
{isEditing ? (
|
|
191
|
+
<input
|
|
192
|
+
className="input mono"
|
|
193
|
+
type="password"
|
|
194
|
+
style={{ flex: 1, minWidth: 200 }}
|
|
195
|
+
value={editValue}
|
|
196
|
+
onChange={(e) => setEditValue(e.target.value)}
|
|
197
|
+
onKeyDown={(e) => {
|
|
198
|
+
if (e.key === 'Enter') handleUpdate(v.name);
|
|
199
|
+
if (e.key === 'Escape') setEditing(null);
|
|
200
|
+
}}
|
|
201
|
+
autoFocus
|
|
202
|
+
/>
|
|
203
|
+
) : (
|
|
204
|
+
<span className={cn('env-var-value mono', !isRevealed && 'env-var-value-masked')}>
|
|
205
|
+
{isRevealed ? v.value : v.value}
|
|
206
|
+
</span>
|
|
207
|
+
)}
|
|
208
|
+
<span className="env-var-source muted">{v.source}</span>
|
|
209
|
+
<div className="env-var-actions">
|
|
210
|
+
{!isEditing && (
|
|
211
|
+
<button
|
|
212
|
+
type="button"
|
|
213
|
+
className="icon-btn"
|
|
214
|
+
title={isRevealed ? 'Hide' : 'Reveal'}
|
|
215
|
+
onClick={() => toggleReveal(v.name)}
|
|
216
|
+
>
|
|
217
|
+
{isRevealed ? <EyeOff size={12} /> : <Eye size={12} />}
|
|
218
|
+
</button>
|
|
219
|
+
)}
|
|
220
|
+
{isEditing ? (
|
|
221
|
+
<>
|
|
222
|
+
<Button variant="primary" size="sm" onClick={() => handleUpdate(v.name)} disabled={saving}>
|
|
223
|
+
Save
|
|
224
|
+
</Button>
|
|
225
|
+
<Button variant="ghost" size="sm" onClick={() => setEditing(null)}>
|
|
226
|
+
Cancel
|
|
227
|
+
</Button>
|
|
228
|
+
</>
|
|
229
|
+
) : (
|
|
230
|
+
<>
|
|
231
|
+
<button type="button" className="icon-btn" title="Edit" onClick={() => startEdit(v)}>
|
|
232
|
+
<Pencil size={12} />
|
|
233
|
+
</button>
|
|
234
|
+
<button type="button" className="icon-btn icon-btn-danger" title="Delete" onClick={() => handleDelete(v.name)}>
|
|
235
|
+
<Trash2 size={12} />
|
|
236
|
+
</button>
|
|
237
|
+
</>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
})}
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
</div>
|
|
246
|
+
);
|
|
247
|
+
}
|