Skip to content

Commit 351953f

Browse files
committed
Add setup diagnostics and model testing
1 parent a622621 commit 351953f

26 files changed

Lines changed: 890 additions & 46 deletions

‎apps/admin-web/src/api/client.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import type {
22
ApiResult,
33
ModelCatalogItem,
4+
DiagnosticCheck,
45
ModelProvider,
56
ModelSettings,
7+
ModelTestResult,
68
PendingAction,
79
PermissionPolicy,
810
RunDetails,
911
RunListItem,
1012
Schedule,
13+
SetupStatus,
1114
VfsEntry,
1215
VfsFile
1316
} from "./types";
@@ -120,6 +123,14 @@ export function createAdminClient(getToken: () => string) {
120123
}>
121124
>("/admin/model-settings");
122125
},
126+
getSetupStatus: () => {
127+
return request<ApiResult<{ status: SetupStatus }>>("/admin/setup/status");
128+
},
129+
getDiagnostics: () => {
130+
return request<ApiResult<{ checks: DiagnosticCheck[]; healthy: boolean }>>(
131+
"/admin/diagnostics"
132+
);
133+
},
123134
createModelProvider: (body: {
124135
name: string;
125136
providerType: ModelProvider["providerType"];
@@ -142,6 +153,15 @@ export function createAdminClient(getToken: () => string) {
142153
{ method: "POST" }
143154
);
144155
},
156+
testProviderModel: (providerId: string, body: { modelId?: string; prompt?: string } = {}) => {
157+
return request<ApiResult<{ result: ModelTestResult }>>(
158+
`/admin/model-providers/${providerId}/test`,
159+
{
160+
method: "POST",
161+
body: JSON.stringify(body)
162+
}
163+
);
164+
},
145165
activateModel: (body: { providerId: string; modelId: string }) => {
146166
return request<ApiResult<{ settings: ModelSettings }>>("/admin/model-settings", {
147167
method: "PUT",

‎apps/admin-web/src/api/types.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,31 @@ export type ModelSettings = {
106106
modelId?: string;
107107
updatedAt: string;
108108
};
109+
110+
export type SetupStep = {
111+
id: "provider" | "credential" | "models" | "active_model";
112+
label: string;
113+
status: "done" | "pending";
114+
detail: string;
115+
};
116+
117+
export type SetupStatus = {
118+
ready: boolean;
119+
steps: SetupStep[];
120+
activeProvider?: string;
121+
activeModel?: string;
122+
};
123+
124+
export type DiagnosticCheck = {
125+
id: string;
126+
label: string;
127+
status: "ok" | "warn" | "error";
128+
detail: string;
129+
};
130+
131+
export type ModelTestResult = {
132+
providerId: string;
133+
modelId: string;
134+
latencyMs: number;
135+
content?: string;
136+
};

‎apps/admin-web/src/styles/components.css‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@
9494
.status-completed,
9595
.status-success,
9696
.status-active,
97+
.status-done,
98+
.status-ok,
9799
.status-file,
98100
.status-directory {
99101
background: #dff3e8;
@@ -102,12 +104,14 @@
102104

103105
.status-failed,
104106
.status-deleted,
107+
.status-error,
105108
.status-cancelled {
106109
background: #f8dfdf;
107110
color: #8f2020;
108111
}
109112

110113
.status-pending,
114+
.status-warn,
111115
.status-needs_confirmation {
112116
background: #fff0cd;
113117
color: #7b4c00;

‎apps/admin-web/src/styles/panels.css‎

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,76 @@
5252
font-weight: 700;
5353
}
5454

55+
.secondary-button {
56+
display: inline-flex;
57+
align-items: center;
58+
justify-content: center;
59+
gap: 8px;
60+
min-height: 38px;
61+
border: 1px solid #cfd7e4;
62+
border-radius: 7px;
63+
background: #ffffff;
64+
color: #33425a;
65+
padding: 0 14px;
66+
font-weight: 700;
67+
}
68+
5569
.primary-button:disabled {
5670
opacity: 0.58;
5771
cursor: default;
5872
}
5973

74+
.setup-actions {
75+
display: flex;
76+
flex-wrap: wrap;
77+
gap: 10px;
78+
margin-bottom: 16px;
79+
}
80+
81+
.setup-list,
82+
.diagnostic-list {
83+
display: grid;
84+
gap: 8px;
85+
}
86+
87+
.setup-row,
88+
.diagnostic-row,
89+
.model-test-result {
90+
display: grid;
91+
gap: 8px;
92+
border: 1px solid #dce3ee;
93+
border-radius: 7px;
94+
background: #ffffff;
95+
padding: 12px;
96+
}
97+
98+
.setup-row,
99+
.diagnostic-row {
100+
grid-template-columns: auto minmax(0, 1fr) auto;
101+
align-items: center;
102+
}
103+
104+
.diagnostic-row {
105+
grid-template-columns: minmax(0, 1fr) auto;
106+
}
107+
108+
.setup-row strong,
109+
.diagnostic-row strong,
110+
.model-test-result strong,
111+
.setup-row span,
112+
.diagnostic-row span,
113+
.model-test-result span {
114+
display: block;
115+
overflow-wrap: anywhere;
116+
}
117+
118+
.setup-row span,
119+
.diagnostic-row span,
120+
.model-test-result span {
121+
color: #687386;
122+
font-size: 12px;
123+
}
124+
60125
.path-bar {
61126
grid-template-columns: minmax(0, 1fr) auto auto;
62127
}
@@ -82,7 +147,7 @@
82147

83148
.model-provider-head {
84149
display: grid;
85-
grid-template-columns: minmax(180px, 1fr) auto auto auto;
150+
grid-template-columns: minmax(180px, 1fr) auto auto auto auto;
86151
gap: 10px;
87152
align-items: center;
88153
margin-bottom: 10px;
@@ -106,15 +171,15 @@
106171
}
107172

108173
.model-choice {
109-
display: flex;
174+
display: grid;
175+
grid-template-columns: minmax(0, 1fr) auto;
110176
align-items: center;
111-
justify-content: space-between;
112177
min-height: 38px;
113178
border: 1px solid #dce3ee;
114179
border-radius: 7px;
115180
background: #ffffff;
116181
color: #253044;
117-
padding: 0 10px;
182+
padding: 2px;
118183
text-align: left;
119184
}
120185

@@ -123,6 +188,25 @@
123188
background: #edf5ff;
124189
}
125190

191+
.model-activate {
192+
display: flex;
193+
min-width: 0;
194+
min-height: 34px;
195+
align-items: center;
196+
justify-content: space-between;
197+
gap: 8px;
198+
border: 0;
199+
background: transparent;
200+
color: #253044;
201+
padding: 0 8px;
202+
text-align: left;
203+
}
204+
205+
.model-activate span {
206+
min-width: 0;
207+
overflow-wrap: anywhere;
208+
}
209+
126210
.editor-pane {
127211
display: grid;
128212
grid-template-rows: auto 1fr;

‎apps/admin-web/src/ui/App.tsx‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { useMemo, useState } from "react";
22
import { createAdminClient } from "../api/client";
33
import { ChatPanel } from "./panels/ChatPanel";
4+
import { DiagnosticsPanel } from "./panels/DiagnosticsPanel";
45
import { ModelsPanel } from "./panels/ModelsPanel";
56
import { PendingPanel } from "./panels/PendingPanel";
67
import { PermissionsPanel } from "./panels/PermissionsPanel";
78
import { RunsPanel } from "./panels/RunsPanel";
89
import { SchedulesPanel } from "./panels/SchedulesPanel";
10+
import { SetupPanel } from "./panels/SetupPanel";
911
import { SystemPanel } from "./panels/SystemPanel";
1012
import { VfsPanel } from "./panels/VfsPanel";
1113
import { Sidebar } from "./Sidebar";
@@ -14,7 +16,9 @@ import type { ViewId } from "./views";
1416
const tokenKey = "serverless-agent:admin-token";
1517

1618
export function App() {
17-
const [active, setActive] = useState<ViewId>("chat");
19+
const [active, setActive] = useState<ViewId>(() =>
20+
localStorage.getItem(tokenKey) ? "setup" : "system"
21+
);
1822
const [selectedRunId, setSelectedRunId] = useState("");
1923
const [token, setToken] = useState(() => localStorage.getItem(tokenKey) ?? "");
2024
const [notice, setNotice] = useState<{ message: string; tone: "ok" | "error" }>();
@@ -40,8 +44,12 @@ export function App() {
4044
<Sidebar active={active} onChange={setActive} />
4145
<main className="main-surface">
4246
{notice && <div className={`notice ${notice.tone}`}>{notice.message}</div>}
47+
{active === "setup" && (
48+
<SetupPanel client={client} notify={notify} onNavigate={setActive} />
49+
)}
4350
{active === "chat" && <ChatPanel client={client} notify={notify} onRun={openRun} />}
4451
{active === "models" && <ModelsPanel client={client} notify={notify} />}
52+
{active === "diagnostics" && <DiagnosticsPanel client={client} notify={notify} />}
4553
{active === "runs" && (
4654
<RunsPanel client={client} notify={notify} selectedRunId={selectedRunId} />
4755
)}

‎apps/admin-web/src/ui/Sidebar.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import {
22
CalendarClock,
3+
ClipboardCheck,
34
Database,
45
FileText,
56
KeyRound,
67
MessageSquare,
8+
Activity,
79
SlidersHorizontal,
810
ShieldCheck,
911
Workflow
@@ -15,8 +17,10 @@ const items: Array<{
1517
label: string;
1618
icon: typeof MessageSquare;
1719
}> = [
20+
{ id: "setup", label: "Setup", icon: ClipboardCheck },
1821
{ id: "chat", label: "Chat", icon: MessageSquare },
1922
{ id: "models", label: "Models", icon: SlidersHorizontal },
23+
{ id: "diagnostics", label: "Diagnostics", icon: Activity },
2024
{ id: "runs", label: "Runs", icon: Workflow },
2125
{ id: "vfs", label: "VFS", icon: FileText },
2226
{ id: "schedules", label: "Schedules", icon: CalendarClock },
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { RefreshCw } from "lucide-react";
2+
import { useEffect, useState } from "react";
3+
import type { DiagnosticCheck } from "../../api/types";
4+
import { StatusBadge } from "../StatusBadge";
5+
import { ToolbarButton } from "../ToolbarButton";
6+
import type { PanelProps } from "./types";
7+
8+
export function DiagnosticsPanel({ client, notify }: PanelProps) {
9+
const [checks, setChecks] = useState<DiagnosticCheck[]>([]);
10+
const [healthy, setHealthy] = useState(false);
11+
12+
async function load() {
13+
try {
14+
const result = await client.getDiagnostics();
15+
setChecks(result.checks);
16+
setHealthy(result.healthy);
17+
} catch (error) {
18+
notify(error instanceof Error ? error.message : "Diagnostics failed", "error");
19+
}
20+
}
21+
22+
useEffect(() => {
23+
void load();
24+
}, []);
25+
26+
return (
27+
<section className="panel">
28+
<header className="panel-header">
29+
<div>
30+
<h1>Diagnostics</h1>
31+
<p>{healthy ? "healthy" : "attention needed"}</p>
32+
</div>
33+
<ToolbarButton label="Run diagnostics" icon={RefreshCw} onClick={() => void load()} />
34+
</header>
35+
36+
<div className="diagnostic-list">
37+
{checks.map((check) => (
38+
<div className="diagnostic-row" key={check.id}>
39+
<div>
40+
<strong>{check.label}</strong>
41+
<span>{check.detail}</span>
42+
</div>
43+
<StatusBadge value={check.status} />
44+
</div>
45+
))}
46+
</div>
47+
</section>
48+
);
49+
}

0 commit comments

Comments
 (0)