managarten/docs/plans/workbench-cards-migration.md
Till JS dc22240483 docs(plans): revise workbench-cards plan for cards-for-workflows rule
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>
2026-04-22 15:05:48 +02:00

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 .pane container with max-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:
    <script lang="ts">
      import ListView from '$lib/modules/{id}/ListView.svelte';
    </script>
    <ListView />
    
    or, for tabbed-card deep-links:
    <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.svelte becomes a tab container with role guard + initialTab prop
  • /admin/users, /admin/system, /admin/user-data routes pass the right initialTab
  • 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 surfaces
  • news: list vs. saved vs. sources — could be tabs
  • storage: files, favorites, search, trash — tab view
  • quotes: feed vs. categories vs. favorites vs. lists
  • cards: list vs. decks vs. explore vs. progress
  • inventory: collections, categories, locations, search
  • music: library / playlists / projects
  • citycorners: 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/templates
  • news/add, plants/add, food/add, photos/upload
  • research-lab/keys, memoro/tags, plants/tags, uload/tags
  • timeline/analytics (probably a tab of timeline eventually)
  • gifts, gifts/redeem/[code] (shareable URLs, keep as routes)
  • organizations, teams (post-Spaces these routes are dead ends — either redirect to /spaces or delete)

Open questions

  • Do we want a convention for per-module tab wiring? The admin card established initialTab prop + tabs/*Tab.svelte subfolder. 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 complexity as a 5th tab? It's the only admin-adjacent surface still living outside the fused card. Iframe height constraints argued against it for now.