Commit Graph

1216 Commits

Author SHA1 Message Date
Maurice cd2f50bc89 chore: trigger CI 2026-04-17 23:36:31 +02:00
Maurice 530550455d feat(ui): unified toolbar design + redesigned budget widgets + polish
Trip planner now has a consistent rounded toolbar across bookings, lists,
budget and files. Each panel shows title, inline filter pills (with
counts where useful) and an accent action button on the right. Moved
per-tab controls into the toolbar — lists import, todo add, budget
currency/add-category, files trash/filters — and dropped the redundant
in-panel headers.

Budget sidebar redesigned: total-budget card with indigo-ringed avatars
and coloured split bar; settlement flows as paired avatar cards;
by-category donut rebuilt in SVG with per-category gradients. Both cards
now follow dark/light mode via a widgetTheme helper.

Todo: add-new-task is a portalled modal on desktop, the add-task input
bar is gone; new SORT BY section in the sidebar; inline category
creation in the task editor.

Reservations: pending / confirmed sections remember their collapsed
state per trip (localStorage).

Misc: per-trip connections toggle moved into the day-plan sidebar,
booking endpoints fixed to show on map for trains/cruises/cars as well,
label localStorage persistence, RESMODAL test updated to the new
airport-select flow.

i18n: the new booking / map / todo / budget strings are translated into
all 15 supported languages.
2026-04-17 23:25:38 +02:00
Julien G. 9a31fcac7b Merge pull request #710 from mauriceboe/feat/photo-thumbnail-cache-686
feat(photos): 1h disk cache for remote thumbnails + fix tab-switch redundant requests
2026-04-17 21:28:42 +02:00
jubnl 677157de1d test(journey): fix getByText assertions broken by keep-mounted tab change
Tabs are now always mounted (visibility toggled via hidden class), so
the same entry title can appear in multiple tab views simultaneously.
Replace getByText with getAllByText for presence checks; scope the
FE-086 click target to the cursor-pointer container.
2026-04-17 21:02:46 +02:00
jubnl b5b1d32b31 feat(photos): add 1h disk cache for remote thumbnails and keep tabs mounted
Closes #686

- Add trekPhotoCache service: SHA1-keyed disk cache under uploads/photos/trek/,
  1h TTL, in-flight dedup map to prevent stampedes on concurrent requests
- Add migration 108: trek_photo_cache_meta table
- Hook cache into streamPhoto for Immich/Synology thumbnail path;
  originals bypass cache
- Add fetchImmichThumbnailBytes / fetchSynologyThumbnailBytes returning
  Buffer instead of piping, used by the cache layer
- Add scheduler entry (every 2h + startup sweep) to evict expired disk
  files and DB rows via sweepExpired()
- Client: convert journey tab conditional-mount to hidden-toggle so
  img elements stay in DOM across tab switches, preventing redundant
  thumbnail requests on rapid tab changes
- Expose invalidateSize() on JourneyMapHandle; call it on map tab
  activation to fix Leaflet rendering in previously-hidden container
2026-04-17 20:49:38 +02:00
jubnl ae4dfc48cc fix(pdf): add allow-scripts to iframe sandbox to suppress CSP warning 2026-04-17 20:22:31 +02:00
Julien G. 3b487519a5 Merge pull request #709 from mauriceboe/feat/system-notice-version-gate
feat(system-notices): replace expiresAt with [minVersion, maxVersion) version gate
2026-04-17 20:15:19 +02:00
Julien G. 1425c4e05b Update maxVersion explanation in system-notices.md
Clarified the explanation regarding setting maxVersion for notices.
2026-04-17 20:09:34 +02:00
Julien G. a84aedc3b4 Fix range notation for app version filtering 2026-04-17 20:07:34 +02:00
jubnl 4b7ba6cb3f feat(system-notices): apply version gates to v3 upgrade notices 2026-04-17 20:04:54 +02:00
jubnl 5952e02971 feat(system-notices): replace expiresAt with [minVersion, maxVersion) version gate
Prevents users who upgrade across multiple versions from seeing all
interim notices at once. Version bounds are evaluated server-side using
semver.coerce so prerelease builds compare as their base release.
Range is lower-inclusive, upper-exclusive: maxVersion: '4.0.0' hides
the notice once 4.0.0 ships.
2026-04-17 20:03:23 +02:00
jubnl 8cd5aa0d23 fix(synology): correct multi-album passphrase assignment and stale trek_photos
- ProviderPicker now tracks per-asset album passphrase in a Map; on confirm,
  assets are grouped by passphrase and submitted as separate batches so each
  asset receives its own album's passphrase instead of the last-selected one
