Commit Graph

975 Commits

Author SHA1 Message Date
Alexander 0ab98690eb feat(shopping): loyalty-card modal with build-time barcodes
Adds a CreditCard button on the shopping list that opens a modal
showing the user's Coop Supercard (Data Matrix) and Migros Cumulus
(Code 128). Card numbers come from SHOPPING_COOP_SUPERCARD_NUMBER
and SHOPPING_MIGROS_CUMULUS_NUMBER env vars; a prebuild script
renders each to an SVG (~1-2 kB) in static/shopping/ so no barcode
library ships to the client. Cards missing their env var are
silently skipped, and the generated SVGs are gitignored to keep
personal numbers out of the repo.
2026-04-23 16:21:15 +02:00
Alexander a8b0d3c722 style(recipes): swap heart emoji for lucide icon
Replaces heart/black-heart emoji on the favorite button and the
card favorite indicators with the lucide Heart icon. Favorited
state uses a vivid #ff2d55 fill with layered drop-shadows so the
mark reads against colorful recipe photos; unfavorited button
shows an outlined white heart.
2026-04-23 16:05:19 +02:00
Alexander b8e5155e2d style(header): replace Login text with lucide icon
Logged-out users now see a LogIn icon in the header nav pill instead
of the "Login" text link. Avatar button still shown when logged in.
2026-04-23 16:00:05 +02:00
Alexander 8c75a2ddda style(fitness/period): make end-period button full-width
Flat nord11 fill, full-width CTA with check icon and uppercase label
replaces the muted tertiary-bg pill so ending an ongoing period is
the clear primary action on the status card.
2026-04-23 15:57:03 +02:00
Alexander c01dff197f perf(recipes/search): memoise per-recipe normalized search string
Every keystroke the filter rebuilt the lowercased + diacritic-stripped
+ soft-hyphen-stripped concat of name/description/tags per recipe. For
a 200+ recipe catalogue that's a lot of regex work on the hot path.

Cache the normalised string in a WeakMap keyed by the recipe object;
first keystroke still pays the full cost, every subsequent one is a
single indexOf per recipe.

Picked client-side memoisation over the audit's suggested server-side
`_searchKey` to avoid duplicating every recipe's text over the wire.
2026-04-23 15:50:08 +02:00
Alexander 38330d7020 docs: tighten #10 summary in TODO 2026-04-23 15:46:39 +02:00
Alexander 03875f2be6 perf: add Cache-Control to stable recipe & fitness API endpoints
rand_array seeds with Math.floor(time / 86400000), i.e. the same
shuffle for every caller during a UTC day — so every list endpoint
that runs through it is safe to share publicly:

  - /items/all_brief, /items/category/[c], /items/tag/[t],
    /items/icon/[i], /items/in_season/[m]
    → public, max-age=28800 (8h), s-maxage=28800, SWR=1d

The distinct-value lists (no shuffle, change only on recipe edit):

  - /items/category, /items/tag, /items/icon
    → public, max-age=3600 (1h), s-maxage=86400 (1d), SWR=1w

Individual recipes change when their author edits them:

  - /items/[name]
    → public, max-age=300 (5m), s-maxage=3600 (1h), SWR=1d

Fitness exercise-picker filters are identical for every logged-in
user but require auth:

  - /fitness/exercises/filters
    → private, max-age=3600

Skipped the calendar page itself: its HTML embeds data.session via the
faith layout's <UserHeader>, so public caching would leak identity.
2026-04-23 15:46:04 +02:00
Alexander ff6a7ce01a docs: mark #9 done in TODO 2026-04-23 15:40:37 +02:00
Alexander 87bf5d100e perf(fitness/history): slim session list projection
List endpoint previously returned full session documents minus GPS
tracks — two months × up to 200 sessions means ~60 KB of payload per
month with a lot of fields (notes, templateId/Name, mode,
activityType, endTime, session-level gpsPreview) that SessionCard
never reads.

Narrow the projection to exactly what the history page + SessionCard
use, and switch the query to .lean() so we skip the Mongoose document
overhead on deserialisation.

Detail view (/fitness/history/[id]) hits a separate endpoint that
keeps the full document.
2026-04-23 15:40:27 +02:00
Alexander 076c6efb38 perf(faith/calendar): trim yearDays, send pre-filtered feastDots
yearDays was a 365-entry array (one per day in the LY window) with
{iso, name, rank, color, seasonKey} on each — the client only needed
the color (for the needle pin on the currently-selected day; RingView
re-did the feast filter itself). Split into:

- yearDays: {iso, color} — unchanged count, but ~60% smaller per entry
  (drops name, rank, seasonKey)
- feastDots: {iso, name, rank, color} — new, pre-filtered to
  rank > ferial server-side (~150 entries instead of 365)

