mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 04:32:42 +00:00
refactor(v1.5): apply simplify-review safe-to-apply suggestions
Drops dead `id` props and `scroll-mt-28` classes from admin section components (load-bearing in v1.4 monolithic admin, dead since the v1.5 phase-4b dynamic-route split). Also removes the unused `LEGACY_ANCHOR_TO_SLUG` map, the `admin-shell` re-export shim, the `BERLIN_TZ` and duplicate `MS_PER_DAY` constants, the verbose `AdminSectionPage` docstring, the `useEffect` paraphrase comment in `ai-section`, and rewrites the `_exhaustive: never` exhaustiveness guard to the modern `slug satisfies never` form. `UserManagementSection` now reads `currentUserId` via `useAuth()` instead of accepting it as a prop. Findings 1, 2, 3, 4, 6, 7, 8, 11, 15, 16, 17 from .planning/phase-6-simplify-findings.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
79864bb8c2
commit
88c8db1abc
22 files changed
+41
-147
No files matched your search
@@ -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
|
||||
* `<AdminSectionRenderer>` 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 `<AdminSectionRenderer>`. 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();
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ export function AdminSectionRenderer({
|
||||
title={t("admin.section.users.title")}
|
||||
subtitle={t("admin.section.users.subtitle")}
|
||||
>
|
||||
<UserManagementSection currentUserId={user.id} />
|
||||
<UserManagementSection />
|
||||
</SectionFrame>
|
||||
);
|
||||
case "api-tokens":
|
||||
@@ -145,12 +145,9 @@ export function AdminSectionRenderer({
|
||||
<DangerZoneSection />
|
||||
</SectionFrame>
|
||||
);
|
||||
default: {
|
||||
// exhaustiveness guard — TS will flag a missing case here
|
||||
const _exhaustive: never = slug;
|
||||
void _exhaustive;
|
||||
default:
|
||||
slug satisfies never;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ describe("admin sections — SSR smoke", () => {
|
||||
});
|
||||
|
||||
it("<UserManagementSection> renders with filter pills", () => {
|
||||
const html = render(<UserManagementSection currentUserId="u1" />);
|
||||
const html = render(<UserManagementSection />);
|
||||
expect(html).toContain("User Management");
|
||||
// Filter pills painted from i18n
|
||||
expect(html).toContain("All");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
id={id}
|
||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
||||
>
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Key className="text-primary h-5 w-5" />
|
||||
|
||||
@@ -29,7 +29,7 @@ function formatBytes(bytes: number, fmt: ReturnType<typeof useFormatters>) {
|
||||
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 (
|
||||
<div
|
||||
id={id}
|
||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
||||
>
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="text-primary h-5 w-5" />
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
id={id}
|
||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
||||
>
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bug className="text-primary h-5 w-5" />
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
@@ -65,10 +65,7 @@ export function DangerZoneSection({ id }: { id?: string } = {}) {
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
className="bg-destructive/5 border-destructive/30 scroll-mt-28 rounded-xl border p-6"
|
||||
>
|
||||
<div className="bg-destructive/5 border-destructive/30 rounded-xl border p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="text-destructive h-5 w-5" />
|
||||
<h2 className="text-destructive text-lg font-semibold">
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
id={id}
|
||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
||||
>
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Inbox className="text-primary h-5 w-5" />
|
||||
<h2 className="text-lg font-semibold">{t("admin.feedback.title")}</h2>
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
id={id}
|
||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
||||
>
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="text-primary h-5 w-5" />
|
||||
<h2 className="text-lg font-semibold">{t("admin.appSettings")}</h2>
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
id={id}
|
||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
||||
>
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="text-primary h-5 w-5" />
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
id={id}
|
||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
||||
>
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<ScrollText className="text-primary h-5 w-5" />
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
id={id}
|
||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
||||
>
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="text-primary h-5 w-5" />
|
||||
<h2 className="text-lg font-semibold">
|
||||
|
||||
@@ -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 `<StatusCardGrid>` 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<string, AdminSectionSlug> = {
|
||||
"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",
|
||||
};
|
||||
@@ -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 (
|
||||
<div
|
||||
id={id}
|
||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
||||
>
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="text-primary h-5 w-5" />
|
||||
<h2 className="text-lg font-semibold">{t("admin.servicesGlobal")}</h2>
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
id={id}
|
||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
||||
>
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Server className="text-primary h-5 w-5" />
|
||||
<h2 className="text-lg font-semibold">{t("admin.systemStatus")}</h2>
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
id={id}
|
||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
||||
>
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="text-primary h-5 w-5" />
|
||||
|
||||
@@ -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<AdminUser | null>(null);
|
||||
const [editUsername, setEditUsername] = useState("");
|
||||
@@ -157,10 +154,7 @@ export function UserManagementSection({
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
||||
>
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="text-primary h-5 w-5" />
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
id={id}
|
||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6"
|
||||
>
|
||||
<div className="bg-card border-border rounded-xl border p-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<BellRing className="text-primary h-5 w-5" />
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user