diff --git a/app/app.css b/app/app.css
index b264497..91732a1 100644
--- a/app/app.css
+++ b/app/app.css
@@ -156,3 +156,21 @@
}
}
+
+/* Accessibility: honour a reduced-motion request. Skyrise leans on ambient
+ * drift (the aurora field), spring transitions, and looping keyframes; for
+ * anyone who asks for less motion, near-instant everything and freeze the
+ * decorative loops. This is the global guard the app previously lacked. */
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
+ }
+ [data-slot="aurora-field"] {
+ animation: none !important;
+ }
+}
diff --git a/app/components/copy-id.tsx b/app/components/copy-id.tsx
new file mode 100644
index 0000000..fde3c7d
--- /dev/null
+++ b/app/components/copy-id.tsx
@@ -0,0 +1,51 @@
+import { useState } from "react"
+import { Check, Copy } from "lucide-react"
+
+/**
+ * A monospace id (UUID, slug, token name) that copies to the clipboard on
+ * click, with a brief checkmark. Raw ids are common in an admin console and
+ * useless if you can't get them into a support ticket or a CLI.
+ */
+export function CopyId({
+ value,
+ label,
+ className = "",
+ dataAction,
+}: {
+ value: string
+ /** What's being copied, for the aria-label. Defaults to "id". */
+ label?: string
+ className?: string
+ dataAction?: string
+}) {
+ const [copied, setCopied] = useState(false)
+
+ const copy = async () => {
+ try {
+ await navigator.clipboard.writeText(value)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 1200)
+ } catch {
+ // Clipboard blocked (insecure context / permissions) — no-op; the value
+ // is still selectable by hand.
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/app/components/layout/app-shell.tsx b/app/components/layout/app-shell.tsx
index 228e0eb..09129cd 100644
--- a/app/components/layout/app-shell.tsx
+++ b/app/components/layout/app-shell.tsx
@@ -360,7 +360,7 @@ export function AppShell({
data-slot="sidebar"
data-expanded={expanded ? "true" : "false"}
className={[
- "sticky top-0 z-30 hidden h-svh shrink-0 flex-col border-r bg-sidebar transition-[width] duration-base ease-standard md:flex",
+ "sticky top-0 z-30 hidden h-svh shrink-0 flex-col border-r bg-sidebar transition-[width] duration-base ease-standard motion-reduce:transition-none md:flex",
expanded ? "w-60" : "w-16",
].join(" ")}
>
@@ -656,7 +656,10 @@ export function AppShell({
one floating card in a sea of black. The floating actions
pill is fixed to the viewport edge and lives outside this
column, so it stays clear regardless of cap width. */}
-
+ {/* pt-16 on mobile reserves a row for the floating hamburger (left)
+ and actions pill (right) so they never overlap the page H1; the
+ desktop rail/pill sit outside this column, so pt drops to p-6. */}
+
{children}
@@ -693,6 +696,9 @@ function NavRow({
to={item.to}
end={item.end}
title={expanded ? undefined : item.label}
+ // When collapsed there's no visible label text, only an icon — give
+ // screen readers the name explicitly (title alone isn't reliably read).
+ aria-label={expanded ? undefined : item.label}
onClick={onNavigate}
data-action={`${prefix}${item.label.toLowerCase()}`}
className={({ isActive }) =>
diff --git a/app/routes/home.tsx b/app/routes/home.tsx
index dce8b46..cde2153 100644
--- a/app/routes/home.tsx
+++ b/app/routes/home.tsx
@@ -11,7 +11,7 @@ import {
Users as UsersIcon,
} from "lucide-react"
-import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-core-client"
+import { useArcadiaClient } from "@crema/arcadia-core-client"
import { AlertBanner } from "@crema/feedback-ui"
import { AppShell } from "~/components/layout/app-shell"
@@ -37,6 +37,7 @@ import { listTenants, type Tenant } from "~/lib/arcadia/tenants"
import { listUsers, type User } from "~/lib/arcadia/users"
import { useRegisterContext } from "@crema/aifirst-ui/context"
import { pageTitle } from "~/lib/page-meta"
+import { errorMessage } from "~/lib/errors"
import { useSession } from "~/lib/session"
export const meta = () => pageTitle("Overview")
@@ -56,7 +57,7 @@ export default function HomeRoute() {
const [data, setData] = useState(EMPTY)
const [loading, setLoading] = useState(true)
- const [error, setError] = useState(null)
+ const [error, setError] = useState(null)
const [refreshedAt, setRefreshedAt] = useState(null)
const refresh = useCallback(async () => {
@@ -70,7 +71,7 @@ export default function HomeRoute() {
listAuditLogs(arcadia, { limit: 10 }),
getHealth(arcadia).catch(() => null),
]).catch((err) => {
- setError(err instanceof ArcadiaError ? err.message : "Failed to load overview.")
+ setError(err)
return [[], [], [], null] as [Tenant[], User[], AuditLog[], OverallHealth | null]
})
setData({ tenants, users, audit, health })
@@ -92,7 +93,7 @@ export default function HomeRoute() {
tenants: { total: data.tenants.length, active: activeTenants },
users: { total: data.users.length, active: activeUsers },
audit: { recent: data.audit.length, errors: errorEvents },
- health: data.health?.status ?? "unconfigured",
+ health: deriveOverallStatus(data.health),
}
}, [data])
@@ -129,7 +130,7 @@ export default function HomeRoute() {
{error ? (
setError(null)}>
- {error}
+ {errorMessage(error, "load the overview")}
) : null}
@@ -408,6 +409,25 @@ function statusLabel(status: HealthStatus | string): string {
return "Unknown"
}
+/**
+ * The overall health hero must never say "Unknown" while its own Subsystems
+ * card reports everything Healthy. When the backend gives no meaningful overall
+ * status (unknown/unconfigured/missing), derive it from the subsystem probes
+ * the same card shows: any error → Down, any degraded → Degraded, otherwise
+ * Healthy if at least one subsystem reported ok.
+ */
+function deriveOverallStatus(health: OverallHealth | null): HealthStatus | string {
+ if (!health) return "unconfigured"
+ if (health.status === "ok" || health.status === "degraded" || health.status === "error") {
+ return health.status
+ }
+ const statuses = Object.values(health.subsystems ?? {}).map((s) => s?.status)
+ if (statuses.some((s) => s === "error")) return "error"
+ if (statuses.some((s) => s === "degraded")) return "degraded"
+ if (statuses.some((s) => s === "ok")) return "ok"
+ return health.status ?? "unconfigured"
+}
+
function statusTone(status: HealthStatus | string): "default" | "ok" | "warning" | "error" {
if (status === "ok") return "ok"
if (status === "degraded") return "warning"
diff --git a/app/routes/monitoring.tsx b/app/routes/monitoring.tsx
index 6cf0557..1c70c25 100644
--- a/app/routes/monitoring.tsx
+++ b/app/routes/monitoring.tsx
@@ -334,7 +334,15 @@ export default function MonitoringRoute() {
/>
+ {busiestDiskMount(data.host.disks)}
+
+ }
icon={}
tone={
busiestDiskPct(data.host.disks) > 90
@@ -691,10 +699,18 @@ function busiestDiskPct(disks: HostStats["disks"]): number {
return disks.reduce((m, d) => Math.max(m, d.used_pct), 0)
}
-function busiestDiskLabel(disks: HostStats["disks"]): string {
- if (disks.length === 0) return "—"
- const busiest = disks.reduce((a, b) => (b.used_pct > a.used_pct ? b : a))
- return `${busiest.used_pct}% (${busiest.mount})`
+function busiestDisk(disks: HostStats["disks"]) {
+ if (disks.length === 0) return null
+ return disks.reduce((a, b) => (b.used_pct > a.used_pct ? b : a))
+}
+
+function busiestDiskValue(disks: HostStats["disks"]): string {
+ const d = busiestDisk(disks)
+ return d ? `${d.used_pct}%` : "—"
+}
+
+function busiestDiskMount(disks: HostStats["disks"]): string {
+ return busiestDisk(disks)?.mount ?? "no mounts"
}
function barColor(pct: number): string {
diff --git a/app/routes/profile.tsx b/app/routes/profile.tsx
index 0beca0d..82908ed 100644
--- a/app/routes/profile.tsx
+++ b/app/routes/profile.tsx
@@ -5,6 +5,7 @@ import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-core-client"
import { AlertBanner } from "@crema/feedback-ui"
import { AppShell } from "~/components/layout/app-shell"
+import { CopyId } from "~/components/copy-id"
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar"
import { Badge } from "~/components/ui/badge"
import { Button } from "~/components/ui/button"
@@ -357,9 +358,11 @@ export default function ProfileRoute() {
{account ? (
<>
-
- Tenant {account.tenant_id} ·
- ID {account.id}
+
+ Tenant
+
+ · ID
+
Last sign-in{" "}