Dashboard: surface fleet load errors instead of infinite skeletons (0.38.1)

The cockpit cards gated purely on `fleet.data`, so any failed
/api/dashboard/fleet request (e.g. a stale backend returning 404, or a
500) left the new components stuck on skeletons forever. Render a clear
error banner with the API message and a Retry button when the query
errors with no data, so the actual cause is visible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
menzelj
2026-06-24 11:17:32 +00:00
co-authored by Claude Opus 4.8
parent c830d28b65
commit 0b95d7d4a2
3 changed files with 46 additions and 28 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
"""Single source of truth for the StackPilot release version.""" """Single source of truth for the StackPilot release version."""
APP_VERSION = "0.38.0" APP_VERSION = "0.38.1"
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "stackpilot-frontend", "name": "stackpilot-frontend",
"private": true, "private": true,
"version": "0.38.0", "version": "0.38.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+22 -4
View File
@@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Clock, RefreshCw } from "lucide-react"; import { AlertTriangle, Clock, RefreshCw } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Card } from "@/components/ui"; import { Card } from "@/components/ui";
import { HostHeader } from "@/components/hosts/HostHeader"; import { HostHeader } from "@/components/hosts/HostHeader";
@@ -66,10 +66,26 @@ export function Dashboard() {
</button> </button>
</div> </div>
{/* ---- Needs attention ---- */} {/* ---- Fleet cockpit (attention + KPIs + status + hosts) ---- */}
{fleet.isError && !fleet.data ? (
<div className="flex items-center justify-between gap-3 rounded-card border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/50 dark:bg-red-900/20 dark:text-red-300">
<span className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 shrink-0" />
Couldnt load fleet overview: {apiErrorMessage(fleet.error)}
</span>
<button
onClick={refreshFleet}
className="shrink-0 rounded-pill border border-red-300 px-3 py-1 text-xs font-medium hover:bg-red-100 dark:border-red-800 dark:hover:bg-red-900/40"
>
Retry
</button>
</div>
) : (
<>
{/* Needs attention */}
<AttentionStrip items={fleet.data?.attention} loading={fleet.isLoading} /> <AttentionStrip items={fleet.data?.attention} loading={fleet.isLoading} />
{/* ---- Fleet KPIs ---- */} {/* Fleet KPIs */}
{fleet.data ? ( {fleet.data ? (
<FleetKpiRow kpis={fleet.data.kpis} /> <FleetKpiRow kpis={fleet.data.kpis} />
) : ( ) : (
@@ -80,7 +96,7 @@ export function Dashboard() {
</div> </div>
)} )}
{/* ---- Status + hosts ---- */} {/* Status + hosts */}
<div className="grid grid-cols-1 gap-4 lg:grid-cols-[340px_1fr]"> <div className="grid grid-cols-1 gap-4 lg:grid-cols-[340px_1fr]">
{fleet.data ? ( {fleet.data ? (
<StackStatusBar totals={fleet.data.status_totals} unhealthy={fleet.data.kpis.unhealthy} /> <StackStatusBar totals={fleet.data.status_totals} unhealthy={fleet.data.kpis.unhealthy} />
@@ -93,6 +109,8 @@ export function Dashboard() {
<div className="sp-skeleton h-40" /> <div className="sp-skeleton h-40" />
)} )}
</div> </div>
</>
)}
{/* ---- Local host stacks ---- */} {/* ---- Local host stacks ---- */}
<section> <section>