logicspike/docs

Seller Dashboard

Seller Dashboard — Frontend Audit & Ideal Structure

Location: docs/seller-dashboard/frontend-audit.md. Scope: the customer-facing product app at logicspike/apps/seller-dashboard (Next.js App Router). This document maps every service, its pages, the navbar/sidebar and what content each page displays, then captures the inconsistencies / irregularities / bugs / gaps found, and ends with the ideal page–nav–content structure each service should converge onto.

Snapshot of the current working tree. This is an audit, not a code change.


0. TL;DR — the problems

  1. Four different module architectures used at once (Redux-registered modules, plain pages, shell+tabs, and a custom workspace shell) — one service shouldn't need to learn how another works.
  2. Three different route-page conventions (page.tsx → *Wrapper.tsx, page.tsx direct dynamic(), and page.tsx → *PageClient server wrapper). No single way to read or extend a route.
  3. ServiceGuard entitlement coverage was uneven — previously chatbot, brain, contacts, newsletter, and mailbox routes were un-gated. This has been resolved by wrapping these services in service layout-level ServiceGuard wrappers (e.g., app/dashboard/chatbot/layout.tsx).
  4. The shared [id]/layout.tsx → shell → tabs pattern exists only for Forms and Collections. Every other service with record-detail routes (Blog [id], Flows [id], Newsletter campaigns/templates) does ad-hoc per-page headers and sibling links.
  5. Header/section composition differs heavily. Chatbot + Contacts pages are the reference (hero + eyebrow + LiveDot + StatStrip + SectionHead + footer); Brain, Collections editors, Media, and parts of Newsletter are near-bare.
  6. Editor/full-screen surfaces are identified ad-hoc per service (hardcoded route regexes in mobile-bottom-nav, isEditorRoute) rather than declared once per service.
  7. Duplicated design primitives — e.g. the dashboard home re-defines its own local SectionHead/Stat instead of using @/components/ui/section-head (+ stat-strip).
  8. Loading-skeleton idioms vary (variant="page" vs "row-list" vs "block"×2) across pages.
  9. Lazy-loading (bundle splitting) is not consistent. Most services already split per-page via next/dynamic, but content-engine, media, and comms load eagerly, and content-engine injects its Redux reducer from a layout side-effect instead of on-demand — so with many services the initial load and unused-service downloads grow unnecessarily.

1. The shell / chrome (all services share this)

