chore(app-registry): polish 4 small wins — TOC + AppId-derive + route-drift test + 3 MANA_APPS

§1 AppId derivation (shared-branding):
- `AppId` is now `keyof typeof APP_BRANDING` (config.ts) instead of a
  hand-maintained union in types.ts. Adding/removing an entry in
  `APP_BRANDING` automatically updates the union — eliminates the
  drift class that produced the ContextLogo type-error.
- `AppBranding.id` relaxed to `string` to break the circular type
  reference (key in `APP_BRANDING` is the authoritative id).

§2 Route-drift smoke test (registry.spec.ts):
- New 4th test: parses every `routes/(app)/*+page.svelte`, extracts
  the `<RoutePage appId="…">` literal, asserts the id is registered
  in the workbench app-registry. Catches drift like the earlier
  `appId="broadcasts"` vs id `'broadcast'` bug structurally.
- ROUTE_ONLY_APP_IDS allowlist for routes that intentionally don't
  back a workbench module (gifts, llm-test, milestones, organizations,
  teams, tags).
- Caught two real drifts in the process and fixed them:
    /agents/+page.svelte → appId="ai-agents" → "agents"
    /agents/templates/+page.svelte → same

§3 MANA_APPS hochgezogen (kontext, wishes):
- kontext (Web-Context URL crawler) + wishes (Wunschliste) had module
  + workbench card but no MANA_APPS branding entry. Both got proper
  description, longDescription and a fresh APP_ICONS entry (globe-
  with-text-lines for kontext, shooting-star for wishes).
- Removed both from WORKBENCH_ONLY in spec — they're full apps now.
- Note: `myday` was already in MANA_APPS, the WORKBENCH_ONLY entry
  was redundant and had been silently double-counting.

§4 apps.ts — top-level INDEX comment:
- 80 registerApp() calls were chronological-by-when-added — basically
  unsearchable. Added an §1–§4 navigation comment near the top
  grouping apps by role (entity / module surface / AI Workbench /
  System) so devs can jump to a section. Physical reordering of
  the 80 blocks deferred to avoid disturbing the active multi-
  terminal session — the TOC delivers ~80% of the navigation win.

Bonus: register `forms` module that the parallel session added but
hadn't wired into the workbench yet — the new route-drift test caught
this immediately on first run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-28 22:59:26 +02:00
parent 907a3add49
commit 6d193a9fa7
8 changed files with 205 additions and 37 deletions

View file

