mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
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:
parent
907a3add49
commit
6d193a9fa7
8 changed files with 205 additions and 37 deletions
|
|
@ -88,9 +88,45 @@ import {
|
|||
Megaphone,
|
||||
Receipt,
|
||||
ClockCounterClockwise,
|
||||
ClipboardText,
|
||||
} from '@mana/shared-icons';
|
||||
|
||||
// ── Apps with entity capabilities ───────────────────────────
|
||||
// ─── INDEX ──────────────────────────────────────────────────────
|
||||
//
|
||||
// Apps are grouped by their workbench role. Inside each group the
|
||||
// order is roughly chronological (by when they were added) — search
|
||||
// by id or use your editor outline to jump to a specific app.
|
||||
//
|
||||
// §1 Apps with entity capabilities (DnD sources/targets)
|
||||
// todo · calendar · contacts
|
||||
//
|
||||
// §2 Apps without entity capabilities — module surfaces
|
||||
// Daily-use: habits · notes · journal · myday · drink ·
|
||||
// mood · sleep · activity · times · finance
|
||||
// Knowledge: chat · kontext · cards · quiz · guides ·
|
||||
// news · news-research · research-lab · articles ·
|
||||
// library · writing · comic · presi
|
||||
// Body & life: body · food · meditate · stretch · period ·
|
||||
// dreams · firsts · lasts · habits · recipes
|
||||
// Places & ev.: places · citycorners · events · who
|
||||
// Creative: picture · music · photos · wardrobe · moodlit
|
||||
// Tools: memoro · uload · calc · plants · inventory ·
|
||||
// storage · skilltree · questions
|
||||
// Long-tail: quotes · automations · companion · wetter ·
|
||||
// goals · website · spaces · augur ·
|
||||
// broadcasts · invoices · timeline
|
||||
//
|
||||
// §3 AI Workbench surfaces
|
||||
// agents · ai-missions · ai-workbench · ai-policy ·
|
||||
// ai-insights · ai-health · rituals
|
||||
//
|
||||
// §4 System / Settings family
|
||||
// settings · themes · profile · credits · api-keys · admin ·
|
||||
// complexity · spiral · wishes · help · feedback · playground
|
||||
//
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
// ── §1 · Apps with entity capabilities ──────────────────────
|
||||
|
||||
registerApp({
|
||||
id: 'todo',
|
||||
|
|
@ -1566,3 +1602,15 @@ registerApp({
|
|||
list: { load: () => import('$lib/modules/timeline/ListView.svelte') },
|
||||
},
|
||||
});
|
||||
|
||||
registerApp({
|
||||
id: 'forms',
|
||||
name: 'Forms',
|
||||
color: '#14b8a6',
|
||||
icon: ClipboardText,
|
||||
views: {
|
||||
// /forms/[id]/edit (builder) and /forms/[id]/responses (analytics)
|
||||
// live as SvelteKit routes; the workbench card hosts the ListView.
|
||||
list: { load: () => import('$lib/modules/forms/ListView.svelte') },
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { MANA_APPS } from '@mana/shared-branding';
|
||||
// Side-effect import: registers every workbench app via ./apps.
|
||||
|
|
@ -32,9 +35,7 @@ const WORKBENCH_ONLY = new Set([
|
|||
'spiral',
|
||||
// User-facing modules that don't yet have MANA_APPS branding.
|
||||
// Add them to MANA_APPS when they ship a marketing surface.
|
||||
'kontext',
|
||||
'rituals',
|
||||
'wishes',
|
||||
// AI Studio surfaces. Open question whether to expose each one in
|
||||
// MANA_APPS or consolidate into a single "AI Studio" branding entry.
|
||||
// Keeping them workbench-only until that decision lands.
|
||||
|
|
@ -97,4 +98,59 @@ describe('app registry ↔ MANA_APPS consistency', () => {
|
|||
const allIds = [...getAllApps().map((a) => a.id), ...MANA_APPS.map((a) => a.id)];
|
||||
expect(allIds).not.toContain('inventar');
|
||||
});
|
||||
|
||||
/**
|
||||
* Catches the kind of drift that produced the `appId="broadcasts"` vs
|
||||
* registered id `'broadcast'` bug — RoutePage looks up the workbench
|
||||
* app-registry by appId for title/icon/color, so a mismatch silently
|
||||
* falls back to the raw id string and breaks navigation styling.
|
||||
*/
|
||||
it('every <RoutePage appId="…"> in routes/(app) resolves to a registered workbench app', () => {
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROUTES_DIR = join(__dirname, '../../routes/(app)');
|
||||
const workbenchIds = new Set(getAllApps().map((a) => a.id));
|
||||
|
||||
// Routes that surface a feature without a workbench module — they
|
||||
// pass an `appId` to RoutePage purely for telemetry/back-button
|
||||
// scoping, not for icon/color lookup. Adding here = "this route
|
||||
// is intentionally not backed by a workbench app".
|
||||
const ROUTE_ONLY_APP_IDS = new Set([
|
||||
'gifts', // /gifts — credit gifting flow, backed by mana-credits API
|
||||
'llm-test', // DEV-only LLM playground
|
||||
'milestones', // cross-module aggregator over firsts + lasts
|
||||
'organizations', // Spaces/membership management
|
||||
'teams', // Spaces/teams management
|
||||
'tags', // global tag browser
|
||||
]);
|
||||
|
||||
function* findPageFiles(dir: string): Generator<string> {
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const full = join(dir, entry);
|
||||
if (statSync(full).isDirectory()) {
|
||||
yield* findPageFiles(full);
|
||||
} else if (entry === '+page.svelte') {
|
||||
yield full;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const violations: { file: string; appId: string }[] = [];
|
||||
for (const file of findPageFiles(ROUTES_DIR)) {
|
||||
const body = readFileSync(file, 'utf8');
|
||||
const matches = body.matchAll(/<RoutePage[^>]+appId="([a-z0-9-]+)"/g);
|
||||
for (const m of matches) {
|
||||
const appId = m[1];
|
||||
if (!workbenchIds.has(appId) && !ROUTE_ONLY_APP_IDS.has(appId)) {
|
||||
violations.push({ file: file.replace(ROUTES_DIR, ''), appId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(
|
||||
violations,
|
||||
`Routes referencing unregistered appIds:\n${violations
|
||||
.map((v) => ` ${v.file} → "${v.appId}"`)
|
||||
.join('\n')}`
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,6 @@
|
|||
import { RoutePage } from '$lib/components/shell';
|
||||
</script>
|
||||
|
||||
<RoutePage appId="ai-agents">
|
||||
<RoutePage appId="agents">
|
||||
<ListView />
|
||||
</RoutePage>
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@
|
|||
</button>
|
||||
{/snippet}
|
||||
|
||||
<RoutePage appId="ai-agents" backHref="/agents">
|
||||
<RoutePage appId="agents" backHref="/agents">
|
||||
<div class="page">
|
||||
<header class="header">
|
||||
<button type="button" class="back" onclick={() => goto('/')}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue