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
129 lines
6.4 KiB
Markdown
129 lines
6.4 KiB
Markdown
# 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.
|