Artur Mukhamadiev 7d6429aca9 frontend: queue-aware submit flow and queued loading view
Submit joins the admission queue first and sends the PAT only once the
reservation is held. Queued state shows the shaping orb, ordinal position
with estimate, and an exit button whose label changes with the wait;
running switches the orb to solving. Rejoin once on a lost reservation or
ticket, best-effort leave on pagehide. Mock server gains the queue
endpoints and five scenarios; contract, unit and e2e tests cover them.
2026-09-15 13:38:59 +03:00

115 lines
7.7 KiB
Markdown

# Confluence Research - Web UI
Minimalist, secure Web UI for Confluence Research, designed to operate against the backend API contracts specified in `docs/SPECIFICATION.md` and `docs/implementation/CONTRACTS.md`.
## Directory Layout
```text
frontend/
├── index.html # Main HTML entrypoint (clean white minimalist theme)
├── css/
│ └── style.css # Responsive styling, accessible components, thinking-orb and logo styles
├── js/
│ ├── app.js # State transitions, keyboard handling, memory credentials, staleness guards
│ ├── api.js # Relative /api/v1/... fetch boundary with UTF-8 byte validation
│ ├── render.js # marked.js + DOMPurify, fail-safe render, bounded sectioning
│ ├── history.js # Sources, lazy bounded history serialization, artifacts listing
│ ├── orb.js # Vanilla canvas driver for the vendored thinking-orbs engine (loading view)
│ ├── queue.js # Pure admission-queue formatting helpers (ordinals, ETA, status line, Exit queue label)
│ └── logo.js # Book logo playback: CSS cover flip on page open and brand hover
├── assets/
│ └── book.svg # Favicon (same book shape as the CSS logo)
├── vendor/ # Pinned vendor libraries & licenses (locally served)
│ ├── marked.min.js
│ ├── marked.LICENSE
│ ├── purify.min.js
│ ├── dompurify.LICENSE
│ ├── thinking-orbs.engine.js # Framework-free engine build of thinking-orbs 0.3.1
│ └── thinking-orbs.LICENSE
├── dev/
│ ├── mock-server.js # Zero-dependency same-origin mock server & scenario runner
│ ├── scenario-toolbar.js # External dev toolbar script (CSP compliant, no inline scripts)
│ └── scenario-toolbar.css # External dev toolbar styling (CSP compliant, no inline styles)
├── tests/
│ ├── contract.test.js # Wire format, status code, header, & scenario tests (23 tests, incl. the 5 admission queue scenarios)
│ ├── api.test.js # UTF-8 byte boundary and credential validation tests (6 tests)
│ ├── queue.test.js # Admission queue formatting helpers: ordinals, ETA, status line, Exit queue label (12 tests)
│ ├── render.test.js # Markdown section partitioning and fallback tests (10 tests)
│ └── e2e_runner.js # End-to-end browser test runner connecting to Chrome (9444) via CDP (19 tests)
├── package.json
├── package-lock.json
├── .gitignore
└── README.md
```
## Security & Architecture Highlights
1. **In-Memory Credentials**:
- Confluence Base URL and Personal Access Token (PAT) reside strictly in browser JavaScript memory.
- Never written to `localStorage`, `sessionStorage`, cookies, query parameters, console logs, or exported files.
- A `cw_session` HttpOnly cookie is set by the origin for artifact download ownership.
2. **Content Security Policy (CSP)**:
- `default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self'; media-src 'none'; font-src 'self'; object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`
- Completely prevents automatic third-party network requests, tracking pixels, and unauthorized script injection. Images are same-origin only (the favicon); the sanitizer allowlist never emits `<img>` from agent output.
- Verified via browser network tracing (zero automatic external requests).
3. **Markdown Sanitization & Link Safety**:
- Restricted element allowlist using locally vendored DOMPurify.
- Fail-safe rendering: if parser or sanitizer are absent or fail, displays a safe notice without ever injecting raw untrusted HTML.
- All links rewritten to require explicit user clicks with `target="_blank"` and `rel="noopener noreferrer"`.
- Disallowed protocols (`javascript:`, `data:`, `file:`) have `href` stripped.
4. **Large Result Handling & Memory Bounding**:
- Large answers partitioned into bounded sections (~48 KiB soft target, ~64 KiB hard cap) rendered on demand.
- Giant code fences (e.g. 12 MB) are safely split and re-opened so every section is a valid Markdown code block.
- Tables preserve row boundaries and repeat column headers across sections.
- Pathological blocks fall back to a bounded plain-text preview with full export available.
- "Export to MD" always exports the complete, untouched raw Markdown client-side via Blob.
- Tool call results in history are rendered lazily with bounded serialization buffers (`serializeBounded`).
5. **Admission Queue** (`docs/QUEUE_SPECIFICATION.md` §7):
- The backend runs one query at a time; submitting calls `POST /api/v1/queue/join` first. A `ready` answer sends the query immediately. A `queued` answer shows the loading view's queued sub-state (`shaping` orb, `js/orb.js`'s `setState`) with a status line such as "You're 3rd in line · about 4 min" and polls `GET /api/v1/queue/status` every 2 seconds until `ready`, then sends the query at once.
- The queued sub-state's "Exit queue" button (aria-label fixed as "Leave the queue") relabels itself by time waited — "I will try next time" (under 1 min), "Ohhh, it's so long" (1-3 min), "I'm dying in this queue" (over 3 min) — and calls `DELETE /api/v1/queue/ticket` on click, returning to the prompt view with the prompt text preserved.
- A `409 busy` right after `ready`, or a `404 ticket_not_found` while polling, triggers exactly one automatic rejoin (`js/api.js`'s `joinQueue`/`queueStatus`/`leaveQueue`); a second failure shows a user-facing message instead of retrying forever.
- A best-effort `DELETE /api/v1/queue/ticket` keepalive fetch fires on `pagehide` while the loading view is visible, so closing the tab releases the ticket promptly instead of waiting out the server's heartbeat timeout.
## Development & Testing
### Running the Dev Mock Server
The mock server runs entirely with Node.js built-ins (zero dependencies) on loopback:
```bash
cd frontend
npm run dev
# Or custom port:
node dev/mock-server.js --port 5173
```
Open `http://127.0.0.1:5173/` in your browser. A floating dev toolbar in the bottom-right corner allows toggling between all 18 deterministic mock scenarios (e.g. normal shared example, 403 verify, 409 busy, 504 timeout, malicious content, large output, delayed cancellation, and the 5 admission queue scenarios below).
Admission queue scenarios (`docs/QUEUE_SPECIFICATION.md` §10), each with its own per-session poll counter that resets whenever the active scenario changes:
| Scenario | Behaviour |
| --- | --- |
| `queued` | join answers position 3 with `eta_seconds` 150; each status poll decrements the position; the third poll answers `ready`; the query then succeeds |
| `queued_no_estimate` | as `queued` with `eta_seconds: null` throughout |
| `queue_full` | join answers `503 queue_full` |
| `reservation_lost` | join always answers `ready`; the first query after `ready` answers `409 busy`; the automatic rejoin then succeeds |
| `ticket_lost` | join answers `queued`; the second status poll answers `404 ticket_not_found`; the automatic rejoin answers `ready` |
### Running Unit & Contract Tests
Tests verify API limits, wire contracts, headers, cookies, markdown partitioning, and the admission queue formatting/scenarios (51 tests):
```bash
cd frontend
npm test
```
### Running E2E Browser Tests
Runs comprehensive browser tests against Chrome on port 9444 via CDP, including the queued → ready → result flow, Exit queue label timing (clock-stubbed), and a reservation-lost rejoin (19 tests):
```bash
cd frontend
npm run test:e2e
```