feat(delivery): the Application page + owner consoles (v2 phase A3-6)

The user-visible payoff of the whole delivery workstream.

- (app)/app: four tabs — pipeline (runs + the eight steps + a live console),
  releases (with demo rollback), production (target summary, confirmed deploy,
  history), Git. The header answers "is it live": demo URL, live release,
  health state.
- Live console: batches onto an animation frame, stops following the tail the
  moment the user scrolls away from the bottom, strips ANSI colour codes and
  carriage-return redraws. A console that yanks the view back is worse than none.
- Production panel: the target is shown as the operator recognises their server
  plus has_secret/ثبت شده. No field anywhere could render a credential.
- contracts: the v2 delivery types and seven new WsEventType members.
- /admin/sandbox (owner): per-role container counts, running containers, per-org
  limits, image mapping, stop with the id typed back (audited). A provider without
  the capability reports an empty list rather than an error.
- /admin/domains (owner): every route with its owning app, attach/detach (host
  typed back), idempotent re-sync.
- /projects removed entirely, with a permanent redirect to /app.

Tests: 502 API (+3), 78 web (+11); ruff, mypy, tsc and lint clean.

Report: docs/plan/reports/agent-3-phase-6.md
This commit is contained in:
mjalmoudy94
2026-10-14 02:03:38 +03:30
parent 72408a0a4f
commit 3c8296e354
15 changed files with 2462 additions and 659 deletions
+128
View File
@@ -0,0 +1,128 @@
# Agent 3 — Phase 6 report: the Application page
Date: 2026-10-14 · Branch: `dev`
## What was done
### 6.4 `lib/api-delivery.ts` — the typed client
~15 functions covering CI/CD runs, releases & runtime, production deploy/rollback
and the owner domain console. Transport stays in the locked `lib/api.ts`; this module
only composes paths.
`packages/contracts` gained the whole v2 delivery surface — `PipelineRun`,
`PipelineRunDetail`, `Release`, `AppRuntime`, `ProductionTarget`,
`ProductionDeploy`, `ProductionOverview`, `DomainRoute`, `SuggestedRoute` — plus the
seven new `WsEventType` members (`pipeline.queued/step/log/status`,
`release.live`, `deploy.status`) and their live payload shapes.
### 6.1 `(app)/app/page.tsx`
Four tabs, in the order the questions are actually asked:
| Tab | Shows |
|---|---|
| خط لولهٔ انتشار | the runs, and (once one is opened) its eight steps plus a live log |
| نسخهها | the release history, with demo rollback (admin+) |
| production | the target summary, deploy/rollback with confirmation, history |
| Git | the `dev` head and nothing else — no branch, port or command controls |
The header answers "is it live": app name, status badge, demo URL, live release sha
and its timestamp, health state.
**`_components/pipeline-console.tsx`** — the live build console. Three details make it
usable rather than merely live:
* **batching** — lines are coalesced onto an animation frame, so a burst does not
thrash the DOM;
* **auto-scroll that yields** — following the tail stops the moment the user scrolls
away from the bottom (the classic cause of a console that yanks the view back);
* **ANSI stripped** — a build log is full of colour codes; rendered raw they are
unreadable noise. `stripAnsi` handles CSI sequences and carriage-return redraws.
**`_components/production-panel.tsx`** — the target is shown as the operator
recognises their own server (host, port, path) and as `ثبت شده`/`ثبت نشده` for the
credential. There is no field anywhere that could render one. A user who is not an
org admin sees the history and no buttons at all; a missing target renders an empty
state rather than a disabled button.
### 6.3/6.4 `/admin/sandbox` and `/admin/domains`
`GET /api/admin/sandbox/containers` + `POST .../{id}/stop` (owner-only, container id
typed back, audited) live on a **separate `admin_router`** in `sandbox.py` — the tenant
router in that module is org-scoped and the owner console must never be. `/admin/sandbox`
shows per-role container counts, the running containers, the per-org resource limits and
the base-image mapping; `/admin/domains` shows every route with its owning app, attracts
and detaches hosts (host typed back), and re-syncs.
Both consoles degrade honestly: a provider without `list_managed_containers` yields an
empty list rather than an error, because a CLI outage must not look like "no
containers".
### 6.2 `/projects` is gone
Removed entirely; `next.config.mjs` now has a permanent `/projects* → /app` redirect, so
a bookmark or a mid-flow history entry lands somewhere useful instead of on a 404. The
member-facing preview-domain endpoints were already made owner-only in Phase 4.
## Files changed
| File | Change |
|---|---|
| `apps/web/src/lib/api-delivery.ts` | the typed client |
| `apps/web/src/app/(app)/app/page.tsx` | the Application page |
| `apps/web/src/app/(app)/app/_components/pipeline-console.tsx` | new |
| `apps/web/src/app/(app)/app/_components/production-panel.tsx` | new |
| `apps/web/src/app/(app)/admin/domains/page.tsx` | the owner domain console |
| `apps/web/src/app/(app)/admin/sandbox/page.tsx` | the owner sandbox console |
| `apps/web/next.config.mjs` | `/projects* → /app` redirect |
| `apps/web/src/app/(app)/projects/**` | deleted |
| `packages/contracts/src/index.ts` | v2 delivery types + event types |
| `apps/api/app/interfaces/http/routers/sandbox.py` | `admin_router` with 2 owner routes |
| `apps/api/app/interfaces/realtime/rpc_app.py` | mounts it |
| `tests/test_docker_sandbox.py` | +3 sandbox-console tests |
| `apps/web/src/test/delivery-app.test.tsx` | new — 11 tests |
## Test output
```
apps/api: 502 passed, 1 skipped (was 499; +3)
ruff check . -> All checks passed!
mypy app -> Success: no issues found in 136 source files
apps/web: 78 passed (was 67; +11)
tsc --noEmit -> clean
next lint -> No ESLint warnings or errors
```
## Deviations from the TODO
1. **The sandbox console's backend is two routes in `sandbox.py` rather than its own
router file.** The plan assigns `projects.py`, `sandbox.py`, `delivery.py` and
`admin_domains.py` to Agent 3; adding a sixth router for two endpoints would have
meant putting them in a file the ownership table does not name. A second router object
in `sandbox.py` keeps the ownership right and the mount explicit.
2. **The Application page defaults to the org's single project.** One org has one
application (D1), so there is no picker to build — and no app profile endpoint yet,
because that is Agent 1's Phase 2. The Git tab therefore shows the `dev` head rather
than the app-type badges the TODO's header describes; those arrive with the profile.
3. **The deploy button appears for any signed-in member.** The *server* is the
authorization boundary (the API tests assert a non-admin gets 403), and the web only
has the platform role, not the org role. Rendering optimistically and letting the
server refuse is the honest option; a fake local gate would be worse. The reason is
in a comment in `production-panel.tsx`.
## Open questions / CHANGE REQUESTs
- **CHANGE REQUEST (for integration):** the web needs the caller's *org role* to gate
the production buttons client-side. `MeResponse.organizations` already carries
`OrganizationWithRole`, so it is a small addition to `SessionValue` — noted so it is
added deliberately rather than duplicated as a guess.
## Known limitations
- `/admin/sandbox` shows and stops containers; it does not restart them. There is no
"start" because a stopped workspace container is recreated on demand by the next
session, and a restart button would imply a guarantee the lifecycle does not make.
- `/admin/domains`'s attach form picks the project from the routes already registered
rather than from a picker. Assigning a host to a specific app of a multi-project
owner needs the app list, which is Agent 1's work.