Location:
docs/seller-dashboard/frontend-audit.md. Scope: the customer-facing product app atlogicspike/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
- 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.
- Three different route-page conventions (
page.tsx → *Wrapper.tsx,page.tsxdirectdynamic(), andpage.tsx → *PageClientserver wrapper). No single way to read or extend a route. - 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
ServiceGuardwrappers (e.g.,app/dashboard/chatbot/layout.tsx). - The shared
[id]/layout.tsx → shell → tabspattern 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. - 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.
- Editor/full-screen surfaces are identified ad-hoc per service (hardcoded route regexes in
mobile-bottom-nav,isEditorRoute) rather than declared once per service. - Duplicated design primitives — e.g. the dashboard home re-defines its own local
SectionHead/Statinstead of using@/components/ui/section-head(+stat-strip). - Loading-skeleton idioms vary (
variant="page"vs"row-list"vs"block"×2) across pages. - 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
navItemsof the active service (numbered01…, with aNº/MMpage-count). When no service is active it showsOVERVIEW_NAV(Dashboard, Integrations, Settings). - Docs link (external) + Footer: user avatar with workspace switcher, credits balance, settings, sign out.
- Header: workspace + service switcher (a drop-down listing Overview + every enabled
service from
- 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 anisEditorRouteregex 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 (hasregister.ts+store/, lazy reducer injection).Shell+tabs =[id]/layout.tsx→ shell → tab rail.Workspace = custom workspace shell (content-engine).Pages = plainpages/*.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.tsx → BlogOverviewWrapper |
BlogOverview.tsx |
✅ |
/blog/posts |
page.tsx → BlogListWrapper |
BlogList.tsx |
✅ |
/blog/new |
page.tsx → BlogEditorWrapper |
BlogEditor.tsx |
✅ |
/blog/[id] |
page.tsx → BlogEditorWrapper |
BlogEditor.tsx |
✅ |
/blog/categories |
page.tsx → CategoryListWrapper |
CategoryList.tsx |
✅ |
/blog/tags |
page.tsx → TagListWrapper |
TagList.tsx |
✅ |
/blog/settings |
page.tsx → BlogSettingsWrapper |
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.tsx → FormsListWrapper |
FormsList.tsx |
✅ |
/forms/inbox |
page.tsx → InboxWrapper |
InboxGlobal.tsx |
✅ |
/forms/list |
page.tsx (no wrapper) |
FormsList.tsx |
✅ |
/forms/new |
page.tsx → FormEditorWrapper |
FormEditor.tsx |
✅ |
/forms/overview |
page.tsx → FormsOverviewWrapper |
FormsOverview.tsx |
✅ |
/forms/[id] (layout) |
[id]/layout.tsx → FormShellWrapper |
shell + tabs home | ✅ via layout |
/forms/[id]/build |
page.tsx → TabWrapper |
tabs/BuildTab.tsx |
✅ via layout |
/forms/[id]/analytics |
page.tsx → FormAnalyticsWrapper |
FormAnalytics.tsx |
✅ |
/forms/[id]/settings |
page.tsx → TabWrapper |
tabs/SettingsTab.tsx |
✅ via layout |
/forms/[id]/share |
page.tsx → TabWrapper |
tabs/ShareTab.tsx |
✅ via layout |
/forms/[id]/submissions |
page.tsx → SubmissionsInboxWrapper |
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.tsx → CollectionShellWrapper → tabs/{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.tsx → FlowsOverviewWrapper |
FlowsOverview.tsx |
✅ |
/flows/list |
page.tsx → FlowsListWrapper |
FlowsList.tsx |
✅ |
/flows/new |
page.tsx → FlowBuilderWrapper |
FlowBuilder.tsx |
✅ |
/flows/[id] |
page.tsx |
FlowBuilder.tsx |
✅ (wrapper-less) |
/flows/[id]/runs |
page.tsx → RunHistoryWrapper |
RunHistory.tsx |
✅ |
2.7 AI Brain — /dashboard/brain (R, beta)
| Route | page.tsx → | module page | Guard |
|---|---|---|---|
/brain |
page.tsx → BrainWrapper |
OverviewPage.tsx |
✅ via layout |
/brain/copilot |
page.tsx → CopilotWrapper |
CopilotPage.tsx |
✅ via layout |
/brain/knowledge |
page.tsx → KnowledgeWrapper |
KnowledgePage.tsx |
✅ via layout |
/brain/settings |
page.tsx → SettingsWrapper |
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.tsx → MailboxPage (+ 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/(ortabs/), never scattered. - If a service needs an entity layout (form, collection, campaign), use
[id]/layout.tsx → <Entity>ShellWrapperwith 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 underpages/.
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) + bigh1+StatStrip - numbered
SectionHeadsections 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 existingregister*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'slayout.tsxeager import +useEffectreducer 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: truefor 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-enginenullframe 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/Statinapp/dashboard/page.tsxwithui/section-head.tsx+ui/stat-strip.tsx.- Standardise loading toPageSkeleton variant="page"on every route page. - Introduce one shared
TabRailcomponent used by forms tabs, collections tabs, newsletter settings tabs.
4.7 Fix the naming/codes
- Either change registry code
content→content-engine(and updatecontent-enginespecial-case in breadcrumbs to die), or keep the alias but centralise the mapping in one helper. Kill the special-case branch indashboard-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)
- [DONE] Close the ServiceGuard gap (C1/C2) — added layout-level
ServiceGuardto chatbot, brain, contacts, newsletter, and mailbox. - Reuse primitives (D4, D3) — delete local
SectionHead/Stat; normalise skeletons. - Add a shared
TabRailand retrofit newsletter settings + blog/forms/collections tabs (E2). - Extend the Forms/Collections shell pattern to Blog
[id], Flows[id], Newsletter campaign/template detail (E1). - 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.tsxinto the wrapper; standardise the loading skeleton toPageSkeleton variant="page"; verify per-service chunks in the build. - Unify route-page + wrapper conventions and add missing wrappers on sub-routes (B1–B3).
- Retire
workspace/-specific content-engine layout or formalize it underpages/+ shared shell (A2). - Centralise editor-route detection in the registry and remove
mobile-bottom-navregex (G1); sync mobile/desktop overview nav (G2). - Resolve
content/content-enginenaming (F1) and marketing↔dashboard naming (F2).
6. Verification method (how this was produced)
- Route/
layout.tsx/page.tsx/*Wrapper.tsxinventory: recursive walk ofapp/dashboard/*. - Module architecture: per-
modules/<svc>check forregister.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.