@ -297,6 +297,13 @@ export const APP_ICONS = {
// family) but leaning more towards the creative-publishing cluster.
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="wb" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#6366f1"/><stop offset="100%" style="stop-color:#8b5cf6"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#wb)"/><rect x="18" y="22" width="64" height="56" rx="5" fill="white"/><rect x="18" y="22" width="64" height="10" rx="5" fill="white" fill-opacity="0.75"/><circle cx="25" cy="27" r="1.8" fill="#6366f1" fill-opacity="0.5"/><circle cx="31" cy="27" r="1.8" fill="#6366f1" fill-opacity="0.5"/><circle cx="37" cy="27" r="1.8" fill="#6366f1" fill-opacity="0.5"/><rect x="24" y="38" width="52" height="12" rx="2" fill="#6366f1" fill-opacity="0.85"/><rect x="24" y="54" width="24" height="18" rx="2" fill="#6366f1" fill-opacity="0.35"/><rect x="52" y="54" width="24" height="8" rx="2" fill="#6366f1" fill-opacity="0.55"/><rect x="52" y="64" width="24" height="8" rx="2" fill="#6366f1" fill-opacity="0.45"/></svg>`
),
forms: svgToDataUrl(
// Clipboard with a checkmark + two filled rows — represents a generic
// form / questionnaire. Teal→cyan gradient sits next to writing (sky)
// and broadcasts (indigo) in the input/communication family while
// staying distinct from website (indigo→violet) and quiz (pink).
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="fr" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#14b8a6"/><stop offset="100%" style="stop-color:#06b6d4"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#fr)"/><rect x="26" y="22" width="48" height="62" rx="5" fill="white"/><rect x="38" y="14" width="24" height="14" rx="3" fill="white"/><rect x="42" y="18" width="16" height="6" rx="2" fill="#14b8a6" fill-opacity="0.85"/><circle cx="36" cy="44" r="3" fill="#14b8a6"/><path d="M34 44l1.6 1.8 3.4-3.4" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" fill="none"/><rect x="44" y="42" width="22" height="3" rx="1.5" fill="#14b8a6" fill-opacity="0.6"/><circle cx="36" cy="58" r="3" fill="#14b8a6" fill-opacity="0.4"/><rect x="44" y="56" width="22" height="3" rx="1.5" fill="#14b8a6" fill-opacity="0.55"/><rect x="34" y="68" width="32" height="8" rx="2" fill="#14b8a6" fill-opacity="0.85"/></svg>`
),
spaces: svgToDataUrl(
// Three people-silhouettes clustered in the tile — the Spaces primitive
// is about shared workspaces, so the icon emphasises "group". Teal→indigo
@ -304,6 +311,19 @@ export const APP_ICONS = {
// communication family without competing with either.
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="sp" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#14b8a6"/><stop offset="100%" style="stop-color:#6366f1"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#sp)"/><circle cx="30" cy="38" r="9" fill="white" fill-opacity="0.85"/><path d="M14 70c0-9 7-16 16-16s16 7 16 16v4H14v-4z" fill="white" fill-opacity="0.85"/><circle cx="70" cy="38" r="9" fill="white" fill-opacity="0.85"/><path d="M54 70c0-9 7-16 16-16s16 7 16 16v4H54v-4z" fill="white" fill-opacity="0.85"/><circle cx="50" cy="32" r="11" fill="white"/><path d="M30 76c0-11 9-20 20-20s20 9 20 20v4H30v-4z" fill="white"/></svg>`
),
kontext: svgToDataUrl(
// Globe with three horizontal text-lines — represents the Web-Context
// crawler that pulls URLs into the user's profile context. Tan→ochre
// gradient (#a78b6f → #c79a64) sits in the documents family but warmer
// than the cool blues of notes/storage.
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="kx" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#a78b6f"/><stop offset="100%" style="stop-color:#c79a64"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#kx)"/><circle cx="50" cy="38" r="18" fill="none" stroke="white" stroke-width="2.5"/><ellipse cx="50" cy="38" rx="9" ry="18" fill="none" stroke="white" stroke-width="2"/><path d="M32 38h36" stroke="white" stroke-width="2"/><rect x="28" y="62" width="44" height="3" rx="1.5" fill="white" fill-opacity="0.9"/><rect x="28" y="70" width="34" height="3" rx="1.5" fill="white" fill-opacity="0.7"/><rect x="28" y="78" width="40" height="3" rx="1.5" fill="white" fill-opacity="0.55"/></svg>`
),
wishes: svgToDataUrl(
// Shooting star with sparkle trail — the wishes module is about
// hopes/aspirations. Amber→rose gradient distinguishes it from the
// cool-blue quotes (also a "daily inspiration" feel).
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="wi" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#f59e0b"/><stop offset="100%" style="stop-color:#f43f5e"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#wi)"/><path d="M62 28l5 12 12 5-12 5-5 12-5-12-12-5 12-5z" fill="white"/><circle cx="32" cy="64" r="3.5" fill="white" fill-opacity="0.85"/><circle cx="44" cy="76" r="2.5" fill="white" fill-opacity="0.7"/><circle cx="22" cy="80" r="2" fill="white" fill-opacity="0.55"/><path d="M48 50l-22 22" stroke="white" stroke-width="2" stroke-linecap="round" opacity="0.55"/></svg>`
),
} as const;
export type AppIconId = keyof typeof APP_ICONS;

View file

