fix(client): give the desktop halves the same fixes their mobile twins got

Several of the fixes in this branch landed on a mobile screen while the
desktop component with the identical bug sat in another package and was left
alone, which leaves the two disagreeing about the same handler.

So: the appearance tab flushes its debounced write on unmount, the journey
share copy guards navigator.clipboard (undefined on a plain-HTTP self-hosted
install), the admin collab toggle rolls back from a scoped snapshot instead
of a stale one, the reservation modal parses metadata through the guarded
helper rather than a bare JSON.parse that takes the route down, and the
notification toggle surfaces its failures and stops racing concurrent
toggles.

The Journey components reached the memories provider-status endpoint with a
raw fetch(), past every apiClient interceptor; they go through a small
memoriesApi now. And the server half of the OSM menu_url finding: the client
renders it through safeHttpUrl, so place-enrichment should not be handing a
javascript: value out in the first place.

The share section hides itself when the server says the link is not this
user's to manage, rather than offering a create button that 403s.
This commit is contained in:
Maurice
2026-08-23 10:38:23 +02:00
committed by Maurice
parent e72338bbbc
commit 3e0bfbbb29
22 changed files with 435 additions and 76 deletions
@@ -1,5 +1,6 @@
import { createHash } from 'node:crypto';
import { Injectable } from '@nestjs/common';
import { placeWebsiteSchema } from '@trek/shared';
import type {
MapsPlaceEnrichmentRequest,
MapsPlaceEnrichmentResult,
@@ -173,8 +174,11 @@ export function collectFacts(details: Record<string, unknown> | null): PlaceFact
// OSM writes several cuisines semicolon-separated and underscored.
if (cuisine) push('cuisine', cuisine.split(';').map((c) => c.replace(/_/g, ' ').trim()).filter(Boolean).join(', '));
const menu = typeof details.menu_url === 'string' ? details.menu_url : null;
if (menu) push('menu', null, menu);
// `menu_url` is a community-editable OSM tag that becomes an href on the
// client, so it goes through the same allow-list as a place's website —
// anything but http(s) is dropped rather than rendered as a link.
const menu = typeof details.menu_url === 'string' ? details.menu_url.trim() : '';
if (placeWebsiteSchema.safeParse(menu).success) push('menu', null, menu);
if (yesNo(details.outdoor_seating) === 'yes') push('outdoorSeating', null);
if (yesNo(details.takeaway) === 'yes') push('takeaway', null);