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