Every /dashboard/* page is rendered inside src/app/dashboard/layout.tsx, which provides:

  • Sidebar (components/app-sidebar.tsx) — collapsible, with:
    • Header: workspace + service switcher (a drop-down listing Overview + every enabled service from session.services × SERVICE_REGISTRY, filtered by permissions).
    • Content: context-sensitive nav — the navItems of the active service (numbered 01…, with a Nº/MM page-count). When no service is active it shows OVERVIEW_NAV (Dashboard, Integrations, Settings).
    • Docs link (external) + Footer: user avatar with workspace switcher, credits balance, settings, sign out.
  • Top bar (components/dashboard-header.tsx) — sidebar trigger, breadcrumbs, credits pill, feedback button, theme toggle.
  • Route progress bar, skip-link, SessionGuard, ServicePermissionGuard, MobileBottomNav.
  • Mobile bottom nav (components/mobile-bottom-nav.tsx) — derives from the same registry; shows up to 5 tabs + "More". It hardcodes an isEditorRoute regex for the newsletter composer to hide itself on full-screen editors.

Breadcrumb logic (dashboard-header.tsx) has a dedicated workaround for the content-engine ↔ code content mismatch — evidence the code/route naming is inconsistent.

Service registry = the single source of truth

src/lib/service-registry.ts defines SERVICE_REGISTRY (label, icon, gradient, basePath, navItems, status) for every service code. The sidebar, dashboard home, and mobile nav all read from it — so nav is consistent by construction. The gaps are not in the nav data but in route pages not matching the registry, and in per-service page internals.


2. Services, pages, and the content each displays

Legend — architecture:

  • Registered = redux module (has register.ts + store/, lazy reducer injection).
  • Shell+tabs = [id]/layout.tsx → shell → tab rail.
  • Workspace = custom workspace shell (content-engine).
  • Pages = plain pages/*.tsx, no register/store.
  • Client/mixed.
Service (code) status basePath Nav items (registry) Architecture
Blog (blog) stable /dashboard/blog Overview, Posts, Categories, Tags, Settings R
AI Chatbot (chatbot) stable /dashboard/chatbot Bots, Inbox, Sandbox, Analytics, Tools, Channels, Settings P
Media (media) stable /dashboard/media Overview, Library P (no store)
Forms (forms) beta /dashboard/forms Inbox, Forms, Overview R + S
Collections (collections) beta /dashboard/collections Collections R + S
Flows (flows) beta /dashboard/flows Overview, Flows R
AI Brain (brain) beta /dashboard/brain Overview, Copilot, Knowledge, Settings R
Content Engine (content) beta /dashboard/content-engine Calendar, Queue, Analytics, Activity, Automations, Connections W (store, no register)
Contacts (contacts) beta /dashboard/contacts Contacts, Analytics, Outreach, Playground R
Newsletter (newsletter) beta /dashboard/newsletter Overview, Campaigns, Subscribers, Signup forms, Segments, Templates, Activity, Deliverability, Settings store, no register
Mailbox (mailbox) beta /dashboard/mailbox Inbox, Domains, Mailboxes, Setup P
Store (store) coming_soon /dashboard/store (planned) n/a
Comms (comms) retired /dashboard/comms → redirects P (client only)

2.1 Blog — /dashboard/blog (R, stable ✅ reference for a registered module)

Route page.tsx → module page Guard
/blog page.tsxBlogOverviewWrapper BlogOverview.tsx
/blog/posts page.tsxBlogListWrapper BlogList.tsx
/blog/new page.tsxBlogEditorWrapper BlogEditor.tsx
/blog/[id] page.tsxBlogEditorWrapper BlogEditor.tsx
/blog/categories page.tsxCategoryListWrapper CategoryList.tsx
/blog/tags page.tsxTagListWrapper TagList.tsx
/blog/settings page.tsxBlogSettingsWrapper BlogSettings.tsx

Content displayed: Overview (stats/counts, recent posts), Posts list (table + filters), editor (posts/new/share), categories/tags (manage lists), settings.

2.2 AI Chatbot — /dashboard/chatbot (P, stable)

Route page.tsx → module page Guard note
/chatbot (Bots) dynamic(→ import BotsPage) BotsPage.tsx ✅ via layout reference page shape
/chatbot/inbox dynamic(→ InboxPage) InboxPage.tsx ✅ via layout no SectionHead/footer
/chatbot/sandbox dynamic(→ SandboxPage) SandboxPage.tsx ✅ via layout barely any chrome
/chatbot/analytics dynamic(→ AnalyticsPage) AnalyticsPage.tsx ✅ via layout full markers
/chatbot/tools dynamic(→ ToolsPage) ToolsPage.tsx ✅ via layout full markers
/chatbot/channels dynamic(→ ChannelsPage) ChannelsPage.tsx ✅ via layout full markers
/chatbot/settings dynamic(→ SettingsPage) SettingsPage.tsx ✅ via layout loading skeleton differs (block×2)

2.3 Media — /dashboard/media (P, stable)

Route page.tsx → module page Guard
/media page.tsx MediaOverview.tsx
/media/files page.tsx MediaFiles.tsx

No Wrapper files, no register; module pages are client components. Both pages only carry an eyebrow — no SectionHead/hero/Stat.

2.4 Forms — /dashboard/forms (R + S, beta ✅ best-in-repo pattern)

Route page.tsx → module page Guard
/forms page.tsxFormsListWrapper FormsList.tsx
/forms/inbox page.tsxInboxWrapper InboxGlobal.tsx
/forms/list page.tsx (no wrapper) FormsList.tsx
/forms/new page.tsxFormEditorWrapper FormEditor.tsx
/forms/overview page.tsxFormsOverviewWrapper FormsOverview.tsx
/forms/[id] (layout) [id]/layout.tsxFormShellWrapper shell + tabs home ✅ via layout
/forms/[id]/build page.tsxTabWrapper tabs/BuildTab.tsx ✅ via layout
/forms/[id]/analytics page.tsxFormAnalyticsWrapper FormAnalytics.tsx
/forms/[id]/settings page.tsxTabWrapper tabs/SettingsTab.tsx ✅ via layout
/forms/[id]/share page.tsxTabWrapper tabs/ShareTab.tsx ✅ via layout
/forms/[id]/submissions page.tsxSubmissionsInboxWrapper SubmissionsInbox.tsx

Why this is the reference: a single [id]/layout.tsx provides one ServiceGuard, one shell (FormShellWrapper), one fetch + one header + one tab rail — child tab pages stay stupid. The layout comment explicitly documents that this replaces an earlier "each screen re-fetches the form, renders its own header, links ad-hoc to siblings" mess.

2.5 Collections — /dashboard/collections (R + S, beta)

Same shell+tabs pattern as Forms: [id]/layout.tsxCollectionShellWrappertabs/{Fields,Embed}.

Route module page Guard
/collections CollectionsList.tsx
/collections/new CollectionEditor.tsx
/collections/[id] (layout) entries home ✅ via layout
/collections/[id]/fields tabs/FieldsTab ✅ via layout
/collections/[id]/embed tabs/EmbedTab ✅ via layout
entries (table/detail) EntriesTable.tsx, EntryEditor.tsx n/a (weakest chrome)

2.6 Flows — /dashboard/flows (R, beta)

Route page.tsx → module page Guard
/flows page.tsxFlowsOverviewWrapper FlowsOverview.tsx
/flows/list page.tsxFlowsListWrapper FlowsList.tsx
/flows/new page.tsxFlowBuilderWrapper FlowBuilder.tsx
/flows/[id] page.tsx FlowBuilder.tsx ✅ (wrapper-less)
/flows/[id]/runs page.tsxRunHistoryWrapper RunHistory.tsx

2.7 AI Brain — /dashboard/brain (R, beta)

Route page.tsx → module page Guard
/brain page.tsxBrainWrapper OverviewPage.tsx ✅ via layout
/brain/copilot page.tsxCopilotWrapper CopilotPage.tsx ✅ via layout
/brain/knowledge page.tsxKnowledgeWrapper KnowledgePage.tsx ✅ via layout
/brain/settings page.tsxSettingsWrapper SettingsPage.tsx ✅ via layout

The wrappers call registerBrainModule() and are protected under /brain/layout.tsx which wraps the subtree in ServiceGuard. Several module pages (BrainOverview, HistoryPage, InsightsPage, PersonalityPage, UsagePage, KnowledgePage) have almost no header/section chrome.

2.8 Content Engine — /dashboard/content-engine (store, no register, beta)

Route page.tsx → Guard
/content-engine ContentEngineOverview
/content-engine/calendar CalendarPageClient
/content-engine/queue
/content-engine/analytics
/content-engine/activity
/content-engine/automations
/content-engine/connections
/content-engine/composer (+ [slotId]) ComposerPage
/content-engine/campaigns /recurring /review
/content-engine/settings SettingsLegacyPage

Presentation is not gathered under pages/ — it lives in workspace/ (WorkspaceShell, CommandPalette, Inspector, ViewHeader) + components/ + composer/. register.ts absent (no lazy reducer). This is the least conventional module to read.

2.9 Contacts — /dashboard/contacts (R, beta)

ContactsListPage, AnalyticsPage, OutreachPage are full-marker reference pages; PlaygroundPage and ContactDetailPage route → wrapper; all are guarded at the layout layer (/contacts/layout.tsx) with ServiceGuard.

2.10 Newsletter — /dashboard/newsletter (store, no register, beta)

page.tsx does direct dynamic()pages/OverviewPage. The entire service is now guarded at the layout layer (/newsletter/layout.tsx) with ServiceGuard. Many tabbed sub-pages (Campaigns new/[id]/edit, Templates new/[id]) are full-screen composers; settings is a 7-tab page (Identity, Brand, Schedule, Consent, Suppression, Footer, Automation). Editors hide the mobile nav via a hardcoded regex. Chrome quality ranges from full-marker (LogsPage, StatsPage) to near-bare (OverviewPage, SubscribersPage).

2.11 Mailbox — /dashboard/mailbox (P, beta)

page.tsxMailboxPage (+ Domains, Manage, Setup). The service is now guarded at the layout layer (/mailbox/layout.tsx) with ServiceGuard (still no register/store). Full inbox UI lives in modules/mailbox/inbox/*. Chrome: Setup is best; MailboxPage has no hero.

2.12 Comms — retired (redirect)

/dashboard/comms/* redirects; only modules/comms/pages/settings survives. Keep as-is.


3. Inconsistencies, irregularities, bugs, gaps

A. Architecture (biggest leverage)

# Issue Where Impact
A1 4 module architectures: registered-reducer (blog/brain/forms/collections/flows/contacts), store-no-register (newsletter/content), plain pages (chatbot/mailbox/media), shell+tabs (forms/collections) src/modules/* Contributor must learn N systems; cross-service patterns (guards, headers, save bars) don't compose
A2 Content Engine has no register.ts and no single pages/ inventory (uses workspace/, components/, composer/) modules/content-engine Unclear ownership; can't map routes quickly
A3 chatbot, mailbox have no register.ts/index.ts/store unlike sibling services modules/{chatbot,mailbox} Inconsistent as state/API patterns shift

B. Route-page conventions

| # | Issue | Where | Impact | | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------- | -------------------------------------- | ---------------- | ----------------------------------------------- | | B1 | 3 conflicting page conventions: page.tsx → *Wrapper.tsx (registered svcs); page.tsx direct dynamic() (chatbot/newsletter/media); page.tsx → *PageClient server wrapper (content-engine calendar) | app/dashboard/*/... | No canonical reading/extending path | | B2 | Wrapper files missing on sub-routes even inside registered services: e.g. forms/list/page.tsx, forms/overview/page.tsx, forms/[id]/build | settings | share, flows/[id]/page.tsx | those route dirs | Guard/register conventions silently not applied | | B3 | Wrapper naming is free-form: BlogListWrapper, BrainWrapper, InboxWrapper, TabWrapper reused by all tabs, FormShellWrapper | various | Hard to grep which module a route uses |

