Skip to content

Commit b480b69

Browse files
authored
feat(practice): all 6 cert banks + theme + reset + branding + code blocks + Node 24 CI
Big polish PR for the practice quiz. Adds DA Associate, ML Pro, and GenAI banks (parser extended to 3 heading formats). Adds reset button in quiz header, theme cycle (auto/light/dark), favicon + branded SVG, fenced-code-block rendering, hide-question-title-until-submit, and Node 24 opt-in for CI workflows. 364 questions total across 6 banks. CHANGELOG [2026.05.21-23].
1 parent 848df2c commit b480b69

14 files changed

Lines changed: 3186 additions & 59 deletions

.github/workflows/deploy-practice.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ on:
88
- '.github/workflows/deploy-practice.yml'
99
workflow_dispatch:
1010

11+
env:
12+
# Opt into Node.js 24 for all JavaScript actions ahead of the June 2026
13+
# deprecation deadline. See lint.yml for the same rationale.
14+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
15+
1116
permissions:
1217
contents: read
1318
pages: write

.github/workflows/lint.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ on:
1717
- 'lychee.toml'
1818
- '.github/workflows/lint.yml'
1919

20+
env:
21+
# Opt into Node.js 24 for all JavaScript actions ahead of the June 2026
22+
# deprecation deadline. Eliminates the "Node.js 20 actions are deprecated"
23+
# annotations and lets us keep using current action versions until each
24+
# vendor publishes a Node-24-native release.
25+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
26+
2027
permissions:
2128
contents: read
2229

CHANGELOG.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,71 @@ Notable changes to the Databricks Certification Study Guide.
44

