Favorites page fetched both /favorites/recipes and /items/all_brief, then
stitched isFavorite flags onto allRecipes so Search could filter across
the full catalogue. But Search is invoked with favoritesOnly={true} and
hideFavoritesFilter (so it's pinned on), so it only ever returns matches
that are already in the favorites list — allRecipes was dead weight.
- drop allRes / allRecipes / favoriteIds / allRecipesWithFavorites
- Search now receives data.favorites directly
- filteredFavorites filters data.favorites by matchedRecipeIds
- use locals.session ?? locals.auth() to reuse the hook's auth lookup
Every recipe list endpoint wrapped its result in
`JSON.parse(JSON.stringify(...))` before handing it to `json()`, which
then serialises again — a full extra stringify+parse cycle per response.
`lean()` already returns plain objects and ObjectIds/Dates serialise
correctly through `json()`'s single `JSON.stringify`, so the extra round
trip was pure waste.
Removed from the 9 output-side call sites (all_brief, category,
category/[cat], tag, tag/[tag], icon, icon/[icon], in_season/[month],
search, favorites/recipes, offline-db, translate/untranslated).
Kept the two deep-clone-before-mutation usages in items/[name] and
json-ld/[name] — those are load-bearing.
Shuffle stays server-side: moving it to the client would need a hero
preload + hydration rework that's bigger than a perf tweak.
Chart.js (~244 KB) was a top-level import, so every route that referenced
FitnessChart.svelte transitively pulled it. Defer it to an async block
inside onMount so non-stats fitness routes (workout, check-in, nutrition,
history list) no longer ship chart.js.
- `ChartCtor` holds the async-loaded constructor
- `disposed` guard handles unmount during the import
- theme MutationObserver / matchMedia wiring moved inside the async
block so it only attaches once the chart actually exists
Barrel `from '@lucide/svelte'` imports pulled every referenced icon into
one shared 748 KB client chunk. Switch every call site to per-icon
subpaths (`@lucide/svelte/icons/<kebab-name>`) so Vite tree-shakes each
icon independently. Also logs the TODO list for the perf audit so we
don't lose track.
- 46 files, 106 unique icons
- single `Minus as MinusIcon` alias preserved
- Lucide-internal aliases (`AlertTriangle`, `BarChart3`) resolve through
Lucide's own re-export shims; no behavioral change
Four pages had their own hardcoded `measureSlug = lang === 'en' ? 'measure' : 'messen'`
derived — all still pointing at the old route. Bumped the value to
check-in / erfassung and renamed the variable so future drift of
this kind is easier to grep for.
Affects links from:
- /fitness/check-in → body-parts card, inline "Edit all fields"
- body-parts flow → back / cancel navigation
- full-edit page → save / delete navigation
- /fitness/stats/history/[part] → "measure this now" CTA
The button was gated on `showWeightHistory`, which stays false on
desktop since the history-list uses CSS (`.collapsed` override at
≥1024 px) instead of the toggle. Move the gating to a `.collapsed`
class on the button too, mirroring the list — hidden on mobile until
the user expands, always visible on desktop.
Body-measurement variation of <4 cm used to stretch the full chart
height, making normal weekly noise look dramatic. Now the y-axis
enforces a 4 cm floor centered on the data's midpoint; wider swings
render at their actual range as before.
- FitnessChart: new optional `yMin` / `yMax` props mapped to Chart.js
`suggestedMin` / `suggestedMax` — soft bounds, so data that exceeds
them still widens the axis.
- `/fitness/stats/history/[part]`: computes min/max across available
values (both sides if paired), enforces the 4 cm floor, passes to
FitnessChart. Tick distance stays on Chart.js auto — small ranges
get 0.5 cm ticks, wider ones scale up naturally.
Route slugs and nav label rename only — storage, API endpoints
(`/api/fitness/measurements`), and the `BodyMeasurement` Mongo model
keep their technical names.
- `/fitness/measure` → `/fitness/check-in` (EN)
- `/fitness/messen` → `/fitness/erfassung` (DE)
- Folder `[measure=fitnessMeasure]` → `[checkin=fitnessCheckIn]`
(git rename; history preserved).
- Param matcher `fitnessMeasure.ts` → `fitnessCheckIn.ts`, accepts
`check-in` / `erfassung`.
- `fitnessSlugs(lang).measure` and `fitnessLabels(lang).measure` code
keys are unchanged — value returns "check-in"/"erfassung" and
"Check-in"/"Erfassung" respectively, so no call site needs touching.
- slugMap language-detection updated to `erfassung ↔ check-in`.
- Service-worker cache list + the layout regex that gates the wider
content width now reference the new slugs.
- Nav icon swapped from `Ruler` to `NotebookPen` — reads as "logging
entries" and spans weight / composition / period better.
Bookmarks on the old URLs will 404; no redirect added.
Mirrors the weight chart pipeline (SMA + ±1σ confidence band) for
body-fat %, but emits deltas from the first displayed measurement so
the y-axis shows change instead of raw numbers. Title surfaces the
baseline (e.g. "Body Fat · Δ from 18.2%"), y-unit is "pp" (percentage
points), colours are purple trend on top of an orange raw-data line
so it reads differently from weight's green+blue at a glance.
FitnessChart gained two shared upgrades: `interaction.mode = 'index'`
on line charts so hovering the x-axis shows tooltips for every dataset
(including the trend line whose pointRadius is 0), and a `σ` dataset
filter so the confidence band doesn't clutter the tooltip. A new
optional `tooltipFormatter` prop lets callers format the hover label;
the BF chart uses it to show the signed delta + reconstructed
absolute % for raw points and additionally the ±1σ window for trend
points (e.g. "+0.30 ±0.45 pp · 18.5% (18.0–18.9%)").
- New "Same as last" pill below each step's stepper. Clicking fills
the input(s) with the prior recorded value(s) — for paired steps
in split mode, both L and R — and advances to the next step.
Only rendered when a previous measurement exists; the placeholder
already surfaces the exact number so the button text stays terse.
- Copy L→R button resized to match the same-as-last pill (0.88 rem
text, 0.55 × 1.1 rem padding) and given top margin. Unicode →
swapped for a proper ArrowRight icon between L and R.
- i18n: added `same_as_last` and split `copy_l_to_r` into
`copy_l_to_r_before` / `copy_l_to_r_after` so each language keeps
its natural wrapping around the arrow (EN "Copy L / R",
DE "L / R übernehmen").
- Server POST now upserts by (user, calendar day). Non-conflicting
fields merge silently; real overwrites (new non-empty value ≠
stored value) return 409 with the conflict list. Client retries
with `?overwrite=1` after a confirm dialog naming each field and
its old→new value. Null/empty payload fields are skipped, so
logging a body-fat entry on a day that already has weight merges
cleanly without flagging a phantom weight conflict.
- `summaryParts` in the history now includes a body-parts count,
e.g. "86 kg · 0.1% bf · 5 body parts" or "5 body parts" instead
of the flat "Body measurements only" fallback. Pluralised in EN
and DE.
- Inline quick-edit: "Full edit →" text replaced by a dashed primary
pill `Pencil · Edit all fields · ChevronRight`, inlined with the
X / ✓ action buttons on the same row. The label collapses to
icons only at ≤480px so the three controls stay on one line.
- Quick-edit date input swapped from native `<input type="date">`
to the site's `DatePicker` component.
- New i18n: `overwrite_title`, `overwrite_message`, `overwrite_confirm`.
- TODO.md marks features #2 and #3 done. CLAUDE.md carries a
policy note (no AI-attribution trailers on commits).
SSR now ships only the 10 most recent measurements (down from 200) to
cut initial page weight. A "Show more (N/total)" pill appears below
the list when more are available; clicking fetches the next 20 via
the existing GET endpoint (offset/limit already supported) and
appends with dedupe by `_id`.
`measurementsTotal` is seeded from the API's `total` field and kept
in sync on save (+1) / delete (−1). The button is hidden when the
history is collapsed or when `measurements.length >= total`.
Added `show_more` i18n string.
Mostly additive JSDoc/TS type annotations and null/undefined guards —
no runtime behavior changes. Starting baseline: 454 errors + 1 warning
across ~50 files. After: 0/0, build is clean.
Highlights:
- Duplicate object-literal keys fixed: 11 in cospendI18n.ts, 2 in
fitnessI18n.ts (dropped second `loading`; renamed `protein_per_kg`
stats-card label to reuse `protein`), 1 in shoppingCategorizer.
- `bind:this` state declared with `HTMLDivElement | null` across
DatePicker + Muscle{Map,Filter,Heatmap}.
- SaveFab's required `onclick` made optional (type="submit" handles
form submission in most callsites).
- Implicit any on ~200 callback parameters replaced with concrete
JSDoc/TS types. Chart.js generics and one mongoose query chain cast
are the only `any` / `unknown as any[]` uses introduced.
- Stats history discriminated union (`paired: true | false`) lets the
template narrow `series` and `stats` properly.
- Food page server guards use `throw new Error('unreachable')` after
`errorWithVerse(...)` awaits so TS narrows `entry`/`recipe`/`meal`
below. Same pattern applied to cospend payments, calendar detail,
and prayers server loads.
- Mongo `Date → string` serialization helper in cospend list so
`IShoppingItem[]` fits `ShoppingItem[]` at the boundary.
- Recipe category/tag pages use a local `RecipeItem` alias (derived
from `BriefRecipeType`) so `rand_array`/filter callbacks type.
- `web-haptics/svelte` has no bundled `.d.ts`; added a local
`@ts-expect-error` shim on the one import line.
Files touched: ~50 across fitness, cospend, faith, recipe, and shared
lib components / API routes.
- Lock +/- button positions by normalizing stepped weight/body-fat
values to .toFixed(1) so trailing zeros stay; placeholders also
normalized. Input width no longer jitters through a step sequence.
- Cap .history-section width on mobile/tablet to match .main-col
(480px / 760px) so "Past measurements" aligns with the metric cards.
- Body-parts page:
- Remove the "Running totals" list from the right panel.
- Hide the keyboard-shortcut legend by default; show on `?` (toggle)
or Escape (dismiss), with a small `?` pill hint in its place.
Added kbd_hint i18n string.
- Push skip + back/next toward the edges of the bottombar; pull
progress dots + close button inward symmetrically.
- Center the keyboard legend / hint on the screen width rather than
between the skip and nav buttons (position: absolute + translate).
- Weight + body fat cards share a unified .metric-card component with wheel
+ keyboard (Arrow/Shift+Arrow) stepping. Side-by-side on tablet and up.
- Replaced body-parts accordion with a prominent card showing a cropped
muscle-front silhouette and overlay dots/bands marking which regions
have measurements. Shoulders + chest render as dotted tape-measure
bands; other parts as dots. "Last measured" now relative (N days ago).
- Desktop layout: .main-col (form + period tracker) left, history on
right. Two columns center together at wider widths instead of drifting
apart. Fitness layout detects measure index and bumps max-width to
1400px, matching nutrition.
- Inline history edit: pencil swaps the row for a compact date/kg/%
form (Enter saves, Escape cancels) via PUT /api/fitness/measurements.
Full-edit link preserved for body-parts tweaks.
- Body-parts history heading renamed to "Past measurements" /
"Frühere Messungen" to avoid collision with the period tracker's
own history.
- "Profil bearbeiten" moved to the top-left of the main column.
- Same-sides toggle in the body-parts flow now uses the shared Toggle
component.
Adds a forearm SVG in the same currentColor-stroked style as thigh.svg and
wires it into both the body-parts wizard and BODY_PART_CARDS so the step
no longer falls back to the ruler placeholder. Also refreshes the hips PNG.
Side list now tints the selected row (theme-aware color-mix on text-primary
into surface; gold variant for today), caps at the ring's height via pure
CSS (absolute-positioned aside in a relative slot so the ring alone drives
row height), and auto-centers the selected item — falling back to the
closest-dated feast when the selection is ferial.
Tauri WebView sessions (and long-lived browser tabs) persist
hydrated load() data indefinitely, so server-side changes never
surface until the user manually navigates across a depends()
boundary. Wire visibilitychange + focus to invalidateAll(),
throttled to once per 5 min to keep expensive loaders cheap.
Android step detector silently returns no events on API 29+
when ACTIVITY_RECOGNITION is ungranted, so cadence was always
absent from recorded tracks. Declare the permission, request
it at GPS start, guard sensor registration and retry it from
MainActivity.onRequestPermissionsResult when the user grants
mid-session, and toast a hint if they deny.
Export each cardio exercise's stored GPS track from the history
detail page. Cadence is emitted per-point via Garmin's
TrackPointExtension v1 so Strava/Garmin Connect preserve it.
Filename: YYYY-MM-DD-<workout> <mins>min <Activity>.gpx.
The store-picker read localStorage at component init, which crashed
SSR on full-page loads of /cospend/list with 'localStorage.getItem is
not a function'. Deferred the read to onMount and wrapped writes in
try/catch.
Long-press modal on /cospend/list now lets you change the item's name
and quantity (e.g. "500g", "3x") alongside category and icon. The
quantity is re-prepended to the name so the existing parser keeps
picking it up.
Replace /1962 and /1969 with /vetus and /novus — matches how Catholics
actually refer to the missals (Vetus Ordo / Novus Ordo), reads the same
across de/en/la, and sidesteps the value-laden old-vs-new framing.
Rite pill labels flip to "Vetus" / "Novus"; the year stays visible in
the subtitle. Legacy year-slug URLs 307-redirect to keep bookmarks alive.
Romcal's liturgical scope emits LY N with a stale post-Pentecost tail
~3 weeks into December; dates from Advent I onward belong to LY N+1.
Month/ring views already shift — port the same rollover to the detail
page so Dec 1–20 stop showing "After Pentecost" data and Dec 21–31 stop
404'ing.
Server runs from build output dir where CWD-relative `static/*.tsv`
misses — adapter-node ships static assets at build/client/. New
resolveStaticAsset() helper uses import.meta.url to find the bundled
location, falls back to <cwd>/static/ in dev.
Fixes ENOENT on drb.tsv/allioli.tsv after deploy.
The projection gate required the date to be strictly after today,
so the current day never showed a projected burn even before any
workout had been logged. Loosened to >= today and removed a
now-duplicate isTodayOrFuture/today declaration introduced by the
earlier round-off flicker fix.
PeriodTracker gains an optional mode prop ('entry' | 'projection' |
'full') that gates which sections render. The measure page keeps the
full tracker for the user's own cycle (logging plus calendar). The
stats page now mirrors it in projection mode and is the sole home
for shared cycles, which used to clutter the measure page.
Unifies PNG and SVG body-part images behind a single CSS-mask
render path, so both now colorize with a --accent CSS variable.
Accent splits by measurement type: --blue for proportion parts
(chest, shoulders, waist, hips) and --nord8 for muscle parts
(neck, biceps, forearms, thighs, calves). Stats cards gain a
matching 8%-tint fill and accent-colored hover border. History
page header image enlarged. Thigh SVG stroke-width bumped to 11
for better mask legibility.
Placeholders and +/- fallback now use the most recent recorded
value per part; previously placeholders were hardcoded "—" and
+/- bumped from 0. Buttons step by 0.5 cm (manual input still
accepts 0.1 resolution).
Body-parts grid now lives on /fitness/stats, and per-part history
pages moved from /fitness/measure/history/<part> to
/fitness/stats/history/<part>. Measure page keeps weight/BF entry
and the body-parts measure launcher.
Replaced the auto-selecting $effect that was clobbering manual slot
picks with explicit init in onMount and advancement inside pray().
Selecting lunch/evening after praying morning now works.
- LanguageSelector: add kalender/calendar/calendarium mappings so swapping
language from /glaube/kalender/... produces /faith/calendar/... instead
of the broken /faith/kalender/... URL.
- HeroCard: move margin-bottom off the anchor so the plain (1969) variant
keeps the same bottom spacing as the linked (1962) variant.
- Calendar overview: omit detail href on 1969 so the hover chevron /
hover elevation don't appear when no detail page exists.
- Detail route: 404 any /detail/... under the 1969 rite — only 1962 has
day-detail pages.
- Auto-fill missing vernacular propers from Allioli (DE) or DRB (EN)
when the 1962 missal bundle lacks a translation, mapped per Latin slot
via romcal's scriptureRef blocks (compound refs split 1-to-1 when
segment count matches slot count).
- Strip Psalm superscriptions and trailing periods so lookups parse and
Bible text aligns with the Latin antiphon.
- Localize the section reference header (Marc → Mk, Vulgate→Hebrew
psalm shift for DE) instead of showing raw Latin.
- Add Latin / Parallel / Vernacular view toggle with localStorage
persistence; hide Allioli/DRB badge in Latin-only view.
- Latin column now takes primary text color; vernacular secondary,
matching the Prayer.svelte convention.
Removes decorative route-label h1s across fitness, recipe and cospend
pages — replaced with sr-only h1s for assistive tech and a shared
.sr-only utility in app.css. On the measure page, the tucked-away
profile chip becomes a dismissible setup banner that only appears
when sex/height/birth year are missing, with a permanent "Edit profile"
link at the foot of the page.
Macro split now always renders (faded rings + hint when no food logged),
and the calorie balance hint distinguishes missing demographics/weight
from missing food-log data with a warning style.
ActionButton now renders as <a> (href) or <button> (onclick), so
SaveFab wraps it to inherit the shake/hover/focus behavior already
used by AddButton/EditButton. Body-parts review replaces its inline
save button with SaveFab for consistency.
Latest measurements render as a 3x3 card grid (vertical mobile,
horizontal on ≥768px) linking to `/fitness/{measure}/{history}/{part}`
with summary stats + chart. Slugs localize (hals, oberschenkel, …)
when on the German route.
Pre-compute romcal year maps on server boot for current + next civil year
across en/de/la in each rite's default diocese, non-blocking so startup is
unaffected.
Also fixes several 1962-rite rendering bugs: commemorations previously
leaked 1969-shape ids (e.g. andrew_apostle) next to proper 1962 sancti;
station church names came through unresolved because RomcalConfig's
internal i18next has no bundle loaded; season names arrived as raw keys
(advent.season) for the same reason. All three now resolve locally via
the shipped 1962 bundle with Latin as fallback. ClassIV ferias get a
small dot on the grid.
The active-workout activity picker updated local state + workout.name
but never synced workout.activityType, so every hiking/walking/cycling
workout was saved with an inner exercise of 'running'. That silently
applied the wrong kcal model — running Minetti for hiking/walking
(~43% overestimate) and running instead of the cycling physics model
(large overestimate for cycling).
Adds an activityType setter on the workout store and invokes it from
selectActivity() so the saved session's exercise matches the picked
activity.
Average calorie balance was comparing logged-day intake against all-day
expenditure, producing a spurious deficit on weeks with untracked days.
Now restrict both sides to days with non-zero logged intake so the
subtraction compares apples to apples.
Past and today return projectedExercise=null, so the estimate no longer
appears on days where the user could have just skipped a workout. Also
skips the WorkoutSchedule lookup when not needed.
- Add `timing` handle in hooks.server.ts emitting Server-Timing headers
and expose `locals.timing.mark/measure` for per-load instrumentation.
- Drop dead `getEnrichedExerciseById` fallback in fitness detail page —
server load already 404s via errorWithVerse, so the client no longer
pulls exercisedb-raw.json (~760KB) into the detail bundle.
- Add `{ createdBy: 1, nextExecutionDate: -1 }` index on RecurringPayment
for user-scoped list queries.
- Narrow populate projections in cospend/debts (title/date/category on
userSplits, _id only on allRelatedSplits) to cut payload + hydration.
- Parallelize today's sessions + WorkoutSchedule lookup in the nutrition
page load via Promise.all; add `.lean()` + `.select('templateId')` to
the lastScheduled query.
Cap shell height to viewport minus header so the bottombar stays visible,
allow the stage to scroll internally, and swap the thigh diagram to a
mask-tinted SVG that tracks the text-primary color across themes.
SvelteKit's handleError hook is skipped for expected `error()` throws,
so verses set there never reached `$page.error` for server-thrown 404s
and auth denials. Introduce `errorWithVerse()` in `$lib/server/errorQuote`
that fetches a random verse first, then throws `error(status, body)`
with `{ message, bibleQuote, lang }`, making the quote available in
every `SectionError`. Convert all page load throws (catchalls, layout
validators, calendar, prayers, recipes, fitness, cospend, admin) and
hooks.server auth gates to the helper. Add `src/error.html` as a
branded last-resort fallback.