RingView's `feastDots` derivation shrinks to filtering out just the
currently-selected day, and `activeFeasts` filters `feastDots` by arc
bounds instead of re-scanning yearDays. needleDay's color lookup still
works with the trimmed YearDay.

Also collapses a stray `locals.session ?? (locals.session ?? …)` the
earlier #5 sweep introduced in both calendar page loaders.
2026-04-23 15:37:38 +02:00
Alexander 4112e38306 perf: add projection + O(1) bucket math to muscle-heatmap endpoint
Endpoint previously pulled full WorkoutSession documents (including
gpsTrack, notes, kcalEstimate etc.) to count sets per muscle group.
Adds a projection that keeps only startTime + exercises.exerciseId +
whole set objects — safe (avoids the malformed-sub-array issue the
earlier narrower projection caused in the stats overview handler),
but still drops the bulky session-level fields.

Also swaps the per-session findIndex() over the weekly bucket array
for direct date-math against the first bucket's Monday, turning
bucket lookup from O(sessions × weeks) into O(sessions).
2026-04-23 15:31:53 +02:00
Alexander 0da3b130e4 fix(fitness/stats): wrap streamed muscle heatmap in {#await}
The $state + $effect pattern I used for the muscle heatmap in
bb0895c didn't propagate the streamed promise into the component's
internal $derived(data.totals) chain — the hover counts stayed at
zero even after the data arrived.

Switch just the heatmap to an {#await} block so it mounts once with
the fully-resolved object. The nutrition card shells, periods, and
shared periods keep their $state pattern because the card templates
read individual fields directly (which gracefully fall through to
the "—" branches while pending) and re-rendering once the value
arrives is fine.

Also drops the two reverted commits for the set-subfield projection
(4d1fed6, fe8d036); those are replaced later with a safer narrowing
that keeps whole set objects.
2026-04-23 15:30:05 +02:00
Alexander bb0895c9b5 perf: stream secondary panels on fitness stats page
Four panel fetches (muscle heatmap, nutrition stats, own periods, shared
periods) are now returned as unawaited promises from load() and resolved
into $state-backed locals on the client via $effect. The load function
keeps awaiting only stats/goal/latest since the main charts, goal
header, and body-part cards depend on them immediately.

Rationale for the $state-backed resolution rather than {#await}: the
user wants the nutrition card shells and the muscle heatmap container
to render their skeleton shape on first paint and only fill in the
numbers once the data arrives. Defaults (`{}`, empty heatmap, `[]`)
match the previous error-fallback shapes so the existing `!= null`
checks inside each card cascade naturally to the "—" branches while
the promise is in flight. No template restructuring beyond dropping
the outer `{#if ns}` (which already hid everything when null).

stats (overview) is intentionally still awaited: it feeds ~30 $derived
chart expressions and wrapping it would mean recreating every Chart.js
instance after the promise settles.
2026-04-23 15:15:29 +02:00
Alexander c912afd46a perf: reuse locals.session from hook in all remaining routes
Extends the previous loader-only sweep across the full tree: every
remaining `await locals.auth()` now falls back through
`locals.session ?? await locals.auth()`, so the hook's cached result
is reused.

68 files, 107 sites touched — loaders, form actions, and API
endpoints across cospend / tasks / fitness / faith / recipe / admin.
hooks.server.ts is intentionally left alone since it's the originating
call that populates locals.session in the first place.
2026-04-23 15:08:10 +02:00
Alexander 800a544190 perf: reuse locals.session from hook instead of re-awaiting locals.auth()
hooks.server.ts already awaits auth() once and stores the result on
locals.session. In-scope loaders (recipe list + filter views, rosary,
prayers, calendar — already done — and fitness stats) were awaiting
locals.auth() a second time per request.

Switched to the existing `locals.session ?? await locals.auth()` pattern
so the hook's result is reused. Also pulls session out of Promise.all
legs since it's now synchronous when the hook ran.

Scope: loaders only — actions, /admin, /edit, /add intentionally skipped.
2026-04-23 15:06:05 +02:00
Alexander dfeeeb5fdf perf: drop all_brief fetch from favorites page
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
2026-04-23 15:03:39 +02:00
Alexander eb3604f9ea perf: drop redundant JSON.parse(JSON.stringify()) in recipe API
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.
2026-04-23 15:00:37 +02:00
Alexander 3b4318206d perf: dynamic-import chart.js in FitnessChart
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
2026-04-23 14:56:19 +02:00
Alexander cf3fe84d95 chore: remove unused /measure-mock route 2026-04-23 14:53:50 +02:00
Alexander abb59f46a6 perf: Lucide subpath imports to split 748 KB icon chunk
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
2026-04-23 14:52:39 +02:00
Alexander ebc59cbf6b fix(fitness): update slug + rename measureSlug → checkinSlug
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
2026-04-23 14:31:41 +02:00
Alexander 934d0d981b fix(fitness/check-in): show "Show more" button in desktop 2-col layout
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.
2026-04-23 14:24:02 +02:00
Alexander 5638913b1d feat(fitness/stats): 4 cm minimum y-axis range on body-part history charts
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.
2026-04-23 14:21:47 +02:00
Alexander 9a15779a44 feat(fitness): rename Measure route to Check-in / Erfassung (NotebookPen icon)
CI / update (push) Successful in 3m47s
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.
2026-04-23 14:12:54 +02:00
Alexander f807a43d58 feat(fitness/stats): body-fat trend chart as Δ from baseline
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%)").
2026-04-23 13:57:47 +02:00
Alexander 8611275bca feat(fitness/body-parts): "Same as last" button + larger Copy L→R pill
- 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").
2026-04-23 13:45:16 +02:00
Alexander 91e1efda6f feat(fitness/measure): consolidate entries by day + richer past-measurements summary
- 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).
2026-04-23 13:35:41 +02:00
Alexander 6d3165f405 feat(fitness/measure): paginate past measurements (SSR 10, "Show more" pulls 20)
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.
2026-04-23 13:18:32 +02:00
Alexander e9ebe492fb chore: clear all svelte-check errors and warnings repo-wide (454 → 0)
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.
2026-04-23 13:12:07 +02:00
Alexander 36058d1b94 chore(fitness): drop unused .totals* CSS from body-parts page
Removes the leftover styles for the running-totals block that was
deleted earlier. Clears 9 Svelte "Unused CSS selector" build warnings.
2026-04-23 11:42:01 +02:00
Alexander 0a188ad4ab fix(fitness): measure page polish — stable steppers, narrow history, body-parts chrome tweaks
CI / update (push) Successful in 4m5s
- 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).
2026-04-23 11:31:25 +02:00
Alexander def176db4d feat(fitness): redesign measure page with muscle-man map, inline edit, and desktop 2-col layout
- 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.
2026-04-23 11:08:41 +02:00
Alexander ae8c699640 feat(fitness): forearm measurement illustration + hips asset refresh
CI / update (push) Successful in 3m56s
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.
2026-04-22 01:35:10 +02:00
Alexander dc1c9b32e9 feat(kalender): highlight + center-scroll selected feast in ring panel
CI / update (push) Successful in 3m49s
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.
2026-04-21 23:39:08 +02:00
Alexander f0ad5b67a5 fix(layout): refresh load() data on tab/app resume
CI / update (push) Successful in 3m51s
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.
2026-04-21 19:45:13 +02:00
Alexander a056618696 fix(fitness): request ACTIVITY_RECOGNITION for cadence
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.
2026-04-21 19:21:25 +02:00
Alexander cf5ac96fc3 feat(fitness): download GPX from history detail
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.
2026-04-21 18:53:19 +02:00
Alexander c99442b54b fix(cospend): avoid localStorage at module init on list page
CI / update (push) Successful in 3m56s
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.
2026-04-21 16:50:54 +02:00
Alexander 5b35c9e63b feat(cospend): edit name and amount in list edit modal
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.
2026-04-21 16:41:21 +02:00
Alexander b66c458a4d fix(cospend): redirect to dash after adding payment
Server action redirected to /cospend which routes to /list. Now
redirects to the dashboard in the current locale's root.
2026-04-21 16:36:29 +02:00
Alexander 2f2fcc2f51 feat(faith): rename rite URL slugs to vetus/novus
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.
2026-04-21 16:32:48 +02:00
Alexander 693db06128 fix(faith): detail page rolls over to next LY past Advent I
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.
2026-04-21 15:54:08 +02:00
Alexander 67700c0e75 fix(faith): resolve bible TSVs relative to module, not CWD
CI / update (push) Successful in 21s
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.
2026-04-21 14:25:40 +02:00
Alexander ae953de5eb chore(deps): drop @romcal/calendar.general-roman, alias to fork bundle
CI / update (push) Successful in 39s
AlexBocken/romcal fork ships the general-roman 1969 bundle internally.
Aliased same as switzerland so single romcal dep covers both.
2026-04-21 13:57:35 +02:00
Alexander 8dbd793acd fix(fitness): include today in exercise kcal projection
CI / update (push) Successful in 3m41s
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.
2026-04-21 12:59:31 +02:00
Alexander 5b7f23b8be feat(fitness): surface period projection on stats page
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.
2026-04-21 12:53:11 +02:00
Alexander 56d438631b feat(fitness): tinted body-part icons with semantic accents
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.
2026-04-21 12:37:46 +02:00
Alexander 5915fd323d fix(fitness): seed body-part inputs from last record
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).
2026-04-21 12:18:33 +02:00
Alexander df36e285ac refactor(fitness): move body-parts overview to stats page
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.
2026-04-21 12:12:28 +02:00
Alexander 7f4b65f009 fix(faith): angelus slot selection sticks after praying
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.
2026-04-21 12:02:17 +02:00