- getOrCreateTrekPhoto unconditionally overwrites the stored passphrase when
  a fresh one is supplied, allowing re-adds to heal a stuck bad passphrase
- deleteTrekPhotoIfOrphan purges the trek_photos row for provider assets when
  no trip_photos or journey_photos reference it anymore; wired into
  removeTripPhoto, removeAlbumLink, and deletePhoto so remove + re-add is a
  clean slate
- Three new integration tests: SYNO-090 (passphrase overwrite), SYNO-091
  (orphan cleanup), SYNO-092 (remove + re-add restores correct passphrase)
2026-04-17 19:48:12 +02:00
Julien G. c0aa252f9a Merge pull request #708 from mauriceboe/fix/google-places-api-quota-reduction
fix(maps): reduce Google Places API quota with persistent caching and kill-switch
2026-04-17 19:33:51 +02:00
jubnl 8a58ce51c0 feat(maps): add kill switches for Google Places autocomplete and details
Add admin toggles for places_autocomplete_enabled and places_details_enabled
alongside the existing places_photos_enabled, all default ON.

- adminService: getPlacesAutocomplete/updatePlacesAutocomplete, getPlacesDetails/updatePlacesDetails
- admin routes: GET/PUT /admin/places-autocomplete, /admin/places-details
- maps routes: autocomplete returns { suggestions: [], source: 'disabled' } when off;
  details returns { place: null, disabled: true } when off
- authService: both flags included in getAppConfig() response
- authStore: placesAutocompleteEnabled + placesDetailsEnabled state and setters
- App.tsx: wire both flags from app-config on load
- AdminPage: two new toggle rows using var(--text-primary)/var(--border-primary) consistent with rest of UI
- i18n: all 15 locales (en, de, ar, br, cs, es, fr, hu, id, it, nl, pl, ru, zh, zhTw)
2026-04-17 19:28:40 +02:00
jubnl 9c2decb095 fix(maps): reduce Google Places API quota usage with persistent caching
P0 — stop the bleeding:
- Honor place.image_url in MapView and TripPlannerPage to skip redundant fetchPhoto calls
- Trim Place Details field mask (drop reviews/editorialSummary from default; new getPlaceDetailsExpanded for inspector)
- Admin toggle places_photos_enabled (default ON) to kill Google photo fetches under quota pressure; Wikimedia unaffected
- Return { photoUrl: null } instead of 204 so client handles disabled state cleanly

P1 — structural fix:
- New placePhotoCache service: persistent disk cache at uploads/photos/google/<sha1>.jpg, atomic writes, stampede dedup via in-flight Map
- Migrations 105-107: google_place_photo_meta table, place_details_cache table, backfill signed Google URLs to stable proxy URLs
- getPlacePhoto rewrites to fetch image bytes directly, store on disk, return /api/maps/place-photo/:id/bytes proxy URL
- Stable proxy URLs written to places.image_url — survive container restarts, no expiry
- New GET /api/maps/place-photo/:placeId/bytes route serving cached files with long-lived Cache-Control
- Place Details DB row cache with 7-day TTL; ?refresh=1 escape hatch
- photoService fast-path: proxy URLs bypass the mapsApi round-trip and go straight to urlToBase64

Bug fixes:
- MapView now requests base64 thumbs for places with proxy image_url (markers were showing color fallback)
- createPlaceIcon accepts /api/maps/place-photo/ URLs as interim fallback while thumb generates
- setSelectedAssignmentId ReferenceError in mobile day-detail handler (use selectAssignment)
- Remove redundant decodeURIComponent on already-decoded Express route param
- Use SHA1 hash for disk filenames to prevent coords:lat:lng pseudo-ID collisions
- Add checkSsrf guard to Wikimedia byte fetch
- Tighten migration 107 LIKE filter to avoid rewriting manually-pasted Google image URLs
- Validate enabled is boolean on PUT /admin/places-photos
- Drop aggressive iconCache.clear() on every thumb arrival

