agent-nuvira 2.7.0 → 2.7.2

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 (114) hide show
  1. package/dist/agents/orchestrator.d.ts.map +1 -1
  2. package/dist/agents/orchestrator.js +17 -3
  3. package/dist/agents/orchestrator.js.map +1 -1
  4. package/dist/app.js +26 -0
  5. package/dist/cli/chat.d.ts.map +1 -1
  6. package/dist/cli/chat.js +108 -2
  7. package/dist/cli/chat.js.map +1 -1
  8. package/dist/config/auth.js +18 -0
  9. package/dist/config/jwt.js +24 -0
  10. package/dist/config/keys.js +14 -0
  11. package/dist/gateway/chat-store.d.ts +32 -5
  12. package/dist/gateway/chat-store.d.ts.map +1 -1
  13. package/dist/gateway/chat-store.js +52 -6
  14. package/dist/gateway/chat-store.js.map +1 -1
  15. package/dist/gateway/registry.d.ts.map +1 -1
  16. package/dist/gateway/registry.js +54 -10
  17. package/dist/gateway/registry.js.map +1 -1
  18. package/dist/inference/model-validator.d.ts.map +1 -1
  19. package/dist/inference/model-validator.js +11 -1
  20. package/dist/inference/model-validator.js.map +1 -1
  21. package/dist/inference/tool-call-utils.d.ts +31 -0
  22. package/dist/inference/tool-call-utils.d.ts.map +1 -1
  23. package/dist/inference/tool-call-utils.js +41 -0
  24. package/dist/inference/tool-call-utils.js.map +1 -1
  25. package/dist/learning/reasoning-trace.d.ts +5 -3
  26. package/dist/learning/reasoning-trace.d.ts.map +1 -1
  27. package/dist/learning/reasoning-trace.js +2 -2
  28. package/dist/learning/reasoning-trace.js.map +1 -1
  29. package/dist/middleware/auth.js +30 -0
  30. package/dist/nlu/conversation-gate.js +1 -1
  31. package/dist/nlu/conversation-gate.js.map +1 -1
  32. package/dist/nlu/entities.d.ts.map +1 -1
  33. package/dist/nlu/entities.js +12 -0
  34. package/dist/nlu/entities.js.map +1 -1
  35. package/dist/nlu/intent.d.ts.map +1 -1
  36. package/dist/nlu/intent.js +83 -15
  37. package/dist/nlu/intent.js.map +1 -1
  38. package/dist/passport.js +34 -0
  39. package/dist/routes/auth.js +42 -0
  40. package/dist/routes/user.js +31 -0
  41. package/dist/server.js +17 -0
  42. package/dist/tools/pipeline-tool.d.ts.map +1 -1
  43. package/dist/tools/pipeline-tool.js +14 -0
  44. package/dist/tools/pipeline-tool.js.map +1 -1
  45. package/dist/tools/release-sync.d.ts +52 -16
  46. package/dist/tools/release-sync.d.ts.map +1 -1
  47. package/dist/tools/release-sync.js +143 -79
  48. package/dist/tools/release-sync.js.map +1 -1
  49. package/dist/web-dashboard/src/App.js +85 -0
  50. package/dist/web-dashboard/src/admin-auth.test.js +186 -0
  51. package/dist/web-dashboard/src/ansi.js +23 -0
  52. package/dist/web-dashboard/src/ansi.test.js +31 -0
  53. package/dist/web-dashboard/src/api-admin-auth.test.js +172 -0
  54. package/dist/web-dashboard/src/api-admin.test.js +65 -0
  55. package/dist/web-dashboard/src/api-hub.test.js +117 -0
  56. package/dist/web-dashboard/src/api.js +1421 -0
  57. package/dist/web-dashboard/src/api.test.js +51 -0
  58. package/dist/web-dashboard/src/artifacts.js +128 -0
  59. package/dist/web-dashboard/src/artifacts.test.js +139 -0
  60. package/dist/web-dashboard/src/components/AdminPanel.js +567 -0
  61. package/dist/web-dashboard/src/components/AdminPanel.test.js +288 -0
  62. package/dist/web-dashboard/src/components/AgentHub.js +1580 -0
  63. package/dist/web-dashboard/src/components/AgentHub.test.js +343 -0
  64. package/dist/web-dashboard/src/components/BedrockOnboarding.js +320 -0
  65. package/dist/web-dashboard/src/components/BenchmarkCharts.js +228 -0
  66. package/dist/web-dashboard/src/components/ChatPage.js +1586 -0
  67. package/dist/web-dashboard/src/components/ChatPage.test.js +899 -0
  68. package/dist/web-dashboard/src/components/ContactsPage.js +141 -0
  69. package/dist/web-dashboard/src/components/CostDashboard.js +105 -0
  70. package/dist/web-dashboard/src/components/DAGView.js +477 -0
  71. package/dist/web-dashboard/src/components/DAGView.test.js +147 -0
  72. package/dist/web-dashboard/src/components/EnvVarEditor.js +138 -0
  73. package/dist/web-dashboard/src/components/EvalsPage.js +73 -0
  74. package/dist/web-dashboard/src/components/EvalsPage.test.js +120 -0
  75. package/dist/web-dashboard/src/components/ExecutionHistory.js +201 -0
  76. package/dist/web-dashboard/src/components/GatewayPage.js +40 -0
  77. package/dist/web-dashboard/src/components/GatewayPage.test.js +74 -0
  78. package/dist/web-dashboard/src/components/HealthPanel.js +68 -0
  79. package/dist/web-dashboard/src/components/HistoryBrowser.js +34 -0
  80. package/dist/web-dashboard/src/components/Layout.js +50 -0
  81. package/dist/web-dashboard/src/components/Markdown.js +152 -0
  82. package/dist/web-dashboard/src/components/Markdown.test.js +126 -0
  83. package/dist/web-dashboard/src/components/MarkdownZeroDep.js +237 -0
  84. package/dist/web-dashboard/src/components/MemoryPanel.js +81 -0
  85. package/dist/web-dashboard/src/components/ModelTimeline.js +298 -0
  86. package/dist/web-dashboard/src/components/ModelsPanel.js +1484 -0
  87. package/dist/web-dashboard/src/components/ModelsPanel.test.js +460 -0
  88. package/dist/web-dashboard/src/components/Overview.js +123 -0
  89. package/dist/web-dashboard/src/components/PhaseTimeline.js +359 -0
  90. package/dist/web-dashboard/src/components/PhaseTimeline.test.js +234 -0
  91. package/dist/web-dashboard/src/components/PlatformConfigSection.js +384 -0
  92. package/dist/web-dashboard/src/components/PlatformConfigSection.test.js +106 -0
  93. package/dist/web-dashboard/src/components/PlatformsPage.js +250 -0
  94. package/dist/web-dashboard/src/components/QuotaPanel.js +159 -0
  95. package/dist/web-dashboard/src/components/QuotaPanel.test.js +85 -0
  96. package/dist/web-dashboard/src/components/RequestsPanel.js +229 -0
  97. package/dist/web-dashboard/src/components/RequestsPanel.test.js +103 -0
  98. package/dist/web-dashboard/src/components/RoutingInsightsPanel.js +938 -0
  99. package/dist/web-dashboard/src/components/RoutingInsightsPanel.test.js +339 -0
  100. package/dist/web-dashboard/src/components/RoutingWalkthrough.js +408 -0
  101. package/dist/web-dashboard/src/components/RoutingWalkthrough.test.js +209 -0
  102. package/dist/web-dashboard/src/components/TaskConsole.js +119 -0
  103. package/dist/web-dashboard/src/components/TaskConsole.test.js +123 -0
  104. package/dist/web-dashboard/src/components/TasksPage.js +256 -0
  105. package/dist/web-dashboard/src/components/TasksPage.test.js +103 -0
  106. package/dist/web-dashboard/src/components/TracePanel.js +306 -0
  107. package/dist/web-dashboard/src/components/WhatsAppPanel.js +195 -0
  108. package/dist/web-dashboard/src/components/WhatsAppPanel.test.js +122 -0
  109. package/dist/web-dashboard/src/jsonOrNull.js +37 -0
  110. package/dist/web-dashboard/src/main.js +15 -0
  111. package/dist/web-dashboard/src/mask.js +11 -0
  112. package/dist/web-dashboard/vite.config.js +23 -0
  113. package/dist/web-dashboard/vitest.config.js +15 -0
  114. package/package.json +5 -3