55
The format is loosely based on [Keep a Changelog](https://keepachangelog.com/). Dates use ISO 8601. Each section is grouped under the date the change shipped, with the Databricks exam-guide version each affected certification tracks.
66

7+
## [2026.05.21-23] — Practice quiz polish: all 6 banks + theme + reset + branding + CI Node 24
8+
9+
### Added — all 6 certifications now have a question bank (364 questions total)
10+
11+
The practice quiz now covers every certification:
12+
13+
| Bank | Questions | Domains |
14+
| :--- | :---: | :---: |
15+
| Data Engineer Associate | 85 | 5 |
16+
| Data Engineer Professional | 73 | 8 |
17+
| **Data Analyst Associate** | **57** | **5** |
18+
| ML Associate | 46 | 4 |
19+
| **ML Professional** | **57** | **4** |
20+
| **GenAI Engineer Associate** | **46** | **4** |
21+
| **Total** | **364** | **30** |
22+
23+
Previous total: 198 (3 banks). **Net addition: 166 questions across 3 new banks + 6 in the ML Associate bank.**
24+
25+
### Changed — `practice/build.py` parser now handles three heading formats
26+
27+
The three certs previously missing (DA Associate, ML Pro, GenAI Engineer Associate) used a different heading convention. The parser is now format-tolerant:
28+
29+
- **Format A** (existing): `## Question 5: Title` with `**Question** *(Easy|Medium|Hard)*: stem` in body
30+
- **Format B** (new): `## Question 5 *(Medium)*: Title` with `**Question**: stem` in body
31+
- **Format C** (new): `## Question 5: Title *(Medium)*` with the stem directly in the body (or `**Question**:` prefix)
32+
33+
The correct-answer regex was also relaxed to accept `**Correct Answer: B**`, `**Correct Answer: B) full choice text**`, and `**Correct Answer:** B`. The choice-line regex permits trailing whitespace (some files use two-space line breaks).
34+
35+
### Added — UI polish
36+
37+
- **Branded header**: SVG checkmark badge in Databricks red (`#FF3621`) alongside the page title; same icon as `favicon.svg` so the browser tab matches the page.
38+
- **`favicon.svg`**: served from `practice/favicon.svg`, linked from `index.html`.
39+
- **Theme cycle button** in the top-right of the header: cycles **auto → light → dark → auto**. Persisted in `localStorage` under `dbx-practice-theme`. Applied before any rendering so there's no flash on load.
40+
- **Reset button** in the quiz header (`class="danger"`) — same `resetHistory()` flow as the existing button in the Stats view, now reachable without navigating away from the quiz. Confirm-dialog protects against accidental clicks.
41+
- **"Switch bank"** label replaces "Exit" so the button's effect is clearer.
42+
43+
### Changed — CSS theme architecture
44+
45+
`styles.css` switched from a single `@media (prefers-color-scheme: dark)` override to a three-state system:
46+
47+
- `:root` defaults are light
48+
- `:root[data-theme="dark"]` forces dark regardless of system preference
49+
- `:root[data-theme="light"]` forces light regardless of system preference
50+
- `:root[data-theme="auto"]` (and the no-attribute fallback) inherits from `@media (prefers-color-scheme: dark)`
51+
52+
This lets users override their OS preference, which is the common ask for night-mode browsing on a system-light setup.
53+
54+
### Changed — CI/CD opt-in to Node.js 24
55+
56+
Adds `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"` as a workflow-level `env:` to both:
57+
58+
- `.github/workflows/lint.yml`
59+
- `.github/workflows/deploy-practice.yml`
60+
61+
Eliminates the "Node.js 20 actions are deprecated" annotations on every CI run (effective deprecation June 2026 per the GitHub Actions changelog). All action versions are unchanged; the env var instructs the runner to host them on Node 24 regardless of their declared runtime.
62+
63+
### Verification
64+
65+
- `python3 practice/build.py --check`**6 banks / 364 questions** parsed cleanly
66+
- JSON shape validated: all `correctAnswer` letters in A-D, all 4 choices present, all `difficulty` values lowercase easy/medium/hard
67+
- markdownlint passes
68+
- lychee passes (favicon.svg is a same-origin file link, all relative paths resolve)
69+
70+
After merge: the practice site auto-redeploys via `.github/workflows/deploy-practice.yml` and the 3 new banks appear on the live page.
71+
772
## [2026.05.21-22] — Deploy practice quiz to GitHub Pages
873

974
### Added

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ tags:
4848
> **These are the exam notes I built while preparing for the Databricks certification series.** Cheat sheets, practice questions, mock exams, and end-to-end worked examples — now open-sourced under MIT so the next person doesn't have to start from scratch. Every certification section maps to the current official Databricks exam guide for that credential. If it helped you, ⭐ the repo and pass it on.
4949
5050
> [!tip]
51-
> **[Try the practice quiz live](https://kengio.github.io/databricks-certification-study-guide/)** — adaptive multiple-choice quiz, 198 questions across 3 cert tracks, runs in your browser, progress saved locally. No install, no signup.
51+
> **[Try the practice quiz live](https://kengio.github.io/databricks-certification-study-guide/)** — adaptive multiple-choice quiz, **364 questions across all 6 cert tracks**, runs in your browser, progress saved locally. No install, no signup. Light/dark/auto theme + accessible reset.
5252
5353
> [!info]
5454
> **Read in another language**: 🇹🇭 [ภาษาไทย](./i18n/th/README.md) (in progress) · For other languages and the translation policy, see [`TRANSLATING.md`](./TRANSLATING.md) and [`i18n/README.md`](./i18n/README.md).
@@ -411,7 +411,7 @@ This guide ships as a living resource. The roadmap below is what's planned for t
411411
-**Renewal guide** for candidates whose 2-year validity expires in 2027–2028 — complete (see [`shared/appendix/renewal-guide.md`](./shared/appendix/renewal-guide.md))
412412
-**Translation scaffolding** — Thai in-tree, other languages via fork model — complete (see [`TRANSLATING.md`](./TRANSLATING.md) + [`i18n/`](./i18n/README.md))
413413
-**Spaced-repetition deck (Anki)** — markdown-source decks + stdlib-only builder, two starter decks (Delta Lake 27 cards, Unity Catalog 22 cards) — complete (see [`anki/README.md`](./anki/README.md))
414-
-**Adaptive practice questions** — static-only web quiz with localStorage progress tracking + adaptive selector across 3 cert banks (198 questions). **Live at [kengio.github.io/databricks-certification-study-guide](https://kengio.github.io/databricks-certification-study-guide/)** — complete (see [`practice/README.md`](./practice/README.md))
414+
-**Adaptive practice questions** — static-only web quiz with localStorage progress tracking + adaptive selector across **all 6 cert banks (364 questions)**. **Live at [kengio.github.io/databricks-certification-study-guide](https://kengio.github.io/databricks-certification-study-guide/)** — complete (see [`practice/README.md`](./practice/README.md))
415415

416416
Legend: ✅ done · 🔄 in progress / next up · ⏳ planned · 🌱 ideas being explored
417417

practice/README.md

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@ A browser-based quiz for the practice-question markdown files in this repo. Trac
2121

2222
## Available banks
2323

24+
All six certifications now have a question bank — **364 questions total**.
25+
2426
| Bank | Source files | Questions |
2527
| :--- | :---: | :---: |
2628
| Data Engineer Associate | 5 markdown files in `certifications/data-engineer-associate/resources/practice-questions/` | 85 |
2729
| Data Engineer Professional | 8 markdown files | 73 |
28-
| ML Associate | 4 markdown files | 40 |
29-
30-
Banks for the other 3 certs (Data Analyst Associate, ML Professional, GenAI Engineer Associate) need their practice-question markdown to adopt the same difficulty-marker format — see [Adding a cert to the bank](#adding-a-cert-to-the-bank) below.
30+
| Data Analyst Associate | 5 markdown files | 57 |
31+
| ML Associate | 4 markdown files | 46 |
32+
| ML Professional | 4 markdown files | 57 |
33+
| GenAI Engineer Associate | 4 markdown files | 46 |
3134

3235
## How to run
3336

@@ -108,23 +111,43 @@ A weighted random pick chooses the next question. After a full pass through the
108111

109112
| File | Purpose |
110113
| :--- | :--- |
111-
| `index.html` | App skeleton |
112-
| `app.js` | Quiz logic, adaptive selector, localStorage, stats |
113-
| `styles.css` | Minimal styling (light + dark via `prefers-color-scheme`) |
114+
| `index.html` | App skeleton, branded header with checkmark badge, favicon |
115+
| `app.js` | Quiz logic, adaptive selector, localStorage, stats, theme cycle |
116+
| `styles.css` | Minimal styling with theme variables (auto / light / dark) |
117+
| `favicon.svg` | Browser tab icon (also displayed in the page header) |
114118
| `build.py` | Markdown → JSON converter (Python 3.9+ stdlib only) |
115119
| `data/<cert>.json` | Generated question banks (committed so the static page Just Works) |
116120
| `format.md` | Markdown source format spec that `build.py` parses |
117121
| `README.md` | This file |
118122

123+
## Theme
124+
125+
The page ships with three theme states: **auto** (follows your OS dark-mode preference), **light**, and **dark**. The button in the top-right of the header cycles through them. Your choice is saved in `localStorage` and applied immediately on every visit.
126+
119127
## Adding a cert to the bank
120128

121-
The three currently-supported certs all use this exact markdown shape in their practice-question files:
129+
The parser supports three heading formats, all with the same body shape (choices + answer callout):
122130

123131
```markdown
132+
# Format A — difficulty in body
124133
## Question 5: Brief title
125134

126-
**Question** *(Easy|Medium|Hard)*: Stem text. Multi-line is OK.
135+
**Question** *(Easy|Medium|Hard)*: Stem text.
136+
137+
# Format B — difficulty between number and colon
138+
## Question 5 *(Medium)*: Brief title
139+
140+
**Question**: Stem text.
127141

142+
# Format C — difficulty appended to title
143+
## Question 5: Brief title *(Medium)*
144+
145+
Stem text (or `**Question**: Stem text.`).
146+
```
147+
148+
Choices + answer callout (identical across all three formats):
149+
150+
```markdown
128151
A) First choice
129152
B) Second choice
130153
C) Third choice
@@ -140,13 +163,16 @@ D) Fourth choice
140163
---
141164
```
142165

143-
The unsupported certs (DA Associate, ML Pro, GenAI) use older variants without the `*(Easy|Medium|Hard)*` difficulty marker. To bring one into the bank:
166+
To add a new cert (e.g., a future Databricks cert):
144167

145-
1. Update the cert's practice-question markdown to match the format above (add a difficulty marker to each question).
146-
2. Run `python3 practice/build.py --cert <cert-id>` and verify the question count matches your expectation.
168+
1. Make sure every question in the practice-question files uses one of the three formats above with all four `A) B) C) D)` choices and a `**Correct Answer: <letter>**` line inside the answer callout.
169+
2. Run `python3 practice/build.py --cert <cert-id>` and check the parsed count.
147170
3. Add a row to "Available banks" above.
148171
4. Add the cert to the `KNOWN_BANKS` list at the top of `app.js`.
149-
5. Open a PR.
172+
5. Add a tuple to `CERT_TITLES` in `build.py` (display name + blueprint version).
173+
6. Open a PR.
174+
175+
A handful of questions where a choice contains a multi-line code block are skipped (the parser sees the bare `A)` / `C)` line with no inline content). If that affects your bank, restructure the choice to inline the code or move the code block into the answer explanation.
150176

151177
## Contributing
152178

practice/app.js

Lines changed: 91 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,16 @@
2727
const KNOWN_BANKS = [
2828
{ cert: "data-engineer-associate", file: "data/data-engineer-associate.json" },
2929
{ cert: "data-engineer-professional", file: "data/data-engineer-professional.json" },
30+
{ cert: "data-analyst-associate", file: "data/data-analyst-associate.json" },
3031
{ cert: "ml-associate", file: "data/ml-associate.json" },
32+
{ cert: "ml-professional", file: "data/ml-professional.json" },
33+
{ cert: "genai-engineer-associate", file: "data/genai-engineer-associate.json" },
3134
];
3235

3336
const STORAGE_PREFIX = "dbx-practice-";
37+
const THEME_KEY = "dbx-practice-theme";
38+
const THEMES = ["auto", "light", "dark"];
39+
const THEME_ICONS = { auto: "🖥️", light: "☀️", dark: "🌙" };
3440
const STATE = {
3541
bank: null,
3642
history: {},
@@ -111,20 +117,60 @@
111117
}
112118
}
113119

120+
const FENCE_OPEN_RE = /^\s*```(\w*)\s*$/;
121+
const FENCE_CLOSE_RE = /^\s*```\s*$/;
122+
114123
function renderMarkdown(s) {
115124
const frag = document.createDocumentFragment();
116-
const paragraphs = s.split(/\n\s*\n/);
117-
for (const para of paragraphs) {
118-
const trimmed = para.trim();
119-
if (!trimmed) continue;
120-
const p = el("p");
121-
const lines = trimmed.split("\n");
122-
for (let i = 0; i < lines.length; i++) {
123-
renderInline(lines[i], p);
124-
if (i < lines.length - 1) p.appendChild(el("br"));
125+
const lines = s.split("\n");
126+
let buffer = [];
127+
128+
const flushParagraphs = () => {
129+
// `buffer` is a slab of lines; split it into paragraphs by blank lines
130+
// and render each as a <p> with inline formatting + <br> for newlines.
131+
let para = [];
132+
const emit = () => {
133+
if (para.length === 0) return;
134+
const p = el("p");
135+
for (let j = 0; j < para.length; j++) {
136+
renderInline(para[j], p);
137+
if (j < para.length - 1) p.appendChild(el("br"));
138+
}
139+
frag.appendChild(p);
140+
para = [];
141+
};
142+
for (const ln of buffer) {
143+
if (ln.trim() === "") emit();
144+
else para.push(ln);
145+
}
146+
emit();
147+
buffer = [];
148+
};
149+
150+
let i = 0;
151+
while (i < lines.length) {
152+
const fence = lines[i].match(FENCE_OPEN_RE);
153+
if (fence) {
154+
flushParagraphs();
155+
const codeLines = [];
156+
i++;
157+
while (i < lines.length && !FENCE_CLOSE_RE.test(lines[i])) {
158+
codeLines.push(lines[i]);
159+
i++;
160+
}
161+
// Skip the closing fence (if present)
162+
if (i < lines.length) i++;
163+
const code = el("code");
164+
code.textContent = codeLines.join("\n");
165+
const pre = el("pre");
166+
pre.appendChild(code);
167+
frag.appendChild(pre);
168+
continue;
125169
}
126-
frag.appendChild(p);
170+
buffer.push(lines[i]);
171+
i++;
127172
}
173+
flushParagraphs();
128174
return frag;
129175
}
130176

@@ -293,7 +339,10 @@
293339
diff.textContent = q.difficulty;
294340
diff.className = "difficulty " + q.difficulty;
295341

296-
$("#quiz-title").textContent = q.title;
342+
// Note: question title is intentionally NOT rendered on the quiz card —
343+
// many titles paraphrase the answer and would give it away. The title is
344+
// still used in Stats → "Questions you're working on" and in the feedback
345+
// panel shown after the user submits.
297346

298347
const qBody = $("#quiz-question");
299348
clear(qBody);
@@ -362,6 +411,10 @@
362411
clear(fb);
363412
fb.appendChild(el("h4", {},
364413
correct ? "✓ Correct" : `✗ Incorrect — correct answer: ${q.correctAnswer}`));
414+
if (q.title) {
415+
fb.appendChild(el("p", { className: "fb-topic" },
416+
el("strong", {}, "Topic: "), q.title));
417+
}
365418
if (q.shortAnswer) {
366419
const p = el("p");
367420
p.appendChild(renderInlineToFragment(q.shortAnswer));
@@ -492,19 +545,46 @@
492545
show("quiz");
493546
}
494547

548+
// --- Theme toggle --------------------------------------------------------
549+
550+
function loadTheme() {
551+
try {
552+
const t = localStorage.getItem(THEME_KEY);
553+
return THEMES.includes(t) ? t : "auto";
554+
} catch (_) { return "auto"; }
555+
}
556+
557+
function applyTheme(theme) {
558+
document.documentElement.setAttribute("data-theme", theme);
559+
const iconNode = $("#btn-theme-icon");
560+
if (iconNode) iconNode.textContent = THEME_ICONS[theme];
561+
}
562+
563+
function cycleTheme() {
564+
const current = loadTheme();
565+
const next = THEMES[(THEMES.indexOf(current) + 1) % THEMES.length];
566+
try { localStorage.setItem(THEME_KEY, next); } catch (_) { /* ignore */ }
567+
applyTheme(next);
568+
}
569+
495570
// --- Init ----------------------------------------------------------------
496571

497572
function init() {
573+
// Apply persisted theme before anything renders so there's no flash
574+
applyTheme(loadTheme());
575+
498576
$("#btn-submit").addEventListener("click", submitAnswer);
499577
$("#btn-next").addEventListener("click", renderQuiz);
500578
$("#btn-stats").addEventListener("click", () => { renderStats(); show("stats"); });
501579
$("#btn-stats-back").addEventListener("click", () => show("quiz"));
502580
$("#btn-export").addEventListener("click", exportProgress);
503581
$("#btn-reset").addEventListener("click", resetHistory);
582+
$("#btn-reset-top").addEventListener("click", resetHistory);
504583
$("#btn-settings").addEventListener("click", () => show("settings"));
505584
$("#btn-settings-cancel").addEventListener("click", () => show("quiz"));
506585
$("#btn-settings-apply").addEventListener("click", applySettings);
507586
$("#btn-exit").addEventListener("click", () => location.reload());
587+
$("#btn-theme").addEventListener("click", cycleTheme);
508588

509589
probeBanks().then(renderSetup);
510590
}

0 commit comments

Comments
 (0)