fix(auth): unblock the about page for unauthenticated visitors

The /about page (GeoLite2 attribution) was listed in proxy.ts
PUBLIC_PATHS but missed in AuthShell's client-side PUBLIC_PATHS, so
unauthenticated visitors bounced through /auth/login. Add /about to
the AuthShell list and treat it as a standalone public page (same
edge-to-edge render as /privacy).

Apply safe-area-inset-top on the privacy and about sticky headers so
iOS notch / Dynamic Island viewports don't overlap the brand row. Add
a default-closed Contents <details> TOC at the top of the privacy
policy and wrap every HealthKit identifier <code> with break-all so
long camelCase strings (heartRateVariabilitySDNN,
environmentalAudioExposure) wrap inside narrow viewports.
This commit is contained in:
Marc-André Bombeck committed 2026-05-15 18:56:49 +02:00
1 parent c77d5252a6
commit fc8a58557a
3 files changed
+133 -23

No files matched your search

+1 -1
View File
@@ -56,7 +56,7 @@ function Section({
export default function AboutPage() {
return (
<div className="bg-background text-foreground min-h-dvh">
<header className="border-border/60 bg-background/80 sticky top-0 z-10 border-b backdrop-blur">
<header className="border-border/60 bg-background/80 sticky top-0 z-10 border-b backdrop-blur pt-[env(safe-area-inset-top)]">
<div className="mx-auto flex max-w-3xl items-center justify-between px-4 py-3 md:px-6">
<Link
href="/"
+116 -20
View File
@@ -107,7 +107,7 @@ export default function PrivacyPage() {
{/* Top bar — visible home-link so an App-Store reviewer can confirm
the policy belongs to the same brand as the iOS app. Public; no
nav menu (this page is reachable without a session). */}
<header className="border-border/60 bg-background/80 sticky top-0 z-10 border-b backdrop-blur">
<header className="border-border/60 bg-background/80 sticky top-0 z-10 border-b backdrop-blur pt-[env(safe-area-inset-top)]">
<div className="mx-auto flex max-w-3xl items-center justify-between px-4 py-3 md:px-6">
<Link
href="/"
@@ -143,6 +143,102 @@ export default function PrivacyPage() {
</p>
</div>
{/* v1.4.27 MB6 — collapsible TOC. Default-closed on every
viewport so the policy's first paragraphs reach the fold
on `<sm`; an open summary on a wide screen costs nothing,
and the reader can flip it open to skim the section list. */}
<details
className="border-border/60 bg-card/40 group rounded-md border"
data-slot="privacy-toc"
>
<summary className="text-foreground hover:bg-muted/50 cursor-pointer list-none rounded-md px-4 py-3 text-sm font-medium select-none">
Contents
<span
aria-hidden="true"
className="text-muted-foreground ml-2 inline-block transition-transform group-open:rotate-90"
>
›
</span>
</summary>
<nav
aria-label="Privacy policy contents"
className="border-border/60 border-t px-4 py-3"
>
<ol className="text-muted-foreground space-y-1.5 text-sm leading-relaxed">
<li>
<a className="hover:text-foreground hover:underline" href="#intro">
1. Overview
</a>
</li>
<li>
<a
className="hover:text-foreground hover:underline"
href="#data-we-collect"
>
2. Data we collect
</a>
</li>
<li>
<a className="hover:text-foreground hover:underline" href="#purpose">
3. Why we collect each category
</a>
</li>
<li>
<a
className="hover:text-foreground hover:underline"
href="#sub-processors"
>
4. Third-party sub-processors
</a>
</li>
<li>
<a className="hover:text-foreground hover:underline" href="#storage">
5. Data storage and retention
</a>
</li>
<li>
<a className="hover:text-foreground hover:underline" href="#rights">
6. Your rights (GDPR Art. 15-22, DSGVO)
</a>
</li>
<li>
<a
className="hover:text-foreground hover:underline"
href="#medical-boundary"
>
7. Medical-device boundary (EU MDR 2017/745, MDCG 2021-24)
</a>
</li>
<li>
<a
className="hover:text-foreground hover:underline"
href="#apple-categories"
>
8. Apple App Store privacy categories
</a>
</li>
<li>
<a
className="hover:text-foreground hover:underline"
href="#children"
>
9. Children
</a>
</li>
<li>
<a className="hover:text-foreground hover:underline" href="#changes">
10. Changes to this policy
</a>
</li>
<li>
<a className="hover:text-foreground hover:underline" href="#contact">
11. Contact
</a>
</li>
</ol>
</nav>
</details>
<Section id="intro" title="1. Overview">
<p>
HealthLog is an open-source, self-hostable personal-health-tracking
@@ -288,64 +384,64 @@ export default function PrivacyPage() {
data-slot="privacy-hk-quantity"
>
<li>
<code className="text-xs">bodyMass</code>
<code className="text-xs break-all">bodyMass</code>
</li>
<li>
<code className="text-xs">bodyFatPercentage</code>
<code className="text-xs break-all">bodyFatPercentage</code>
</li>
<li>
<code className="text-xs">bodyTemperature</code>
<code className="text-xs break-all">bodyTemperature</code>
</li>
<li>
<code className="text-xs">bloodPressureSystolic</code>
<code className="text-xs break-all">bloodPressureSystolic</code>
</li>
<li>
<code className="text-xs">bloodPressureDiastolic</code>
<code className="text-xs break-all">bloodPressureDiastolic</code>
</li>
<li>
<code className="text-xs">bloodGlucose</code>
<code className="text-xs break-all">bloodGlucose</code>
</li>
<li>
<code className="text-xs">oxygenSaturation</code>
<code className="text-xs break-all">oxygenSaturation</code>
</li>
<li>
<code className="text-xs">heartRate</code>
<code className="text-xs break-all">heartRate</code>
</li>
<li>
<code className="text-xs">restingHeartRate</code>
<code className="text-xs break-all">restingHeartRate</code>
</li>
<li>
<code className="text-xs">heartRateVariabilitySDNN</code>
<code className="text-xs break-all">heartRateVariabilitySDNN</code>
</li>
<li>
<code className="text-xs">vo2Max</code>
<code className="text-xs break-all">vo2Max</code>
</li>
<li>
<code className="text-xs">stepCount</code>
<code className="text-xs break-all">stepCount</code>
</li>
<li>
<code className="text-xs">activeEnergyBurned</code>
<code className="text-xs break-all">activeEnergyBurned</code>
</li>
<li>
<code className="text-xs">flightsClimbed</code>
<code className="text-xs break-all">flightsClimbed</code>
</li>
<li>
<code className="text-xs">distanceWalkingRunning</code>
<code className="text-xs break-all">distanceWalkingRunning</code>
</li>
<li>
<code className="text-xs">environmentalAudioExposure</code>
<code className="text-xs break-all">environmentalAudioExposure</code>
</li>
<li>
<code className="text-xs">headphoneAudioExposure</code>
<code className="text-xs break-all">headphoneAudioExposure</code>
</li>
<li>
<code className="text-xs">timeInDaylight</code> (iOS 17 and later)
<code className="text-xs break-all">timeInDaylight</code> (iOS 17 and later)
</li>
</ul>
<p className="font-medium">HKCategoryTypeIdentifier:</p>
<ul className="list-disc space-y-1 pl-5">
<li>
<code className="text-xs">sleepAnalysis</code> — read with full
<code className="text-xs break-all">sleepAnalysis</code> — read with full
per-stage granularity (Awake, REM, Core, Deep). Stages are stored
server-side as separate rows so the chart can stack them.
</li>
+16 -2
View File
@@ -11,7 +11,18 @@ import { BottomNav } from "./bottom-nav";
import { SidebarNav } from "./sidebar-nav";
import { TopBar } from "./top-bar";
const PUBLIC_PATHS = ["/auth/login", "/auth/register", "/privacy"];
// v1.4.27 MB6 — `/about` joins the public-path list so the GeoLite2
// CC BY-SA 4.0 attribution stays reachable for unauthenticated
// visitors. The `/about` route is already registered in `proxy.ts`
// PUBLIC_PATHS; this constant gates the client-side auth shell, and
// both lists must agree or the route round-trips through the
// sign-in redirect.
const PUBLIC_PATHS = [
"/auth/login",
"/auth/register",
"/privacy",
"/about",
];
export function AuthShell({ children }: { children: React.ReactNode }) {
const { user, isAuthenticated, isLoading } = useAuth();
@@ -25,7 +36,10 @@ export function AuthShell({ children }: { children: React.ReactNode }) {
// squash a 3000-word policy into a "login card" layout, so we let it
// render edge-to-edge while still resolving as a public page for the
// auth gate above.
const isStandalonePublicPage = pathname.startsWith("/privacy");
// v1.4.27 MB6 — `/about` follows the same shape (own header, own
// footer, full-width body), so it joins the standalone list.
const isStandalonePublicPage =
pathname.startsWith("/privacy") || pathname.startsWith("/about");
const isAdminPage = pathname.startsWith("/admin");
const isOnboardingPage = pathname === "/onboarding";
const showUnlockNotifier = isAuthenticated && !isPublicPage && !!user?.id;