Files
HealthLog/src/app/settings/[section]/page.tsx
T
Marc-André Bombeck c86c7d1a79 refactor(lint): enforce the lib → components and client → server boundaries
Add the in-repo rule `healthlog/import-boundaries`:

- a file under src/lib may not import src/components or src/app (alias,
  relative, re-export and dynamic forms, type-only included);
- a "use client" file may not make a value import of the database client,
  the crypto module, the Prisma client entry, the job queue, pg, pg-boss or
  next/headers. Type-only imports and the Prisma enums entry stay legal.

The four lib files that still import upward are listed in the rule's allow
map with a reason each; a listed file that stops importing upward is
reported, so the map can only shrink.

Five of the nine upward imports are gone by moving pure modules to their
layer, without behaviour change:

- supply-runway estimate → src/lib/medications/supply-runway.ts
  (read by the low-stock job and the medication list read);
- weekday BYDAY tokens → src/lib/medications/scheduling/weekday-tokens.ts
  (read by the medication extraction prompt);
- settings section slugs → src/lib/settings/section-slugs.ts
  (read by the record-settings classification);
- the command palette index → src/components/command-palette/ (it is a
  client index built from component registries).

Proof the rule can fail: an import of @/components/ui/card appended to
src/lib/medications/supply-runway.ts reported libUpward, and a planted
"use client" file importing @/lib/db reported clientServerOnly; both were
removed again.
2026-10-22 04:15:11 +02:00

80 lines
3.0 KiB
TypeScript

import { notFound } from "next/navigation";
import {
SETTINGS_SECTION_SLUGS,
isSettingsSectionSlug,
} from "@/lib/settings/section-slugs";
import { SettingsShell } from "@/components/settings/settings-shell";
import {
SettingsSectionLoader,
SettingsSectionPreload,
} from "@/components/settings/settings-section-loader";
import { RecordSettingsSectionGate } from "@/components/settings/record-settings-section-gate";
import { getIntegrationCallbackUrls } from "@/lib/integrations/callback-urls";
/**
* Dynamic settings section route. Each of the `SETTINGS_SECTION_SLUGS`
* is pre-rendered at build via
* `generateStaticParams()` so the URLs are
* statically known to Next.js, while the `dynamicParams = false` flag below
* tells the router to 404 (instead of attempting on-demand rendering) for any
* slug not in the list — which is exactly what `notFound()` would do at
* request time, just earlier and without rendering.
*/
export const dynamicParams = false;
export function generateStaticParams() {
return SETTINGS_SECTION_SLUGS.map((section) => ({ section }));
}
interface PageProps {
// Next.js 16 made route `params` an async Promise. We `await` it before use.
params: Promise<{ section: string }>;
}
export default async function SettingsSectionPage({ 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 (!isSettingsSectionSlug(section)) {
notFound();
}
// Each section is its own chunk (see `SettingsSectionLoader`), so the route
// ships the one it shows rather than all of them. Integrations is the
// one section that needs a server-resolved value: the OAuth callback URL
// each provider registers, read from the runtime env here, per request,
// because a `NEXT_PUBLIC_*` read inside a client module is inlined at build
// time and the published image is built without it.
//
// v1.18.6.1 — the heading + subtitle (and the Layout-hub "← back" link)
// live in `<SettingsShell>`, which places them in their own grid row
// spanning only the content column so the left nav's first item lines up
// with the top of the first card. The page body is pure card content,
// wrapped in the labelled `<section>` so the historic
// `settings-section-<slug>-title` `aria-labelledby` linkage still resolves.
return (
<SettingsShell active={section}>
<SettingsSectionPreload section={section} />
<RecordSettingsSectionGate section={section}>
<section
aria-labelledby={`settings-section-${section}-title`}
className="space-y-6"
>
<SettingsSectionLoader
section={section}
callbackUrls={
section === "integrations"
? getIntegrationCallbackUrls()
: undefined
}
/>
</section>
</RecordSettingsSectionGate>
</SettingsShell>
);
}