C. ServiceGuard entitlement coverage (functional risk)

# Issue Where Impact
C1 [RESOLVED] Chatbot, Brain, Contacts, Newsletter, Mailbox routes previously rendered without ServiceGuard those route pages Resolved: Added layout-level ServiceGuard to wrap the respective service routes
C2 Guard lives at different layers per service: route page.tsx (blog/media/flow) vs layout layout.tsx (chatbot/brain/contacts/newsletter/mailbox) vs [id]/layout.tsx (forms/collections) across services Entitlement enforcement not uniform

D. Header / section composition

# Issue Where Impact
D1 Reference shape (hero header + eyebrow + LiveDot + StatStrip + SectionHead + footer) is fully present only on chatbot/contacts pages BotsPage, ContactsListPage, etc. Perceived polish differs service-to-service
D2 Near-bare pages: most brain, media, collections EntryEditor/EntriesTable, newsletter Overview/Subscribers, sandbox/playground module pages Feels unfinished; no consistent "where am I"
D3 Loading-skeleton idiom differs (variant="page" vs "row-list" vs block×2) e.g. chatbot/settings vs blog Micro-inconsistency on every load
D4 Dashboard home re-implements its own SectionHead/Stat, duplicating ui/section-head.tsx + ui/stat-strip.tsx app/dashboard/page.tsx Two of the same primitive drift apart

