Proof-of-work rather than a puzzle, and — the reason for it — nothing
about the person signing up is described to a third party in order to let
them in. Turnstile and then hCaptcha were both here; both told Cloudflare
who was at the door.
The `cap` service runs on the compose network with its own Redis
database, kept apart from the app's so a flush of one cannot clear the
other's challenges. The widget talks to /cap/ on this origin, proxied by
the frontend's nginx, so the browser reaches nobody else either. Caddy
passes the whole host through to that container, so it needed no change.
Two things that had to be found rather than read:
Cap's key API is undocumented. The routes are `/auth/login` and
`/server/keys`, and the Bearer value is base64 JSON of `{token, hash}` —
not the session token itself, which is why the obvious call returns
"Malformed session token". The site key and secret were created that way
rather than by hand in a dashboard.
And an nginx proxy_pass whose target is a variable passes the URI through
untouched: the trailing slash that strips a location prefix on a literal
target does nothing. Cap was being asked for /cap/<key>/challenge and
answering NOT_FOUND until the prefix was stripped by an explicit rewrite.
Verified end to end against the running service: a challenge is issued
through the public path, and a token that was never issued is refused
rather than waved through.
Also here: the register modal's Name and Email were bare labels that
neither wrapped their input nor named it, so a screen reader met two
boxes with no names and clicking the word did nothing.
And the knowledge profile paginates ten to a page and expands each row to
its two bars beside the next step. "Correct using hints" is missing from
that bar because /study-tools/recommendations does not carry it per
topic — inferring it from the lifetime figure would be a different set of
answers, so the bar is honestly two-tone until the backend offers it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
106 lines
14 KiB
Markdown
106 lines
14 KiB
Markdown
# Quiz revamp progress
|
||
|
||
## Milestone 1 — custom tests and categories
|
||
|
||
Implemented in `affd717` plus the follow-up review-fix commit:
|
||
|
||
- Learner custom-test builder: multiple categories and descendants, exact available counts, 1–200 questions, study/exam modes, unused/incorrect/bookmarked filters, fixed saved membership and optional sharing.
|
||
- Existing categories retain IDs/assignments; optional hierarchy, cycle guards, breadcrumbs and educator reparenting support added.
|
||
- Central general-quiz visibility and sharing checks across web, attempts and mobile; ownerless-question revocation is NULL-safe and both Hide and Unshare revoke public access.
|
||
- Explicit question selection rejects private/course/missing IDs. Peer deletion of private/manual questions is denied.
|
||
- Web, mobile and expiry use one grading function. Duplicate/out-of-pool submissions are rejected; skips have recorded incorrect outcomes; reviews use selected questions only, and unfinished attempts do not return answer review.
|
||
- Category UI handles validation arrays, refreshes reparented filters and offers relocation even when the visible question count is zero. Private quiz titles are keyboard-accessible links.
|
||
- Category migration supports real offline SQL rendering and existing/fresh schemas.
|
||
|
||
### Verification
|
||
|
||
Initial implementation received two independent read-only reviews (access/correctness and UI/migration). The parent applied the accepted fixes and added behavioral regressions.
|
||
|
||
- Backend: **14 tests passed** inside the exact deployed backend image `sha256:77d9af981537a09396fb5a511f27b4efd00794443823db8eb522beb3aa70a5b9` (Python 3.11 and deployed library versions). Disposable SQLite only; network disabled; no production data.
|
||
- Frontend: **13 tests passed in four suites** with `NODE_ENV=test npm test`; production build passed with `NODE_ENV=production npm run build`.
|
||
- PostgreSQL 16: actual migration upgrade, FK enforcement, existing-row/question-assignment preservation, repeated upgrade, downgrade and re-upgrade passed in a disposable network-isolated container. Container/data cleaned up automatically.
|
||
- Alembic offline SQL: explicit `5f8c1c2a9d40:c82d19e4a601` upgrade and reverse downgrade exercised without mocking inspection.
|
||
- Changed Python syntax and `git diff --check` passed.
|
||
- The same regression suite fails against frozen `affd717` as expected: unauthorized revocation/deletion, premature review answers, invalid mobile selections, duplicate scoring (200%) and offline SQL inspection are reproduced. The legacy-Hide test also catches its missing sharing-state response. All tests pass on the fixed code.
|
||
|
||
The first Docker validation attempt failed before tests because a read-only mountpoint was absent; setup was corrected. One frontend regression initially used an ambiguous Create Quiz selector; the modal was labeled and the test scoped to it, then all tests passed.
|
||
|
||
### Release boundaries
|
||
|
||
This milestone is source work on the feature branch, **not a production deployment or completion of the whole revamp**. Apply migrations explicitly before serving updated code against the existing production DB; startup does not automatically upgrade existing tables.
|
||
|
||
Older tutor-context and question-image delivery authorization gaps identified by review remain a release blocker and have their own tracked privacy task. Already-downloaded offline content cannot be recalled by server revocation. No new AI provider calls or production database/service changes were performed.
|
||
|
||
## Login-only Turnstile removal
|
||
|
||
User-requested removal covers both `/login` and the landing-page sign-in modal, the shared login client payload, and the backend password-login handler/schema. Registration and contact Turnstile are unchanged; no keys/configuration were removed.
|
||
|
||
Verification: 18 backend tests passed in the exact deployed image, including login with a configured Turnstile secret but no challenge token, incorrect-password rejection, email verification, SSO-only mode, the eleventh-request rate limit and retained registration verification. All 17 frontend tests and the production build passed, including both login entry points with a configured site key and retained registration/contact widgets. The first backend run used reserved `.test` email addresses; only fixtures were corrected to `example.com`, not validation rules.
|
||
|
||
This source change is committed/pushed with the feature work; it has not been deployed to production.
|
||
|
||
## Milestone 2 — runner and study tools (under verification)
|
||
|
||
Implemented a wide white/blue question layout with category breadcrumbs, numbered answers, provisional study selections, explicit response confirmation, real response-distribution bars, question navigation and Review & Complete. The results screen reviews one question at a time. Full question/explanation text, images, notes, highlighting and tutor access are retained.
|
||
|
||
Added a safe arithmetic calculator (no eval), keyboard shortcuts that do not interfere with text entry/dialogs, native focus-trapping tool dialogs, and educator-managed lab references with required population/specimen/units/source and explicit publication. No clinical reference ranges are fabricated or seeded without verification.
|
||
|
||
Attempt mode is persisted server-side: a query flag cannot reveal active exam answers/statistics, and mismatched attempt/quiz/user IDs are rejected. Legacy attempts without stored mode resume as exam mode rather than exposing answers; their saved answers remain intact. The new `d94a26b8f302` migration must run before deployment.
|
||
|
||
Combined verification after review fixes and login removal: **25 backend tests passed in the deployed image; 53 frontend tests and production build passed.** Actual PostgreSQL mode/lab upgrade, repeated upgrade, downgrade and re-upgrade passed; existing scores were preserved and the real lab ORM defaults/publication/FK were checked.
|
||
|
||
Independent review identified a failed-resume overwrite path, nullable review-setting serialization, and missing Alembic lab-model registration. Fixes add retry-only resume recovery, re-read saved progress for reused attempts, explicit cache failure responses, autosave warnings, blocked navigation when suspension cannot save, nullable/fail-closed course review handling, and model registration verified in a fresh process. New tests cover each case. Follow-up independent review resolved all three findings and found no blocker in their affected paths; see [milestone-2-review.md](quiz-revamp/milestone-2-review.md). Milestone 2 is integrated at source level. Nothing has been deployed; linked articles/cards, AI authoring, discussions, and the existing related-content privacy release gate remain.
|
||
|
||
Browser checks used a loopback-only fixture with synthetic accounts/questions and in-memory data, not production authentication. Desktop layout, provisional/confirmed answers, live fixture response counts, calculator arithmetic, honest empty lab references, and review confirmation were checked. At 390×844 the document width was exactly 390 and no question, option, image or toolbar overflowed; a taller narrow frame captures the full page. A browser-profile reset interrupted the check; re-login restored the saved attempt successfully.
|
||
|
||
Proof images: [desktop](quiz-revamp/desktop-question.png), [study feedback](quiz-revamp/study-feedback.png), [mobile width](quiz-revamp/mobile-question.png). These are synthetic previews, not screenshots of deployed clinical content.
|
||
|
||
## Related-content privacy (under verification)
|
||
|
||
Tutor authorization now precedes quota/model/AI work, attempt context must own the selected question, course answers require an authorized study/completed attempt unless privileged, and similarity context is filtered in SQL before ranking/LIMIT. Full prompts retain long stems and more than five options.
|
||
|
||
Uploaded media moved from a public static mount to a permission-aware boundary. Browser/Markdown/Milkdown image loading continues through a Path=/uploads media-only cookie mirroring the existing JavaScript-readable token (not HttpOnly/XSS-hardening). API mutations remain bearer-only; stale refresh/error responses cannot switch or resurrect accounts.
|
||
|
||
References authorize bytes: bank visibility, course stem/review rules, per-uploader drafts, document/extraction ownership, card owner/admin/shared rules, and moderator handling that respects card-only privacy. Legacy local URL spellings are canonicalized for classification and permissions. Attachment validation runs before mutation on create and both edit paths, so private files cannot be republished into shared content. New uploads live under questions/{user_id}/. SVG gets a sandbox CSP; Nginx no longer caches uploads and provides native byte ranges.
|
||
|
||
Verification: 37 backend tests in the exact deployed image (new card-moderator, legacy-alias, orphan and same-site URL regressions), 69 frontend tests and production build, real Nginx 206/HEAD/416 with no-store/Vary/sandbox through the deployed frontend image, real pgvector eligibility-before-LIMIT, and synthetic browser owner/peer/logout/quiz-image checks. Follow-up independent review resolved both earlier blockers and accepted the work; see [privacy-review.md](quiz-revamp/privacy-review.md).
|
||
|
||
Known boundaries: only course_files/course_thumbnails/scorm/certificates retain legacy LMS policy; already-cached public responses cannot be recalled by new no-store headers; downloaded offline content likewise. Privacy work is not committed or deployed yet.
|
||
|
||
## Milestone 3 — linked articles and cards (under verification)
|
||
|
||
Added an educator-authored topic library with draft/published states, stable section IDs (heading renames never break links; removed sections remediate their links), category breadcrumbs and whole-article/section question links. Articles reuse the Milkdown rich editor; reading is side-by-side on desktop with a drawer on mobile. External references stay links. Empty content is honestly labeled.
|
||
|
||
Reused the existing flashcard system and added manual card creation plus card↔question and card↔article associations. Linked content follows bank visibility and deck ownership/sharing; mutation stays owner/admin-only. New migration `e8d4f1a27c93` (after `d94a26b8f302`) must run before deployment.
|
||
|
||
Verification: 42 backend tests in the deployed image, 72 frontend tests across 12 suites and production build, plus a real isolated PostgreSQL upgrade/repeat/downgrade/re-upgrade with defaults and FK enforcement. Independent review and browser checks pending.
|
||
|
||
## Milestone 4 — share links, comments, AI authoring (deployed)
|
||
|
||
Quiz share links replaced the PIN copy: owners enable a public `/share/{token}` landing page (sign-in to take it) and copy the link without displaying it; revocation clears the token and sharing flags. Public responses never expose questions or answers.
|
||
|
||
Moderated comments on articles and study-mode questions: 1–2000 chars, 20/day per user, own-pending visibility, educator approval, owner-or-moderator deletion, markdown rendering without raw HTML.
|
||
|
||
Educator AI authoring: article drafts and refinement (drafts never auto-publish; refinement sends existing content), card generation into private decks, Celery job polling. Prompts forbid fabricated references.
|
||
|
||
Independent review found no blockers; the refine-content medium finding and minor notes were fixed (`d3663fd`). Verification: 50 backend tests in the deployed image, 85 frontend tests across 15 suites and production build; migrations `f2a1c9d4e801` and `g4b7e2f5a903` applied; services healthy.
|
||
|
||
## Cap replaces Turnstile
|
||
|
||
Cloudflare Turnstile is gone from the two places a stranger can reach: the registration form (standalone page and landing-page modal) and the contact form. Both now solve an Cap challenge, verified against `https://api.cap.com/siteverify` with `secret` and `response`.
|
||
|
||
The duplication went with it. `frontend/src/components/Captcha.jsx` is the only widget — three near-copies of `TurnstileWidget` had drifted, one of them loading the script and one assuming somebody else had — and `backend/app/services/captcha.py` is the only verifier, where the two router copies had already diverged on what an unreachable provider means. That difference is now deliberate and named: registration fails open, because an outage that blocks sign-ups costs the site its users; the contact form fails shut, because a bounced message costs the sender one retry. A blank secret still skips the check entirely, so an unconfigured install keeps working.
|
||
|
||
The wire field is `captcha_token`, the runtime key is `CAP_SITE_KEY` (injected by `frontend/docker-entrypoint.sh` into `window.__APP_CONFIG__`), the backend settings are `CAP_SECRET_KEY` and `CAP_SITE_KEY`, and the nginx CSP now allows `cap.com` and its subdomains instead of `challenges.cloudflare.com`.
|
||
|
||
Verification: 404 backend tests passed in the rebuilt image, 13 of them on this change — the service's four answers (unset secret, solve accepted, solve refused, provider unreachable both ways), the two 400s the gate distinguishes, the contact form's three, and the retained login rules. 451 frontend tests passed across 64 suites, 10 of them on this change. Live keys were not migrated: `CAP_SECRET_KEY` and `CAP_SITE_KEY` are blank until issued from the Cap dashboard, which means no challenge is shown or required until they are set.
|
||
|
||
Not deployed; left in the working tree for review.
|
||
|
||
## Release state
|
||
|
||
The revamp is deployed incrementally from `feat/orthobullets-quiz-revamp` (`d3663fd`, local == remote): category tests/hierarchy, runner/study tools, articles/cards, tutor/media privacy, login Turnstile removal, share links, moderated comments and AI authoring. Live database is migrated through `g4b7e2f5a903`; backend/Celery/frontend images match the branch; pre-deploy database dumps exist under `backups/` (manual and daily). Hard-refresh (Ctrl+Shift+R) to load the new bundle.
|
||
|
||
## Next
|
||
|
||
Continue with the Orthobullets-inspired runner/results UI, question navigation and study tools; then article/subsection reading, linked flashcards, educator AI authoring and moderated comments. Complete related-content privacy work and end-to-end desktop/mobile validation before deployment.
|