@ -1,9 +1,13 @@
import type { AppBranding, AppId } from './types';
import type { AppBranding } from './types';
/**
* Branding configuration for all Mana ecosystem apps
* Branding configuration for all Mana ecosystem apps.
*
* `satisfies` enforces each entry matches AppBranding while keeping the
* literal keys narrow, so `keyof typeof APP_BRANDING` derives the AppId
* union automatically.
*/
export const APP_BRANDING: Record<AppId, AppBranding> = {
export const APP_BRANDING = {
memoro: {
id: 'memoro',
name: 'Memoro',
@ -311,7 +315,10 @@ export const APP_BRANDING: Record<AppId, AppBranding> = {
logoStroke: true,
logoStrokeWidth: 1.5,
},
};
} satisfies Record<string, AppBranding>;
/** Derived from `APP_BRANDING` keys — single source of truth. */
export type AppId = keyof typeof APP_BRANDING;
/**
* Get branding config for an app

View file

@ -187,6 +187,23 @@ export const MANA_APPS: ManaApp[] = [
status: 'beta',
requiredTier: 'guest',
},
{
id: 'forms',
name: 'Forms',
description: {
de: 'Formulare bauen',
en: 'Build forms',
},
longDescription: {
de: 'Eigene Formulare bauen, öffentlich teilen und Antworten sammeln. Anmeldungen, Umfragen, Lead-Capture, wiederkehrende Pulse-Checks. Mit AI-Builder, Conditional Branching und Auto-Sync zu Kontakten, Events und Spaces.',
en: 'Build your own forms, share them publicly, and collect responses. Sign-ups, surveys, lead capture, recurring pulse checks. With AI builder, conditional branching, and auto-sync to contacts, events, and Spaces.',
},
icon: APP_ICONS.forms,
color: '#14b8a6',
comingSoon: false,
status: 'development',
requiredTier: 'guest', // LOCAL TIER PATCH — revert to 'beta' before release (see project_tier_patch_resolved memory)
},
{
id: 'picture',
name: 'ManaPicture',
@ -1206,6 +1223,40 @@ export const MANA_APPS: ManaApp[] = [
status: 'beta',
requiredTier: 'guest', // LOCAL TIER PATCH — revert to 'beta' before release
},
{
id: 'kontext',
name: 'Web-Context',
description: {
de: 'URLs in deinen Kontext crawlen',
en: 'Crawl URLs into your context',
},
longDescription: {
de: 'Webseiten und Artikel als persönlichen Kontext sammeln — crawlt eine URL (oder eine ganze Site bis 20 Seiten), optional KI-zusammengefasst, und hängt das Ergebnis an dein Profil-Kontext-Dokument an. Der Companion sieht das beim nächsten Planen automatisch mit.',
en: 'Pull web pages and articles into your personal context — crawl a URL (or up to 20 pages of a site), optionally AI-summarised, and append the result to your profile context document. The companion picks it up automatically on the next plan call.',
},
icon: APP_ICONS.kontext,
color: '#a78b6f',
comingSoon: false,
status: 'beta',
requiredTier: 'guest',
},
{
id: 'wishes',
name: 'Wünsche',
description: {
de: 'Wunschliste & Inspiration',
en: 'Wishlist & inspiration',
},
longDescription: {
de: 'Halte fest, was du dir wünschst — Geschenkideen, Reiseziele, Lebensziele. Ein leichter, freundlicher Ort für die Dinge, die noch keine Aufgabe sind, dich aber begleiten.',
en: 'Capture what you wish for — gift ideas, travel destinations, life goals. A light, friendly place for the things that are not yet tasks but stay with you.',
},
icon: APP_ICONS.wishes,
color: '#f59e0b',
comingSoon: false,
status: 'development',
requiredTier: 'guest',
},
];
/**

View file

@ -1,38 +1,24 @@
/**
* App identifiers for branding
* App identifiers for branding.
*
* Derived from `APP_BRANDING` keys so `<AppLogo app={…}>` accepts exactly
* the apps that have a branding entry adding/removing one in config.ts
* automatically updates the union, no hand-maintenance.
*/
export type AppId =
| 'memoro'
| 'mana'
| 'cards'
| 'uload'
| 'chat'
| 'presi'
| 'food'
| 'quotes'
| 'picture'
| 'contacts'
| 'calendar'
| 'storage'
| 'clock'
| 'todo'
| 'mail'
| 'moodlit'
| 'inventory'
| 'questions'
| 'skilltree'
| 'plants'
| 'lightwrite'
| 'context'
| 'music'
| 'citycorners';
import type { AppId } from './config';
export type { AppId };
/**
* App branding configuration
* App branding configuration.
*
* `id` is `string` (not `AppId`) because `AppId` is derived from the keys
* of `APP_BRANDING`, and constraining `id` to `AppId` would create a
* circular type reference. Each entry's key in `APP_BRANDING` is the
* authoritative id; the `id` field is a redundant convenience.
*/
export interface AppBranding {
/** Unique app identifier */
id: AppId;
id: string;
/** Display name */
name: string;
/** Short description/tagline */