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",
+44 -26
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,33 +66,51 @@ export function Dashboard() {
</button> </button>
</div> </div>
{/* ---- Needs attention ---- */} {/* ---- Fleet cockpit (attention + KPIs + status + hosts) ---- */}
<AttentionStrip items={fleet.data?.attention} loading={fleet.isLoading} /> {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">
{/* ---- Fleet KPIs ---- */} <span className="flex items-center gap-2">
{fleet.data ? ( <AlertTriangle className="h-5 w-5 shrink-0" />
<FleetKpiRow kpis={fleet.data.kpis} /> Couldnt load fleet overview: {apiErrorMessage(fleet.error)}
) : ( </span>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6"> <button
{Array.from({ length: 6 }).map((_, i) => ( onClick={refreshFleet}
<div key={i} className="sp-skeleton h-24" /> 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> </div>
)} ) : (
<>
{/* Needs attention */}
<AttentionStrip items={fleet.data?.attention} loading={fleet.isLoading} />
{/* ---- Status + hosts ---- */} {/* Fleet KPIs */}
<div className="grid grid-cols-1 gap-4 lg:grid-cols-[340px_1fr]"> {fleet.data ? (
{fleet.data ? ( <FleetKpiRow kpis={fleet.data.kpis} />
<StackStatusBar totals={fleet.data.status_totals} unhealthy={fleet.data.kpis.unhealthy} /> ) : (
) : ( <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
<div className="sp-skeleton h-40" /> {Array.from({ length: 6 }).map((_, i) => (
)} <div key={i} className="sp-skeleton h-24" />
{fleet.data ? ( ))}
<HostResourceTable hosts={fleet.data.hosts} /> </div>
) : ( )}
<div className="sp-skeleton h-40" />
)} {/* Status + hosts */}
</div> <div className="grid grid-cols-1 gap-4 lg:grid-cols-[340px_1fr]">
{fleet.data ? (
<StackStatusBar totals={fleet.data.status_totals} unhealthy={fleet.data.kpis.unhealthy} />
) : (
<div className="sp-skeleton h-40" />
)}
{fleet.data ? (
<HostResourceTable hosts={fleet.data.hosts} />
) : (
<div className="sp-skeleton h-40" />
)}
</div>
</>
)}
{/* ---- Local host stacks ---- */} {/* ---- Local host stacks ---- */}
<section> <section>