@@ -0,0 +1,567 @@
1
+ "use strict";
2
+ /**
3
+ * Admin panel — command-runner + the auth'd WRITE surface (Sessions 17–19).
4
+ *
5
+ * Three layers, per the user's roadmap:
6
+ * 1. Command-runner (Session 17): the dashboard EXECUTES the state commands
7
+ * (doctor system + enterprise checks) on demand — no command needed.
8
+ * 2. Write surface (Session 18): API-key / provider configuration in the
9
+ * dashboard, gated behind a user-id + password (the control layer). The CLI
10
+ * is NEVER deprecated — writes go through the SAME ConfigManager save() the
11
+ * CLI uses (GUI parallel, one config file). Keys are ALWAYS masked; the key
12
+ * input only ever receives a NEW value.
13
+ * 3. RBAC roles (Session 19): sessions carry an admin/operator/viewer role
14
+ * (rbac.json assignment wins over the stored role — CLI governance parity);
15
+ * provider writes + user management require admin; operator/viewer get a
16
+ * read-only table. Admins manage dashboard users (role.manage) in-panel.
17
+ *
18
+ * Auth states: unconfigured → bootstrap setup form; configured → login form;
19
+ * authenticated → role-gated editor. Read paths (checks/catalog) stay open;
20
+ * every write requires the Bearer token issued at login AND the admin role.
21
+ */
22
+ var __importDefault = (this && this.__importDefault) || function (mod) {
23
+ return (mod && mod.__esModule) ? mod : { "default": mod };
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.default = AdminPanel;
27
+ const react_1 = require("react");
28
+ const api_1 = require("../api");
29
+ const QuotaPanel_1 = __importDefault(require("./QuotaPanel"));
30
+ const STATUS_ICON = { pass: '✅', warn: '⚠️', fail: '❌' };
31
+ const STATUS_LABEL = { pass: 'PASS', warn: 'WARN', fail: 'FAIL' };
32
+ const KEY_SOURCE_LABEL = {
33
+ env: 'Environment',
34
+ config: 'Config',
35
+ vault: 'Vault',
36
+ local: 'Local (no key)',
37
+ none: 'Not configured',
38
+ };
39
+ function CheckRow({ check }) {
40
+ return (<div className="admin-check-row" data-status={check.status}>
41
+ <span className={`admin-check-badge admin-check-${check.status}`}>
42
+ {STATUS_ICON[check.status]} {STATUS_LABEL[check.status]}
43
+ </span>
44
+ <div className="admin-check-body">
45
+ <div className="admin-check-name">{check.name}</div>
46
+ <div className="admin-check-message">{check.message}</div>
47
+ {check.detail ? <div className="admin-check-detail">{check.detail}</div> : null}
48
+ {check.fix ? <div className="admin-check-fix">💡 {check.fix}</div> : null}
49
+ </div>
50
+ </div>);
51
+ }
52
+ function AdminPanel() {
53
+ const [data, setData] = (0, react_1.useState)(null);
54
+ const [loading, setLoading] = (0, react_1.useState)(true);
55
+ const [error, setError] = (0, react_1.useState)(null);
56
+ // Auth (Session 18 control layer) + RBAC role (Session 19)
57
+ const [authStatus, setAuthStatus] = (0, react_1.useState)(null);
58
+ const [authed, setAuthed] = (0, react_1.useState)(false);
59
+ const [role, setRole] = (0, react_1.useState)(null);
60
+ const [userName, setUserName] = (0, react_1.useState)(null);
61
+ const [user, setUser] = (0, react_1.useState)('');
62
+ const [password, setPassword] = (0, react_1.useState)('');
63
+ const [confirm, setConfirm] = (0, react_1.useState)('');
64
+ const [authError, setAuthError] = (0, react_1.useState)(null);
65
+ const [authBusy, setAuthBusy] = (0, react_1.useState)(false);
66
+ // User management (Session 19 — role.manage = admin only)
67
+ const [users, setUsers] = (0, react_1.useState)([]);
68
+ const [usersMsg, setUsersMsg] = (0, react_1.useState)(null);
69
+ const [newUserName, setNewUserName] = (0, react_1.useState)('');
70
+ const [newUserPass, setNewUserPass] = (0, react_1.useState)('');
71
+ const [newUserRole, setNewUserRole] = (0, react_1.useState)('viewer');
72
+ // Provider editor
73
+ const [catalog, setCatalog] = (0, react_1.useState)([]);
74
+ const [drafts, setDrafts] = (0, react_1.useState)({});
75
+ const [busy, setBusy] = (0, react_1.useState)(null);
76
+ const [rowMsg, setRowMsg] = (0, react_1.useState)({});
77
+ const [newType, setNewType] = (0, react_1.useState)('');
78
+ // Shutdown controls (dashboard / gateway) — admin sees both, operator sees
79
+ // only the gateway stop (gateway.manage), viewer sees neither.
80
+ const [shutdownMsg, setShutdownMsg] = (0, react_1.useState)(null);
81
+ const [shutdownBusy, setShutdownBusy] = (0, react_1.useState)(null);
82
+ const refresh = (0, react_1.useCallback)(async () => {
83
+ setLoading(true);
84
+ setError(null);
85
+ const result = await api_1.dashboardAPI.fetchAdminChecks();
86
+ setData(result);
87
+ if (!result)
88
+ setError('Could not reach the dashboard server, or the server is older than this panel. Run `buff dashboard` to start it.');
89
+ setLoading(false);
90
+ }, []);
91
+ const loadCatalog = (0, react_1.useCallback)(async () => {
92
+ const c = await api_1.dashboardAPI.fetchAdminCatalog();
93
+ if (c && c.length > 0)
94
+ setCatalog(c);
95
+ }, []);
96
+ const loadUsers = (0, react_1.useCallback)(async () => {
97
+ const r = await api_1.dashboardAPI.fetchAdminUsers();
98
+ if (r.ok && r.users)
99
+ setUsers(r.users);
100
+ }, []);
101
+ (0, react_1.useEffect)(() => {
102
+ let alive = true;
103
+ api_1.dashboardAPI.fetchAdminAuthStatus().then((s) => {
104
+ if (!alive)
105
+ return;
106
+ if (!s) {
107
+ setError('Could not reach the dashboard server, or the server is older than this panel. Run `buff dashboard` to start it.');
108
+ setLoading(false);
109
+ return;
110
+ }
111
+ setAuthStatus({ configured: s.configured, authenticated: s.authenticated });
112
+ setAuthed(s.authenticated);
113
+ setRole(s.role ?? null);
114
+ setUserName(s.user ?? null);
115
+ if (s.authenticated) {
116
+ void refresh();
117
+ void loadCatalog();
118
+ void loadUsers();
119
+ }
120
+ else {
121
+ setLoading(false);
122
+ }
123
+ });
124
+ return () => { alive = false; };
125
+ }, [refresh, loadCatalog, loadUsers]);
126
+ const isAdmin = role === 'admin';
127
+ const sessionExpired = () => {
128
+ setAuthed(false);
129
+ setAuthStatus((s) => (s ? { ...s, authenticated: false } : s));
130
+ setAuthError('Session expired — log in again.');
131
+ };
132
+ const handleLogin = async (e) => {
133
+ e.preventDefault();
134
+ setAuthBusy(true);
135
+ setAuthError(null);
136
+ const r = await api_1.dashboardAPI.adminLogin(user, password);
137
+ if (r.ok) {
138
+ setAuthed(true);
139
+ setAuthStatus({ configured: true, authenticated: true });
140
+ setRole(r.role ?? 'admin');
141
+ setUserName(r.user ?? null);
142
+ setUser('');
143
+ setPassword('');
144
+ void refresh();
145
+ void loadCatalog();
146
+ void loadUsers();
147
+ }
148
+ else {
149
+ setAuthError(r.error || 'Login failed.');
150
+ }
151
+ setAuthBusy(false);
152
+ };
153
+ const handleSetup = async (e) => {
154
+ e.preventDefault();
155
+ setAuthBusy(true);
156
+ setAuthError(null);
157
+ if (password.length < 8) {
158
+ setAuthError('Password must be at least 8 characters.');
159
+ setAuthBusy(false);
160
+ return;
161
+ }
162
+ if (password !== confirm) {
163
+ setAuthError('Passwords do not match.');
164
+ setAuthBusy(false);
165
+ return;
166
+ }
167
+ const r = await api_1.dashboardAPI.adminSetup(user, password);
168
+ if (r.ok) {
169
+ setAuthed(true);
170
+ setAuthStatus({ configured: true, authenticated: true });
171
+ setRole(r.role ?? 'admin');
172
+ setUserName(r.user ?? null);
173
+ setUser('');
174
+ setPassword('');
175
+ setConfirm('');
176
+ void refresh();
177
+ void loadCatalog();
178
+ void loadUsers();
179
+ }
180
+ else {
181
+ setAuthError(r.error || 'Setup failed.');
182
+ }
183
+ setAuthBusy(false);
184
+ };
185
+ const handleLogout = async () => {
186
+ await api_1.dashboardAPI.adminLogout();
187
+ setAuthed(false);
188
+ setAuthStatus((s) => (s ? { ...s, authenticated: false } : s));
189
+ setRole(null);
190
+ setUserName(null);
191
+ setData(null);
192
+ setDrafts({});
193
+ setRowMsg({});
194
+ setUsers([]);
195
+ };
196
+ /** Shut down the gateway (any terminal can restart it with `buff gateway start`). */
197
+ const stopGateway = async () => {
198
+ if (!window.confirm('Stop the running gateway? Channels will stop responding until you restart it with `buff gateway start`.'))
199
+ return;
200
+ setShutdownBusy('gateway');
201
+ setShutdownMsg(null);
202
+ const r = await api_1.dashboardAPI.shutdown('gateway');
203
+ setShutdownBusy(null);
204
+ if (r.ok) {
205
+ setShutdownMsg(r.stopped ? '✅ Gateway stopped.' : `ℹ️ ${r.reason || 'No running gateway found — nothing to stop.'}`);
206
+ }
207
+ else {
208
+ setShutdownMsg(`❌ ${r.error || 'Stop failed.'}`);
209
+ if (r.unauthorized)
210
+ sessionExpired();
211
+ }
212
+ };
213
+ /** Shut down this dashboard server — the page will disconnect. */
214
+ const stopDashboard = async () => {
215
+ if (!window.confirm('Shut down the dashboard? This page will disconnect and the server will exit. Restart with `buff dashboard`.'))
216
+ return;
217
+ setShutdownBusy('dashboard');
218
+ setShutdownMsg('🛑 Shutting down the dashboard — this page will disconnect…');
219
+ await api_1.dashboardAPI.shutdown('dashboard');
220
+ // The server exits on its own; if the request failed the page stays up.
221
+ setShutdownBusy(null);
222
+ setShutdownMsg('ℹ️ The dashboard server did not stop (see the server terminal). You can also stop it with `buff dashboard stop`.');
223
+ };
224
+ const addUser = async (e) => {
225
+ e.preventDefault();
226
+ setUsersMsg(null);
227
+ if (newUserPass.length < 8) {
228
+ setUsersMsg('Password must be at least 8 characters.');
229
+ return;
230
+ }
231
+ const r = await api_1.dashboardAPI.addAdminUser(newUserName, newUserPass, newUserRole);
232
+ if (r.ok) {
233
+ setNewUserName('');
234
+ setNewUserPass('');
235
+ setUsersMsg('✅ User added');
236
+ void loadUsers();
237
+ }
238
+ else {
239
+ setUsersMsg(`❌ ${r.error || 'Failed to add user'}`);
240
+ if (r.unauthorized)
241
+ sessionExpired();
242
+ }
243
+ };
244
+ const removeUser = async (name) => {
245
+ if (!window.confirm(`Remove dashboard user '${name}'?`))
246
+ return;
247
+ setUsersMsg(null);
248
+ const r = await api_1.dashboardAPI.removeAdminUser(name);
249
+ if (r.ok) {
250
+ setUsersMsg(`✅ Removed ${name}`);
251
+ void loadUsers();
252
+ }
253
+ else {
254
+ setUsersMsg(`❌ ${r.error || 'Failed to remove user'}`);
255
+ if (r.unauthorized)
256
+ sessionExpired();
257
+ }
258
+ };
259
+ const setDraft = (type, field, value) => {
260
+ setDrafts((d) => ({ ...d, [type]: { ...(d[type] || {}), [field]: value } }));
261
+ };
262
+ const saveProvider = async (type) => {
263
+ setBusy(type);
264
+ setRowMsg({});
265
+ const draft = drafts[type] || {};
266
+ const r = await api_1.dashboardAPI.saveProvider(type, draft);
267
+ if (r.ok) {
268
+ setData((d) => {
269
+ if (!d)
270
+ return d;
271
+ // Replace the row IN PLACE when it exists (keeps provider order), append when new.
272
+ const exists = d.providers.some((p) => p.type === type);
273
+ const providers = exists
274
+ ? d.providers.map((p) => (p.type === type && r.provider ? r.provider : p))
275
+ : r.provider
276
+ ? [...d.providers, r.provider]
277
+ : d.providers;
278
+ return { ...d, providers };
279
+ });
280
+ setDrafts((d) => { const next = { ...d }; delete next[type]; return next; });
281
+ setRowMsg((m) => ({ ...m, [type]: '✅ Saved' }));
282
+ }
283
+ else {
284
+ setRowMsg((m) => ({ ...m, [type]: `❌ ${r.error || 'Save failed'}` }));
285
+ if (r.unauthorized)
286
+ sessionExpired();
287
+ }
288
+ setBusy(null);
289
+ };
290
+ const removeProvider = async (type) => {
291
+ if (!window.confirm(`Remove ${type}'s API key and configuration? This cannot be undone.`))
292
+ return;
293
+ setBusy(type);
294
+ setRowMsg({});
295
+ const r = await api_1.dashboardAPI.deleteProvider(type);
296
+ if (r.ok) {
297
+ if (r.envSourced) {
298
+ // The key lives in the environment — the row stays (env re-injects on
299
+ // every load); explain how to actually remove it instead of hiding it.
300
+ setData((d) => (d ? { ...d, providers: d.providers.map((p) => (p.type === type ? (r.provider || p) : p)) } : d));
301
+ setRowMsg((m) => ({
302
+ ...m,
303
+ [type]: `⚠️ Key comes from ${r.envVar ? `$${r.envVar}` : 'an env var'} — unset it there to remove it (config can't override the env).`,
304
+ }));
305
+ }
306
+ else {
307
+ setData((d) => (d ? { ...d, providers: d.providers.filter((p) => p.type !== type) } : d));
308
+ setDrafts((d) => { const next = { ...d }; delete next[type]; return next; });
309
+ }
310
+ }
311
+ else {
312
+ setRowMsg((m) => ({ ...m, [type]: `❌ ${r.error || 'Remove failed'}` }));
313
+ if (r.unauthorized)
314
+ sessionExpired();
315
+ }
316
+ setBusy(null);
317
+ };
318
+ const testProvider = async (type) => {
319
+ setBusy(type);
320
+ const r = await api_1.dashboardAPI.testProvider(type);
321
+ setRowMsg((m) => ({
322
+ ...m,
323
+ [type]: r.ok
324
+ ? `✅ Connected — ${r.models?.length ?? 0} model(s) reachable`
325
+ : `❌ ${r.error || 'Connection failed'}`,
326
+ }));
327
+ setBusy(null);
328
+ };
329
+ const addProvider = () => {
330
+ if (!newType)
331
+ return;
332
+ setDrafts((d) => ({ ...d, [newType]: {} }));
333
+ setNewType('');
334
+ };
335
+ // ─── Auth gate ────────────────────────────────────────────────────────────
336
+ if (error && !authStatus) {
337
+ return (<div className="admin-header">
338
+ <h2 className="section-title">🛠️ Admin</h2>
339
+ <div className="admin-error">{error}</div>
340
+ </div>);
341
+ }
342
+ if (!authStatus || (!authed && loading)) {
343
+ return (<div className="admin-header">
344
+ <h2 className="section-title">🛠️ Admin</h2>
345
+ <div className="loading-state"><div className="loading-spinner"/><p>Loading…</p></div>
346
+ </div>);
347
+ }
348
+ if (!authed) {
349
+ const isSetup = !authStatus.configured;
350
+ return (<div className="admin-header">
351
+ <h2 className="section-title">🛠️ Admin — {isSetup ? 'Set up access' : 'Log in'}</h2>
352
+ <p className="admin-subtitle">
353
+ {isSetup
354
+ ? 'Create the admin user-id and password that gate the dashboard write surface (API-key/provider configuration). The CLI keeps working exactly as before — this only guards the GUI.'
355
+ : 'The write surface (API-key/provider configuration) is gated by the admin credentials. Read-only checks stay open.'}
356
+ </p>
357
+ <form className="admin-gate-form" onSubmit={isSetup ? handleSetup : handleLogin}>
358
+ <label>
359
+ <span>Username</span>
360
+ <input type="text" value={user} onChange={(e) => setUser(e.target.value)} autoComplete="username" placeholder="admin"/>
361
+ </label>
362
+ <label>
363
+ <span>Password</span>
364
+ <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} autoComplete={isSetup ? 'new-password' : 'current-password'} placeholder="••••••••"/>
365
+ </label>
366
+ {isSetup ? (<label>
367
+ <span>Confirm password</span>
368
+ <input type="password" value={confirm} onChange={(e) => setConfirm(e.target.value)} autoComplete="new-password" placeholder="••••••••"/>
369
+ </label>) : null}
370
+ {authError ? <div className="admin-error">{authError}</div> : null}
371
+ <button className="admin-refresh-btn" type="submit" disabled={authBusy || !user || !password}>
372
+ {authBusy ? '⏳ …' : isSetup ? '🔐 Create admin' : '🔐 Log in'}
373
+ </button>
374
+ {isSetup ? (<p className="admin-hint">
375
+ Automation alternative: set <code>BUFF_DASHBOARD_ADMIN_USER</code> and{' '}
376
+ <code>BUFF_DASHBOARD_ADMIN_PASSWORD</code> in the dashboard's environment.
377
+ </p>) : null}
378
+ </form>
379
+ </div>);
380
+ }
381
+ // ─── Authenticated: checks + provider editor ─────────────────────────────
382
+ const systemPass = data?.system.filter((c) => c.status === 'pass').length ?? 0;
383
+ const systemWarn = data?.system.filter((c) => c.status === 'warn').length ?? 0;
384
+ const systemFail = data?.system.filter((c) => c.status === 'fail').length ?? 0;
385
+ const entPass = data?.enterprise.filter((c) => c.status === 'pass').length ?? 0;
386
+ const entWarn = data?.enterprise.filter((c) => c.status === 'warn').length ?? 0;
387
+ const entFail = data?.enterprise.filter((c) => c.status === 'fail').length ?? 0;
388
+ const providers = data?.providers ?? [];
389
+ const catalogLabel = (type) => {
390
+ const entry = catalog.find((c) => c.id === type);
391
+ return entry ? entry : { label: type };
392
+ };
393
+ return (<>
394
+ <div className="admin-header">
395
+ <h2 className="section-title">
396
+ 🛠️ Admin — Command Runner &amp; Configuration
397
+ {userName ? (<span className={`admin-role-badge admin-role-${role || 'viewer'}`}>
398
+ {userName} · {role || 'viewer'}
399
+ </span>) : null}
400
+ </h2>
401
+ <div className="admin-header-actions">
402
+ <button className="admin-refresh-btn" onClick={() => void refresh()} disabled={loading}>
403
+ {loading ? '⏳ Running checks…' : '🔄 Refresh (run all commands)'}
404
+ </button>
405
+ {isAdmin || role === 'operator' ? (<button className="admin-logout-btn" onClick={() => void stopGateway()} disabled={shutdownBusy !== null} title="Stop the running gateway (buff gateway stop)">
406
+ {shutdownBusy === 'gateway' ? '⏳ Stopping…' : '⏻ Stop gateway'}
407
+ </button>) : null}
408
+ {isAdmin ? (<button className="admin-logout-btn" onClick={() => void stopDashboard()} disabled={shutdownBusy !== null} title="Shut down this dashboard server (buff dashboard stop)">
409
+ {shutdownBusy === 'dashboard' ? '⏳ Stopping…' : '🛑 Stop dashboard'}
410
+ </button>) : null}
411
+ <button className="admin-logout-btn" onClick={() => void handleLogout()}>🚪 Log out</button>
412
+ </div>
413
+ </div>
414
+ {shutdownMsg ? <div className="admin-error">{shutdownMsg}</div> : null}
415
+ {!isAdmin ? (<div className="admin-readonly-note">
416
+ 🔒 Your role (<code>{role || 'viewer'}</code>) is read-only here — provider configuration
417
+ and user management require the <code>admin</code> role. The CLI enforces the same
418
+ governance policy (rbac.json).
419
+ </div>) : null}
420
+ <p className="admin-subtitle">
421
+ The dashboard executes the same state commands as the CLI — no command needed. Provider
422
+ keys are masked here and every write goes through the same config the CLI uses.
423
+ </p>
424
+
425
+ {error ? <div className="admin-error">{error}</div> : null}
426
+
427
+ {data ? (<>
428
+ <div className="admin-summary-grid">
429
+ <div className="admin-summary-card admin-summary-pass">
430
+ <div className="admin-summary-value">{systemPass + entPass}</div>
431
+ <div className="admin-summary-label">✅ Passing</div>
432
+ </div>
433
+ <div className="admin-summary-card admin-summary-warn">
434
+ <div className="admin-summary-value">{systemWarn + entWarn}</div>
435
+ <div className="admin-summary-label">⚠️ Warnings</div>
436
+ </div>
437
+ <div className="admin-summary-card admin-summary-fail">
438
+ <div className="admin-summary-value">{systemFail + entFail}</div>
439
+ <div className="admin-summary-label">❌ Failing</div>
440
+ </div>
441
+ <div className="admin-summary-card">
442
+ <div className="admin-summary-value">{providers.length}</div>
443
+ <div className="admin-summary-label">Providers</div>
444
+ </div>
445
+ </div>
446
+
447
+ <h3 className="section-subtitle">⚙️ System Checks (buff doctor)</h3>
448
+ <div className="admin-check-list">
449
+ {data.system.map((c) => <CheckRow key={c.name} check={c}/>)}
450
+ </div>
451
+
452
+ <h3 className="section-subtitle">🏥 Enterprise Self-Check (doctor --enterprise)</h3>
453
+ <div className="admin-check-list">
454
+ {data.enterprise.length > 0
455
+ ? data.enterprise.map((c) => <CheckRow key={c.name} check={c}/>)
456
+ : <div className="empty-state">No enterprise checks returned.</div>}
457
+ </div>
458
+
459
+ <h3 className="section-subtitle">🔑 Provider Configuration (keys masked)</h3>
460
+ <div className="admin-table-wrapper">
461
+ <table className="admin-table">
462
+ <thead>
463
+ <tr>
464
+ <th>Provider</th>
465
+ <th>Status</th>
466
+ <th>Key</th>
467
+ <th>Base URL</th>
468
+ <th>Model</th>
469
+ <th>Actions</th>
470
+ </tr>
471
+ </thead>
472
+ <tbody>
473
+ {providers.map((p) => {
474
+ const meta = catalogLabel(p.type);
475
+ const draft = drafts[p.type] || {};
476
+ return (<tr key={p.type}>
477
+ <td className="admin-provider-type">
478
+ {meta.icon ? `${meta.icon} ` : ''}{meta.label}
479
+ <div className="admin-provider-sub">
480
+ {p.type} · {KEY_SOURCE_LABEL[p.keySource] || p.keySource}
481
+ </div>
482
+ {p.keySource === 'env' ? (<div className="admin-hint">comes from {meta.envVar || 'env var'} — change it there, config can't override it</div>) : p.keySource === 'vault' ? (<div className="admin-hint">stored in the OS keyring (vault)</div>) : null}
483
+ </td>
484
+ <td>
485
+ <span className={`admin-check-badge ${p.configured ? 'admin-check-pass' : 'admin-check-warn'}`}>
486
+ {p.configured ? '✅ Configured' : '⚠️ Not configured'}
487
+ </span>
488
+ </td>
489
+ <td>
490
+ {meta.keyless ? (<div className="admin-hint">No key needed (probed)</div>) : isAdmin ? (<input className="admin-input admin-key-input" type="password" aria-label={`${p.type} API key`} placeholder={p.keyMasked || 'set a new key…'} value={draft.apiKey ?? ''} onChange={(e) => setDraft(p.type, 'apiKey', e.target.value)} autoComplete="new-password"/>) : (<div className="admin-hint">{p.keyMasked || '—'}</div>)}
491
+ </td>
492
+ <td>
493
+ {isAdmin ? (<input className="admin-input" type="text" aria-label={`${p.type} base URL`} placeholder={p.baseUrl || 'default endpoint'} value={draft.baseUrl ?? ''} onChange={(e) => setDraft(p.type, 'baseUrl', e.target.value)}/>) : (<div className="admin-hint">{p.baseUrl ? p.baseUrl.replace(/^https?:\/\/(?:[^@/]*@)?/, '') : 'default endpoint'}</div>)}
494
+ </td>
495
+ <td>
496
+ {isAdmin ? (<input className="admin-input" type="text" aria-label={`${p.type} model`} placeholder={p.model || 'default model'} value={draft.model ?? ''} onChange={(e) => setDraft(p.type, 'model', e.target.value)}/>) : (<div className="admin-hint">{p.model || 'default model'}</div>)}
497
+ </td>
498
+ <td>
499
+ {isAdmin ? (<>
500
+ <div className="admin-row-actions">
501
+ <button className="admin-mini-btn" disabled={busy === p.type} onClick={() => void saveProvider(p.type)}>💾 Save</button>
502
+ <button className="admin-mini-btn" disabled={busy === p.type} title="Tests the SAVED configuration — save first to test a new key" onClick={() => void testProvider(p.type)}>🔌 Test</button>
503
+ <button className="admin-mini-btn admin-mini-danger" disabled={busy === p.type} onClick={() => void removeProvider(p.type)}>🗑 Remove</button>
504
+ </div>
505
+ {rowMsg[p.type] ? <div className="admin-row-msg">{rowMsg[p.type]}</div> : null}
506
+ </>) : (<div className="admin-hint">read-only</div>)}
507
+ </td>
508
+ </tr>);
509
+ })}
510
+ {providers.length === 0 ? (<tr>
511
+ <td colSpan={6} className="admin-empty-cell">No providers configured yet.</td>
512
+ </tr>) : null}
513
+ </tbody>
514
+ </table>
515
+ </div>
516
+
517
+ {isAdmin && catalog.length > 0 ? (<div className="admin-add-provider">
518
+ <select className="admin-input" value={newType} onChange={(e) => setNewType(e.target.value)}>
519
+ <option value="">Add a provider…</option>
520
+ {catalog
521
+ .filter((c) => !providers.some((p) => p.type === c.id))
522
+ .map((c) => (<option key={c.id} value={c.id}>
523
+ {c.icon ? `${c.icon} ` : ''}{c.label} ({c.id})
524
+ </option>))}
525
+ </select>
526
+ <button className="admin-mini-btn" disabled={!newType} onClick={addProvider}>➕ Add</button>
527
+ </div>) : null}
528
+
529
+ <QuotaPanel_1.default authed={authed} role={role || 'viewer'}/>
530
+
531
+ {isAdmin ? (<>
532
+ <h3 className="section-subtitle">👥 Dashboard Users (role.manage)</h3>
533
+ <div className="admin-users-list">
534
+ {users.map((u) => (<div className="admin-user-row" key={u.user}>
535
+ <span className="admin-user-name">{u.user}</span>
536
+ <span className={`admin-role-badge admin-role-${u.role}`}>{u.role}</span>
537
+ <span className="admin-user-since">since {new Date(u.createdAt).toLocaleDateString()}</span>
538
+ {u.user !== userName ? (<button className="admin-mini-btn admin-mini-danger" onClick={() => void removeUser(u.user)}>🗑 Remove user</button>) : (<span className="admin-hint">you</span>)}
539
+ </div>))}
540
+ {users.length === 0 ? <div className="empty-state">No dashboard users yet.</div> : null}
541
+ </div>
542
+ <form className="admin-gate-form admin-user-form" onSubmit={addUser}>
543
+ <label>
544
+ <span>Username</span>
545
+ <input type="text" value={newUserName} onChange={(e) => setNewUserName(e.target.value)} autoComplete="off"/>
546
+ </label>
547
+ <label>
548
+ <span>Password (min 8 chars)</span>
549
+ <input type="password" value={newUserPass} onChange={(e) => setNewUserPass(e.target.value)} autoComplete="new-password"/>
550
+ </label>
551
+ <label>
552
+ <span>Role</span>
553
+ <select value={newUserRole} onChange={(e) => setNewUserRole(e.target.value)}>
554
+ <option value="viewer">viewer — read-only</option>
555
+ <option value="operator">operator — read-only in the dashboard today</option>
556
+ <option value="admin">admin — full control</option>
557
+ </select>
558
+ </label>
559
+ {usersMsg ? <div className="admin-row-msg">{usersMsg}</div> : null}
560
+ <button className="admin-refresh-btn" type="submit" disabled={!newUserName || !newUserPass}>
561
+ ➕ Add user
562
+ </button>
563
+ </form>
564
+ </>) : null}
565
+ </>) : loading ? (<div className="loading-state"><div className="loading-spinner"/><p>Running all state checks…</p></div>) : null}
566
+ </>);
567
+ }