diff --git a/src/app/admin/[section]/page.tsx b/src/app/admin/[section]/page.tsx index 9a3454459..c831f19df 100644 --- a/src/app/admin/[section]/page.tsx +++ b/src/app/admin/[section]/page.tsx @@ -7,19 +7,8 @@ import { isAdminSectionSlug, } from "@/components/admin/section-slugs"; -/** - * Dynamic admin section route — mirrors the v1.4 settings split. Each of - * the `ADMIN_SECTION_SLUGS` is pre-rendered at build via - * `generateStaticParams()` so the URLs are statically known to Next.js, - * while `dynamicParams = false` tells the router to 404 (rather than - * attempting on-demand rendering) for any slug not in the list. - * - * The actual section components are mounted by the client-only - * `` because most of them use TanStack Query and - * the auth hook (server-rendered they'd just paint a loader). The shell, - * however, is server-rendered so the sidebar paints with no JS hydration - * lag. - */ +// Section bodies need TanStack Query / useAuth, so they're rendered by the +// client-only ``. The shell stays server-rendered. export const dynamicParams = false; @@ -28,16 +17,12 @@ export function generateStaticParams() { } interface PageProps { - // Next.js 16 made route `params` an async Promise. We `await` it before use. params: Promise<{ section: string }>; } export default async function AdminSectionPage({ params }: PageProps) { const { section } = await params; - // Defence-in-depth — `dynamicParams = false` already 404s unknown slugs at - // routing time, but we re-check here so a hand-rolled override of the - // route config can never silently fall through to a typo'd slug. if (!isAdminSectionSlug(section)) { notFound(); } diff --git a/src/app/admin/[section]/renderer.tsx b/src/app/admin/[section]/renderer.tsx index 203a03771..ce76feddb 100644 --- a/src/app/admin/[section]/renderer.tsx +++ b/src/app/admin/[section]/renderer.tsx @@ -106,7 +106,7 @@ export function AdminSectionRenderer({ title={t("admin.section.users.title")} subtitle={t("admin.section.users.subtitle")} > - + ); case "api-tokens": @@ -145,12 +145,9 @@ export function AdminSectionRenderer({ ); - default: { - // exhaustiveness guard — TS will flag a missing case here - const _exhaustive: never = slug; - void _exhaustive; + default: + slug satisfies never; return null; - } } } diff --git a/src/components/admin/__tests__/sections.test.tsx b/src/components/admin/__tests__/sections.test.tsx index 1c3bf7b29..e76ec5c35 100644 --- a/src/components/admin/__tests__/sections.test.tsx +++ b/src/components/admin/__tests__/sections.test.tsx @@ -106,7 +106,7 @@ describe("admin sections — SSR smoke", () => { }); it(" renders with filter pills", () => { - const html = render(); + const html = render(); expect(html).toContain("User Management"); // Filter pills painted from i18n expect(html).toContain("All"); diff --git a/src/components/admin/admin-shell.tsx b/src/components/admin/admin-shell.tsx index 4eedc23b0..ec2cbe593 100644 --- a/src/components/admin/admin-shell.tsx +++ b/src/components/admin/admin-shell.tsx @@ -33,17 +33,7 @@ import { import { cn } from "@/lib/utils"; import { useTranslations } from "@/lib/i18n/context"; -import { - ADMIN_SECTION_SLUGS, - isAdminSectionSlug, - type AdminSectionSlug, -} from "./section-slugs"; - -// Re-export so callers don't have to chase two modules. New server-side -// callers (e.g. `generateStaticParams()`) should still import from -// `./section-slugs` directly — values exported through this client -// boundary become unusable proxies on the server. -export { ADMIN_SECTION_SLUGS, isAdminSectionSlug, type AdminSectionSlug }; +import { isAdminSectionSlug, type AdminSectionSlug } from "./section-slugs"; interface AdminSection { slug: AdminSectionSlug; diff --git a/src/components/admin/api-token-overview-section.tsx b/src/components/admin/api-token-overview-section.tsx index 48f8931f3..0504f9460 100644 --- a/src/components/admin/api-token-overview-section.tsx +++ b/src/components/admin/api-token-overview-section.tsx @@ -9,7 +9,7 @@ import { formatDate, formatDateTime } from "@/lib/format"; import { useTranslations } from "@/lib/i18n/context"; import { type ApiTokenInfo } from "./_shared"; -export function ApiTokenOverviewSection({ id }: { id?: string } = {}) { +export function ApiTokenOverviewSection() { const { t } = useTranslations(); const [expanded, setExpanded] = useState(false); @@ -24,10 +24,7 @@ export function ApiTokenOverviewSection({ id }: { id?: string } = {}) { }); return ( -
+
diff --git a/src/components/admin/backups-section.tsx b/src/components/admin/backups-section.tsx index af37f3ea4..8f7568e65 100644 --- a/src/components/admin/backups-section.tsx +++ b/src/components/admin/backups-section.tsx @@ -29,7 +29,7 @@ function formatBytes(bytes: number, fmt: ReturnType) { return `${fmt.number(bytes / 1024 / 1024, 2)} MB`; } -export function BackupsSection({ id }: { id?: string } = {}) { +export function BackupsSection() { const { t } = useTranslations(); const fmt = useFormatters(); const queryClient = useQueryClient(); @@ -72,10 +72,7 @@ export function BackupsSection({ id }: { id?: string } = {}) { const rows: BackupRow[] = data?.rows ?? []; return ( -
+
diff --git a/src/components/admin/bug-report-section.tsx b/src/components/admin/bug-report-section.tsx index b1bbc0b32..871b26c85 100644 --- a/src/components/admin/bug-report-section.tsx +++ b/src/components/admin/bug-report-section.tsx @@ -10,7 +10,7 @@ import { Switch } from "@/components/ui/switch"; import { useTranslations } from "@/lib/i18n/context"; import { PasswordInput, useAdminSettings, useUpdateSettings } from "./_shared"; -export function BugReportSection({ id }: { id?: string } = {}) { +export function BugReportSection() { const { t } = useTranslations(); const { data: settings } = useAdminSettings(); const updateSettings = useUpdateSettings(); @@ -45,10 +45,7 @@ export function BugReportSection({ id }: { id?: string } = {}) { } return ( -
+
diff --git a/src/components/admin/danger-zone-section.tsx b/src/components/admin/danger-zone-section.tsx index ba5c6b33f..ed7b53a06 100644 --- a/src/components/admin/danger-zone-section.tsx +++ b/src/components/admin/danger-zone-section.tsx @@ -17,7 +17,7 @@ import { import { Button } from "@/components/ui/button"; import { useTranslations } from "@/lib/i18n/context"; -export function DangerZoneSection({ id }: { id?: string } = {}) { +export function DangerZoneSection() { const { t } = useTranslations(); const queryClient = useQueryClient(); const [wipeMsg, setWipeMsg] = useState(null); @@ -65,10 +65,7 @@ export function DangerZoneSection({ id }: { id?: string } = {}) { }); return ( -
+

diff --git a/src/components/admin/feedback-inbox-section.tsx b/src/components/admin/feedback-inbox-section.tsx index d1b86e116..b3bd89559 100644 --- a/src/components/admin/feedback-inbox-section.tsx +++ b/src/components/admin/feedback-inbox-section.tsx @@ -45,7 +45,7 @@ import { useSystemStatus, } from "./_shared"; -export function FeedbackInboxSection({ id }: { id?: string } = {}) { +export function FeedbackInboxSection() { const { t } = useTranslations(); const queryClient = useQueryClient(); const { data: status } = useSystemStatus(); @@ -72,10 +72,7 @@ export function FeedbackInboxSection({ id }: { id?: string } = {}) { } return ( -
+

{t("admin.feedback.title")}

diff --git a/src/components/admin/general-settings-section.tsx b/src/components/admin/general-settings-section.tsx index acfea9162..71fa24dac 100644 --- a/src/components/admin/general-settings-section.tsx +++ b/src/components/admin/general-settings-section.tsx @@ -4,16 +4,13 @@ import { Settings } from "lucide-react"; import { useTranslations } from "@/lib/i18n/context"; import { SettingsToggle, useAdminSettings, useUpdateSettings } from "./_shared"; -export function GeneralSettingsSection({ id }: { id?: string } = {}) { +export function GeneralSettingsSection() { const { t } = useTranslations(); const { data: settings } = useAdminSettings(); const updateSettings = useUpdateSettings(); return ( -
+

{t("admin.appSettings")}

diff --git a/src/components/admin/glitchtip-section.tsx b/src/components/admin/glitchtip-section.tsx index 818c017f7..5052ef90f 100644 --- a/src/components/admin/glitchtip-section.tsx +++ b/src/components/admin/glitchtip-section.tsx @@ -16,7 +16,7 @@ import { useUpdateSettings, } from "./_shared"; -export function GlitchtipSection({ id }: { id?: string } = {}) { +export function GlitchtipSection() { const { t } = useTranslations(); const { data: settings } = useAdminSettings(); const updateSettings = useUpdateSettings(); @@ -72,10 +72,7 @@ export function GlitchtipSection({ id }: { id?: string } = {}) { } return ( -
+
diff --git a/src/components/admin/login-overview-section.tsx b/src/components/admin/login-overview-section.tsx index c50d9cde1..f593d6791 100644 --- a/src/components/admin/login-overview-section.tsx +++ b/src/components/admin/login-overview-section.tsx @@ -14,7 +14,7 @@ import { formatDateTime } from "@/lib/format"; import { useTranslations } from "@/lib/i18n/context"; import { type AdminAuditEntry } from "./_shared"; -export function LoginOverviewSection({ id }: { id?: string } = {}) { +export function LoginOverviewSection() { const { t } = useTranslations(); const [expanded, setExpanded] = useState(false); const [filter, setFilter] = useState<"all" | "failed">("all"); @@ -49,10 +49,7 @@ export function LoginOverviewSection({ id }: { id?: string } = {}) { : data?.entries; return ( -
+
diff --git a/src/components/admin/reminders-section.tsx b/src/components/admin/reminders-section.tsx index 6a560a491..66fea8867 100644 --- a/src/components/admin/reminders-section.tsx +++ b/src/components/admin/reminders-section.tsx @@ -22,7 +22,7 @@ import { useUpdateSettings, } from "./_shared"; -export function RemindersSection({ id }: { id?: string } = {}) { +export function RemindersSection() { const { t } = useTranslations(); const { data: settings } = useAdminSettings(); const updateSettings = useUpdateSettings(); @@ -113,10 +113,7 @@ export function RemindersSection({ id }: { id?: string } = {}) { }); return ( -
+

diff --git a/src/components/admin/section-slugs.ts b/src/components/admin/section-slugs.ts index a4776b753..c1309cc05 100644 --- a/src/components/admin/section-slugs.ts +++ b/src/components/admin/section-slugs.ts @@ -28,25 +28,3 @@ export type AdminSectionSlug = (typeof ADMIN_SECTION_SLUGS)[number]; export function isAdminSectionSlug(value: string): value is AdminSectionSlug { return (ADMIN_SECTION_SLUGS as readonly string[]).includes(value); } - -/** - * Legacy hash-anchor → new slug mapping. Used by `` to - * rewrite hrefs and by `proxy.ts` to handle any server-side legacy URL - * forms (path-style anchors are redirectable; pure `#fragments` never - * reach the server, so those are fixed at the call-sites instead). - */ -export const LEGACY_ANCHOR_TO_SLUG: Record = { - "section-system-status": "system-status", - "section-admin-general": "general", - "section-admin-services": "services", - "section-admin-umami": "integrations", - "section-admin-glitchtip": "integrations", - "section-admin-webpush": "integrations", - "section-admin-bugreport": "integrations", - "section-admin-feedback": "feedback", - "section-admin-reminders": "reminders", - "section-user-management": "users", - "section-api-tokens": "api-tokens", - "section-login-overview": "login-overview", - "section-danger-zone": "danger-zone", -}; diff --git a/src/components/admin/services-section.tsx b/src/components/admin/services-section.tsx index 7980bb7a0..f84960c32 100644 --- a/src/components/admin/services-section.tsx +++ b/src/components/admin/services-section.tsx @@ -4,16 +4,13 @@ import { Bell, Globe, Key, MessageCircle } from "lucide-react"; import { useTranslations } from "@/lib/i18n/context"; import { SettingsToggle, useAdminSettings, useUpdateSettings } from "./_shared"; -export function ServicesSection({ id }: { id?: string } = {}) { +export function ServicesSection() { const { t } = useTranslations(); const { data: settings } = useAdminSettings(); const updateSettings = useUpdateSettings(); return ( -
+

{t("admin.servicesGlobal")}

diff --git a/src/components/admin/system-status-section.tsx b/src/components/admin/system-status-section.tsx index f54995da2..2d883465b 100644 --- a/src/components/admin/system-status-section.tsx +++ b/src/components/admin/system-status-section.tsx @@ -19,16 +19,13 @@ import { formatDateTime } from "@/lib/format"; import { useFormatters, useTranslations } from "@/lib/i18n/context"; import { StatusItem, useSystemStatus } from "./_shared"; -export function SystemStatusSection({ id }: { id?: string } = {}) { +export function SystemStatusSection() { const { t } = useTranslations(); const fmt = useFormatters(); const { data: status, isError } = useSystemStatus(); return ( -
+

{t("admin.systemStatus")}

diff --git a/src/components/admin/umami-section.tsx b/src/components/admin/umami-section.tsx index 28db85189..561fbd3ec 100644 --- a/src/components/admin/umami-section.tsx +++ b/src/components/admin/umami-section.tsx @@ -16,7 +16,7 @@ import { useUpdateSettings, } from "./_shared"; -export function UmamiSection({ id }: { id?: string } = {}) { +export function UmamiSection() { const { t } = useTranslations(); const { data: settings } = useAdminSettings(); const updateSettings = useUpdateSettings(); @@ -75,10 +75,7 @@ export function UmamiSection({ id }: { id?: string } = {}) { } return ( -
+
diff --git a/src/components/admin/user-management-section.tsx b/src/components/admin/user-management-section.tsx index 70acfa48a..fd319bb9e 100644 --- a/src/components/admin/user-management-section.tsx +++ b/src/components/admin/user-management-section.tsx @@ -19,6 +19,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; +import { useAuth } from "@/hooks/use-auth"; import { formatDate } from "@/lib/format"; import { useTranslations } from "@/lib/i18n/context"; import { type AdminUser, PasswordInput } from "./_shared"; @@ -32,14 +33,10 @@ import { type AdminUser, PasswordInput } from "./_shared"; */ type UserFilter = "all" | "admin" | "user"; -export function UserManagementSection({ - id, - currentUserId, -}: { - id?: string; - currentUserId: string; -}) { +export function UserManagementSection() { const { t } = useTranslations(); + const { user } = useAuth(); + const currentUserId = user?.id ?? ""; const queryClient = useQueryClient(); const [editingUser, setEditingUser] = useState(null); const [editUsername, setEditUsername] = useState(""); @@ -157,10 +154,7 @@ export function UserManagementSection({ } return ( -
+
diff --git a/src/components/admin/web-push-vapid-section.tsx b/src/components/admin/web-push-vapid-section.tsx index 7c1c79dae..f8c77a841 100644 --- a/src/components/admin/web-push-vapid-section.tsx +++ b/src/components/admin/web-push-vapid-section.tsx @@ -9,7 +9,7 @@ import { Label } from "@/components/ui/label"; import { useTranslations } from "@/lib/i18n/context"; import { PasswordInput, useAdminSettings, useUpdateSettings } from "./_shared"; -export function WebPushVapidSection({ id }: { id?: string } = {}) { +export function WebPushVapidSection() { const { t } = useTranslations(); const { data: settings } = useAdminSettings(); const updateSettings = useUpdateSettings(); @@ -48,10 +48,7 @@ export function WebPushVapidSection({ id }: { id?: string } = {}) { } return ( -
+
diff --git a/src/components/settings/ai-section.tsx b/src/components/settings/ai-section.tsx index e38ded9d0..4bec8fead 100644 --- a/src/components/settings/ai-section.tsx +++ b/src/components/settings/ai-section.tsx @@ -214,14 +214,7 @@ function InsightsSettingsCard({ // 15-minute device-code window expires. Polling stops as soon as // `deviceCode` is cleared (success / cancel / unmount). useEffect(() => { - if (!deviceCode) { - // No active device code → make sure the spinner is off. setState - // inside an effect is intentional here; the alternative (deriving - // `devicePolling` from `deviceCode`) would force every error - // path to also reset `deviceCode`, which we don't want — the - // start handler resets `devicePolling` itself when fetch fails. - return; - } + if (!deviceCode) return; let cancelled = false; const intervalMs = Math.max(deviceCode.intervalSeconds, 3) * 1000; diff --git a/src/lib/insights/bucket-series.ts b/src/lib/insights/bucket-series.ts index ced23921b..f9922e7e9 100644 --- a/src/lib/insights/bucket-series.ts +++ b/src/lib/insights/bucket-series.ts @@ -16,10 +16,8 @@ * does not produce 360 zero-rows of noise. */ -const BERLIN_TZ = "Europe/Berlin"; - const BERLIN_DAY_FORMATTER = new Intl.DateTimeFormat("en-US", { - timeZone: BERLIN_TZ, + timeZone: "Europe/Berlin", year: "numeric", month: "2-digit", day: "2-digit", diff --git a/src/lib/insights/mood-status.ts b/src/lib/insights/mood-status.ts index 1d5a37f8c..595c7e201 100644 --- a/src/lib/insights/mood-status.ts +++ b/src/lib/insights/mood-status.ts @@ -17,8 +17,6 @@ import { } from "@/lib/insights/bucket-series"; import { annotate } from "@/lib/logging/context"; -const MS_PER_DAY = 24 * 60 * 60 * 1000; - const BERLIN_DAY_FORMATTER = new Intl.DateTimeFormat("en-US", { timeZone: "Europe/Berlin", year: "numeric", @@ -285,7 +283,7 @@ export async function generateMoodStatusForUser( // ~90-day window so the model still gets a recency-weighted view of // tag patterns. The mood DB query already pulls 3 years (no where // filter); we slice by date here, not by record count. - const tagWindowCutoff = new Date(now.getTime() - 90 * MS_PER_DAY); + const tagWindowCutoff = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000); const recentEntries = entries.filter( (entry) => entry.moodLoggedAt.getTime() >= tagWindowCutoff.getTime(), );