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
- 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)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
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.
- 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
- 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.
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.
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 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
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
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.
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.
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).
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
- 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
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
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 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.