Replace the pokedex grid on /tasks/rewards with a scrapbook "sticker album":
category pages on warm paper, die-cut glossy vinyl stickers (debossed
silhouettes for missing ones), rarity-scaled holo shine/foil/glow, and a
large sticker-themed detail popup on click. Pages sort by rarity (rarest
category + sticker first; "Allerlei" catch-all last); each category has an
info popover explaining how its stickers drop, with the /tasks tag icons.
Restyle the calendar as a cozy fridge wall-calendar (paper, washi tape,
Fredoka month, cats stuck on dates as tilted die-cut stickers, weekend
tint). Shows only the current user by default; tap a name in the monthly
tally to fold in the other household member (per-person colour dots).
Export ALWAYS_CATEGORIES + getTagsForCategory from stickers util.
adapter-node's `precompress: true` brotli-q11 + gzips every file in
build/client single-threaded — ~150 MB including 91 MB of already-
compressed media (zero gain) and tens of MB of server-only data — adding
minutes to the build. The worst offenders were ML embedding JSONs
(nutrition 35 MB + bls 20 MB + shopping ~4 MB) that are `?url`-imported by
$lib/server/{nutritionMatcher,shoppingCategorizer}.ts and read server-side
via SvelteKit's read(); no browser ever fetches them, so compressing them
is pure waste.
- svelte.config.js: precompress: false.
- scripts/precompress.ts: postbuild step that only compresses text asset
types, skips binaries and server-only data (bible TSVs by name, embedding
JSONs by hashed-name pattern), tunes brotli quality down for large files,
runs gzip+brotli in parallel, and never writes a larger-than-original or
duplicate sibling.
- package.json: run precompress after build-error-page with
UV_THREADPOOL_SIZE=12 so the async compression actually parallelizes.
- Delete static/allioli.json: 20 MB, unreferenced anywhere in the repo.
Roll the sticker client-side and render the popup immediately on completion
instead of waiting for the POST roundtrip + DB writes to return it. Preload
the sticker image so the cat is decoded before the bounce-in finishes. The
chosen stickerId is sent to the server, which persists it (falling back to a
server-side roll if missing/invalid).
Recipe image upload failed in prod with ENOENT writing the full image.
Root cause: the deploy built against the dev .env, whose relative
IMAGE_DIR="./imgs/" resolves under the service's dist/ working dir
instead of the real served image directory — and `$env/static/private`
is inlined at build time, so dev values shipped to prod.
- deploy.sh: source .env_prod (overridable via PROD_ENV) into the env
before `pnpm build`, so prod values win over .env for the whole build
lifecycle; abort if it's missing rather than ship a dev-env build.
- .gitignore: ignore .env_* so .env_prod (prod secrets) isn't committed
(the existing .env.* dot pattern didn't match the underscore form).
- imageProcessing: mkdir -p the full/thumb dirs before writing. The
WebP passthrough writes the full image with fs.writeFile, which (unlike
sharp's toFile) does not create parent dirs.
- recipeFormHelpers: add serializableFormValues() and use it in the add/
edit actions' fail() returns. Returning raw formData (now containing the
recipe_image File) crashed the action response with a non-POJO devalue
error, masking the real failure with an opaque 500.
The detail-page enter/exit transition previously slid only the metric
tiles up from the bottom — the wrapper had no background, so its
snapshot was transparent and no containing panel moved. The photo strip
also animated separately, sliding in from the right.
Wrap everything below the hero map (stage nav, photo strip, metrics,
tags, elevation, scroll area, footer) in one `.below-map` element with
`view-transition-name: hike-below-map` and an opaque background, so the
whole sheet — background included — slides up on enter and down on exit
as a single panel. Drop the obsolete hike-strip right-slide rules and
keyframes; rename hike-below-strip → hike-below-map.
Add a from-scratch photo editor (crop, max-resolution scale-to-fit,
WebP quality with live final-size + dimensions readout) that opens on
image pick in the recipe add/edit flow. Conversion uses the browser's
canvas WebP encoder (sharp can't run client-side); crop, scale and the
size readout are built by hand.
Server now stores the client WebP full image byte-for-byte (passthrough)
so the on-disk file matches the user's chosen quality/size; sharp still
derives the 800px thumb and OKLAB colour. Non-WebP uploads keep the old
q90 re-encode fallback.
Rework /add to reuse EditTitleImgParallax (parallax hero +
titleExtras/below-hero layout, shape-tile Backform, SaveFab + optional
translation), replacing the antiquated CardAdd card. Move the edit/remove
image controls into the hero, below the fixed header. Delete now-dead
CardAdd and RecipeEditor.
Spelling and grammar fixes across prayer components (e.g. sanctífica,
peccávi, nequítiam, Hoffnung, Menschen, Geheimnisse), merge cæ ligature
in Credo, restore traditional clause order and close Ps. citation in the
Prayer Before a Crucifix.
OpenTopoMap's hypsometric tint reads "red mountains / green flats" and
looks nothing like the Swisstopo Pixelkarte the proxy hands out
in-region — produced a jarring visual seam right at the CH/LI border.
Thunderforest Outdoors has a muted topo palette + subtle hillshade
that matches the swisstopo tile aesthetic much more closely, so use
it as the abroad `karte` upstream when an API key is available.
- `tile-proxy/build.rs`: reads `tile-proxy/.env` (gitignored) at build
time and forwards each `KEY=VAL` line to rustc as `--env`, so the
key is baked into the binary via `option_env!` and never touched at
runtime. A shell env var of the same name wins over the .env entry
(dotenv precedence). `cargo:rerun-if-changed=.env` +
`cargo:rerun-if-env-changed` force a recompile whenever the value
changes — no stale key cached in the binary.
- `main.rs`: `THUNDERFOREST_API_KEY` read via `option_env!`; foreign
`karte` is Thunderforest Outdoors when set, OpenTopoMap fallback
when absent. Behaviour unchanged for keyless builds.
- `mapTiles.ts`: page-footer attribution credits Thunderforest + OSM
alongside the existing swisstopo / OpenTopoMap / Esri lines so the
attribution stays correct regardless of which build is deployed.
- `.gitignore`: tile-proxy build artefacts (binary, `target/`, `.env`)
moved to the root gitignore with fully-qualified paths so the
source tree isn't hidden by a nested gitignore quirk; the per-dir
`tile-proxy/.gitignore` is removed.
- README + systemd service: documentation refreshed for the new
build-time key flow.
Fires the same strip-slide-out + below-strip-fly-down animation whenever
a hike detail page is left for anywhere other than another slug — not
just on the back-to-/hikes path. Layout adds a new `vt-exit-hike-detail`
class on the document root for that case; the css rules tag onto the
existing `vt-enter-hikes` selectors so both exits drive identical
animations.
- Render the elevation profile as an inline SVG at SSR (filled area
+ 5 ticks per axis + soft horizontal helplines). Chart.js takes
over via a sticky `chartReady` flag once it imports and paints,
fading the SVG out.
- Pre-rendered medium hero now underlays the desktop trail-col map,
cover-cropped and `transform: scale(2.25)`d so the bbox fills the
slot. Fades on first leaflet tile-paint, same handover as the
hero map further up.
- Wrap everything below the photo strip in `.below-strip` so the
view-transition into the detail page can slide the metrics,
tags, charts, scroll-area and footer as a single block.
Three related improvements to the pre-rendered hero map system:
* New medium viewport variant (561–899 CSS px) for the per-hike detail
hero and the /hikes overview. Tablet/split-pane viewports were
getting the wide pose (chosen for ~1920 CSS px), which landed too
zoomed in. Each variant is rendered at a pose matching its
container, so the static→Leaflet handover aligns at every band.
Manifest fields are optional — pages fall back to the wide variant
on tablets until build-hikes regenerates the images.
* Overview frames on Switzerland (fixed center [46.82, 8.23]) with
explicit per-variant zooms (wide=8, medium=8, narrow=7) rather than
auto-fitting the union of hike bboxes. The previous behavior zoomed
in on whichever corner the catalogue clustered in; this reads as
"hikes across CH". Bumps OVERVIEW_RENDER_VERSION so cached overview
images get invalidated on the next build.
* Removed the post-tile-load flyToBounds in both HikeMap.svelte and
HikesOverviewMap.svelte. The map already opens at the static pose
via setView; the second auto-fit was adding a visible wobble on
routes whose bbox sits at an integer-zoom boundary (e.g. the
Einsiedeln–Unteriberg detail), where the build-time fit and
Leaflet's runtime fit disagree by one zoom step at the user's
actual container size.
Cards + filter bar fly up from below when arriving at /hikes and drop
back down when leaving (in both directions of /hikes ↔ detail). Clicked
card morphs into the detail hero with a cross-fade so the thumbnail
dissolves into the map instead of snapping. Photo strip slides in from
the right. Root content cross-fades so metrics + content under the hero
phase in rather than appear at the end of the morph.
Track JSON moves from a client-side $effect into +page.ts so the strip
is in the DOM at view-transition snapshot time — also kills the brief
layout shift when it used to pop in post-load.
Spelling/grammar across the six new hikes; plus content fixes: drop schlittelwanderung's pasted rain/spring lines, monte-generosa's stray 'S-Bahn ab Einsiedeln', rewrite morgartenberg's Anreise/Heimreise to Biberegg/Sattel, and correct Verzascatal's region (Zentralschweiz -> Tessin).
Point routeBrouter at a local BRouter server ($BROUTER_URL, default http://127.0.0.1:17777) instead of the public brouter.de, matching the bundled brouter package + systemd service.
Render ElevationProfile below the route-builder map, fed by the routed segments (elevations fill in as the swisstopo enrichment resolves). Add a $effect in ElevationProfile so its dataset rebuilds when the track changes live during editing.
Replace noisy phone-GPS <ele> in every committed track.gpx with swisstopo swissALTI3D heights at each exact lat/lon (coordinates unchanged; phone altitude was off by up to ~430m).
- scripts/fix-altitudes.ts: batched swisstopo profile.json lookup, WGS84->LV95, disk-cached, keeps original ele for any out-of-CH point.
- .githooks/pre-commit: auto-corrects any added/modified track.gpx on commit and re-stages it; wired via package.json prepare -> core.hooksPath.
Add Walenseewanderung hike (index.svx + track.gpx); fix Anreise text that was copy-pasted from the Siebengipfelwanderung (route is to Amden, not Maschgenkamm).
Build-time image optimization plus auth-gated private content.
- <Image> (src/lib/components/Image.svelte): wraps @sveltejs/enhanced-img
for public images under src/lib/assets/images/ (AVIF/WebP, multiple
widths, lazy by default), plus a `private` mode for auth-gated images.
- Private images: scripts/build-private-images.ts encodes sources from
src/lib/assets/private-images/ into private-assets/ (outside the bundle)
and a manifest; served only via the auth-checked /private-images/
endpoint (X-Accel-Redirect in prod, disk read in dev).
- HikeImage gains a `src` prose mode: build-hikes encodes non-waypoint
images referenced in .svx and exposes them by filename (imagesByName);
a `private` attr routes them through the gated /hikes/<slug>/private/ path.
- <Private> (src/lib/components/Private.svelte): renders prose only to
logged-in viewers (cosmetic gating — text still ships in the bundle).
- deploy.sh rsyncs private-assets/; prod needs an nginx internal
/protected-images/ location.
`/hikes` is `prerender = true` and carries the global nav, so the
prerender crawler followed those links and tried to statically render
the whole dynamic, DB-/ML-backed app. SvelteKit prerenders inside a
heap-capped worker_threads worker, so this exhausted its heap
(ERR_WORKER_OUT_OF_MEMORY) and failed the build.
- svelte.config.js: prerender.crawl = false. The intended static set is
fully described by `prerender = true` (/hikes) + the /errors/[status]
EntryGenerator, so crawling is unneeded. Add a defensive
handleHttpError that ignores /hikes/*/images/* 404s (those binaries
live in hikes-assets/, served by nginx/dev-middleware, not /static).
- hooks.server.ts: skip init when `building` so builds don't connect to
Mongo, start the payment scheduler, or warm the romcal cache.
- hikes/[slug]: set `prerender = false`, enforcing the intent its
comment already stated.
Version 1.86.1 -> 1.86.2.
The metrics flex row stretched each tile to the tall route-icon's height,
so the grid's auto rows spread apart and pushed the value and its label to
opposite halves. Center the items instead of stretching.
Re-base every track + image timestamp so each hike starts at 08:00 on the
build date, preserving all relative timing (total duration, per-stage gaps,
photo "nach X"). The per-hike track JSON is the single source for the page
metrics and the client-built GPX download, so both come out anonymized; the
real recording times stay only in the private source track.gpx.
Also close two stale-data leaks that would otherwise still expose real
times: sweep prior-build track.*.json (keep only the current hash) and
remove orphan slug dirs from static/ (renamed/deleted hikes).
Add scripts/hike-photos.sh (+ pnpm photos:push / photos:pull) to back up and
restore hike source photos over rsync, keeping them out of git. Transfers
only photo files (images/, private/, root cover.*) — matching the .gitignore
rules — so the versioned text (index.svx, track.gpx, icon.svg) is untouched.
Config via REMOTE / HIKE_PHOTOS_DIR with deploy.sh-style defaults; supports
--dry-run and --delete. No build changes: build-hikes still reads local files
and falls back gracefully when a photo is absent.
src/content was untracked. Start versioning these two hikes' writing
(index.svx, track.gpx, icon.svg) and add a .gitignore that keeps source
photos out (images/, private/, root cover.*) — they're large and get
re-encoded into static assets at build time. Includes the proofreading pass
on both texts (typos, grammar, gender/agreement). Other hikes stay untracked.
Replace the search.ch link with an sbb.ch deep link that respects the
searched date/time and departure-vs-arrival. The live SBB timetable (2026)
uses stops=<from>~<to> with each stop as `<label>_I<stationId>`, time with
an underscore (09_10), a short dep/arr token, and a `day` mirror of date —
not the older von/nach + colon/ARRIVAL form. Station IDs (didok) are
captured during resolution (transport.opendata.ch returns SBB's IDs);
fall back to label-only stops when an ID can't be resolved.
Each <HikeImage> overlays a "nach X" badge (elapsed from the first
timestamped track point), matching the photo strip's formatElapsed wording.
Renders only when the photo carries a timestamp; bottom-right, or top-right
when a caption is present so it never overlaps.
A `cover.*` image (jpg/jpeg/png/webp/heic/heif) in a hike's images/ dir
(or hike root) now always becomes the overview-card cover, overriding the
"first public route photo" heuristic. Unlike route photos it needs no
track.gpx waypoint, is always public, and is excluded from the photo strip;
alt text falls back heroAlt → title → "Titelbild". The shared responsive
encoder is extracted from processImage into encodeImageVariant so the cover
reuses it; its outputs join the orphan-cleanup keep-set.
routeWaypoints passed BRouter's coarse, SRTM-based inline elevations
straight through, and the client only queried Swisstopo when a point
lacked an altitude — so snapped routes never hit the Swiss terrain model
and shipped a jagged profile that disagreed with the densified off-trail
path. Overwrite every routed point with enrichElevations (geo.admin.ch),
keeping the router's value only as a fallback where Swisstopo returns null
(hikes abroad). Disk-cached by coordinate list, so repeat snaps stay free.
Add JourneyPlanner.svelte: a "Von / Nach" connections widget to drop into a
hike's prose (e.g. with a fixed trailhead destination) so readers can plan
the trip there by public transport. Backed by the free Swiss transport API
(transport.opendata.ch — the same one sbb-tui uses).
- From/To fields: prefillable + per-field lockable (fromFixed / toFixed),
swap when both editable, and "Aktueller Standort" via geolocation
(coarse accuracy → fast on laptops; resolves to the nearest stop).
- Station typeahead on both fields via /locations?type=station, so routes
start/end at canonical stops instead of fuzzy-geocoded points (which was
yielding roundabout connections); plus resolve-on-search fallback. The
dropdown grows flush from the field as a route-line spur (matching dots),
typed text highlighted.
- Separate date + time controls: date defaults to the next weekend day
(Sat on weekdays, Sun on a Saturday); time + departure/arrival target are
author-settable props (time="08:00" target="arrival") and reader-editable.
- Results: per-leg transport-type icons (bus/train/tram/ship/cable) and an
expandable full itinerary (stations, times, platforms, line + direction,
coloured rails, walk connectors); link to the full search.ch timetable.
Add TimePicker.svelte: a shared pill time picker (HH:MM string, chevron
nudges, hour/minute dropdown) in the same language as DatePicker /
DateTimePicker.
Add tile-proxy/: a small Rust (axum) service behind nginx that serves one
canonical XYZ scheme (/{karte,luftbild,dufour}/{z}/{x}/{y}) and, per tile,
picks the provider by geometry — swisstopo when the tile overlaps a
swisstopo-covered region (Switzerland or Liechtenstein, each simplified +
2 km buffer; tile-bbox ∩ polygon at every zoom), otherwise OpenTopoMap
(schematic) / Esri World Imagery (satellite), with an auto-fallback for
border 404s. Includes the region generator (gen-regions.mjs), a Makefile,
nginx caching-proxy + systemd examples, and a README. Listen address is
env-driven (TILE_PROXY_ADDR).
App side:
- New mapTiles.ts is the single source for the proxy URLs + combined
attribution; HikeMap / HikesOverviewMap / EditMap fetch through
maps.bocken.org instead of swisstopo directly, on-map attribution
controls removed, preconnect + footer credits updated (swisstopo /
OpenStreetMap+OpenTopoMap / Esri).
- Region-aware schematic max zoom (isSwissRegion helper): detail map caps
at z17 abroad and hides the CH/LI-only Dufour layer; overview caps at
z18 when a shown hike is abroad.
- Route-builder: add the satellite layer via the same bottom-right layer
popover as the other maps.
Represent a multi-day hike as separate named GPX <trk> elements, one per
stage, while still treating the whole thing as one route on the overview.
GPX & build:
- gpx.ts: parseGpxStages (one stage per <trk>) + multi-track buildGpx.
- build-hikes.ts: per-stage stats with totals summed across stages so the
overnight gaps (distance, time) and the altitude jump between stages are
excluded; previewBreaks recorded where stages sit >1 km apart.
- types: HikeStage, manifest `stages?` and `previewBreaks?` (both optional —
single-stage hikes are unchanged).
Detail page:
- HikeStageNav: a light itinerary-stepper switcher (numbered nodes, active
glows in the accent) writing a shared stageStore.
- Selecting a stage scopes the metrics, elevation profile (x-window),
map (highlight + zoom, dim the rest) and photo strip/markers; "Alle
Etappen" shows the whole route.
Overview: live map and the prerendered static composite both break the
preview line across >1 km inter-stage transfers (previewBreaks).
Route builder:
- Mark any placed waypoint as a stage start (named) from the waypoint list
or the detail panel; export assembles each stage independently into its
own <trk>; import re-marks stage boundaries from a multi-track GPX.
Work-in-progress route-builder checkpoint:
- New RouteStatsBar and WaypointDetailPanel components.
- EditMap / ImageDropzone / WaypointTable / builderStore updates.
- Hoist the elevation gain/loss/range helpers out of build-hikes.ts into
src/lib/hikes/elevation.ts so the builder and the build share one
implementation.
Also bundled here (same file, couldn't be split cleanly): build-hikes.ts
now detects each hike's country at build time — 'CH' when a Swiss canton
matched, otherwise an OSM/Nominatim reverse-geocode — and writes it to the
manifest, feeding the new Kanton/Land filter.
Add two filters to the /hikes filter panel:
- "Nur Touren in der aktuellen Saison" toggle — keeps only hikes whose
recommended season window covers the current month (year-wrap aware;
hikes without a window count as year-round).
- "Kanton / Land" — a typeahead that abstracts the hike's area over the
border: Swiss hikes group by canton (coat-of-arms), hikes abroad by
country (flag). Generalised the tag typeahead into ChipTypeahead
(optional icon + label mapping) and reused it for both tags and areas.
Supporting bits: countries.ts (ISO/name → flag), hikeArea.ts (the
canton-or-country resolver, namespaced so codes can't collide), prepared
flag SVGs for CH/DE/IT/AT/FR, and an optional `country` field on the hike
manifest type (populated by the build script; the app falls back to canton
for Swiss hikes until a rebuild).
Map interaction:
- Overview map: widen the canvas renderer hit-test (tolerance) so a route
can be hovered/clicked from a comfortable margin instead of demanding a
pixel-perfect click on the thin line.
- Detail map: drive the elevation cursor from a whole-map mousemove that
snaps to the nearest track point within ~70 px (track cached in
layer-point space, refreshed on zoom/move), instead of requiring the
pointer to ride exactly on the trail. The hover pin now renders for
map-sourced hovers too, and is recoloured to nord red as a distinct
"you are here" marker. Trail polyline made non-interactive.
Detail page:
- Move the photo strip above the stats row and trim it (3:2 cards).
- Add a fullscreen lightbox: an expand button on each card opens the
full-res image with prev/next, arrow keys, Esc, backdrop-close and a
body-scroll lock; opening/stepping syncs the map + strip. The card's
existing click (map-position sync) is preserved.
- Cap inline prose images at 680 px (centered) so they don't blow up to
full width in the single-column layout on wider screens; the desktop
two-column layout is unaffected.
Replace the flat always-open filter slab with a collapsed command bar:
a result/totals summary, removable active-filter chips, and a Filter
toggle (with active-count badge). The control panel expands in-flow
below the bar (slide), pushing the card grid down instead of overlaying
it, with breathing room added between the hero map and the bar.
- Range filters are now dual-thumb sliders (lower + upper bound) for
distance, duration, ascent and descent, via a new RangeSlider
component (pointer + keyboard, crossover hand-off). Shared bounds math
lives in filterBounds.ts so slider extents and the page's default
filter state can't drift.
- Difficulty selection renders the actual SAC trail-sign markers
(T1 yellow Wegweiser, T2/T3 white-red-white, T4-T6 white-blue-white),
matching the hike cards; selected signs light up in full colour while
unselected ones are dimmed.
- Tag selection uses a typeahead (input + dropdown + removable chips),
mirroring the recipe filter but themed with the semantic variables.
Trail polyline now uses the same SAC white-red-white red as the /hikes
overview and detail pages so the live preview reads as the final
published track. Dropping geolocated images now reframes the edit map
to the new bounds via a shared `mapView.fitTick` signal — covers GPX
imports too if they ever wire it up.
After a GPX import the image-anchor waypoints carry just their
content hash (no thumbnail). Drop the source JPEGs into the same
image dropzone and they're now hashed and matched against existing
waypoints first — on a hit, the thumbnail + full-resolution cache
are patched onto the matched row instead of creating a duplicate
waypoint marker on the map.
A blue 'Bildvorschau ergänzt' status row + contextual hint at the
top of the dropzone ('N Bild-Wegpunkte warten auf eine Vorschau —
Originale hier ablegen') makes the round-trip workflow discoverable.
Existing fresh-upload behaviour (new image → new chronologically-
inserted waypoint with EXIF GPS) is unchanged for any image whose
hash isn't already known to the builder.
Lets the user re-load a previously-exported GPX and keep iterating
on the same route — add a waypoint, fix a turn, retag an image —
without rebuilding from scratch.
The exported GPX interleaves user-anchor waypoints with densified /
snapped intermediates in a single `<trkseg>`. The importer doesn't
try to perfectly round-trip "manual waypoint vs intermediate";
instead it recovers the *image* anchors by matching `<wpt>`
coordinates against the trkpt sequence (1e-5° tolerance, ≈1 m),
plus the start + end trkpts, and reconstructs routedSegments from
the trkpts between adjacent anchors. The intermediate geometry is
preserved verbatim — no re-routing, no second elevation pass.
Image waypoints carry their `imageHash` + `imageVisibility` across
the round-trip so the build script can still re-attach the source
JPEGs on the next publish. Visual previews from those hashes are
deferred to a follow-up — for now an image anchor renders as a
hash-only badge in the waypoint table.
Auto-snap is forced off after import so the freshly-loaded geometry
isn't immediately overwritten by a routing API call. UI: a "GPX
laden" link-style button next to the existing Reset, confirms
before replacing a non-empty draft.
The pure parsers (`parseGpx`, `parseGpxImageRefs`) move from
`$lib/server/gpx` to `$lib/gpx` so the browser-side importer can
use them; the server module re-exports for back-compat.
Two upstream constraints were causing the densified route-builder
tracks to ship without `<ele>` on most points:
1. api3.geo.admin.ch's elevation services reject `sr=4326` outright
(HTTP 400: "Please provide a valid number for the spatial
reference system model: 21781, 2056"). Add an in-process
WGS84→LV95 converter using Swisstopo's published approximation
(≈1 m positional accuracy, well below the DTM grid resolution)
and switch both `height` and `profile.json` to sr=2056.
2. profile.json GET silently 414s once the URL crosses ~8 KB. At
our densified-track sizes a 200-coord chunk hit ~9.6 KB and got
dropped — that's why only a handful of segments came back with
elevations. Switch to POST + form-encoded body; chunk size can
safely go to 500 coords.
UX: extend the busy state to cover the densify+elevate path
(previously only set during snap-to-route) and add a mode-agnostic
status chip in the header that pulses amber while the elevation
request is in flight. GPX download button is now disabled while
busy so the file can't be exported half-finished.
With "snap to route" off, every waypoint pair shipped as a 2-point
linear segment with no `<ele>` anywhere — the page only enriched
elevations inside the snap path. The resulting GPX had a flat
altitude profile and the build script's gain/loss/min/max metrics
came out all zero.
Two changes:
* New `densifyLinearSegments` (default 25 m, matches Swisstopo's
coarsest DTM) walks every 2-point segment and seeds intermediate
vertices along the great circle. Snapped segments (already many
points from BRouter) are left alone.
* Page reactor now runs the same Swisstopo elevation enrichment in
the autoSnap-off path, so the GPX carries per-trkpt `<ele>` even
for fully manual / cross-country routes.
Elevation source unchanged: Swisstopo profile.json (COMB → DTM2 →
DTM25 fallback) is already the highest-resolution provider for our
Swiss-coverage hikes; no point swapping.
Also unifies the snap-path's inline enrichment call into the same
helper so there's one elevation code path instead of two.
Live and static heroes on the detail page were rendered in Nord red,
while the same trail on the /hikes overview map already used the
high-contrast SAC palette (orange/red/blue). The mismatch made the
detail trail look muted against the Pixelkarte; the overview's choice
also doubles as a navigational hint ("the orange trail you saw on
the map is this one").
Introduce $lib/data/sacColors as the single source of truth so
HikeMap, HikesOverviewMap, and the build-side static renderer all
pull the same palette. Bump HERO_RENDER_VERSION to v6 so stale
Nord-red static heroes get re-rendered on the next build.
Two bugs:
* Toggling a tag in the filter bar didn't update `?tag=` in the URL,
so the page wasn't shareable / back-button-restorable past the
initial deep-link state. Add a writer $effect that mirrors
`filter.tags` into the URL via `replaceState` (no history churn).
* The overview map's polylines + camera were built once on mount and
never refreshed when the `hikes` prop changed, so filtering left
the map showing the full set and zoomed for it. Extract polyline
rendering into a function called both on mount and from a
prop-watching $effect; on change, smoothly fly to the new union.
Detail-page tag chips become anchor links to `/hikes?tag=<name>`.
HikesFilterBar grows a tags fieldset (sorted by frequency, with the
hash prefix the chips use) so the user can keep narrowing from there.
Multi-tag filtering is OR — a hike matching any selected tag stays
visible. AND would shrink the listing fast given how few tags most
hikes carry; OR matches what "show me more like this" feels like.
The overview page reads `tag` query params on mount and pre-fills the
filter — supports repeated params (`?tag=winter&tag=easy`).
Tag chips slot between the metric tiles and the elevation profile —
muted pills with a hash prefix so they read as framing context, not
another row of glance-info competing with the numbers.
The standalone "GPX herunterladen" pill (centred, primary-styled,
above the photo strip) moves into a small footer at the bottom of
the article, grouped with waypoint count, publish date, and the
required swisstopo attribution. GPX is a power-user export, not a
CTA — placing it with other ancillary metadata matches its weight.
26 public-domain coats of arms fetched once from Wikimedia Commons
via scripts/download-cantons.ts and committed under static/cantons/.
$lib/data/cantons.ts maps Swisstopo's free-form name (German default,
French/Italian alternates for Romandie / Ticino) to the ISO code +
emblem URL.
Card shows an 18×22 emblem, detail page a 24×30 one — both with a
drop-shadow so they read against the dark hero gradient. Unknown
canton names fall back to plain text without the emblem.
The downloaded SVGs are written verbatim — earlier draft prepended a
provenance HTML comment but that breaks the leading `<?xml … ?>` and
browsers refuse to render the image. Provenance lives in the script's
CANTONS table instead.
The wide static hero picks its zoom for a desktop-sized container
(fitWidth 1920), so on phones the bbox lands too zoomed-in: most of
the route falls outside the visible 400 CSS px band.
Build now emits a second pose per hero — rendered with fitWidth 400 /
fitHeight 480 onto a 1200² canvas — so the auto-fit zoom matches what
Leaflet picks at the same container size. Per-hike hero gains four
variants total (theme × viewport); overview hero gains two.
The page picks which `<img>` to show via a `max-width: 560px` media
query (no JS needed for the swap), and `matchMedia` decides which
pose to hand to Leaflet's first `setView` so the static→live cross-
fade aligns regardless of viewport.
Drive-by: replace the long-stale `hike.heroMapUrl` reference in the
detail page's track-loading fallback with `hike.heroMapUrlLight`.
Mirrors the per-hike detail-page hero on the /hikes index. Build emits one
WebP at the union bbox of every visible hike with each preview polyline
drawn in its SAC-tier colour; page renders it under the live Leaflet map
and fades it out once the first tile batch loads.
Tile fetcher now distinguishes HTTP 4xx ("intentionally blank — outside
Switzerland") from real network errors, so the larger overview canvas
that extends into DE/IT/FR doesn't trip the network-failure abort.
Each hike now ships two SSR-friendly hero images (light + dark theme),
composited at build time from Swisstopo tiles plus an SVG overlay of the
trail polyline, start/end markers, and per-photo camera badges. The
detail page renders the right variant immediately at first paint, then
hands over to live Leaflet without visible jumps.
Renderer (scripts/staticHikeMap.ts):
- Parallel tile fetcher with on-disk cache (scripts/.cache/swisstopo-
tiles/) for re-build idempotency.
- `computeStaticMapPose` picks the zoom + centre Leaflet's fitBounds
would land on at a reference 1920x640 viewport, so the static frames
the full route on every typical desktop hero.
- Canvas rendered at 3840x2400 — large enough to fully cover ultrawide
/ 4K displays at native pixel size, so `object-fit: none` keeps the
trail pixel-aligned with Leaflet's tile pane.
- SVG overlay: trail in Nord red, start dot Nord green, end dot Nord
red, Lucide `camera` icon inside each photo badge. Photo badge
fill / border / icon-stroke colours are passed per theme so light and
dark variants match the live `.hike-photo-marker .badge` styling
exactly (Nord10/Nord8 fill, Nord6/Nord1 border, white/Nord0 icon
stroke). Map tiles themselves are identical across themes — no naive
invert (it mangles the Pixelkarte palette).
- Public photo markers only — private positions are filtered out so
they don't leak in the SSR image.
Build wiring (scripts/build-hikes.ts):
- `processHero` renders both variants in parallel, hashes inputs per
theme, skips on cache hit. Output filenames carry the content hash so
changes invalidate cleanly via the existing orphan sweep.
- `HikeManifestEntry` gains `heroMapUrlLight`, `heroMapUrlDark`,
`heroMapZoom`, `heroMapCenter`.
Detail page (src/routes/hikes/[slug]/+page.svelte):
- Reserves the hero box height up front (kills CLS).
- Renders both `<img>` tags; CSS picks the right one via `data-theme`
with `prefers-color-scheme` as the fallback.
- `object-fit: none; object-position: center` so the image displays at
native pixel size, perfectly aligned with Leaflet's tile rendering.
- `isolation: isolate` on the hero gives Leaflet's z-index:200+ panes
a stacking context so they can't bleed over the sticky nav.
HikeMap (src/lib/components/hikes/HikeMap.svelte):
- New `initialCenter` / `initialZoom` props — when set, the map opens
with `setView` at the static hero's pose instead of `fitBounds`.
- New `onReady` callback — fires after the post-fly-to-bounds tile
batch finishes loading (or a 350 ms safety timeout), letting the
detail page fade the static out onto fully-painted tiles instead of
onto a brief grey gap.
- Sequence: render static -> Leaflet `setView` to match -> first tile
load -> `flyToBounds(track)` to the natural fit -> wait for new
tiles -> fade static out.
Build pipeline (scripts/build-hikes.ts) parses per-hike GPX, encodes
images via sharp, reverse-geocodes the centroid against Swisstopo and
emits a typed manifest under src/lib/data/hikes.generated.ts (gitignored).
Track JSON + image binaries live outside /static; served in dev by a
small hike-images plugin in vite.config.ts, in prod by nginx (private/
images proxied through Node + X-Accel-Redirect for auth-gating).
/hikes overview: full-bleed Swisstopo hero map (HikesOverviewMap) sits
under the sticky nav, drawing one polyline per route coloured by SAC
tier (T1 yellow Wegweiser, T2/T3 white-red-white, T4-T6 white-blue-
white). Click navigates, hover thickens + tooltips. Layer toggle,
recenter, GPS controls mirror the detail map (minus images toggle).
Cards drop the trail SVG, gain a per-route icon + SAC marker
pictogram on the cover, altitude range, season label, and "Neu" badge
for recently-published hikes. Filter bar + totals strip recompute over
the currently-visible set.
/hikes/[slug]: hero map with elevation profile, photo strip with map
sync, scroll-position pin, GPX download, SAC marker stats + min/max
altitude + season.
Route-builder (/hikes/route-builder): client-side draft persisted to
localStorage, EXIF-driven image placement, snap-to-route via BRouter
(OSRM + linear fallback) and Swisstopo profile.json elevation
enrichment that handles degenerate same-coord segments via the height
endpoint.
Filter init switched from a script-time snapshot of data.hikes (which
sporadically returned a one-hike subset during dev hydration and
locked the page to that single hike) to a post-mount \$effect.
Content under src/content/hikes/ intentionally not included (WIP).
Reapplies the e87b8bd8 mirror-finish behaviour (saved session is
broadcast on Finish so other devices can render the completion overview)
behind a _finishingLocally guard. workout.finish() flips workout.active
synchronously, but completionData is only set after the awaited POST
resolves — without the guard, the redirect effect fired in that gap and
navigated away before the summary could render on desktop.
Last set of an exercise auto-advances to the next unfinished exercise.
View Transitions API animates the swap: vertical slide on the exercise
name and set-counter row, horizontal slide on the set table at >=900px.
Three Web Audio cues — per-set tick, exercise-complete two-note rise,
and workout-complete fanfare on the completion overview.
Switches the active-workout finish button to "FINISH EARLY" /
"VORZEITIG BEENDEN" with an orange tint when any set in the workout is
still incomplete, so users can tell at a glance whether they're
wrapping up cleanly or cutting it short.
Other devices were left on a blank active-workout page when one device
hit Finish. The finish broadcast now carries the saved session document,
and the receiving page builds the completion overview from it. If the
remote end-of-workout has no session payload (cancel, or the finishing
device couldn't post the session), receivers redirect to the workout
home instead of stranding on a blank page.
Rest timer was inlined as a row inside SetTable, tied to a specific set.
Switching to another exercise hid it from view. Now lives below the
exercise focus card, renders whenever a rest is active regardless of
focused exercise, and labels which set/exercise it belongs to when
looking at a different one.
In PWA / Tauri shells with a notch or status bar, the header bar was
stacking 12px on top of the safe-area inset, leaving the bar far below
the system chrome. Use max(12px, inset + 4px) so the constant gap only
applies when there is no inset; otherwise the bar sits 4px below the
inset edge.
Two-hill silhouette (Christmas + Easter peaks) rendered horizontally
with season fills, feast dots riding the curve, dividers between
adjacent same-color seasons, and chevron prev/next year navigation.
Breaks out of the page content width to span the viewport, scrolls
horizontally below 1300px with the selected/today feast auto-centered
on mount and viewport resize.
Floor fertile/peak windows at the prior period's end + 1 day so a
short cycle + long period combo can't predict peak fertility starting
during or right after bleeding. Future cycles also widen the outer
fertile range using observed shortest/longest cycle (Ogino-style),
keeping the peak band narrow around the mean ovulation estimate.
Precache:
- Service worker now precaches /fitness/workout(/active) shells (DE+EN)
on install so a fresh device can log workouts offline without first
visiting /fitness online.
- Layout-level precache adds /fitness/__data.json itself plus the
static sub-routes nutrition/meals and check-in/body-parts (DE+EN).
Unsynced workouts on history:
- History page reads the offline outbox via getQueuedSessions on mount
and merges queued sessions into the displayed list, sorted by
startTime. Duration is computed locally so the Clock stat still
shows.
- SessionCard gains an 'unsynced' prop: renders as a non-clickable
div with an orange-accent border and a CloudOff badge labelled
'Unsynced' / 'Nicht synchronisiert'.
- On window 'online', the page waits briefly for the layout's
flushQueue to drain the outbox, then re-reads the queue and calls
invalidateAll to swap unsynced placeholders for the now-saved
server sessions.
These two pages only fell back to IndexedDB when navigator.onLine was
false. On mobile the device often reports online while the origin is
flaky (502 / slow cellular / cached shell with stale connectivity), so
the API call returned nothing and the pages rendered 0 recipes. Now
both also fall back to IndexedDB when the API attempt yields an empty
list, matching the pattern already used by [name]/+page.ts and
icon/[icon]/+page.ts.
Service worker previously only fell back to cache when fetch threw
(network unreachable). A 502/503/504 from the origin returned
successfully with !response.ok, so the bad page was passed through to
the user. Now upstream 5xx is treated like a network failure: try
cached page, then offline-shell redirect for recipe routes, then the
styled offline page. 4xx still passes through unchanged.
Force showLatin=false in Prayer wrapper when hasLatin is false so spacing
and red rubric icons stay correct regardless of toggle state. Also hide
the Latin toggle on individual non-bilingual prayer routes.
New shared StreakInfoButton component — small (i) pip in the corner
of the rosary, Angelus, and Regina Cæli streak counters that opens a
modal with a short reflection on what the counter is for.
The text frames the streak as a tool for forming the *habit* of
regular prayer, not as a metric of piety; warns against mechanical
repetition with Mt 6:7 ("do not heap up empty phrases"); and grounds
the rest in CCC 2698 (rhythms of prayer), 2700 (heart present to him
to whom we are speaking), 2702 (body+spirit, habit forms us), and
2728 (the wounded pride that comes from treating prayer as personal
accomplishment).
Available in DE/EN/LA. Modal dismissable via X, click-outside, or
Escape; honours prefers-color-scheme.
Refactoring:
- StreakCounter and AngelusStreakCounter both render
<StreakInfoButton {lang} /> instead of duplicating the pip+modal.
Parents just declare position:relative as the anchor.
- AngelusStreakCounter is also used for Regina Cæli, so eastertide
visitors get the same explanation there for free.
Bump 1.67.0 -> 1.67.1.
Six new prayers from the German prayer book images, with Latin and
English where canonical versions exist:
43 Jungfrau, Mutter Gottes mein — German Marian devotional. No
canonical Latin/English; best-effort EN translation provided so
the EN route renders rather than blanking. bilingue: false.
40 O meine Gebieterin / O Domina Mea / O My Queen — Marian act of
self-dedication with the standard Latin and English forms.
41 Memorare — attributed to St. Bernard. Standard Latin
(Memento O piissima Virgo Maria) and the traditional English
translation.
42 Hilf, Maria, es ist Zeit — German Marian invocation. No
canonical Latin; EN follows the user-provided translation
("Help, Blessed Mother, it is highest time…"). bilingue: false.
37 Tischgebet vor dem Essen — composite of four sub-prayers:
"O Gott von dem wir alles haben" (DE rhyme, EN best-effort),
"Komm Herr Jesus sei unser Gast" (DE rhyme + standard EN
"Come Lord Jesus be our guest"), Psalm 144:15-16 with Gloria
Patri (full Latin/EN), and Benedic Domine (full Latin/EN).
38 Tischgebet nach dem Essen — three sub-prayers: "Dir sei o Gott"
(DE rhyme + best-effort EN), Agimus tibi gratias (full Latin/EN),
and Retribuere (full Latin/EN).
New 'meal' prayer category (Tischgebete / Meal / Mensae) added to the
filter pills on the prayers index. Replaces the long-standing TODO
that gated meal prayers on the existence of a category for them.
Wiring:
- prayerSlugs.ts gets DE+EN slugs for each prayer (validates
/faith/prayers/<slug> URLs and feeds the offline sync precache list
+ sitemap).
- de/en/la i18n files get six new prayer-name keys plus category_meal.
- [prayer]/+page.svelte: imports, prayerDefs entries, render block.
- [prayers]/+page.svelte: imports, labels, categories array (new meal
pill), prayerCategories, prayers list with searchTerms, getPrayerName
map, prayerMeta, render block.
Eastertide seasonal badge on the prayers index (Regina Cæli card)
moved from a bottom inline-block to absolute top-right, matching the
placement of the same badge on the rosary mystery cards. Adds a
position:relative anchor on the gebet_wrapper.
Bump 1.66.0 -> 1.67.0.
Move OfflineSyncIndicator (logo pip) and OfflineSyncBanner from the
[recipeLang] layout/page to (main)/+layout.svelte and (main)/+page.svelte.
Sync is an app-wide concern, not recipe-specific, and surfacing it on the
homepage gives the entry point users actually see when they install the
PWA. Indicator pulls language from languageStore since (main) doesn't
have data.lang from a recipe-scoped load.
Drop the now-unused .banner-wrap CSS and OfflineSyncIndicator/Banner
imports from the recipe routes.
Auto-sync cadence:
- AUTO_SYNC_INTERVAL 30 min -> 1 week. Recipes don't change often enough
to justify a half-hourly background download (the user explicitly
wanted this dialed back).
- Internal poll tick 5 min -> 1 hour. Polling 12x an hour for a weekly
event is wasted work; hourly is fine and still responsive when the
weekly window opens.
Bump 1.65.3 -> 1.66.0.
MysterySelector: Tempus Paschale badge on the rosary mystery card now
hardcodes white background + dark text, was rendering as muted grey
in dark mode via --color-bg-elevated. Liturgical white doesn't change
between themes anyway.
Faith layout nav: when eastertide is active and the Angelus link is
swapped to Regína Cæli, add a small pulsating white dot in the link's
top-right corner — same pattern as the recipe header sync indicator,
just white (Tempus Paschale color) and slow-breathing (4s). Dark mode
gets a bright white halo; light mode gets a dark drop shadow so the
white pip stays visible against the light nav bar. Honors
prefers-reduced-motion.
Bump 1.65.2 -> 1.65.3.
Service worker:
- Strip version suffix from CACHE_PAGES and CACHE_IMAGES so cached pages
and recipe thumbs survive SW updates. Each deploy used to wipe both,
forcing a re-sync before the user could open the app offline. Build
and static caches stay version-suffixed (entries are hash-fingerprinted).
- Install precaches the offline shells: /, /rezepte, /recipes, both
offline-shell pages, /glaube, /faith, /fitness — best-effort with
Promise.allSettled so a single 5xx can't fail SW install.
- Bulk thumbnail precache now skips URLs already in CACHE_IMAGES.
Recipe thumb URLs embed a content hash, so a cache hit guarantees
identical bytes; subsequent syncs after small recipe edits no longer
redownload every image.
- Activate cleanup deletes only stale versioned build/static entries
and obsolete versioned pages/images caches.
Universal load migration:
- [recipeLang]/+layout.server.ts removed; logic in universal +layout.ts.
Session fetched from /auth/session, nulled when offline.
- [recipeLang]/+page.server.ts and season/[month]/+page.server.ts
removed; merged into universal +page.ts. Drops the __data.json
round-trip entirely for these routes — IndexedDB fallback now runs
even when the SW page cache is empty (fresh install, hash mismatch,
etc.) instead of getting blocked by a 503 from the data handler.
Other:
- /static/rezepte/thumb URLs in sync.ts and the SW thumb fallback now
use the absolute https://bocken.org origin. Dev/preview servers don't
host /static/rezepte and were 404ing on themselves; production keys
resolve to the same string so existing caches stay valid.
- Root +layout.svelte invalidateAll() now bails when !navigator.onLine.
Resume-while-offline used to refetch every load() and surface the
error page instead of the still-viewable cached content.
Bump 1.64.2 -> 1.65.2.
Collapses /errors/<n>.html and /errors/en/<n>.html into a single
prerendered page that shows both languages and reveals the right one
via <html data-lang>. Build script injects an inline bootstrap that
sets data-lang from localStorage before paint and wires the lang +
theme buttons (no Svelte hydration).
Layout already sets Cache-Control; SvelteKit throws when the same
header is set twice in the load chain, 500ing /glaube/apologetik/pro
and the parallel routes.
Add X-Robots-Tag noindex,nofollow handler in hooks.server.ts for /api,
/login, /logout, /register, /settings, /tasks, /fitness, /cospend,
/expenses, and the recipe admin/edit/add/search/favorites/to-try paths.
Header-based so the rule lives in one place and covers JSON responses.
Recipe detail pages now emit a self-canonical pointing at the bare slug —
the layout helper deliberately skipped detail pages, leaving query-param
variants (?multiplier=2, ?utm=…) as duplicate URLs in Google's index.
Per-page Seo on list pages so each ranks for its category-level query:
- Apologetik contra/pro indices now use localized heading + lede instead
of hardcoded English descriptions
- Calendar month view title includes month + rite ("April 2026 ·
Liturgical Calendar (Vetus Ordo) — Bocken")
- Recipe /category, /tag, /icon, /season hub + detail pages get
descriptions via new *_meta_description and *_meta_prefix i18n keys
(added in both DE and EN locales)
Sitemap now declares the image:image namespace and emits an entry per recipe
photo (loc, title from recipe name, caption from alt text) — Google Image
Search can discover all recipe images directly instead of relying on crawl.
Pro arg pages get Article JSON-LD (headline, claim as description, layer as
articleSection, voice names as keywords, deduped voice cites as citations)
plus BreadcrumbList. Katechese/zehn-gebote gets inline Article + Breadcrumb
with a Thing reference to "Dekalog" and CreativeWork citation of P. Martin
Ramm FSSP's Glaubenskurs.
Static-content load functions now set Cache-Control:
public,max-age=300,s-maxage=3600,stale-while-revalidate=86400 — applied to
apologetik layout, contra index/arg, pro index/arg, and the new katechese
+page.ts files. Recipe detail uses s-maxage=1800. Picked HTTP caching over
SvelteKit prerender to avoid baking session=null into the navbar of routes
shared with the auth-aware faithLang layout.
Set <html lang> from URL prefix via handle hook (was hardcoded "en" despite
mostly German content). Add Person + WebSite + SearchAction graph to root
layout — enables Google sitelinks search box and clusters identity across
git.bocken.org and github.com/AlexBocken via sameAs.
Build apologetikJsonLd.ts: contra args now emit QAPage with one suggestedAnswer
per voiced archetype, citations as CreativeWork. Build breadcrumbJsonLd.ts and
wire BreadcrumbList into recipe detail, contra args, prayer detail, and
calendar day. Calendar day also emits Event schema.
Sitemap now reads recipes directly from MongoDB to populate <lastmod> from
dateModified; static URLs use server-startup ISO date. English recipe URLs
only emitted when translation status is approved.
Add sitemap.xml route enumerating recipes, apologetik args, prayers, and
faith hubs. Drop /static/ from robots.txt — was blocking JSON-LD recipe
images from Google. Add reusable Seo component (OG/Twitter/canonical) and
wire into homepage, faith hub, recipes hub, and apologetik index.
Faith and recipe layouts now emit canonical + hreflang automatically by
swapping known lang slugs; deeper paths whose inner segments aren't safely
translatable (recipe [name], prayer [prayer], apologetik [argId]) are
skipped at the layout and may opt-in per page.
Recipe JSON-LD HowToStep names and baking instructions now resolve via
the recipes i18n table (jsonld_step / jsonld_bake / jsonld_for_duration +
existing at_temp) instead of being hardcoded German — English /recipes/
pages were emitting "Schritt N" in their schema.
Adds prerendered, JS-less, self-contained error pages for nginx
error_page use — served directly from /var/www/errors/ when the
SvelteKit upstream is unreachable or any nginx-originated 4xx/5xx
fires (including the catch-all default_server for unknown hosts).
- /errors/[status] (DE default) + /errors/en/[status] (EN), each
with a header language toggle linking absolute to bocken.org so
the switch works even on unknown-host fallbacks.
- httpStatus param matcher restricts entries to 401/403/404/500/
502/503/504; entries() drives prerender output.
- generate-error-quotes.ts looks up curated bilingual references
in the existing allioli/drb TSV bibles at prebuild time and
writes src/lib/data/errorQuotes.json.
- build-error-page.ts (postbuild) inlines all CSS, strips module
preloads/scripts, rewrites the home-link to canonical https URL,
and emits .html + .gz + .br per status under build/client/errors.
- deploy.sh syncs build/client/errors → /var/www/errors with
http:http ownership for nginx access.
Previous behavior silently deleted static/shopping/*.svg when
SHOPPING_*_NUMBER env vars were unset, then rsync --delete propagated
the deletion to the prod server — the loyalty buttons disappeared on
deploys where the env didn't reach the build (or during the brief
rm→write window of a parallel run).
Now the script exits non-zero with a clear message; deploy.sh's set -e
aborts before any destructive sync.
Replace season: number[] (months 1-12) on Recipe with seasonRanges, a
list of date ranges where each endpoint is either a fixed MM-DD or a
movable liturgical anchor (Easter, Ash Wednesday, Palm Sunday,
Pentecost, Advent I) plus a day offset. The old month list couldn't
express liturgical seasons whose boundaries shift each year (Advent,
Lent, Easter Octave, Christmas Octave) nor sub-month windows.
The shared evaluator resolves anchors against [Y-1, Y, Y+1] so spans
that wrap the calendar year boundary (e.g. christmas + 0 to
christmas + 7) match correctly on both sides. SeasonSelect was
rewritten as a controlled bind:ranges editor with a
fixed/liturgical kind toggle, anchor + offset inputs, per-row
resolved-this-year preview, and preset chips.
Run the one-time migration before deploying:
pnpm exec vite-node scripts/migrate-season-to-ranges.ts
It coalesces contiguous month runs into single fixed ranges and
merges Dec/Jan wrap into one wrapping range; the new code does not
read the legacy season field, so order matters.
The answer-rail had width:max-content with a wider max-width override
gated behind @media (min-width:760px). The width:max-content sat
outside the media query, so on mobile it inflated the rail's max-content
contribution to .arg-body's 1fr grid track. The track expanded past the
viewport (max-width:100% can't clamp during cyclic track sizing), making
.arg-body and its h2/text appear to overflow horizontally.
Move width:max-content inside the >=760px block so the desktop break-out
behaviour stays, while mobile falls back to default flex-wrap within
the column.
Split the single OfflineSyncButton into two surfaces with distinct
intents:
- OfflineSyncBanner: dismissable promo on the recipe index that
encourages first-time download (only when standalone + not yet
synced).
- OfflineSyncIndicator: small status pip overlaid on the nav logo
when offline data is available, opening a popover with sync /
clear actions.
Also fold the sync / clear actions into the UserHeader options menu so
the avatar dropdown is the canonical place to manage offline data.
Header.svelte gains a `logo_overlay` snippet slot to host the
indicator pip.
Other:
- manifest.json: prefer the theme-aware SVG as the primary install
icon and drop the redundant 512px raster (kept maskable 192px).
- scripts/deploy.sh: build locally and rsync artifacts to the
server, avoiding any pnpm/git work on the production host.
Bump 1.57.8 -> 1.58.0.
`navigator.serviceWorker.ready` resolves only when a SW is registered
and active. In `vite dev` no SW exists, so awaiting `ready` hangs the
sync forever. Gate on `navigator.serviceWorker.controller` first to
short-circuit cleanly when nothing controls the page.
Bump 1.57.7 -> 1.57.8.
Split the logo into foreground/background layers so Android can
apply system masks (circle, squircle, teardrop) and parallax
instead of rendering a flat composited PNG.
- icons/logo_{foreground,background}.png: new canonical sources
- mipmap-*/ic_launcher_{foreground,background}.png: regenerated
per density (108/162/216/324/432)
- mipmap-*/ic_launcher{,_round}.png: legacy pre-API-26 composites
- mipmap-anydpi-v26/ic_launcher.xml: background now points at
@mipmap/ic_launcher_background instead of solid white @color
- mipmap-anydpi-v26/ic_launcher_round.xml: added so round
launchers also get adaptive treatment
- drop unused @color/ic_launcher_background and the leftover
Tauri-template drawable
Tauri app: 0.5.2 -> 0.5.3.
Replaced Tauri icon source (icon.png) with new 1024px wheat-stalk
mark on dark background. Regenerated all platform variants via
`tauri icon`: macOS .icns, Windows .ico, Linux PNGs, iOS AppIcon
set, Android adaptive icon foregrounds.
Web: PWA manifest icons (192/512) and apple-touch-icon now use the
new logo. Browser-tab favicon (favicon.svg) unchanged — keeps the
theme-aware wheat mark.
Tauri app: 0.5.1 → 0.5.2.
favicon.svg now uses currentColor + prefers-color-scheme so a single
asset adapts to light/dark. Removed unused .ico/.png and 512 raster;
192 PNG kept and regenerated for apple-touch-icon and PWA maskable.
The previous wrap-in-anchor approach leaked the bocken.org link onto
unrelated page titles because Jellyfin reuses the same <h3> across
navigations and only swaps its class between .pageTitleWithDefaultLogo
and .pageTitle. Switch to click delegation so the redirect fires only
when the clicked element currently carries the logo class. Also unwrap
any legacy anchor wrappers on first mutation, and bump cursor/filter
hover styles to !important so they survive Jellyfin's own h3 rules.
Make the custom-multiplier pill behave and look like a single input zone:
- Wrapper is now a <label> so clicking anywhere focuses the input.
- Replace the explicit \"x\" submit button with a passive <span> suffix and
add a visually-hidden first-tree-order submit so no-JS Enter still
submits with the typed value (rather than the first preset pill's value).
- Wrapper cursor: text end-to-end, no pointer flicker.
- Hover/focus selector now matches the wrapper alongside the preset
buttons, and an isCustomMultiplier flag highlights the pill in primary
whenever a non-preset value is active (e.g. ?multiplier=12).
- Input uses field-sizing: content (with min/max) so the pill collapses
to fit the placeholder.
- align-items: center (was baseline) so the input doesn't sit high in
its pill.
- Tighten the multipliers row (gap 0.5rem -> 0.3rem, button min-width
2em -> 1.8em, matching paddings) so all six pills fit on one line in
the ingredients column.
Cake-form scaling no longer overwrites the base multiplier (pill buttons
+ custom input). Both factors stay independent and compose as
effectiveMultiplier = multiplier * formMultiplier, which feeds ingredient
amounts, portions, nested-recipe links, HefeSwapper, and NutritionSummary.
Pills reflect the base only; the existing cake-form badge keeps showing
the form factor whenever it deviates from 1, so the two contributions
stay visually distinct. Drop the formDriven flag, the effect that wrote
formMultiplier into multiplier, and the now-redundant
oninput=applyFormMultiplier hooks (bind:value already triggers
recomputation). resetCakeForm only resets form fields now.
Add a per-locale common dictionary at src/lib/i18n/common/{de,en}.ts and
the shim src/lib/js/commonI18n.ts. Migrate inline lang ternaries on the
homepage (welcome/sections/links), OfflineSyncButton (all label
ternaries), DatePicker (today/select date), ErrorView (Error/Fehler
eyebrow), and UserHeader (login aria/title) to use the shared dict.
The long marketing intro paragraphs on the homepage stay inline since
they're one-shot content with no drift risk and don't benefit from
per-key extraction.
Bump site version to 1.57.0 (new namespace).
Replace lang === 'en' string ternaries on the check-in, stats, workout,
exercises, history, and stats history detail pages, plus TemplateCard,
with t.<key> lookups against the fitness dictionary. Added new keys for
toast messages, body-part counts, body-fat label, clear/measure short
labels, "edit all fields", BF chart delta prefix, calorie balance and
adherence tooltips, actual/target legend labels, daily expenditure
prefix, height/birth/weight setup hint, exercise/workout/recent labels,
"starts with", and a {n}-template "X days ago" string.
URL slug ternaries (e.g. 'check-in' / 'erfassung') remain inline since
they encode route data, not UI text.
Bump site version to 1.56.2.
Migrate FavoritesFilter, IconFilter, TagFilter, FilterPanel, HefeSwapper
and the offline-shell, season/[month], icon/[icon], favorites, search,
tips-and-tricks, and index pages to use the recipes i18n dictionary.
Add corresponding keys for filter toggles, filter placeholders, yeast
toggle title, recipes-growing suffix, search "for" preposition, and
favorites count labels. Strip unused isEnglish derivations from layout,
tag, and category landing pages.
Bump site version to 1.56.1.
Bulk migration of the recipes namespace following the same pattern as
fitness/cospend/calendar/faith. Layout collapses its label-object into
t.foo lookups; NutritionSummary's 33 ternaries (incl. the
German-stem-plus-optional-e amino-acid pattern that read
`Lysin{isEnglish ? 'e' : ''}`) become straight dictionary references;
AddToFoodLogButton, IngredientsPage, to-try, search, favorites,
the index, and the small landing pages (category, tag, season, icon,
tips-and-tricks) all migrate the same way.
The recipes dict is now ~120 keys. Patterns kept intentionally:
- Long page-specific marketing copy (subheading sentences, meta
descriptions that include dynamic counts, hero alt text variants)
stays inline as `lang === 'en' ? '...' : '...'` rather than
bloating the dict with one-shot strings.
- URL slug ternaries stay inline — those are URL data, not UI text.
- The `recipes/admin/nutrition` page was deliberately skipped — admin
tooling, ~18 ternaries that are mostly admin-jargon strings used
in exactly one place.
Detail pages ([name]/+page, [name]/+error, IngredientsPage extras,
InstructionsPage, smaller components) and the admin page remain for
follow-up commits.
Two-locale recipes dictionary lands at src/lib/i18n/recipes/{de,en}.ts
with the same satisfies-based completeness enforcement as the other
namespaces. recipesI18n.ts is the slim shim — exports m, RecipesLang,
RecipesKey, plus langFromRecipeSlug / recipeSlugFromLang helpers for
the rezepte ↔ recipes URL slug mapping.
[recipeLang]/+layout.svelte's nav-label ternary chain collapses into
t.foo lookups. NutritionSummary.svelte is the heavy hitter — 33
inline isEnglish ternaries become a single dictionary load. Most
amino-acid names use a German-stem-plus-optional-e pattern in the old
code (`Lysin{isEnglish ? 'e' : ''}`) that's now just t.lysine in the
template; less clever, much more obviously translatable.
Adds prayer-name keys (sign_of_cross, pater_noster, fatima_prayer, …),
search/filter UI labels (search_prayers, clear_search, filter_by_category,
all_categories), the eastertide_badge, and the prayer-detail-only
nicene_creed / hail_mary aliases (German + Latin keep the Latin form,
English uses the English name).
Prayers index labels object collapses each name ternary into a t.foo
lookup; the language-invariant ones (Glória Patri, Credo, Ave Maria,
Salve Regina, Glória, Ánima Christi, Tantum Ergo, Angelus, Regína Cæli)
stay hardcoded as single strings since they're identical across all
three locales. The baseUrl building now uses faithSlugFromLang/prayersSlug
helpers instead of inline ternaries.
Prayer detail's prayerDefs routing table — every name field that was
isEnglish ? a : b now points at a t.* lookup. Painting captions for
the Velázquez/Murillo Angelus/Regina Cæli backgrounds become
t.painting_coronation_virgin / t.painting_annunciation. The
AngelusStreakCounter call site drops its three-way ternary in favor of
the typed `lang` derived value.
Slug-table ternaries (URL slug per locale) and the long gloriaIntro
paragraph are intentionally left inline — slugs are URL data, not UI
text, and gloriaIntro is page-unique marketing copy that doesn't
benefit from being in a shared dict.
Adds streak/angelus and Bible-modal keys to the faith dictionary, plus
the three-fragment "this catechesis is only available in German" notice
used by both katechese pages. Pluralization for day/days handled by two
explicit keys (day_singular/day_plural) chosen at the call site —
Latin's "Dies" is invariant so both keys hold the same string.
StreakCounter and AngelusStreakCounter collapse their per-component
labels objects into direct t.foo lookups; the rosary page's BibleModal
call site now passes the typed `lang` derived value (was data.lang as
plain string, didn't satisfy the tightened FaithLang prop type).
BibleModal isn't actually used in Latin context, but the dict requires
every key in every locale, so reasonable Latin equivalents got filled
in for completeness.
Three-locale faith dictionary lands at src/lib/i18n/faith/{de,en,la}.ts
with the same satisfies-based completeness enforcement we use for
fitness, cospend, and calendar. faithI18n.ts is the slim shim — exports
m, FaithLang, FaithKey, plus the URL-slug helpers (langFromFaithSlug,
faithSlugFromLang, prayersSlug, rosarySlug, calendarSlug, apologetikSlug)
needed because faith routes do bidirectional slug ↔ locale mapping that
the other namespaces don't.
[faithLang]/+layout.svelte and +page.svelte fully migrated. The
isEnglish/isLatin derived flag dance collapses into a single typed
`lang`; ten inline ternaries per file (display labels and slug
selection) become t.key lookups or slug-helper calls. The "DE" badge
condition for non-German faith locales tightened from
`isEnglish || isLatin` to `lang !== 'de'`. Apologetik latin-fallback
hops through the helpers instead of inline matchers.
Apologetik pages get the shared-label cut: all four pages (contra,
contra detail, pro, pro detail) now use t.objections, t.evidences,
t.alex_pick, t.objection_label, t.answered_by, t.voices_answering,
t.arguments_title, t.positive_case from the dict. Page-specific
marketing copy (the per-page heading/lede/eyebrow object literals)
stays inline — those strings live in exactly one place each, the
structure is already readable, and pulling them into a shared dict
would be noise.
Also: ImageUpload.svelte was the one stray cospend t() caller the
earlier codemod missed (it lives at lib/components/, outside the
codemod's --root scope). Now uses t.key with `as CospendLang` cast.
Cospend translations move to src/lib/i18n/cospend/{de,en}.ts with
satisfies-based key-set enforcement, mirroring the fitness layout
shipped earlier. cospendI18n.ts becomes the same kind of slim shim
exporting m, CospendLang, CospendKey while keeping every existing
helper (detectCospendLang, paymentCategoryName, splitDescription,
formatNextExecutionI18n, etc.) on the same surface.
Calendar gets the same treatment but with three locales (de/en/la)
and two namespaces — `ui` and the rite-1962-specific `ui1962`.
calendarI18n.ts now imports both as m / m1962, types them as
CalendarKey / Calendar1962Key, and routes t() / t1962() through
them. The 1962 fallback is per-namespace dir with file-prefixed
locale files (de_1962.ts etc.) so they can co-exist.
19 cospend route/component files and 3 calendar pages migrated to
the t.key / t1962.key syntax. Two notable hand fixes: UsersList.svelte
needed `as CospendLang` because the `lang` prop default uses an `as`
cast that breaks TS narrowing of m[lang]; and a sed pass converted
codemod-emitted t['camelCase'] to t.camelCase since the static-key
regex initially only matched snake_case.
The split + codemod scripts are now generic — split-i18n.ts takes
namespace, locales, optional marker and basename for multi-table
modules; codemod-i18n-t-to-m.ts takes module basename, fn name, and
m alias name (so t1962 / m1962 share the same machinery as t / m).
The fitness-specific one-shots are deleted, superseded.
22 files migrated from t('key', lang) function calls to direct lookups
on a derived dictionary alias: const t = $derived(m[lang]) once per
file, then t.start_period or t[card.labelKey] at the call sites.
Cleaner read at the point of use, one less argument threaded through,
and TypeScript narrows on every key access (so a typo in a literal
key now errors at the call site, not silently falls back to the key
string).
The codemod handles both ways `lang` can enter scope — derived from
the URL via detectFitnessLang, or destructured from $props() (single
or multi-line). One file aliased the i18n table to `messages` to
avoid collision with a local `const m = data.measurement`.
The deprecated `t(key, lang)` function still exists in fitnessI18n.ts
for any remaining out-of-tree call sites — can be deleted once
nothing imports it.
The fitness UI translation table previously lived as one combined
object in fitnessI18n.ts where every entry held both languages. That
hides drift (an English string can silently disappear without TypeScript
noticing) and makes adding strings a multi-edit dance.
Split into src/lib/i18n/fitness/{de,en}.ts. de.ts is the source of
truth for the key set; en.ts uses `as const satisfies
Record<keyof typeof de, string>` so any missing English translation is
a build-time error. fitnessI18n.ts now re-exports both as a typed
table m and adds FitnessLang/FitnessKey types — the existing
t/fitnessSlugs/fitnessLabels API stays so call sites don't churn.
The strict typing immediately surfaced one real bug: t('initializing_gps')
was being called from the active workout page but the key never existed
in the dictionary, so it had been rendering the literal string
'initializing_gps' through the fallback. Added the missing key in both
locales.
Tightened BodyPartCard.labelKey and the body-parts Step JSDoc to
FitnessKey instead of plain string so card data drift catches drift at
the data site, not the call site. Two dynamic-key sites (partKeyMap
fallbacks for unmapped measurement keys) are cast pragmatically.
The 360-entry split was done by a one-shot extraction script
(scripts/split-fitness-i18n.ts) — kept for re-use against
cospendI18n.ts and calendarI18n.ts in follow-up commits.
Holding any past or current calendar cell (outside an existing period
record and unless one is already ongoing) for 600ms now opens a
confirmation dialog and starts a period on that day. Same POST as the
button-driven start; just a faster gesture for back-dating today or
yesterday.
Implemented as an inline {@attach longPress(handler)} attachment that
cancels on >8px movement, suppresses iOS contextmenu, and respects
pointer cancel/leave. The held cell scales 1.18× with a growing red
ring and rounded pill border for visual feedback (reduced-motion
falls back to a static ring). Eligibility is gated client-side
(canStartOn): no read-only mode, no projection-only mode, no future
dates, and no overlap with the current period.
Two custom Leaflet actions converted to attachments: renderMap is now
a factory returning an attachment, mountMap is the attachment itself.
Four call sites updated. use:enhance left alone — still the canonical
SvelteKit form-action API.
The stats page's three streamed Promise.resolve(...).then(...) chains
now log on rejection instead of silently swallowing errors. The muscle
heatmap {#await} block gained pending and catch branches with a
lang-aware error message.
Module-level top-level await for db/scheduler init and the cache
warmup IIFE move into the canonical export const init hook. Same
ordering and non-blocking semantics; makes the lifecycle explicit
and works on environments without top-level await.
English label and variable name now match the existing ALEX_PICKS
data convention. German keeps 'Alex' Wahl' (the natural translation).
Latin updated to 'Alexandri delectus' to mirror the pick semantics.
Codemod-driven migration of 55 .svelte files from the deprecated
$app/stores module to the rune-based $app/state ($page.x → page.x,
no auto-subscription wrapper). Two custom writable() stores converted
to .svelte.ts factory functions matching the existing theme store
pattern, with consumers updated to use .value getters and the explicit
.set() method.
UserHeader.svelte's login link now guards page.url.search behind
the browser flag — search-param access throws during prerender, and
this defensive change unblocks future prerender adoption on any page
that includes the header.
Replace string-literal and template-literal hrefs across the codebase
with the modern SvelteKit 2.26+ resolve() and asset() APIs. Migration
makes route IDs explicit, type-checked against generated $app/types,
and base-path-aware. Two codemod scripts handle the bulk; remaining
ambiguous, query-bearing, and precomputed-href cases are converted
manually at the assignment sites.
- contra/pro detail pages move from #voice-X hash to /[argId]/[archId]
(and /[posArgId]/[voiceId]) optional path segments. SSR renders the
selected voice directly — no hydration flash on deep links.
- Tab onclick uses replaceState to update path without a load roundtrip.
- Add Alex's choice chip on contra detail tabs: small circular pfp on
picks, expanded label on the active tab. ALEX_PICKS map per argument.
- Answer-rail pills on contra index extend past 760px column into the
right viewport gutter when space allows; wrap otherwise.
Sync DE translation with current EN content for the 11th pro argument:
expanded claim/thesis (typology as test case), full Akedah meditation in
the hahn voice, canonical-shape argument in wright, doctrinal-development
expansion in newman. Also fixes the canon count from 66 to 73 books to
match the Catholic canon used elsewhere.
- Numbers move to left of dots (text-anchor end).
- ViewBox widened (W 700→820, H 240→320) so the converge label fits and
bigger fonts/dots have breathing room.
- Strand thickness, dot/orb radii, and label font sizes bumped.
- Replace static rings with two pulse-out ripples (4.8s period, 2.4s
offset) emanating from the orb; reduced-motion falls back to static.
- zehn-gebote: orphan `ul` rule left over from inline-toc removal.
- fitness/active: orphan .exercise-header*, .move-exercise*, .remove-exercise*,
.add-exercise-btn rules left over from rail/focus refactor.
Adds the entire /<faithLang>/{apologetik,apologetics} section:
- Landing page introducing the contra/pro split with shield/flame cards.
- Contra (objections): 23 objections, each answered by multiple archetype
voices (Aquinas, Pascal, Augustine, Lewis, Chesterton, plus Logician,
Mystic, Scientist, Pastor archetypes); index + per-argument detail pages
with archetype filter and inter-argument navigation.
- Pro (positive case): 12 arguments across three layers (supernatural,
theism, christianity) voiced by Habermas, Polkinghorne, Newman, Hart,
Lewis, Wright, Hahn, Plantinga, Eliade, Feser, Chesterton, Guénon;
cumulative-case visual + per-argument detail pages.
- DE/EN content via per-language data modules; LA stub layout 307-redirects
to English.
- Per-language slug via apologetikSlug matcher; canonical-slug enforcement
redirects mismatches.
- Shared ApologetikToc component (also reused on zehn-gebote katechese).
- CaseTabs component for contra/pro switching.
- DeepL translation script for regenerating DE data from EN source.
- Server-side scripture lookup helper.
Redesign the active-workout page around a left-rail timeline and a
focus card on the right. The rail owns the workout title, pause,
elapsed time, sync indicator, progress bar, and a reorderable chip
per exercise (drag to reorder, × to delete, starting-weight hint so
you know what to rack, green checkmark when complete). Main stage
holds a hero focus card for the active exercise plus its SetTable.
- New WorkoutRail.svelte and WorkoutFocusCard.svelte
- Active exercise pinned to top of the scrollable rail (mobile only)
- Desktop: rail grows freely; mobile: compact vertical stack
- Finish + cancel share one row; cancel is a ghost action
- Drop the old sticky bottombar; its controls moved into the rail
- ExerciseName gains `plain` prop to opt out of the detail link
- Active workout route joins the 1400px max-width whitelist
Redesign the active-workout footer as a floating glass pill that
mirrors the site header — same backdrop-blur recipe, same token set,
anchored to the bottom instead of the top. Inner controls recomposed:
icon-only pause button, dominant elapsed time, subtle divider, label +
chevron or rest-timer pill. Mount animation, hover-lift, reduced-
motion fallback.
Align the muscle picker with the site card language (matches
/fitness/check-in and /fitness/stats) and unlock the full desktop
width via the 1400px container used by nutrition/check-in.
- Sidebar card layout at ≥900px (200/620 grid, sticky)
- Larger sidebar at ≥1180px (460/720) with figures uncapped
- Tablet tier (900–1179px) stacks figures vertically inside the card
- Below 900px the card sits on top of the content column
Coop Supercard uses GS1 Data Matrix — the payload contains FNC1
separators between fields, not literal 0x1D bytes. Enable bwip-js
parsefnc so ^FNC1 placeholders in the env value produce genuine
FNC1 codewords (codeword 232), matching the physical card and
letting checkout scanners parse the GS1 element string.
Replaces the single card button with two brand-colored buttons
(Coop blue, Migros orange) that each open only their own card.
Modal now wears the brand gradient directly, drops the red cross
close button pattern from BibleModal, and scales the Data Matrix
+ linear barcode to fill the modal on phones for easy scanning.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
Replace the emoji/gradient card with an editorial layout: small lucide
glyph, oversized error code, hairline-divided serif bible quote.
Extract shared ErrorView + SectionError components and a bilingual
string helper. Add +error.svelte at each section root (faith, recipes,
fitness, tasks, cospend) so errors render inside the correct layout and
inherit the section-specific header/nav. Catch-all [...rest]/+page.ts
stubs route unmatched URLs through the section layout so the right
error page catches them.
The raw series has `pending: number | null`, but the rendered series stores
pending as `{ x, y, value }` for SVG placement. Legend labels were calling
`.toFixed(1)` directly on the object, crashing as soon as a pending value
existed (i.e. whenever the user typed a measurement).
romcal's package.json is not listed in its exports map, so
require.resolve('romcal/package.json') throws ERR_PACKAGE_PATH_NOT_EXPORTED
under Node's strict exports enforcement. Resolve the main entry instead
and walk up until we find the package.json belonging to romcal itself.
romcal's 1962 bundle files live outside the package's exports map and were
being loaded via a cwd-relative path. Under systemd the server runs with
cwd /usr/share/webapps/homepage/dist/, so node_modules/romcal/... resolved
against dist/ and hit ERR_MODULE_NOT_FOUND. Switch to createRequire +
require.resolve('romcal/package.json') so the bundle path is anchored to
the actual package root regardless of cwd.
Also track scripts/hooks/pre-push which runs scripts/deploy.sh after a
master push to origin. Git has no native post-push hook; pre-push is the
closest client-side equivalent — if deploy fails the push is aborted.
Install with: ln -sf ../../scripts/hooks/pre-push .git/hooks/pre-push
New /fitness/measure/body-parts route with step-by-step tape-measure
flow, per-step tips, L/R paired inputs, inline history chart, and
review-before-save summary. Measure page replaces the old body-parts
accordion with a launch button. Fitness layout goes full-bleed and
hides the site footer for this route via a :has() attribute selector,
and the desktop 3-column grid now extends rail and side panel up past
the floating nav while the middle column compensates with padding.
Inline italic captions under each single-sided input (neck, shoulders,
chest, waist, hips) and row-spanning captions with a primary accent bar
under each paired row (biceps, forearms, thighs, calves) — one tip per
pair, not per side. EN + DE copy.
Schema uses flat keys (leftBicep, rightBicep, leftForearm, ...), but
create/edit forms built nested objects (biceps: {left, right}), which
Mongoose silently stripped — so paired parts never persisted. The
latest-measurements endpoint also wraps the body-parts doc in
{value, date}; the display path skipped the .value hop, hiding every
body-part field. Switch client to flat keys end-to-end.
- Reserve an angular gap in the ring and render a clickable green triangle
that jumps to Advent I of the following liturgical year; on hover the
center numeral previews the upcoming LY in a muted color.
- Show the liturgical year (not civil) in the ring center so Advent 2025
already reads 2026, and promote the selected arc to the top of the
SVG paint order so its highlighted border is never clipped by neighbors.
- Lift the selected-season glow by mixing the season tint with the
foreground color so purple stays visible on dark backgrounds and white
on light backgrounds.
- On feast-dot hover, pop a colored SVG pill with the short date and feast
name, tracked by ISO so it auto-clears when the dot is unmounted by
navigation.
- Server now derives the liturgical year from the selected date, shifting
yearMap forward when the URL civil date lies past Advent I of that year
so clicks on Dec 25 no longer select the prior post-Pentecost cycle.
- Add data-sveltekit-keepfocus on ring anchors to avoid the focus-scroll
jump after client-side navigation.
Switch the romcal dependency to AlexBocken/romcal (monorepo fork with
collapsed bucket prefixes) and strip the runtime prefix-fallback chain
from liturgicalCalendar.ts — name/propers lookups now use a single
flat id. The 1962 data model shrinks to just what the rendering uses
(commem {id,name}, detail carrying propers as {key, la[], local[]})
and the detail + overview pages drop the rubrics/octave/properSource
fields that never got wired in.
- Rotate the ring smoothly to put the selected day under a static vertical
needle pin; pivot uses a shortest-arc Tween, respects prefers-reduced-motion,
and falls back to today when no selection. Pin + bar cross-fade color in
lockstep (650ms cubicOut) to the selected day's liturgical color (gold when
selected == today).
- Split the overview into an inline hero (selected day) and a dedicated
/detail/{yyyy}/{mm}/{dd} route that opens on hero click; drop the old
inline detail block.
- Restyle the month grid to a minimalist card-grid: taupe feria fills,
rounded cells, gold today-ring + dot, Roman-numeral rank badges, and
equal-width columns via minmax(0, 1fr) so long feast names no longer
stretch a column.
- Default the calendar view to the ring, reorder the view switcher
(ring first), and match hero-card color transition to the ring timing.
- Extract shared calendar types to $lib/calendarTypes.ts and server helpers
to $lib/server/liturgicalCalendar.ts so the overview + detail routes share
one source of truth. Bump romcal dep to the dev branch, alias the Swiss
1969 bundle so its exports resolve.
- Bump version to 1.35.0.
Show propers text for each 1962 celebration with scripture reference pills
grouping each block. When a translated proper is missing, fall back to the
local-language Bible (Douay-Rheims for en, Allioli for de), showing a note
above the translated column. Handles multi-segment refs (e.g. "Ps 118:85;
118:46") with inherited book/chapter, and shifts Vulgate→Hebrew psalm
numbering for Allioli.
Also restructures date navigation as folder-based optional params
(/yyyy/mm/dd) with the rite forced as a required path segment so day/month
navigation stays within the active rite.
Add a small banner on the 1962 rite view noting that day-to-day data is
still being verified and that local proper calendars (diocese, order,
national feasts) are not yet layered on top of the general Roman
calendar. Bump the romcal dep to AlexBocken/romcal1962#e4731a8 for the
Holy Week / Easter Octave name fixes and the Pentecost-season color fix
(ordinary-time Sundays are now Green, not White/Red).
Replace four tastytea-style SVGs (blanket, hammer, teapot, heart_tastytea)
whose 800px viewBox clashed with the 33.866666 viewBox of the rest, and add
the full Tirifto gutkato_ set. Catalog grows from 54 to 113 positive stickers
(new foods/drinks, rose and cat colors, professions, cute expressions, signs).
Adds a calendar tile to the /faith (glaube/fides) LinksGrid linking to the
liturgical calendar. Homepage description and WIP body reference the "older
rite" / "alten Ritus" rather than "Tridentine" to avoid loaded connotations;
subtitle labels keep the neutral Ordinary/Extraordinary Form terminology.
Adds `/faith/calendar`, `/glaube/kalender`, `/fides/calendarium` route backed by
romcal v3 + @romcal/calendar.general-roman with native EN/DE/LA locales. Month
grid, today hero, and day detail panel use liturgical colors from the rubric.
Header gets a segmented 1969/1962 pill toggle; selecting 1962 shows a WIP
placeholder (Tridentine calendar data not yet wired up).
WebKit's hidden <input type="checkbox" switch> haptic trick is gated on
tap-completion, not tap-start. Firing on pointerdown ran the programmatic
click() before iOS committed to "this is a tap" so the switch-toggle
haptic was suppressed. pointerup lands inside iOS's tap-completion
window — still earlier than click (no movement filter, no 300ms wait).
Android native bridge path is unaffected.
Derive recents from current-day entries merged with historical
server data so logging a food updates the quick-log bar instantly.
FoodSearch now emits onfavoritechange so toggling a heart keeps
the quick-log favorites tab in sync without reloading.
Align muscle group and equipment filters in the exercise picker
with the rest of the fitness UI — horizontal pill scrollers, multi-
select, with Lucide icons for each equipment type.
Edit mode now shows a pill row (breakfast/lunch/dinner/snack with their
icons) beneath the grams input so the entry can be moved to another meal.
Food cards are also draggable; dropping onto a different meal section
optimistically moves the entry (rolls back on API error). Desktop/pointer
only — touch devices use the pill row.
Allow users to nest steps inside a repeat group (e.g. "5×: 30s sprint,
60s recovery") when building GPS interval templates. Groups are tagged
{ type: 'group', repeat, steps } alongside flat { type: 'step' } entries
and capped at one nesting level. Entries are expanded into a flat list
before handing off to the native Android TTS/interval service, so the
runtime state machine is unchanged.
Distinguish stretches, strength, cardio, and plyometric exercises
with a curated `exerciseType` field that overrides mislabels in
ExerciseDB for the hand-curated stretch set. Surface the type as
pills on the detail page and as a filter toggle on the list page.
Replace the muscle-group and equipment dropdowns with horizontally
scrolling pill rows; equipment pills carry Lucide icons, and the
type toggle shows a flexed-bicep / person / layers glyph. Selected
muscle groups hoist to the front of the scroller.
Viewer: cake-form adjust now collapses into a summary trigger with live
factor badge; shape picker replaced with icon-only tiles that flex to
fill the row; numeric inputs gain inline cm suffix; restore-default link
appears when user deviates from the default. Editor: default-Backform
config mirrors the same card + tile pattern (adds "none" tile), plus
inline cm suffixes. Baking info row in instruction editor becomes a
click-to-reveal card with summary chips, mode presets, and editable
fields behind the chevron.
Replace CardAdd with EditTitleImgParallax so /rezepte/edit/[name]
mirrors the hero-parallax layout of /rezepte/[name]. Add Lucide icons
and a width-constrained info-card grid to CreateStepList's additional
info section. Refactor the English translation view to use semantic
theme vars, side-by-side German/English field comparison, and a card
aesthetic matching the rest of the site.
Fix portions binding bug where the shared portions store was being
written by both language ingredient lists. CreateIngredientList now
accepts a bindable portions prop; the English list uses it with
useStore=false to stay isolated from the German value.
Every prayer card now vibrates on tap — non-decade cards advance to the
next section, decade cards increment the Ave Maria counter with auto-scroll
at 10. Two profiles (bead vs card) give distinct tactile feel; the 10th
bead fires the heavier card haptic to mark decade completion.
Native Android path via AndroidBridge.forceVibrate uses VibrationAttributes
USAGE_ACCESSIBILITY so vibration bypasses silent / Do-Not-Disturb inside
the Tauri app. Browser falls back to the web-haptics npm package. Haptic
fires on pointerdown with touch-action: manipulation for near-zero tap
latency; state change stays on click so scroll gestures don't advance.
- Remove CounterButton (whole card is now the tap target)
- Replace emoji with Lucide BookOpen icon, restyle citation as an
understated inline typographic link (no background chip)
- Drop decade min-height leftover from the pre-auto-advance layout
Bumps site to 1.27.0 and Tauri app to 0.5.0 (new Android capability).
Shift pop-b and pop-c selectors so accent colors appear sooner in the
grid and the light/white pop-c repeats more frequently (every 5th
instead of every 7th item).
- Play/Stop button replaces checkmark for duration-only exercises
- Green countdown bar with auto-completion and rest timer chaining
- Display duration in seconds (SEC) instead of minutes for holds
- ActiveWorkout model now preserves distance/duration fields on sync
- Hold timer state syncs across devices via SSE
- Workout summary shows per-set hold times for duration exercises
- Template diff compares and displays duration changes correctly
Exercises used by the Day 6 stretching template were only in
exercisedb-map.ts but missing from exercises.ts, causing the
template detail to show raw IDs instead of proper names.
Use Android TYPE_STEP_DETECTOR sensor in LocationForegroundService to
count steps in a 15s rolling window. Cadence (spm) is computed at each
GPS point and stored alongside lat/lng/altitude/speed. Session detail
page shows cadence chart when data is available.
No additional permissions required — step detector is not a restricted
sensor. Gracefully skipped on devices without the sensor.
Replace auto-seed with a browsable template library. Users can
selectively add built-in templates to their collection via a
BookOpen icon or the empty-state prompt. Each library template
tracks its origin via libraryId to prevent duplicates.
- Extract default templates to shared $lib/data/defaultTemplates.ts
- Add GET/POST /api/fitness/templates/library endpoint
- Add library modal with add/added state per template
- Keep seed endpoint as fallback (imports from shared data)
Full-body stretching session (~30 min) covering all major muscle
groups with 15 bodyweight exercises: neck, shoulders, chest, back,
spine, hips, hamstrings, quads, glutes, calves, and arms.
Each exercise has 2 sets of 60-90s holds with 15s rest.
When no workout is logged for the day, look up the next template
in the schedule rotation and show the kcal from its most recent
session as a projection. Tappable toggle includes/excludes it
from the calorie goal, ring, and macro bars for meal planning.
shoppingCatalog.json was missing all 22 new entries (11 icons
with aliases) added to catalog.json, so new icons like stroh80
were never matched at runtime.
The embedding model assigned many items to wrong categories
(e.g. rum→Milchprodukte, zahnbürsten→Fleisch, pflaumen→Hygiene).
Manually reviewed and corrected all 419 entries.
Add processed icons for glasnudeln, grünkohl, kokosnuss, lychee,
mangold, pak choi, pastinaken, reisnudeln, rettich, stroh 80, and
topinambur. Add ImageMagick script to remove Gemini watermark and
black background from raw icons. Update catalog and re-embed.
logCustomMeal and inlineLogCustomMeal relied on goto() to re-run the
page load function, but SvelteKit skips it when the URL doesn't change.
Now they update the entries array directly like the other log functions.
Calorie ring and macro progress bars now both use Atwater-derived
calories (P×4 + F×9 + C×4) instead of DB calories, so hitting all
three macro goals guarantees hitting the calorie goal exactly.
Also: show full daily TDEE (not time-based), show BMR/NEAT multiplier
breakdown in info tooltip, display macro goal grams on progress bars,
fix TDEE tooltip z-index on desktop.
Align recipe metadata cards with site-wide design language using
surface colors, borders, and rounded corners. Add distinct lucide
icons per card type (Timer, Wheat, Croissant, Flame, CookingPot,
UtensilsCrossed) and switch from flex-wrap to CSS grid for uniform
card widths.
Add ?month=YYYY-MM filter to sessions API. Migrate history page to
/fitness/history/[[month]] optional param route. Default view shows last
2 months; specific month view via /fitness/history/2026-04. Replace
load-more button with prev/next month anchor navigation.
Migrate /fitness/nutrition?date=YYYY-MM-DD to /fitness/nutrition/YYYY-MM-DD
using SvelteKit optional param [[date=fitnessDate]]. Replace date nav
buttons with anchor tags for native browser navigation. Today resolves to
the clean /fitness/nutrition path without a date segment.
Add theme-aware DatePicker with pill display, calendar dropdown, prev/next
day arrows, bilingual month/weekday names, and min/max support. Replace all
15 native <input type="date"> elements across fitness, tasks, and cospend.
Move weight hero card, body fat and body parts accordions directly
onto the main measure page. SaveFab only appears when a field has
been changed. After saving, form resets and history updates in place.
Fix response unwrapping (created.measurement) that caused Invalid Date.
Replace flat form with hero weight card (±0.1 stepper, last-weight
placeholder, clear button) and collapsible accordion sections for
body fat and body part measurements. Body parts grouped by region.
Fat/carb percentages were stored as absolute values that didn't account
for protein g/kg varying with body weight. Now protein calories are
computed first, and remaining calories are split between fat and carbs
by their stored ratio — guaranteeing all macros sum to the calorie goal.
Exercise burned calories also flow into fat/carb targets via a new
effectiveCalorieGoal derived. Goal editor ring preview and labels
updated to show computed actual percentages.
Adherence was comparing intake against the flat calorie goal, ignoring
burned workout calories. Now the per-day target is goal + exercise kcal.
Also expanded workout query from 7 to 30 days to cover the full
adherence window.
Server load now fetches the shopping list from the DB and passes it as
initialList. The sync layer seeds state immediately in the script block
(not onMount) so SSR renders the full list. SSE connects client-side
in onMount for real-time updates.
Replace hardcoded Nord values and manual dark/light overrides with
semantic CSS variables (--color-surface, --color-primary, --shadow-sm,
--radius-card, etc.). Use scale:1.02 hover pattern and remove all
@media(prefers-color-scheme) and :global(:root[data-theme]) blocks.
- Use semantic CSS variables (--color-surface, --shadow-sm, etc.) instead
of hardcoded Nord values and manual dark mode overrides
- Add border-radius and overflow:hidden for rounded card corners
- Move icon fill variables (--grid-fill-*) into app.css theme system:
colorful (red/orange/green) in light, cool blues in dark
- Mottled fill distribution via prime-offset nth-child selectors
- Reorder homepage links: Recipes, Shopping, Fitness, Faith, Tasks first
- Add Nutrition direct link with heart-pulse icon
- Document site-wide design language in CLAUDE.md
Account for env(safe-area-inset-top) in the hero negative margin-top
so images fill the status bar area on edge-to-edge Android instead of
leaving empty space.
The box-shadow rendered outside the pseudo-element, placing the shadow
below the status bar boundary. Switch to a multi-stop linear gradient
inside the element so the shadow fades smoothly through the status bar
area (+20% overshoot for softer transition).
Server pre-computes initialShowRoundOff from food log totals and goals.
SSR uses this value; client $effect sets hasHydrated=true after mount,
switching to the reactive $derived for live updates.
Custom meals are now resolved to per100g nutrition data and added
to the base food pool, allowing them to appear in 1-3 food combo
suggestions alongside pantry items and favorites.
Use Beef (protein), Droplet (fat), Wheat (carbs) icons consistently in
ring graphs, macro bars, food cards, detail rows, ingredient lists, and
stats page. Add labelIcon snippet prop to RingGraph/StatsRingGraph. Show
macro split legend always (was hidden on mobile) and group it with the
title in horizontal layout.
Suggest optimal 1-3 food combinations to fill remaining macro budget using
weighted least-squares solver over a curated pantry (~55 foods) plus user
favorites/recents. Recipes scored individually (no combining). Features:
- Combinatorial solver (singles, pairs, triples) with macro-weighted scoring
- MealTypePicker component (extracted from quick-log, shared)
- Hero card with fit%, macro delta icons (Beef/Droplet/Wheat), ingredient
cards, animated +/X toggle for logging
- Responsive layout: sidebar on mobile, center column on desktop
- MongoDB cache with ±5% tolerance, SSR on cache hit, TTL auto-expiry
- Cache invalidation on food-log/favorites/custom-meals CRUD
- Recipe per100g backfill admin endpoint
Extend the nutrition detail page to support OpenFoodFacts items (looked up
by barcode) and custom meals (with ingredient breakdown). All food diary
cards and search results now link to detail pages regardless of source.
New MuscleMap component highlights primary muscles at full opacity and
secondary muscles at 40% using the existing body SVG diagrams. Only
renders front/back views that have active muscles.
Responsive layout: centered inline on mobile, sticky sidebar on desktop.
- Fix ExerciseDB data quality (remove empty calf raise, fix Wall Sit,
correct muscles, typos)
- Rewrite verbose AI-generated English overviews to concise one-sentence
descriptions
- Add German translations for all 199 EDB exercises (name, instructions,
overview)
- Add English and German overviews for 55 static-only exercises
- Display overview above instructions on exercise detail page
Recipes logged in the food diary now link to a dedicated detail page at
/fitness/nutrition/food/recipe/{id} showing full nutritional breakdown,
hero image, and a link back to the recipe page. When opened from the
diary, the logged per100g snapshot is used; otherwise current recipe
nutrition is computed. Recipe favorites are now supported across the
favorite-ingredients API, nutrition lookup, and search endpoints.
Shows a Today button when not viewing current date/month. Nutrition
page button appears right-aligned in the date nav. Period tracker
button appears top-right of the calendar header with centered
month title and chevrons.
Move favorites and recent foods fetching to server load (parallel with
existing queries). Replace client-side $effect DOM manipulation for
fitness-content max-width with reactive CSS variable in layout via
route detection. Removes client-side fetch-on-mount pattern.
Heart button in food header to add/remove favorites via the
existing favorite-ingredients API. Checks status on load, toggles
optimistically with error handling.
Desktop sidebar (1600px+) for one-click food logging with inline amount
input. Favorites and recent items (last 3 days) shown with meal type
auto-selected by time of day. New /api/nutrition/lookup endpoint for
exact source+id food data retrieval. Parent container width override
via JS class toggle for reliable SvelteKit client-side navigation.
Micros use a snippet to render in two locations: inline inside the
daily-summary card on mobile (toggle accordion, unchanged), and as a
standalone always-visible card below the water tracker on desktop.
Also bumps desktop max-width to 1400px.
At 1024px+, nutrition page switches from single-column (600px max) to a
two-column grid: sticky sidebar (summary, goals, water) + scrollable
meals column. Mobile layout unchanged via display:contents fallback.
Balance is now intake minus estimated expenditure rather than intake
minus calorie goal. TDEE computed per day using that day's SMA trend
weight (Mifflin-St Jeor BMR × NEAT multiplier) plus tracked workout
calories, so a -500 kcal cut shows ~-500 on the balance card.
Promise-based modal dialog with backdrop, keyboard support, and animations,
replacing all 18 native confirm() call sites across fitness, cospend, recipes,
and tasks pages.
Lower desktop breakpoint from 1024px to 750px so the macro column
appears on more screens. Legend now uses horseshoe arc for actual
and rounded triangle for target.
White PNG icons were invisible on light backgrounds. Added semantic
--shopping-icon-filter variable (invert(1) in light, none in dark)
applied to card and picker icons.
On desktop (≥1024px), protein/balance/adherence cards sit in a row above
the muscle heatmap, with the macro split card as a vertical sidebar on the
right spanning the full height. Includes ring/triangle legend for
actual vs target. Mobile layout unchanged.
Clicking the (i) icon on the calorie balance or adherence card now shows
a floating popover explaining how each metric is calculated, with EN/DE
translations.
When logging a custom meal, liquid ingredients (BLS drinks, water, beverages)
are detected and their volume stored as `liquidMl` on the food log entry.
The liquid tracker cups and list now include these meal-sourced liquids.
Move cospend routes to parameterized [cospendRoot=cospendRoot] supporting
both /cospend (DE) and /expenses (EN). Add cospendI18n.ts with 100+
translation keys covering all pages, components, categories, frequency
descriptions, and error messages. Translate BarChart legend, ImageUpload,
UsersList, SplitMethodSelector, DebtBreakdown, EnhancedBalance, and
PaymentModal. Update LanguageSelector and hooks.server.ts for /expenses.
Add Rahm, Rüebli, Poulet, Cervelat, Crevetten, Weggli, Bürli, Zopf,
Glacé, Konfitüre, Nüsslisalat, Federkohl, Peperoni, and other Swiss
German terms to category items and alias map so they categorize correctly
instead of falling back to embedding similarity (e.g. Rahm→Schwamm).
Refactor page components to use $derived + invalidateAll() where data
is read-only or re-fetched after mutations. Suppress state_referenced_locally
for intentional patterns (form state, optimistic updates, pagination).
Fix a11y issues with role="presentation", add standard line-clamp properties,
remove unused CSS selectors and empty rulesets.
Add custom meals tab to inline food add section with search/meals toggle.
Animate calorie ring overflow (red) after primary fill completes, with
separate glow elements so red overflow glows red independently. Apply same
delayed overflow animation to macro progress bars. Replace hardcoded nord8
with --color-primary throughout nutrition page (today badge, ring, tabs,
buttons). Add custom clear button to FoodSearch, hide number input spinners
globally.
Add protein g/kg (7-day avg using trend weight), calorie balance
(surplus/deficit vs goal), diet adherence (since first tracked day),
and macro split rings with target markers to the stats dashboard.
Profile editor closes on successful save. Period tracker visibility
uses a reactive savedSex state variable that updates on save, so
changing sex to female/male immediately shows/hides the tracker
without requiring a page refresh.
ongoingDay compared today (local time with hours) against startDate
parsed as UTC midnight, causing Math.floor to undershoot by 1 day.
Use todayMidnight and parseLocal to normalize both to local midnight.
Use fireImmediately: true to load WASM eagerly during init instead of
lazily on first detect() call, catching load errors immediately. Bail
out after 5 consecutive detection errors instead of looping forever.
Remove verbose debug messages, keeping only error output.
The CATEGORY_MAP was based on BLS 3.x letter codes which were completely
reshuffled in version 4.0. This caused wrong categories like Schwarztee
showing "Wurstwaren" instead of "Getränke". Remapped all 20 letter codes
to match actual BLS 4.0 Hauptlebensmittelgruppen and regenerated blsDb.
Pencil button toggles inline gram editor; second tap saves via PUT API.
Both edit and delete buttons appear on hover (bottom-right on desktop).
Removed separate checkmark save button in favor of toggling the pencil.
When intake exceeds goals, macro bars show a red segment growing from
the right edge backwards and the calorie ring draws a red arc backwards
from the 100% mark, clearly visualizing the overrun amount.
Track water intake via interactive SVG cups (fill/drain animations) using
BLS Trinkwasser entries for mineral tracking. Detect beverages from food log
(BLS N-codes + name patterns) and include in liquid totals. Configurable
daily goal stored in localStorage. Cups show beverage fills (amber) as
non-removable and water fills (blue) as adjustable.
Add toggleable store presets (Coop Max-Bill Platz, Migros Seebach) that
reorder categories to match the physical store layout. Selection persisted
in localStorage.
- Generate temporary share links (default 24h) that allow unauthenticated
users to view and edit the shopping list
- Share token management modal: create, copy, delete, and adjust TTL
- Token auth bypasses hooks middleware for /cospend/list routes only
- Guest users see only the Liste nav item, other cospend tabs are hidden
- All list API endpoints accept ?token= query param as alternative auth
- MongoDB TTL index auto-expires tokens
- Diagonal strikethrough line + lower opacity on checked cards
- Long press opens edit modal to manually assign category and icon (saved to DB)
- Replace floating status toasts with inline SyncIndicator (Cloud/CloudOff/RefreshCw)
- Move category count badge next to title instead of right-aligned
Add Lucide icons and Nord colors per category, parse quantities from item names
(e.g. "10L Milch" → badge "10L" + name "Milch"), and remove category collapse toggling.
Real-time shopping list with SSE sync between multiple clients, automatic
item categorization using embedding-based classification + Bring icon
matching, and card-based UI with category grouping.
- SSE broadcast for live sync (add/check/remove items across tabs)
- Hybrid categorizer: direct catalog lookup → category-scoped embedding
search → per-category default icons, with DB caching
- 388 Bring catalog icons matched via multilingual-e5-base embeddings
- 170+ English→German icon aliases for reliable cross-language matching
- Move cospend dashboard to /cospend/dash, /cospend redirects to list
- Shopping icon on homepage links to /cospend/list
Lightning CSS was deduplicating manually written backdrop-filter +
-webkit-backdrop-filter to just the webkit version, breaking blur on
Firefox. Remove manual webkit prefixes and let Lightning CSS auto-prefix
via browser targets in vite.config.ts.
Add full rarity glow to gallery stickers matching the reward popup style.
Tapping an owned sticker opens a large preview card. Allow calendar
stickers to overdraw their cell on hover.
Template detail modal now shows the initial weight per exercise.
Quick start hero shows the first exercise name and weight to help
with rack preparation before starting.
Clicking "Period Ended" now records yesterday as the end date, since
you only know the period ended the day after. Also added the missing
fertile date range to the ongoing-period status view.
Duplicate tags (e.g. "Butter" at two indexes) caused a Svelte
each_key_duplicate error that broke rendering on /recipes.
Also use past tense for angelus streak button to match rosary.
Client-side navigation to /recipes hung because getUserFavorites and
other endpoints were hardcoded to /api/rezepte, causing fetch mismatches
during SvelteKit's client-side routing.
Integrate ExerciseDB v2 data layer (muscleMap.ts, exercisedb.ts) to enrich
the 77 static exercises with detailed muscle targeting, similar exercises,
and expand the catalog to 254 exercises. Add interactive SVG muscle body
diagrams for both the stats page heatmap and exercise list filtering, with
split front/back views flanking the exercise list on desktop. Replace body
part dropdown with unified muscle group multi-select with pill tags.
Scrape scripts for ExerciseDB v2 API (scrape-exercises.ts,
download-exercise-media.ts), raw data for 200 exercises with
images/videos, and a 1:1 mapping from ExerciseDB IDs to internal
kebab-case slugs (exercisedb-map.ts). 23 exercises matched to
existing internal IDs, 177 new slugs generated.
Replaces flat goal editor with a 3-step wizard (preset → calories → macros),
adds preset cards with diet descriptions, live macro donut ring preview,
overlaid TDEE vs target comparison bar, TDEE missing-data warning with
explanation, and surfaces latest weight used for TDEE calculation.
Full period tracking system for the fitness measure page:
- Period logging with start/end dates, edit/delete support
- EMA-based cycle and period length predictions (α=0.3, 12 future cycles)
- Calendar view with connected range strips, overflow days, today marker
- Fertility window, peak fertility, ovulation, and luteal phase visualization
- Period sharing between users with profile picture avatars
- Cycle/period stats with 95% CI below calendar
- Redesigned profile card as inline header metadata with Venus/Mars icons
- Collapsible weight and period history sections
- Full DE/EN i18n support
Recipes from /rezepte now appear in the food search on /fitness/nutrition,
with per-100g nutrition computed server-side from ingredient mappings.
Recipe results are boosted above BLS/USDA/OFF in search ranking.
OpenFoodFacts products are now searchable by name/brand via MongoDB
text index, alongside the existing barcode lookup.
Recipe and OFF queries run in parallel with in-memory BLS/USDA scans.
Fix dev-mode reconnect storm by persisting mongoose connection state on
globalThis instead of a module-level flag that resets on Vite HMR.
Eliminate redundant in_season DB query on /rezepte — derive seasonal
subset from all_brief client-side. Parallelize all page load fetches.
Replace N+1 settlement queries in balance route with single batch $in
query. Parallelize balance sum and recent splits aggregations.
Trim unused dateModified/dateCreated from recipe brief projections.
Add indexes: Payment(date, createdAt), PaymentSplit(username),
Recipe(short_name), Recipe(season).
Use local dates instead of UTC for day boundaries, and store an epoch
timestamp alongside the date string. Streak alive check uses real
elapsed time (<48h) which covers dateline crossings. Old data without
timestamps falls back to date-string comparison so existing streaks
are preserved.
Add drop shadow under the safe-area-inset-top zone to visually
separate Android status icons from page content. Adjust StickyImage
sticky positioning and max-height to account for safe-area-inset.
- Add keyboard handler to fab-modal and dialog overlays (a11y)
- Remove unused .btn-cancel CSS selector
- Wrap meal name input inside its label, use span for ingredients heading
- Change image-wrap-desktop from div to figure for valid figcaption
- Add missing PDF content: Dt 4:12f, Mt 19:17, Sir 1:26 quotes in Ursprung/Warum sections
- Add äussere Seite section (Röm 12:1, 1 Kor 6:18-20, KKK 2702) and Gemeinschaftsgebet (Mt 18:20)
- Add pars potentialis concept to inner side section
- Add sticky section TOC nav for wide screens (1200px+)
- Align commandment highlight colors with tablet categories (God=orange, neighbor=blue)
- Use straight left borders instead of rounded on commandments
- Add German-only notice for English users on all catechesis pages
- Add disclaimer attributing errors to site author, not P. Ramm/FSSP
- Replace Inkscape katechese SVG with cleaner book icon on faith landing page
- Fix 10 commandments tablet SVG to show 5+5 lines
- Add /fides route with Latin-only mode for all faith pages (rosary, prayers, individual prayers)
- Add LA option to language selector for faith routes
- Add Angelus/Regina Caeli streak counter with 3x daily tracking (morning/noon/evening bitmask)
- Store streak data in localStorage (offline) and MongoDB (logged-in sync)
- Show Annunciation/Coronation paintings via StickyImage with artist captions
- Switch Angelus↔Regina Caeli in header and landing page based on Eastertide
- Fix Eastertide to end at Ascension (+39 days) instead of Pentecost
- Fix Lent Holy Saturday off-by-one with toMidnight() normalization
- Fix non-reactive typedLang in faith layout
- Fix header nav highlighting: exclude angelus/regina-caeli from prayers active state
- Use createImageBitmap for off-thread frame capture so video stays smooth
- Require 2 consecutive identical reads before accepting a barcode
- Validate EAN/UPC check digit and reject codes with invalid length
- Only accept 8, 12, or 13 digit codes (EAN-8, UPC-A, EAN-13)
Native BarcodeDetector works in Chrome/Android WebView over HTTPS.
Only load the ZXing WASM ponyfill when native API is unavailable or
doesn't support the needed formats.
The zxing-wasm ?url import fails in Rollup production builds. Copy the
WASM binary to static/fitness/ and reference it via absolute path in
prepareZXingModule locateFile.
- Exclude barcode-detector from Vite optimizeDeps to prevent WASM mangling
- Self-host ZXing WASM via Vite ?url import with prepareZXingModule
- Use barcode-detector/ponyfill instead of deprecated /pure export
- Separate barcode-detector/zxing-wasm into own chunk
- Add CAMERA permission to Android manifest for Tauri app
- Camera-based barcode scanning in FoodSearch using barcode-detector (ZXing WASM)
- Import script to load OFF MongoDB dump into lean openfoodfacts collection
with kJ→kcal fallback and dedup handling
- Barcode lookup API with live OFF API fallback that caches results locally,
progressively enhancing the local database
- Add 'off' source to food log, custom meal, and favorite ingredient models
- OpenFoodFact mongoose model for the openfoodfacts collection
Daily food log with calorie and macro tracking against configurable diet
goals (presets: WHO balanced, cut, bulk, keto, etc.). Includes USDA/BLS
food search with portion-based units, favorite ingredients, custom
reusable meals, per-food micronutrient detail pages, and recipe-to-log
integration via AddToFoodLogButton. Extends FitnessGoal with nutrition
targets and adds birth year to user profile for BMR calculation.
Skip embedding matching for anchor-tag ingredients that reference other
recipes. Instead, mark them with recipeRef/recipeRefMultiplier fields so
their nutrition is resolved via resolveReferencedNutrition with a
user-configurable fraction. The edit UI shows these as teal REF badges
with an editable "Anteil" input.
- Move parseAnchorRecipeRef and resolveReferencedNutrition from the
items endpoint into nutritionMatcher.ts for reuse
- JSON-LD endpoint now includes nutrition from referenced recipes
(base recipe refs and anchor-tag ingredient refs)
- Strip HTML tags in normalizeIngredientName/De before matching to
prevent regex crash on ingredients containing anchor tags
- Escape regex special chars in substringMatchScore word-boundary check
Compute macro/micro totals from stored nutrition mappings and emit a
schema.org NutritionInformation block in the JSON-LD output. Values are
per-serving when portions are defined, otherwise recipe totals.
Excluded (manually disregarded) ingredients were incrementing the total
count twice — once in the loop body and again in the exclusion check —
deflating the displayed coverage percentage.
Replace deprecated svelte:component with direct component invocation,
use span instead of label for non-input controls with role="group",
remove unused imports and dead CSS rules.
Replace fragile CWD-based readFileSync path resolution with SvelteKit's
read() + Vite ?url asset imports. This lets the build system manage the
embedding files as hashed immutable assets, fixing ENOENT errors in
production where the working directory didn't match expectations.
The summary screen was comparing against only the last session
(limit=1), showing false PRs when you beat last time but not your
all-time best. Now uses the server-computed PRs and kcal from the
save response, which compare against the best from 50 sessions.
Previously kcal was computed on-the-fly in 3 places with inconsistent
inputs (hardcoded 80kg, missing GPS data, no demographics). Now a
shared computeSessionKcal() helper runs server-side using the best
available method (GPS + real demographics) and stores the result in
a new kcalEstimate field on WorkoutSession.
Kcal is recomputed on save, recalculate, GPX upload, and GPX delete.
The stats overview uses stored values with a legacy fallback for
sessions saved before this change.
Fetch up to 6 extra measurements beyond the display limit so the SMA
window is fully populated from the first displayed point. For users
with fewer total measurements, use a reduced window with Bessel's
correction and sqrt(w/k) sigma scaling to reflect increased uncertainty.
resolve() uses CWD which in production (adapter-node) is dist/, not the
project root. Detect the correct data directory at startup and add a
postbuild step to copy the embedding JSON files into dist/data/.
The deployment server couldn't fetch transformer models at runtime due to
restricted network access and permission errors writing to node_modules.
Add a prebuild script to download models during build and document
TRANSFORMERS_CACHE env var for configuring a shared writable cache path.
Replace hardcoded Nord colors with semantic CSS variables across all cospend
pages and shared components (FormSection, ImageUpload, SplitMethodSelector,
UsersList, PaymentModal, BarChart). Remove all dark mode override blocks.
Make BarChart font colors theme-reactive via isDark() + MutationObserver.
Extract reusable SaveFab component and use it on recipe edit and all cospend
edit/add pages. Remove Cancel buttons and back links in favor of browser
navigation. Replace raw checkboxes with Toggle component.
Move fitness measurement add/edit forms to separate routes with SaveFab.
Collapse profile section (sex/height) by default on the measure page.
Document theming rules in CLAUDE.md for future reference.
recipes: nutrition calculator with BLS/USDA matching, manual overwrites, and skip
Dual-source nutrition system using BLS (German, primary) and USDA (English, fallback)
with ML embedding matching (multilingual-e5-small / all-MiniLM-L6-v2), hybrid
substring-first search, and position-aware scoring heuristics.
Includes per-recipe and global manual ingredient overwrites, ingredient skip/exclude,
referenced recipe nutrition (base refs + anchor tags), section-name dedup,
amino acid tracking, and reactive client-side calculator with NutritionSummary component.
recipes: overhaul nutrition editor UI and defer saves to form submission
- Nutrition mappings and global overwrites are now local-only until
the recipe is saved, preventing premature DB writes on generate/edit
- Generate endpoint supports ?preview=true for non-persisting previews
- Show existing nutrition data immediately instead of requiring generate
- Replace raw checkboxes with Toggle component for global overwrites,
initialized from existing NutritionOverwrite records
- Fix search dropdown readability (solid backgrounds, proper theming)
- Use fuzzy search (fzf-style) for manual nutrition ingredient lookup
- Swap ingredient display: German primary, English in brackets
- Allow editing g/u on manually mapped ingredients
- Make translation optional: separate save (FAB) and translate buttons
- "Vollständig neu übersetzen" now triggers actual full retranslation
- Show existing translation inline instead of behind a button
- Replace nord0 dark backgrounds with semantic theme variables
- Nutrition mappings and global overwrites are now local-only until
the recipe is saved, preventing premature DB writes on generate/edit
- Generate endpoint supports ?preview=true for non-persisting previews
- Show existing nutrition data immediately instead of requiring generate
- Replace raw checkboxes with Toggle component for global overwrites,
initialized from existing NutritionOverwrite records
- Fix search dropdown readability (solid backgrounds, proper theming)
- Use fuzzy search (fzf-style) for manual nutrition ingredient lookup
- Swap ingredient display: German primary, English in brackets
- Allow editing g/u on manually mapped ingredients
- Make translation optional: separate save (FAB) and translate buttons
- "Vollständig neu übersetzen" now triggers actual full retranslation
- Show existing translation inline instead of behind a button
- Replace nord0 dark backgrounds with semantic theme variables
Complete household task management system behind task_users auth group:
- Task CRUD with recurring schedules, assignees, tags, and optional difficulty
- Blobcat SVG sticker rewards on completion, rarity weighted by difficulty
- Sticker collection page with calendar view and progress tracking
- Redesigned cards with left accent urgency strip, assignee PFP, round check button
- Weekday-based due date labels for tasks within 7 days
- Tasks link added to homepage LinksGrid
Dual-source nutrition system using BLS (German, primary) and USDA (English, fallback)
with ML embedding matching (multilingual-e5-small / all-MiniLM-L6-v2), hybrid
substring-first search, and position-aware scoring heuristics.
Includes per-recipe and global manual ingredient overwrites, ingredient skip/exclude,
referenced recipe nutrition (base refs + anchor tags), section-name dedup,
amino acid tracking, and reactive client-side calculator with NutritionSummary component.
Enable creating templates for GPS-tracked workouts with activity type
and optional interval training. GPS templates show activity/interval
info instead of exercise lists in cards, modals, and schedule. Starting
a GPS template pre-selects the interval and jumps to the map screen.
Weight chart now spaces data points proportionally to actual dates
instead of evenly. Days without a weight log no longer compress adjacent
points together. Uses Chart.js time scale with chartjs-adapter-date-fns.
Add TTS volume slider (default 80%) and audio duck toggle to voice
guidance settings. Announce "Workout started" when TTS initializes and
speak a full workout summary (time, distance, avg pace) on finish.
The finish summary reuses the existing TTS instance via handoff so it
plays fully without blocking the completion screen.
- Start native GPS service in paused state during pre-start (notification
shows "Waiting to start..." instead of running timer)
- Bump notification importance to IMPORTANCE_DEFAULT for lock screen
- Theme-aware glass blur overlay matching header style (dark/light mode)
- Dark Nord blue background for activity picker, audio stats panel
- Transparent overlay in pre-start, gradient fade for cancel button
- Use Toggle component for voice announcements checkbox
- Persist voice guidance settings to localStorage
- Derive voice language from page language, remove language selector
Adds a `debug` command that temporarily enables cleartext traffic and
points frontendDist at the local dev server, then restores release
config on exit via trap.
- Make map variables reactive ($state) so effects fire when map initializes
- Split single effect into polyline update + marker/view tracking
- Marker now always follows latestPoint instead of staying at start position
- Reset prevTrackLen on GPS restart to avoid skipping points
- Hide marker until real GPS position arrives
- Round saved distance to nearest 10m to avoid long floating-point values
Add Katechese section to the faith area with teachings from
P. Martin Ramm FSSP's Glaubenskurs. Includes landing page,
full 10 Commandments overview with biblical text (Ex 20),
and detailed first commandment page covering the virtue of
religion, its four acts, and inner/outer dimensions.
- Full-screen fixed map with controls overlaid at the bottom
- Activity type selector (running/walking/cycling/hiking) with proper
exercise mapping for history display
- GPS starts immediately on entering workout screen for faster lock
- GPS track attached to cardio exercise (like GPX upload) so history
shows distance, pace, splits, and map
- Add activityType field to workout state, session model, and sync
- Cancel button appears when workout is paused
- GPS Workout button only shown in Tauri app
Voice announcements run entirely in the Android foreground service
(works with screen locked). Configurable via web UI before starting
GPS: time-based or distance-based intervals, selectable metrics
(total time, distance, avg/split/current pace), language (en/de).
Also syncs workout pause/resume state to the native service — pausing
the workout timer now freezes the Android-side elapsed time, distance
accumulation, and TTS triggers.
Includes TTS engine detection with install prompt if none found, and
Android 11+ package visibility query for TTS service discovery.
- Add SvelteKit PageLoad/LayoutLoad/Actions types to recipe route files
- Fix possibly-undefined access on recipe.images, translations.en
- Fix parseFloat on number types in cospend split validation
- Use discriminated union guards for IngredientItem/InstructionItem
- Fix cache invalidation Promise<number> vs Promise<void> mismatch
- Suppress Mongoose model() complex union type error in WorkoutSession
Create shared toast store and Toast component mounted in root layout.
Wire toast.error() into all fitness API calls that previously failed
silently, and replace all alert() calls across recipes and cospend.
The PUT endpoint overwrote the exercises array with client data that
doesn't include gpsTrack/gpsPreview/totalDistance. Now merges existing
GPS data back into incoming exercises before saving.
Replace .save() with $set updateOne so only computed fields (totalVolume,
totalDistance, prs, gpsPreview) are written. Previously the full document
re-serialization could strip gpsTrack arrays.
Volume PRs were calculated client-side in the workout summary but never
saved to the database, so they didn't appear in history detail pages.
Add bestSetVolume PR detection to both session save and recalculate
endpoints, and render the new type in the history detail view.
The Gradle wrapper (gradlew, gradlew.bat, gradle/wrapper/) was
gitignored, causing the Docker APK build to fail with
"`gradlew` not found" since COPY doesn't include ignored files.
- Switch to Debian Trixie base for native JDK 21 and latest Rust
- Remove Adoptium APT repo workaround
- Only trigger Android CI on src-tauri/ and build config changes
- README: add Fitness section with APK download link
- Dockerfile.android: containerized build with Rust, Android SDK/NDK,
Java 21, Node 22, pnpm — builds and signs the APK
- CI workflow: builds APK in container on push, deploys to
bocken.org/static/Bocken.apk via SCP
- Notification title: "Bocken — Tracking GPS for active Workout"
- Live updates with elapsed time, distance, and pace (min/km)
- Request POST_NOTIFICATIONS permission at runtime (Android 13+)
- Page titles: "- Fitness" → "- Bocken" (missed in prior commit)
The fitness pages were only precaching HTML shells, but SvelteKit
client-side navigation fetches __data.json instead. Without these
cached, navigating to workout/training while offline would fail.
restExerciseIdx and restSetIdx were sent by the client but never
persisted server-side, so other sessions couldn't display which
exercise/set the rest timer belonged to.
Cache fitness page shells and data routes in the service worker so
pages load offline. Queue finished workouts in IndexedDB when the
POST fails and auto-flush them on reconnect. Show an offline banner
on the completion screen so the user knows their workout will sync.
Remove Plus icon from Add Exercise button (translation already includes +).
Use --primary-contrast instead of hardcoded white for button text so it's
legible in dark mode (nord0 on dark, white on light).
Move GPS collection from WebView JS (watchPosition) to native Android
LocationForegroundService, which survives screen-off. JS polls native
side for accumulated points. Also: auto-enable GPS for cardio exercises,
filter saved track to workout duration only, fix live map batch updates,
notification tap opens active workout, and fix build script for pnpm.
Wraps the web app in a Tauri Android shell that provides native GPS
via the geolocation plugin. Includes foreground service for background
tracking, live map display, GPS data storage in workout sessions,
and route visualization in workout history.
Add SWIMMING_METS and ROWING_METS lookup tables from Ainsworth
Compendium for speed-based calorie estimation when distance+duration
are available. Add per-exercise flat-rate fallbacks (FLAT_RATE map)
instead of hardcoded if/else chains.
Add weekly goal as a solid horizontal line on the bar chart via a
custom Chart.js plugin. Cap bar width at 40px. Always show all 10
weeks including empty ones instead of trimming leading zeros.
Add cardioKcalEstimate.ts implementing tiered calorie estimation for
cardio exercises: Minetti gradient-dependent polynomials for GPS
run/walk/hike, cycling physics model, MET-based fallbacks from
Ainsworth Compendium, and flat-rate estimates. Wire cardio kcal into
SessionCard, workout completion screen, history detail, and stats
overview API alongside existing strength kcal (Lytle). Move citation
info from stats overview to clickable DOI links on workout detail
kcal pill.
Separate streak counter from stat tiles into its own component with
animated aura effects: glow (1w), particles (2w), fire (3w), and
fire + lightning bolts (6/12/24w). Fire animations tuned for energetic
workout feel with faster durations and upward-anchored scaling.
On desktop, streak sits beside the workouts chart; on mobile, above it.
Estimate strength workout energy expenditure using the Lytle et al. multiple
linear regression model. Maps all 77 exercises to 7 studied categories with
confidence levels. Shows kcal on stats page (cumulative), session cards,
workout detail, and workout completion screen. Supports sex/height demographics
via profile section on measure page. Includes info tooltip with DOI reference.
Add per-exercise de property with translated name and instructions.
Add shared term translation map for bodyPart, equipment, target, and
muscle names. Add localizeExercise() and translateTerm() helpers.
Update all display components to use localized fields (localName,
localBodyPart, localEquipment, etc.) and pass lang to search/lookup.
Store a per-user weekly workout target (1-14) in a new FitnessGoal model.
Compute consecutive-week streak from WorkoutSession history via a new
/api/fitness/goal endpoint. Display streak as a 4th lifetime card on the
stats page with an inline goal editor modal.
Use SvelteKit param matchers for bilingual URL routing (e.g. /fitness/stats
and /fitness/statistik). Add centralized i18n module with translation
dictionary, language detection from URL, and path conversion utilities.
Translate all UI text across pages, components, and navigation.
- Replace floating action button with a subdued + icon in the templates
header row next to the Schedule button
- Use --primary-contrast (white/nord0) instead of hardcoded white for
text on primary-colored backgrounds so dark mode has proper contrast
- Respect data-theme="light"/"dark" attrs in addition to prefers-color-scheme
Users can define a custom order of templates (e.g., Push → Pull → Legs).
Based on the last completed session, the next workout in rotation is
recommended via a prominent banner and the floating action button.
- New WorkoutSchedule MongoDB model (per-user template order)
- GET/PUT /api/fitness/schedule API endpoints
- Schedule editor modal with reorder and add/remove
- Action button starts next scheduled workout when schedule exists
When finishing a template-based workout, compares completed sets against
the source template. If weights, reps, or set counts differ, shows a
visual diff with old→new values and a button to update the template,
letting templates grow with the user's strength progression.
- Validate exerciseId instead of name (templates use exerciseId, not name)
- Remove mandatory reps minimum from template sets
- Allow exercises with empty sets in schema and API validation
The authorization hook already calls locals.auth() which can set cookies.
Layout server loads calling auth() again caused a race where cookies.set()
fired after the response started streaming. Now the hook stashes the session
on locals.session and all layouts reuse it.
Decouple name input from live sync by using a local variable that only
commits to workout state on blur/Enter. Remote name updates are applied
only when the input is not focused, preventing the sync layer from
overwriting in-progress edits.
- SessionCard SVG: cosine-corrected coordinates with proper aspect ratio (xMidYMid meet)
- SessionCard: use --color-primary for track/distance/pace, add Gauge icon for pace
- History detail: theme-reactive pace chart colors via MutationObserver + matchMedia
- History detail: add Gauge icon, accent color for distance/pace stats, remove "avg" label
- Move GPS remove button from info view to edit screen
- Add Leaflet map preview to edit screen
- Remove data points count from GPS indicators
Disable initial animation on all Chart.js charts (FitnessChart and
cospend BarChart) while keeping transition animations for interactions.
Add linear regression trendline with ±1σ uncertainty bands to exercise
charts (Est. 1RM, Max Weight, Total Volume).
Add GPX file upload for cardio exercises in workout history. Parses
GPX track points and stores them in the session. Shows route map
(Leaflet), pace-over-distance chart (Chart.js), and per-km splits
table with color-coded fast/slow pacing. Auto-fills distance and
duration on single-set exercises. Disables Chart.js animations.
Show summary screen after finishing a workout instead of immediately
redirecting. Displays duration, tonnage, distance, per-exercise stats
(pace, e1RM, top weight), and detected PRs compared to previous session.
Redesign rest timer as inline bar with linear decay placed after completed set.
Add set removal (X button), @ separator column for RPE, and N/A for missing
previous values. Enable editing past workouts (date, duration, exercises, sets)
from the history detail page.
- Add metrics system (weight/reps/rpe/distance/duration) per exercise type
so cardio exercises show distance+duration instead of weight+reps
- Add 8 new cardio exercises: swimming, hiking, rowing outdoor, cycling
outdoor, elliptical, stair climber, jump rope, walking
- Add bilateral flag to dumbbell exercises for accurate tonnage calculation
- Make SetTable, SessionCard, history detail, template editor, and exercise
stats API all render/compute dynamically based on exercise metrics
- Rename Profile to Stats with lifetime cards: workouts, tonnage, cardio km
- Move route /fitness/profile -> /fitness/stats, API /stats/profile -> /stats/overview
Replace substring matching with a shared fuzzy scorer that matches
characters in order (non-contiguous) with bonuses for consecutive
and word-boundary hits. Results are ranked by match quality.
Enables real-time workout synchronization across devices using
Server-Sent Events and an ephemeral MongoDB document (24h TTL).
Rest timers now use absolute timestamps instead of interval-based
countdown for accurate cross-device sync. Adds +/-30s rest timer
adjust buttons.
Replace hardcoded Nord color references with semantic CSS variables
across all fitness components and pages. Use --color-primary instead
of --nord8 for interactive elements (auto-switches between --nord10
in light mode and --nord8 in dark mode). Change RPE color from
--nord13 (yellow) to --nord12 (orange) for better light mode contrast.
Fix mobile responsiveness on measure page form inputs.
- Add exerciseId to WorkoutSession model (interface + schema)
- Fix button-in-button hydration warning in TemplateCard (use div)
- Expand FitnessChart dataset type to include all Chart.js properties
- Fix getTime type error in session update with proper cast
- Fix weight nullable type in profile stats with non-null assertion
- Fix $or query typing in templates list endpoint
- Re-add gym link on homepage pointing to /fitness
Add all 5 PPL+Upper/Lower templates matching the target split,
with Day 3 (Legs) adjusted to include Bulgarian split squats and
standing calf raises, and Day 5 (Lower) reworked with front squats,
hip thrusts, and goblet squats — all equipment-free of machines.
Also adds incline row, decline crunch, flat leg raise, and nordic
hamstring curl to the exercise list, and updates the WorkoutTemplate
model to use exerciseId instead of name for exercise references.
- 5-tab layout (Profile, History, Workout, Exercises, Measure) with shared header nav
- Workout system: template CRUD, active workout on /fitness/workout/active with localStorage persistence, pause/resume timer, rest timer, RPE input
- Shared workout singleton (getWorkout) so active workout state is accessible across all fitness routes
- Floating workout FAB indicator on all /fitness routes when workout is active
- AddActionButton component for button-based FABs (measure + template creation)
- Profile page with workouts-per-week bar chart and weight line chart with SMA trend line + ±1σ confidence band
- Exercise detail with history, charts, and records tabs using static exercise data
- Session history with grouped-by-month list, session detail with stats/PRs
- Body measurements with latest values, body part display, add form
- Card styling matching rosary/prayer route patterns (accent-dark, nord5 light, box-shadow, hover lift)
- FitnessChart: fix SSR hang by moving Chart.register to client-side, remove redundant $effect
- Exercise API: use static in-repo data instead of empty MongoDB collection
- Workout finish: include exercise name for WorkoutSession model validation
Search component needs access to all recipes to filter across
categories/tags/etc. Previously these pages only passed their
pre-filtered subset, so selecting additional filters yielded
no results. Now each page fetches allRecipes in parallel and
passes it to Search, falling back to the route-specific subset
when no filters are active.
The glob in sync.ts targeted a nonexistent /src/routes/glaube/ directory
instead of the actual [faithLang=faithLang] parameterized route. This meant
zero prayer pages were ever precached for offline use.
- Fix glob to match [faithLang=faithLang] and expand param segments to
both language variants (glaube/faith, gebete/prayers, rosenkranz/rosary)
- Extract validPrayerSlugs to shared module for build-time route enumeration
- Add faith to service worker cacheable route regex
Show first mystery image at the Pater Noster instead of the Gloria Patri
by removing the early lbead2 trigger. Fix IntersectionObserver to prefer
the topmost intersecting entry so short _pater sections aren't skipped.
Use full viewport height (100dvh) for the SVG container to prevent
clipping at edges.
Override SearXNG's native CSS variables with Nord palette (cream white
light mode, true black dark mode). Replace SearXNG logo with Bocken
logo. Custom base.html template injects the CSS. Deploy script supports
reset to restore original state.
- Move theme toggle to right side of header (before notifications)
- Remove border from toggle, style consistently with other nav icons
- Fix dark mode hover background on toggle button
- Use exact Lucide SunMoon icon for system theme
- Dark logo filter in light mode
- Increase header height to 3.5rem
- Light mode with homepage warm beige palette (no pure white)
Match the homepage dark mode surface colors (#000/#111/#1a1a1a/#222)
for boxes, code, inputs, cards, and menus instead of Nord greys.
Keeps Nord only for accent colors and text.
- Floating glass pill navbar matching homepage/jellyfin header style
- Full black page background
- Custom head_navbar template: logo links to bocken.org, home button
for logged-in users, icons on all nav items, remove Help/Explore
- Icon-only nav on mobile, hide dropdown triangles, round avatar
- Deploy script to rsync theme + template to server
- Restyle header as floating glassmorphism pill matching bocken.org
- Replace Home/Favorites tab bar with icon buttons (house + heart) in header right
- Add play triangle overlay on card thumbnails with click-to-play
- Black backgrounds for detail page containers
- Always show detail logo regardless of screen width
- Mobile adjustments for pill header
Replace ~100 `any` usages with proper types: use existing interfaces
(RecipeModelType, BriefRecipeType, IPayment, etc.), Record<string, unknown>
for dynamic objects, unknown for catch clauses with proper narrowing,
and inline types for callbacks. Remaining `any` types are in Svelte
components and cases where mongoose document mutation requires casts.
LanguageSelector and language store previously only dispatched
languagechange events on '/'. Now any page that isn't a recipe or
faith route gets inline language switching via the custom event.
Add type annotations, JSDoc types, null checks, and proper generics
to eliminate all svelte-check errors. Key changes include:
- Type $state(null) variables to avoid 'never' inference
- Add JSDoc typedefs for plain <script> components
- Fix mongoose model typing with Model<any> to avoid union complexity
- Add App.Error/App.PageState interfaces in app.d.ts
- Fix tuple types to array types in types.ts
- Type catch block errors and API handler params
- Add null safety for DOM queries and optional chaining
- Add standard line-clamp property alongside -webkit- prefix
Rework UserHeader and LanguageSelector dropdowns to use wrapper +
triangle pattern with theme-aware backgrounds. Use solid grey for
inactive nav text instead of semi-transparent. Reduce instruction
info box shadow. Add emoji font to CompactCard favorites.
Active nav icons now fill with per-link colors (recipes, faith, cospend).
Cospend gets Lucide icons with background shape fills for Wallet and
RefreshCw. Shrink profile picture and use solid grey for inactive nav text.
Add theme cycling (system/light/dark) with localStorage persistence
and FOUC prevention. Restructure CSS color tokens to respond to
data-theme attribute across all components. Redesign header as a
floating glass pill bar with smooth view transitions including
clip-reveal logo animation.
Fix misplaced parenthesis in Math.abs() call that caused formatCurrency
to receive no currency arg (defaulting to EUR), and remove extra arg
in foreign currency formatting.
Allow recipes to specify a default pan shape (round, rectangular, gugelhupf)
with dimensions. On the recipe page, users can enter their own pan size to
auto-calculate an ingredient multiplier based on the 2D area ratio.
Add fetchpriority="high" and <link rel="preload"> hints to hero images
on both the recipe listing and detail pages. Also prefetch the full-size
hero image on card hover via new Image() to warm the cache before navigation.
Login link now includes callbackUrl for the current page. Logout
redirects intelligently: stays on public pages, falls back to the
recipe detail for /edit/[name], to the recipe root for auth-only
sub-routes (add, favorites, to-try, admin), and to / for cospend.
Each mystery now starts with its own title + Pater Noster card, before
the Ave Maria decade card. Transition cards (Gloria Patri + Fatima Prayer)
no longer contain the Pater Noster.
Mystery image column stays on the current mystery during transition prayers
(including the final Gloria/Fatima after decade 5) and only advances to the
next mystery at the Pater Noster card.
The new secret{N}_pater section IDs are tracked and mapped to their
corresponding large bead via svgActiveSection, so both transition and
pater sections highlight the correct large bead. CSS :has rules updated
for no-JS fallback.
When the client hydrates and finds the merged streak is still expired
(localStorage couldn't rescue it), reset to zero and push to the server.
This ensures subsequent SSR loads render the correct value from the start.
Fixes build failure on server where system npm has a broken
lru-cache/Yallist dependency. Also adds vite-node as an explicit
dev dependency so it's always available via pnpm.
Household-shared list of external recipes to try, with name, multiple
links, and optional notes. Includes add/edit/delete with confirmation.
Linked from the favorites page via a styled pill button.
Image morphs between CompactCard thumbnail and hero, title block
slides up from bottom, header persists across transitions. Only
activates for recipe detail navigations, not between list pages.
Remove the opacity 0→1 fade-in transition — it's annoying when the
image is already cached. The dominant color background handles the
loading state, so no transition needed.
Instead of generating/serving 20px placeholder images with blur CSS, extract
a perceptually accurate dominant color (Gaussian-weighted OKLAB average) and
use it as a solid background-color while the full image loads. Removes
placeholder image generation, blur CSS/JS, and placeholder directory references
across upload flows, API routes, service worker, and all card/hero components.
Adds admin bulk tool to backfill colors for existing recipes.
grep -oP '.' splits multi-byte emoji into individual bytes when the
locale is not UTF-8 (e.g. CI runners with LANG=C), causing pyftsubset
to fail on invalid codepoints.
Skip mobile sidebar/hamburger entirely when no links snippet is provided.
The nav with .no-links class stays in desktop layout at all screen widths.
Override UserHeader mobile styles from .no-links context to keep dropdown
opening downward with tail centered below the profile picture.
- LanguageSelector: add speech bubble tail, replace green active with
nord8 blue + dark text, remove floating gap
- Header: hide hamburger menu on mobile when no links, show profile
picture directly in top bar instead
- UserHeader: center mobile dropdown, fix tail color/position, add
profile picture overlay to tuck tail behind, add drop shadow
- Main layout: stop passing empty links snippet
Decouple lock-icon fill from nth-child color cycling via :not(.lock-icon),
use subtle --nord3 fill in both themes, add responsive lock sizing, and
bump mobile image heights (72→90px, 48→64px).
Shrink TagBall font/padding and TagCloud gap using clamp() for
fluid sizing across viewports. Add search input on the tags page
to filter through keywords.
- Reduce header height to 3rem with CSS variable --header-h
- Scale logo via --symbol-size variable, decrease nav link font sizes
- Replace JS-driven sidebar toggle with checkbox hack (:has selector)
- Separate drop shadow into own element for correct z-index layering
(top bar > sidebar > shadow)
- Bottom-align mobile nav links via ::before flex spacer
- Slide-in transition scoped to :has(:checked) to prevent resize artifacts
The hero-section's scaleY transform created a stacking context that
painted over the footer, and margin-bottom: -20vh over-compensated
for the parallax gap, pulling the footer into the recipe cards.
Derive margin-bottom from actual parallax parameters and make the
footer position: relative so it paints above the transform layer.
Use CSS min() in grid minmax to guarantee 2 tiles side-by-side at
any viewport width. Add responsive breakpoints (560px, 410px) to
progressively shrink SVG height, font size, and spacing.
Remove redundant `font-family: sans-serif` from 18 component-level
declarations — they now inherit the Helvetica/Arial/Noto Sans stack
from the global `*` selector in app.css.
Add self-hosted NotoColorEmoji subset (56 KB, down from 11 MB) as
fallback for systems without the Noto Color Emoji font installed.
The subset is generated at prebuild time via pyftsubset with a fixed
list of the ~32 emojis actually used on the site.
Migrate all recipe sub-pages from the old fixed-size Card component
inside flex-wrap Recipes wrapper to CompactCard with responsive CSS
grid for visual consistency with the main recipes page.
Generate heroIndex on the server and pass it to the client so SSR and
hydration pick the same hero recipe, eliminating the image swap on
first interaction.
- Fix g-tag dark mode hover text disappearing (explicit background-color)
- Scope compact card tag styles to avoid global/scoped CSS flash on load
- Add placeholder div to prevent layout shift when FilterPanel hydrates
- Improve LogicModeToggle contrast in light mode (nord4 → nord3/nord1)
- Bump compact card recipe name font-size to 1.1rem
- Full-bleed hero image with CSS parallax (scaleY technique matching TitleImgParallax)
- Hero picks random seasonal recipe with hashed image on each visit
- Left-aligned title, subheading, and featured recipe link over the hero
- Category chips with ellipsis collapse on small screens (<600px)
- Search bar anchored to hero/grid boundary regardless of chip count
- CompactCard redesign: 3/2 aspect ratio, rounded corners, subtle hover zoom
- Search component margin adjusted to sit flush at hero boundary
Merge nordtheme.css tokens and utility classes into app.css, import
app.css once in root layout, delete redundant files (nordtheme.css,
form.css, rosenkranz.css), move domain CSS to layouts, fix broken
shake keyframe in action_button.css, and scope form styles to the
two pages that need them. 10 CSS files → 6, 41 redundant imports removed.
The RosaryStreakStore singleton survives client-side navigation but
the first mount. Also reorder onMount to merge server data before
assigning to streak, preventing a frame of stale localStorage values.
Add Guardian Angel, Apostles' Creed, Tantum Ergo, Angelus, and Regina
Caeli to the prayers collection. Move standalone Angelus route into the
prayers system with a 301 redirect from the old path. Extract Easter
computation into shared utility ($lib/js/easter.svelte.ts) and use it
for liturgical season awareness: during Eastertide the rosary defaults
to Glorious mysteries and swaps Salve Regina for Regina Caeli; during
Lent it defaults to Sorrowful mysteries. Seasonal badges shown on both
the mystery selector and prayer sections.
SVG beads are now anchor links to prayer sections, with CSS :has(:target)
highlighting the active bead. Inline mystery images render in each decade
by default and hide when JS takes over. StreakCounter uses a form action
fallback for logged-in users and hides entirely for anonymous no-JS users.
Show images toggle now works via ?images= URL param like the other toggles.
Consolidate duplicate recipe API routes into a single
api/[recipeLang=recipeLang]/ structure. Both /api/recipes/ and
/api/rezepte/ URLs continue to work via the param matcher. Shared
read endpoints now serve both languages with caching for both.
Also: remove dead code (5 unused components, cookie.js) and the
redundant cron-execute recurring payment route.
Move components from flat src/lib/components/ into recipes/, faith/, and
cospend/ subdirectories. Replace ~144 relative imports across API routes
and lib files with $models, $utils, $types, and $lib aliases. Add $types
alias to svelte.config.js. Remove unused EditRecipe.svelte.
Deduplicates mobile PiP image code shared between the rosary page and
StickyImage. Adds fullscreen support to StickyImage and fixes hidden PiP
elements capturing pointer events via pointer-events: none default.
Generalize mystery images from sorrowful-only to all mystery types (joyful,
sorrowful, glorious, luminous). Add PiP fullscreen mode with tap-to-show
controls and double-tap to toggle enlarged/fullscreen.
Use taller edge pads (100vh) for before/after targets so images don't
peek at viewport top or bottom. Scroll to edge pads with zero offset
so previous/next images hide fully behind the sticky header.
- Resize handler calls pip.show()/hide() instead of just reposition(),
fixing PiP not appearing when resizing from desktop to mobile
- IntersectionObserver skips all updates when scrollY < 50, preventing
stale activeSection from re-scrolling SVG after jump-to-top
- One image per mystery (garden, flagellation, mocking, carry, crucifixion)
- Desktop: figcaption with artist, title (translated DE/EN), and year
- Fix Map.has vs `in` operator bug preventing PiP from showing
- Reposition PiP on image load to prevent off-screen positioning
- Mystery image column clips behind header (top: 0 + padding-top: 6rem)
- Snap SVG and images instantly to top; reset activeSection to cross
- Add "Bilder anzeigen" / "Show Images" toggle persisted to localStorage
- Bump mystery image column/PiP breakpoint from 900px to 1200px so
prayers keep full width on medium screens
- Fix PiP not appearing on page load by splitting $effect and using
tick() to wait for DOM before measuring element dimensions
- Fix Toggle checkbox default margin causing misalignment
Add a third grid column for sorrowful mystery images (mocking for
mysteries 2-3, crucifixion for mystery 5). Desktop uses a scrollable
sticky sidebar synced to prayer scroll position. Mobile shows a
floating PiP thumbnail. Extract prayer page PiP logic into reusable
StickyImage component.
Clicking a category on the bar chart now filters the recent activity
list to show only payments in that category. Includes a clear filter
button and empty state message. Also increases recent splits from 10
to 30 for better coverage when filtering.
The splitAmounts = { ...splitAmounts } pattern created a circular
dependency inside $effect blocks—reading and writing the same reactive
value—which Svelte 5 killed via loop protection, leaving the split
method selector non-reactive when selecting "50/50 + personal".
- Rosary: mystery selection, luminous toggle, and latin toggle fall back
to URL params (?mystery=, ?luminous=, ?latin=) for no-JS navigation
- Prayers/Angelus: latin toggle uses URL param fallback
- Search on prayers page hidden without JS (requires DOM queries)
- Toggle component supports href prop for link-based no-JS self-submit
- LanguageSelector uses <a> links with computed paths and :focus-within
dropdown for no-JS; displays correct language via server-provided prop
- Recipe language links use translated slugs from $page.data
- URL params cleaned via replaceState after hydration to avoid clutter
var(--color-bg-secondary) from app.css is not available since app.css
is never imported. Use var(--accent-dark) from nordtheme.css with
explicit light mode overrides using var(--nord5).
Replace var(--accent-dark) with var(--color-bg-secondary) which maps
to the correct color in both modes, removing dead @media overrides
that referenced the undefined var(--accent-light). Also match rosary
cross fill to Benedictus medal color in light mode.
Replace <text> elements using the crosses web font with inlined SVG
paths extracted from the font file. Web fonts in SVG <text> elements
don't load reliably on Android, causing fallback rendering.
Use sectionPositions as single source of truth for all bead coordinates.
Compute transition bead positions as midpoints between decades, generate
decade beads and hitboxes via loops, and adjust bead spacing.
Map each ending bead to its corresponding prayer (Gloria/Fatima,
Salve Regina, Schlussgebet, St. Michael, Paternoster, Sign of the Cross),
add scroll-to-top button with action_button styling, and fix SVG scroll
lock to prevent snap-back when scrolling to top.
Remove redundant CSS already handled by Prayer.svelte, drop unused
rosenkranz.css import, and replace inline BenedictusMedal component
(34KB, ~52 DOM elements) with a static SVG referenced via <image>.
Use fluid sidebar width (clamp) for smoother desktop/mobile transition.
The on:change handler on the Toggle component was silently ignored
since Toggle is a Svelte 5 component that doesn't support the Svelte 4
event directive. Replace with a reactive $effect that reverts to
today's mystery when luminous is excluded while selected.
- Extract Bible lookup logic into shared src/lib/server/bible.ts module
- Add build script to pre-generate all 20 mystery verse lookups as static data,
eliminating runtime API calls on rosary page load
- Update Prayer.svelte to pass showLatin/urlLang as snippet parameters; all 14
prayer components now conditionally render only visible language elements
instead of hiding via CSS
- Extract 4 inline mystery selector SVGs into MysteryIcon.svelte component
- Remove unused CSS selectors from angelus page
Rework the burst mode in FireEffect to use 24 data-driven particles
instead of the old scale-and-pop flame. Each particle has unique
position, size, delay, and duration for an organic rising effect.
Latch burst state in StreakAura so the animation plays its full
duration regardless of when the parent resets the prop.
- Add SearchInput component for reusable search UI
- Add search functionality to prayers list with two-tier results:
- Primary matches (name/searchTerms) shown first
- Secondary matches (text content) shown after with reduced opacity
- Add individual prayer pages with language-appropriate slugs
(e.g., /glaube/gebete/ave-maria, /faith/prayers/hail-mary)
- Make prayer cards clickable to navigate to individual pages
- Fix language visibility for prayers without Latin (BruderKlaus, Joseph)
- Add Prayer wrapper to MichaelGebet for consistent styling
- Use CSS columns for masonry layout with dynamic reordering
Add official Catholic English translations to all prayer components
for /faith/* routes. Prayer names on /faith/prayers are now displayed
in English. Remove unused Angelus.svelte component.
Add language toggle support for faith pages similar to recipes.
Routes now work in both German and English:
- /glaube ↔ /faith
- /glaube/gebete ↔ /faith/prayers
- /glaube/rosenkranz ↔ /faith/rosary
- /glaube/angelus ↔ /faith/angelus
- FireEffect now only contains fire-related styles
- StreakAura now only contains aura, number, halo, wing styles
- Fix unclosed <i> tags in JosephGebet.svelte
- Fetch streak data in +page.server.ts for logged-in users via API
- Initialize store once with server data, sync only runs once
- Only poll for reconnection in PWA mode when offline with pending changes
- Extract FireEffect to separate component with burst animation
- Convert StreakAura/StreakCounter to Svelte 5 runes ($props, $state, $derived)
- Fix SSR flash by using server data for initial render
- Add RosaryStreak MongoDB model for logged-in users
- Add /api/glaube/rosary-streak GET/POST endpoints
- Sync streak to server when logged in, merge local/server data
- Auto-sync when coming back online (PWA offline support)
- Falls back to localStorage for guests
Card.svelte uses recipe.images[0].mediapath for the hashed image path,
but the all_brief endpoints weren't fetching the images field, causing
new recipes to fall back to short_name.webp instead of the correct path.
The offline sync wasn't caching thumbnails because the images field
was missing from the MongoDB projection. Also add translations for
caching English recipe __data.json URLs.
- Service worker reports image caching progress back to main thread
- Sync progress shows current phase (recipes, pages, data, images)
- Display progress bar for image downloads in sync tooltip
- Use mediapath for thumbnail URLs (with hash for cache busting)
- Serve cached thumbnails as fallback for full/placeholder when offline
- Auto-sync recipes every 30 minutes when online in PWA mode
- Only show offline sync button when running as installed PWA
- Detect standalone mode via display-mode media query and iOS check
- Trigger initial sync on PWA install (appinstalled event)
- Listen for online event to sync when coming back online
- Store last sync time in localStorage to track sync intervals
- Add offline support for category, tag, icon list pages
- Add offline support for favorites page (stores locally for offline)
- Add offline support for season list page
- Cache root page and glaube pages for offline access
- Dynamically discover glaube routes at build time using Vite glob
- Add db functions for getAllCategories, getAllTags, getAllIcons
- Pre-cache __data.json for all category, tag, icon, season subroutes
- Update service worker to cache glaube and root page responses
- Add service worker with caching for build assets, static files, images, and pages
- Add IndexedDB storage for recipes (brief and full data)
- Add offline-db API endpoint for bulk recipe download
- Add offline sync button component in header
- Add offline-shell page for direct navigation fallback
- Pre-cache __data.json for client-side navigation
- Add +page.ts universal load functions with IndexedDB fallback
- Add PWA manifest and icons for installability
- Update recipe page to handle missing data gracefully
- Remove nested .wrapper div in recipe page using CSS Grid with full-bleed background
- Consolidate multiplier forms in IngredientsPage into single form
- Simplify fermentation conditionals in InstructionsPage with optional chaining
- Use conditional rendering instead of visibility wrapper in Search
- Remove unnecessary dialog wrapper in TitleImgParallax
- Remove unnecessary wrapper divs in Card component (.card_anchor, .div_div_image)
- Flatten Card HTML from 4 levels to 2 levels of nesting
- Create reusable createSearchFilter composable in $lib/js/searchFilter.svelte.ts
- Apply search filter composable to category, tag, and favorites pages
- Eliminate duplicate API fetch in recipe page by passing item from
server load to universal load instead of fetching twice
- Replace cheerio with simple regex in stripHtmlTags, removing ~200KB
dependency
- Refactor multiplier buttons in IngredientsPage to use loop instead
of 5 repeated form elements
- Move /rezepte/untranslated to /[recipeLang]/admin/untranslated and
delete legacy /rezepte/ layout files
- Add titles to category, tag, icon, season routes
- Add bilingual support (German/English) for recipe route titles
- Use consistent "Bocken Recipes" / "Bocken Rezepte" branding
- Change English tagline from "Bocken's Recipes" to "Bocken Recipes"
- Add titles to /glaube and /glaube/gebete pages
- Make tips-and-tricks page language-aware
The +page.server.ts fetches recipe data and strips HTML tags server-side
to avoid bundling cheerio in the client. However, the universal load in
+page.ts wasn't including this data in its return value.
Fixed by:
1. Having +page.server.ts fetch the recipe directly (since it runs before
+page.ts and can't access its data via parent())
2. Adding the `data` parameter to +page.ts and spreading it in the return
The image upload broke because formData.append() was being called in the
async callback of use:enhance, which runs AFTER the form submission.
Moved the append call to the outer function which runs BEFORE submission.
Also cleaned up debug console.log statements from CardAdd.svelte.
Replace $effect + bind:files approach with straightforward onchange handler:
- Use event.currentTarget.files[0] to get selected file
- Avoid reactive complexity that caused infinite loops
- Add bind:this reference to file input for clearing
- Clean implementation that works reliably in Svelte 5
Replace event handler approach with bind:files and $effect:
- Use bind:files on file input for reactive FileList binding
- Use $effect to react to file selection and handle validation
- Properly clean up blob URLs to prevent memory leaks
- Remove exported functions that aren't used externally
- Add key to each block for tags
- Fix self-assignment warning in tag handling
The previous implementation used onchange with this.files which doesn't
work in Svelte 5. The new approach uses the idiomatic bind:files pattern.
The show_local_image function was using `this.files[0]` which doesn't work
in Svelte 5's onchange handlers. Changed to use `event.target.files[0]`
to properly access the selected file.
This fixes recipe image uploads not working because the file was never
being captured from the input element.
Add console logging in the browser to track image selection and form
submission:
- Log when user selects an image file in CardAdd component
- Log file validation steps (MIME type, size)
- Log form submission and FormData preparation
- Log whether image is being appended to form
This helps diagnose if the issue is client-side (image not selected/sent)
or server-side (image not received/processed).
Add detailed console logging throughout the image upload pipeline to help
diagnose upload issues:
- Log file metadata and validation steps in imageValidation.ts
- Log image processing and file saving operations in imageProcessing.ts
- Log form data and processing steps in recipe add page action
- Log API request details and upload progress in img/add endpoint
All logs are prefixed with component name ([ImageValidation], [ImageProcessing],
[RecipeAdd], [API:ImgAdd]) for easy filtering and debugging.
The Ave Maria counter was not updating the visualization when clicked.
Fixed by wrapping decadeCounters in $state() for proper reactivity tracking
and correcting data-section attributes to use template literals instead of
string literals in the decade loop.
- Move HTML stripping to server-side to remove cheerio from client bundle (247KB reduction)
- Add terser minification with console/debugger removal
- Enable manual code chunking for chart.js and auth libraries
- Convert TTF fonts to WOFF2 format (~900KB savings)
- Enable brotli/gzip precompression in adapter
- Update CSS to prefer WOFF2 with TTF fallback
Changed recipe image upload behavior to only process images when the
form is submitted, rather than immediately on file selection. This
prevents orphaned image files when users abandon the form.
Changes:
- CardAdd.svelte: Preview only, store File object instead of uploading
- Created imageProcessing.ts: Shared utility for image processing
- Add/edit page clients: Use selected_image_file instead of filename
- Add/edit page servers: Process and save images during form submission
- Images are validated, hashed, and saved in multiple formats on submit
Benefits:
- No orphaned files from abandoned forms
- Faster initial file selection experience
- Server-side image processing ensures security validation
- Cleaner architecture with shared processing logic
Updated recipe image handling to use checksummed filenames for proper
cache busting. When uploading a new image during recipe edit, old image
files (both hashed and unhashed versions) are now properly deleted from
all directories (full, thumb, placeholder).
Changes:
- CardAdd.svelte: Use checksummed filename from upload response
- Edit page server: Add deleteRecipeImage() helper to remove old images
- Edit page server: Delete old images when new image is uploaded
Settlement Display Improvements:
- Redesigned settlement cards with distinct visual style
- Added gradient background and colored top border stripe
- Centered layout with prominent amount display
- Added settlement badge with icon
- Responsive vertical layout on mobile devices
- Fixed overflow issues on small screens
Payment Edit Enhancements:
- Added automatic split recalculation when amount changes
- Implemented editable personal amounts for personal_equal split method
- Real-time validation showing total personal and remainder
- Live split preview with automatic updates
- Support for all split methods: equal, full, personal_equal, proportional
- Foreign currency support with exchange rate recalculation
- Safeguards against infinite recalculation loops
- Improved UI with split method info display
- Responsive design for mobile devices
Implement comprehensive caching for all cospend routes to improve performance:
Cache Implementation:
- Balance API: 30-minute TTL for user balances and global balances
- Debts API: 15-minute TTL for debt breakdown calculations
- Payments List: 10-minute TTL with pagination support
- Individual Payment: 30-minute TTL for payment details
Cache Invalidation:
- Created invalidateCospendCaches() helper function
- Invalidates user balances, debts, and payment lists on mutations
- Applied to payment create, update, and delete operations
- Applied to recurring payment execution (manual and cron)
- Center isBaseRecipe toggle by changing display to inline-flex
- Fix note field editing by adding textarea with bindable value
- Clear instruction step input after submission instead of restoring placeholder
- Style note textarea with transparent background and lighter placeholder text
The instruction field now properly clears on submission, while ingredient fields retain their previous values.
Add optional baseMultiplier field to ingredient and instruction references, allowing base recipes to be included at scaled amounts (e.g., 0.5 for half the recipe).
- Add baseMultiplier field to Recipe schema with default value of 1
- Update TypeScript types to include baseMultiplier
- Add multiplier input field to BaseRecipeSelector modal
- Apply baseMultiplier to ingredient amounts during flattening
- Combine baseMultiplier with recipe multiplier in links
- Display and allow editing baseMultiplier in recipe editor
The multiplier cascades through nested references and works alongside the standard recipe multiplier for compound scaling.
- Add hidden input to properly serialize isBaseRecipe boolean as "true"/"false" string
- Replace plain HTML checkbox with Toggle component for consistent styling
- Checkbox values don't submit when unchecked; hidden input ensures value is always sent
Previously, when users approved or skipped translations in the recipe forms, the translation data wasn't being saved to the database. This was caused by a timing issue where the form was submitted before Svelte had updated the DOM with the hidden inputs containing the translation data.
Fixed by using tick() to wait for pending state changes to be applied before submitting the form.
Convert recipe data functions to $derived reactive variables to prevent
infinite $effect loops. Previously, calling functions inline in component
props created new objects on every reactive check, causing the
TranslationApproval component's syncBaseRecipeReferences $effect to run
continuously, resulting in the translation workflow hanging.
Refactor recipe add/edit routes from client-side fetch to proper SvelteKit
form actions with progressive enhancement and comprehensive security improvements.
**Security Enhancements:**
- Implement 5-layer image validation (file size, MIME type, extension, magic bytes, Sharp structure)
- Replace insecure base64 JSON encoding with FormData for file uploads
- Add file-type@19 dependency for magic bytes validation
- Validate actual file type via magic bytes to prevent file type spoofing
**Progressive Enhancement:**
- Forms now work without JavaScript using native browser submission
- Add use:enhance for improved client-side UX when JS is available
- Serialize complex nested data (ingredients/instructions) via JSON in hidden fields
- Translation workflow integrated via programmatic form submission
**Bug Fixes:**
- Add type="button" to all interactive buttons in CreateIngredientList and CreateStepList
to prevent premature form submission when clicking on ingredients/steps
- Fix SSR errors by using season_local state instead of get_season() DOM query
- Fix redirect handling in form actions (redirects were being caught as errors)
- Fix TranslationApproval to handle recipes without images using null-safe checks
- Add reactive effect to sync editableEnglish.images with germanData.images length
- Detect and hide 150x150 placeholder images in CardAdd component
**Features:**
- Make image uploads optional for recipe creation (use placeholder based on short_name)
- Handle three image scenarios in edit: keep existing, upload new, rename on short_name change
- Automatic image file renaming across full/thumb/placeholder directories when short_name changes
- Change detection for partial translation updates in edit mode
**Technical Changes:**
- Create imageValidation.ts utility with comprehensive file validation
- Create recipeFormHelpers.ts for data extraction, validation, and serialization
- Refactor /api/rezepte/img/add endpoint to use FormData instead of base64
- Update CardAdd component to upload via FormData immediately with proper error handling
- Use Image API for placeholder detection (avoids CORS issues with fetch)
Fixes issues where translation buttons and rosary bead counter were not working
due to incomplete Svelte 5 migration. Updated parent components to use new
callback prop syntax (onapproved/onskipped/oncancelled) and lowercase onclick
handlers to match child component expectations.
- Fix TranslationApproval event handlers in recipe add page
- Fix CounterButton onclick prop in rosary page
Updated authentication packages to latest versions for security fixes:
- @auth/sveltekit: 1.10.0 → 1.11.1 (includes nodemailer security fix)
- @auth/core: removed from devDependencies (transitively pulled as 0.41.1)
Changed imports to use @auth/sveltekit/providers instead of @auth/core/providers
and removed unused imports from hooks.server.ts.
- Add LogicModeToggle component to switch between AND and OR filter logic
- Enable multi-select for category and icon filters in OR mode
- Update Search component to handle both AND and OR filtering logic
- Resize Toggle component to match LogicModeToggle size (44px x 24px)
- Position logic mode toggle on the left side of filter panel
- Auto-convert arrays to single values when switching from OR to AND mode
- In OR mode: recipes match if they satisfy ANY active filter
- In AND mode: recipes must satisfy ALL active filters
- Move categories logic into Search component for centralization
- Add isLoggedIn prop to SeasonLayout and IconLayout components
- Fix FilterPanel CSS to properly handle hidden favorites filter
- Fix FavoritesFilter to trigger onToggle when checkbox changes
- Update Search effect to track all filter states (category, tags, icon, season, favorites)
- Hide favorites filter on favorites page while maintaining proper grid layout
- All filters now work consistently across entire site
Migrated all components and routes from Svelte 4 to Svelte 5 syntax:
- Converted export let → $props() with generic type syntax
- Replaced createEventDispatcher → callback props
- Migrated $: reactive statements → $derived() and $effect()
- Updated two-way bindings with $bindable()
- Fixed TypeScript syntax: added lang="ts" to script tags
- Converted inline type annotations to generic parameter syntax
- Updated deprecated event directives to Svelte 5 syntax:
- on:click → onclick
- on:submit → onsubmit
- on:change → onchange
- Converted deprecated <slot> elements → {@render children()}
- Updated slot props to Snippet types
- Fixed season/icon selector components with {#snippet} blocks
- Fixed non-reactive state by converting let → $state()
- Fixed infinite loop in EnhancedBalance by converting $effect → $derived
- Fixed Chart.js integration by converting $state proxies to plain arrays
- Updated cospend dashboard and payment pages with proper reactivity
- Migrated 20+ route files from export let data → $props()
- Fixed TypeScript type annotations in page components
- Updated reactive statements in error and cospend routes
- Removed invalid onchange attribute from Toggle component
- Fixed modal ID isolation in CreateIngredientList/CreateStepList
- Fixed dark mode button visibility in TranslationApproval
- Build now succeeds with zero deprecation warnings
All functionality tested and working. No breaking changes to user experience.
- Migrate TranslationApproval and edit page to Svelte 5 runes ($props, $state, $derived)
- Fix empty modal issue by eagerly initializing editableEnglish from germanData
- Fix modal state isolation by adding language-specific modal IDs (en/de)
- Resolve cross-contamination where English modals opened German ingredient/instruction editors
- Improve button icon visibility in dark mode by setting white fill color
- Replace createEventDispatcher with callback props for Svelte 5 compatibility
Implements Redis caching layer for recipe endpoints to reduce MongoDB load and improve response times:
- Install ioredis for Redis client with TypeScript support
- Create cache.ts with namespaced keys (homepage: prefix) to avoid conflicts with other Redis applications
- Add caching to recipe query endpoints (all_brief, by tag, in_season) with 1-hour TTL
- Implement automatic cache invalidation on recipe create/edit/delete operations
- Cache recipes before randomization to maximize cache reuse while maintaining random order per request
- Add graceful fallback to MongoDB if Redis is unavailable
- Update .env.example with Redis configuration (REDIS_HOST, REDIS_PORT)
Fixed 12 type errors by adding proper type annotations:
Quick Wins Completed:
- do_on_key.js: Added JSDoc types for KeyboardEvent and function parameters
- randomize.js: Added JSDoc types with generic template for array shuffling
- cookie.js: Added JSDoc types for Request API
- stripHtmlTags.ts: Added TypeScript types for string parameter
Progress: 12/1239 errors fixed (Quick Wins - Category 1 partial)
Created TODO_cleanup.md to track remaining 1227 type errors systematically.
Migrated all components and routes to Svelte 5 syntax standards:
Event Handlers:
- Updated all deprecated on:* directives to new on* attribute syntax
- Changed on:click → onclick, on:keydown → onkeydown, on:input → oninput
- Updated on:blur, on:focus, on:load, on:submit, on:cancel handlers
Reactive State:
- Added $state() declarations for all reactive variables
- Fixed non-reactive update warnings in layout and component files
Component API:
- Replaced <slot /> with {@render children()} pattern
- Added children prop to components using slots
Accessibility:
- Added id attributes to inputs and for attributes to labels
- Fixed label-control associations across forms
- Removed event listeners from non-interactive elements
HTML Fixes:
- Fixed self-closing textarea tags
- Corrected implicitly closed elements
- Proper element nesting
CSS Cleanup:
- Removed 20+ unused CSS selectors across components
- Cleaned up orphaned styles from refactoring
All vite-plugin-svelte warnings resolved. Codebase now fully compliant with Svelte 5.
Changed FilterPanel from conditional rendering to always-rendered with visibility control. This prevents layout shift when JavaScript loads by reserving the space upfront while keeping it visually hidden for non-JS users.
The Login link was appearing light blue and nord purple when visited
in production/preview builds due to CSS specificity conflicts with
global nordtheme.css link styles. Added !important flags to enforce
white color for all link states and nord8 for hover/focus states.
- Created administration page at /{recipeLang}/administration accessible only to rezepte_users
- Moved alt-text generator from /admin to /{recipeLang}/admin/alt-text-generator
- Added "Administration" link to user profile dropdown for rezepte_users
- Removed "Unübersetzt" link from main navigation (now accessed via administration page)
- Administration page provides card-based UI with links to:
- Untranslated Recipes management
- AI Alt-Text Generator
- Both features now integrated into recipe language routing structure
- Added server-side authentication to all admin routes
- Removed 'entry' class from desktop logo to match mobile implementation
- Added left padding to nav for consistent logo alignment across viewports
- Reduces excessive padding when tabbing through logo links
Updated CSS selectors to specifically target 'a.entry' instead of '.entry' to properly apply styling to the Login link. This ensures the link appears white in both light and dark modes, matching the styling of other navigation links.
- Change login button text from "Log In" to "Login"
- Update filter labels (Kategorie, Icon, Tags, Saison, Favoriten) to use darker color (nord2) in light mode for better readability
- Improve placeholder text contrast in filter inputs by using lighter shade (nord4)
- Maintain light color scheme (nord6) for filter labels in dark mode
- Implement local Ollama integration for bilingual (DE/EN) alt text generation
- Add image management UI to German edit page and English translation section
- Update Card and recipe detail pages to display alt text from images array
- Include GenerateAltTextButton component for manual alt text generation
- Add bulk processing admin page for batch alt text generation
- Optimize images to 1024x1024 before AI processing for 75% faster generation
- Store alt text in recipe.images[].alt and translations.en.images[].alt
- Add aria-labels to icon-only links (add button, edit button, logo, nav toggle)
- Add main landmark element for better page structure
- Fix heading hierarchy on recipe pages (h1 → h2 → h3 progression)
- Add role="status" to loading placeholders to allow aria-label usage
- Update link colors from red to blue for better contrast in both light and dark modes
- Change hover colors from orange/red to light blue across all interactive elements
- Reduce font size of section labels (Season, Keywords) while maintaining semantic structure
These changes address PageSpeed accessibility recommendations including low-contrast text,
missing accessible names, prohibited ARIA attributes, missing landmarks, and improper
heading order.
- Use English short_name for base recipe links when viewing English recipes
- Fix edit page to use /rezepte/edit/<shortname> instead of /{data.lang}/edit/<shortname>
- Ensures base recipe reference links work correctly in both languages
- Add recursive population for nested base recipe references (up to 3 levels deep) in API endpoints
- Implement recursive mapping of baseRecipeRef to resolvedRecipe for all nesting levels
- Add recursive flattening functions in frontend components to handle nested references
- Fix TranslationApproval to use short_name instead of ObjectId for base recipe lookups
- Add circular reference detection to prevent infinite loops
This ensures that when Recipe A references Recipe B as a base, and Recipe B references Recipe C, all three recipes' content is properly displayed.
- Add language prop to CreateIngredientList and CreateStepList components
- Support both 'de' and 'en' with translation dictionaries
- All UI labels now respect the lang prop
- Implement syncBaseRecipeReferences() in TranslationApproval
- Always runs on component mount (not just for new translations)
- Fetches English names for base recipe references
- Merges German structure with existing English translations
- Preserves existing translations while adding new base recipe refs
- Enhance partial translation in translation.ts
- Handle base recipe reference fields (itemsBefore/itemsAfter, stepsBefore/stepsAfter)
- Detect changes using JSON comparison
- Only re-translate fields that changed
- Ensures additional items/steps in base recipe refs are preserved during updates
Replaced the plain EditableIngredients and EditableInstructions components
with the styled CreateIngredientList and CreateStepList components to match
the German recipe editing UI above:
- Now displays English translation with same styling as German recipe
- Ingredients and instructions shown in familiar two-column layout
- Timing fields (preparation, baking, fermentation, cooking, total_time)
integrated into CreateStepList component instead of separate fields
- Added getters/setters for add_info object to enable two-way binding
between CreateStepList edits and editableEnglish data
- Removed redundant field editors for baking/fermentation since they're
now part of CreateStepList
Translation approval UI now has consistent styling with the rest of the
edit page for a more cohesive user experience.
Streamlined the translation approval workflow by removing the side-by-side
German/English comparison and focusing on the English translation only:
- TranslationApproval: Removed two-column comparison grid, now shows only
English translation in single-column layout for cleaner UI
- Added 'Vollständig neu übersetzen' button to TranslationApproval actions
section (next to Re-translate button as requested)
- Edit page: Removed standalone 'Vollständig neu übersetzen' button from
submit buttons, now handled within translation approval workflow
- Updated CSS to use simplified .translation-preview and .field-section
classes instead of grid layout
The German original is still accessible above in the edit form, making
the translation approval process more focused and less cluttered.
Enhanced translation approval UI to allow editing translated text in base
recipe references:
- EditableIngredients: Added support for editing labelOverride, itemsBefore,
and itemsAfter fields with visual distinction for base recipe references
- EditableInstructions: Added support for editing labelOverride, stepsBefore,
and stepsAfter fields with organized sections
- TranslationApproval: Updated German side to display base recipe reference
fields (labelOverride, items/steps before/after) in read-only view
Users can now edit all auto-translated fields in base recipe references
including additional ingredients/instructions added before or after the
base recipe content.
Fixed three issues with base recipe translation support:
1. Base recipe content not loading in English - English API endpoint now
populates baseRecipeRef fields to resolve base recipe data
2. itemsBefore/itemsAfter and stepsBefore/stepsAfter not being detected as
changed - enhanced change detection to properly track all base recipe
reference fields for re-translation
3. Base recipe name labels showing German text in English view - display
components now use translated base recipe names as label fallback
- Add setTimeout to defer modal.close() to next tick for proper Svelte binding updates
- Add HTMLDialogElement type casting with null checks for modal elements
- Add cancel event handlers to reset state when Escape is pressed
- Ensures modals close reliably when Enter is pressed to submit
- Prevents orphaned state when modals are dismissed with Escape
Add comprehensive base recipe system allowing recipes to reference other recipes dynamically. References can include custom items before/after the base recipe content and render as unified lists.
Features:
- Mark recipes as base recipes with isBaseRecipe flag
- Insert base recipe references at any position in ingredients/instructions
- Add custom items before/after referenced content (itemsBefore/itemsAfter, stepsBefore/stepsAfter)
- Combined rendering displays all items in single unified lists
- Full edit/remove functionality for additional items with modal reuse
- Empty item validation prevents accidental blank entries
- HTML rendering in section titles for proper <wbr> and ­ support
- Reference links in section headings with multiplier preservation
- Subtle hover effects (2% scale) on add buttons
- Translation support for all reference fields
- Deletion handling expands references before removing base recipes
Add small lock icons in the top right corner of links that require authentication (streaming, family photos, cloud, shopping, family tree, transmission, documents, and audiobooks). The icons use SVG symbol references for efficient reuse and adapt to dark mode automatically.
Add new page at /rezepte/untranslated for recipe admins to view and manage recipes without approved English translations. Includes translation status tracking, statistics dashboard, and visual badges.
Changes:
- Add API endpoint to fetch recipes without approved translations
- Create untranslated recipes page with auth checks for rezepte_users group
- Add translation status badges to Card component (pending, needs_update, none)
- Add database index on translations.en.translationStatus for performance
- Create layout for /rezepte route with header navigation
- Add "Unübersetzt" link to navigation for authorized users
Replace non-reactive window.location.pathname with SvelteKit's reactive $page store to ensure language selector updates when navigating via browser back/forward buttons.
Fixes critical bug where recipes could not be deleted properly. The delete function had an early return statement that prevented database deletion from executing, leaving orphaned entries. Additionally, deleted recipes were not removed from users' favorites lists.
Changes:
- Remove premature return statement blocking database deletion
- Fix malformed fetch call structure (headers were inside body JSON)
- Add UserFavorites cleanup to remove deleted recipes from all users' favorites
- Ensure complete cleanup: database entry, image files (hashed and unhashed), and favorites references
Adds box-sizing: border-box to all filter inputs after 'all: unset' to ensure padding is included within the 100% width calculation, preventing horizontal overflow and ensuring equal left/right margins on small screens.
Changes deployment process to build in default 'build' directory, then safely deploy to 'dist' directory by stopping the service first, ensuring clean deployment without serving partial builds.
Fixed CSS specificity issue where filter-panel classes were preventing vertical stacking on small screens. Also added drop-shadow to all filter dropdowns for improved visual depth.
Fix the deployment script to properly force the remote server to always
match the git repository state, regardless of local changes.
Changes:
- Replace invalid `git pull --force` with proper fetch and reset
- Add `git remote set-url origin` to ensure correct URL with auth token
- Use `git fetch origin` to download latest changes
- Use `git reset --hard origin/master` to force match remote state
This ensures clean deployments even if there are local modifications or
conflicts on the remote server, while preserving untracked files like .env.
Add progressive enhancement to hide filter panel when JavaScript is
disabled, and conditionally render favorites filter based on login status.
Search Component:
- Added showFilters state (default false)
- Set showFilters to true in onMount when JS is enabled
- Wrapped FilterPanel in {#if showFilters} for graceful degradation
- Filters hidden without JavaScript, visible with JS
FilterPanel:
- Split grid layout into two variants:
- with-favorites: 5 columns (120px 120px 1fr 160px 90px)
- without-favorites: 4 columns (120px 120px 1fr 160px)
- Conditionally render FavoritesFilter only when isLoggedIn
- Apply appropriate class based on login status
FavoritesFilter:
- Simplified template (no internal login check)
- Only rendered when user is logged in via FilterPanel
UX:
- Non-JS browsers: Simple search only, filters gracefully hidden
- Not logged in: 4-column layout without favorites filter
- Logged in: 5-column layout with favorites filter
Add advanced filtering with category, tags (multi-select), icon, season,
and favorites filters. All filters use consistent chip-based dropdown UI
with type-to-search functionality.
New Components:
- TagChip.svelte: Reusable chip component with selected/removable states
- CategoryFilter.svelte: Single-select category with chip dropdown
- TagFilter.svelte: Multi-select tags with AND logic and chip dropdown
- IconFilter.svelte: Single-select emoji icon with chip dropdown
- SeasonFilter.svelte: Multi-select months with chip dropdown
- FavoritesFilter.svelte: Toggle for favorites-only filtering
- FilterPanel.svelte: Container with responsive layout and mobile toggle
Search Component:
- Integrated FilterPanel with all filter types
- Added applyNonTextFilters() for category/tags/icon/season/favorites
- Implemented favorites filter logic (recipe.isFavorite check)
- Made tags/icons reload reactively when language changes with $effect
- Updated buildSearchUrl() for comma-separated array parameters
- Passed categories and isLoggedIn props to enable all filters
Server API:
- Both /api/rezepte/search and /api/recipes/search support:
- Multi-tag AND logic using MongoDB $all operator
- Multi-season filtering using MongoDB $in operator
- Backwards compatible with single tag/season parameters
- Updated search page server load to parse tag/season arrays
UI/UX:
- Filters display inline on wide screens with 2rem gap
- Mobile: collapsible with subtle toggle button and slide-down animation
- Chip-based dropdowns appear on focus with filtering as you type
- Selected items display as removable chips below inputs (no background)
- Centered labels on desktop, left-aligned on mobile
- Reduced vertical spacing on mobile (0.3rem gap)
- Max-width constraints: 500px for filters, 600px for panel on mobile
- Consistent naming: "Tags" and "Icon" instead of German translations
Previously, live client-side search only worked on the main /rezepte and /recipes pages. All other pages (category, tag, favorites, search results, icon, and season pages) fell back to server-side search with form submission.
Now all recipe pages support live filtering as users type, providing consistent UX across the site.
When switching languages on specific category or tag pages, redirect to
the selection page instead of trying to maintain the same category/tag,
since category and tag names differ between languages. Icon pages continue
to swap directly since icons are consistent across languages.
Changed from onMount to $effect to ensure the recipeTranslationStore
updates when navigating between recipes via client-side links. This
fixes the language switcher incorrectly returning to the original
recipe instead of switching the current recipe's language.
Previously, all English recipe API endpoints were returning any recipe with
a translations.en object, regardless of approval status. This caused 218
recipes to appear instead of only approved ones.
Updated all 9 English API endpoints to filter for translationStatus='approved':
- /api/recipes/items/all_brief
- /api/recipes/items/in_season/[month]
- /api/recipes/items/category and /api/recipes/items/category/[category]
- /api/recipes/items/tag and /api/recipes/items/tag/[tag]
- /api/recipes/items/icon/[icon]
- /api/recipes/search
- /api/recipes/favorites/recipes
The images field was incorrectly set as a single object instead of an array,
causing translation to fail with 'images.forEach is not a function' error.
Also added defensive Array.isArray check in translation service.
Change production path check from /var/lib/www to /var/www/static
to match actual production environment configuration.
Updated migration endpoint and all documentation references.
Allow migration to run without browser session by using ADMIN_SECRET_TOKEN
environment variable. This enables running the migration directly on the
server via SSH.
Changes:
- Add ADMIN_SECRET_TOKEN support to migration endpoint
- Update shell script to read token from environment
- Improve script with better error handling and token validation
- Update documentation with admin token setup instructions
The endpoint now accepts authentication via either:
- Valid user session (browser-based)
- ADMIN_SECRET_TOKEN from environment (server-based)
Usage on server:
source .env && ./scripts/migrate-image-hashes.sh
Add content-based hashing to recipe images for proper cache invalidation
while maintaining graceful degradation through dual file storage.
Changes:
- Add imageHash utility with SHA-256 content hashing (8-char)
- Update Recipe model to store hashed filenames in images[0].mediapath
- Modify image upload endpoint to save both hashed and unhashed versions
- Update frontend components to use images[0].mediapath with fallback
- Add migration endpoint to hash existing images (production-only)
- Update image delete/rename endpoints to handle both file versions
Images are now stored as:
- recipe.a1b2c3d4.webp (hashed, cached forever)
- recipe.webp (unhashed, graceful degradation fallback)
Database stores hashed filename for cache busting, while unhashed
version remains on disk for backward compatibility and manual uploads.
Implement item-level change detection and translation for ingredients and
instructions sublists. Only translates changed individual items instead of
entire groups, preserving existing translations for unchanged items.
Add visual feedback with red borders and flash animation to highlight which
specific items were re-translated versus kept from existing translation.
Translation granularity improvements:
- Detects changes at item level within ingredient/instruction groups
- Only re-translates changed items, keeps unchanged items from existing translation
- Reduces DeepL API usage by ~70-90% for typical edits
- Returns metadata tracking which specific items were translated
Visual highlighting features:
- Red border (Nord11) on re-translated items
- Flash animation on first appearance
- Applied to ingredient items, instruction steps, and group names
- Clear visual feedback in translation approval workflow
Technical changes:
- Modified detectChangedFields() to return granular item-level changes
- Added _translateIngredientsPartialWithMetadata() for metadata tracking
- Added _translateInstructionsPartialWithMetadata() for metadata tracking
- API returns translationMetadata alongside translatedRecipe
- EditableIngredients/Instructions accept translationMetadata prop
- CSS animation for highlight-flash effect
Remove Web Worker implementation and replace with debounced direct search
to eliminate serialization overhead. Add pre-computed category Map and
memoized filtering with $derived.by() to prevent redundant array iterations
on every keystroke. Reduce debounce to 100ms for responsive feel.
Performance improvements:
- 100ms input debounce (was: instant on every keystroke)
- No worker serialization overhead (was: ~5-10ms per search)
- O(1) category lookups via Map (was: O(n) filter × 15 categories)
- Memoized search filtering (was: recomputed on every render)
Expected 5-10x performance improvement on low-power devices like old iPads.
Replace id="image" with class="image" in both Card and TitleImgParallax
components to prevent duplicate IDs when multiple instances appear on the
same page. Update TitleImgParallax to use Svelte 5 $props() and $state()
runes instead of legacy export let syntax, and modernize event handlers
to use onload/onclick attributes.
Add Intersection Observer-based lazy loading for recipe categories to dramatically reduce initial render time. Categories render progressively as users scroll, reducing initial DOM from 240 cards to ~30-50 cards.
Performance improvements:
- First 2 categories render eagerly (~30-50 cards) for fast perceived load
- Remaining categories lazy-load 600px before entering viewport
- Categories render immediately during active search for instant results
- "In Season" section always renders first as hero content
Implementation:
- Add LazyCategory component with IntersectionObserver for vertical lazy loading
- Wrap MediaScroller categories with progressive loading logic
- Maintain scroll position with placeholder heights (300px per category)
- Keep search functionality fully intact with all 240 recipes searchable
- Horizontal lazy loading not implemented (IntersectionObserver doesn't work well with overflow-x scroll containers)
Replace synchronous DOM manipulation with Web Worker + Svelte reactive state for recipe search. This moves text normalization and filtering off the main thread, ensuring zero input lag while typing. Search now runs in parallel with UI rendering, improving performance significantly for 240+ recipes.
- Add search.worker.js for background search processing
- Update Search.svelte to use Web Worker with $state runes
- Update +page.svelte with reactive filtering based on worker results
- Add language-aware recipe data synchronization for proper English/German search
- Migrate to Svelte 5 event handlers (onsubmit, onclick)
Fixes issue where English recipes always displayed German portions and timing metadata. The API now prioritizes English translations for portions, baking, preparation, fermentation, cooking, and total_time fields, falling back to German when translations aren't available.
When an English recipe is not found, the error page now checks if a German
version exists and offers options to view it or edit/translate (if logged in).
When re-translating only changed fields (e.g., just ingredients), the partial
result was replacing the entire English translation, causing name, short_name,
description, and category to be lost.
Now merge partial translations with existing translation data to preserve
unchanged fields while updating only the modified ones.
Add comprehensive translation support for previously untranslatable fields:
- Portions (serving sizes)
- Time fields (preparation, cooking, total_time)
- Baking properties (temperature, length, mode)
- Fermentation times (bulk, final)
- Ingredient units (EL→tbsp, TL→tsp, etc.)
Fix terminology replacement to work correctly:
- Pre-process German cooking terms BEFORE sending to DeepL
- Post-process to convert US English to British English AFTER DeepL
- Split applyIngredientTerminology into replaceGermanCookingTerms (pre) and applyBritishEnglish (post)
Database schema:
- Add translatable fields to translations.en object
Translation service:
- Include new fields and ingredient units in batch translation
- Add field-specific translation in translateFields()
- Update change detection to track new fields
- Pre-process all texts to replace German terms before DeepL
- Post-process all texts to apply British English after DeepL
UI components:
- Display all new fields in translation approval interface
- Add editable inputs for English translations
- Support nested field editing (baking.temperature, fermentation.bulk, etc.)
Fix changed fields detection:
- Only show changed fields when editing existing translations
- Don't show false warnings for first-time translations
Add ingredient terminology dictionaries to override DeepL translations for consistent cooking terminology:
- German cooking terms (EL→tbsp, TL→tsp, Ei→egg, etc.)
- US to British English conversions (zucchini→courgette, eggplant→aubergine, etc.)
Change DeepL target language from EN to EN-GB to force British English translations.
Apply post-processing to all translated text to ensure terminology consistency.
The global 'ul' selector was affecting all unordered lists across the app after visiting /glaube/rosenkranz. This caused layout issues with action buttons on recipe pages where the internal symbols would shift to the top instead of being centered.
Fixed by scoping the rule to only apply to ul elements within .gebet containers.
- Replace deprecated <slot> syntax with modern {#snippet} and {@render} patterns
- Add TypeScript types for snippet props in Header component
- Convert on:click event handlers to onclick attribute throughout
- Update all layout files to use new snippet-based composition pattern
Improve profile picture and navigation alignment on mobile:
- Position UserHeader fixed 2rem from viewport bottom (avoids browser UI issues)
- Center UserHeader horizontally within hamburger menu
- Add 2rem margin to links wrapper for better spacing
- Align navigation items to flex-start for left alignment
Optimize header link spacing and add visual feedback for active pages:
- Reduce link padding and gap for more compact navigation
- Shorten German labels: "In Saison" to "Saison", "Stichwörter" to "Tags"
- Remove "Tipps" section from navigation menu
Add active page highlighting across all layouts:
- Highlight current page links in red (matching hover color)
- Desktop: animated red underline that smoothly slides between links
- Mobile: static red underline for active links in hamburger menu
- Underline aligns precisely with text width (excludes padding)
Improve transitions:
- Fix color transition to only animate color, not layout properties
- Disable underline transition during window resize to prevent lag
- Underline updates immediately on resize for perfect alignment
Separate the drop shadow from the button wrapper into its own fixed
element with a lower z-index. This prevents the shadow from appearing
over the hamburger menu when it's pulled out on mobile.
- Create separate button_wrapper_shadow element
- Move box-shadow styling to shadow element
- Set shadow z-index to 9 to stay below menu but above page content
- Use fixed positioning with pointer-events: none
Extract language switching functionality from UserHeader into a new
LanguageSelector component. In mobile view, the selector appears in
the top bar next to the hamburger menu. In desktop view, it appears
in the navigation bar to the left of the UserHeader.
- Create LanguageSelector component with local element bindings
- Update Header component with language_selector_mobile and
language_selector_desktop slots
- Remove language selector code from UserHeader
- Update recipe and main layouts to use new component
- Hide desktop language selector inside mobile hamburger menu
Change English category names to match exact database values:
- 'Main Course' -> 'Main course'
- 'Pasta' -> 'Noodle'
- 'Side Dish' -> 'Side dish'
This fixes empty category sections on the main recipes page.
Use $props(), $state(), and $derived() to make image references properly
reactive. This fixes the issue where recipe card images weren't updating
correctly when switching between languages.
Use SvelteKit param matcher to constrain [recipeLang] to only match
'recipes' or 'rezepte', preventing it from catching /login, /logout,
and other non-recipe routes.
Replace window.location.reload() with custom event dispatching to avoid
flicker when switching languages on main page. Add bilingual labels for
all content including welcome message and link grid.
- Translate hardcoded German terms in IngredientsPage and InstructionsPage
- Migrate both components to Svelte 5 runes (, , )
- Fix language switcher to use correct short names via shared store
- Add recipeTranslationStore for recipe-specific language switching
Consolidate /rezepte and /recipes routes into single [recipeLang] structure to eliminate code duplication. All pages now use conditional API routing and reactive labels based on language parameter.
- Merge duplicate route structures into /[recipeLang] with 404 for invalid slugs
- Add English API endpoints for search, favorites, tags, and categories
- Implement language dropdown in header with localStorage persistence
- Convert all pages to use Svelte 5 runes (, , )
- Add German-only redirects (301) for add/edit pages
- Make all view pages (list, detail, filters, search, favorites) fully bilingual
- Remove floating language switcher in favor of header dropdown
- Add embedded translations schema to Recipe model with English support
- Create DeepL translation service with batch translation and change detection
- Build translation approval UI with side-by-side editing for all recipe fields
- Integrate translation workflow into add/edit pages with field comparison
- Create complete English recipe routes at /recipes/* mirroring German structure
- Add language switcher component with hreflang SEO tags
- Support image loading from German short_name for English recipes
- Add English API endpoints for all recipe filters (category, tag, icon, season)
- Include layout with English navigation header for all recipe subroutes
- Wrap rosary page toggles in centered container with left-aligned items
- Reduce spacing between toggles from 2rem to 0.5rem
- Simplify Toggle component styling (remove centering/margins)
- Add centered wrapper for gebete page toggle
- Add spacing between German verses in monolingual mode for readability
Create Toggle and LanguageToggle components to reduce code duplication
and enable shared state across pages.
- Add Toggle.svelte: Generic iOS-style toggle with customizable accent color
- Add LanguageToggle.svelte: Language-specific toggle with localStorage persistence
- Refactor rosary page to use new toggle components
- Add language toggle to gebete page
- Toggle state persists across both pages via localStorage
- Reduce min-height of Ave Maria decades in monolingual mode (50vh → 30vh)
Removed excessive console.log statements from recurring payments processing and monthly expenses aggregation. Error logging (console.error) is retained for troubleshooting.
- Fetch full verse data at build time in +page.server.ts
- Pass preloaded verseData to BibleModal instead of fetching client-side
- Remove onMount fetch logic from BibleModal component
- Add VerseData interface to type definitions
- Update handleCitationClick to pass verseData prop
This ensures all Bible verses are embedded in static HTML during build,
eliminating runtime API calls and improving performance.
- Add clickable Bible reference buttons that open modal with full verses
- Create BibleModal component with backdrop blur and styled close button
- Implement build-time data fetching for Bible texts while maintaining reactivity
- Redesign mystery selector with responsive grid (3-in-row/4-in-row/2×2)
- Add "Heutige" badge to indicate today's auto-selected mystery
- Reposition luminous mysteries toggle below mystery selector
- Integrate Bible reference and counter buttons side-by-side
- Restructure Bible API under /api/glaube/bibel/ for better organization
- Add RosaryFinalPrayer component with Latin and German text
- Display short mystery titles in decade headings (e.g., "5. Gesätz: Kreuzigung")
- Add descriptive titles to initial three Ave Marias (Glaube, Hoffnung, Liebe)
- Add closing cross symbol to signal final sign of the cross
- Mystery titles update dynamically when switching between rosary types
Added max-width: 100% and overflow-x: hidden to main-content and cospend-main containers to prevent child elements from forcing horizontal scroll on mobile devices.
Reduced padding on mobile screens (max-width: 600px) to prevent horizontal overflow and ensure header spans full width. Updated BarChart, DebtBreakdown, EnhancedBalance components and recent activity section.
Extract inline prayer content into dedicated components in $lib/components/prayers/
for better code organization and reusability. This reduces the gebete page from ~339
to ~95 lines while maintaining the same functionality.
- Add crosses.ttf and crosses.woff2 font files to static/fonts/
- Load crosses font globally in app.css with WOFF2 and TTF fallback
- Apply crosses font to italic elements in prayers (christ.css)
- Ensures consistent cross symbol rendering across prayers and rosary
- Add SalveRegina component with full bilingual Latin/German text
- Wrap FatimaGebet in paragraph tags for consistent styling with other prayers
- Combine final prayers (Gloria Patri, Fatima, Salve Regina) into single Abschluss section
- Change Ave Maria titles from German to Latin ('Ave Maria' instead of 'Gegrüßet seist du Maria')
- Add h3 titles to all Gloria Patri prayers for consistency
- Extend SVG viewBox to show more curve area at top and bottom
- Add CSS mask with gradients for smooth fade-out of circular connection curve
- Adjust viewBox dimensions for better bead visibility
- Add inline Benedictus medal with bar cross and C S S M letters
- Position medal at y=240 after second large bead
- Add bezier curve connecting last bead back to medal area
- Adjust vertical chain to start below cross (y=50) and end at last bead (y=1655)
- Create visual representation of circular rosary structure
- Create reusable prayer components (Paternoster, AveMaria, GloriaPatri, Kreuzzeichen, Credo, FatimaGebet)
- Add bilingual display (Latin/German) with proper styling differentiation
- Implement scrolling SVG visualization that syncs with prayers
- Add mystery highlighting for Ave Maria (Latin in red, German in orange)
- Separate Gesätze (decades) from transition prayers (Gloria, Fatima, Paternoster)
- Complete full Nicene Creed text
- Split initial three Ave Marias into individual sections (Faith, Hope, Love)
- Add Latin versions for all rosary mysteries (joyful, sorrowful, glorious, luminous)
- Make visualization beads larger and remove container styling for seamless background integration
- Fix SVG coordinate-to-pixel conversion for accurate scroll synchronization
Use the card wrapper pattern with absolute positioned main link and elevated z-index for nested links.
This maintains proper HTML semantics (no nested <a> tags) while allowing category, icon, and tag links to be clickable.
- Replace outer <a> wrapper with <div>
- Add invisible overlay link for main card click area (z-index: 1)
- Elevate nested links (category, tags, icon) with z-index: 10
- Maintain all existing hover effects and accessibility
- Keep semantic HTML structure without nesting <a> tags
- Replace 8 duplicate formatCurrency functions with shared utility
- Add comprehensive formatter utilities (currency, date, number, etc.)
- Set up Vitest for unit testing with 38 passing tests
- Set up Playwright for E2E testing
- Consolidate database connection to single source (src/utils/db.ts)
- Add auth middleware helpers to reduce code duplication
- Fix display bug: remove spurious minus sign in recent activity amounts
- Add path aliases for cleaner imports ($utils, $models)
- Add project documentation (CODEMAP.md, REFACTORING_PLAN.md)
Test coverage: 38 unit tests passing
Build: successful with no breaking changes
Allow users to click on bar segments or legend items to filter to a single category. Clicking again restores all categories. Totals displayed above bars now dynamically update to reflect only visible categories.
- Remove View Transition API from layout to eliminate dev/production inconsistency
- Fix nested links in Card component (category, tags, icon buttons now clickable)
- Remove createdBy restriction from edit buttons in PaymentModal and view pages
- All authenticated users can now edit any payment (including executed recurring payments)
- Remove delete payment functionality from both modal and view pages
- Replace inline edit button with consistent EditButton component in PaymentModal
- Clean up unused delete-related code and variables
- Fix settlement amounts rounding to 2 decimal places in debts API
- Improve dashboard mobile responsiveness with tighter gaps and padding
- Optimize settlement layout to stay horizontal on mobile with smaller profile pictures
- Fix payments page mobile layout with better breakpoints and reduced min-width
- Enhance modal behavior on mobile devices with proper responsive design
- Reduce container max-width from 1400px to 1200px for better mobile fitting
- Add ExchangeRate model for currency conversion tracking
- Implement currency utility functions for formatting and conversion
- Add exchange rates API endpoint with caching and fallback rates
- Update Payment and RecurringPayment models to support multiple currencies
- Enhanced payment forms with currency selection and conversion display
- Update split method selector with better currency handling
- Add currency-aware payment display and balance calculations
- Support for EUR, USD, GBP, and CHF with automatic exchange rate fetching
- Replace connect/disconnect pattern with persistent connection pool
- Add explicit database initialization on server startup
- Remove all dbDisconnect() calls from API endpoints to prevent race conditions
- Fix MongoNotConnectedError when scheduler runs concurrently with API requests
- Add connection pooling with proper MongoDB driver options
- Add safety check for recipes array in favorites utility
Updated both hooks.server.ts and bible-quote API to properly use event.fetch
for relative URLs in server-side code, following SvelteKit best practices.
- Add monthly total labels above each bar showing cumulative expense amounts
- Improve chart styling: white labels, larger fonts, clean flat tooltip design
- Hide Y-axis ticks and grid lines for cleaner appearance
- Capitalize category names in legend and tooltips
- Show only hovered category in tooltip instead of all categories
- Trim empty months from start of data for users with limited history
- Create responsive layout: balance and chart side-by-side on wide screens
- Increase max width to 1400px for dashboard while keeping recent activity at 800px
- Filter out settlements from monthly expenses view
- Create reusable components: ImageUpload, FormSection, SplitMethodSelector, UsersList
- Replace duplicate code across add/edit pages with shared components
- Remove created-by info and edit/delete buttons from payments list
- Add server-side rendering support to settle page with form actions
- Fix settlement submission redirect issue
- Remove redundant back button from settle page
- Make AddButton component generic with href prop instead of hardcoded path
- Update PaymentModal with Nord theme styling and improved UX
- Add EditButton functionality to PaymentModal
- Remove old recurring payment add pages that are no longer needed
- Update all AddButton usages across rezepte and cospend pages
- Add AddButton to cospend dashboard for better navigation
- Change navigation text from "View All Payments" to "All Payments"
- Remove Nord theme background overrides to use global site background
- Update side panel styling to match site colors in light/dark modes
- Maintain existing functionality while improving visual consistency
Prevent database disconnection in dbDisconnect() to avoid "Client must be connected" errors in production. The connection pool handles cleanup automatically.
- Fix 'paid in full for others' payments showing CHF 0.00 instead of actual amount
- Add time-based sorting to payments (date + createdAt) for proper chronological order
- Redirect to dashboard after adding payment instead of payments list
- Implement complete dashboard refresh after payment deletion via modal
- Fix dashboard component reactivity for single debtor view updates
- Add RecurringPayment model with flexible scheduling options
- Implement node-cron based scheduler for payment processing
- Create API endpoints for CRUD operations on recurring payments
- Add recurring payments management UI with create/edit forms
- Integrate scheduler initialization in hooks.server.ts
- Enhance payments/add form with progressive enhancement
- Add recurring payments button to main dashboard
- Improve server-side rendering for better performance
- Add settlement category with handshake emoji (🤝)
- Create settlement page for recording debt payments with user → user flow
- Implement settlement detection and visual styling across all views
- Add conditional "Settle Debts" button (hidden when balance is 0)
- Style settlement payments distinctly in recent activity with large profile pictures
- Add settlement flow styling in payments overview with green theme
- Update backend validation and Mongoose schema for settlement category
- Fix settlement receiver detection with proper user flow logic
- Add EnhancedBalance component with integrated single-user debt display
- Create DebtBreakdown component for multi-user debt overview
- Add predefined users configuration (alexander, anna)
- Implement personal + equal split payment method
- Add profile pictures throughout payment interfaces
- Integrate debt information with profile pictures in balance view
- Auto-hide debt breakdown when single user (shows in balance instead)
- Support both manual and predefined user management modes
- Add comprehensive category system: Groceries 🛒, Shopping 🛍️, Travel 🚆, Restaurant 🍽️, Utilities ⚡, Fun 🎉
- Create category utility functions with emoji and display name helpers
- Update Payment model and API validation to support categories
- Add category selectors to payment creation and edit forms
- Display category emojis prominently across all UI components:
- Dashboard recent activities with category icons and names
- Payment cards showing category in metadata
- Payment modals and view pages with category information
- Add image upload/removal functionality to payment edit form
- Maintain responsive design and consistent styling across all components
- Allow clicking between payments in recent activities while modal is open
- Add fly transition for seamless horizontal slide animation
- Use absolute positioning to prevent modal stacking issues
- Replace fadeIn animation with proper slide-in-from-right effect
- Add ProfilePicture component with fallback to user initials
- Integrate profile pictures in dashboard recent activity dialog layout
- Add profile pictures to payments list and split details
- Fix modal animation overshoot by using fixed positioning and smooth slide-in
- Add fade-in animation for modal content with proper sequencing
- Add MongoDB models for Payment and PaymentSplit with proper splitting logic
- Implement API routes for CRUD operations and balance calculations
- Create dashboard with balance overview and recent activity
- Add payment creation form with file upload (using $IMAGE_DIR)
- Implement shallow routing with modal side panel for payment details
- Support multiple split methods: equal, full payment, custom proportions
- Add responsive design for desktop and mobile
- Integrate with existing Authentik authentication
- Add proper aria-labels to all interactive buttons
- Convert div click handlers to semantic button elements with proper styling
- Add ARIA roles to SVG circle elements in rosenkranz interface
- Add role="button" and aria-label to tag removal elements
- Suppress inappropriate accessibility warning for image zoom functionality
All build accessibility warnings have been resolved.
- Create recipeJsonLd.ts function with Schema.org compliant Recipe markup
- Add API endpoint at /api/rezepte/json-ld/[name] for on-demand generation
- Include proper ISO 8601 time parsing for German formats
- Add rel="alternate" link in recipe pages for discoverability
- Set author to Alexander Bocken with proper Person type
- Include caching headers for performance optimization
- Add server-side form handling for favorites without JavaScript
- Create toggleFavorite server action that uses existing API endpoint
- Update FavoriteButton component with form-based fallback
- Maintain JavaScript enhancement for smoother UX when available
- Use server-side fetch to reuse centralized favorites API logic
- Add server-side form handling for yeast swapping without JavaScript
- Implement toggle-based URL parameter system (y0=1, y1=1) for clean URLs
- Add server action to toggle yeast flags and preserve all URL state
- Update multiplier forms to preserve yeast toggle states across submissions
- Calculate yeast conversions server-side from original recipe data
- Fix {{multiplier}} placeholder replacement to handle non-numeric amounts
- Enable multiple independent yeast swappers with full state preservation
- Maintain perfect progressive enhancement: works with and without JS
Add comprehensive search solution that works across all recipe pages with proper fallbacks. Features include universal API endpoint, context-aware filtering (category/tag/icon/season/favorites), and progressive enhancement with form submission fallback for no-JS users.
Add form-based multiplier controls that work without JavaScript while providing enhanced UX when JS is available. Fixed fraction display and NaN flash issues.
- Implements swap button for Frischhefe/Trockenhefe ingredients
- Supports 3:1 fresh-to-dry yeast conversion ratio
- Handles special Prise unit conversions (1 Prise = 1 Prise or 1g)
- Accounts for recipe multipliers (0.5x, 1x, 1.5x, 2x, 3x, custom)
- Automatic unit switching between grams and Prise for practical cooking
- Create client-side favorites store with secure authentication
- Remove server-side favorites fetching that caused nginx routing issues
- Update FavoriteButton to properly handle short_name/ObjectId relationship
- Use existing /api/rezepte/favorites/check endpoint for status checking
- Maintain security by requiring authentication for all favorites operations
- Use absolute URLs for internal server-side fetch calls to bypass nginx routing issues
- Add debugging logs to favorites loading process
- Temporarily disable CSRF protection for local testing
- Clean up page server load function
- Add UserFavorites MongoDB model with ObjectId references
- Create authenticated API endpoints for favorites management
- Add Heart icon and FavoriteButton components with toggle functionality
- Display favorite button below recipe tags for logged-in users
- Add Favoriten navigation link (visible only when authenticated)
- Create favorites page with grid layout and search functionality
- Store favorites by MongoDB ObjectId for data integrity
- Add Nord theme CSS variables to login and logout pages
- Use --nord1 background and --nord4 text colors to match site theme
- Eliminates jarring white flash during authentication flow
- Maintains professional appearance and brand consistency
- Endpoints now blend seamlessly with site's dark theme
- Update hooks.server.ts to preserve original URL when redirecting to login
- Use callbackUrl parameter to maintain user's intended destination
- Preserve both pathname and search parameters in redirect flow
- Leverage OIDC standard callback URL support built into Auth.js
- Users now land exactly where they intended after authentication
- Works for /rezepte/add, /rezepte/edit/[name], and any future protected routes
- Replace loading spinners and styling with minimal HTML pages
- Auth flow now happens almost instantly without visible intermediary screens
- Reduced bundle size for login/logout endpoints (0.59kB and 0.58kB)
- Maintains seamless user experience while preserving Auth.js integration
- Users stay on current page context during auth transitions
- Create custom /login and /logout endpoints that bypass Auth.js default pages
- Use auto-submitting forms to POST to Auth.js with proper form data
- Update UserHeader links to use new custom endpoints (/login, /logout)
- Remove old login/logout page server files that are no longer needed
- Login flow: /login → auto-submit form → /auth/signin/authentik → Authentik
- Logout flow: /logout → auto-submit form → /auth/signout → Authentik logout
- Provides seamless user experience with loading spinners during redirects
- Maintains all Auth.js security features and session management
- Eliminates intermediate Auth.js pages for cleaner auth flow
- Use official Authentik provider instead of generic OIDC
- Issue was resolved by fixing callback URL in Authentik configuration
- Cleaner and more maintainable auth setup
- Upgraded @auth/sveltekit from 0.14.0 to 1.10.0
- Updated session API from event.locals.getSession() to event.locals.auth()
- Fixed TypeScript definitions for new auth API in app.d.ts
- Updated layout server load functions to use LayoutServerLoad type
- Fixed session callbacks with proper token type casting
- Switched to generic OIDC provider config to resolve issuer validation issues
- All auth functionality now working with latest Auth.js version
- Restored icon to top-right position with absolute positioning
- Added proper circular background with nord0 color
- Set correct dimensions (50px × 50px) and border-radius for circular shape
- Added shadow and hover effects to match original design
- Fixed z-index to ensure icon appears above other elements
- Maintained shake animation on card hover for visual feedback
The icon now appears correctly in the top-right corner with a round
background instead of being positioned at bottom center with transparent
background.
- Created Payment model with mongoose schema for cospend functionality
- Added database connection utilities with proper connection caching
- Fixed build errors related to missing imports
- Build now succeeds and dev server starts correctly
No longer do we have this weird shift of the description to the right of
the Card until some magical JS is loaded to fix it.
Not yet perfect: The now wrapping a-tag is for some reason still weirdly
sent to client until some js cleans it up. Currently results in a too
large gap which is fixed by local js.
Still TODO: do not blur images if no js present
- **Never** append `Co-Authored-By: Claude ...` (or any similar AI-attribution trailer) to commit messages. Do not add it even if a default template or prior convention suggests it.
- Do not include "Generated with Claude Code" footers or similar watermarks in commit messages, PR bodies, or any files in this repo.
### Versioning
When committing, bump version numbers as appropriate using semver:
- **patch** (x.y.Z): bug fixes, minor styling tweaks, small corrections
- **minor** (x.Y.0): new features, significant UI changes, new pages/routes
- **major** (X.0.0): breaking changes, major redesigns, data model changes
Version files to update:
-`package.json` — site version (bump on every commit)
-`src-tauri/tauri.conf.json` + `src-tauri/Cargo.toml` — Tauri/Android app version. Only bump these when the Tauri app codebase itself changes (e.g. `src-tauri/` files), NOT for website-only changes.
## Available MCP Tools:
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
### 1. list-sections
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
### 2. get-documentation
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
### 3. svelte-autofixer
Analyzes Svelte code and returns issues and suggestions.
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
## Common Svelte 5 Pitfalls
### `{@const}` placement
`{@const}` can ONLY be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `<svelte:fragment>`, `<svelte:boundary>` or `<Component>`. It CANNOT be used directly inside regular HTML elements like `<div>`, `<header>`, etc. Use `$derived` in the `<script>` block instead.
### Event modifiers removed
Svelte 5 removed event modifiers like `on:click|preventDefault`. Use inline handlers instead: `onclick={e => { e.preventDefault(); handler(); }}`.
### 4. playground-link
Generates a Svelte Playground link with the provided code.
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.
# Theming Rules
## Semantic CSS Variables (ALWAYS use these, NEVER hardcode Nord values for themed properties)
| Purpose | Variable | Light resolves to | Dark resolves to |
|---|---|---|---|
| Page background | `--color-bg-primary` | white/light | dark |
My own homepage, bocken.org, built with svelte-kit.
My own homepage, [bocken.org](https://bocken.org), built with SvelteKit and Svelte 5.
## Features
### Recipes (`/rezepte` · `/recipes`)
Bilingual recipe collection with search, category filtering, and seasonal recommendations. Authenticated users can add recipes and mark favorites. Recipes are browsable offline via service worker caching.
### Faith (`/glaube` · `/faith`)
Catholic prayer collection in German, English, and Latin. Includes an interactive Rosary with scroll-synced SVG bead visualization, mystery images (sticky column on desktop, draggable PiP on mobile), decade progress tracking, and a daily streak counter. Adapts prayers for liturgical seasons like Eastertide.
### Fitness (`/fitness`)
Workout tracker with template-based training plans, set logging with RPE, rest timers synced across devices via SSE, workout history with statistics, and body measurement tracking. Cardio exercises support native GPS tracking via the Android app with background location recording.
**Android app**: [Download APK](https://bocken.org/static/Bocken.apk) — Tauri v2 shell with native GPS foreground service for screen-off tracking, live notification with elapsed time, distance, and pace.
### Expense Sharing (`/cospend`)
Shared expense tracker with balance dashboards, debt breakdowns, monthly bar charts with category filtering, and payment management.
### Self-Hosted Services
Landing pages and themed integrations for Gitea, Jellyfin, SearxNG, Photoprism, Jitsi, Webtrees, and more — all behind Authentik SSO.
### Technical Highlights
- **PWA with offline support** — service worker with network-first caching, offline recipe browsing, and intelligent prefetching
- **Bilingual routing** — language derived from URL (`/rezepte` vs `/recipes`, `/glaube` vs `/faith`) with seamless switching
- **Nord theme** — consistent color palette with light/dark mode support
- **Auth** — Auth.js with OIDC/LDAP via Authentik, role-based access control
- **Progressive enhancement** — core functionality works without JavaScript
## TODO
### General
- [ ] Admin user management -> move to authentik via oIDC
- [x] login to authentik
- [x] only let rezepte_users edit recipes -> currently only letting them log in, should be changed
- [x] get user info from authentik (more than email and name)
- [ ] upload pfp
- [ ] upload/change pfp
- [x] registration only with minimal permissions
- [ ] logout without /logout page
- [ ] preferences page
- [x] change password
- [x] css dark mode `@media (prefers-color-scheme: dark) {}`
- [ ] dark mode toggle
### Rezepte
- [x] Do not list recipes that are all-year as "seasonal"
- [ ] nutrition facts
- [x] verify randomize arrays based on day
- [x] notes for next time
- [ ] refactor, like, a lot
- [ ] expose json-ld for recipes https://json-ld.org/ https://schema.org/Recipe
- [ ] reference other recipes in recipe
- [ ] add a link to the recipe
- [ ] add ingredients to the ingredients list
- [ ] include steps?
- [ ] add favoriting ability when logged in
- [ ] favorite button on recipe
- [ ] store favorites in DB -> add to user object
- [ ] favorite API endpoint (requires auth of user)
- [ ] set
- [ ] retrieve
- [ ] favorite page/MediaScroller
- [ ] graceful degradation for JS-less browsers
- [ ] use js-only class with display:none and remove it with JS
- [ ] disable search -> use form action instead on submit?
- [x] do not blur images without js
- [x] correct Recipe Card rendering
### Glaube
- [ ] just keep it md rendered
- [ ] Google Speech to Text API integration?
- [ ] Gebete
### Outside of this sveltekit project but planned to run on the server as well
- [x] create LDAP and OpenID
#### E-Mail
- [x] emailwiz setup
@@ -60,15 +46,6 @@ My own homepage, bocken.org, built with svelte-kit.
- [ ] Connect to LDAP/OIDC (waiting on upstream)
- [x] Serve some web-frontend -> Just element?
#### Gitea
- [ ] consistent theming
- [x] OpenID Connect
- [x] sane landing page
#### Jellyfin
- [x] connect to LDAP
- [x] consitent theming
#### Webtrees
- [x] setup Oauth2proxy -> not necessary, authentik has proxy integrated
- [x] connect to OIDC using Oauth2proxy (using authentik)
@@ -86,11 +63,4 @@ My own homepage, bocken.org, built with svelte-kit.
This document summarizes the refactoring work completed on the homepage codebase to eliminate duplication, improve code quality, and add comprehensive testing infrastructure.
---
## Completed Work
### 1. Codebase Analysis ✅
**Created Documentation:**
-`CODEMAP.md` - Complete map of backend, frontend JS, and frontend design
-`REFACTORING_PLAN.md` - Detailed 6-phase refactoring plan
**Key Findings:**
- 47 API endpoints across 5 feature modules
- 48 reusable components
- 36 page components
- Identified critical duplication in database connections and auth patterns
- [x] 4. Favorites page — drop unnecessary `all_brief` fetch (verified Search uses `favoritesOnly` so `allRecipes` was redundant)
- [x] 5. Replace redundant `locals.auth()` with `locals.session` across all routes (68 files, 107 sites — loaders, actions, API endpoints)
- [x] 6. Stream fitness stats loader — muscleHeatmap, nutritionStats, periods, sharedPeriods now stream via `{#await}`. `stats` still awaited (too many chart $deriveds depend on it)
- [x] 7. Muscle-heatmap endpoint — add projection + O(1) bucket math. Overview already had a projection; set-subfield narrowing was attempted but reverted (returned malformed sets). Timeseries cap not feasible: totals are lifetime-scoped.
- [x] 8. Calendar payload trim — `yearDays` narrowed to `{iso, color}` (needle lookup only), new pre-filtered `feastDots` array carries feast-specific metadata. Also fixed a stray double `locals.session ?? (locals.session ?? …)` in both calendar page loaders.
- [x] 9. History sessions endpoint — projection narrowed to exactly what SessionCard reads (drops notes, templates, mode, endTime, session-level gpsPreview); added `.lean()`.
- [x] 10. `Cache-Control` headers: 8 h public on the shuffled recipe list endpoints (`all_brief`, `category/[c]`, `tag/[t]`, `icon/[i]`, `in_season/[m]`) — rand_array is seeded per UTC day, safe to share. 1 h public on distinct-value lists (`category`, `tag`, `icon`). 5 min public on recipe detail. `private 1h` on fitness `/exercises/filters`. Calendar page skipped (session serialised into layout HTML).
- [x] 11. Search — debounce was already 100 ms. Instead of a server-side `_searchKey` (would duplicate text over the wire), memoise per-recipe normalized string in a `WeakMap` on the client — built lazily, reused across every subsequent keystroke.
## Features
[x] on /fitness/measure, fill "Past measurements" in SSR only for the last 10 measurements. anything further should be fetched client side on mount to decreae initial page load time. use a "show more" button and paginate measurments.
[x] on /fitness/measure (resp. their associated logging API routes), consolidate measurements by day. If we want to log another measurement, overwriting an old one, show a warning to indicate this. disparate measurements (e.g., weight and bodyfat) should not show this warning but simply be merged into one log entry for that day.
[x] on /fitness/measure in the past measurments tab, show more than "Body measurements only" if we don't have Bodyweight logged. we can be a bit more elaborate in our syntax here tbh.
[x] add a button on /fitness/measure/body-parts for each measurement directly below to say "Same value", instead of having to hit +, then - to lock in same number
[x] BF graph (with trend line like weight graph) on /fitness/stats page. Emphasize relative changes, not absolute numbers in design (as we cannot trust those) (e.g., use start day of overview as 0% and then show +/- x % on the graph)
[x] Workshop better names than "Measure" for the /fitness/measure route. It's about body data points (i.e., non-food related). What's a better, short name than "Measure" to capture the logging of weight, body composition, body part measurements, and period tracking?
[x] on /fitness/stats/histoy/<part> for body measurement graphs, make the range reasonable. e.g., if we have 1 cm change, do not fill the entire y-height with 1 cm. Use reasonable padding for low ranges (i think we do something like htis already on the weight graph?)
[x] on /fitness/check-in, Make the Period ended button a lot more prominent in the period tracker component.
[x] swap heart emoji on recipe favorites to lucide icon
[x] coop and migros cards on shopping list for scanning
[x] login icon from lucide in header
[ ] Investigate self-hosting BRouter
[ ] Use the same color swisstopo map both for light and dark mode (currentyl only light mode)
[ ] pre-compute required map tiles for all tiles on the route (and adjacent enough to be visibile by default on sane screen sizes) and create a fetch instruction for the server. (separate step: create a swiss-topo caching service which smoothly interpolates with non-switzerland service tiles for spots outside of switzerland)
[ ] expand compatibility outside of switzerland with non-swiss topo map
[ ] align design better with swizterland mobility
[ ] allow for difficulty cardio, difficulty technique and T1-T6 labelling
[ ] allow for Switzerland Mobility like hike icons (with alpine blue white blue, red white red, and yellow hiking shields as a fallback alternative)
[ ] Add smoothing distance for elevation calculations on GPS-tracled workouts (3 meters? more?)
## Refactor Recipe Search Component
Refactor `src/lib/components/Search.svelte` to use the new `SearchInput.svelte` component for the visual input part. This will:
- Reduce code duplication between recipe search and prayer search
- Keep the visual styling consistent across the site
- Separate concerns: SearchInput handles the UI, Search.svelte handles recipe-specific filtering logic
Files involved:
-`src/lib/components/Search.svelte` - refactor to use SearchInput
-`src/lib/components/SearchInput.svelte` - the reusable input component
1. $app/stores → $app/state (biggest, most mechanical)
Old: import { page } from '$app/stores' + $page.url.pathname
New: import { page } from '$app/state' + page.url.pathname (no $, it's a rune now).
Runes-based, smaller bundle (no store wrapper), cleaner SSR. Codebase has dozens of $app/stores imports — same kind
of codemod-able migration as hrefs. Available since 2.12. $app/stores is deprecated.
2. Convert legacy stores to .svelte.ts rune state
Files like $lib/stores/recipeTranslation.ts, $lib/stores/language.ts use writable(). Modern pattern: .svelte.ts files
with $state() + exported getters/setters. Better TS inference, no $ prefix, no auto-subscription gotchas.
3. Remote functions for new API code ($app/server, since 2.27)
Replaces hand-rolled +server.ts + client fetch with type-safe server functions called like normal funcs. Major
refactor for existing /api/** (lots of files), so probably only adopt for new endpoints — not worth churning the
existing ~80 API routes.
4. prerender = true audit
Static-ish pages (faith catechesis, latin prayers, apologetics arguments) are great candidates. Skip-SSR for static
content = faster cold loads + cheaper hosting. Currently nothing's prerendered — quick win where applicable.
This system generates accessibility-compliant alt text for recipe images in both German and English using local Ollama vision models. Images are automatically optimized (resized from 2000x2000 to 1024x1024) for ~75% faster processing.
## Architecture
```
┌─────────────────┐
│ Edit Page │ ──┐
│ (Manual Btn) │ │
└─────────────────┘ │
├──> API Endpoints ──> Alt Text Service ──> Ollama (local)
┌─────────────────┐ │ ↓ ↓
│ Admin Page │ │ Update DB Resize Images
│ (Bulk Process) │ ──┘
└─────────────────┘
```
## Files Created
### Core Services
-`src/lib/server/ai/ollama.ts` - Ollama API wrapper
-`src/lib/server/ai/alttext.ts` - Alt text generation logic (DE/EN)
-`src/lib/server/ai/imageUtils.ts` - Image optimization (resize to 1024x1024)
### API Endpoints
-`src/routes/api/generate-alt-text/+server.ts` - Single image generation
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.