Files
HealthLog/src/components/admin/glitchtip-section.tsx
T
Marc-André Bombeck b2e1aa0cde refactor(settings): one header, one action row, one integration card
The sweep that follows from the primitives. Nothing here is new behaviour;
every change removes a second way of saying something the surface already
said one way somewhere else.

Headers. The two collapsible security cards stop rebuilding
`SettingsCardHeader` by hand to carry a chevron — the chevron rides inside
the title button, and the description moves out of the collapsed region, so
the first paint is no longer a bare title. The import card shell, the export
and import section headings, and the four free-standing group headers stop
inventing their own type scale: a heading above a GROUP of cards is
`SectionHeading` (`text-base`), a card title is `SettingsCardHeader`
(`text-lg`), and the step between them is what says which is which. The
targets card gets a title instead of a naked icon.

Actions. Every card-level row is `SettingsCardActions`. The primary action
comes out of the header's status slot in six cards (password, web push,
pillar order, coach memory, and the two danger-zone triggers) and out of the
middle of the card in three more; the reset that hung in the dashboard-layout
header joins save as a secondary in the same row. Result and status lines
move ABOVE the row — sources' cross-link, the invite form's two status lines
— because nothing follows a card's actions. Revoking a share link is
`variant="destructive"` rather than an `outline` hand-tinted red in two
different ways. The import row had four variants for four buttons; it has
two. Export downloads are the card's primary, not an outline link.

Integrations. One description sentence with the setup-guide link inline: the
second paragraph was byte-identical across six of eight cards, which is the
tell that it said nothing about the provider it sat under. The `<hr>` under
the header — which existed only in this folder — is gone. So is the hint that
pointed at a guide 40 px above it, and the second format for "not
configured". Connect is one button everywhere: primary, `sm`, in the action
row, instead of primary-and-small in three cards and outline-and-large in
four.

Plus the standards leftovers the audit listed: two `pl-5` list gutters, one
`scroll-mt-20` anchor, a purple header icon, `text-[10px]` badges, four
hand-rolled callout shells that `SettingsInfoTile` already paints, and the
two inline buttons that never had the mobile tap floor at all.
2026-08-07 19:39:11 +02:00

160 lines
5.1 KiB
TypeScript

"use client";
import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { AlertTriangle, Loader2 } from "lucide-react";
import { toast } from "sonner";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardActions } from "@/components/settings/_card-actions";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useTranslations } from "@/lib/i18n/context";
import {
SettingsToggle,
useAdminSettings,
useUpdateSettings,
ConfiguredBadge,
} from "./_shared";
import { apiPost } from "@/lib/api/api-fetch";
export function GlitchtipSection() {
const { t } = useTranslations();
const { data: settings } = useAdminSettings();
const updateSettings = useUpdateSettings();
const [glitchtipDsnDraft, setGlitchtipDsnDraft] = useState<string | null>(
null,
);
const [glitchtipEnvironmentDraft, setGlitchtipEnvironmentDraft] = useState<
string | null
>(null);
const glitchtipDsnValue = glitchtipDsnDraft ?? settings?.glitchtipDsn ?? "";
const glitchtipEnvironmentValue =
glitchtipEnvironmentDraft ?? settings?.glitchtipEnvironment ?? "production";
const configured = Boolean(settings?.glitchtipDsn);
const testGlitchtip = useMutation({
mutationFn: async () => {
const data = await apiPost<{ message?: string } | undefined>(
"/api/admin/monitoring/glitchtip-test",
);
return data?.message ?? t("admin.monitoringTestSuccess");
},
onSuccess: (message) => {
toast.success(message);
},
onError: (error) => {
toast.error(
error instanceof Error
? error.message
: t("admin.monitoringTestFailed"),
);
},
});
function saveGlitchtipSettings() {
updateSettings.mutate(
{
glitchtipDsn: glitchtipDsnValue,
glitchtipEnvironment: glitchtipEnvironmentValue,
},
{
onSuccess: () => {
setGlitchtipDsnDraft(null);
setGlitchtipEnvironmentDraft(null);
},
},
);
}
return (
<SettingsCard>
<SettingsCardHeader
icon={AlertTriangle}
title={t("admin.glitchtipTitle")}
description={t("admin.glitchtipDescription")}
status={configured ? <ConfiguredBadge /> : null}
/>
<div className="space-y-3">
<SettingsToggle
label={t("admin.glitchtipEnabled")}
icon={AlertTriangle}
checked={settings?.glitchtipEnabled ?? false}
onCheckedChange={(checked) =>
updateSettings.mutate({ glitchtipEnabled: checked })
}
disabled={updateSettings.isPending}
/>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5 sm:col-span-2">
<Label htmlFor="admin-glitchtip-dsn" className="text-xs">
{t("admin.glitchtipDsn")}
</Label>
<Input
id="admin-glitchtip-dsn"
name="admin-glitchtip-dsn"
value={glitchtipDsnValue}
onChange={(event) => setGlitchtipDsnDraft(event.target.value)}
placeholder={t("admin.glitchtipDsnPlaceholder")}
autoComplete="new-password"
spellCheck={false}
data-lpignore="true"
data-1p-ignore="true"
data-bwignore="true"
disabled={updateSettings.isPending}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="admin-glitchtip-environment" className="text-xs">
{t("admin.glitchtipEnvironment")}
</Label>
<Input
id="admin-glitchtip-environment"
name="admin-glitchtip-environment"
value={glitchtipEnvironmentValue}
onChange={(event) =>
setGlitchtipEnvironmentDraft(event.target.value)
}
placeholder={t("admin.glitchtipEnvironmentPlaceholder")}
autoComplete="new-password"
spellCheck={false}
data-lpignore="true"
data-1p-ignore="true"
data-bwignore="true"
disabled={updateSettings.isPending}
/>
</div>
</div>
</div>
<SettingsCardActions>
<Button
size="sm"
variant="outline"
onClick={() => testGlitchtip.mutate()}
disabled={testGlitchtip.isPending || updateSettings.isPending}
>
{testGlitchtip.isPending && (
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
)}
{t("common.test")}
</Button>
<Button
size="sm"
onClick={saveGlitchtipSettings}
disabled={updateSettings.isPending}
>
{updateSettings.isPending && (
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
)}
{t("common.save")}
</Button>
</SettingsCardActions>
</SettingsCard>
);
}