▶️ 2026-08-26 play surface repair

Historical record — 2026-08-26

Preserved as observed. Cluster 1 of the ordered series delivered by the 2026-08-27 canonical handoff; the four findings it recorded as not fixed have since been closed, per the dated status lines below. Current truth: Program Masterplan.

Scope: WRDP bench, 2026-08-26, Cluster 1 of the play-flow masterplan. Citations are against the installed source under wp/wp-content/plugins/quizwizz/. Every figure on this page was measured this session; nothing is carried over from a plan, changelog, or prior page.

Defects

  • operator-reported:
    • clicking Play landed on /?qwizz_play=<id> and showed a dead intermediate screen, a hint plus a single Start quiz button, before the quiz began
    • the quiz surface rendered a dark card floating inside a theme-white page with 56px gutters and no site chrome
  • adjacent defects on the same route, in scope by operator decision:
    • the route emitted no <title> element at all, so the browser tab was blank
    • body class reported home … page-id-2 while rendering quiz 99

End state reached: the play route opens question 1 with no second gesture; a non-site palette paints the whole standalone surface with no white frame and no card seam; the route is a singular document titled after the quiz it renders. All three Play entry points were exercised as real guest gestures, not typed URLs.

Files changed (5)

All bench-local, releaseEligible: false.

  • includes/Frontend/QuizEmbed.php
    • render() now ORs in self::is_play_route( $quiz_id ) after the access checks
    • new private helper is_play_route() reads $_GET['qwizz_play'] and compares it to the rendered quiz id
  • includes/Frontend/QuizQueryViewer.php
    • explicit main-query state: global $wp_query; plus posts, post, post_count, found_posts, max_num_pages, queried_object and queried_object_id, with is_singular and is_single true and is_home, is_front_page, is_page, is_404 false
    • one <title><?php echo esc_html( wp_get_document_title() ); ?></title> after the viewport meta and before wp_head()
    • block output wrapped in <main class="qwizz-single-quiz" id="qwizz-content">
  • assets/js/qwizz-player.js
    • applyPresentation() mirrors a non-site theme onto document.body.dataset.qwizzTheme and deletes it otherwise, guarded by document.body.classList.contains('qwizz-has-player')
    • diff is exactly 6 added lines; prepareIntro() and its resume branch are byte-identical
  • assets/css/qwizz-tokens.css
    • selector-only extension: each of the 6 palettes plus the generic [data-theme] block also matches body[data-qwizz-theme…], and body[data-qwizz-theme] joins the @media (forced-colors: active) selector list
    • zero declaration changes; the --qw-card: occurrence count is identical to the pristine copy
  • assets/css/qwizz-player.css
    • two appended rules: body.qwizz-has-player[data-qwizz-theme]{background:var(--qw-card);color:var(--qw-ink)} and body.qwizz-has-player[data-qwizz-theme] .qwizz-player__card{background:transparent;border-radius:0}

Deliberately not changed

  • the bool $autostart = false default in QuizEmbed::render() is unchanged, so passive embeds keep asking
  • no <h1> was added to the play route: the player owns the title there, and adding one would restore the duplicate-title defect closed 2026-08-25
  • .qwizz-single-quiz padding untouched
  • no min-height added to body
  • no palette values duplicated in assets/css/qwizz-tokens.css; the palette work is selector-only

Canonical handoff

Three artifacts sit beside this page in q5vault/audits/qwizz/:

  • 2026-08-26-cluster1-source.patch — 7302 bytes, sha256 644de6e5d6e88f974c3262ee2963b8fea19ac9096c1e92c728c349f85b6c18ea
  • 2026-08-26-cluster1-preimage.sha256 — 5 pre-fix digests, plugin-root-relative
  • 2026-08-26-cluster1-postimage.sha256 — 5 post-fix digests, plugin-root-relative

Apply from the plugin root, never with --reject:

git apply -p1 2026-08-26-cluster1-source.patch

Integrity, all measured:

  • generated with git diff --no-index --binary --no-prefix a b, exit 1, a nonempty diff being the success condition
  • git diff --no-index --check returned 1 with empty whitespace output
  • header audit: exactly 5 diff --git stanzas over the 5 allowlisted relative paths; zero new file mode, deleted file mode, rename, old mode, new mode, or binary stanzas
  • 50 added lines, 8 removed lines
  • forward round-trip: git apply -p1 --check rc 0, apply rc 0, diff -r verify b rc 0, byte-identical
  • reverse round-trip: git apply -p1 -R --check rc 0, apply rc 0, diff -r revert a rc 0, byte-identical to pristine, so the rollback path is proven rather than assumed
  • sha256sum -c of the postimage sidecar against the live plugin tree: 5/5 OK. The preimage sidecar correctly does not match live; it is the before-state record.
  • superseded 2026-08-26 for two of the five files: Cluster 2 changed includes/Frontend/QuizQueryViewer.php and includes/Frontend/QuizEmbed.php again, so sha256sum -c of this postimage sidecar against the live tree now reports 3/5 OK and 2 mismatches. That is the expected chain, not drift: the Cluster 2 preimage digests for those two paths equal the Cluster 1 postimage digests recorded here, and the live tree matches the Cluster 2 postimage. Verify the chain, not this sidecar alone — the playable-quiz repair record.

