The generic medication card surfaces the upcoming schedule dose with
a `font-medium text-purple-400` accent so titration overrides on the
schedule visually pop out of the muted next-intake line. The GLP-1
card omitted the accent entirely.
Mirror the same accent on the GLP-1 card so the only difference
between Mounjaro and Ramipril detail surfaces is the data being
shown, not the styling treatment. The schedule.dose still rides
inside the existing next-injection paragraph (hidden below sm: like
the generic card so the narrow-viewport row stays one line).
Registers a new drain-per-sample-cumulative pg-boss queue + cron
(03:45 Europe/Berlin, slotting between the audit-log cleanup at
03:15 and the feedback aggregator at 04:00). The handler invokes
drainPerSampleCumulative across every user with cutoffHours=36 so
today + the trailing watch-sync window stay per-sample while
completed-and-stable days collapse into one stats: row per
calendar day per cumulative HK type.
Replaces the manual CLI + admin-endpoint pattern from v1.4.30 for
the recurring case (the operator-driven paths remain for one-shot
backfills) and avoids the prod-image tsx + Prisma symlink hack
documented in feedback_prod_image_tsx_prisma_gap.md.
The drain previously collapsed every per-sample APPLE_HEALTH cumulative
row it could see, including today's still-in-flight watch syncs. When
called from the scheduled nightly job we want to leave a grace window
around now so the list view keeps surfacing late watch reconciliations
in real time before the day stabilises.
cutoffHours adds an optional window: rows newer than now() -
cutoffHours are excluded from the scan. CLI + admin-endpoint callers
keep the previous all-rows behaviour by omitting the option.
When the type filter is a cumulative HK type (steps, active energy,
distance, flights, daylight) the measurements list now opts into the
route's groupBy=day mode and renders one row per user-TZ day with
the day's total + a sampleCount caption. A chevron in the row
action column lazy-fetches the day's per-sample chunks via a second
query against ?dayKey=YYYY-MM-DD and inlines them beneath the row
so power users can still answer wann wurden Schritte gemeldet
without scrolling past 200 step rows on the default view.
Mirrors the universal Apple-Health / Garmin / Withings pattern
captured in the v1.4.37 research brief: one number per day as the
primary surface, drill-down to the chunks behind it.
GET /api/measurements gains two new branches:
* groupBy=day + cumulative type → returns one synthesised row per
user-TZ day with value=SUM, sampleCount, and a dayKey anchor so
the list view paints one row per day instead of hundreds of
per-sample chunks from Apple Health.
* dayKey=YYYY-MM-DD + cumulative type → returns the per-sample rows
for that single calendar day in the user's IANA timezone, so the
expand chevron in the row can lazy-fetch the chunks that summed
into the daily total.
Both branches gate on CUMULATIVE_HK_TYPES so the iOS contract for
spot metrics (BP, weight, pulse, BG) stays untouched. The collapsed
row's measuredAt anchors to local noon to match the
canonicalDailyTimestamp convention used by the drain helper.
The generic medication card painted a coloured take-now / overdue /
very-overdue pill at the top of the detail surface; the GLP-1 card
silently omitted it. Marc reported the asymmetry during the v1.4.37
UX audit (audit item 11): Mounjaro and Ramipril cards felt "different"
even though both should read as the same row shape.
Wire the shared `reduceCurrentWindowStatus` reducer into the GLP-1
card, fetch the same reminder thresholds as the generic card, and
re-render once a minute so the pill tracks wall-clock progress without
a route reload. The pill renders only when a schedule is currently
in/past its window and the user has not yet covered every overdue
schedule with today's intake events — same suppression rules as the
generic card.
Add a render assertion that the inner column ships with grid +
flex-1 + grid-rows-[auto_auto_auto_auto_auto_1fr_auto] so a future
refactor can't silently revert to a flex stack and re-introduce
the v1.4.36 "Karte hört bei der Trennlinie auf" report. The
mt-auto + h-full + flex-col + flex assertions stay in place
alongside the new grid contract — they describe complementary
load-bearing classes.
Mock useFeatureFlags so the suite can drive both the flag-on (CTA
mounts) and flag-off (CTA hides) branches alongside the existing
aiEnabled gate test. Both branches share the all-on default unless
the case overrides it with mockReturnValueOnce, so the rest of the
suite continues to render with the default-mock flag set.
Every chip in <SuggestedPrompts> is a Coach affordance — clicking one
seeds the Coach drawer's composer with the localised prompt and opens
the drawer. The strip rendered unconditionally until now, so a
deployment with the operator's global Coach flag off still painted
five inert chips below the hero band.
Read flags.coach inside the component and short-circuit when the flag
is off. The HeroStrip caller already wraps the mount in the same
guard (W5 commit prior); this is defence-in-depth so a future caller
that mounts <SuggestedPrompts> outside the hero band can never leak a
Coach surface.
Read useFeatureFlags() inside TargetCard and pair the existing
aiEnabled guard with flags.coach so the per-card Coach CTA hides
entirely when the operator disables the Coach feature globally.
Pre-fix the CTA mounted whenever the user-level provider chain was
configured, even when the operator-level kill-switch was off; the
disable cascade is supposed to make every Coach surface disappear,
not just the hero-strip button. Sister sites (hero-strip,
suggested-prompts, /targets CoachDrawer) belong to W5.
Adds an SSR-only contract suite for `<ArztberichtHeroCard>` covering
the hero slot, value statement, CTA touch-target floor, the
`aria-describedby` link from the CTA to the value statement, the i18n
key resolution path (DE + EN), the hero-gradient + glow-purple visual
treatment, and the closed-state pass that keeps the dialog markup out
of the SSR output until the CTA opens it.
Updates the existing `<ExportSection>` smoke suite for the v1.4.37 W7a
shape: four secondary cards in the grid (not five), the hero mounted
above the grid, and the "Other export options" sub-heading.
The doctor-report PDF is HealthLog's flagship "data out" path. The
v1.4.16 phase B7 layout treated it as one of five equal cards in the
grid; this change lifts it into a hero card at the top of
`/settings/export`. The remaining four export cards (Measurements CSV,
Medications CSV, Mood CSV, Full JSON backup) move under a dedicated
"Weitere Export-Optionen" / "Other export options" sub-heading.
The hero reuses the `hero-gradient + glow-purple` visual treatment from
the Insights hero strip, so the surface feels consistent across the
app. The CTA routes through the existing `<DoctorReportDialog>` +
`/api/doctor-report` flow — only the framing changed. Mobile-first: at
< sm the CTA falls onto its own row and clears the 44 px WCAG touch
floor (`h-11 sm:h-10`); the button carries an `aria-describedby` link
to the value statement so screen readers announce the framing
alongside the action label.
/targets carries two Coach affordances — the per-card 'Ask Coach' CTA
on every TargetCard and the page-level CoachDrawer mount. The CTAs
were gated through `aiEnabled` (provider chain status), but the
drawer mount always rendered regardless of the operator's global
Coach flag. When the operator turned Coach off in Settings the drawer
still loaded its SSE machinery + Sheet portal subtree even though no
on-page CTA could open it.
Read flags.coach via useFeatureFlags and fold it into the existing
`aiEnabled` gate so the per-card CTAs vanish when either layer is
off. Wrap the page-level <CoachDrawer> mount in the same gate as
defence-in-depth, since the CTAs already hide when the flag is off.
Tests: add two new cases to targets-coach-mount.test.tsx covering the
Coach-flag-off branch (CTAs hide + drawer mount disappears). The
sibling targets-* test files mock @tanstack/react-query without
exposing QueryClientContext; add a use-feature-flags mock that
short-circuits to the all-on default so the existing assertions keep
running against the visible page shape.
The generic medication card mapped `medication.category` to one of the
localised category labels via an inline lookup table, while the GLP-1
card hard-coded `medications.treatmentClassGlp1` into the same slot.
The asymmetry made Mounjaro and Ramipril cards visually diverge on the
medications list even though both should read the same row shape.
Lift the lookup into `src/lib/medications/category-label.ts` and use it
from both card variants. The GLP-1 nature of a medication is already
implied by the rotation hint + injection-metadata rows below the
header, so the category badge now carries the real classification
(BLOOD_PRESSURE, HORMONE, OTHER, ...) on both cards.
Tests pinned the old contract; they now assert the new shared label
shape for both English and German locales.
Switch the title-row flex from items-start to items-center
sm:items-start. The dashboard.title plus welcomeText sub-line
wraps to two lines on < 380 px viewports and the button used to
hug the top of that 2-line block without a baseline anchor.
items-center on mobile centres the button against the title; sm+
returns to the original top-aligned posture where the title is
one line.
Collapse the Card's inter-row gap to 0 and drop the CardHeader
inner gap to 0 so the only space between the title and the chart
strip is the CardHeader's own pb-1 (4 px). HealthChart mini paints
a plain div with mb-1 under its title; Mood's pre-fix gap-1
(4 px) on top of the header pb-1 stacked an extra 4 px below the
title, leaving Mood ~12-16 px taller than the BP/weight siblings
inside the trends row.
The doctor-report PDF is HealthLog's flagship "data out" path — the
artefact a user actually carries into a doctor's appointment. The
v1.4.16 phase B7 layout treated it as one of five equal cards in the
grid, which undersold the feature.
This change extracts the doctor-report flow into a new
`<ArztberichtHeroCard>` component and renders it at the top of
`/settings/export` with the same `hero-gradient + glow-purple` visual
treatment the Insights hero strip uses, so the surface feels
consistent with the rest of the app. The remaining four export cards
(Measurements CSV, Medications CSV, Mood CSV, Full JSON backup) move
under a dedicated "Weitere Export-Optionen" / "Other export options"
sub-heading.
The CTA routes through the existing `<DoctorReportDialog>` +
`/api/doctor-report` flow — only the framing changed. Mobile-first:
the hero stacks at < sm with the CTA falling onto its own row; the
button clears the 44 px WCAG touch floor (`h-11 sm:h-10`) and carries
an `aria-describedby` link to the value statement so screen readers
announce both alongside the action.
Adds a new `settings.sections.export.hero.{eyebrow,valueStatement,cta,
formatHint}` bag plus a `settings.sections.export.otherOptionsHeading`
key across DE, EN, FR, ES, IT, PL so the doctor-report hero card and
the "Other export options" sub-heading resolve everywhere. Retires the
now-unreferenced `settings.sections.export.cards.doctorReport.action`
key — the hero owns the CTA copy from this point on.
DE + EN ship fully translated; the four AI-initial locales (FR, ES, IT,
PL) receive native translations for the hero copy in line with the rest
of their export bag.
Both the generic medication card and the GLP-1 variant need to paint
the same coloured take-now / overdue / very-overdue pill, but the
schedule-window math lived inlined inside medication-card.tsx only.
Move parseTimeToMinutes, toBerlinDate, getWindowStatus,
isLastIntakeInCurrentWindow, and the reducer that picks the
most-actionable schedule into src/lib/medications/window-status.ts so
both cards can consume the same code path.
No behaviour change for the generic card. The extracted reducer
mirrors the previous inline implementation, including the
overnight-window adjustment, the late / very-late thresholds, and the
"don't show in-window if the last intake already covered the window
today" guard. The helper accepts any schedule shape that exposes
windowStart, windowEnd, and daysOfWeek, which both card variants
already satisfy.
Override the Card primitive's default gap-4 md:gap-6 with gap-3
md:gap-4 and drop the CardHeader's pb-3 to pb-0. The default
spacing stacked on top of the CardHeader padding rendered a ~36 px
dead gap between the metric label and the headline number on md+,
so the card visually split into two disconnected blocks. The
tighter rhythm pulls the value back under the label so the card
reads as one cohesive unit.
Swap the inner column from a flex stack to a 7-row grid with a 1fr
slack row on the provenance accordion. The hero strip's row already
stretches the card via items-stretch, the card already declares
h-full, and the disclaimer keeps mt-auto for the no-grid render
path. The remaining work was teaching the inner column to actually
distribute the recovered vertical slack — flex with mt-auto only
on the disclaimer parked the recovered space under the disclaimer
instead of between the body and the footer, so the visible card
ended a divider-line above the left column's bottom edge.
The Personal Health Score builder used to read 37 days of raw weight
rows from the measurements table on every dashboard mount. When DAY
rollup coverage exists for WEIGHT, the new helper derives the weight
series from per-day MEAN values in measurement_rollups and only
issues a narrow 2-column projection against measurements for the
source-attribution pill (the rollup table does not carry the source
enum). Partial-coverage and brand-new accounts keep the legacy 37-day
raw read so the score stays correct on first cold-mount.
Mood, medications, and intake events still read live regardless of
coverage — none has a rollup equivalent today. A path:"rollup"|
"live" annotate on meta.healthScore.path makes the branch selection
provable in prod logs, matching the bp_in_target and summaries-slice
patterns.
Body relocated from route.ts to lib/analytics/health-score-fast-path.ts
so the dispatcher can be unit-tested against mocked Prisma without a
container. Unit test pins both branches (probe → rollup, probe →
live), the empty-component-set early-out, and the implicit-probe
shortcut for callers that omit the coverage map.
The hero band carries three Coach affordances — the action-row 'Ask the
coach' button, the SuggestedPrompts chip strip, and (via prop drilling)
the HealthScoreCard's onAskCoach handler. All three open the same Coach
drawer that the layout already gates on the operator's global Coach
flag. Until now they painted unconditionally; users on a Coach-disabled
deployment would click the chips or button and the drawer mount would
refuse to render, leaving the controls inert.
Read flags.coach in the hero and short-circuit each Coach surface when
the flag is off. The action row + chip strip are conditionally
rendered; the HSC's onAskCoach prop drills through as undefined so a
future re-addition of an inline HSC Coach button stays gated too.
Add whitespace-nowrap to the DropdownMenuItem primitive so every
dropdown row stays on a single line app-wide, and bump the sidebar
3-dot overflow menu from w-56 to w-60. The Benachrichtigungs-Center
label (23 chars) plus its Bell icon and the default text-sm cap sat
right at the 14-rem ceiling and wrapped at the hyphen on sub-pixel
rounding.
Split the trigger's horizontal padding (pl-3 pr-2.5) and push the
chevron 4 px further right via [&_svg:last-child]:mr-1 so the
trailing glyph sits at the same visual gutter as the browser-native
date-input calendar icon. The old uniform px-3 parked the chevron
12 px from the edge, which read visibly tighter than the date-picker
glyph when both controls shared a form row.
Run prettier --write on the four files the v1.4.37 W3 commits
touched (the schema, the route, the V2 component, and the two test
files). Drive-by also collapses two pre-existing whitespace drifts
in the InventoryItem type aliases and the glp1PostBodySchema refine
call so the file matches the formatter contract end-to-end. No
behavioural change.
Five SVG diagrams anchor the architecture story across README, the
public doc site and the landing page:
- 01-data-flow: source → ingest → store → read → surface
- 02-coach-pipeline: question → snapshot → prompt → provider chain → cited reply
- 03-self-hosting-topology: reverse proxy → app + worker → Postgres + image registry
- 04-source-priority: per-metric ladder with audit-trail kept/dropped lanes
- 05-security-model: three concentric layers plus rate-limit / audit / SSRF rails
Hand-authored SVG, no external dependencies, mirrored into the
healthlog-docs and healthlog-landing repos so each surface ships its own
copy.
Add two SSR fixtures to lock the v1.4.37 W3 fix in place: a mixed
taken+skipped seed asserts the distinct lucide icon + green styling
for taken rows and the muted outline styling for skipped rows, and a
German-locale skipped-only seed proves the table body never leaks the
"Eingenommen" label onto a skipped row. The seed helper threads the
new `status:"completed"` cache-key shard so the component reads its
own seed without firing a network call.
Marc reported that rows with `takenAt:null AND skipped:false` rendered
as "Eingenommen" with an empty leading column on the medication
detail page — the binary `event.skipped ? "Übersprungen" : "Eingenommen"`
mapping never accounted for the "missed / never confirmed" branch the
v1 component used to surface as "verpasst". The list now pins
`?status=completed` so those ambiguous rows stay off the detail-page
table (they remain visible on the calendar / today surfaces), and the
status cell branches explicitly: green secondary chip with a check
icon for taken rows, muted outline chip with a skip-forward icon for
skipped rows. Derived from `!skipped && takenAt` rather than `skipped`
alone so a malformed row cannot sneak past as "Eingenommen" again.
Extend the list endpoint with an optional `status` query knob —
`all` (default), `taken`, `skipped`, `completed` — so the detail-page
intake history can hide unconfirmed rows server-side without changing
the byte-stable contract the iOS Swift client and the existing
dashboard tiles depend on. `completed` (taken OR skipped) covers the
detail-page surface; `taken` / `skipped` are there for any future
single-arm consumer. Co-located route test pins the Prisma where
fragment per branch and the 422 on unknown values.
When DAY-bucket coverage exists for both BLOOD_PRESSURE_SYS and
BLOOD_PRESSURE_DIA, the /api/analytics default slice now derives the
five in-target windows (7d / 30d / all-time / prior month / prior
year) from rollup mean values instead of paging the trailing 365 days
of raw measurements. The new helper pairs SYS and DIA per-day means
by day key, runs the existing isBpReadingInTarget predicate, and
buckets days into the reporting windows. Partial-coverage and
brand-new accounts fall back to the legacy chunked aggregator so the
per-event pairing contract still holds where the rollup table cannot
guarantee parity. A path:"rollup"|"live" annotate on the meta dict
makes the branch selection visible in prod logs, matching the
v1.4.36 summaries-slice pattern.
The per-day-mean approximation is documented in the helper's header.
For typical multi-reading-per-day usage it lands within ~2 % of the
per-event count; outlier days mixing extreme readings can flip the
classification either way. The live fallback remains the per-event
source of truth.
Captures the full scope handed over after v1.4.36 went live: perf
carry-over from the v1.4.36 deferred list, IntakeHistoryListV2
Ramipril regression with concrete UAT details (planned/skipped
rows misrendered as eingenommen with grey em-dash), UI symmetry
punch list (HealthScoreCard height parity, 3-dot overflow nowrap,
targets-page header gap, dropdown-arrow margin, Trends-card sizes,
dashboard Hinzufuegen button), medication detail card symmetry
(Ramipril vs Mounjaro), global Coach disable cascade, settings
cleanup (timezone override removal, IP-whois resolution fix, BMI
status stuck loading), three new features (Arztbericht hero card,
Hinzufuegen medication-intake overlay, Apple Health step daily
consolidation), and a documentation refresh wave covering README,
landing page, and the doc site with Excalidraw diagrams. Last big
web release before iOS becomes the sole focus.
Captures the four items raised right after the v1.4.36 live
verify: full /api/analytics cold-path (carry-over from v1.4.36
deferred list, 111 s worst-case cold), Insights overview hero-row
height parity for the HealthScoreCard, TopBar 3-dot overflow
single-line constraint (Benachrichtigungscenter currently wraps),
and an IntakeHistoryListV2 regression that surfaces planned
schedule rows as "Status: eingenommen" on the medication detail
page. Last item flagged as a v1.4.36.1 hotfix candidate pending
confirmation.
Adds the v1.4.36 row to the audit-marathon closure ledger (now
five releases on 2026-05-17) with the headline numbers and the
post-deploy perf-QA write-up. Live measurements confirm the
page-blocking insights.comprehensive call dropped from 10.88 s
to 4.57 s cold (-58%), the AI prompt payload trim landed
(insights.generate codex run used 30,096 tokens versus the
25.9 MB raw baseline -- a 99.5% reduction), and the rollup
fast-path is firing on the slim summaries slice with the
"path":"rollup" annotate. Full /api/analytics correlations
branch still runs live SQL on a cold pool (worst-case 111 s
recorded, stabilising to 756 ms warm and 11-28 ms cached) and
is filed as the v1.4.37 P1.
Heavy-aggregate skip on the rollup-fresh + fully-covered path,
daily chart fetches opt into rollup buckets, Insights extractFeatures
swap drops the prompt payload from double-digit MB to the low
hundreds of kB on power-user accounts, page-level isLoading gate
replaced with per-section early-skeleton paint, trends-row chart
slot pinned for CLS, IntakeHistoryListV2 restored on the medication
detail page, About card folded into the Admin Console with an
auto-checking update badge, cumulative metric tiles read the day's
running sum, IP-whois fallback surfaces city + country when the
ASN lookup misses. Migration 0068 adds an additive
users.insights_exclude_metrics column. Unit suite 4354 passing;
typecheck + lint clean.
The read-path warm-up's bare `catch {}` hid populator regressions —
a deadlock or pool exhaustion inside `recomputeUserRollups` would
return `{ recomputed: false }` silently and the read fall-through
to the live aggregator masked the failure until performance bled
through.
The catch now annotates `rollup_refresh_failed` plus the error
message on the wide-event pipeline (no-op when there's no request
context) and emits a `console.error` with the same shape so the
worker log path surfaces the failure regardless of caller surface.
Return shape unchanged — the read path still must not fail.
Unit test simulates a deadlock inside the inner recompute and asserts
both the annotate meta and the console fallback fire while the outer
call still returns `{ recomputed: false }`.
The generate route caught `FeaturesPayloadTooLargeError` from the
raw-mode `extractFeatures` call and retried in aggregated mode. The
retry ran the same `enforceSizeGuard` from `features.ts`, so a
very large medication history plus multi-year context could blow past
the 5 MB ceiling a second time → the uncaught throw bubbled to the
apiHandler's generic 500 path.
The retry now sits in its own try/catch. On a second oversize the
route applies the existing `applyInsightsExcludeFilter` with the
optional context blocks (anthropometrics, medications, sleep, steps,
hrv, resting_hr) excluded and re-extracts. A third failure annotates
`insights_payload_too_large` for ops visibility and returns 422
with a readable user-facing message rather than 500. Unit test mocks
all three extractor calls to throw and asserts the 422 + annotate.
The dedicated update card was removed in 0738ef50 but its seven
copy keys (updatesHeading, checkUpdates, checking, lastChecked,
lastCheckedNever, upToDate, checkFailed) stayed in all six
locales. Only newerAvailable is still referenced — by the
UpdateBadge. Sweep the 42 unused strings.
The build-time row read its label from `t("builtAt", { time: "" }).trim()`
with a fallback to the gitSha key. The fallback was effectively dead
(the trim never produced an empty string) and the pattern silently
risked rendering two side-by-side "Build" columns when an
implementer dropped the trailing `{time}` placeholder. Replace with
a dedicated `settings.about.builtAtLabel` key across all six locales.
Extract the newer-version badge into an exported UpdateBadge
component. Renders as a 44 px-square anchor (or role=img span
fallback) with text-primary colour, focus-visible:ring-* on the
anchor branch, and aria-label/title carrying the latest tag so
keyboard + screen-reader users get the same signal sighted users
get from the icon. Drops the hardcoded text-dracula-purple in
favour of the semantic primary token, which already meets WCAG AA
on light + dark.
The v1.4.36 perf wave wrapped the briefing / correlation / trends
blocks in `<Suspense>` boundaries. The page is `"use client"`,
the blocks load via `next/dynamic({ ssr: false })` (no promise
throw on hydrate), and TanStack Query hooks never throw thenables, so
the boundaries never engaged — dead code, masking the real shape of
the delivery.
The actual perceptual win remains intact: the page-level
`isLoading` gate is gone, the hero plus each section's own
loader skeleton paints inside the first paint budget while data fills
in. A comment in the file explains why no `<Suspense>` wrapper
belongs here so a future contributor doesn't re-add one.
NOTE for CHANGELOG drafting: the perf-wave bullet should read
"early-skeleton paint" rather than "Suspense streaming".
Genuinely streamed server children remain a v1.5.x track.
The v1.4.35/v1.4.36 read-swap gated on a global `COUNT(*) > 0`
against `measurement_rollups`. A user with BP fully rolled up who
logged their first WEIGHT measurement saw the per-write hook upsert
one WEIGHT DAY bucket, the global probe flipped to true, the read
path picked the bucket-derived branch for WEIGHT, and the all-time
WEIGHT count collapsed to whatever the trailing 90d narrow aggregate
plus the single fresh bucket could reconstruct.
The new shared probe in `rollup-coverage.ts` joins distinct
measurement types against the rollup table per user and surfaces
`{ type, has_buckets }` rows. Both `computeSummariesSlice` and
`buildComprehensiveAggregate` take the rollup-derived branch only
when every type the user has logged is covered; partial coverage
falls back to the live aggregate so the brand-new-type case stays
correct. Integration test seeds BP rolled up + WEIGHT raw-only and
asserts the WEIGHT summary count matches live SQL.
shadcn Button size="sm" renders at 32 px — below the project's
44 px touch floor adopted in v1.4.34.4. Use the default size with
an explicit min-h-11 sm:min-h-9 override so the pagination row
stays compact on desktop but clears WCAG 2.5.5 on touch.
Sort headers relied on hover:underline only and dropped below the
44 px touch target floor on mobile. Add focus-visible:ring-* and
min-h-11 sm:min-h-9 so keyboard + touch users can hit the column
header reliably.
Pulls the v1.4.36 W4c MeasurementType -> SourcePriorityMetricKey
chain out of the dashboard per-type loop into a typed switch helper.
Same call sites, same null fallback for the unmapped TIME_IN_DAYLIGHT
branch.
v1.4.36's perf wave added a `SELECT COUNT(*) FROM measurement_rollups`
probe at the head of `computeSummariesSlice`. The slim-slice route test
mocks `$queryRaw` with two `mockResolvedValueOnce` calls (aggregate +
latest); the new probe consumed the first slot, leaving the aggregate
read pointing at undefined and the route returning 500.
Seed `n: BigInt(0)` as the first mock value so the route deterministically
takes the legacy heavy-aggregate fallback path the rest of the test
expects.
The carrier chip on the admin login-overview audit table was empty
whenever the offline GeoLite2-ASN MMDB had no entry for the IP. In
practice this is every IP behind a non-DACH or cloud-NAT range — the
free-tier MMDB carries DACH carriers densely but thins out
elsewhere — and the chip went blank even though the city + country
were already populated from the parallel location lookup.
When `entry.carrier` is null but `entry.location` is non-null,
render the chip with the city/country string and a "carrier
unavailable" qualifier. Private/loopback IPs still hide the chip
entirely (both columns null).
The dedicated "Updates" card carried a "Check for updates" button,
a last-checked timestamp, and three result paragraphs covering
up-to-date / newer-available / unknown. All of it surfaced an answer
to one question — "is there a newer release?" — and the answer was
overwhelmingly "no, you're current". The card was three full rem of
chrome for that signal.
The 24 h auto-check still runs on mount. When it returns
`newer_available`, an ArrowUpCircle badge appears next to the
version line; hover/tap exposes the latest tag via the title
attribute. The link points at the release HTML URL when present.
No bouncing, no badge counter, no manual recheck button.
The settings/about route now reads as a tight identity card +
external links list, matching how often the surface actually
gets used.