The final functional web release before the iOS native client lands.
Headline work: a streaming Apple Health export.zip importer for
prospective iOS-migrating accounts, a server-side aggregation cache
that collapses the three hottest dashboard reads onto in-process LRU
slots with single-flight coalescing and per-user invalidation, a
broader compliance classifier with a dedicated early bucket so
ahead-of-window doses count as compliant, the Coach launch surface
hoisted onto every authed page with a dashboard-hero CTA, and a
trimmer Settings shelf that folds Sources into Targets. The release
also lights up the cache.<name>.outcome annotation on the active
wide event so production logs carry the cache hit-ratio signal
without leaking userIds.
Added
- Server-side aggregation cache. A new
ServerCache<T>primitive
(src/lib/cache/server-cache.ts) extends the v1.4.33 Coach snapshot
LRU shape into a reusable per-route layer with TTL expiry,
capacity-bounded LRU eviction, single-flight read coalescing, and
per-instance hit / miss / eviction / stampede counters. Wires the
three hottest dashboard reads —/api/analytics(slim + default),
/api/gamification/achievements,/api/medications/intake?scope=compliance
— and bolts per-user invalidation onto every measurement / mood /
medication / workout / dashboard-widget write endpoint so the next
read paints fresh data instead of waiting out the TTL. Every cache
hit / miss surfaces on the active wide event as
cache.<name>.outcome+cache.<name>.key_hashso production logs
carry the hit-ratio signal without leaking userIds. - Apple Health
export.zipimport. New endpoint
POST /api/import/apple-health-export(synchronous multipart
upload, asynchronous ingest via a dedicated pg-boss
apple-health-importworker, per-MeasurementTypeingestion
stats). Streams the upload straight to disk so a multi-gigabyte
export never lands in V8 heap, hashes the bytes inline for
content-based idempotency, and unpacksapple_health_export/export.xml
with a hand-rolled ZIP central-directory walker that handles Zip64
for archives past the 4 GB barrier. The parser folds every
<Record>,<Workout>,<Correlation>, and<ClinicalRecord>
into the existingMeasurementandWorkoutrow shapes — spot
rows keyed byHKMetadataKeyExternalUUID(or a deterministic
sample:<sha256>fallback), cumulative HK types collapsed into
onestats:<HKType>:<YYYY-MM-DD>row per user-local day to match
the iOS daily-aggregation convention. Live per-stage progress
surfaces throughGET /api/import/apple-health-export/{jobId}/status.
An admin variant atPOST /api/admin/import-apple-health-export
imports on behalf of a target user (cookie-onlyrequireAdmin()
gate; Bearer tokens never elevate). ImportJobschema model + migration. New Prisma model captures
the per-upload state machine (queued | unpacking | parsing | upserting | done | failed), the content-hash for idempotency
short-circuits, and the per-MeasurementTypeingestion counters
the status route surfaces back to the client.lastSeenByTypeon/api/analytics. Both the slim
(?slice=summaries) and default slices return a per-type
{ lastSeenAt, daysAgo }map. The dashboard trend tiles wire 12
mounts to a newtileStaleDays()helper so an "X days / weeks /
months ago" hint paints under any per-metric tile whose last
sample crosses the 7-day floor. Six locales gained additive
week / month plural keys; Polish uses the genitive-plural form
that covers every non-1 count the bucket math can produce.- Dashboard "Ask the coach" hero CTA. A new pill next to the
existing "Hinzufügen" / "Add" launches the Coach drawer directly
from the dashboard hero. The<CoachLaunchProvider>hoisted from
the insights layout to the auth shell so the drawer is reachable
from every authed route; the floating action button stays scoped
to/insights/**so it cannot distract on the dashboard. - Shared achievements query hook.
useAchievementsQuery()
centralises the three previous consumers — recent-achievements
card, the/achievementsmother page, and the unlock notifier —
onto one TanStack queryKey + cache slot so dashboard cold mount
fires the endpoint once instead of twice. - Typed authed Cache-Control presets. New
src/lib/http/cache-headers.tsmodule exports
NO_STORE_BUT_BFCACHE = "private, max-age=0, must-revalidate",
SHORT_LIVED_PUBLIC, and anapplyAuthedHeaders()helper. The
presets land at the framework level via anext.config.ts
headers()rule that stamps the bfcache-friendly directive on
every authed HTML response, restoring Chromium bfcache eligibility
for in-app back / forward navigation. scripts/print-bundle-report.mjs. Restores the at-a-glance
"top client chunks by parsed size" signal Turbopack dropped from
next build; reads.next/analyze/client.json(written by
@next/bundle-analyzerwhenpnpm analyzeruns withANALYZE=1)
and prints a sorted table plus totals.
Changed
- Settings sidebar: "Sources" folded into "Targets & Sources".
Per-metric threshold ranges and per-metric source priority now
share one/settings/thresholdspage so the same metric's
threshold and device-source preference sit together instead of
one sidebar entry away./settings/sourceskeeps a
permanentRedirectso external bookmarks and docs links follow
through unchanged. Section count: 11 → 10. - Insights tab strip: vital pills collapse under a "Vitals"
parent. Five vital pills (HRV, Resting HR, Oxygen, Body
Temperature, Active Energy) hide behind one parent pill that
opens a popover sub-list. Parent-pill active state mirrors the
URL so spatial orientation survives the collapse; each sub-page
keeps its own URL and bookmark resolves unchanged. Strip
footprint: 14 → 10 entries when every metric has data. - Coolify env-var audit.
mcp__coolify-apps01__env_vars
inspection captured the section-1 / section-2 duplicates that
have accumulated under apps-01 since v1.3.1. Audit pinned at
.planning/round-v1434-iwa-coolify-env-audit.md; no env-var
deletes performed (operator action).
Fixed
- Compliance classifier: early intakes count as compliant. The
classifier picked up a dedicatedearlybucket (window-start
minus three hours through window-start), theon_timepost-window
tolerance widened from one hour to three hours, and thelate
band sits between the newon_timeceiling and the configurable
lateMinutesknob. Heatmap consumers route the new bucket
through the compliant path; the dedicatedearlycounter rides
onDailyComplianceEntryfor downstream consumers that want to
distinguish ahead-of-window from on-window intakes. - Compliance heatmap fallback retired. The v1.4.33 defensive
looksClassifierBugfallthrough is gone now that the underlying
classifier widening removes the every-dose-very-late mode the
fallback covered.
Performance
/api/gamification/achievementsconsumer collapse. Two of
three previous consumers shared the same TanStack queryKey
literal; the third carried a per-user discriminator so the cache
treated it as a fresh slot. Dashboard cold mount fired the
endpoint twice. Collapsing onto the shared hook trims that to a
single request; with the new server-side cache slot warm, the
duplicate-mount worst case rides single-flight coalescing.- GHCR build fires once per release tag. The
docker-publish.ymlworkflow lost itspush.branches: [main]
trigger; the:latestraw-tag enable rule moved onto the tag
ref so each release produces exactly one multi-arch build that
refreshes both the semver tags and the:latestalias. - NFT-trace warnings silenced.
next.config.tsgained an
outputFileTracingExcludesblock that narrows the Turbopack
tracer away from theMAXMIND_LICENSE_KEYenv-access path so
the trace-report no longer warns on every dependent route file.
Refactor
- Dashboard analytics consumers. The two cards that previously
decided their own freshness copy now lean on the typed
tileStaleDays()helper and the additivelastSeenByTypefield
on the analytics response. Mood and BD-Zielbereich tiles keep the
defaultnullbecause they have no underlying per-type freshness
signal.
Accessibility
- Dashboard hero CTA hits the WCAG 2.5.5 touch-target floor.
The new "Ask the coach" pill carriesmin-h-11on mobile and
the matchingsm:min-h-9desktop floor so the touch target
stays above 44 px on the Pixel 5 boundary. - Insights tab-strip Vitals parent reads correctly to assistive
tech. Parent pill carriesaria-current="page"whenever the
current URL matches one of its child vital sub-pages, plus a
data-slot="insights-tab-strip-group"hook for visual-regression
testing.
Internal
- Two e2e flake windows tightened.
onboarding-flickerswapped
its 12-sample 50 ms poll loop for a single Playwright
auto-retryingtoBeHidden({ timeout: 700 })assertion so the
1-2 ms race window between the analytics-pending shell and
useAuth().userresolving collapses to a single retry slot.
mobile-viewportdroppednav a[href]from the touch-target
sweep (the bottom-nav owns its own WCAG spec) and gated the
44-px floor onmatchMedia('(min-width: 640px)').matches === false
so the Pixel 5 viewport never tripped into thesm:tier during
WebKit render commits. - Server-cache observability. Every
cached()invocation passes
anannotatecallback that lands two keys on the wide event:
cache.<name>.outcome∈{ hit | miss | stampede }and
cache.<name>.key_hash(non-reversible djb2 32-bit hash). Ops
can grepcache.analytics.outcomeover any time window to
compute the hit ratio per deployment. - AASA followups verified. The v1.4.33
/.well-known/apple-app-site-association
handler serves a direct 200 on every fronting origin; Apple's
CDN has ingested matching bodies for all three domains.
Web freeze
v1.4.34 marks the last functional release of the web codebase before
the iOS native client launches. Subsequent v1.4.x tags carry security
fixes, dependency bumps, and hotfix-only corrections. New feature work
is paused until the iOS app clears Apple review, at which point a
v1.5.0 version-bump-only release tags the milestone. The Prisma
schema head comment and .planning/v15-strategic-plan.md §5
decision-log row pin the freeze trigger in-tree.