Measured before/after

checkbefore (pre-edit, live)after (live)
/?qwizz_play=99 intropresent, Start quiznull, question 1 direct
data-autostart"""1"
<title> count01, Geografie-Weltreise – QuizWizz
body classhome wp-singular page-template-default page page-id-2 …wp-singular qw_quiz-template-default single single-qw_quiz postid-99 …
body backgroundrgb(255,255,255)rgb(23,24,29), body.dataset.qwizzTheme === 'dark'
.qwizz-player__card backgroundrgb(23,24,29)rgba(0,0,0,0)
.qwizz-player__card border-radius18px0px
white-painting elements between card and viewport edgecard gutters white[], full-DOM sweep, both surfaces, both viewports

Before-state provenance: the pre-edit values come from curl and browser reads taken before any file was installed, plus the 2026-08-26-cluster1-preimage.sha256 digest record. NOT MEASURED: no pre-fix screenshot exists. The before-state is textual and computed-style evidence plus the preimage digest record, and nothing on this page claims a pixel comparison.

Verification

Shell and lint:

  • php -l in-container clean on both changed PHP files, at every install and re-install
  • standing gate ./scripts/quizwizz-probe.sh --wave all1..51, assertions=51 failures=0, up from 1..46 and 46 before this cluster; the 5 new assertions are Wave 7
  • logs/debug.log 38536 → 38544 lines across the whole session. All 8 added lines are WordPress Automatic updates starting/complete cron blocks at 2026-08-26 08:00:36 and 19:49:17 UTC. Zero fatals, zero warnings, zero notices from any changed path.

Wave 7, new in scripts/quizwizz-probe.sh, the host-owned harness rather than plugin source:

  • ok 47 - play route autostartautostart=1 gesture=0
  • ok 48 - passive embed still asksplayers=1 asks=1 autostarts=0, via a wp-cli do_shortcode render that creates no post and no attempt row
  • ok 49 - play route document titletitles=1 quiz-titled=1
  • ok 50 - play route document identitysingle-qw_quiz=1 postid=1 home=0 page-id-2=0
  • ok 51 - play route layout wrapperwrapper=1 h1=0
  • every assertion is theme-independent: the served play-route markup carries no theme attribute, and the palette is applied client-side from the REST payload

Mutation proof. The five assertions were proved to defend their contracts, not merely to pass:

  • counterfactual pass: a throwaway local HTTP server served textually mutated copies of the real play-route document, and each mutation failed exactly its own assertion and no other — m1 autostart, m3 title removed, m4 home body class, m5 wrapper replaced by an <h1>
  • live pass: both changed PHP files were installed back to their pristine pre-fix bytes under an ERR-trap, php -l clean, and --wave 7 then reported assertions=5 failures=4. Assertions 47, 49, 50 and 51 failed with the exact pre-fix shapes, the identity detail reading actual=single-qw_quiz=0 postid=0 home=1 page-id-2=1. Assertion 48 correctly stayed ok, because the passive default was never part of the fix. Files were then restored, php -l clean, postimage digests 5/5 OK, and --wave 7 green again.
  • assertion 48 was proved non-vacuous by rendering the shortcode with a play-route signal present, $_GET['qwizz_play']=99, which is behaviourally identical to a flipped default: players=1 asks=0 autostarts=1, not the expected shape. It was deliberately not proved by flipping the real default, because a flipped default would POST /quizwizz/v1/quiz/<id>/start and INSERT attempt rows on a page view.

Browser, cache disabled, https://wrdp.loca.zone:

  • logged-out guest in a separate incognito browser context at 1600×1000 and 375×812: no admin bar, --wp-admin--admin-bar--height unset, question 1 direct, titles=1, body class correct, body rgb(23,24,29), card rgba(0,0,0,0), zero white-painting elements, scrollWidth - clientWidth = 0 so no horizontal overflow, REST nonce empty confirming the true guest path, and elementFromPoint(2, viewportHeight-3) resolving to MAIN.qwizz-single-quiz, so the surface reaches the bottom edge
  • all three Play entry points exercised as real gestures rather than typed URLs:
    • library fixture page 1289 as a guest: clicked a Play quiz card → landed ?qwizz_play=1284 → question 1 directly (How many bits are there in one byte?), no intro, titles=1, postid-1284, dark full-bleed
    • craft wizard on page 57 as a guest: selected Geography, clicked Play → craft created quiz 11081 and redirected to ?qwizz_play=11081 → question 1 directly, no intro, no failure card, autostart=1, titles=1, postid-11081, dark full-bleed, zero page errors and zero failed requests
    • wp-admin dashboard ?page=qw-dashboard: its Play links do emit ?qwizz_play=<id> and the route honoured them, but all five links pointed at empty auto-drafts; see the not-fixed section below
  • all six palettes verified on the page host by setting body.dataset.qwizzTheme and reading computed styles: light rgb(255,255,255) with ink rgb(33,29,49), dark rgb(23,24,29), retro rgb(239,230,208), cyber rgb(14,11,30), love rgb(255,241,245), aqua rgb(234,247,251). Each matches its --qw-card, each keeps its own --qw-ink, and the card is transparent with radius 0 in every case.
  • unthemed control, attribute removed: body returns to rgb(255,255,255) and the card returns to rgb(23,24,29) with 18px radius, proving both new rules are scoped strictly to the themed-body case and nothing leaks
  • site theme control: site was temporarily enabled in qw_exposure_settings, quiz 1284 then resolved to site, and /?qwizz_play=1284 showed no data-qwizz-theme on body, the Twenty Twenty-Five white page background, and the card keeping its own surface. The setting was restored and the option value md5 is identical before and after, cf4ea924e3fd29c2405b1073ddfad73c, with enabled_theme_ids() back to ["dark"].
  • forced-colors emulation via Emulation.setEmulatedMedia with forced-colors: active: --qw-card resolves to Canvas and --qw-ink to CanvasText, and body paints system white on black. After disabling emulation --qw-card returns to #17181d.
  • passive embed: a scratch page carrying [quizwizz_quiz id="99"] rendered the intro with exactly one Start quiz button, data-autostart="", body unthemed and white, and body.qwizz-has-player absent. The attempt table read 69 before and 69 after that page view. The scratch page was deleted with --force.
  • resume path intact: answered one question on /?qwizz_play=99, reloaded, and the intro returned reading Resume question 2 of 10 / Start over

Access control re-checked, because the craft redirect carries a qwizz_token:

  • private craft quiz 11081, anonymous cookieless request: HTTP 404
  • same URL with qwizz_token=bogus: HTTP 404
  • so the crafted quiz was readable only inside the crafting session and there is no access regression. As measured on 2026-08-26 the 404 branch rendered the site home template under a 404 status, a soft-404; that was pre-existing behavior at QuizQueryViewer.php and unrelated to this cluster. Closed 2026-08-27: the branch is now QuizQueryViewer::emit_404() (:64-82), which clears the main query, calls set_404() and renders the theme’s own 404 template — the 2026-08-27 not-found and token-scope record.

Screenshots

Guest, cache disabled, in cluster1-2026-08-26/.

Play route at 1600×1000 as a logged-out guest

  • play-dark-desktop-guest.png, 1600×1000
  • independently confirmed in this capture: a single dark colour fills all four edges, no white band or gutter, no separate rounded card seam around the content, no admin bar and no site header, nav, or footer chrome, and no horizontally clipped content

Play route at 390×844 as a logged-out guest

  • play-dark-mobile-guest.png, 390×844
  • independently confirmed in this capture: the same findings as the desktop shot — a single dark colour on all four edges, no white band or gutter, no card seam, no admin bar and no site chrome, no horizontally clipped content

Library fixture page 1289 as a logged-out guest, the surface the Play gesture starts from

  • library-play-gesture-guest.png, 1600×1000
  • what this capture is: the surface the Play gesture starts from. The independent screenshot inspection covered only the two play captures above; the gesture’s own outcome was measured in the browser, recorded under the entry-point bullets, not read off this image.

Bench state left behind

Disclosed, not hidden.

  • scratch page for the passive-embed check: created and deleted with --force; page count 8 before and 8 after
  • craft-created quiz 11081: created by the real Play gesture and deleted with --force; SELECT COUNT(*) WHERE ID=11081 returns 0
  • wp_qwizz_attempts is harness-managed test data. It read 68 before this session’s verification, 69 mid-session, and 21 after the standing gate ran its own cleanup. No manual row purge was performed: deleting rows would touch child answer rows for no benefit on a bench whose gate already manages this table.
  • qw_exposure_settings restored byte-equivalent, md5 cf4ea924e3fd29c2405b1073ddfad73c before and after
  • pristine pre-fix copies retained at /tmp/qwfix-pristine/ and /tmp/qw-c1/a/; the durable record is the preimage digest sidecar plus the reversible patch

Not fixed — Cluster 2 candidates

New findings from this session. None is authorized, none is scheduled, and none was touched.

Status added 2026-08-26 after Cluster 2: findings 1 and 2 are closed, findings 3 and 4 remain open. The paragraph above records the state when this page was written and is not restated.

Status added 2026-08-27 after Cluster 5, superseding the line above: finding 3 is closed; finding 4 is partly closed, and its one remaining stale site lies outside this page’s file scope. Per-finding closure lines are recorded under each finding below.

  1. Dashboard Play links all point at empty auto-drafts.
    • root cause: Admin/DashboardPresenter.php:308-341. recent_quizzes() queries post_status => array('publish','private','draft','pending','future') ordered by modified with RECENT_LIMIT rows, and emits a Play link whenever QuizAccess::can_read() passes.
    • measured symptom: the bench holds 109 draft quizzes titled Automatischer Entwurf with LENGTH(post_content) = 0, and they dominate orderby=modified, so all 5 dashboard Play links resolved to content-less drafts and zero published quiz was reachable from the dashboard. ?qwizz_play=10989 returns 200 with a correct <title> and postid-10989 body class but no player at all, NO CARD, on a white page.
    • correction 2026-08-27: that symptom line conflates two populations. Re-measured, the bench held 109 drafts in TOTAL, of which only 15 carried zero questions, and only 5 of those 15 were titled Automatischer Entwurf. The dashboard symptom itself is unaffected — zero-question drafts did dominate orderby=modified and all 5 slots were dead — but the count 109 never described the zero-question population. All 15 zero-question rows were deleted on 2026-08-27; see finding 4 on the playable-quiz repair record.
    • closed 2026-08-26: recent_quizzes() now requires qw_question_count > 0 in both the query and the Play CTA, so no dashboard slot offers a dead Play link — the playable-quiz repair record.
  2. The Play route renders an empty document for a content-less quiz instead of the QuizEmbed::unavailable() contract.
    • root cause: the route’s do_blocks() call in includes/Frontend/QuizQueryViewer.php; no line number was recorded for this finding. do_blocks() on empty post_content emits nothing, so the route bypasses the unavailable card entirely.
    • measured symptom: after this cluster the blank page at least carries a correct title and a singular body class, but it is still a dead end.
    • closed 2026-08-26: the route now precomputes the block render and substitutes the QuizEmbed::unavailable() card when it is empty — the playable-quiz repair record.
  3. The route’s not-found branch is a soft-404.
    • root cause: status_header( 404 ); return; in includes/Frontend/QuizQueryViewer.php falls through to the theme; no line number was recorded for this finding.
    • measured symptom: an inaccessible quiz id serves the full site home page, 121 KB, under a 404 status instead of a not-found surface.
    • closed 2026-08-27 by Cluster 5: the branch is now QuizQueryViewer::emit_404() (:64-82), called from the not-a-quiz guard (:21) and from the access-denied branch (:30). It clears $wp_query, calls set_404(), sends the status and renders the theme’s 404 template. All three not-found conditions now serve 404 at 55,655 bytes rather than 121,285, and two of the three had also been answering HTTP 200 rather than 404 — the 2026-08-27 not-found and token-scope record.
  4. Doc drift created by this cluster’s Wave 7 addition, listed for the owners.
    • root cause: the wave-count and current-result statements at AGENTS.md:82, .omp/skills/quizwizz-probe-harness/SKILL.md lines 10, 26, 27, 48 and 49, .omp/agents/probe-runner.md lines 21 and 54, .omp/commands/probe.md:10, q5vault/audits/qwizz/index.md:19, and the current-result statement in q5vault/audits/qwizz/probes.md.
    • measured symptom: each names Wave 1–6 or the 1..46 result, which the standing gate no longer reports. The two q5vault entries in that list are corrected in the same change set as this page; the AGENTS.md and .omp/** entries lie outside this page’s file scope and belong to their owners.
    • status 2026-08-27, verified by reading the cited files today: every site in this finding is now corrected. AGENTS.md:82, .omp/commands/probe.md:10 and .omp/agents/probe-runner.md:21,54 name --wave 1|2|3|4|5|6|7|8|9|10|all and the 1..76 standing gate; .omp/skills/quizwizz-probe-harness/SKILL.md:10,26-27,48-49 names waves 1-10 and 1..76, the :10 site having been the last to be fixed because it wrote the range with an ASCII hyphen (Waves 1-7) and so escaped an en-dash sweep. None of those files is in this page’s file scope; the corrections belong to their owners.

Release boundary

Every plugin change here is releaseEligible: false. The bench runtime package is a development artifact, not the canonical release repository: no vendor/, build/, release ZIP, provenance output, QUIZWIZZ_VERSION, or db_data/ was modified, and no version bump, build, or release happened. The release owner applies these source changes through the locked pipeline in BUILDING.md, using the patch and the two digest sidecars above. The harness contract this cluster extended is recorded in the probe harness page.