App shell & sidebar
The application shell is the single layout that wraps every authenticated
route. It is mounted once in src/routes/(app)/+layout.svelte, which fetches
the user profile, restores the last visited route, and renders <AppShell>.
Structure
AppShell.svelte is responsible for bootstrapping the runtime: probing
backend health, loading shell navigation, starting services polling, and
registering global error handlers (unhandledrejection, window error).
When the backend is offline or the DB/IDP is degraded, health is re-probed
every 5 seconds; on recovery the modules list is reloaded.
Topbar
AppTopbar.svelte is a 3-column grid:
- Left: sidebar trigger (collapse/expand)
- Center:
CommandPalette - Right: detected IANA timezone,
LangSelect, errors button (badge with impact-colored count, opens the errors sheet), notifications,ThemeToggle
Module navigation
Module loading and selection is centralized in
src/lib/shell/modules-shell.svelte.ts (exported as shellNav).
| Function | Purpose |
|---|---|
loadShellNav() | Fetches the access-filtered GET /api/v1/modules payload — every record embeds nav, routes and per-root home |
selectModule(id) | Selects a module — navigation comes from the already-loaded ModuleInfo, no second request |
reloadModuleNav() | Rebuilds the current module's navigation from the loaded payload |
syncModuleFromRoute(pathname) | Auto-selects the module that owns the current route via route_prefixes |
getLastRoute() / saveLastRoute(pathname) | localStorage persistence of the last visited route |
When the user navigates to a route, syncModuleFromRoute matches the
pathname against each module's route_prefixes and switches module
automatically, loading that module's navigation structure.
SidebarAppLauncher renders the Finder-style launcher that replaces the
old root/module switchers: one trigger ({app} / {module}) opens a
three-column panel Applications | Modules | Pages.
Application launcher (Finder cascade)
The launcher is always visible, even when a single app level exists.
Modules carry a part_of field (/api/v1/modules) — the root level
they are part of — distinct from the module code:
part_of: "system"→ module lives under/system/<code>(settings, job, emailsender, webhook, ai…)part_of === code→ the module IS its own root level and the namespace collapses to/<code>(crm →/crm)
A module may also declare extra root memberships — route_prefixes
entries of the form /{other-root}/{code} (e.g. crm owns /crm/* plus
/system/crm/pipeline). Every prefix's first segment becomes an app
level the module lists under; part_of alone decides where is_default
applies.
The payload is already access-filtered server-side — a user only ever
sees the modules/pages their access.{app}.{module}.{page} grants cover
(see the BE dev-guide for the grammar). The FE never re-filters.
Columns, left to right — click/tap descends into the next column (Finder-style), hover only highlights, and every row carries an icon-only goto CTA (Enter/Space) that navigates without descending:
- Applications — every NAVIGABLE root level: a root appears only
when at least one module has
home[root](≥1 reachable page under it post access-filter). Click = descend; goto CTA = the app's declaredis_defaultmodule's first page under that app (home[root]). - Modules — modules NAVIGABLE under the selected app (
home[root]present — declared membership without a reachable page is routing-domain only and never lists). Click = descend; goto CTA =home[root]. Labels resolve from the embeddednav/label_keyin the same payload — no per-module meta fetch. - Pages — the selected module's nav links scoped to the selected app (a multi-root module shows only that app's slice). Click = direct navigation.
The current route marks its ancestors in all three columns; the shown
"current app" is the ROUTE's root (shellNav.rootForRoute), so a crm
page at /system/crm/pipeline marks System, not CRM.
Module labels come from ONE source: service_registry.label_key
(ModuleInfo.label_key), resolved by resolveModuleLabel(module, t)
against the module's own dictionary with the raw name as the only
fallback. The launcher trigger, the Modules column, moduleBreadcrumbSegments
(breadcrumb) and the service-admin pages all share it — "Impostazioni"
everywhere, never a registry-name drift. Row goto CTAs and the keyboard
footer use app.nav.goTo ("Vai a"/"Go to"); app.nav.open stays on the
sidebar open-trigger where "open navigation" is the literal action.
The root segment is also the first breadcrumb segment (auto-prepended by
AppPageBreadcrumb), giving {app} / {module} / {page}. The sidebar nav
links are likewise root-scoped — under /system/crm/* only the crm
pages declared below /system/crm render.
Organization switcher
SidebarOrgSwitcher fetches active organizations from
/api/v1/system/organizations/active and lets the user pick one. The
selected org is tracked in local state.
Code
Profile menu
SidebarProfileMenu shows the user avatar (with avatar_color /
avatar_initials from the profile), display name, and email. The dropdown
contains a Settings action (switches to the settings module and
navigates to /system/settings/profile) and a Sign out action that
calls the onLogout callback (clears cookies, redirects to /login).
Health badge
SidebarHealthBadge uses the useHealthChip() composable and reads
backendState.healthChip. The chip state drives the icon and color:
| State | Icon | Color |
|---|---|---|
backend_offline | CloudOff | red |
db_offline | Database | red |
idp_offline | ShieldAlert | orange |
ok | Cloud | green |
loading | — | gray |
Clicking the badge opens the shell.versions sheet, which shows detailed
version and health information.
Version badge
SidebarVersionBadge displays APP_VERSION from $lib/version in a
monospace v{version} badge. It is hidden when the sidebar is collapsed on
desktop but always visible on mobile. Clicking it also opens the
shell.versions sheet.
Services store
src/lib/services-store.svelte.ts polls microservice status every 30s
(startServicesPolling(30000)), started by AppShell on mount. It exposes
probeServices(), aggregateStatus(instances), and groupByCode(services),
which the Settings → Modules page uses to render grouped service cards with
per-instance status badges (online, going_live, offline, unknown).
Global side sheets
The right-hand sheet is not route-owned. A single SheetHost is mounted
in AppShell and picks the panel component from a registry by sheetState.panelId.
Open a panel with openSheet('<id>', props, { contentClass, side }) from a
click handler — never from an $effect tied to a bindable open flag (that
causes re-open loops while the sheet is closing).
Built-in shell panels live in src/lib/shell/sheets/panels/ (errors,
versions). Entity-list panels (columns, filters, search-in, preview, version
history) live in src/lib/entity-list/sheets/panels/.