The origin check is an exact match including the context path, so users had to type "https://collab.lge.com/main" precisely. GET /api/v1/config now returns the approved origins in canonical form (non-secret: they are the only destinations the backend will talk to), and the UI swaps the URL text field for a select listing them, keeping the element id, focus handling and the Test connection flow unchanged. The text field remains the fallback when the fetch fails. Backend validation of the submitted URL is untouched. Mock server serves the endpoint; contract, API and e2e tests cover it.
8.0 KiB
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
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)
│ ├── running-status.js # Time-based status line for the loading view's running state
│ ├── 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
- In-Memory Credentials:
- Confluence Base URL and Personal Access Token (PAT) reside strictly in browser JavaScript memory.
- The URL field is a select filled from
GET /api/v1/config(the backend's approved origins, canonical form). If that fetch fails, the plain text input stays as the fallback. - Never written to
localStorage,sessionStorage, cookies, query parameters, console logs, or exported files. - A
cw_sessionHttpOnly cookie is set by the origin for artifact download ownership.
- 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).
- 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"andrel="noopener noreferrer". - Disallowed protocols (
javascript:,data:,file:) havehrefstripped.
- 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).
- Admission Queue (
docs/QUEUE_SPECIFICATION.md§7):- The backend runs one query at a time; submitting calls
POST /api/v1/queue/joinfirst. Areadyanswer sends the query immediately. Aqueuedanswer shows the loading view's queued sub-state (shapingorb,js/orb.js'ssetState) with a status line such as "You're 3rd in line · about 4 min" and pollsGET /api/v1/queue/statusevery 2 seconds untilready, 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/ticketon click, returning to the prompt view with the prompt text preserved. - A
409 busyright afterready, or a404 ticket_not_foundwhile polling, triggers exactly one automatic rejoin (js/api.js'sjoinQueue/queueStatus/leaveQueue); a second failure shows a user-facing message instead of retrying forever. - A best-effort
DELETE /api/v1/queue/ticketkeepalive fetch fires onpagehidewhile the loading view is visible, so closing the tab releases the ticket promptly instead of waiting out the server's heartbeat timeout.
- The backend runs one query at a time; submitting calls
Development & Testing
Running the Dev Mock Server
The mock server runs entirely with Node.js built-ins (zero dependencies) on loopback:
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):
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):
cd frontend
npm run test:e2e