@aeolus-fleet/console
The Aeolus operator web app (Next.js). It reaches the server only through its tRPC API at /trpc.
Pre-v1: expect changes between minor releases. Its main pages; every route, the rest of /sign-in/* and the console's own /api/* included, is under app/:
| Path | What |
|---|---|
/sign-in | Sign in with argo's email and password once set, or a link from aeolus-core sign-in; where the console signs accounts in through pagasae, that sign-in instead, with Sign in as argo for an account allowed as argo. A hosted console sends you to the hosting service's sign-in. The session crews argo; only a session cookie stays in the browser |
/ | The fleet overview, live over a WebSocket: every ship with status and where a crewed ship's session runs or an awaiting ship's prompt status, updated without a reload (a ship's type is a filter, not shown in its row; with squadrons on, a member shows its squadron instead); search and filters in the URL; commission a ship; each ship's actions behind their dialogs: get a new starting prompt for a ship awaiting crew (shown once with its crew line; asks first while an unclaimed one is out), release or re-crew a crewed ship (re-crew shows a fresh prompt once), and retire any ship but argo (the ship's name typed to confirm when it still has open deliveries; retired ships show under Show retired); sign out from the account menu. Without a session it sends you to /sign-in, which says so when you signed in somewhere else |
/ships/[shipId] | A ship's page, live: status, type, where its session runs, since when, when it was commissioned and its id, and how its last ping stands, and its not handled deliveries with Resend and Dismiss; Ping, Get starting prompt, Re-crew, Release, Rename (live name check and typed confirm) or Retire; a Timeline tab with every change to the ship, newest first, and a Messages tab with its messages grouped into threads by reply. A message opens in a side sheet (?message=) with its envelope, payload (formatted JSON or raw) and the history of its delivery, and Reply, which opens Compose with its sender as the recipient and the message it answers |
/fleet-health | Health, live: whether delivery works, where deliveries are stuck by cause and place with one next step each, the 7-day trend, the acknowledged in 24 hours, the computers and the silent squadron members. Its stuck count shows in the Sidebar and TabBar on every page. /needs-attention opens it |
/fleet-health/[cause] | One cause opened: each place's facts and its deliveries one by one. An undeliverable one offers Resend and Dismiss, with a toast |
/inbox | argo's one inbox, live (#849): questions on top, open ones oldest first with the asker's pick chosen; then messages, newest first, their body rendered (markdown, links) with their thread; then plugin ships' messages folded into one row with one Done. Open and Done on phone, with All on desktop (?filter=); two panes with keys (J, K, E, R) on desktop and a page of its own on phone (?message=). Opening one marks it read; Mark as unread undoes that; Done, Reply or Answer acknowledges it. Its open count, questions and messages together, shows in the Sidebar and TabBar on every page |
/health | Web up, plus the server's health. Nothing about fleets |
/version | Without a session: the web process's version, plus the versions the server answers from its /api/version (its own and common's), or server: null when it does not answer. Only versions: nothing about plugins, migrations, configuration or fleets |
Running from npm
The package holds the console already built, as Next.js's standalone output (.next/standalone): the server, the browser's files and only the modules the server runs, Next.js and React among them. Installing it installs nothing else: about 30 MB, where Next.js and its build tooling took 300 MB and more. It knows no server address until it runs:
npm install @aeolus-fleet/console
AEOLUS_SERVER_URL=https://fleet.example.com AEOLUS_CONSOLE_URL=https://fleet.example.com PORT=3000 npx aeolus-console start
aeolus-console start runs the standalone server (.next/standalone/packages/console/server.js), without Next.js telemetry, until SIGINT or SIGTERM. An image can copy .next/standalone alone and run that server.js with node, setting PORT, HOSTNAME and AEOLUS_CONSOLE_URL. The licence notices of the third-party code and fonts the console ships travel with it, in .next/standalone/THIRD_PARTY_NOTICES.
Configuration
Environment variables, read when the app runs, never when it is built:
| Variable | Default | Meaning |
|---|---|---|
AEOLUS_SERVER_URL | http://localhost:4000 | The Aeolus server. The browser calls its /trpc there, with credentials, and follows the fleet over a WebSocket to the same path (ws:// or wss://), and the web app's /health asks its /health (at AEOLUS_SERVER_INTERNAL_URL when set), so both must reach it. Behind one host with the server, it is the server's public URL. On another host, the server needs COOKIE_DOMAIN and CONSOLE_ORIGIN for the console (see the server README) |
AEOLUS_SERVER_INTERNAL_URL | AEOLUS_SERVER_URL | Where the web app's server reaches the server, for its own calls: /health, /version, the operator's theme and connecting squadrons. Server side only; the browser always uses AEOLUS_SERVER_URL. Set it when the public address routes back through a proxy in front of the web app, for example http://server:4000 inside Compose, so /health on web does not loop |
AEOLUS_CONSOLE_URL | none: required | The console's own public address, such as https://fleet.example.com, the same origin as the server's CONSOLE_ORIGIN. The web app's server sees only the address it binds, so pagasae's sign-in redirect, the origin the console checks and the one it shows the server come from this. A production start without it, or with no http(s) URL, stops at once and says so; npm run dev uses the request's address |
AEOLUS_HOSTED_SIGN_IN_URL | unset | A hosted console's sign-in, at the hosting service (pagasae for hosted Aeolus). Set, /sign-in redirects there at once (with ?notice=signed-in-elsewhere it shows that notice and links there instead) and no password form is served; the hosting service hands the operator back with a one-time ticket at /sign-in/ticket?ticket=..., and a failed hand-off links back here. Unset, the console signs in with a password |
AEOLUS_HOSTED_ACCOUNT_URL | unset | A hosted operator's account page, where the fleet's limits are listed. Set, the account menu offers Your account, opening it, and the console's limit notices (Commission at the ship limit, the overview and Compose at the daily message limit) and the overview's limit meters link to it with View limits; unset, there is no Your account and no View limits link |
AEOLUS_HOSTED_ANALYTICS_PROVIDER | unset | The console's usage analytics, for a hosted install (decision 0025): posthog is the only provider. Unset, or with the key or host missing, the console sends no analytics at all |
AEOLUS_HOSTED_ANALYTICS_KEY | unset | The provider's project key, read by the web app's server only |
AEOLUS_HOSTED_ANALYTICS_HOST | unset | The provider's https address, such as https://eu.i.posthog.com. The web app's server sends there; the browser only ever posts to the web app's own /api/analytics |
AEOLUS_SQUADRONS_URL | unset | Where squadrons runs, read by the web app's server only: the browser never calls squadrons. Unset means the console has no squadrons. Settings then connects it: the web app's server commissions the management ship (squadrons, with fleet:read, fleet:commission, fleet:manage, labels:define, labels:assign, fleet:on-behalf and scopes:declare) or gives it a new starting prompt, and hands its secret to squadrons server to server; no one sees the secret. It reaches the server at AEOLUS_SERVER_INTERNAL_URL (or AEOLUS_SERVER_URL) from the web app's server too |
AEOLUS_TRIERARCH_PLUGIN_URL | unset | Where the trierarch plugin runs, read by the web app's server only: the browser never calls it. Unset means the console has no trierarch plugin. Settings then connects it: the web app's server commissions its ship (trierarch-plugin, with fleet:read, fleet:commission, crew:assign, crew:run, labels:define and labels:assign) or gives it a new starting prompt, and hands its secret to the trierarch plugin server to server; no one sees the secret |
AEOLUS_GATEWAY_URL | unset | Where the gateway runs, read by the web app's server only: the browser never calls it. Unset means the console has no gateway. Settings then connects it: the web app's server commissions its ship (gateway, with fleet:read, fleet:commission, fleet:on-behalf, scopes:declare and fleet:verify) or gives it a new starting prompt, and hands its secret to the gateway server to server; no one sees the secret. The gateway serves its own pages |
AEOLUS_PAGASAE_URL, AEOLUS_PAGASAE_CLIENT_ID, AEOLUS_PAGASAE_CLIENT_SECRET | unset | pagasae's address and the console's app credentials there (APP_CLIENTS, client console), read by the web app's server only. Set, accounts sign in to the console as themselves through pagasae's shared sign-in (/sign-in/start, /sign-in/done, /sign-in/enter, /sign-in/renew, /sign-in/out); an account pagasae allows as argo of the fleet signs in as argo from the avatar menu or the sign-in page (/sign-in/argo, warning first at /sign-in/argo/confirm when someone holds argo) and signs out of argo back to itself (/sign-in/argo/out); and the account menu shows an account by the name pagasae gives it, asked each time it shows and never stored (decisions 0054, 0058), and the header lists the apps and fleets the account signed in through pagasae may open (header design H1); unset, or while pagasae gives none, the account by its id and the header the console and its own fleet alone |
PORT | 3000 | Port to listen on |
HOST | 127.0.0.1 | Interface to listen on |
OTEL_EXPORTER_OTLP_ENDPOINT and the other OTEL_* settings | unset | Traces and metrics over OpenTelemetry, as for the server: see the server README's Telemetry. Unset, the console sends nothing |
npm run dev builds @aeolus-fleet/common and @aeolus-fleet/ui first: the console bundles both built packages, with common's schemas on the web app's server and only @aeolus-fleet/common/rules (no zod) in the browser, for live form hints (decision 0033).
Beyond localhost
On localhost, plain http works. On any other host, serve the console over https: over plain http the browser drops the Secure session cookie, so sign-in fails (the sign-in page says sign-in needs https), and the password travels in clear text. Put a TLS proxy in front, set AEOLUS_SERVER_URL to the server's https address (the console warns at startup when it is plain http beyond localhost), and HOST only when the proxy runs on another machine or container. The server README's "Beyond localhost" has the server's settings and a Caddy example for both behind one host.
Design system
The console is themed from tokens.css in @aeolus-fleet/ui (packages/ui/src), the single copy of the design tokens, beside the header and the atoms it stands on (light on :root, dark on .dark, which follows the system setting). app/globals.css maps them onto Tailwind v4. Each feature's parts live in features/<feature>/ (layout: CLAUDE.md, Web), the shared ones in components/; atoms are shadcn/ui on Base UI (components.json). Geist and Geist Mono are self-hosted from app/fonts/ under the SIL Open Font License.
npm run storybook -w @aeolus-fleet/console shows every part, with a story per meaningful state and a toolbar for light and dark. npm run build-storybook -w @aeolus-fleet/console builds it; CI does on every pull request.
Compose, in the Header (and the root TopBar on phone) of every page, sends a plain-text message as argo to one active ship by name or any ship of a type; each message keeps one idempotency key until it is sent.
The AccountMenu (the Sidebar's account button on desktop, the root TopBar's Avatar as a bottom sheet on phone) shows who is signed in and this session's device, the theme (Light, Dark, System), and Sign out. The theme is stored on the operator's account; the root layout asks the server for it with the session cookie when it renders, so the page never flashes the wrong theme, and the sign-in page follows the system.
Ship names and types are typed into the console's fields (commission, rename, the Compose recipient) in any case: uppercase becomes lowercase as it is typed, so HemmaFeature reads hemmafeature. The API stays strict and refuses uppercase.
The CommandPalette opens on Cmd+K (Ctrl+K) or the Header's search on desktop, and full screen from the root TopBar's search icon on phone: actions (Commission ship, Compose), the active ships by name or type, and the pages. Commission ship lands on the fleet overview with the CommissionDialog open (/?commission=new).
Crewed ships show when they were last seen ("Last seen 20 s ago") in the overview and on their page. A call writes no event, so an open page reads the snapshot again every 30 s, and relative times move on every 10 s.
Ping (in a fleet row and on the ship page, crewed agent ships only; disabled while a ship awaits crew) asks the ship's session to answer with pong. Under Last seen, the ship then shows "Pinged 3 min ago, no answer yet", "Answered ping in 4 s", or "Received, not answered with pong" after a plain ack. While a ping waits, Ping says so in a toast instead of sending another. An acknowledgement or an undeliverable delivery reloads the snapshot live, so the answer shows at once.