E. Detail-page shell/tab pattern (only partially adopted)

# Issue Where Impact
E1 The good [id]/layout.tsx → shell → tabs exists only for Forms & Collections. Blog [id], Flows [id], Newsletter campaign/template [id] still do ad-hoc per-page headers + sibling links those modules Recreation of the exact bug FormLayout was built to fix
E2 Tab rails differ: forms tabs/BuildTab, collections tabs/FieldsTab, newsletter settings inline 7 tabs, no shared TabRail primitive forms/collections/newsletter Tab UX is not consistent

F. Naming / code↔route mismatch

# Issue Where Impact
F1 Service code content but route content-engine; breadcrumbs need a special-case branch service-registry.ts + dashboard-header.tsx Dedicated workaround code; easy to break
F2 Marketing vs dashboard naming: leads (marketing) vs contacts (dashboard); brain/forms/collections/flows/mailbox have no marketing page website/lib/products-data.ts vs registry Users can't find docs/landing for a service
F3 newsletter settings has 7 tabs but the registry lists it as a single nav item; content-engine lists only 6 nav items while 11 routes exist registry navItems Sidebar/pages can drift from reality

G. Mobile / full-screen handling is ad-hoc

# Issue Where Impact
G1 isEditorRoute hardcoded regex for newsletter composer only; other full-screen editors (FormEditor, FlowBuilder, BlogEditor, EntryEditor) not declared mobile-bottom-nav.tsx Non-newsletter editors may double-up bars or get wrong margins
G2 Mobile OVERVIEW_NAV lacks Integrations that desktop OVERVIEW_NAV has mobile-bottom-nav vs app-sidebar Desktop/mobile overview pages diverge