Observability:
- googleFetch() wrapper counts and debug-logs every outbound Google API call with running total
2026-04-17 19:07:39 +02:00
Maurice 5e9c8d2c43 fix(bookings): client test failures after map overlay refactor
- Make useEndpointPane tolerant when map mock lacks getPane/createPane
- Add useMapEvents to react-leaflet mock in MapView.test
- Rewrite RESMODAL-042 to use the new AirportSelect flow (airline and
  flight number only; airport codes are now saved as endpoints, not
  metadata)
2026-04-17 19:03:21 +02:00
Julien G. 39f13881c5 Merge pull request #707 from mauriceboe/fix/journey-page-bugs
fix(journey): fix issue #704 — active logic, archive, places rename, search & trip reminders
2026-04-17 17:05:43 +02:00
jubnl 3b94727c07 fix(journey): fix issue #704 — active logic, archive, places rename, search, trip reminders
- Derive journey lifecycle from linked trip dates (live/upcoming/completed/draft)
  instead of relying solely on status field; status=archived always wins
- Add Archive/Restore Journey action in journey settings dialog
- Rename cities → places end-to-end (SQL alias, TS types, stats field, all locales)
- Wire up search icon: toggles inline input, filters by title+subtitle client-side
- Fix channelConfigured check: trip reminders enabled by default since inapp is
  always available; remove channel check, controlled solely by admin setting
