# Realtime Console Shell and App Scope

> How the Channels console shell resolves your session, lists apps, scopes each tab to an org or an app, and what it shows when a screen fails.

The Channels console (the realtime modality's dashboard) wraps every tab
in a shared shell: a top bar with the product mark, an app switcher, a
status pill, a theme toggle and an account menu, over a horizontal tab
bar. This page documents the shell itself — the parts that stay on
screen no matter which tab you are on.

The tab bar routes to these paths:

| Tab            | Path           |
| -------------- | -------------- |
| Overview       | `/`            |
| Get started    | `/get-started` |
| Apps & keys    | `/apps`        |
| Stats          | `/stats`       |
| Messages       | `/messages`    |
| Event Creator  | `/events`      |
| Webhooks       | `/webhooks`    |
| Settings       | `/settings`    |

Any unrecognised path redirects to `/`.

## What the app switcher reads and what the status pip means

The app switcher is backed by the real `realtime.apps.list` procedure,
called with your `orgId`. The query only runs once an `orgId` is
available, so the switcher has three pre-app states (see below) before
it can show anything.

Once the list loads, the switcher shows the **active app** — the one you
last selected, or the first app in the list if you have not selected
one. It displays:

- the app's `name`
- its `app_id`
- a coloured **pip**

The pip is a two-state indicator derived from the app's `status` field:

| Pip state   | Condition            |
| ----------- | -------------------- |
| `connected` | `status` is `active`  |
| `off`       | any other `status`   |

Opening the switcher lists every app in the org with the same
`name` / `app_id` / `status` triple and the same pip per row. Picking a
row changes the active app for the whole shell — every app-scoped tab
re-reads it immediately. The selection is shell state, not a URL
parameter, so switching apps does not change the path you are on.

Next to the switcher, when an app is active, the shell renders a
**status pill** with a globe icon, the app's `app_id` in monospace, and
the raw `status` string. The pill is absent entirely when there is no
active app. It is the same data as the switcher, surfaced so you can see
which app the current screen is talking to without opening the menu.

## Organisation scope versus app scope across the tabs

Two scopes coexist in this console, and the distinction explains why
some tabs work before you have an app and others do not.

**Organisation-scoped.** The app list itself is org-scoped: it is
fetched by `orgId`, not by app. Apps & keys, Webhooks and Settings are
management screens that live at the org level.

**App-scoped.** Stats, Messages and the Event Creator read live engine
telemetry or publish through the `realtime.*` procedures against the
currently selected app. Without an active app there is nothing for them
to address.

Every screen receives the same props from the shell, so each one can
decide what to render for itself:

| Prop           | What it carries                                        |
| -------------- | ------------------------------------------------------ |
| `app`          | The active app object, or `null`                       |
| `apps`         | The full org app list                                   |
| `orgId`        | The resolved organisation id                            |
| `signedIn`     | Whether a session was resolved                          |
| `authLoading`  | Whether auth is still resolving                         |
| `appsLoading`  | Whether `apps.list` is in flight                        |
| `appsError`    | The `apps.list` error message, or `null`                |
| `refetchApps`  | Re-runs `apps.list`                                     |
| `setApp`       | Changes the active app                                  |
| `go`           | Navigates to a tab by key                               |
| `theme`        | The current light/dark mode                             |

Because `appsError` and `appsLoading` are passed down rather than
handled centrally, a failed app list shows up inside the screen you are
on, not as a shell-level banner.

## The three pre-app states: session resolving, signed out, no app

The app switcher renders one of three placeholders before it can show a
real app. They look similar, so it is worth knowing which is which.

**"Checking session…"** — auth is still resolving. The pip is `off` and
the control is inert. This state exists specifically so that a cold load
does not flash the signed-out "Sign in" call to action for a moment
before the session hydrates. If you see it persist, the session lookup
has not settled yet; nothing is wrong with your apps.

**"Sign in"** — there is no session, or no `orgId`, so the app list
cannot be fetched at all. The switcher becomes a link to `/login` on the
portal, with the subtitle *"app list is org-scoped · sign in on the
portal"* and an external-link icon. It deliberately links out instead of
rendering a disabled button, because the portal session is shared across
subdomains and signing in there resolves this console too.

Separately, on cold load the console fires a portal-session guard before
the shell paints. The guard resolves when a session exists — or when it
has already attempted the check once in this tab — and otherwise
navigates you to the portal. So in practice most signed-out visitors are
redirected rather than landing on the "Sign in" switcher.

**"No app yet"** — you are signed in and the org list came back empty.
The switcher becomes an internal link to `/apps` with the subtitle
*"create one in Apps & keys"*. This is the state to fix before Stats,
Messages or the Event Creator have anything to show.

## Signing in, the shared portal session, and the account menu

Authentication is not owned by this console. The session lives in the
portal and is shared with the sibling consoles, which is why both the
signed-out switcher and the account menu send you to portal URLs rather
than rendering a login form here.

The account menu is the circular avatar at the right of the top bar. Its
letter is the first character of your signed-in email address, upper
cased, or an em dash when no email is known. The button's tooltip is the
full email, or "Account" when there is none.

The menu subscribes to auth state changes, so the email updates in place
if the session changes in another tab without a reload.

Opening it shows:

- your email, or "Not signed in"
- the active app as `App · <name>`, or "No app connected" — the same
  active app the switcher and status pill reflect
- **Portal hub** — a link to the portal root
- **Sign out** when signed in, or **Sign in** (portal `/login`) when not

**Sign out is global.** It calls the shared sign-out bridge, which signs
you out everywhere rather than only in this console, then redirects to
the portal root. The redirect happens regardless of whether the sign-out
call itself succeeded, so you are never left sitting on an authenticated
shell after clicking it.

## Branding in the console shell

Two parts of the top-left mark behave differently under white-labelling.

**"Channels" is the product name of this console and does not change.**
It is the name of the realtime modality's dashboard, not a
ClutchCall brand string.

**The logo mark and the platform chip do change.** The shell asks the
shared branding helpers for a logo source and a platform name. If a
branded logo image is configured it renders that; otherwise it falls
back to a generated broadcast mark. The small monospace chip beside
"Channels" reads `<platform> realtime`, where the platform name comes
from the same branding lookup.

A white-labelled deployment replaces those two through the portal, under
**Settings → Branding**.

The theme toggle sits between the docs link and the account menu and
flips the shell between dark and light mode. The current mode is also
passed down to every screen as the `theme` prop, so screens can match
their own rendering to it.

## When a screen fails to render

Every routed screen is wrapped in a single top-level error boundary. If
a screen throws during render, you get a fallback card in the content
area instead of a white screen:

> **Something broke rendering this screen**
> Reload, or check Sentry for the details.

with a **Reload** button that performs a full page reload.

Three things follow from how this boundary is wired:

- **The shell survives.** The top bar, app switcher, status pill and tab
  bar are outside the boundary, so they keep working. You can still
  switch apps, open the account menu, or navigate to another tab.
- **Navigating clears the error.** The outlet wrapper is keyed by
  pathname, which remounts the boundary on every navigation. Moving to a
  different tab resets the error state without a reload — so if only one
  screen is broken, the rest of the console remains usable.
- **The error is reported.** The boundary captures the exception to
  Sentry along with the React component stack. Error monitoring is a
  no-op when no Sentry DSN is configured, and events from this console
  are tagged with `vertical='realtime'`.

The fallback only catches *render* errors from the screens beneath it.
A failed data fetch — for example `apps.list` erroring — is not a render
error; it arrives at the screen as the `appsError` prop and is displayed
by that screen.
