Plan 74 R4 Part 6 - SekiroKenjii/Quizzivy GitHub Wiki

Part 6 of 6 of R4 — Teacher workspace (v0.10.0) · Previous

T-R4.49 — R2's contract steps, if R4 ships before v0.9.1

Depends on: R2 released, and no earlier than 2026-10-10 Default (2026-10-08): this task runs. work/redesign-r4 holds 00080 to 00091, so T-R3.1 to T-R3.3 are numbered here, after them (73-r3.md, "Migration numbers"). If Thuong ships v0.9.1 separately instead, its migrations take numbers below R4's and this task first renumbers R4's merged migrations after them, with 20-data-model.md and this file's schema table. That also invalidates every development database that applied 00080 to 00091, because goose tracks versions, not names. Touches: what 73-r3.md T-R3.1, T-R3.2 and T-R3.3 touch, if R4 ships before v0.9.1; otherwise nothing Size: as T-R3.1–T-R3.3, or none Done when:

  • If R4 ships before v0.9.1 (73-r3.md, "Two versions"): 73-r3.md T-R3.1, T-R3.2 and T-R3.3 run here unchanged, each migration named with a working Down. Otherwise nothing: R3 has no expand step for R4 to contract
  • The v0.9.1 items of 73-r3.md's release checklist run inside v0.10.0's: the Neon rehearsal of the two contract migrations, the seven days of production legacy_admin_path logs (read by Thuong), and the docs sweep
  • Nothing R4 itself expanded is contracted in R4; this file's release checklist hands R5 its list

Decided 2026-10-12 (the owner): v0.10.0 ships with B (00105) and C (00106) and without A. The seven days of legacy_admin_path logs could not be read before the release, so the /admin alias (T-R3.3) stays in v0.10.0 and moves to R5, which removes it with the Admin console's /admin tree. No production-branch rehearsal was run (docs/setup/deploy.md, "Rolling out v0.10.0").

