From e00e6f5a083779560e8aee36125508e228bcff24 Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 9 Apr 2026 15:14:52 +0200 Subject: [PATCH] refactor(shared-branding): derive APP_URLS from APP_ICONS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hand-maintained APP_URLS map kept silently drifting from the AppIconId union — most recently the new 'who' entry was missing, which crashed getPillAppItems at runtime with "Cannot read properties of undefined (reading 'prod')". Drift was already flagged by the type system but the error was lost in the existing svelte-check noise. APP_URLS is now generated at module load by walking Object.keys of APP_ICONS (the source of AppIconId), so every id is guaranteed a URL. A small APP_URL_OVERRIDES map carries the handful of apps that don't follow the unified mana.how/{id} pattern (root path for the unified shell, subdomains for standalone apps like arcade). Adds two integrity tests as defense-in-depth: one asserts every MANA_APPS id has a matching APP_ICONS icon, the other asserts every AppIconId resolves to a non-empty dev+prod URL. Both would have caught the 'who' regression on its own without needing svelte-check. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../shared-branding/src/mana-apps.spec.ts | 25 +++++++ packages/shared-branding/src/mana-apps.ts | 69 +++++++------------ 2 files changed, 49 insertions(+), 45 deletions(-) diff --git a/packages/shared-branding/src/mana-apps.spec.ts b/packages/shared-branding/src/mana-apps.spec.ts index 6ff090ded..d6a79b42e 100644 --- a/packages/shared-branding/src/mana-apps.spec.ts +++ b/packages/shared-branding/src/mana-apps.spec.ts @@ -7,7 +7,9 @@ import { getManaApp, getManaAppsByStatus, MANA_APPS, + APP_URLS, } from './mana-apps'; +import { APP_ICONS } from './app-icons'; describe('getTierLevel', () => { it('returns correct levels for all tiers', () => { @@ -121,4 +123,27 @@ describe('MANA_APPS integrity', () => { expect(app.description.en).toBeTruthy(); }); }); + + it('every MANA_APPS entry has a corresponding APP_ICONS icon', () => { + // Catches the case where a new app is registered but no icon was + // added to APP_ICONS (which is also where AppIconId is derived). + const missing = MANA_APPS.filter((app) => !APP_ICONS[app.id]); + expect(missing.map((a) => a.id)).toEqual([]); + }); +}); + +describe('APP_URLS integrity', () => { + it('every AppIconId resolves to a non-empty dev + prod URL', () => { + // Regression guard: when `who` was added to AppIconId but never to + // the hand-maintained APP_URLS map, getPillAppItems crashed at + // runtime with "Cannot read properties of undefined (reading + // 'prod')". APP_URLS is now derived from APP_ICONS, so this test + // will fail loudly if that derivation ever stops covering every id. + for (const id of Object.keys(APP_ICONS) as Array) { + const entry = APP_URLS[id]; + expect(entry, `APP_URLS missing entry for "${id}"`).toBeDefined(); + expect(entry.dev, `APP_URLS["${id}"].dev is empty`).toBeTruthy(); + expect(entry.prod, `APP_URLS["${id}"].prod is empty`).toBeTruthy(); + } + }); }); diff --git a/packages/shared-branding/src/mana-apps.ts b/packages/shared-branding/src/mana-apps.ts index f8a6b4051..41ec22d24 100644 --- a/packages/shared-branding/src/mana-apps.ts +++ b/packages/shared-branding/src/mana-apps.ts @@ -795,57 +795,36 @@ export const APP_SLIDER_LABELS = { } as const; /** - * Default app URLs for local development and production - */ -/** - * App URLs — unified app uses internal paths, separate apps use subdomains. + * App URLs — derived automatically from MANA_APPS. * - * All productivity apps are now served under mana.how/{appId}. - * Games remain on separate subdomains. + * Almost every productivity app lives under the unified mana.how/{id} + * surface, so listing each entry by hand was duplicated bookkeeping that + * silently drifted (a missing entry crashed `getPillAppItems` at runtime + * when the new `who` app was added). Instead we generate the map from + * MANA_APPS at module load and only override the few apps that don't + * follow the unified-path convention. + * + * To add a new app: register it in MANA_APPS and you're done. To put it + * on its own subdomain or use a custom port: add an entry to + * APP_URL_OVERRIDES below. */ -export const APP_URLS: Record = { - // ─── Unified App (internal paths) ───────────────────────── +const APP_URL_OVERRIDES: Partial> = { + // The unified app itself lives at the root, not at /mana. mana: { dev: 'http://localhost:5173', prod: 'https://mana.how' }, - todo: { dev: 'http://localhost:5173/todo', prod: 'https://mana.how/todo' }, - calendar: { dev: 'http://localhost:5173/calendar', prod: 'https://mana.how/calendar' }, - contacts: { dev: 'http://localhost:5173/contacts', prod: 'https://mana.how/contacts' }, - chat: { dev: 'http://localhost:5173/chat', prod: 'https://mana.how/chat' }, - picture: { dev: 'http://localhost:5173/picture', prod: 'https://mana.how/picture' }, - cards: { dev: 'http://localhost:5173/cards', prod: 'https://mana.how/cards' }, - zitare: { dev: 'http://localhost:5173/zitare', prod: 'https://mana.how/zitare' }, - clock: { dev: 'http://localhost:5173/clock', prod: 'https://mana.how/clock' }, - music: { dev: 'http://localhost:5173/music', prod: 'https://mana.how/music' }, - storage: { dev: 'http://localhost:5173/storage', prod: 'https://mana.how/storage' }, - presi: { dev: 'http://localhost:5173/presi', prod: 'https://mana.how/presi' }, - inventory: { dev: 'http://localhost:5173/inventory', prod: 'https://mana.how/inventory' }, - photos: { dev: 'http://localhost:5173/photos', prod: 'https://mana.how/photos' }, - skilltree: { dev: 'http://localhost:5173/skilltree', prod: 'https://mana.how/skilltree' }, - citycorners: { dev: 'http://localhost:5173/citycorners', prod: 'https://mana.how/citycorners' }, - times: { dev: 'http://localhost:5173/times', prod: 'https://mana.how/times' }, - context: { dev: 'http://localhost:5173/context', prod: 'https://mana.how/context' }, - questions: { dev: 'http://localhost:5173/questions', prod: 'https://mana.how/questions' }, - nutriphi: { dev: 'http://localhost:5173/nutriphi', prod: 'https://mana.how/nutriphi' }, - planta: { dev: 'http://localhost:5173/planta', prod: 'https://mana.how/planta' }, - uload: { dev: 'http://localhost:5173/uload', prod: 'https://mana.how/uload' }, - calc: { dev: 'http://localhost:5173/calc', prod: 'https://mana.how/calc' }, - moodlit: { dev: 'http://localhost:5173/moodlit', prod: 'https://mana.how/moodlit' }, - memoro: { dev: 'http://localhost:5173/memoro', prod: 'https://mana.how/memoro' }, - guides: { dev: 'http://localhost:5173/guides', prod: 'https://mana.how/guides' }, - habits: { dev: 'http://localhost:5173/habits', prod: 'https://mana.how/habits' }, - notes: { dev: 'http://localhost:5173/notes', prod: 'https://mana.how/notes' }, - dreams: { dev: 'http://localhost:5173/dreams', prod: 'https://mana.how/dreams' }, - cycles: { dev: 'http://localhost:5173/cycles', prod: 'https://mana.how/cycles' }, - events: { dev: 'http://localhost:5173/events', prod: 'https://mana.how/events' }, - finance: { dev: 'http://localhost:5173/finance', prod: 'https://mana.how/finance' }, - places: { dev: 'http://localhost:5173/places', prod: 'https://mana.how/places' }, - wisekeep: { dev: 'http://localhost:5173/wisekeep', prod: 'https://mana.how/wisekeep' }, - news: { dev: 'http://localhost:5173/news', prod: 'https://mana.how/news' }, - mail: { dev: 'http://localhost:5173/mail', prod: 'https://mana.how/mail' }, - who: { dev: 'http://localhost:5173/who', prod: 'https://mana.how/who' }, - // ─── Separate Apps (own subdomains) ─────────────────────── + // Standalone apps on their own subdomain / port. arcade: { dev: 'http://localhost:5201', prod: 'https://arcade.mana.how' }, }; +export const APP_URLS: Record = Object.fromEntries( + (Object.keys(APP_ICONS) as AppIconId[]).map((id) => [ + id, + APP_URL_OVERRIDES[id] ?? { + dev: `http://localhost:5173/${id}`, + prod: `https://mana.how/${id}`, + }, + ]) +) as Record; + /** * App item type for PillNavigation app switcher */