mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
fix(ui): draw every failed read through a read-failure primitive
Around thirty surfaces drew a failed read their own way: a bare destructive paragraph with no retry, a muted line saying the data was still being learned, a skeleton that never ended, an error card nested in a tile, or nothing at all. None of them told an ended session from a refusal. They now use QueryErrorRow or QueryErrorCard with the query's error and a retry, inset where a card already frames them, and keep their card and title where the page would otherwise jump. Hydration and caffeine now fail the same way. The recovery page no longer points to Integrations while Oura's first resilience level is simply on its way. A guard reads every read hook's isError branch and refuses hand-rolled markup there, and refuses a framed error row inside a card.
This commit is contained in:
1 parent
a1b56f0b75
commit
923249dbf6
60 files changed
+579
-175
No files matched your search
@@ -104,6 +104,7 @@ export function AiQualitySection() {
|
||||
</div>
|
||||
) : query.isError ? (
|
||||
<QueryErrorRow
|
||||
inset
|
||||
error={query.error}
|
||||
message={t("admin.aiQuality.loadError")}
|
||||
onRetry={() => void query.refetch()}
|
||||
|
||||
@@ -1243,6 +1243,7 @@ export function BackupsSection() {
|
||||
</div>
|
||||
) : isError ? (
|
||||
<QueryErrorRow
|
||||
inset
|
||||
error={error}
|
||||
message={t("admin.section.backups.loadError")}
|
||||
onRetry={() => void refetch()}
|
||||
|
||||
@@ -99,6 +99,7 @@ export function CoachFeedbackSection() {
|
||||
</div>
|
||||
) : query.isError ? (
|
||||
<QueryErrorRow
|
||||
inset
|
||||
error={query.error}
|
||||
message={t("admin.coachFeedback.loadError")}
|
||||
onRetry={() => void query.refetch()}
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
} from "@/components/settings/security-section/use-recent-proof";
|
||||
import { KeyBackupCard } from "./key-backup-card";
|
||||
import { useKeyBackupStatus } from "./use-key-backup-status";
|
||||
import { QueryErrorRow } from "@/components/ui/query-error-row";
|
||||
|
||||
interface ColumnScan {
|
||||
model: string;
|
||||
@@ -182,9 +183,15 @@ export function EncryptionSection() {
|
||||
{t("admin.section.encryption.coverageDetail")}
|
||||
</p>
|
||||
|
||||
<p role="alert" className="text-destructive text-sm">
|
||||
{t("admin.section.encryption.loadError")}
|
||||
</p>
|
||||
{statusQuery.isError ? (
|
||||
<QueryErrorRow
|
||||
inset
|
||||
error={statusQuery.error}
|
||||
slot="encryption-status-error"
|
||||
message={t("admin.section.encryption.loadError")}
|
||||
onRetry={() => void statusQuery.refetch()}
|
||||
/>
|
||||
) : null}
|
||||
</SettingsCard>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -198,6 +198,7 @@ export function HostMetricsChart() {
|
||||
}
|
||||
/>
|
||||
<QueryErrorRow
|
||||
inset
|
||||
error={error}
|
||||
message={t("admin.hostMetrics.loadError")}
|
||||
onRetry={() => void refetch()}
|
||||
|
||||
@@ -75,6 +75,7 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiDelete, apiGet, apiPost } from "@/lib/api/api-fetch";
|
||||
import { QueryErrorRow } from "@/components/ui/query-error-row";
|
||||
|
||||
interface InviteRedemptionEntry {
|
||||
id: string;
|
||||
@@ -192,7 +193,12 @@ export function InviteTokensSection() {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [revokeTarget, setRevokeTarget] = useState<AdminInvite | null>(null);
|
||||
|
||||
const { data: invites, isError } = useQuery({
|
||||
const {
|
||||
data: invites,
|
||||
isError,
|
||||
error,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: queryKeys.adminInvites(),
|
||||
queryFn: async () => {
|
||||
return apiGet<AdminInvite[]>("/api/admin/invites");
|
||||
@@ -313,9 +319,13 @@ export function InviteTokensSection() {
|
||||
/>
|
||||
|
||||
{isError && (
|
||||
<p className="text-destructive text-sm">
|
||||
{t("admin.invites.loadError")}
|
||||
</p>
|
||||
<QueryErrorRow
|
||||
inset
|
||||
error={error}
|
||||
slot="admin-invites-error"
|
||||
message={t("admin.invites.loadError")}
|
||||
onRetry={() => void refetch()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{invites && invites.length === 0 && (
|
||||
|
||||
@@ -155,6 +155,7 @@ export function KeyBackupCard() {
|
||||
/>
|
||||
{status.isError ? (
|
||||
<QueryErrorRow
|
||||
inset
|
||||
error={status.error}
|
||||
message={t("admin.keyBackup.loadError")}
|
||||
onRetry={() => void status.refetch()}
|
||||
|
||||
@@ -170,6 +170,7 @@ export function OffhostBackupsSection() {
|
||||
</div>
|
||||
) : isError ? (
|
||||
<QueryErrorRow
|
||||
inset
|
||||
error={error}
|
||||
message={t("admin.section.backups.offhost.loadError")}
|
||||
onRetry={() => void refetch()}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { QueryErrorRow } from "@/components/ui/query-error-row";
|
||||
|
||||
/**
|
||||
* v1.37.31 — the delivery-health readout the `provider_health` ledger never
|
||||
@@ -66,7 +67,7 @@ export function ProviderHealthSection() {
|
||||
const groupDigits = (value: number): string =>
|
||||
new Intl.NumberFormat(locale).format(value);
|
||||
|
||||
const { data, isPending, isError } = useQuery({
|
||||
const { data, isPending, isError, error, refetch } = useQuery({
|
||||
queryKey: queryKeys.adminProviderHealth(),
|
||||
queryFn: () =>
|
||||
apiGet<{
|
||||
@@ -134,9 +135,13 @@ export function ProviderHealthSection() {
|
||||
{t("admin.providerHealth.loading")}
|
||||
</p>
|
||||
) : isError ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("admin.providerHealth.loadError")}
|
||||
</p>
|
||||
<QueryErrorRow
|
||||
inset
|
||||
error={error}
|
||||
slot="provider-health-error"
|
||||
message={t("admin.providerHealth.loadError")}
|
||||
onRetry={() => void refetch()}
|
||||
/>
|
||||
) : providers.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("admin.providerHealth.empty")}
|
||||
|
||||
@@ -84,6 +84,7 @@ export function RecentAuditPreview() {
|
||||
</div>
|
||||
) : isError ? (
|
||||
<QueryErrorRow
|
||||
inset
|
||||
error={error}
|
||||
message={t("admin.overview.auditLoadError")}
|
||||
onRetry={() => void refetch()}
|
||||
|
||||
@@ -277,6 +277,7 @@ export function SystemStatusSection() {
|
||||
// transient 500 (rolling deploy, DB blip) doesn't require a
|
||||
// full page reload to recover.
|
||||
<QueryErrorRow
|
||||
inset
|
||||
error={error}
|
||||
message={t("admin.systemStatusLoadError")}
|
||||
onRetry={() => void refetch()}
|
||||
|
||||
@@ -110,6 +110,7 @@ export function SystemStatusSummary() {
|
||||
</div>
|
||||
) : isError ? (
|
||||
<QueryErrorRow
|
||||
inset
|
||||
error={error}
|
||||
message={t("admin.overview.snapshotLoadError")}
|
||||
onRetry={() => void refetch()}
|
||||
|
||||
Reference in new issue
Block a user