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."""
APP_VERSION = "0.38.0"
APP_VERSION = "0.38.1"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "stackpilot-frontend",
"private": true,
"version": "0.38.0",
"version": "0.38.1",
"type": "module",
"scripts": {
"dev": "vite",
+44 -26
View File
@@ -1,6 +1,6 @@
import { useState } from "react";
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 { Card } from "@/components/ui";
import { HostHeader } from "@/components/hosts/HostHeader";
@@ -66,33 +66,51 @@ export function Dashboard() {
</button>
</div>
{/* ---- Needs attention ---- */}
<AttentionStrip items={fleet.data?.attention} loading={fleet.isLoading} />
{/* ---- Fleet KPIs ---- */}
{fleet.data ? (
<FleetKpiRow kpis={fleet.data.kpis} />
) : (
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="sp-skeleton h-24" />
))}
{/* ---- 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} />
{/* ---- Status + hosts ---- */}
<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>
{/* Fleet KPIs */}
{fleet.data ? (
<FleetKpiRow kpis={fleet.data.kpis} />
) : (
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="sp-skeleton h-24" />
))}
</div>
)}
{/* Status + hosts */}
<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 ---- */}
<section>