H. Other

# Issue Where
H1 comments/comms legacy: redirects + leftover modules/comms/pages/settings comms
H2 Several register*Module() files import themselves (self-register at module boundary) added a redundant call site in modules/<svc>/register.ts register files
H3 Blog [id] new share an editor but have separate Wrappers/pages — fine, but duplicated; requires the BlogEditorWrapper be copied in 2 places blog

I. Per-service lazy loading (bundle splitting) — not consistent

This is the key pattern for a product that will grow to many services: each service's JS should load in the browser only when the user first opens it (route-level code-splitting), so the initial dashboard load stays small and a service you never open is never downloaded.

# Issue Where Impact
I1 Lazy by design (✅): registered services split via *Wrapper.tsx + next/dynamic() and chatbot/newsletter/mailbox split at the route page.tsx — their module bundles load on first visit blog, brain, forms, collections, flows, contacts, chatbot, newsletter, mailbox The intended behaviour already works for most services
I2 NOT lazy — eager static imports (❌): content-engine routes (page.tsx) import their *PageClient pages statically; its layout.tsx eagerly imports the whole module (@/modules/content-engine) and injects its reducer in a useEffect content-engine/*, content-engine/layout.tsx Whole content-engine bundle is in the route graph as soon as any CE page is reachable
I3 Reducer injection differs: registered modules inject lazily via register*Module() inside the same dynamic import; content-engine injects via layout.tsx useEffect; newsletter has a store but no register layout.tsx/register.ts patterns Redux wiring is not "one way"; a service can be hard to make lazy
I4 media/comms are missing the registered pattern entirely (plain pages, no register) — fine for a lightweight service, but there is no single stated policy for "lightweight vs heavy" services modules/{media,comms} No rule telling a future service author when lazy is required
I5 Loading skeleton while a lazy chunk arrives varies per page (variant="page" vs "row-list" vs block×2) and content-engine renders null for a frame in its layout across route pages Inconsistent first-paint feedback

Current lazy-loading status per service (verified from route files):

Service Route split (bundle loads on visit) Reducer lazy
blog ✅ via Blog*Wrapper ✅ register
brain ✅ via *Wrapper ✅ register
forms ✅ via *Wrapper ✅ register
collections ✅ via *Wrapper ✅ register
flows ✅ via *Wrapper ✅ register
contacts ✅ via *Wrapper ✅ register
chatbot dynamic() at route n/a
newsletter dynamic() at route ❌ store, no register
mailbox dynamic() at route n/a
media ❌ static import in page.tsx n/a
content-engine eager static imports everywhere ⚠️ layout useEffect
comms ❌ static import (retired, redirect) n/a

4. Ideal structure — where every service should converge

Goal: the reader of any service page can predict its file layout, its guard, its header, and how to add a page — because every service follows the same skeleton. The Forms/Collections shell+tabs pattern plus the Chatbot/Contacts page shape are the two halves of the target.

4.1 One canonical module layout (for every future service)

src/modules/<service>/
  register.ts            // LOADABLE lazy reducer (reducerManager.add) — optional if stateless
  index.ts               // re-exports register()
  api/                   // server client fns (proxy-aware)
  pages/                 // ONE place for every page/route component
    <AreaPage>.tsx
  shell/                 // optional shared chrome for an entity (e.g. form), with header + tab rail
  tabs/                  // tab-page components (when entity has tabs)
  components/            // page-local building blocks
  hooks/, lib/, store/

Rules:

  • Every displayed route is authored in pages/ (or tabs/), never scattered.
  • If a service needs an entity layout (form, collection, campaign), use [id]/layout.tsx → <Entity>ShellWrapper with a single guard + fetch + header + shared TabRail.
  • No workspace/-style bespoke shell unless it’s a genuinely new interaction type, and even then put its page components under pages/.

4.2 One canonical route-page convention

Pick one and apply everywhere. Recommended (matches Forms/Collections and is SSR-friendly):

app/dashboard/<svc>/.../<AreaWrapper>.tsx  (client)  // dynamic() → register → import page
app/dashboard/<svc>/.../page.tsx           (server)   // headers(); <ServiceGuard service><AreaWrapper/></ServiceGuard>

or equivalently route-level ServiceGuard at a layout when there is an [id] group. Do not mix direct-dynamic() and server-PageClient and wrapper styles within the same app.

4.3 One canonical page canvas

Every non-editor page renders through a shared ServicePage canvas that supplies:

  • hero (eyebrow Service · Area + LiveDot) + big h1 + StatStrip
  • numbered SectionHead sections
  • EmptyState (no-data), PageSkeleton (loading), consistent list/table borders
  • footer (vlozi · <svc> · <area> + count)

Editors (FormEditor, FlowBuilder, BlogEditor, EntryEditor, composers) use a documented second template: full-screen, but still eyebrow + SectionHead chrome + save bar (FormShell/FormSaveBar is the model) + footer. Declare editor routes once per service in the registry (e.g. entry/editor: true) so mobile-bottom-nav derives isEditorRoute from that, not a regex.

4.4 One entitlement path

Require every service to be wrapped by ServiceGuard at the coarsest safe layer:

  • detail groups → the group layout.tsx (as Forms/Collections do already);
  • flat pages or service directories → the top-level service layout.tsx (as chatbot, brain, contacts, newsletter, mailbox now do).

4.5 Lazy-loading policy (one service = one on-demand bundle)

Every service must load in the browser only when the user opens it — this is what makes a many-service product scale. Concretely:

  • Every route component is code-split. Standardise on the wrapper idiom:

    app/dashboard/<svc>/.../<AreaWrapper>.tsx (client)
        const X = dynamic(() => import("@/modules/<svc>/pages/<AreaPage>"), {
            ssr: false, loading: () => <PageSkeleton variant="page" /> })
    app/dashboard/<svc>/.../page.tsx (server)
        <ServiceGuard service="<svc>"><AreaWrapper /></ServiceGuard>
  • Reducers are injected lazily inside the same dynamic import via register<Service>Module() (the existing register*Module() pattern) — never in a layout side-effect. This means the reducer + page + api all arrive together and only when visited.

  • No eager @/modules/<svc> imports in a layout that the rest of the dashboard pulls in. Content-engine's layout.tsx eager import + useEffect reducer injection must be converted to the wrapper pattern (rule I2): the layout should only wrap children; the reducer is injected when a CE route loads.

  • Declare a service's expected weight. Give each registry entry a flag (e.g. lazy: true for heavy services; lightweight ones may skip Redux). A future author then knows the bar to clear.

  • One loading skeleton everywhere: PageSkeleton variant="page" for first paint of a lazy chunk (rule I5) — replace the "row-list"/block×2 variants and the content-engine null frame with it.

  • Verify with the build: each service should emit its own chunk (webpack/Turbopack code-split), so opening the dashboard never downloads another service's bundle.

4.6 Reuse the primitives (kill duplication)

  • Replace home-grown SectionHead/Stat in app/dashboard/page.tsx with ui/section-head.tsx + ui/stat-strip.tsx.- Standardise loading to PageSkeleton variant="page" on every route page.
  • Introduce one shared TabRail component used by forms tabs, collections tabs, newsletter settings tabs.

4.7 Fix the naming/codes

  • Either change registry code contentcontent-engine (and update content-engine special-case in breadcrumbs to die), or keep the alias but centralise the mapping in one helper. Kill the special-case branch in dashboard-header.tsx.
  • Align leads (marketing) ↔ contacts (dashboard) labels, and add marketing pages for the beta services, so "service" means the same thing everywhere.

5. Suggested migration order (lowest risk first)

  1. [DONE] Close the ServiceGuard gap (C1/C2) — added layout-level ServiceGuard to chatbot, brain, contacts, newsletter, and mailbox.
  2. Reuse primitives (D4, D3) — delete local SectionHead/Stat; normalise skeletons.
  3. Add a shared TabRail and retrofit newsletter settings + blog/forms/collections tabs (E2).
  4. Extend the Forms/Collections shell pattern to Blog [id], Flows [id], Newsletter campaign/template detail (E1).
  5. Make every service lazy-load (I1–I5) — bring content-engine, media, and comms onto the wrapper + dynamic-import pattern; move content-engine's reducer injection out of layout.tsx into the wrapper; standardise the loading skeleton to PageSkeleton variant="page"; verify per-service chunks in the build.
  6. Unify route-page + wrapper conventions and add missing wrappers on sub-routes (B1–B3).
  7. Retire workspace/-specific content-engine layout or formalize it under pages/ + shared shell (A2).
  8. Centralise editor-route detection in the registry and remove mobile-bottom-nav regex (G1); sync mobile/desktop overview nav (G2).
  9. Resolve content/content-engine naming (F1) and marketing↔dashboard naming (F2).

6. Verification method (how this was produced)

  • Route/layout.tsx/page.tsx/*Wrapper.tsx inventory: recursive walk of app/dashboard/*.
  • Module architecture: per-modules/<svc> check for register.ts, index.ts, store/, shell/, tabs/, api/.
  • Guard usage: grep each route page.tsx/wrapper for <ServiceGuard service=….
  • Chrome markers per module page: presence of SectionHead/EmptyState/PageSkeleton/StatStrip/ <header/LiveDot/<footer/tracking-[0.22em].
  • Nav data: lib/service-registry.ts, lib/service-permissions.ts; chrome: components/app-sidebar.tsx, dashboard-header.tsx, mobile-bottom-nav.tsx, app/dashboard/layout.tsx, app/dashboard/page.tsx.
Seller Dashboard