feat(practice): mock exams + auto-rebuild on cert markdown edits (931 q / 18 banks)#29
Merged
Conversation
… q / 18 banks)
Adds 12 full-length mock-exam banks alongside the 6 practice banks.
Source files are the existing certifications/<cert>/resources/mock-exam{,-2}/questions.md
already used for the mock-exam study material in this repo — no
parallel content to maintain.
Bank totals (was 364 / 6):
Practice (6 banks) 364 q
Mock Exam 1 (6 banks) 286 q
Mock Exam 2 (6 banks) 281 q
────
931 q across 18 banks
Builder:
- build_mock(cert, exam_n) parses mock-exam questions.md, pre-scanning
for `## <Domain> (Questions X-Y)` section headings to build a
qnum→domain map, then reuses parse_questions() and overlays the
per-question domain
- --kind {practice,mock,all} flag for selective builds (default all)
UI:
- KNOWN_BANKS now declared via spread from a CERTS array (one source,
three groups: practice, mock-1, mock-2)
- Bank picker groups cards under labelled section headings
("Practice questions — drill by topic" / "Mock exams — full-length")
- Two-column grid layout on screens ≥ 700 px (was single column)
Auto-rebuild + auto-deploy from cert markdown edits:
- deploy-practice.yml now triggers on changes to:
- practice/**
- certifications/**/practice-questions/**
- certifications/**/mock-exam/**
- certifications/**/mock-exam-2/**
- Workflow runs `python3 practice/build.py` BEFORE uploading the
Pages artifact, so the live JSON always reflects current cert
markdown — no need to re-run build.py locally before pushing.
ubuntu-latest already ships python3, so no setup-python step needed.
Cache versioning bumped to ?v=4 on app.js, styles.css, and JSON fetches.
Verification:
- python3 practice/build.py --check → 18 banks / 931 q parsed cleanly
- JSON shape validated across all 18 banks (correctAnswer in A-D,
all 4 choices present, difficulty in {easy,medium,hard})
- node --check passes on app.js
- YAML syntax valid on deploy-practice.yml
- markdownlint passes on touched md
Refactors the bank picker from a flat 18-card grid into a two-step flow:
Step 1: Pick a certification (6 cards, one per cert)
Each card shows cert name + total questions across its banks
+ blueprint date + a red accent bar on the left
Step 2: Pick a bank for that cert (Practice / Mock 1 / Mock 2 — 3 cards
in a row on desktop)
Includes a "← All certifications" back button
This matches how candidates actually think about cert prep — they pick the
cert first, then decide between topic drill or full mock.
Quiz UI now also has syntax highlighting for code blocks via Prism.js (CDN,
data-manual mode so we control when to highlight). Supported languages:
python, sql, scala (covers everything in the existing question banks).
Tokens override Prism's prism-tomorrow theme on light mode so colors don't
glow against a light code-block background.
Theme variables extended:
- --color-code-bg (separate from --color-card-bg so code blocks have a
distinctive subtle tint per theme)
- Light-mode token colors hand-picked to read against soft backgrounds
- Dark mode keeps Prism's bundled colors
State changes:
- STATE.certBanks caches the Map<certKey, items[]> from loadAllBankMetadata
so re-entering the cert picker from the quiz doesn't refetch
- "Switch bank" in the quiz now returns to the cert picker (step 1), not
the full page reload
Other:
- APP_VERSION bumped to 5 for cache invalidation
- New cert-card / back-button / cert-subtitle / bank-purpose styles
…ts + answer-reveal effects
Big design pass addressing user feedback during local testing:
Typography fix — "ฟ้อนต์อ่านยากอ่ะ + monospace ไม่เข้ากัน":
- Drop Instrument Serif / Bricolage Grotesque (display serifs that fought
with inline code spans).
- Switch to **Geist + Geist Mono** — Vercel's sans+mono designed together
with matching x-height and proportions, so inline `code` spans now blend
smoothly into question stems instead of looking glued on.
Topic-bug fix — "topic คือ Question 2":
- All 12 mock-exam banks have title = "Question N" placeholders (source
markdown headings `## Question N *(Difficulty)*` carry no real title).
- Audit confirmed: 6 practice banks have 0 fallback titles; 12 mock banks
have 100 % fallback titles.
- Fix: suppress the Topic line in the feedback panel when title matches
/^Question \d+$/. The running head already shows the domain, which is
the meaningful context for mock questions.
Modern redesign — "อยากให้ดูทันสมัย น่าสนใจกว่านี้":
- Per-cert accent gradients on cert cards (DE → coral, DE Pro → indigo,
DA → emerald, ML → purple-pink, ML Pro → amber-red, GenAI → cyan-blue)
— top edge gradient bar + blurred halo on hover. Each cert has a
recognisable visual identity.
- Big tabular-figures question count (2.5rem) as the dominant element on
cert cards — confident, scannable.
- Cards: subtle lift on hover, glow halo behind, top gradient strip.
- Pill-shaped primary buttons with hover-shadow + accent-coloured glow.
- Larger, more confident H2 (clamp 1.75-2.5rem, -0.03em tracking).
Answer-reveal effects — "เพิ่ม effect ตอนตอบถูก/ผิด":
- Correct answer: green pulse + 0-6px glow ring + brief card-bg flash to
positive-soft + floating "+1" rising from the choice.
- Incorrect answer: 6-step horizontal shake on the user's wrong choice +
brief card-bg flash to negative-soft.
- Feedback panel slides down from above when revealed (0.35s).
- Streak toast pill appears at 3/5/7/10/+5 consecutive corrects ("3 in a
row · keep going") — top of screen, 1.8s, accent-coloured number badge.
Resets on first wrong answer.
- All animations gated behind prefers-reduced-motion: no-preference.
Other:
- Cache version bumped to ?v=8 across CSS, JS, and JSON fetches.
- Drop paper-grain texture (didn't suit the modern aesthetic).
- Theme toggle: remove old emoji icon span (was overlapping with the new
dot indicator), use pure CSS ::before dot.
… button
Three things in one batch:
1. Fix keyboard shortcuts that weren't actually working.
Bug: the condition `if (letter && !$("#btn-next").hidden === false)` was
inverted — it blocked choice selection BEFORE submit instead of after.
Now: choice keys (1-4 and A-D) work pre-submit; Enter submits or
advances; modifier keys (Ctrl/Cmd/Alt) are passed through to the browser
so things like Cmd+R still work.
2. Display choices as 1/2/3/4 instead of A/B/C/D.
The source markdown still uses A/B/C/D internally (no data migration
needed), but the rendered choice label and the "Correct answer: X" line
in the feedback panel now both show numbers. Reason: number labels
make the available keyboard shortcuts obvious — see "1" beside a
choice, press 1 to select it.
3. Add a Skip button (also bound to the S key).
The user can now skip a question they don't know without recording an
incorrect attempt. Skipped questions go into seenThisSession so they
don't immediately repeat, but adaptive mode will re-surface them in a
future session (untouched history → high weight = early in the queue).
Skip is only visible before the user submits — after submit only
"Next question" shows.
Updated kbd hint: "1 2 3 4 select · ↵ submit · S skip"
Cache version bumped to ?v=9 across app.js, styles.css, and JSON fetches.
… keys
Restructure the in-quiz chrome so the question/choices/feedback are the
only thing that scrolls, with header info and action buttons pinned.
Top sticky masthead (52px tall):
- Brand mark (28×28) + "DBX Practice" wordmark on the left
- Quiz meta strip — cert · domain · difficulty · counter — slides in
when quiz is active, hidden in setup/stats/settings
- Theme toggle on the right
- Uses backdrop-filter blur for a "frosted" look over scrolled content
Bottom sticky actionbar (64px tall, visible only in quiz mode):
- Left: Submit / Skip OR Next question + kbd hint
- Right: session stats, bank stats, then Stats / Settings / Reset / Switch
- Replaces the old in-card .quiz-actions + .quiz-footer rows
Keyboard:
- Already-added: 1-4 select, S skip, Enter submit
- New: Space, N, → all advance after submit
- Pre-submit kbd hint: 1/2/3/4 select · ↵ submit · S skip
- Post-submit kbd hint: N or → next question
- Hints swap automatically based on submit state
Behaviour guarantees ("next ต้องกดไม่ได้ถ้ายังไม่ submit"):
- btn-next has the `hidden` attribute initially and is only un-hidden
inside submitAnswer() after recordAttempt runs
- handleKeydown checks `submitted` (= !btn-next.hidden) before firing
N / → / next-via-Enter — there's no path to advance without submitting
Mobile (≤ 720px):
- Drops wordmark and kbd hints to keep the bar compact
- Hides bank stats in the right side of the actionbar
- Action bar grows taller to wrap submit + skip on its own line
- body.quiz-active main { padding-bottom: 10rem } so the last choice
isn't covered by the actionbar
Cache versions bumped to ?v=10 across app.js, styles.css, and JSON fetches.
When the user picks choice 2 and changes their mind, they can now press ↑ or ↓ to move the selection without taking their hand off the home row. Behaviour: - ↑ / ↓ are only active before submit (after submit they fall through to other handlers; specifically → / N for next-question still work) - Wrap around at edges: ↓ from choice 4 → choice 1; ↑ from 1 → 4 - No current selection: ↓ starts at choice 1, ↑ starts at choice 4 - Triggers the radio's change event so the existing onchange wiring (setting STATE.currentChoice + enabling the Submit button) fires exactly as if the user had clicked Pre-submit kbd hint updated to: 1 2 3 4 or ↑ ↓ select · ↵ submit · S skip Cache versions bumped to ?v=11.
…+ show Space in kbd hint
Three small fixes from live-testing feedback:
1. ↑/↓ now move keyboard focus along with the radio's checked state.
Previously the focus ring would stay on the previously-clicked choice
while the filled radio moved elsewhere — visually decoupled.
Fix: call radio.focus() inside the arrow handler. The :focus-within
rule on the label then shows the focus outline on the same choice
that's checked.
2. Next button could be clicked before submit because button.primary
{ display: inline-flex } has higher CSS specificity (0,0,1,1) than
the user-agent [hidden] { display: none } rule (0,0,1,0), so the
hidden attribute was silently being ignored.
Fix: explicit `[hidden] { display: none !important }` at the top of
styles.css. This makes the HTML `hidden` attribute always win,
regardless of other selectors. Covers all current and future cases.
3. kbd hint now spells out Space alongside Enter.
Pre-submit: 1234 / ↑↓ select · ↵ / Space submit · S skip
Post-submit: ↵ / Space / N / → next question
Cache version bumped to ?v=12.
…radio
When the user picked a choice via click or ↑/↓, focus landed on a radio
button — and my handleKeydown had `if (target.matches('input, textarea,
select')) return` which fired on the radio (it's an <input>), bailing
before reaching the Space/Enter submit branch.
Fix: narrow the early-return to *text* form controls. Radios and
checkboxes are kept in the handler so Space/Enter/↑/↓/letter keys all
keep working when focus is on a choice radio.
Cache version bumped to ?v=13.
User reported: scrolling to the bottom of a long explanation, the
sticky actionbar covered the last lines. Root cause was a hard-coded
padding-bottom: 6rem that didn't account for the actionbar wrapping
onto a second row (Next button + kbd hint on one line, session stats
+ Stats/Settings/Reset/Switch links on another).
Fix:
- Read --actionbar-h CSS variable as the bottom padding, with a 7rem
fallback + 1.5rem of breathing room
- New syncActionbarHeight() in app.js measures the actionbar's
bounding rect and writes the value to body's style
- ResizeObserver on the actionbar fires the sync whenever its rendered
height changes (content wrap, viewport resize, content-driven layout
shifts when feedback shows)
- Also fires on window resize and on show() transitions
- Removed the obsolete mobile-specific padding-bottom override
Also include the inline-code wrap fix from earlier:
- code { overflow-wrap: anywhere; word-break: break-word } so long
unbreakable identifiers (e.g. spark.sql.streaming.stateStore...) wrap
inside the question/feedback container instead of overflowing right
- pre code { overflow-wrap: normal } so fenced code blocks still use
horizontal scroll (preserving indentation)
- question-card and #quiz-feedback get min-width:0 + overflow-wrap as
defense-in-depth so any other unwrappable content can't stretch the
cards past their container
Cache version bumped to ?v=14.
…nk already there)
40 colourful particles launch from the centre of the correct-answer label, follow a parabolic arc (upward burst → gravity pulls them down), rotate randomly, and fade out around 1.4-1.6s. Pure CSS animation + zero-dependency JS — no library. Implementation details: - fireConfetti(originEl) reads the bounding rect of the correct label so the burst feels like it's coming from that row - Per-particle CSS custom props (--mid-dx, --mid-dy, --end-dx, --end-dy, --rot) describe the parabola apex and final position; one shared CSS keyframe interpolates through them - 10-colour palette (matches per-cert accent gradients + white) - 30% of particles are circular for shape variety - Animation-delay 0-80ms per particle so they don't all launch in a perfectly synchronised flash - Container element appended/removed from <body> so positioning is fixed-relative-to-viewport regardless of scroll Auto-disabled when prefers-reduced-motion: reduce (CSS rule + JS guard). Cache version bumped to ?v=15.
…outline is enough) Was rendering a double focus ring on keyboard-focused choices: one around the radio button itself (from the universal :focus-visible rule) and one around the whole label row (from :focus-within). Per user feedback, keep only the outer label outline.
User feedback: the keyboard-selected choice rendered with a loud orange focus-within outline while the mouse-selected choice used a subtle cream-background tint. They preferred the subtle look for both. Changes: - New rule label:has(input:checked):not(.correct):not(.incorrect) → same surface-soft background + fg-soft border as :hover, so checked state matches hover/click visual regardless of how it was selected - Dropped the label:focus-within and input[type=radio]:focus-visible outlines — no more orange ring on keyboard nav Cache version bumped to ?v=17.
… question User reported choices looking "stuck selected" between questions. Two contributing issues: 1. Hover and selected used identical styling (cream bg + fg-soft border), so when the mouse rested on any choice in a new question it was indistinguishable from the actually-picked choice. Fix: - Hover: just darken the border (no background) - Selected: cream background + darker (--fg) border These are now clearly distinct at a glance. 2. After clicking Next via a keyboard shortcut, focus could remain on the previously-focused control, and any residual :focus-within / :hover state could carry over visually to the new question's choices at the same screen position. Fix: blur the active element at the start of renderQuiz so the new question loads with a clean focus state. Cache version bumped to ?v=18.
…moval Four user-driven UX improvements in one batch: 1. Drop the hover visual entirely. Hovering a choice now gives no border/background change — only an actual selection (click, ↑/↓, or 1-4) colors a row. Fixes "stuck highlight" complaints where a leftover mouse position made hover look identical to selection while the user was keyboard-navigating. 2. Slide-in transition on every new question. The .question-card class gets a slide-in animation re-triggered via a class toggle + reflow in renderQuiz(), giving a clear visual cue that a fresh question has appeared. 3. Wall clock pill in the masthead — shows local time HH:MM, updates every second via setInterval. Helps you keep an eye on the time during long practice sessions. 4. Exam timer pill in the masthead — set duration in Settings (None / 30 / 60 / 90 / 120 minutes; 90 = Associate exam, 120 = Professional exam). Counts down MM:SS, pulses warning-red under 5 minutes, locks to 00:00 + shows a "Time's up" toast when it expires. Timer choice persisted in localStorage; starts fresh on each new bank load. CSS: - Selected state now uses accent-soft bg + accent border — distinctive color hover can never produce, so there's no visual confusion about which row is picked even if the mouse is over something else - New .meta-pill base style + .timer .warning/.expired variants - @Keyframes question-slide gated behind prefers-reduced-motion Cache version bumped to ?v=19.
… animation Header (ambient awareness): 14:32 ●Auto Action bar (active session state): [TIMER 47:23] [Submit] [Skip] … Two visually distinct regions instead of two near-identical pills: - Wall clock in the masthead is now plain mono text (no border, no background) in a muted tone — secondary, "what's the wall time" - Exam timer moved to the action bar with a real labelled pill: small "TIMER" header in uppercase letterspaced caps + large MM:SS value. Sits right next to the Submit button so it visually belongs to the "active state" group, not the ambient header chrome. - Warning state (< 5 min) pulses accent-coloured; expired state locks to negative red, no animation. - Mobile (≤ 720px): clock is hidden to save room, timer drops the "TIMER" label and shows just MM:SS. Slide transition slowed from 0.28s → 0.55s with a softer ease curve and a longer travel (28px vs 18px). The new fade-in opacity stop at 30% gives the eye a clearer "okay, new question arrived" cue. Cache version bumped to ?v=20.
… buttons Per user feedback the timer sitting next to Submit/Skip/Next felt crowded with the action cluster. Moved to the rightmost slot of the actionbar after the Stats/Settings/Reset/Switch links, with a vertical divider separating it from the controls so it reads as its own zone. Cache version bumped to ?v=21.
…tion counter Per user feedback the timer worked better visually beside the question counter — "Q6 this session · ⏱ 22:27" reads as a single line of session state instead of being split between header and footer. Changes: - HTML: #quiz-timer moved from the actionbar back into the masthead quiz-meta-strip, after #quiz-counter - CSS: new .meta-timer pill style — compact, ⏱ icon via ::before, monospace MM:SS, pulses accent-coloured under 5 min, locks negative red when expired. Removed the obsolete .actionbar-timer styling. - Action bar is back to just submit/skip/next on the left and the stats + Stats/Settings/Reset/Switch links on the right. Cache version bumped to ?v=22.
…dden Putting #quiz-timer inside the .quiz-meta-strip hid it whenever the cert + domain text pushed past the strip's allotted width — the strip's overflow:hidden + text-overflow:ellipsis chopped off the rightmost child (= the timer). Fix: keep the timer as a SIBLING of the strip, not a child. Sits visually next to the counter (same masthead row, after the strip) but is its own flex item with flex-shrink:0 so it never gets squished. Cache version bumped to ?v=23.
…urther Two user-driven changes: 1. Drop #quiz-difficulty and #quiz-counter from the masthead quiz-meta- strip and render them in a new .question-header above the question card instead. Masthead now shows only: cert / domain · [⏱ timer] Above the card now shows: Q1 THIS SESSION · MEDIUM Question identity (which question + how hard) reads as context for the card, not as global chrome competing with cert/domain. 2. Slide-in transition slowed 0.55s → 0.85s with a longer travel (28px → 40px) and a sharper start (opacity ramp from 0 → 0.25 → 1 instead of 0 → 0.4 → 1). The new question-header animates alongside the card at 0.7s so the whole row arrives as one unit. renderQuiz() now class-toggles both .question-header and .question-card to re-trigger the animation per question. Cache version bumped to ?v=24.
1. Cert name in masthead is now a back-to-certifications link.
- Rendered as a <button class="cert-link"> instead of a <span>
- Hover: accent color + underline
- Click triggers confirmBackToCertPicker() — if a session has any
answered questions or an active timer, a confirm() prompt asks
before discarding the in-progress exam (history is always kept;
only session counts + the running timer get reset)
- If confirmed, jumps straight back to the cert picker (step 1)
without forcing a page reload
2. Timer moved from masthead back to the right end of the action bar,
styled as a noticeably bigger pill:
- Value font 1.05rem (was 0.78rem in masthead) so it's readable at
a glance during a timed run
- Larger ⏱ icon + more padding
- Same warning-pulse + expired-locked states
- Divider before it visually separates it from Switch bank link
Cache version bumped to ?v=25.
…right Restructure: #quiz-timer is now a sibling of .actionbar-left and .actionbar-right inside .actionbar-inner. margin-left:auto pushes it to the right edge of the first flex row; .actionbar-right has flex-basis:100% to force the stats + controls onto a second row. Layout: Row 1: [Submit][Skip] 1234·↵·S … [⏱ 29:52] Row 2: Session 0/0 · Bank 85/85 STATS SETTINGS RESET SWITCH BANK Plus the in-flight clickable brand changes are kept (DBX Practice brand goes to all-certs picker with a confirm). The cert-link still goes to that cert's bank picker. Cache version bumped to ?v=26.
Both masthead links lead to a different breadcrumb level, both gated behind the same confirm-leave-exam helper: - DBX Practice brand → renderCertPicker (step 1: pick which cert) - cert name e.g. "DATA ENGINEER ASSOCIATE" → renderBankPicker for that cert's items (step 2: pick Practice / Mock 1 / Mock 2) Refactored the confirm logic into hasActiveSession + resetSessionState + confirmLeaveExam(destinationFn) so the same prompt covers both links. History in localStorage is preserved either way; only the session counters + running timer get reset. Cache version bumped to ?v=27.
1. New --selected / --selected-soft theme tokens (blue: #2563EB light,
#60A5FA dark). The "I picked this choice" state now uses blue instead
of the Databricks red accent. Reason: red conflicted with the
post-submit incorrect state — both looked like "wrong answer".
Semantics now read cleanly:
blue = picked, awaiting submit
green = correct
red = incorrect
2. Settings page redesigned to match the rest of the app:
- Wrapped fields in a .settings-panel card (surface bg + border +
shadow, matching .question-card aesthetic)
- Each field is now a row with a heading + helper description on
the left and the select on the right
- Custom CSS chevron (rotated bordered square) replaces the native
dropdown arrow so it themes consistently
- Focus ring uses --selected (blue) instead of the red accent
- Cancel button added next to Apply at the bottom
- "Customize how you practice…" intro paragraph
- Mobile (≤ 700px): rows stack vertically
Cache version bumped to ?v=28.
…uck radio focus - Custom theme-aware confirm modal with focus trap, ESC/Enter/backdrop handling; replaces native window.confirm() in confirmLeaveExam and resetHistory. Quiz keyboard shortcuts are gated while modal is open. - Fix stuck radio focus outline: kill :focus + :focus-visible with !important and preserve the checked indicator's box-shadow. Browser default focus ring (system accent color) could leak through the previous override for programmatically-focused radios. - Mobile responsive overhaul: new 540px + 380px + landscape breakpoints. Choice text bumped to 16px on phones, denser card padding, full-width action bar buttons, secondary stats hide on tiny screens. - Touch target compliance (≥44px) for actionbar link buttons, brand cluster, cert-link, theme pill (≥36px). - @media (hover: none) safeguards to prevent sticky :hover effects lingering after taps on touch devices. - Replace stopwatch emoji (⏱) with inline SVG for the timer icon. - aria-live="polite" + role="status" on quiz-feedback for screen readers; aria-label on the choices fieldset; role="alertdialog" + aria-describedby on the confirm modal.
…edge The brand-cluster, cert-link, and #btn-theme buttons live inside a sticky masthead with backdrop-filter, which creates a stacking context + filter clipping region. An outline drawn outside the button's own bounds (which is how :focus-visible draws) gets cut off by that region — visually it looks like the bottom border is missing. Fix: replace outline with an in-bounds visual (border-color tint on the existing 1px transparent/hairline border, plus a soft background shift). The indicator now stays inside the button's painted area, so nothing can be clipped.
Editorial touches that lift the design from clean to refined:
- Q.N watermark — faded oversized numeral in the question-card corner.
Padding-right reserved on #quiz-question so long lines never collide
with it; clamp() + lowered opacity ensure it reads as texture.
- Soft radial glow at the top of the body bg via background-attachment:
fixed — replaces flat single-tone bg with subtle depth.
- Editorial eyebrow + lead paragraph on the cert picker — establishes
curated framing before the cards.
- Bigger, sans verdict heading ('Correct' / 'Incorrect …') replacing
the small mono-uppercase that felt under-celebrated.
- Three-line colophon footer: tagline · meta · set notes ('Set in
Geist & Geist Mono · No tracking · Progress stays in your browser').
- text-wrap: balance on h2 / card titles / modal title / lead.
- Per-cert confetti palette: each cert celebrates in its own gradient
stops (DE coral, DE Pro indigo, DA emerald, ML purple-pink, ML Pro
amber-red, GenAI cyan-blue) — cohesive instead of generic candy mix.
- Cert cards now expose data-cert which finally activates the per-cert
gradient strips defined in styles.css (the selector was there, the
attribute was missing). Also surfaces the question-count as a big
numeral with a multi-line label.
Actionbar swap requested by user — primary actions on the right for
thumb-reach (also matches mobile convention):
Row 1: [Timer ←] … [kbd-hint] [Next] [Skip] [Submit →]
Row 2: [session · bank stats · Stats Settings Reset Switch]
HTML source order is Next → Skip → Submit so the rightmost visible
button is always the primary action (Submit before submit, Next after).
On mobile the cluster wraps full-width but justify-content:flex-end
keeps it pinned to the right edge.
v=31.
The Q.N watermark on the question card used 0.035 opacity, which on a near-black ink + near-white surface reads as essentially zero contrast. The previous value was tuned visually on dark mode (where the inverse pairing has more headroom) and the same number was wrong for light. Light: 0.035 → 0.08 Dark: 0.05 → 0.06 (was a touch heavy on the dark surface) Mobile keeps a slightly lower value (0.07 / 0.055) since the smaller glyph reads denser at the same opacity.
… multi-digit Q.N
Layout change:
- actionbar-inner is now a 3-column grid: timer (left) | kbd-hint (centre)
| buttons (right). With equal-fr outer columns, the kbd-hint sits on
true visual centre of the bar regardless of how wide the timer or
buttons cluster grow.
- HTML: kbd-hint moved out of .actionbar-left to become a direct
grid child so it can be addressed by grid-column.
- Mobile (≤720px): kbd-hint is already display:none; mobile collapses
the grid to 2 columns (auto / 1fr) so the buttons get the full
remaining width whether or not the timer is showing.
Watermark fix:
- Q.N watermark was overlapping the wrapped question text once N
reached 2+ digits. Two reinforcing fixes:
* Cap the watermark font-size lower (clamp max 2.1rem, was 2.75rem)
with max-width:6rem + white-space:nowrap so width is bounded.
* Bump #quiz-question's reserved right padding to 6rem desktop /
4rem mobile so wrapped text always clears the watermark zone.
…e timer A completion experience for finishing a bank, persisted history across sessions, weak-area highlighting, exportable summaries, and a pause/resume flow so users can step away mid-exam without losing the run. Summary screen (when every question in the filtered bank has been visited and at least one answered): - Hero block: big tone-coloured percentage + correct/total + verdict badge (PASS/BELOW for mock exams using a 70% threshold; descriptive labels for practice mode). - Per-module breakdown: name + ratio + progress bar tinted by the same tone bands as the hero. - Weak-area callout: domains scoring below 70% are surfaced as 'Focus next on these modules' so users know where to drill. - Time stats (only when the exam timer was running): total time, average per question. Paused time is subtracted so the figures reflect actual effort, not wall-clock. - Best streak metric (always shown when > 0). - Confetti at >= 80% (live summaries only; respects reduced-motion). History: - Each completed session is appended to localStorage under 'dbx-practice-sessions-<cert>' (50-record ring buffer per bank). - Summary screen shows the last six past attempts as a clickable compact list for at-a-glance comparison. - Stats page now lists every past attempt for the current bank with date/time, score, fraction, duration and verdict — clicking any row reopens that attempt's summary in read-only mode. - 'Clear history' button on stats with confirm modal. Export: - HTML export: self-contained printable report with inline styles, the same hero + breakdown + focus-areas + stats layout, downloads as 'dbx-<cert>-<timestamp>.html'. - CSV export: two-section sheet — metadata pairs at top, per-module table at bottom; opens cleanly in Excel/Numbers/Sheets. Pause / resume: - Click the timer pill (or press P) to pause. Click again or press Esc / Enter / Space / outside the resume card to come back. - A backdrop-blur overlay covers the page so the questions aren't readable while the user is away; main content gets pointer-events none + filter:blur so accidental input is blocked. - Paused time accumulates in STATE.totalPausedMs and is subtracted from the session's elapsed-time figure on the summary screen. - Pause state is reset by resetSessionState + when entering a new bank. Plumbing: - show() now knows about the 'summary' section. - renderQuiz redirects to renderSummary on completion instead of silently looping back through the bank. - handleKeydown skips quiz shortcuts while paused or while a confirm modal is open; P is a global toggle when a timer is configured. - APP_VERSION bumped to 34, styles.css to v=34.
…ired timer Three small leave-prompt fixes: - After the summary screen has rendered (summarySaved=true) the user shouldn't be asked 'Leave this session?' when they click the brand link or cert link — the session is already over. - An expired timer ends the exam too, so it shouldn't trigger the leave-confirm either. - When the timer is paused, treat that as an active session even if sessionTotal is still 0 — leaving would lose the pause state.
Color tokens darkened so every text usage meets WCAG AAA (≥7:1 normal, ≥4.5:1 large). Each value annotated with measured ratio in the CSS. Light theme bumps: - --muted: #71717A → #52525B (4.45 → 7.1) - --positive:#15803D → #14532D (4.6 → 9.0) - --negative:#B91C1C → #991B1B (6.4 → 8.2) - --selected:#2563EB → #1D4ED8 (5.7 → 8.6) Dark theme bumps: - --muted: #8B8B95 → #A1A1AA (5.8 → 7.4) - --positive: #4ADE80 → #6EE7B7 (8.5 → 11.2) - --negative: #F87171 → #FCA5A5 (7.5 → 9.7) - --selected: #60A5FA → #93C5FD (7.0 → 10.4) New AAA-safe text-only variants for the vivid brand accent: - --accent-text in light = #9A3412 (7.1:1) - --accent-text in dark = #FFA88B (7.2:1) - --warn in light = #92400E (7.5:1) — for small warning text - --warn in dark = #FBBF24 (11.5:1) The vivid --accent (#FF4F2C light / #FF7A5C dark) stays vivid for graphic/large-display uses (focus rings, gradient strips, big numerals) where 3:1 / AA-Large is sufficient. Small text that used --accent now routes through --accent-text: - .eyebrow - .difficulty.medium (was 2.8:1 against accent-soft bg — failed even AA) - .actionbar-timer.warning .timer-value - footer-meta a:hover - summary-export button.link:hover - .brand-cluster:hover .brand-title - .cert-link:hover / :focus-visible Hardcoded #D97706 warn replaced with var(--warn) across: - .summary-hero[data-tone='warn']::before - .summary-num[data-tone='warn'] - .summary-verdict[data-tone='warn'] - .summary-meter-fill[data-tone='warn'] - .attempt-pct[data-tone='warn'] - HTML export inline stylesheet Primary button hover redesigned: was background-swap to vivid accent (white-on-coral fails AA in dark mode at 2.7:1). Now keeps the AAA- strong idle colours and adds elevation + an accent-coloured glow as the only visual cue. Destructive modal button hardcoded #991B1B → #7F1D1D hover; semantic --negative token is too light in dark mode for white-text-on-button. Rich aria-labels on past-attempt rows so screen readers announce the full context in one phrase: 'View attempt from 21 May 2026 at 14:35, scored 85 percent, 38 of 45 correct, took 24:15, passed' instead of reading each cell separately. Settings selects now properly associated with their h4 + helper text via aria-labelledby + aria-describedby. Mobile timer-pause discoverability: surface the pause glyph at opacity 0.45 on touch devices (no hover state) so users can see the timer is interactive without tapping blind. APP_VERSION + styles.css bumped to v=35.
3 tasks
kengio
added a commit
that referenced
this pull request
May 21, 2026
…that shipped in PR #29 / #30 (#31) Per the README & CLAUDE.md Sync Rule in CLAUDE.md, the practice-quiz overhaul that landed in PR #29 (mock exams + summary + history + export + pause + AAA) shipped without a corresponding doc update for the new user-facing surfaces. This PR closes that gap. CHANGELOG.md - New entry '2026.05.21-25 — Practice quiz: completion summary, attempt history, export, pause/resume, WCAG AAA' covering everything that rode along with the mock-exam launch but is its own feature surface. Includes the AAA contrast-ratio tables (before/after, both themes), the actionbar grid layout, the export formats, and the pause/resume effort-time accounting. - Existing '2026.05.21-24' entry stays as the mock-exam launch. practice/README.md - New 'Features' section listing every user-visible capability (two-step picker, adaptive selector, timer + pause/resume, completion summary, attempt history, HTML/CSV export, keyboard map, theme, AAA, mobile). - 'Privacy' section expanded with the two localStorage key families (per-question + completed sessions) and the new summary-export route. - 'Caveats' counts updated (was '198 + 573'; now '364 + 567 = 931'). README.md - Top-level live-link blurb expanded from 'Light/dark/auto theme + accessible reset' to enumerate the new features so casual visitors know what's there without clicking through. CLAUDE.md - No change. The sync table doesn't require updates for adding banks to existing certs, and the one-line tree-diagram comment for 'practice/' is still accurate at that level of abstraction. i18n/th/STATUS.md - No change. Top-level README.md is already tracked as ⏳ (not yet translated, so no Thai counterpart to mark stale). CHANGELOG.md and CLAUDE.md are ❌ policy (never translated). practice/README.md isn't in the Thai scope.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two big wins requested in this session:
Combined totals went from 364 q / 6 banks to 931 q / 18 banks.
Three-round self-review (per CONTRIBUTING.md)
Round 1 — Technical correctness
python3 practice/build.py --check)correctAnswer∈ A-D, all 4 choices present,difficulty∈ easy/medium/hard)node --check practice/app.jspassesdeploy-practice.ymlRound 2 — Factual / blueprint accuracy
<Cert Name> — Mock Exam NRound 3 — Style & conventions
setup-python@v5step (ubuntu-latest pre-installs python3)[YYYY.MM.DD-N]formatWhat changed
practice/build.pybuild_mock(cert, exam_n)— pre-scans the mock-examquestions.mdfor## <Domain> (Questions X-Y)headings to build a qnum→domain map, then reuses the existingparse_questions()and overlays the per-question domain--kind {practice,mock,all}flag (defaultall)practice/app.jsKNOWN_BANKSdeclared via spread from a CERTS array — three groups (practice, mock-1, mock-2) generated programmaticallyrenderSetup()groups bank cards under labelled section headingsAPP_VERSIONbumped to4for cache-bustingpractice/styles.css.bank-group-headinguppercase + letter-spacing label style.github/workflows/deploy-practice.ymlcertifications/**/practice-questions/**,certifications/**/mock-exam/**,certifications/**/mock-exam-2/**python3 practice/build.pyruns before artifact upload, so deploy always reflects current cert markdownDocs
practice/README.md— Available banks table split into Practice vs Mock sections; Deploy section explains the auto-rebuild flowCHANGELOG.md—[2026.05.21-24]entryAfter-merge expectation
The deploy workflow auto-runs (workflow file change triggers it). Within ~1 min the live site at https://kengio.github.io/databricks-certification-study-guide/ shows:
Future edits to any practice or mock-exam markdown will auto-redeploy on push to main.
Test plan
python3 practice/build.py --check→ 18 banks / 931 q, no errorsnode --check)Follow-up (separate PR, not this one)
Per user comment "Anki ทำเป็น interactive ด้วยได้มั้ยบน github page" — yes, possible via a
/flashcards/route with SM-2 spaced repetition. Doable in ~300 lines vanilla JS. Will scope as a separate PR to keep this one focused.