mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
Consolidated hotfix landing the findings from a six-axis audit
covering mobile security, UX + accessibility, code quality + docs,
README + discoverability, web performance, and mobile-deep responsive
behaviour. None of the changes break the public API or the existing
data shape; every fix is additive and SAFE under the v1.4.34.x web
freeze. Full audit reports live in `.planning/round-audit-*.md`.
Security
- Password rotation now wipes API tokens + refresh tokens in the
same `$transaction` as session revocation. A stolen iOS Bearer no
longer survives a user-initiated password change or an admin reset.
- `/.well-known/` proxy bypass tightened to an explicit-allowlist
`Set`. Stale `/api/auth/codex/callback` PUBLIC_PATHS entry dropped.
- HSTS gains `preload`. Withings host removed from the global CSP
and gated to the integration's own routes.
- `getClientIp` warns once per process on `TRUST_PROXY_HOPS`/XFF
mismatch + exposes a tagged helper for tighter universal buckets.
- Service worker rejects off-origin notification-click URLs. Offline
fallback body switched to a locale-neutral icon. Manifest gains
`scope`, `id`, `display_override`.
UX + accessibility
- i18n leaks closed on Dialog/Sheet close-X, ChartSkeleton's loading
announcement, and the notifications-section breadcrumb.
- Focus rings on bottom-nav, the More overflow button, and the
mobile top-bar user menu.
- Tap-target floor: Input / NativeSelect / Select / DateTimeInput
switch to `h-11 sm:h-10`.
- Admin chip-strip auto-scroll-into-view (matches v1.4.33 settings).
- List-page subtitles visible on mobile.
- Insights empty-state CTA vocabulary normalised so the link opens
the right measurement form (was silently failing for
BODY_TEMPERATURE / BLOOD_GLUCOSE / PULSE).
Mobile-deep
- `viewport.themeColor` reacts to light/dark mode.
- TopBar + Coach FAB reserve safe-area-inset-top / -bottom for
notched iPhones.
- Sonner toaster switches to `theme="system"`.
Code quality
- `SESSION_SECRET` dropped — the env var was documented as required
but never read by code. Three secrets, not four. Self-hosters
upgrading can delete the existing line.
- `toJson<T>()` helper consolidates seven Prisma JSON-cast sites.
- `db-compat.ts` gains a header docstring naming it the
schema-bootstrap path.
- Passkey response types adopted from `@simplewebauthn/server`;
`any` + eslint-disable pragmas drop out.
- Zero-TODO CI gate added at `.github/workflows/no-todo-markers.yml`.
Documentation + discoverability
- README rework: How-it-compares matrix, Apple Health + AI Coach
promoted to standalone Key-Features bullets, status block, latest
release + GHCR badges, OpenAPI pointer, three-secret Quick Start.
- openGraph + twitter cards now carry `images` and a
`summary_large_image` Twitter card.
- GitHub repo metadata: description tightened from 290 to 156
chars + keyword-front, `homepageUrl` flipped from a personal
tenant to the demo site, six generic / PII-adjacent topics
swapped for six high-intent ones.
- Five new user-facing docs: `docs/self-hosting/getting-started.md`,
`docs/self-hosting/reverse-proxy.md`,
`docs/integrations/apple-health.md`,
`docs/integrations/withings.md`,
`docs/integrations/ai-providers.md`.
- `docs/README.md` clarifies the docs tree as internal playbooks
vs the user-facing `docs.healthlog.dev` site.
83 lines
3.2 KiB
TypeScript
83 lines
3.2 KiB
TypeScript
import * as React from "react";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
/**
|
|
* Base input primitive.
|
|
*
|
|
* v1.4 hardening: defaults `autoComplete` to `"off"` and tells
|
|
* LastPass / 1Password to skip the field when the caller does not
|
|
* opt in to a semantic autocomplete value. HealthLog is a health-data
|
|
* app — letting browsers / password managers autofill measurement,
|
|
* medication, mood, and AI-token inputs with the user's last-saved
|
|
* email or account password is at best confusing and at worst leaks
|
|
* credentials into a free-text field that gets persisted server-side.
|
|
*
|
|
* Auth and profile forms (login, register, change-email) keep working
|
|
* because they already pass an explicit `autoComplete` value —
|
|
* `"username"`, `"email"`, `"current-password"`, `"new-password"`. When
|
|
* `autoComplete` is anything other than `"off"`, the LastPass /
|
|
* 1Password ignore attrs are dropped so password managers fill the
|
|
* field normally.
|
|
*
|
|
* v1.4.27 mobile pass: derive a sensible `inputMode` default from the
|
|
* `type` prop so every numeric / email / tel / url / search field
|
|
* surfaces the right on-screen keyboard on iOS Safari and Android
|
|
* Chrome without each call site having to repeat the attribute. Most
|
|
* `type="number"` fields in HealthLog accept decimals (kg, mmol/L,
|
|
* sleep hours), so the default is `"decimal"`; integer-only call
|
|
* sites can still pass `inputMode="numeric"` explicitly.
|
|
*/
|
|
function deriveInputMode(
|
|
type: React.HTMLInputTypeAttribute | undefined,
|
|
): React.HTMLAttributes<HTMLInputElement>["inputMode"] | undefined {
|
|
switch (type) {
|
|
case "number":
|
|
return "decimal";
|
|
case "tel":
|
|
return "tel";
|
|
case "email":
|
|
return "email";
|
|
case "url":
|
|
return "url";
|
|
case "search":
|
|
return "search";
|
|
default:
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function Input({
|
|
className,
|
|
type,
|
|
autoComplete,
|
|
inputMode,
|
|
...props
|
|
}: React.ComponentProps<"input">) {
|
|
const resolvedAutoComplete = autoComplete ?? "off";
|
|
const skipAutofill = resolvedAutoComplete === "off";
|
|
const resolvedInputMode = inputMode ?? deriveInputMode(type);
|
|
|
|
return (
|
|
<input
|
|
type={type}
|
|
data-slot="input"
|
|
autoComplete={resolvedAutoComplete}
|
|
inputMode={resolvedInputMode}
|
|
data-lpignore={skipAutofill ? "true" : undefined}
|
|
data-1p-ignore={skipAutofill ? "true" : undefined}
|
|
className={cn(
|
|
// v1.4.34 IW-G — floor at 44 px on mobile to clear WCAG 2.5.5;
|
|
// shrink to 40 px on sm+ where pointer precision is higher.
|
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-11 sm:h-10 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export { Input };
|