User feedback after the first batch shipped: the scene-picker got cluttered when every admin/settings subpage became its own card. Revise the plan to codify the sharper rule instead: - Cards are for daily workflows - Power-user domains get ONE card with internal tabs (initialTab prop for route deep-links) - Config/settings stay as routes opened from the parent module's ⚙ Document the tabbed-card pattern (lib/modules/admin/tabs/*Tab.svelte + ListView container with role guard + initialTab), rewrite the backlog around this principle, and fold batches 2/3/4 into a single consolidated history that makes the scope revision explicit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.6 KiB
Workbench cards over subroutes — migration plan
Started 2026-04-21. Scope revised 2026-04-22 after the first batch landed.
Why
User preference (2026-04-21):
"wir wollen möglichst alles als cards darstellen statt unterrouten"
The unified Mana app has a workbench that lets users compose scenes
from draggable cards. Every module already registers itself as a card
via registerApp() in apps/mana/apps/web/src/lib/app-registry/apps.ts
so the list/detail view shows up when the user drops the app into a
scene.
After the first pass (Batch 1 + 2, 9 new cards) the scene-picker got noticeably cluttered. User feedback 2026-04-22:
"finde das doch nicht so gut mit einzelnen Karten für alles — dadurch haben wir sehr viele pages in der workbench"
The revised principle is sharper than "everything as a card":
Cards are for daily workflows. Config and power-user panels are either a) fused into a single tabbed card per domain, or b) stay as routes opened from the parent module's ⚙ button.
Decision matrix
| Surface type | Example | Ship as |
|---|---|---|
| Operative daily view | spaces (member list), todo, calendar |
Single-view card |
| Multi-surface power-user domain | admin (overview / users / system / user-data) |
One card with internal tabs |
| Per-module configuration | broadcasts/settings, news/preferences |
Route, opened from module via ⚙ |
| Entity detail | notes/[id], admin/user-data/[userId] |
Route (URL = entity id) |
| Deeply nested flow | citycorners/cities/[slug]/… |
Route tree |
| Auth-critical | /login, /register |
Route with SSR redirect |
Patterns
Single-view card
apps/mana/apps/web/src/
├── lib/modules/{id}/ListView.svelte # self-contained pane
├── lib/app-registry/apps.ts # registerApp({ id, views: { list } })
└── routes/(app)/{path}/+page.svelte # thin wrapper: <ListView />
Use when the module has one primary surface users interact with
daily (e.g. spaces member list).
Tabbed card (fused domain)
lib/modules/admin/
├── ListView.svelte # tab container + role guard
└── tabs/
├── OverviewTab.svelte
├── UsersTab.svelte
├── SystemTab.svelte
└── UserDataTab.svelte
ListView takes an initialTab prop so route wrappers can deep-link
to a tab (/admin/users → <ListView initialTab="users" />). One role
guard at the container level, one card id in the registry.
Use when a domain has several sibling surfaces that belong together conceptually and would otherwise each need their own card.
Route-only (config / settings)
routes/(app)/{module}/settings/+page.svelte # self-contained page
Opened from the parent module's ⚙ button. No card, no registry entry.
Use for one-time configuration surfaces that the user touches once and forgets. Cards are for things you come back to.
Rules of thumb
- Pane chrome: card ListViews wrap their own content in a
.panecontainer withmax-width: 720px(or similar) + theme tokens (hsl(var(--color-foreground)),hsl(var(--color-card)),hsl(var(--color-border))). No<PageHeader>— the workbench provides scene chrome; the route wrapper works without one. - Role guards at the container level for tabbed domain cards — one gate-screen, not one per tab.
- No MANA_APPS entry for power-user cards (admin). MANA_APPS is
the app-drawer; admin panels shouldn't appear there. Only add a
MANA_APPS entry when the surface is user-facing (e.g.
spaces). - Route wrapper is short:
or, for tabbed-card deep-links:<script lang="ts"> import ListView from '$lib/modules/{id}/ListView.svelte'; </script> <ListView /><ListView initialTab="users" />
Shipped
Batch 1 — Spaces card (commit 88eca8a75, 2026-04-21)
| Card id | Route | Module |
|---|---|---|
spaces |
/spaces (canonical) + /spaces/members (legacy alias) |
lib/modules/spaces/ |
Also: APP_ICONS.spaces + MANA_APPS entry + SpaceSwitcher link
updated to /spaces. Daily-workflow surface (managing Space members)
— classic single-view card.
Batch 2 — Admin panels + module settings (commit 92fe23d46, 2026-04-21) — SUPERSEDED
Created 4 admin sub-cards + 4 settings cards. See superseding commits below.
Batch 3 — Admin cleanup (commit 5bf3ea8cb, 2026-04-21)
/admin/+layout.svelte shrunk to auth-guard only — nav tabs + overview
duplication removed since each /admin/* was a thin wrapper.
Batch 4 — Revise scope (2026-04-22)
Two superseding commits in response to scene-picker clutter:
commit 3e65637fc — revert settings cards to routes:
- Delete
lib/modules/{broadcast-settings, invoices-settings, uload-settings, news-preferences}/ - Restore each settings route's original content
- Remove 4 registerApp entries
commit 43b4570e6 — fuse admin cards into one tabbed card:
- New
lib/modules/admin/tabs/{Overview, Users, System, UserData}Tab.svelte admin/ListView.sveltebecomes a tab container with role guard +initialTabprop/admin/users,/admin/system,/admin/user-dataroutes pass the rightinitialTab- Delete
lib/modules/admin-{users, system, user-data}/ - Remove 3 registerApp entries
- Complexity stays its own card (iframe, pre-existed separately)
Net: the scene-picker gained 1 card (spaces) for this whole migration.
Backlog — revised
The original backlog proposed 35+ individual sub-page cards. Per the new decision matrix, most of those are better off as routes (rare config) or tabs within the module's existing card (archives / filtered lists) rather than as new cards.
To migrate into the module's existing card as tabs
Examples where a module has multiple daily-use surfaces that would benefit from fusion:
times:clients,projects,reports,templates,entries,clock— natural candidate, many sibling surfacesnews: list vs.savedvs.sources— could be tabsstorage:files,favorites,search,trash— tab viewquotes: feed vs.categoriesvs.favoritesvs.listscards: list vs.decksvs.explorevs.progressinventory:collections,categories,locations,searchmusic: library / playlists / projectscitycorners: map / favorites (the detail routes stay)skilltree: tree / achievements
To leave as routes (config or rare-use)
broadcasts/settings,invoices/settings,uload/settings,news/preferences,todo/settings(shipped as routes again)agents/templates,calendar/calendars,chat/templatesnews/add,plants/add,food/add,photos/uploadresearch-lab/keys,memoro/tags,plants/tags,uload/tagstimeline/analytics(probably a tab oftimelineeventually)gifts,gifts/redeem/[code](shareable URLs, keep as routes)organizations,teams(post-Spaces these routes are dead ends — either redirect to/spacesor delete)
Open questions
- Do we want a convention for per-module tab wiring? The admin card
established
initialTabprop +tabs/*Tab.sveltesubfolder. Formalising this in a helper or documenting it in the module pattern would make future tab fusions consistent. - Should the tabbed admin card also pick up
complexityas a 5th tab? It's the only admin-adjacent surface still living outside the fused card. Iframe height constraints argued against it for now.