Plan 74 R4 Part 6 - SekiroKenjii/Quizzivy GitHub Wiki
Part 6 of 6 of R4 — Teacher workspace (v0.10.0) · Previous
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.mdT-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 productionlegacy_admin_pathlogs (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.roleandRole, withmake genas its own commit. The web had no runtime read of a user's role (a grep ofweb/srcfinds none); its fixtures,client.typecheck.tsand 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
Verifyignores it (a fixture token pins it). The docs session no longer passes a literal"admin".UserandAccountloseRoleand the twoCASEprojections overstudent_like_roles. - 00105 runs in one transaction under
lock_timeout5 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.goexempts 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_timeout5 s: aDOblock that names the first table with a NULL owner (23502,TABLEandCOLUMNset), thenVALIDATE CONSTRAINTon the five owner constraints, then the fiveDROP TRIGGERs and the threeDROP 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.0tag finds sevenINSERTs, in theclasses,media,questionsandtestsrepositories, and no otherINSERTinto the five tables outsideseed/and tests. The fixtures did not: 66 test inserts and the two dev seeds (02,05) relied on the triggers, and now nameowner_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.goat migration 34, the rollback test ofgroup_graph_test.goat 37) or are v0.7.0's own (expand_r2_test.go);constraints_test.go's class fixture namesteacher_idwhere the column exists. -
contract_r3_ownership_test.gois a file of its own, not T-R3.1'scontract_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 with55P03; 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.gopins the not-validated constraints to a list and checks that an insert without an owner is refused.TestTheOwnershipExpandKeepsTheOldBinaryWorkingstops at the migration before 00105, PR B's stop, which ends the expand half earlier than 00106 does and keepsusers.rolefor the v0.7.0 inserts it replays. - The not-validated list in
constraints_test.gois 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) and40P01(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, and20-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.sqlidentical 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).
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, whatTeacherWorkspaceholds, whereStudentAreaandhomePathForsend 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 behindVITE_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 suppliesJOIN_CODE_KEYandJOIN_CODE_KEY_PREVIOUStocmd/maintenancefrom the offline copy for R2's rekey;window-schedulealso 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.
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.sqland07-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 at5ac243ee, 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#skeletonat thedestinationpropteacher, 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 withdesign-deckon 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 withgetBoundingClientRectCompared 2026-10-12 against86f21905, 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 besidemain, ten routes), QA-51-2, QA-51-3, QA-51-4, QA-51-5. Retested 2026-10-12 at5ac243ee(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 at5ac243ee: 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/newis 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 at5ac243ee(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 at5ac243ee: 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 at5ac243ee. - The live E2E job builds and starts
cmd/import-workerbeside the API, with the converter image CI already builds for the Go pipeline test, andimport.live.spec.tstakes a synthetic.docxexam 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 at5ac243ee(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.
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.
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
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.Normalizecomposes a prose document (text,label,alt, a legacy document'smarkdown) and returns an already composed one byte for byte;content.NFC,NFCPtr,NFCAllandNFCWithincompose a plain text;content.Composercomposes 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, soPlainText()still equals the text kept beside it.preserveProsecompares 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_FAILEDnaming the field, 422group_contentfor a group. The operations that had no mapping (createTest,publishTest,createClass,updateClass,createStudent,updateStudent,setAttemptNote, and the import operations, a commit included) got one, throughhttpapi.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.sqlis 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 anupdated_attrigger it disables that one trigger by name and enables it again before the transaction ends, soupdated_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, underlock_timeout5s. 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'scompose_test.go(domain and application, with recording fakes) and, with a database,questions/application/tests/ compose_integration_test.go(the stored-pair rule) andmedia/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.
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
apirecord stays DNS only (grey) withCLIENT_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
/adminalias 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
mainruns every job before the deploy. -
Canaries. All six are green.
-
audio-player.test.tsx,autosave-unmount.test.tsxandevents_test.goare identical to v0.9.0. -
client.refresh.test.tslost one fixture line (role, T-R4.49). -
publish_snapshot_test.gochanged its fixture setup only (T-R4.51b). -
router-chunks.test.tsnames 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_SHAREDandno-store. Its fixes are #527. The/meand leak walks it asked for were added there. -
Not run, by the owner's decision. The Neon rehearsal and
EXPLAINon 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.0tagse06511fc. The API log was not read (the deploy token cannot), solegacy_join_codes_rotatedand 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.tsxgreen -
Security: the isolation suite covers every new
/teacher/*operation with anx-resourceentry for every uuid in its path, query or body (kindsnotificationandoverrideadded), 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 listsresetStudentsPasswordsandlistClasses; theSTUDENT_SHAREDescalation case passes;/me/*responses areno-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_pathcount read -
On the same Neon branch,
EXPLAIN (ANALYZE)ofgetTeacherSummaryandgetDashboardfor 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-v3retires a v2 run, which failsPIPELINE_RETIREDafter 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.exampleand Fly config carryMEDIA_OWNER_QUOTA_MIB;VITE_RICH_QUESTION_EDITORis gone from.env.exampleand from the Pages build's variables -
Deploy API, then Pages; the API log shows
legacy_join_codes_rotatedwith 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/testsbookmark lands on/teacher/tests -
Smoke of the fixes
develophas 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
/teacherand 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 narrowLegacyTeacherRedirectto known teacher paths; dropopenAssignments,activeStudents,totalStudentsandrecentAttemptsfromDashboard(T-R4.19); grantquizzivy_maintenanceSELECT, UPDATE (closes_at)onassignment_student_overridesingrant_maintenance_role(T-R4.12); rename theadmin-*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
tfngmapping 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-ffwith Thuong's go; tagv0.10.0; back-merge todevelop
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
QuestionContentholds 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.mdgains 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
clipboardHTMLrefuses a paste that holds an image,clipboard.test.tspins 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_EDITORis 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 behindVITE_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 withPOST /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_noteand the contract, and T-R4.26 and T-R4.27b add a plain "Note to students (optional)" field, placed by a new DG entry indocs/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_SHAREDand 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
unpublishedChangesnull, 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.