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).
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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
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.
- 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
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.
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.
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.
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.
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.
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.
- 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
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.
- 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