# 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

<Mermaid chart={`flowchart TB
  Layout["(app)/+layout.svelte<br/>fetch /api/v1/auth/me"]
  Layout --> AppShell
  AppShell --> Toaster
  AppShell --> SheetHost["SheetHost<br/>global side sheets"]
  AppShell --> SP["Sidebar.Provider"]
  SP --> AppSidebar
  SP --> Inset["Sidebar.Inset"]
  Inset --> AppTopbar
  Inset --> Banner["AppServerBanner"]
  Inset --> Children["page children"]
  AppSidebar --> OrgSwitcher["SidebarOrgSwitcher"]
  AppSidebar --> AppLauncher["SidebarAppLauncher"]
  AppSidebar --> NavMenu["Navigation menu<br/>from module_nav"]
  AppSidebar --> Footer["Footer"]
  Footer --> ProfileMenu["SidebarProfileMenu"]
  Footer --> HealthBadge["SidebarHealthBadge"]
  Footer --> VersionBadge["SidebarVersionBadge"]
`} />

`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:

1. **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 declared
   `is_default` module's first page **under that app** (`home[root]`).
2. **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 embedded `nav`/`label_key` in
   the same payload — no per-module meta fetch.
3. **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.

```ts
type ActiveOrg = {
  uuid: string;
  idp_code: string;
  idp_name: string;
  display_name: string;
  avatar: string | null;
};
```

## 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/`.

## Next steps

- [Authentication & sessions](/frontend/dev/authentication)
- [System settings](/frontend/dev/settings)
- [API reference](/frontend/dev/api-reference)