- Expose notify_trip_reminder toggle in Admin → Settings → Notifications
- Add trip_date_min/trip_date_max to listJourneys SQL for client-side lifecycle
- Add archived status to Journey type (server + client)
- Update all 15 locale files with new keys (search, archive, places, trip reminders)
2026-04-17 16:59:23 +02:00
Julien G. 4a5a461d25 Merge pull request #701 from mauriceboe/fix/mobile-overlay-bottom-nav
fix(mobile): account for bottom navbar in overlays and improve system notices UX
2026-04-17 15:40:57 +02:00
jubnl 1963573db4 fix(synology): use Thumbnail API with size xl for originals to avoid HEIC
Replace SYNO.Foto.Download with SYNO.Foto.Thumbnail (size=xl) for the
original kind, mirroring the Immich approach. Synology's download endpoint
returns the raw file (HEIC for iPhone photos), while the Thumbnail API
always serves a browser-compatible JPEG render.
2026-04-17 15:35:42 +02:00
jubnl 5046e1a2e0 fix(synology): wire shared-album passphrase through journey-entry add flow
Thread selectedAlbumPassphrase from ProviderPicker through onAdd →
journeyApi.addProviderPhotos → POST /entries/:entryId/provider-photos →
addProviderPhoto service → getOrCreateTrekPhoto so shared-album photos
have their passphrase encrypted and persisted on trek_photos at add-time,
enabling streamPhoto to forward it to Synology correctly (#689).
2026-04-17 15:33:05 +02:00
jubnl a1f3b4476e fix(system-notices): overhaul mobile bottom sheet UX
- Replace "Next Notice >" CTA with proper < > pager buttons
- Fix shared scroll container: each slot now scrolls independently
- Sheet uses fixed h-[85dvh] so height is consistent across all notices
- Sticky footer (pager + CTA) always anchored at bottom of each slot
- Content area vertically centered when shorter than available space
- Dismiss-drag suppressed when slot is scrolled down (pan-up to scroll back)
- Scroll position resets on navigation via per-slot refs
- Adjacent slot scroll cleared on horizontal gesture classification
- OK button navigates to next notice on non-last pages, dismisses on last
- OK button only shown when dismissible or on last notice
2026-04-17 15:06:23 +02:00
Maurice 8defc90e95 feat(bookings): show transport routes on map (#384, #587)
Adds from/to endpoints to flight/train/cruise/car reservations with
live map rendering. Flights use geodesic arcs and a curved duration +
distance badge; train/car/cruise render as straight or geodesic lines
with endpoint markers. Airports come from an embedded OurAirports
database (~3200 airports, offline-capable); train/cruise/car locations
via Nominatim. Per-trip connection toggle sits in the day plan
sidebar, persisted in localStorage. Clicking a map endpoint opens the
existing transport detail popup. New display setting toggles endpoint
labels on the map. Migration 105 adds the reservation_endpoints table
plus needs_review flag; existing flights are backfilled from their
IATA metadata on server startup.
2026-04-17 14:04:40 +02:00
jubnl b2a39a3071 Merge dev into fix/mobile-overlay-bottom-nav, resolve conflicts 2026-04-17 00:01:18 +02:00
Maurice 21511c2f68 Merge pull request #700 from mauriceboe/feat/v3-thankyou-notice
feat: v3 thank-you notice, mobile map+timeline, modal UX improvements
2026-04-16 23:51:13 +02:00
Maurice 0e5c819f7c fix: adapt tests for last-page-only dismiss and fix editor z-index
- SystemNoticeModal tests: navigate to last page before testing
  X button, ESC, and CTA dismiss (matches new last-page-only behavior)
- EntryEditor: use z-[9999] instead of portal (fixes iOS stacking
  without breaking test DOM queries)
- Pros/cons inputs: remove colored backgrounds in dark mode
2026-04-16 23:46:07 +02:00
Maurice 0f44d7d264 feat(journey): combined map+timeline view on mobile (Polarsteps-style)
Merge the separate Timeline and Map tabs into a single fullscreen
combined view on mobile (<1024px). A Leaflet map fills the background
while a horizontal snap-scroll carousel of entry cards sits at the
bottom. Scrolling the carousel auto-focuses the corresponding map
marker; tapping a marker scrolls to the card. Tapping a card opens
a new fullscreen entry view with edit/delete actions.

- New: MobileMapTimeline, MobileEntryCard, MobileEntryView components
- New: useIsMobile hook (matchMedia < 1024px)
- JourneyMap: fullScreen + paddingBottom props, focusMarker guard
- Desktop layout completely unchanged
- Public share page gets the same combined view (read-only)
- Fix: entry editor now portaled to body (iOS stacking context)
- Fix: pros/cons dark mode input backgrounds
- Fix: mood button borders in dark mode
- Fix: location icon color (neutral instead of green/indigo)
2026-04-16 23:37:09 +02:00
jubnl e078a9d9e1 fix: getAppVersion now getting 1st from environment, fallback to package.json, fallback to 0.0.0 if all failed 2026-04-16 23:36:33 +02:00
jubnl fef12b0e8b fix(mobile): account for bottom navbar in overlays and improve system notices UX
- Add paddingBottom: var(--bottom-nav-h) to all mobile overlays that were
  clipping content behind the bottom navbar: EntryEditor, SystemNoticeModal,
  JourneyPage create modal, TodoListPanel sheets, TripPlannerPage
  PlaceInspector, PackingListPanel bag modal, both PhotoLightboxes,
  FileManager viewer, and shared Modal primitive
- Replace single-notice mobile bottom sheet with a 3-slot horizontal strip
  so adjacent notices are physically present during drag
- Add live-follow swipe left/right to navigate between notices with
  spring-back when under threshold and flushSync to eliminate blink on commit
- Add live-follow swipe down to dismiss all notices with spring-back;
  backdrop tap also triggers the slide-down animation
- Normalize notice height with useLayoutEffect minHeight on strip and
  align-items: stretch so all slots are always the tallest notice height
- Pin CTA button at consistent Y across notices via flex-1 + mt-auto;
  always render invisible Not now placeholder to equalise CTA section height
- Move pager dots/counter below CTA buttons
2026-04-16 22:49:20 +02:00
Maurice df075630fb feat(system-notices): add personal thank-you notice for v3.0.0
Personal note from the creator shown as the first page in the 3.0
upgrade modal. Includes community links (Discord, Ko-fi) and a
special shout-out to jubnl. Modal UX improved: users must click
through all pages before dismissing, wider layout, enhanced
markdown rendering with styled links, signature, and HR separator.
i18n coverage across all 15 languages.
2026-04-16 22:25:03 +02:00
Julien G. bffb55d8c0 Merge pull request #699 from mauriceboe/fix/journey-gallery-lightbox-grouping
fix(journey): gallery lightbox navigates all photos, not just same-day entry
2026-04-16 21:43:07 +02:00
jubnl 5c24213b0e fix(journey): gallery lightbox navigates all photos, not just same-day entry 2026-04-16 21:35:52 +02:00
Julien G. 12a457801a Merge pull request #697 from mauriceboe/fix/journey-photo-thumbnail-cache
fix(journey): serve local file when uploading photos with Immich syncenabled
2026-04-16 21:29:59 +02:00
jubnl ae4d317dc3 fix(journey): serve local file when uploading photos with Immich sync enabled
After upload, trek_photos.provider is immediately flipped to 'immich' even
though Immich's thumbnail generation is async. streamPhoto then routed to
Immich, which returned an error for the not-yet-processed asset. Because
Cache-Control was set before the proxy attempt, the error response was cached
by the browser for 24h — breaking thumbnails until a hard refresh bypassed
the cache and Immich had finished processing.

- streamPhoto now prefers the local file_path when it exists on disk,
  regardless of provider; Immich/Synology are only used when no local
  file is available (fixes the immediate broken-thumbnail symptom)
- pipeAsset sets Cache-Control: no-store on upstream errors and uses the
  caller-supplied default only on success (prevents cache poisoning)
- streamImmichAsset no longer pre-sets Cache-Control before the proxy
- streamSynologyAsset passes the same defaultCacheControl through pipeAsset

Closes #691
2026-04-16 21:20:38 +02:00
Julien G. f7c6854059 Merge pull request #693 from mauriceboe/fix/synology-shared-albums-pagination
Fix/synology shared albums pagination
2026-04-16 21:06:28 +02:00
jubnl bdb6b01765 fix(synology): paginate all three album sources past 100 albums and tighten targetUserId type
- Extract _fetchAllSynologyAlbums helper that loops until the source is
  exhausted; listSynologyAlbums now uses it for personal, shared-out,
  and shared-with-me instead of a hard-capped single request of 100
- Make getSynologyAssetInfo targetUserId required (number, not number|undefined)
  to match every call site and eliminate an implicit any at the _requestSynologyApi
  boundary
2026-04-16 20:54:35 +02:00
jubnl 129dfabaa3 feat(synology): persist and use passphrase for shared album photo streaming (#689-4)
- syncSynologyAlbumLink now uses getAlbumLinkForSync to read the stored
  passphrase and passes it in the SYNO.Foto.Browse.Item call when present,
  falling back to album_id for links without a passphrase.
- Selection type gains optional passphrase field; addTripPhotos and
  _addTripPhoto thread it through to getOrCreateTrekPhoto.
- getOrCreateTrekPhoto accepts an optional passphrase (4th param) and
  encrypts it when inserting a new trek_photos row; backfills existing
  rows that lack a passphrase.
- streamPhoto and getPhotoInfo decrypt the stored passphrase from
  trek_photos and forward it to streamSynologyAsset / getSynologyAssetInfo
  so shared-album photos resolve correctly at access time.
- Add SYNO-054 integration test covering the passphrase sync-and-persist
  path end-to-end.
2026-04-16 20:05:18 +02:00
jubnl 8a6d1b2aaf feat(synology): merge personal, shared-out, and shared-with-me albums in listSynologyAlbums
Fire all three Synology album sources in parallel via Promise.allSettled so a
permissions failure on one source (e.g. SYNO.Foto.Sharing.Misc) never blocks
personal album display. Deduplicate by album id (last-write-wins), propagate
passphrase from shared/shared-with-me entries, and return the merged list sorted
by albumName. Extends AlbumsList type to carry optional passphrase.

Adds SYNO-027/028/029 integration tests; updates SYNO-060/061/081 to match
the new multi-source call pattern.
2026-04-16 19:56:10 +02:00
jubnl 465b78411a fix(synology): resolve pagination offset using correct size before computing page offset
The `size` → `limit` assignment was evaluated after `page * limit`, causing
the offset to be computed using the hardcoded default (100) instead of the
caller-supplied page size. Swapping the two `if` blocks ensures `limit` is
resolved from `size` first so the offset is always `(page-1) * size`.

Adds SYNO-025 and SYNO-026 integration tests that capture the raw Synology
API body and assert `offset` and `limit` are forwarded correctly.
2026-04-16 19:49:08 +02:00
Julien G. 272b32b410 Merge pull request #685 from mauriceboe/fix/hide-mobile-scrollbars
fix(ui): hide scrollbars on mobile, keep styled bars on desktop
v3.0.0-pre.47
2026-04-16 16:50:20 +02:00
jubnl 7945e752d6 fix(ui): restore scrollbar-width: thin on .scroll-container 2026-04-16 16:44:27 +02:00
jubnl 6eb3ab38fb fix(ui): hide scrollbars on mobile, keep styled bars on desktop
Scrollbars on mobile caused layout shift (content pushed left).
Hidden via media query on mobile; desktop retains thin styled scrollbars.
Also removes inline scrollbarWidth override in DayPlanSidebar that bypassed the CSS rule.
2026-04-16 16:42:36 +02:00
Julien G. c7a9210215 Merge pull request #684 from mauriceboe/fix/batch-673-674-675-678-679-680
fix(journey): batch bug fixes #673 #674 #675 #678 #679 #680
v3.0.0-pre.46
2026-04-16 16:06:52 +02:00
jubnl d5d63aa979 test(journey): fix FE-PAGE-JOURNEYDETAIL-027 flaky spinner assertion
Pre-seed the store into loading state before render instead of relying on
timing. RTL's render() flushes all microtasks via act(), so the MSW response
lands before render() returns, leaving no observable loading window.
2026-04-16 16:01:06 +02:00
jubnl 84574020f2 fix(journey): increase PDF preview button touch targets for mobile
Raises button min-height to 44px and bumps padding/font-size to meet Apple HIG
minimum touch-target guidelines on iOS PWA. Fixes #680.
2026-04-16 15:55:20 +02:00
jubnl 1b7ea2c87d fix(journey): replace window.open with srcdoc iframe overlay for PDF preview
Rewrites downloadJourneyBookPDF to render the preview in an in-page srcdoc
iframe overlay instead of calling window.open(), which Safari iOS PWA blocks
in async callbacks. Matches the existing TripPDF pattern. Fixes #679.
2026-04-16 15:54:07 +02:00
jubnl 47b7678975 fix(journey): remove backdropFilter from modal overlays to fix iOS Safari PWA white screen
backdrop-filter: blur() on position:fixed elements is a known Safari iOS
compositing failure in standalone (PWA) mode. When the GPU layer behind
a fixed overlay is uninitialized, the blur samples white instead of the
actual content, overriding the semi-transparent background and rendering
a fully white screen that requires a force-close to escape.

The JourneySettingsDialog (bottom-sheet on mobile) was most affected due
to its items-end layout, but all five modal overlays in JourneyDetailPage
had the same pattern. Removed backdropFilter from all five and bumped
opacity from 0.6 to 0.75 to maintain visual separation. Closes #678.
2026-04-16 15:45:37 +02:00
jubnl da70388f4b fix(journey): resolve Immich photos on public share by matching trek_photos.id
validateShareTokenForPhoto was querying journey_photos by jp.id but the
public page sends p.photo_id (trek_photos.id) in the URL. In a fresh
database the IDs coincidentally match, masking the bug. In production
instances with many Immich-synced photos the trek_photos autoincrement
is far ahead of journey_photos, causing a 404 for every Immich photo
on the public share page.

Fix: change the lookup to jp.photo_id = ? so validation is keyed on
trek_photos.id, which is what the client sends and what streamPhoto
needs. Updated the test helper to return trekId and added a regression
test that pre-populates trek_photos to produce diverging IDs. Closes #675.
2026-04-16 15:37:24 +02:00
jubnl 6c1a795460 fix(journey): paginate Immich picker and group photos by date
The /search route was looping up to 20 pages server-side, returning a
blob of up to 1000 photos with no hasMore flag, which prevented the
client's existing ScrollTrigger infinite scroll from ever firing.

Now the route proxies the client's page param directly to Immich and
returns a single page plus hasMore, enabling full library browsing.

The photo picker grid now groups photos by takenAt date (already
present in every asset response) with a date label above each group,
restoring the date-oriented browsing from V2. Closes #674.
2026-04-16 15:32:56 +02:00
jubnl 75d23eb6aa fix(journey): keep page mounted during in-place journey refetch
loadJourney previously set loading=true unconditionally, causing the
JourneyDetailPage guard (if loading || !current) to unmount the entire
page tree on every background refetch — entry saves, settings saves,
trip link/unlink, contributor invite, delete, and WS realtime events
all triggered the full-page spinner flash.

Now loading is only toggled on cold loads (current?.id !== id).
Warm refreshes replace current silently so the hero, sidebar, map,
and timeline stay mounted throughout. Closes #673.
2026-04-16 15:27:13 +02:00