#Loading, error & empty states
Every API-driven screen defines all of these states. No screen ships with only the happy path.
| State | Trigger | UI | Recovery |
|---|---|---|---|
| Loading | Request in flight | Skeletons shaped like the content (tables: 5 rows; cards: card outline). Spinner only inside buttons. Route loading.js for page transitions | — |
| Empty | 200 with no items | Icon + one-line explanation + primary CTA (e.g. "Connect your first server"). Filtered-empty is different: "No results for these filters" + Clear filters | CTA / clear filters |
| Error | 5xx / unexpected | Readable message (translated) + Retry. Show OSS/BE reference when present ("Reference: 3f9a…") | Retry (manual), auto-retry GET once on 502/503/504 |
| Success | Mutation OK | Sonner toast + the UI updated (refetch or optimistic update rolled back on failure) | — |
| Validation | 422 | Field errors under inputs from errors. Focus the first invalid field | Fix + resubmit |
| Unauthorized (403) | Permission or plan | 403 state: "You don't have access to this", why (role / plan / expired subscription when the API says), link to dashboard / upgrade | Ask an owner / upgrade |
| Not found (404) | Missing or other organization's resource | not-found.js: "This page or item doesn't exist" + back link | Back |
| Session expired (401) | Token expired / revoked | Try refresh once. If that fails, clear the session → /[locale]/login?next=<path> with "Your session expired" | Log in again |
| Network failure | No response / offline | Inline banner "You're offline / can't reach Central" + Retry. Keep the form data | Retry |
| Rate limited (429) | Too many requests | "Too many requests, try again in N s" from Retry-After. Disable the action until then | Wait |
| Server unreachable | OSS offline / timeout (proxied) | Section-level "This server isn't responding (last seen …)" + Retry + Disconnect. Other sections of Central keep working | Retry / fix the server |
| Server key invalid | OSS 401 (proxied) | "Central can no longer access this server" + "Reconnect with a new key" | Reconnect |
| Long-running job | 202 + run id | Progress with steps, safe to leave the page, notification when done | — |
#Rules
- One error boundary per route segment (
error.js) plus per-widget boundaries on the dashboard. - Errors never show stack traces or raw HTML. Unknown errors show a generic translated message + reference.
- Mutations disable their trigger while pending (no double submit).
- Destructive actions use a confirm dialog that states the effect, sometimes type-to-confirm (server name). The V8 delete protection setting (3.6) is checked by the backend before any delete is sent to OSS.
- Toasts are not the only feedback for errors that need action. Use inline messages.