As built, T-R3.1 (PR B, 00105_drop_users_legacy_role.sql). The three steps ship as three pull requests in the order B, C, A (A waits for production evidence only Thuong can read).

  • The contract drops User.role, CurrentUser.role and Role, with make gen as its own commit. The web had no runtime read of a user's role (a grep of web/src finds none); its fixtures, client.typecheck.ts and one comment change, and a contract test pins the removal.
  • An access token carries the user, its times and the session epoch. A token minted before this release names a role, and Verify ignores it (a fixture token pins it). The docs session no longer passes a literal "admin". User and Account lose Role and the two CASE projections over student_like_roles.
  • 00105 runs in one transaction under lock_timeout 5 s: VALIDATE, then the drops (20-data-model.md §42). The previous binary, v0.9.0, never names the column, so the release command running the migration while it serves, and a rollback to it, are safe.
  • The dev seeds name role_id. expand_r2_test.go's first two tests stop at the migration before 00105 instead of running to head, so they keep proving the old binary on the expand schema; legacy_role_test.go exempts exactly the directory of migration tests. The test that a review names a staff sitter by their own role goes with the field it asserted.
  • The production gate passed on 2026-10-11 (Thuong): both machines run v25, nothing older than v0.8.0 since 2026-10-04, and the Neon rehearsal took 00105 in 469 ms with section A identical before and after (https://github.com/SekiroKenjii/Quizzivy/pull/486#issuecomment-6109429331).

As built, T-R3.2 (PR C, 00106_validate_ownership.sql).

  • 00106 runs in one transaction under lock_timeout 5 s: a DO block that names the first table with a NULL owner (23502, TABLE and COLUMN set), then VALIDATE CONSTRAINT on the five owner constraints, then the five DROP TRIGGERs and the three DROP FUNCTIONs (20-data-model.md §43). The validations hold SHARE UPDATE EXCLUSIVE; the drops hold ACCESS EXCLUSIVE for milliseconds, last, to commit. Down recreates the objects byte for byte (a test compares their definitions, owner and ACL) and leaves the constraints validated.
  • Every insert path already names its owner, and the previous binary's do too: a grep of the v0.9.0 tag finds seven INSERTs, in the classes, media, questions and tests repositories, and no other INSERT into the five tables outside seed/ and tests. The fixtures did not: 66 test inserts and the two dev seeds (02, 05) relied on the triggers, and now name owner_id, in a commit before the migration. The inserts that stay without an owner run at a schema that has no owner column yet (gap_rollback_test.go at migration 34, the rollback test of group_graph_test.go at 37) or are v0.7.0's own (expand_r2_test.go); constraints_test.go's class fixture names teacher_id where the column exists.
  • contract_r3_ownership_test.go is a file of its own, not T-R3.1's contract_r3_test.go, so PR B and PR C add no file in common. It holds: what is gone and what is kept; a row without an owner in each table stops the migration naming that table and leaves the schema as it was; a held lock makes it give up with 55P03; the previous binary's statements (inserts into each table, reads) run through the migration without an error; Down restores the objects and a bare insert is filled again; and validating blocks neither a read nor a write. constraints_test.go pins the not-validated constraints to a list and checks that an insert without an owner is refused. TestTheOwnershipExpandKeepsTheOldBinaryWorking stops at the migration before 00105, PR B's stop, which ends the expand half earlier than 00106 does and keeps users.role for the v0.7.0 inserts it replays.
  • The not-validated list in constraints_test.go is empty. It held one line, users_role_id_not_null, until PR B (00105) merged into this branch; the test fails on a stale line, so the merge removed it, and it now fails on a validation that is ever undone.
  • 00106 can stop three ways, each rolling the transaction back and leaving the schema, the version row and the v0.9.0 binary as they were: 23502 (a table has a NULL owner, named in the message), 55P03 (a lock did not arrive within five seconds) and 40P01 (the deadlock detector ended the migration instead of a transaction of the previous binary that holds two of the five tables in the opposite order). The detector may choose the other transaction, so one v0.9.0 request can be the victim and fail where the migration proceeds; the caller retries it. docs/setup/deploy.md, "If it stops", names all three, and 20-data-model.md §43 says why.
  • The production gate passed on 2026-10-11 (Thuong): both machines run v25, nothing older than v0.8.0 since 2026-10-04, and the Neon rehearsal on a throwaway branch of production went from 79 to 106 with 00106 taking 798 ms, section A of docs/setup/rehearsals/r4-ownership.sql identical before and after, all five constraints validated, no trigger or function left, and no constraint unvalidated (https://github.com/SekiroKenjii/Quizzivy/pull/491#issuecomment-6109430067).

T-R4.50 — Docs

Depends on: the tasks whose behaviour each part describes Touches: docs/quizzivy-spec-v0.3.md (§2, §5.4, §6, §7.1, §8, §10.4, §11, §13, §15, §16; version bumped per §18), AGENTS.md, docs/plan/17-word-import.md, docs/plan/18-word-import-ux.md, docs/plan/20-data-model.md, docs/plan/30-risks.md, docs/plan/40-open-items.md, docs/plan/70-redesign-overview.md, docs/setup/dns.md, docs/setup/operations.md, docs/setup/word-import-worker.md Size: S Done when:

  • Spec: §2 and §5.4 the teacher tree at /teacher (the three route trees, what TeacherWorkspace holds, where StudentArea and homePathFor send staff, and /admin/* redirected until R5); §6 codes shown in full, the rotation done, expiry at 23:59; §8 the teacher routes and screens with their "Availability: R4" lines flipped; §10.4 the teacher's integrity view per D8; §7.1 rich prose is how a prompt and an explanation are written, no longer an opt-in behind VITE_RICH_QUESTION_EDITOR, the two-way Markdown switch, tables and strikethrough in Markdown, and a pasted image is left out and counted instead of refusing the paste; §11 replace, rename, default play limit, the audio size, an image as question media and its alt text; §13 and §15 the new columns, tables and operations; §16 the pasted-text source beside Word and PDF (the operation, the 100,000 code-point limit, plain text only, the conventions read for text sources, and the one-line answer key read in Word and PDF files too, #233)
  • AGENTS.md: repository map (features/notifications, modules/notifications, pages/teacher/), the teacher shell facts (sidebar never collapses by itself, content-width breakpoints, 768 drawer), the "Redesign in progress" note, canary paths if any moved
  • 20-data-model.md: every R4 migration and its deviation rows; 18-word-import-ux.md: the deck layout, the paste mode, the history's filters, grid and cards, and what DG-67 kept or gated; 17-word-import.md: §1.41 rewritten from planned to built, in the manner of §1.39 (the "Nothing here is built yet" line removed; the intake, the extractor and its evidence version, the conventions, the versions and the measurements recorded as shipped), D-11 checked against it; docs/setup/word-import-worker.md: text sources need no converter, and the migration floor; 30-risks.md: PR-6 closed; 40-open-items.md: this file's open items as O-R entries; docs/setup/dns.md: Cloudflare's visitor location headers for DG-58
  • operations.md: the legacy rotation runs once at API start-up under (73819, 41); the operator supplies JOIN_CODE_KEY and JOIN_CODE_KEY_PREVIOUS to cmd/maintenance from the offline copy for R2's rekey; window-schedule also moves override close times
  • Every PR that changed behaviour earlier in the release already changed its lines; this task checks the set and closes gaps before the release

As built: two pull requests. The server half (#516, spec v0.85) brought §6, §11, §13, §15 and §16, 17-word-import.md §1.41 and D-11, 18-word-import-ux.md §12, 20-data-model.md D-40 and D-41, word-import-worker.md, operations.md and 30-risks.md level; dns.md and most §15 lines were already written by the PRs that changed them. The Tech Lead's half (spec v0.86) wrote §2, §3, §5.4, §7.1, §8, §9's overlays, §10.4 and §12's console lines, AGENTS.md, 70 (§5's /admin/* row, §6's storage keys, PR-6) and the O-R4 entries in 40-open-items.md. Still to be recorded by T-R4.51: the cold worker's time (DG-16) and the local-corpus run for #233, which §1.41 names.


T-R4.51 — Browser verification against the deck

Depends on: T-R4.23 … T-R4.48, T-R4.57, T-R4.63 … T-R4.66 Touches: seed/08-dev-teacher-workspace.sql (new, development only), seed/99-assert.sql, seed/README.md and, for #139, web/tests/e2e/import.live.spec.ts (new), .github/workflows/ci.yml Size: M Done when:

  • The dev seed holds data shaped like the deck's fixtures: seven tests (one with a draft ahead and three versions), eight media files, four classes with members, assignments in every status, a flagged attempt, answers waiting to be graded, four imports in different states, one of them pasted text in review. One test holds a question of each of the five types: a rich prompt with a table and a link, a prompt with two gaps, a prompt still stored as Markdown, a question with audio and one with an image and its alt text. Its number follows R2's 06-dev-access.sql and 07-dev-classroom.sql, whose teachers it relies on, and its header says so Observed 2026-10-12, not audited row by row (docs/team/verification-r4-51.md, section 9): the shape is there; the stale ten-year lease on the processing run held the worker's slot (QA-51-7, F-65, fixed on #518; retested closed at 5ac243ee, section 14.7). Left unticked: T-R4.51a's.
  • The teacher skeleton registered with R1's splash hand-off (T-R4.4) is compared with Quizzivy Splash.dc.html#skeleton at the destination prop teacher, at 360 and 1280, light and dark Verified 2026-10-12 (docs/team/verification-r4-51.md, section 9): 32 and 56 blocks at 360 and 1280, no geometry delta, equal gradient and pixels, light and dark.
  • Every /teacher/* route compared with design-deck on 5175 at 360, 768, 1024, 1280 and 1440, light and dark, sidebar expanded, collapsed and as a drawer; the dev server confirmed fresh; sizes measured with getBoundingClientRect Compared 2026-10-12 against 86f21905, 720 product cells and the deck's shell (docs/team/verification-r4-51.md, sections 4 and 5). Compared, not matching: QA-51-1 (the document scrolls beside main, ten routes), QA-51-2, QA-51-3, QA-51-4, QA-51-5. Retested 2026-10-12 at 5ac243ee (section 14): QA-51-1 to QA-51-8 closed (#518, #523, #524); open, all minor: QA-51-9 (Brand warning), QA-51-10 (partly fixed, #521), QA-51-11 (the history's pager), QA-51-12 (line-heights), QA-51-13 (the formats order).
  • Where a Teacher-deck heading sits 6px lower than the product's, the comparison cites DG-122 and does not open a new gap: sixteen of the Teacher deck's 24px h1s set no line-height and inherit the page root's 1.5, as the Sign in deck's do (DG-117) Cited, no gap opened (docs/team/verification-r4-51.md, section 6). The Grading heading kept 1.5 (QA-51-5); closed on #524, retested at 5ac243ee: 30 px like the other fifteen.
  • Imports: the history shows cards at 360, 768 and 1024 and the grid at 1280 and 1440, with the sidebar expanded and collapsed, the 1280 expanded case also with scrollbars shown at a viewport short enough for the page to scroll, and never scrolls sideways; /teacher/imports/new is compared in both Source modes, the paste mode empty and with text Layout verified 2026-10-12 (docs/team/verification-r4-51.md, section 9): cards at 360, 768 and 1024, grid at 1280 and 1440, both sidebar states, 1280x600 with scrollbars, no sideways scroll. The history and the new import page differed from the deck: QA-51-2, QA-51-3; closed on #523, retested at 5ac243ee (widths 360, 768, 1280, 1440, both sidebar states, light and dark, both Source modes), every remaining difference a cited DG apart from QA-51-11, QA-51-12 and QA-51-13 (section 14.4, 14.5).
  • The content editor is compared with the deck in the builder's "Prompt" and "Explanation" and in the review's "Question": Rich text and Markdown, Write and Preview, a table with its row of tools, a gap, the link popover, the notice band and both dialogs. A passage with a table, a list and bold text is copied from Word and from Google Docs and pasted into "Prompt" in Chromium, Firefox and WebKit, through "Preview content after pasting"; a passage with a picture is pasted from each and shows the left-out line Compared in Chromium 2026-10-12 (docs/team/verification-r4-51.md, section 9): frame, toolbar, modes, band, table row, gap, popover, paste dialog and the review's Question equal to the deck's in size at five widths, light and dark (QA-51-6, minor; closed on #524, retested at 5ac243ee: the popover equals the deck's at five widths, both themes). Left unticked: Word and Google Docs in Chromium, Firefox and WebKit, not run.
  • Pasting for real: a test copied from Word and one from Google Docs are pasted into the box, and "Paste from clipboard" is tried allowed, refused and empty, in Chromium, Firefox and WebKit. What the plain-text clipboard keeps of automatic numbering and option letters is recorded (open item) Chromium "Paste from clipboard" allowed, empty and refused (injected) done; Word, Google Docs, Firefox, WebKit and O-R4.19 not run, needs Thuong (docs/team/verification-r4-51.md, sections 9 and 10).
  • The deck's sample is pasted and started with a cold worker (idle, woken by the API), and the time from Start processing to the Ready card is recorded. At 10 s or less the paste footer takes the deck's "Processing takes a few seconds."; otherwise the file flow's sentence stays and DG-16 records the measurement Measured 2026-10-12: 2.33 to 2.42 s in ten runs (docs/team/verification-r4-51.md, section 7); DG-16 records it. The deck's footer sentence was owed in the product, both languages (QA-51-8); closed on #524, retested at 5ac243ee.
  • The live E2E job builds and starts cmd/import-worker beside the API, with the converter image CI already builds for the Go pipeline test, and import.live.spec.ts takes a synthetic .docx exam and key (generated in the test, never a real paper) from the rebuilt Imports page through the worker to the review and to a draft whose questions carry their keys (#139) Done by #513 (a598a44d); read at the merge, not run here (docs/team/verification-r4-51.md, section 9).
  • Deviations fixed in the owning task's branch or filed as issues; Vite and the API stopped afterwards Filed 2026-10-12 as QA-51-1 to QA-51-10 in docs/team/verification-r4-51.md. Retested 2026-10-12 at 5ac243ee (section 14): QA-51-1 to QA-51-8 closed; left unticked because QA-51-9, QA-51-10 (partly fixed), QA-51-11, QA-51-12 and QA-51-13 are open, all minor. Vite, the API, the worker and the deck server were stopped and the scratch databases dropped after each pass.

T-R4.51b — Publish snapshot fixture lifecycle

Depends on: — (verification follow-up; does not replace T-R4.51) Touches: server/internal/modules/tests/application/tests/{publish_harness_test.go,mediakinds_test.go,publish_snapshot_test.go,publish_fixture_lifecycle_test.go,publish_fixture_probe_failure_test.go} Size: S

The five single-session snapshot cases used a committed Admin fixture whose cleanup ignored errors and attempted audit deletion. In an isolated database it could leave the last Admin behind. Their setup now owns a genuine Teacher and publication inside one outer transaction; the production application still begins and commits its nested savepoints. The snapshot commands, names, expected values and behavioral assertions are byte-identical after reversing only the five setup substitutions and removing four unused setup locals. This is a disclosed fixture-only change to a canary file; release evidence must not call the whole file byte-identical. The other four canary files are unchanged by this task.

Done when:

  • Cleanup is registered before the first insert; exact owned identities are captured, rollback errors are checked, and absence is verified before the pool closes. No audit or attempt-event UPDATE/DELETE, privilege, role, trigger or production change
  • Normal and actual Fatal-subprocess publication prove scoped rollback. A transient absence-query error suppresses the success marker and is refused by the same Fatal verifier even when later parent reads succeed; the new regression failed before repair
  • Independent source review and QA approve the exact repaired surface: eight top-level integration cases and one lifecycle subcase pass on PostgreSQL18.6 with the actual app role, zero parent failures/skips. Expected child failures remain negative evidence
  • Scoped vet/staticcheck/format pass. Independent before/after59 app-table counts agree with the clean purpose baseline, app append-only ACL is unchanged, and connections close. The purpose database is intentionally preserved; counts are not a full-row/storage proof
  • Exact-head CI result passes and the task PR merges into work/redesign-r4

The original committed pubMakeAuthor helper stays byte-identical for its other callers. Its ignored-error/audit-delete cleanup and their committed-concurrency lifecycle remain separate work; these single-session snapshots do not prove committed visibility or lock ordering. No historical/quarantined data repair, database teardown, main merge or deployment belongs here.


T-R4.52 — Release v0.10.0

Depends on: T-R4.1 … T-R4.51, T-R4.53 … T-R4.57, T-R4.62 … T-R4.66 Touches: docs/plan/, release notes Size: S Done when:

  • The release checklist below is complete, with Thuong's go before main

F-37 — Composed text on the server

Touches: server/internal/shared/content/, every module's write commands, migrations/00103 As built (2026-10-10, Thuong's go for the backfill): the server stores the text a person types composed to NFC (spec v0.75, §12 and §13), so a title, a name or a note no longer falls back to another font in a plain field.

  • content.Normalize composes a prose document (text, label, alt, a legacy document's markdown) and returns an already composed one byte for byte; content.NFC, NFCPtr, NFCAll and NFCWithin compose a plain text; content.Composer composes the plain text of one write and keeps the fields that composing leaves over their limit. Each module's input composes itself (Input.Composed, UpdateInput.Composed, GroupBundle.Composed, Answer.Composed) and its command handler calls that first, after the contract has validated the body as typed; repositories stay exact. A document and its companion text are composed together, so PlainText() still equals the text kept beside it. preserveProse compares under NFC and, on a match, composes the stored pair, so an old decomposed row accepts the composed text of a client that sends no document.
  • A text that composing leaves over its limit (a few scripts, U+0958 among them, grow) is answered as any other invalid value: 400 VALIDATION_FAILED naming the field, 422 group_content for a group. The operations that had no mapping (createTest, publishTest, createClass, updateClass, createStudent, updateStudent, setAttemptNote, and the import operations, a commit included) got one, through httpapi.Invalid. A name that comes from a provider (EnrolNewMember) or a file (an import source, a media file name) is kept as it came instead, and a student's save is never refused over it.
  • Never composed: emails, passwords, tokens, join codes, ids, gap and asset ids, links, storage_key, enums, the published versions and their change notes, attempt_events, audit_log, and the rows of a student's saved answers written before.
  • 00103_compose_text_nfc.sql is data only. It rewrites the plain columns a person types, where they are not composed, and leaves a value that composing would take past its CHECK. It leaves every prose document and the plain text kept beside one (a write checks that the text equals the document's projection, which SQL cannot derive): those are composed when next saved and the web composes them where it draws. For the eight tables that have an updated_at trigger it disables that one trigger by name and enables it again before the transaction ends, so updated_at, the autosave token an open editor holds, does not move; that takes SHARE ROW EXCLUSIVE on those tables for the milliseconds the transaction lasts at production size, under lock_timeout 5s. It deletes the decomposed twin of an accepted answer before composing, because (blank_id, answer) is unique. Its Down is a no-op. The binary that was running keeps writing decomposed text until the new one takes over; the display composes it, and the next save composes it.
  • Tests: shared/content/tests/compose_test.go; each module's compose_test.go (domain and application, with recording fakes) and, with a database, questions/application/tests/ compose_integration_test.go (the stored-pair rule) and media/application/tests/ compose_integration_test.go; platform/db/tests/nfc_backfill_test.go (every column the migration lists and every column it must not touch, updated_at, the triggers, a second run and the Down); server/tests/nfc_write_flow_test.go (four flows over the whole application). The grader's own test, TestFillBlankIgnoresUnicodeComposition, is unchanged.

Release checklist

Decided 2026-10-12 (the owner): v0.10.0 is released to Fly as v0.9.0 was. A step that needs infrastructure access this team does not have, or the owner's laptop, does not block it; each such line below says so. In particular:

  • No Neon rehearsal on a branch of production. The migrations passed up, down to 00079 and up again on a seeded scratch database. The runbook is docs/setup/rehearsals/r4-neon-rehearsal.md.
  • The api record stays DNS only (grey) with CLIENT_IP_HEADER = "Fly-Client-IP" (hotfix #431, merged into R4 by #525). The DG-58 line below does nothing on a grey record and is struck: Signed-in devices shows no location until the record is proxied and the header changed with it (docs/setup/dns.md).
  • The /admin alias stays (T-R4.49 A to R5).
  • The deploy target is Fly. The VPS path (#434) is later work.

Status at the release (2026-10-12). Ticked lines are done; the others say why not.

  • CI. Not repeated locally, as for v0.9.0: CI has been the full gate since 2026-10-04, every R4 pull request merged on it, and the push to main runs every job before the deploy.

  • Canaries. All six are green.

    • audio-player.test.tsx, autosave-unmount.test.tsx and events_test.go are identical to v0.9.0.
    • client.refresh.test.ts lost one fixture line (role, T-R4.49).
    • publish_snapshot_test.go changed its fixture setup only (T-R4.51b).
    • router-chunks.test.ts names the teacher shell, refactors a helper and adds one test.
    • No assertion changed anywhere.
  • Security. The release review's access lane passed the isolation suite (22 new operations), the credential limits, STUDENT_SHARED and no-store. Its fixes are #527. The /me and leak walks it asked for were added there.

  • Not run, by the owner's decision. The Neon rehearsal and EXPLAIN on a branch of production, the exam-window and import-queue queries on production, and the Cloudflare step (the record is grey).

  • Left with the owner. The phone smoke of the fixes, the v0.9.0 checks, and Neon compute hours for the week after.

  • Hand-offs. Recorded in the ledger and in 40-open-items.md (O-R4.21 to O-R4.23); R5's and R6's planning take them.

  • Deploy. The first deploy (4abfe63d, run 38238171020) applied 00080 to 00106 and started the worker, but the API refused to start: the image's import work directory is 0755 (INC-5 in the ledger). The API was down from about 10:45 to 11:20 UTC, and Pages kept v0.9.0 meanwhile. Hotfix #532 (e06511fc, run 38241790867) deployed both; the API's health check passes since 11:20 and Pages serves 0.10.0 since 11:21. v0.10.0 tags e06511fc. The API log was not read (the deploy token cannot), so legacy_join_codes_rotated and the smoke stay with the owner.

  • 70 §8.1: every CI step locally, in order, judged by exit code — make lint, make gen-check, make test-api, goose up/down/up, pnpm lint, pnpm typecheck, pnpm format:check, pnpm test:unit, pnpm test:integration, pnpm build, pnpm e2e, pnpm e2e:content, pnpm e2e:live (join codes, imports and the take-test start path changed), node scripts/check-design-deck.mjs

  • Canaries green and untouched: publish_snapshot_test.go, audio-player.test.tsx, attempts/application/tests/events_test.go, client.refresh.test.ts, router-chunks.test.ts (regexes updated only); builder/autosave-unmount.test.tsx green

  • Security: the isolation suite covers every new /teacher/* operation with an x-resource entry for every uuid in its path, query or body (kinds notification and override added), and its two-user pair covers /me/*; the payload-leak walk covers /me/notifications, the result's class average and the note to students; the credential-limit test lists resetStudentsPasswords and listClasses; the STUDENT_SHARED escalation case passes; /me/* responses are no-store

  • Migrations rehearsed on a Neon branch of production: timings recorded; questions with more than eight options reported; the number of legacy join codes to rotate recorded; with T-R4.49, the two contract migrations rehearsed and the legacy_admin_path count read

  • On the same Neon branch, EXPLAIN (ANALYZE) of getTeacherSummary and getDashboard for a teacher with the most attempts shows the five indexes T-R4.19 names (QA-F1-3, PR #397)

  • Exam windows checked: no assignment closing within the deploy window

  • No import run is queued or running at the deploy: word-pipeline-v3 retires a v2 run, which fails PIPELINE_RETIRED after the ten-minute grace and is retried by its teacher

  • Cloudflare "Add visitor location headers" enabled before the deploy (DG-58) struck: the record is grey (above); .env.example and Fly config carry MEDIA_OWNER_QUOTA_MIB; VITE_RICH_QUESTION_EDITOR is gone from .env.example and from the Pages build's variables

  • Deploy API, then Pages; the API log shows legacy_join_codes_rotated with the rehearsed count; smoke /healthz, /livez, a sign-in as the owner (lands on /teacher) and as a student; the owner builds, publishes and assigns a test, writing one prompt with a table and attaching an image with alt text, opens an import in review, pastes a test and reaches its review, and grades one answer; an old /admin/tests bookmark lands on /teacher/tests

  • Smoke of the fixes develop has gathered since v0.9.0, which first ship here: a student on a phone answers one question in airplane mode, closes the tab, turns the network on and presses Continue, and the answer is there (#330); a reload of the engine opens the question that was open (#320); the owner's roster shows a recording's end and the events of a student who left the test with the ✕ (#314, #331)

  • The checks v0.9.0 left with the owner are done here if they are still open: a sign-in as an admin and as a student on production, a short test on a phone and on a laptop, and SELECT has_database_privilege('quizzivy_app', current_database(), 'TEMPORARY') answering false (#194, docs/setup/operations.md)

  • Release notes: the teacher workspace moved to /teacher and old links redirect; every class has a new join code — share it with students who have not joined yet; students already in a class are not affected; the bell and notification settings; per-student extensions and reopening; the version history and change notes; media rename and replace; a test can be pasted as text, with the marks the page lists for answers; the question editor formats text, tables and links, keeps formatting pasted from Word and Google Docs, and attaches an image as well as audio

  • Release notes, the fixes since v0.9.0 (no fix-only release was cut, by Thuong's decision of 2026-10-04): for students, answers typed without a network are kept when the student comes back through Continue (#330), the engine reopens on the question that was open (#320), the timer stops when time is up (#329), and a recording shows one length in the test and on the result (#321); for teachers, the timeline shows when a recording ended or was blocked and keeps the events of a student who leaves the test (#314, #331); for everyone, a sign-out no longer leaves "sign in again" over the sign-in page (#328)

  • Hand-off to R5: remove the legacy join-code lookup branch; give /admin/* to the Admin console and narrow LegacyTeacherRedirect to known teacher paths; drop openAssignments, activeStudents, totalStudents and recentAttempts from Dashboard (T-R4.19); grant quizzivy_maintenance SELECT, UPDATE (closes_at) on assignment_student_overrides in grant_maintenance_role (T-R4.12); rename the admin-* query keys (24, about 140 uses) and the file names that still say "admin" in one mechanical commit after v0.10.0, with a unit test that refuses a query key starting "admin-", since a partial rename can miss an invalidation (T-R4.48)

  • Hand-off to R6: T-R6.13's tfng mapping covers pasted text as it covers files; the builder's "Matching" and "Include “Not given”" (DG-113); images and audio inside a prompt or an explanation, if Thuong confirms the open item's default (DG-109)

  • Neon compute hours measured for the week after the release and recorded against NFR-A10 (PR-7)

  • release/0.10.0 → main --no-ff with Thuong's go; tag v0.10.0; back-merge to develop

Open items

Moved to 40-open-items.md as O-R4.1 to O-R4.20 at the release (T-R4.50), with what v0.10.0 built; the text below is as the release planned it.

  • Audio length and WAV (DG-63, spec §11.1). R4 raises the audio size cap to 50 MiB and keeps MP3/M4A and the five-minute limit; the deck's "MP3 or WAV" copy is written as "MP3 or M4A". A listening section longer than five minutes, and WAV, need Thuong's answer before the cap on duration moves. The 2026-10-04 builder adds "MP3, M4A or WAV up to 25 MB" for question media and GIF for an image in the text. Default: every screen states DG-63's limits, and GIF is not taken.
  • Images and audio inside a prompt or an explanation (DG-109, D1). The 2026-10-04 editor draws "Image" and "Audio" in its toolbar and an image pasted or dropped into the text. The contract's QuestionContent holds no media. Building it means a contract change, asset references inside content in the publish snapshot and in media replace and delete, signed URLs in the student payload, and the engine and the result rendering them, with a rule for how inline audio is limited and recorded. Default: not in R4; T-R4.63 leaves the two buttons out and refuses a pasted or dropped file. It is built, as D1 requires, in R6, whose track already runs through those areas; 76-r6.md gains the tasks when Thuong confirms. The alternative is to build it in R4, as at least three more tasks.
  • A pasted image no longer refuses the paste (DG-115, spec §7.1). The deck's paste preview says "{n} images in the copied content were left out.". Today clipboardHTML refuses a paste that holds an image, clipboard.test.ts pins it, and spec §7.1 says to reject it as one paste. Default: T-R4.63 leaves images out and counts them, keeps every other refusal, and T-R4.50 changes the spec line. The alternative keeps the refusal and does not build the left-out line. Thuong confirms.
  • "Choose from Media" lists own files only (DG-112, DG-90). The dialog's sub speaks of files "other teachers shared with you", and media is not a shared kind. Default: the caller's own files and the sub "Files you uploaded."; shared files wait for R7's sharing to cover media and for a drawing.
  • The rich editor for every teacher. VITE_RICH_QUESTION_EDITOR is off in production. Default: R4 removes the flag and ships the editor as the deck draws it (T-R4.64; removed in T-R4.64d); rich option text stays behind VITE_RICH_OPTION_EDITOR, off, because the deck draws plain option inputs. Thuong confirms the first, and whether rich options are ever turned on.
  • What the Markdown mode is (DG-110). Default: the mode is the stored form, nothing converts without the teacher's confirmation, and Markdown gains tables and strikethrough through the micromark and mdast extensions for the two, behind one plugin: up to four direct dependencies, which also load on the student's path.
  • The bank's Question editor was not redrawn (DG-108). Default: T-R4.33 builds the builder's 2026-10-04 drawing of the form on that page, and keeps the page's own frame. A later export that redraws the screen is followed.
  • "Students can pause", "Matching" and "Not given" (DG-111, DG-113). None has a model in R4. Default: "Students can pause" is not built in R4, and the two existing playback switches stay; "Matching" and the "Include “Not given”" switch arrive in R6 with their types. D4 builds "Students can pause" as allowPause (DG-183): T-D4.13 stores and serves it, T-D4.9 acts on it in the engine and T-D4.15 draws the switch (74d-d4-deck-update.md, Q4).
  • Alt text for a question's image (DG-111). Default: T-R4.62 adds one nullable column to the question and its snapshot. The alternative is alt text on the media file, set once in the library; the deck draws it on the question.
  • The review's editor (DG-114). Default: T-R4.38 keeps gaps, stored content and the read-only view, and builds no Markdown mode there. The deck draws the opposite on each point, in a prototype that saves plain text.
  • Join codes without a use limit (spec §6, O-06). The deck's "Leave empty for no limit" conflicts with O-06's default of 40 and the non-nullable request field. R4 keeps O-06.
  • Username sign-in (spec §5). The deck's Add student asks for "Username or email"; sign-in is by email, so R4 asks for Email.
  • Question import in the bank (D1). The deck's Import dialog (CSV, XLSX, .quizzivy, a tag for every question, "Skip questions that already exist") has no release in 70, and D1 says what the deck draws is built. Default: the button stays absent in R4, and the dialog with POST /teacher/questions/import (content.questions.write, formula-safe, bounded in the API or run by the worker) goes to a named release: R5 after T-R5.13 brings XLSX, or R6 after T-R6.3 so the importer knows every type. Otherwise Thuong records a D1 exception in 70 and a DG entry.
  • Import controls without a backend (DG-67, D1). Saved profiles, source conventions, the Word template and the audio slot, plus "Create another test from this file", "Delete import", "Process again" after a cancel, Move / Split / Merge and the processing history. No release R5–R11 has their backend yet. Default: they stay absent in R4; their backend and UI tasks go to a named release (R6 beside T-R6.13, or R11's reconciliation), or Thuong lists them as an approved post-1.0 exception in 70 §2 and DG-67's "Needed by" moves to it.
  • Note to students. The Student deck's Test intro draws a teacher note; the Teacher deck draws no field to write it. Default: T-R4.11 adds assignments.student_note and the contract, and T-R4.26 and T-R4.27b add a plain "Note to students (optional)" field, placed by a new DG entry in docs/design/gaps.md. If Thuong decides not to build it, T-R4.11's note bullet goes and the element is recorded as not built.
  • Resetting or re-addressing a shared student (security posture). A teacher may reset, or give a new email to, only a student no one else reaches, by T-R2.13's rule (72-r2.md): every class of theirs, archived ones included, is the teacher's; no other account created them; no other account's assignment targets them individually; and with no class, the teacher created them. Anything else answers STUDENT_SHARED and goes to an admin (T-R2.13, T-R4.20). Thuong confirmed the refusal, the safer choice, on 2026-09-28.
  • Wave 2's defaults (2026-10-04). Seven decisions the wave-2 briefs took where the plan and the deck leave a choice; each is built unless Thuong says otherwise.
    • T-R4.63: a paste that carries a file beside HTML text (an Excel or Word selection) is previewed, and Ctrl+Shift+V inserts its plain text. The alternative refuses any paste that carries a file.
    • T-R4.22: a rotated legacy code answers through today's code paths, with no special case: "revoked, ask your teacher" where self-join is open, the unknown-code answer where it is closed or the class archived (spec §6).
    • T-R4.2c: the stepper's value can be typed, so a 47-minute limit stays possible although the deck draws only − and +.
    • T-R4.2c and T-R4.66: "nghe" and "nghé" can both be entered as accepted answers, and "London" and "london" under Match case, because the server grades them as different.
    • T-R4.17b: an import in review that names a replaced file keeps the old file and binds it at commit; the file is live but outside the library.
    • T-R4.22: a class still unrotated after its one retry waits for the next API start-up, so the release check re-runs the count query after the deploy.
    • T-R4.16: a draft whose group graph cannot be read answers unpublishedChanges null, so the builder shows no banner for it.
  • The Admin's own-row lists (DG-53, D3). In the teacher workspace the Admin's content lists show only their own rows; everything stays reachable by id and, from R5, in the Admin console (T-R4.54). Default: build it so; Thuong may prefer an "All teachers" filter, which would be a deck change. As built (T-R4.54), an Admin who opens another teacher's draft by id cannot add a question to it: the picker lists the Admin's bank, a new question is the Admin's, and a draft accepts only its owner's questions (T-R2.12a), so the save answers "The test refers to a question that does not exist." Default: leave it; T-R4.31b builds the picker for the owner. Both alternatives need the contract, which names no owner on a test: an owner flag so the builder hides its add controls, or a bank list scoped to the draft's owner.
  • Student activity (DG-54). "Not signed in", a true "Last active" and the password's "Last changed" line wait for R5's activity tracking.
  • What a paste keeps of automatic numbering. Whether the plain-text clipboard of Word and Google Docs keeps auto-numbered question numbers and option letters is not known, and it decides how well a pasted test recognises. T-R4.51 records it with real documents. Default: nothing changes, since the page's tips already ask for typed numbers and letters.

Part 6 of 6 of R4 — Teacher workspace (v0.10.0) · Previous


Generated from docs/plan/74-r4.md. Edit that file in the repository; changes made on this page are overwritten.

⚠️ **GitHub.com Fallback** ⚠️