Add the Academy: narrated, animated roasting & brewing lessons; cupping link on roast detail
Test and deploy / test-and-deploy (push) Successful in 53s
Test and deploy / test-and-deploy (push) Successful in 53s
Academy (/academy, 'Learn' nav group): - 10 lessons / 45 slides across two tracks. Roasting: seed anatomy and composition, drying & the turning point, Maillard, first crack & development (DTR), curve/RoR reading (crash, flick, stall), defects → one-change discipline. Brewing: extraction physics (dissolving order, yield, grind, temp/time/ratio), then technique per brewer family — immersion (French press, AeroPress, Clever/Hario Switch, cold brew, cupping), percolation (bloom, V60, Chemex/Kalita, batch, percolator), espresso & pressure (puck prep, shot reading, dialing in, moka) - One cohesive scene system: 25 parameterized animated SVG scenes drawn from the app's palette (beans, curves, phase bars, brewers reusing the silhouette library), CSS keyframe animations, reduced-motion support - Narration: Azure TTS (en-US-Andrew HD, eastus) pre-generated to 45 committed MP3s by scripts/generate-academy-audio.mjs; slides speak phonetic respellings (my-YARD, KEM-ex, MOH-kah…) while captions show normal spelling; hands-free autoplay advances after each clip - Player: slide dots, keyboard arrows/space, per-slide captions Roast detail: 'Open cupping session' button next to cup notes (opens or creates the linked plan's session) — its score and flavors flow into the updated .alog download. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
0b4857c225
commit
64c1b3605b
@@ -423,6 +423,29 @@ test("gear: owned brewers/grinders round-trip, validate, and stay private", asyn
|
||||
assert.deepEqual((await stranger.get("/api/gear")).body.gear.brewers, []);
|
||||
});
|
||||
|
||||
test("academy shell page requires a session and lesson audio ids all exist", async () => {
|
||||
const { agent } = await setup();
|
||||
const anonymous = await agent.get("/academy");
|
||||
assert.equal(anonymous.status, 302);
|
||||
assert.equal(anonymous.headers.location, "/login");
|
||||
await signup(agent, "[email protected]");
|
||||
const page = await agent.get("/academy");
|
||||
assert.equal(page.status, 200);
|
||||
assert.match(page.text, /Academy/);
|
||||
// Every slide must have committed narration audio — a missing file would silently break
|
||||
// the hands-free autoplay flow.
|
||||
const { ACADEMY_TRACKS } = await import("../public/js/academy-content.js");
|
||||
const { existsSync } = await import("node:fs");
|
||||
for (const track of ACADEMY_TRACKS)
|
||||
for (const lesson of track.lessons)
|
||||
for (const slide of lesson.slides)
|
||||
assert.equal(
|
||||
existsSync(new URL(`../public/academy-audio/${slide.id}.mp3`, import.meta.url).pathname),
|
||||
true,
|
||||
`missing audio for ${slide.id}`,
|
||||
);
|
||||
});
|
||||
|
||||
test("per-user export and openapi spec", async () => {
|
||||
const { agent } = await setup();
|
||||
const { csrf } = await signup(agent, "[email protected]");
|
||||
|
||||
Reference in New Issue
Block a user