Rig Cockpit surfaces¶
"Rig Cockpit" is three different clients, and they are not equivalent. Only one of them can talk to the GPU brain. Opening the wrong one is the most common way to conclude that something is broken when nothing is.
1. The three surfaces¶
| Web cockpit | iOS app | Capacitor shell | |
|---|---|---|---|
| Where | rig-cockpit.dashecorp.com (also rig-cockpit-worker.invotekas.workers.dev) |
TestFlight, app "Rig Cockpit" | not shipped |
| Repo | dashecorp/rig-cockpit-worker (src/web/dashboard.html) |
dashecorp/rig-cockpit-ios |
dashecorp/rig-cockpit-worker (shell/) |
| Runs on | Cloudflare Worker rig-cockpit-worker |
iPhone, bundle no.invotek.rig-cockpit, ASC app 6782443501 |
would wrap the web cockpit in a WKWebView / Android WebView |
| Chat with Rig-E / the GPU | Yes — the only surface that can | No. No chat code at all | would inherit the web cockpit's |
| Rig status, tenants, activity | Yes | Yes (read-only) | inherits |
| Push (APNs) | no | yes | yes (native registration) |
| Status | live; installable since 2026-09-09 | build 9 in TestFlight; never released to the App Store | merged, never built for release |
The GPU brain is reachable from the web cockpit and nowhere else. The Manager tab has a
"Talking to" target picker (Manager plus every agent returned by GET /v1/agents); choosing
rig-e routes sends to POST /v1/agents/rig-e/chat, which the Worker proxies to
rig-agent-runtime with the per-agent X-Chat-Token injected server-side. The iOS app has five
tabs — Rig, Terminal, Activity, Tenants, Settings — and no chat surface in any of them
(rig-cockpit-ios#44 would add one; still open).
Which one to open¶
| Job | Surface |
|---|---|
| Chat with Rig-E; wake or read the GPU brain | Web cockpit — see gpu-brain.md |
| Glance at agents / stuck issues / tenants from the phone | either — the iOS app is fine for read-only |
| Anything requiring a push notification on the phone | iOS app (the web cockpit has no push) |
| Demo the cockpit as an app icon without TestFlight | Web cockpit → Add to Home Screen |
2. Web cockpit¶
A single-file SPA (src/web/dashboard.html) served by the Worker, which also proxies
/api/* to rig-conductor behind Cloudflare Access. Both rig-cockpit.dashecorp.com and the
workers.dev origin serve it; the custom-domain route is not declared in wrangler.toml on
purpose — it is managed in OpenTofu so the app repo never owns DNS/route state. Note that
APP_URL (the base used to build magic-link emails) still points at the workers.dev origin.
Installable (rig-cockpit-worker#99, merged 2026-09-09). The Worker now serves a
/manifest.webmanifest, PWA icons (/icons/icon-{192,512}.png, /icons/icon-512-maskable.png,
/apple-touch-icon.png), iOS meta tags, and a shell-only service worker at /sw.js. Add to
Home Screen gives a real app icon and a standalone launch.
The service worker is deliberately conservative, because the cockpit is a live operations console:
it precaches only the shell at install, and every /api/*, /v1/*, non-GET and cross-origin
request is passed straight through without respondWith(). A stale agent roster is worse than an
error, so nothing fetched at runtime can enter the cache.
3. iOS app (rig-cockpit-ios)¶
Native SwiftUI, forked from the Cuti-E admin shell. Read-only. It authenticates against the same
Worker (POST /v1/auth/apple) and reads the same proxied conductor API.
Three things about this repo that cost time if you do not know them:
- The default branch is
feature/fork-cutie-admin-cockpit, notmain. Branch from it, PR into it, and read files from it. - The app did not compile from 2026-08-20 to 2026-09-09.
APIErrorgained a.notAuthorizedcase and five view-levelswitches over it were left non-exhaustive with nodefault:. Fixed inrig-cockpit-ios#51. Compile locally before pushing Swift:xcodebuild -project Cuti-E-Admin/Cuti-E-Admin.xcodeproj -scheme Cuti-E-Admin -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build - Certificates, provisioning profiles and the App Store Connect key are healthy. The 2026-09-09 build signed and uploaded, which was that build's real purpose.
App Store state: version 1.0 has sat in PREPARE_FOR_SUBMISSION since 2026-06-20. Nothing has
ever been submitted for review. TestFlight builds expire 90 days after upload — the June builds
(1–8) expire 2026-09-18/19; build 9 (2026-09-09) expires 2026-12-08.
4. Capacitor shell¶
shell/ in rig-cockpit-worker (#100) — a thin Capacitor 6 container that remote-loads
https://rig-cockpit.dashecorp.com and adds native push, splash and status bar, so the same web
codebase can ship to the App Store and Play Store. ios/ and android/ are not committed; CI
regenerates them from capacitor.config.json.
It is not shipped. Two gates remain, both in rig-cockpit-worker:
- The signing secrets (
MATCH_*,ASC_*) are not set in that repo, sobuild-ios-testflight.ymldegrades to archive-only. shell/Gemfile.lockis not committed. The TestFlight job runsbundle install --deploymentand refuses to run without it; thelock-fastlane.ymlworkflow (ubuntu, zero secrets in scope) has to be dispatched and its PR merged first.
5. Uploading is not releasing¶
Verified 2026-09-09. The build workflow ran green — UPLOAD SUCCEEDED with no errors — build 9
appeared in App Store Connect as processingState: VALID / APP_STORE_ELIGIBLE, and the
operator's TestFlight app still showed build 8.
The cause: the Internal Testers beta group has hasAccessToAllBuilds: false, so every build
must be attached to the group explicitly. Nothing did it. Build 9 was attached by hand.
You cannot fix this by flipping the flag. Apple rejects hasAccessToAllBuilds on an UPDATE
with 409 ENTITY_ERROR.ATTRIBUTE.NOT_ALLOWED — "can not be included in a 'UPDATE' operation". It
is create-time only. Verified, not assumed.
How a build actually reaches the phone¶
| Step | What happens |
|---|---|
| 1 | Trigger iOS TestFlight (.github/workflows/build-and-upload.yml) — workflow_dispatch, or push a v* tag. Runs on GitHub-hosted macos-26. |
| 2 | The build job computes the next build number from App Store Connect, signs with fastlane match (read-only, certs from dashecorp/ios-certs), verifies the signed entitlements, and uploads with altool. |
| 3 | The release-to-testers job (ubuntu-latest, needs: build) polls GET /v1/builds until processingState: VALID — 20 min cap, 30 s interval — then resolves the beta group by name (Internal Testers) and POSTs to /v1/betaGroups/{id}/relationships/builds. Added in rig-cockpit-ios#55 (merged 2026-09-09). It runs on the cheap runner on purpose: the wait is pure idling and macOS minutes are billed at a premium. |
| 4 | Only now is the build in TestFlight. Confirm the operator can see it. |
The group is resolved by name rather than UUID because the UUID changes if the group is ever recreated.
Manual attach (older build, or the job was skipped) — App Store Connect MCP:
add_build_to_beta_group. Do not try to set the group flag instead; see the 409 above.
Rule: never say "it's on TestFlight" until a build is attached to the group and the operator can see it. A green workflow and a
VALIDbuild prove neither.
6. Why the cockpit is shaped this way¶
Client stack — one web codebase, thin native shells. Not React Native, not three native apps.
Chosen from four proposals scored by three judges. Two factors decided it: the agent test loop
(web tests run headless in milliseconds, while Swift needs a macOS runner and no agent can verify
it locally) and the fact that every feature already lands on web first — which is precisely why
the iOS app never grew a chat tab. Recorded in
rig-cockpit-worker docs/2026-09-09-native-shell-decision.md.
Live transport (issue rig-cockpit-worker#95, open). A phone drops an idle request after ~60 s
(Safari and WKWebView alike) and a GPU wake takes ~9 minutes, so chat fails on the phone on every
stack. The design decided on 2026-09-09: the turn lives in the runtime's Postgres and is
delivered over an authenticated stream — no Durable Object. The insight is that when the
phone locks every transport dies identically, so the transport decides nothing about the headline
case; where the turn lives does. Since the connection to vLLM lives in the runtime pod, whoever
resumes a turn must own that socket — and that pod already has durable storage. Nine PRs; PR 1 is
open as rig-conductor#2068.
Capacitor was adopted without the security review this workspace requires. The review was done
after the fact — verdict adopt with conditions: the packages are genuinely Ionic's, the
lockfile is integrity-pinned, and none of the resolved packages has an install script. Three
blockers were found and are now fixed on main: the pinned version predated a navigation fix
(6.2.1 → 6.2.2), no aps-environment entitlement existed anywhere in the repo (so the shell's
App Store 4.2 justification did not actually function), and fastlane was unpinned in the same job
that holds the signing secrets. See
docs/2026-09-09-shell-shippability.md.
Owning docs¶
| Doc | Covers |
|---|---|
rig-cockpit-worker docs/web-cockpit.md |
Canonical for the web cockpit — every route, the auth flows, the Manager tab picker |
rig-cockpit-worker docs/native-shell.md |
Canonical for the shell — files, endpoints, build pipeline |
| gpu-brain.md | The GPU pod itself: cost, wake, status, stop, the waking chat replies |
| xcode-cloud.md | The shared iOS build-and-upload pipeline |