Context. surface customer · domain metis · route /metis/ingest · auth signed-in · source apps/oshun/web/src/app/metis/ingest/page.tsx
Last walked. 2026-06-29 real-infra Playwright chromium — signed-in customer shell, idle URL default, true radio source selector with Arrow/Home/End roving keyboard, PDF reference accepted through the live BFF, delayed pasted-text pending state, completed outline status card, real invalid-URL 422 validation, explicit offline abort branch, Queue another reset, mobile standalone shape, no horizontal overflow, and shared axe scan; 2026-06-29 telemetry addendum verifies the ingest form, submit button, accepted status card, Back-to-Metis link, Queue-another button, and descendants expose no data-telemetry-event, data-analytics-event, or data-event hooks. Evidence: WALKTHROUGH/results/v1-real-infra-run-2026-06-22.md#212-2026-06-29-metis-ingest-submitaccept-telemetry-coverage
Purpose#
Creator-facing surface to hand Metis one piece of raw material — a public URL, a
PDF upload, or a pasted passage — and queue an ingest job. On accept the BFF
returns a 202 with an ingestJobId: pasted text is processed immediately into
state=completed with a study outline, while URL/PDF references are honestly
recorded as state=awaiting_source_content until deploy-bound fetch/parsing can
provide the source body. See
V1/features.md#metis.
Entry points#
- Metis schoolroom / BYOM — verify whether
metis.mdormetis-byom.mdlinks to/metis/ingest - Direct URL / bookmark — yes (auth required);
canonicalis/metis/ingest - Search result — page metadata title "Metis · ingest"
Layout regions#
page.tsx is a server component:
<LWebShell top={<LCustomerNav active="explore" />}> wrapping a 640px-max
centered column with <LMasthead>, an italic intro, then the client
<MetisIngestForm />.
- Header: web shell top —
LCustomerNavwithactive="explore" - Masthead:
LMasthead— left "Metis · ingest", right "creator", kicker "From source to syllabus", title "Ingest a source." - Intro:
LEyebrow"How this opens" + serif-italic copy ("Hand Metis a single source. She returns a draft outline with section breaks, quotable passages, and a first-pass assessment rubric …") - Main: the ingest form card (
L.paper,1px solid L.rule) - Footer / sticky bottom: None.
- Automation anchors: root exposes
data-metis-ingest-pageanddata-route; the form exposesdata-metis-ingest-form, source-kind, pending/state, field-length/file-name, and error attributes; the accepted card exposesdata-metis-ingest-status, job id, BFF state, outline presence, segment/prompt/minute counts, and pending reason.
States#
- Idle form —
kinddefaults tourl; the URL input is shown; submit reads "Queue for ingest" - Kind = URL —
input#ingest-url(type=url,required) visible - Kind = PDF —
input#ingest-pdf(type=file,accept=application/pdf,required) visible; selected file name echoed below in mono - Kind = Text —
textarea#ingest-text(required,minLength={60},rows={6}) visible - Pending — submit text "Queueing…",
opacity 0.6,cursor: progress,disabled; proven with a delayedroute.fetch()that still completes against the live BFF - Accepted (text status card) —
acceptedset;role="status" aria-live="polite"card: eyebrow "Outline ready", "Job<ingestJobId>processed.", section/minute summary, outline segments, "Study prompts", with "Back to Metis" + "Queue another"; on acceptkindresets tourlandurl/text/fileNameclear. Verified 2026-06-29 inmetis-byom-ingest.spec.tsandmetis-create-forms.spec.ts - Accepted (PDF reference status card) — BFF returns
state=awaiting_source_content; card eyebrow "Received", "Job<ingestJobId>accepted.", and the deploy-bound fetch/parse or PDF parse reason. URL reference remains contract-covered; the browser proof covers PDF because it exercises the sameawaiting_source_contentcard branch. - Error (recoverable) —
role="alert"banner "Ingest could not start (<status>)." on non-ok; real invalid URL422preserves fields - Offline — fetch throws → "Metis is offline. Try again in a moment."; submit is a no-op (no queue)
- Loading — N/A; client-rendered form with no initial fetch
- Empty — N/A; create form
- Signed-in shell context — walked through an authenticated customer shell with the Explore nav active
- Standalone PWA — renders within
LWebShellat 390px width with no horizontal overflow
Interactions#
Source-kind selector (role="radiogroup" aria-label="Source kind")#
- URL (
<button type="button" role="radio" aria-checked>)- Function:
setKind('url'); cell shows "URL" / "archive / paper"; selected cell getsL.accentborder,L.bgbackground, italic label - Keyboard: true roving radio group; ArrowRight/ArrowDown move forward, ArrowLeft/ArrowUp move backward, Home selects URL, End selects Text
- Function:
- PDF (
<button type="button" role="radio" aria-checked>)- Function:
setKind('pdf'); cell "PDF" / "upload a file"
- Function:
- Text (
<button type="button" role="radio" aria-checked>)- Function:
setKind('text'); cell "Text" / "paste a passage"
- Function:
Conditional inputs#
- Public URL (
input#ingest-url, shown whenkind==='url')- Function: controlled
url;type=url+required(native validation) - Placeholder: "https://archive.org/details/…"
- Function: controlled
- PDF file (
input#ingest-pdf, shown whenkind==='pdf')- Function:
type=file accept=application/pdf required; on change setsfileName = files?.[0]?.name; file name echoed in mono below - Note: only the file name is sent; the file bytes are not uploaded (see gaps)
- Function:
- Passage (
textarea#ingest-text, shown whenkind==='text')- Function: controlled
text;required,minLength={60},rows={6},resize: vertical - Placeholder: "Paste a passage — sixty characters minimum so Metis has enough to chew."
- Function: controlled
Buttons#
- Back (
LBtn kind="ghost" size="lg",href="/metis")- Function: navigates to
/metiswithout submitting
- Function: navigates to
- Queue for ingest (
<button type="submit">)- Function:
submit()→ POST/v1/metis/ingestwith{ kind, url, text, fileName }(non-active fields sent asnull); on ok setsaccepted, resetskind='url', and clears fields - Keyboard: Enter submits
- Disabled when:
pending→ text "Queueing…" - Offline behavior: catch → error banner; not queued
- Telemetry: none wired; E2E asserts the form, submit button, and form
descendants expose no
data-telemetry-event,data-analytics-event, ordata-eventhook before submit and while the live BFF request is pending.
- Function:
Accepted-state buttons#
- Back to Metis (
LBtn kind="ghost" size="md",href="/metis") - Queue another (
LBtn kind="primary" size="md")- Function:
setAccepted(null)→ returns to the reset idle URL form - Telemetry: none wired; E2E asserts the accepted status card, Back-to-Metis
link, Queue-another button, and descendants expose no
data-telemetry-event,data-analytics-event, ordata-eventhook.
- Function:
Data & contracts#
- Reads: None — no initial fetch
- Writes:
POST /v1/metis/ingest(viabuildOshunBffUrl) with body{ kind: 'url'|'pdf'|'text', url, text, fileName },credentials: 'include', headerscontent-type: application/json+csrfHeaders()+ bearer auth when an API token is present- BFF handler (
apps/oshun/bff/src/routes/domain-stubs.ts) is guarded byoriginGuard,csrfGuard, andcollectionsAuthGuard; missing auth returns401 { reason: 'missing_auth_context' } - For
kind='text', the handler validates at least 60 characters, persists an owner-scoped job inmetisIngestJobStore, derives a real study outline from the submitted text, and returns202 { ingestJobId: 'ingest_<uuid>', state: 'completed', outline, pendingReason: null } - For
kind='url', the handler validates anhttp(s)URL, persists the owner-scoped reference, and returnsstate: 'awaiting_source_content'with a pending reason because fetch/extraction is deploy-bound - For
kind='pdf', the handler validates a file name, persists the owner-scoped reference, and returnsstate: 'awaiting_source_content'because the browser sends no file bytes GET /v1/metis/ingest/:jobIdreads a job scoped to the same owner and 403s on owner mismatch- Client
AcceptedResponseshape:{ ingestJobId, state: 'completed'|'awaiting_source_content', pendingReason, outline } - Client success handling stores the accepted response, resets the next source
kind to URL, and clears
url,text, andfileName
- BFF handler (
- Realtime: None. — pasted text completes synchronously in the
202response; URL/PDF reference extraction is pending/deploy-bound, not streamed - Caching: write response is
no-store - Auth/role check: shell middleware enforces signed-in; BFF also enforces origin, CSRF, and collection auth on the write
No typed contract in libs/contracts/ or libs/openapi/; shapes are inline.
E2E coverage#
apps/oshun/web/e2e/metis-byom-ingest.spec.ts— 2026-06-29 live browser test opens/metis/ingest, selects Text, submits a pasted passage to the real BFF, waits for the202, and asserts the rendered outline status cardapps/oshun/web/e2e/metis-create-forms.spec.ts— 2026-06-29 live browser route test verifies idle defaults, true radio source-kind keyboard behavior, PDF reference acceptance through the real BFF, pasted-text delayed pending and completed outline states, Queue another reset, real invalid-URL 422 handling, offline abort handling, mobile standalone layout, route-level axe scan, and no telemetry hook attributes on the ingest form, submit button, accepted status card, accepted-state action controls, and descendantsapps/oshun/web/e2e/metis-ingest-bff.spec.ts— contract-level coverage for text completion, URL/PDFawaiting_source_content, validation, auth, and scoped job reads
Cross-references#
- Feature spec:
V1/features.md#metis - Architecture:
V1/ARCHITECTURE.md#metis - Sibling Metis routes:
metis.md,metis-byom.md,metis-courses-new.md,metis-lesson.md - Journeys:
../../journeys/metis-byom-ingest-to-course.md - Component sources:
apps/oshun/web/src/app/metis/ingest/page.tsxapps/oshun/web/src/app/metis/ingest/MetisIngestForm.tsx- BFF:
apps/oshun/bff/src/routes/domain-stubs.ts
Open questions / known gaps#
- PDF is not actually uploaded — only
fileNameis posted as JSON; there is no multipart upload or presigned URL flow, so Metis has no file bytes to ingest. Confirm the intended upload path. -
The BFF— resolved: the handler now validates source kind, processes pasted text, stores owner-scoped jobs, and honestly records URL/PDF references as/v1/metis/ingeststub ignoreskind/url/text/fileNameand returns a syntheticingestJobIdawaiting_source_content. -
— resolved: the route now uses/v1/metis/ingestlacksoriginGuard/csrfGuardwhile/v1/metis/courseshas bothoriginGuard,csrfGuard, andcollectionsAuthGuard. -
The radiogroup uses— resolved: the selector now uses<button aria-pressed>rather than radio inputs; verify arrow-key roving and screen-reader announcement meet the radiogroup contractrole="radio",aria-checked, rovingtabIndex, and Arrow/Home/End keyboard behavior. - No telemetry on submit/accept:
metis-create-forms.spec.tsasserts the ingest form, submit button, accepted status card, Back-to-Metis link, Queue-another button, and descendants expose nodata-telemetry-event,data-analytics-event, ordata-eventhooks. Product decision remains whether a futuremetis_ingest_queuedevent is expected.