Commit Graph

1000 Commits

Author SHA1 Message Date
Alexander cfdd58fb18 feat(hikes): inline cantonal Wappen next to region label
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.
2026-05-19 08:44:30 +02:00
Alexander 2c3886296c fix(hikes): square-ish SAC red/blue painted markers (44px → 28px) 2026-05-19 08:30:02 +02:00
Alexander c082da700d feat(hikes): phone-sized static hero variant for ≤560 px viewports
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`.
2026-05-19 08:27:08 +02:00
Alexander fe08e06a02 feat(hikes): pre-rendered overview hero map with same handover pattern
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.
2026-05-19 08:18:23 +02:00
Alexander fd2d8a58d9 feat(hikes): pre-rendered static hero map with smooth Leaflet handover
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.
2026-05-18 23:38:24 +02:00
Alexander f3d16d5187 feat(hikes): route-builder, overview map + cards, SAC-coloured pipeline
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).
2026-05-18 21:13:00 +02:00
Alexander 928774084f fix(fitness): restore SSE mirror-finish without racing local summary
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.
2026-05-12 17:40:41 +02:00
Alexander 8c09b0b2f4 Revert "fix(fitness): mirror finish overview to other devices via SSE"
This reverts commit e87b8bd864.
2026-05-12 17:30:02 +02:00
Alexander 5ac56db46c feat(fitness): auto-advance exercises with view transitions + audio cues
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.
2026-05-12 17:25:54 +02:00
Alexander 5fd8027d3e feat(fitness): label finish button "FINISH EARLY" with unfinished sets
CI / update (push) Has been cancelled
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.
2026-05-10 14:56:12 +02:00
Alexander e87b8bd864 fix(fitness): mirror finish overview to other devices via SSE
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.
2026-05-10 14:42:50 +02:00
Alexander eeed31aaf4 fix(fitness): hoist rest timer above set table, persist across exercise switches
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.
2026-05-10 14:17:36 +02:00
Alexander e59e9679da fix(faith): align calendar view tab labels on timespan + shape
Year/Hills/Month mixed two axes. Now: Year · Ring, Year · Hills,
Month · Grid (with de + la translations).
2026-05-10 14:06:40 +02:00
Alexander 685f4cc892 fix(header): drop extra 12px gap when safe-area inset is present
CI / update (push) Has been cancelled
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.
2026-05-10 13:00:25 +02:00
Alexander 60e651de72 feat(faith): add hills view of liturgical year as calendar tab
CI / update (push) Has been cancelled
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.
2026-05-10 12:48:28 +02:00
Alexander 98417046bc fix(fitness): fertile window no longer overlaps period bleed
CI / update (push) Has been cancelled
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.
2026-05-10 10:46:14 +02:00
Alexander 244050fa75 feat(fitness): cache more fitness shells & show unsynced workouts on history
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.
2026-05-08 16:28:26 +02:00
Alexander 0814803fc7 fix(offline): IndexedDB fallback when API returns empty on /recipes & /season/[month]
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.
2026-05-08 16:01:59 +02:00
Alexander eb2ffac536 fix(offline): fall back to cached shell on upstream 5xx
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.
2026-05-07 07:52:29 +02:00
Alexander 9a97e41c28 fix(faith): no-Latin prayers always render in monolingual style
CI / update (push) Successful in 1m2s
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.
2026-05-07 07:38:57 +02:00
Alexander 109ac8e13a feat(faith): add closing refrain to Jungfrau Mutter Gottes mein
Adds the traditional final stanza repeating the opening invocation.
2026-05-07 07:35:26 +02:00
Alexander 6275b526d8 feat(faith): info pip on streak counters explaining habit vs piety
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.
2026-05-05 18:15:50 +02:00
Alexander 6456804fc3 feat(faith): add 6 prayers (Marian devotions + meal blessings)
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.
2026-05-05 07:55:54 +02:00
Alexander 585c03a11e feat(offline): hoist sync UI to homepage, slow auto-sync to weekly
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.
2026-05-04 22:21:16 +02:00
Alexander 0372c50084 style(faith): eastertide indicators — fix badge in dark mode, pulse pip in nav
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.
2026-05-04 22:14:34 +02:00
Alexander 065c435d8b feat(offline)!: deploy-proof PWA cache + universal recipe loads
CI / update (push) Successful in 1m10s
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.
2026-05-04 21:32:59 +02:00
Alexander 1bceabe967 feat(errors): merge DE/EN into one page with client-side toggle
CI / update (push) Successful in 48s
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).
2026-05-03 21:42:41 +02:00
Alexander 86c72c2dc3 fix(apologetik): drop duplicate Cache-Control in child loads
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.
2026-05-03 21:42:05 +02:00
Alexander 4623d7a1f7 feat(seo): noindex hook, recipe self-canonical, list-page metadata
CI / update (push) Successful in 37s
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)
2026-05-02 22:23:15 +02:00
Alexander d59cc0a732 feat(seo): image sitemap, Article schemas on apologetik pro + katechese, edge caching
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.
2026-05-02 22:05:50 +02:00
Alexander ecbd24d7a4 feat(seo): per-route html lang, QAPage/Breadcrumb/Event/WebSite schemas, sitemap lastmod
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.
2026-05-02 21:48:05 +02:00
Alexander 7e33ea833e feat(seo): sitemap, OG/canonical/hreflang, JSON-LD i18n
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.
2026-05-02 21:32:06 +02:00
Alexander b10634f831 feat(errors): per-status static error pages for nginx fallback
CI / update (push) Successful in 39s
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.
2026-05-02 20:11:34 +02:00
Alexander e85a2508e8 fix(shopping/loyalty): fail build when card env missing instead of rmSync
CI / update (push) Successful in 52s
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.
2026-05-02 18:36:55 +02:00
Alexander 096d6e2868 feat(rezepte)!: liturgical-aware seasonality via date ranges
CI / update (push) Successful in 3m31s
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.
2026-05-02 17:53:27 +02:00
Alexander 68b078c146 fix(apologetik/contra): scope answer-rail width:max-content to >=760px
CI / update (push) Successful in 3m35s
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.
2026-05-02 16:13:29 +02:00
Alexander 2af845bfc6 feat(offline): redesign sync UI and PWA polish
CI / update (push) Successful in 4m49s
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.
2026-05-02 15:56:21 +02:00
Alexander 6875e8762e fix(offline): gate SW precache on controller, not just registration
`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.
2026-05-02 15:55:12 +02:00
Alexander 4ed0251bb4 feat(branding): adaptive Android launcher icon
CI / update (push) Successful in 3m45s
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.
2026-05-02 15:21:50 +02:00
Alexander 6871e703e8 branding: keep original source svg for icon on git branch 2026-05-02 15:06:13 +02:00
Alexander f02a11afd2 feat(branding): new app logo for Tauri + PWA install
CI / update (push) Successful in 4m26s
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.
2026-05-02 14:45:17 +02:00
Alexander eb9d7a17b3 feat(favicon): single theme-aware SVG, drop legacy raster fallbacks
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.
2026-05-02 14:28:24 +02:00
Alexander ccca1a7959 fix(jellyfin): scope logo bocken.org link to home header only
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.
2026-05-02 13:10:24 +02:00
Alexander 2e8685d02b style(recipes): unify custom multiplier pill with preset pills
CI / update (push) Successful in 5m48s
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.
2026-05-01 14:50:13 +02:00
Alexander bcdb9a9c4b refactor(recipes): split base + cake-form multipliers
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.
2026-05-01 14:27:21 +02:00
Alexander dbce9629a5 fix(recipes): coerce season month to string for resolve()
resolve() requires string params; season[0] is a number, which made
param_value.startsWith blow up on /[recipeLang]/[name] pages.
2026-05-01 14:20:24 +02:00
Alexander 79f4dbb101 i18n(common): bootstrap shared namespace + migrate top-level UI
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).
2026-05-01 14:03:52 +02:00
Alexander 71f7322624 i18n(fitness): migrate inline ternaries across pages and components
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.
2026-05-01 14:01:06 +02:00
Alexander bd9e9b397f i18n(recipes): finish remaining ternaries across components and pages
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.
2026-05-01 13:54:41 +02:00
Alexander ea1a85e935 i18n(recipes): migrate 13 pages and components
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.
2026-05-01 13:34:44 +02:00