Skip to content

Commit 17eb560

Browse files
committed
feat(console): ProviderHealthPanel — inline login action + inactive collapse
Adds a shared ProviderHealthPanel component used by both Dashboard and Gateway pages to surface provider health with actionable UI. - New component: console/src/components/provider-health.tsx - Shared ProviderHealthPanel with ProviderRow - Resolves status (healthy / warning / inactive / down) per provider - Inactive local providers (ollama, lmstudio, llamacpp, vllm) that are not running collapse into a single footer line instead of red rows - Warning rows get an amber left-border accent + pulsing status dot - "Log in →" appears inline as an action chip when loginRequired is true (no provider name hardcoding — driven by backend flag) - Backend: adds loginRequired?: boolean to GatewayProviderHealthEntry and sets it in buildGatewayProviderHealth when codex.reloginRequired - Console types: propagates loginRequired onto the providerHealth record - gateway.tsx and dashboard.tsx replaced their list-stack provider rows with <ProviderHealthPanel>
1 parent 1a64b1e commit 17eb560

7 files changed

Lines changed: 476 additions & 54 deletions

File tree

console/src/api/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export interface GatewayStatus {
8787
error?: string;
8888
modelCount?: number;
8989
detail?: string;
90+
loginRequired?: boolean;
9091
}
9192
>;
9293
localBackends?: Record<
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
/* ─── Animations ───────────────────────────────────────────────────────────── */
2+
3+
@keyframes pulse {
4+
0%, 100% { opacity: 1; }
5+
50% { opacity: 0.45; }
6+
}
7+
8+
/* ─── Panel shell ──────────────────────────────────────────────────────────── */
9+
10+
.panel {
11+
border-radius: 10px;
12+
border: 1px solid rgba(0, 0, 0, 0.08);
13+
background: #fff;
14+
overflow: hidden;
15+
}
16+
17+
.panelHeader {
18+
padding: 12px 16px 11px;
19+
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
20+
display: flex;
21+
align-items: center;
22+
justify-content: space-between;
23+
}
24+
25+
.panelTitle {
26+
font-size: 12.5px;
27+
font-weight: 600;
28+
color: #111827;
29+
letter-spacing: -0.01em;
30+
}
31+
32+
/* ─── Row ──────────────────────────────────────────────────────────────────── */
33+
34+
.row {
35+
display: flex;
36+
flex-direction: column;
37+
padding: 9px 16px;
38+
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
39+
border-left: 2.5px solid transparent;
40+
transition: background 0.1s ease;
41+
}
42+
43+
.row:last-of-type {
44+
border-bottom: none;
45+
}
46+
47+
.row:hover {
48+
background: rgba(0, 0, 0, 0.012);
49+
}
50+
51+
.rowWarning {
52+
border-left-color: #f59e0b;
53+
background: rgba(245, 158, 11, 0.03);
54+
}
55+
56+
.rowWarning:hover {
57+
background: rgba(245, 158, 11, 0.055);
58+
}
59+
60+
.rowDown {
61+
border-left-color: #ef4444;
62+
background: rgba(239, 68, 68, 0.02);
63+
}
64+
65+
.rowDown:hover {
66+
background: rgba(239, 68, 68, 0.04);
67+
}
68+
69+
/* ─── Row top: name line + meta ────────────────────────────────────────────── */
70+
71+
.rowTop {
72+
display: flex;
73+
align-items: center;
74+
justify-content: space-between;
75+
gap: 12px;
76+
}
77+
78+
.nameGroup {
79+
display: flex;
80+
align-items: center;
81+
gap: 6px;
82+
min-width: 0;
83+
}
84+
85+
/* ─── Status dot ───────────────────────────────────────────────────────────── */
86+
87+
.dot {
88+
width: 6px;
89+
height: 6px;
90+
border-radius: 50%;
91+
flex-shrink: 0;
92+
}
93+
94+
.dotHealthy { background: #22c55e; box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.15); }
95+
.dotWarning {
96+
background: #f59e0b;
97+
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.15);
98+
animation: pulse 2s ease-in-out infinite;
99+
}
100+
.dotDown {
101+
background: #ef4444;
102+
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.15);
103+
animation: pulse 1.6s ease-in-out infinite;
104+
}
105+
.dotInactive { background: #d1d5db; }
106+
107+
/* ─── Name + badge ─────────────────────────────────────────────────────────── */
108+
109+
.name {
110+
font-size: 12.5px;
111+
font-weight: 600;
112+
color: #111827;
113+
letter-spacing: -0.015em;
114+
line-height: 1.2;
115+
white-space: nowrap;
116+
overflow: hidden;
117+
text-overflow: ellipsis;
118+
}
119+
120+
.badge {
121+
font-size: 9px;
122+
font-weight: 600;
123+
padding: 1.5px 5px;
124+
border-radius: 4px;
125+
text-transform: uppercase;
126+
letter-spacing: 0.07em;
127+
line-height: 1.5;
128+
white-space: nowrap;
129+
flex-shrink: 0;
130+
}
131+
132+
.badgeRemote {
133+
background: #eff6ff;
134+
color: #3b82f6;
135+
border: 1px solid rgba(59, 130, 246, 0.18);
136+
}
137+
138+
.badgeLocal {
139+
background: #f0fdf4;
140+
color: #16a34a;
141+
border: 1px solid rgba(22, 163, 74, 0.18);
142+
}
143+
144+
/* ─── Meta (right side of name line) ──────────────────────────────────────── */
145+
146+
.meta {
147+
display: flex;
148+
align-items: center;
149+
gap: 8px;
150+
flex-shrink: 0;
151+
}
152+
153+
.detail {
154+
font-size: 11px;
155+
color: #6b7280;
156+
font-family: "SFMono-Regular", "JetBrains Mono", "Fira Code", monospace;
157+
white-space: nowrap;
158+
}
159+
160+
.modelCount {
161+
font-size: 11px;
162+
color: #9ca3af;
163+
white-space: nowrap;
164+
font-variant-numeric: tabular-nums;
165+
}
166+
167+
/* ─── Warning inline action ────────────────────────────────────────────────── */
168+
169+
.loginBtn {
170+
font-size: 11.5px;
171+
font-weight: 500;
172+
padding: 3px 9px;
173+
border-radius: 5px;
174+
border: 1px solid rgba(217, 119, 6, 0.45);
175+
background: rgba(255, 251, 235, 0.9);
176+
color: #92400e;
177+
cursor: pointer;
178+
white-space: nowrap;
179+
line-height: 1.5;
180+
transition: background 0.1s ease, border-color 0.1s ease;
181+
flex-shrink: 0;
182+
}
183+
184+
.loginBtn:hover {
185+
background: #fef3c7;
186+
border-color: #d97706;
187+
}
188+
189+
.loginBtn:active {
190+
background: #fde68a;
191+
}
192+
193+
/* ─── Inactive footer ──────────────────────────────────────────────────────── */
194+
195+
.inactiveFooter {
196+
display: flex;
197+
align-items: center;
198+
gap: 6px;
199+
padding: 8px 16px;
200+
border-top: 1px solid rgba(0, 0, 0, 0.04);
201+
font-size: 11px;
202+
color: #b0b7c3;
203+
}
204+
205+
.inactiveDot {
206+
width: 5px;
207+
height: 5px;
208+
border-radius: 50%;
209+
background: #e5e7eb;
210+
flex-shrink: 0;
211+
}
212+
213+
.inactiveNames {
214+
font-family: "SFMono-Regular", "JetBrains Mono", "Fira Code", monospace;
215+
font-size: 10.5px;
216+
color: #c4c9d4;
217+
}
218+
219+
/* ─── Empty state ──────────────────────────────────────────────────────────── */
220+
221+
.panelEmpty {
222+
padding: 20px 16px;
223+
font-size: 13px;
224+
color: #9ca3af;
225+
text-align: center;
226+
}
227+
228+
/* ─── Dark mode ────────────────────────────────────────────────────────────── */
229+
230+
:global(html[data-theme="dark"]) .panel {
231+
background: #111827;
232+
border-color: rgba(255, 255, 255, 0.07);
233+
}
234+
235+
:global(html[data-theme="dark"]) .panelHeader {
236+
border-bottom-color: rgba(255, 255, 255, 0.05);
237+
}
238+
239+
:global(html[data-theme="dark"]) .panelTitle {
240+
color: #e5edf7;
241+
}
242+
243+
:global(html[data-theme="dark"]) .row {
244+
border-bottom-color: rgba(255, 255, 255, 0.04);
245+
}
246+
247+
:global(html[data-theme="dark"]) .row:hover {
248+
background: rgba(255, 255, 255, 0.02);
249+
}
250+
251+
:global(html[data-theme="dark"]) .rowWarning {
252+
border-left-color: #d97706;
253+
background: rgba(245, 158, 11, 0.05);
254+
}
255+
256+
:global(html[data-theme="dark"]) .rowWarning:hover {
257+
background: rgba(245, 158, 11, 0.09);
258+
}
259+
260+
:global(html[data-theme="dark"]) .rowDown {
261+
border-left-color: #dc2626;
262+
background: rgba(239, 68, 68, 0.04);
263+
}
264+
265+
:global(html[data-theme="dark"]) .rowDown:hover {
266+
background: rgba(239, 68, 68, 0.07);
267+
}
268+
269+
:global(html[data-theme="dark"]) .name {
270+
color: #e5edf7;
271+
}
272+
273+
:global(html[data-theme="dark"]) .badgeRemote {
274+
background: rgba(59, 130, 246, 0.12);
275+
color: #93c5fd;
276+
border-color: rgba(59, 130, 246, 0.22);
277+
}
278+
279+
:global(html[data-theme="dark"]) .badgeLocal {
280+
background: rgba(22, 163, 74, 0.12);
281+
color: #86efac;
282+
border-color: rgba(22, 163, 74, 0.22);
283+
}
284+
285+
:global(html[data-theme="dark"]) .detail {
286+
color: #6b7280;
287+
}
288+
289+
:global(html[data-theme="dark"]) .modelCount {
290+
color: #4b5563;
291+
}
292+
293+
:global(html[data-theme="dark"]) .loginBtn {
294+
background: rgba(245, 158, 11, 0.09);
295+
border-color: rgba(245, 158, 11, 0.3);
296+
color: #fbbf24;
297+
}
298+
299+
:global(html[data-theme="dark"]) .loginBtn:hover {
300+
background: rgba(245, 158, 11, 0.15);
301+
border-color: rgba(245, 158, 11, 0.5);
302+
}
303+
304+
:global(html[data-theme="dark"]) .inactiveFooter {
305+
border-top-color: rgba(255, 255, 255, 0.04);
306+
color: #374151;
307+
}
308+
309+
:global(html[data-theme="dark"]) .inactiveDot {
310+
background: #374151;
311+
}
312+
313+
:global(html[data-theme="dark"]) .inactiveNames {
314+
color: #374151;
315+
}

0 commit comments

Comments
 (0)