The Velvet Room (50 stars) now ships in every build like the bar and
arena: 4 reactive crowd loops, 2 stingers, and a balcony flyover intro
rendered from the AXA Music Stage scene (110 spectators, state-scaled
stage washes over the venue's own neon). Audio files are dive-bar
placeholders until club-scale recordings land.
The installed/delete test now asserts bundled-fallback semantics:
with every venue bundled, deleting a downloaded pack reveals the
bundled copy instead of uninstalling.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Feedback Arena (150 stars) now ships in every build like the bar:
4 reactive crowd loops, 2 stingers, and a flyover intro rendered
from the UE5 arena scene (200 spectators + 396-body intro fill,
state-reactive rig lighting). Served by the existing bundled-pack
fallback; venues.json unchanged. Audio files are dive-bar
placeholders until arena-scale recordings land.
Largest file is 89MB — future re-renders must stay under GitHub's
100MB hard limit.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(career): Gold tier — a family-style goldImprov artifact upgrades an earned badge
The drill-state relay's goldImprov map (virtuoso gold_improv mints,
gained-only merged like drill nodes) turns an earned badge gold when the
passport's genre — or its genre family — has a verified improv artifact.
Gold never substitutes for the badge bar: gold-without-bronze stays
in_progress.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(career): Gold tier frontend — relay, ceremony, slam, gold ink everywhere
The drill-state relay now carries virtuoso's goldImprov map; a badge that
comes back gold gets its own ceremony + notification (tier-suffixed seen
ids — the bronze moment stays seen under its legacy id, a gold slam marks
both), a gold stamp slam in the book, gold ink on the shelf-cover mini
stamp, and the real gold foil chip. The bronze page's dashed 'Gold rung
coming' preview becomes a live invitation to jam the style in Virtuoso.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(career): gold review fixes — family-space style matching, intake guards, rail counter
The review's showstopper: virtuoso mints goldImprov under raw
STYLE_PALETTES ids ('punk', 'djent', 'disco'), which are mostly NOT
family keys — the tier check now matches in family space (artifact style
and passport genre bucket through the same _genre_family keyword match),
so a 'punk' gold reaches a 'punk rock' passport. Also: non-dict
goldImprov 400s loudly instead of silently dropping; evidence-free
artifacts (no verifier) never mint; goldImprov gets the same pre-merge
size bound byNode has (junk under the cap could otherwise persist
forever and wedge every later relay at the post-merge check); the
instrument-rail badge counter counts gold (earning gold no longer made a
badge vanish from the rail); first-artifact-wins is now asserted against
the persisted snapshot instead of vacuously.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(career): gigs frontend — poster, runner strip, summary, encore
Career v3, WS3 (frontend half), rebuilt cleanly on merged main (v3-a/b/c
in) after git interleaved the structurally-similar canvas functions:
- Book a gig from any opened passport: /gigs/propose renders as a GIG
POSTER (venue presents GENRE NIGHT, numbered bill) with re-roll and
Save/Copy poster (natively-drawn canvas via blob-io, audible failure
paths, slash-safe filenames).
- Play the gig: venue override + Venue viz handoff, then
playQueue.start(..., {source:'gig'}) — the queue's auto-advance runs
the set; zero new playback machinery.
- Floating gig strip (body-level, pointer-events none, z 35 per the
chrome invariant) tracks set position and names what's next.
- Completion = song:ended with an empty queue → POST /gigs → summary
poster overlay with per-song accuracies; encore fires the crowd
celebrate + confetti (reduced-motion: neither). song:stop with a dead
queue = abandoned (no log); end-of-song teardown (queue still active)
must NOT abandon.
- Gigs played render as dated rows in the passport book.
vm tests: runner advance/abandon semantics via the queue-state seam.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(career): stage restore survives a setViz throw; size-register row
CodeRabbit on #956: a setViz failure nulled the restore snapshot AFTER
the overrides were written, permanently borrowing the stage — snapshot
now captured before any write, write failures keep it intact. Also
registers career screen.js in docs/size-exemptions.md 'Planned, not
exempt' (1,516 lines; max-lines WARNS non-blocking — the split plan
needs Byron's sign-off).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: register career screen.js in the size register (planned, not exempt)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(career): gigs backend — propose a setlist, log the completed set
Career v3, WS3 (backend half). A gig is career's verb:
- POST /gigs/propose {instrument, genre, size}: setlist from the
passport's own stubs — qualifying songs (per the genre's badge bar,
family-aware) shuffled for a free re-roll, topped with the
highest-accuracy near-bar songs as stakes, and filled from UNPLAYED
genre songs when the passport is young (the first gig is how stubs
start). Names the highest venue the current stars can book.
- POST /gigs: logs a COMPLETED set only (abandoned sets never log — no
fail state). Per-song accuracy = MAX(last_accuracy) from song_stats,
freshly written by the set's own plays; encore = avg ≥ the data-driven
bar (passports.json gig.encore_accuracy, 0.75). Appends to the career
state file (same atomic _save_json pattern).
- Passports view: per-passport gigs (newest first, capped 20) and
per-instrument gig_count — the profile wall's gig line lights up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(career): gig backfill offsets by qualifying taken, not picks length
CodeRabbit on #954: after the stakes loop appends near-bar songs,
qualifying[len(picks):] overshoots and skips eligible qualifying songs
— a stocked passport could still get a short set.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(career): profile passport wall, home career card, shareable PNG card
Career v3, WS2. The identity artifact leaves the plugin tab:
- Profile: #v3-profile-passports-mount (core, one div) filled by career
on v3:profile-rendered — per-instrument shelves of earned covers,
hours, gig count, open-career link. Absent-not-empty.
- Home: the plugin-count stat tile becomes #v3-dash-career-slot with the
old stat as fallback content; career replaces it with a trading-card
tile (leather + foil shine, badge count, hours, closest-stamp ask) on
the existing v3:dashboard-rendered event.
- Shareable card: static/js/blob-io.js (downloadBlob lifts the idiom
duplicated verbatim in settings-io/diagnostics-export — both
refactored; copyImageBlob wraps ClipboardItem, returns false to signal
the download fallback). Earned passports get Save/Copy card: a
natively-drawn 480×640 canvas (leather, stamp ring, stubs+hours line);
copy falls back to download with a notice when the clipboard refuses.
- Mount-point convention documented in docs/plugin-v3-ui.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(career): external surfaces stay absent until a passport exists
CodeRabbit on #955: a bare commitment produced a zero-passport wall and
replaced the dashboard fallback. Docs also now say mounts may hold
fallback content and plugins REPLACE, never append.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(career): practice invitations — closest stamps + bring-these-up
Career v3, WS1. The passport now points at the practice that pays:
- Stubs carry next_star_at (the same primitive _stars() uses) and each
passport exposes `nearest`: the top 3 non-qualifying songs by distance
to their next star, in the worklist order.
- "Closest stamps" strip above the shelf: the in-progress graded
passports nearest to minting, each row naming the ask — N more songs
(with the nearest title + best %) or the blocking Virtuoso drill.
Rows open the passport.
- "Bring these up" list on the stubs page of in-progress passports;
earned pages stay memorabilia (no homework on a won badge).
- Invitation-voiced throughout: no meters, no completion pressure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(test): comment says qualifying-bar ranking, matching the assertion
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The enrichment fallback made the passport rack real (hundreds of MB
sub-genres) but only the five exact umbrella keys carried Virtuoso
drills. Genres now resolve to a family by keyword substring (MB's
vocabulary is open — 'metalcore' must hit metal without an alias),
first-match-wins in list order ('blues rock' → blues), and inherit the
family's requirement from the same genres map. Exact entries still win;
per-instrument scoping unchanged; unmatched genres stay songs-only.
Data: families for metal (incl. djent/grindcore/thrash/doom), blues,
jazz (bebop/swing/bossa), funk (disco), rock (punk/grunge/shoegaze).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(career): passport visuals pack — tilt, emerging ink, gold foil
Career v2, WS4. All CSS + a rAF-throttled pointer handler, no deps:
- Trading-card tilt on EARNED artifacts only (shelf covers + the badge
page stamp): pointer-tracked perspective rotateX/Y with a glint sweep
following the pointer. The cover's jitter rotation moves into a CSS
var so the tilt transform composes with it; the blanket cover-hover
translate is scoped :not(.pp-tilt) so it can't fight the tilt.
Hover-capable pointers only; off under prefers-reduced-motion.
- Emerging-stamp ink: the ghost stamp fills by qualifying/required via
a conic-gradient — the stamp visibly carves in, no numbers added.
- Gold foil preview: the "coming" note gains a dashed foil chip with a
periodic shimmer — honest, never earnable-looking.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(career): tilt rAF race + freshly-slammed stamp becomes a card
CodeRabbit on #944: a queued tilt frame closed over the departed card
and re-applied vars after pointerleave; and the just-slammed stamp
never regained pp-tilt until the next open.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Career v2, WS3. Bronze in blues/rock/metal/funk/jazz now also asks for
the genre's signature Virtuoso drill, data-driven in passports.json:
blues_shuffle, rock_power_backbeat, melodic_metal_gallop,
sixteenth_pocket, vl_shells — with career-side display labels the
passport page renders instead of raw node ids.
- virtuoso_nodes becomes {instrument: [node_ids]} so a keys passport
never demands a guitar drill; a flat list keeps meaning guitar
(virtuoso's content is guitar-first).
- _node_cleared also accepts keysCleared (a top-tier clean pass in one
key — virtuoso's FIRST gained-only artifact). The depth rungs
additionally require a maxed speed tier, too high a bar for Bronze.
- Genres without a curated entry stay songs-only.
Note: pre-release behavior change — v1 passports aren't in any shipped
build, so no earned badge can demote in the wild.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Career v2, WS2. Nothing measured play time before (the achievements
plugin's final-position shortcut double-counts loops and mis-reads
seeks). Now:
- stats-recorder.js accrues WALL-CLOCK seconds across song:play/resume ↔
pause/stop/ended spans (single spans clamp at 2h against suspend
inflation) and piggybacks them as `seconds` on the POSTs it already
sends; failed POSTs restore the accumulator; a session reset flushes
first so time can't re-attribute to the next song/arrangement.
- POST /api/stats accepts optional `seconds` (finite, 0 < s ≤ 6h) on the
scored and position branches, plus a new seconds-only branch for
unscored plays that ran to the natural end — banks time WITHOUT
touching the resume position (song:ended must not overwrite Continue)
and still counts as playing today for the streak.
- song_stats gains additive idempotent `seconds_total`; record_session/
touch_position accrue, new add_play_seconds() for the seconds-only
path; the legacy-encoding stats merge sums seconds across duplicates.
- Passports surface it: "14.2 h in Blues" under the badge stamp and on
the shelf cover sub-line — a true fact that only grows, never a
target or a meter (Stage 5 post-cap, per the career design).
Tests: seconds accrual/validation/seconds-only branch (stats API),
per-instrument-and-genre summing (career), fmtHours formatting (vm).
Full suites: pytest 2480, JS 1165.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Earning a genre badge now stages the full moment (career v2, WS1):
- venue-crowd.js gains a public celebrate(): machine.force('ecstatic')
commits instantly (bypassing STABLE_MS/DWELL_MS; the stamped
lastSwitchAt makes the dwell window HOLD the forced state before the
real perf machine reasserts) + a cheer stinger via the same
_lastStingerAt=-Infinity bypass the end-of-song reaction uses. If a
stinger/intro owns the idle layer, the ecstatic loop is queued via
_pendingLoop exactly like onPerformanceState. No-op without a
manifest/active venue.
- career detectNewBadges() calls badgeCeremony(): crowd first, then a
body-appended full-screen overlay 300ms later (it cannot live in
#pp-overlay — #plugin-career is display:none during playback): dimmed
backdrop, the bronze stamp slamming in with a shine sweep, a 42-piece
canvas confetti burst, click-or-4s dismiss.
- prefers-reduced-motion: chime + fbNotify only, no overlay.
Tests: machine.force commit/dwell-hold/bogus-state, celebrate export +
no-manifest no-op, celebrate-called-once-per-badge, crowd-absent and
crowd-throwing degradation.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* chore(career): refresh bar pack to v4 + restore crowd-SFX setting
Pack v4: per-character desynced animation starts, flyover intro,
per-venue reaction sounds (sfx-up/sfx-down in manifest).
Settings: re-add the crowd sound reactions toggle that was dropped
when settings.html became the passports data panel.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(v3): crowd mood-change sound reactions (cheer up / boo down)
Port the venue-crowd SFX runtime that the settings toggle and the
pack's sfx-up/sfx-down files were built for: on a committed mood
transition, play the venue's own cheer (up) or boo (down) one-shot,
gated by the feedBack-venue-crowd-sfx setting (default off).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: rebuild tailwind.min.css for settings toggle classes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
CodeRabbit follow-up on #936 (the one Major — overlay outside the click
root — was verified false: the host mounts every screen.html root inside
#plugin-career, ✕-close confirmed working live):
- Tabs: aria-selected/aria-controls + role=tabpanel/aria-labelledby.
- Book overlay: role=dialog + aria-modal + aria-label; focus moves to
the close button on open and returns to the opener on close.
- seenBadges(): guard non-object JSON so a corrupt stored value cannot
throw on every passport refresh (covered by a new corruption test).
- Fresh-session suppression test (badge seen → no re-notification).
- Stylelint declaration-empty-line-before nit in .pp-stamp.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The Passports tab beside Venues renders the badge journey physically:
- Per-instrument passport book: embossed CSS-leather cover, 3D page-turn
spread (badge page left, ticket stubs right), Escape/backdrop close.
- Wax-seal commitment ceremony (Stage 0) — pressing the seal commits the
instrument; opening a first passport runs the ceremony implicitly.
- Rubber-stamp badge slam: earned badges chime + notify immediately, the
slam (with ink bleed, page shake, deterministic sin-hash jitter) plays
when the passport is next opened, then the badge is marked seen.
- Ticket-stub repertoire: qualifying songs as collected stubs.
- Brochure rack: unopened genres as "Explore next" invitations — no
greyed slots, no completion meters (the anti-list as layout).
- Drill relay: on virtuoso:progress bus events the career screen posts
the full virtuoso.progress localStorage snapshot to the drill-state
intake (debounced; one-time bootstrap when the server has none).
- Four synthesized sfx (stamp/seal/page/chime, 13 KB total) as plugin
assets; prefers-reduced-motion disables the theatrics.
Pure logic (ppKey, ppJitter, badge diff/seen) is covered by a bare-vm
node --test suite via a window.__careerPassportTest seam.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The badge-journey layer on top of career stars (Christian's career-mode
v2 design, composed with the shipped venue system). Badges are computed
on read from song_stats × the library's effective genre — never stored:
Bronze = N genre songs at min_stars (data-driven in passports.json,
default 5 songs at 2★) plus any configured virtuoso drill nodes.
New endpoints under /api/plugins/career/:
- GET /passports passport walls per instrument: badges, ticket
stubs (qualifying songs), library genres, drills
- POST /passports/commit instrument commitment (idempotent wax seal)
- POST /passports/open open a genre passport (implies commitment)
- POST /drill-state intake for the relayed virtuoso.progress
snapshot (career's frontend listens on the bus)
Instrument attribution reuses progression.instrument_for_arrangement via
the song_stats arrangement index; the genre column goes through the
host's override-aware effective-genre SQL. Non-graded instruments (bass,
drums) render shown-not-judged — repertoire, never a false badge denial.
Persisted state (commitments, opened passports, drill snapshot) lives
under CONFIG_DIR/career/ and rides the settings export bundle via
settings.server_files; a minimal settings.html documents it.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(career): career plugin — stars from song_stats, venue tiers, pack downloads (career mode PR2)
Bundled plugin: per-song stars from best_accuracy (60/75/85% → 1/2/3★),
cumulative stars unlock bar → club → arena (data-driven venues.json).
Venue packs (UE-rendered crowd loops) download on demand to
CONFIG_DIR/plugin_uploads/career/ on a background thread with sha256 +
zip-slip validation, served via FileResponse. Career screen (promoted
sidebar entry) shows progress and pushes the active venue's manifest
into the crowd video layer (v3VenueCrowd, PR1) — degrades cleanly when
either side is absent. Pack URLs land in venues.json in PR3.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(career): keep manifest cleanup path alive on delete; badge only for installed venues
Codex preflight: nulling _appliedManifestVenue on delete skipped
pushCrowdManifest's setManifest(null) cleanup, leaving the crowd layer on
a deleted pack; and the 'playing here' badge showed for an override venue
whose pack was removed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(career): generation-guard in-flight manifest fetches
Codex preflight: a manifest fetch resolving after a newer refresh (pack
deleted, venue switched) could re-apply a stale pack over the user's
newer selection — fetches now carry a generation token and bail when
superseded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(career): exclude orphaned song_stats from star totals
Codex preflight: scans hide rather than delete stats of removed songs, so
stars now apply the same existing-song filter other stats surfaces use
(filename IN (SELECT filename FROM songs)).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(career): 50/150 star thresholds + star collection overview
Byron's progression tuning: club at 50★, arena at 150★. /state now
returns star_detail rows (title/artist joined from the library, stars,
best accuracy, next-star threshold) sorted closest-to-next-star first,
and the career screen renders a collection panel: tier summary plus a
per-song list with a 'N% to next star' practice hint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(career): venue select/unselect UX, intro manifest support, fullmatch guards
- 'Play here' now also defaults the visualization to Venue (remembering
the prior viz); active venues show 'Leave venue' which restores it and
sets the '__none__' override so no installed venue silently reapplies.
- Pack manifests may ship an intro block (flyover video + ambience mp3);
files validate like loops/stingers, .mp3 added to the serving whitelist.
- Codex preflight: whitelist regexes use fullmatch (trailing-newline names
could validate but 500 on serving).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(career): let pushCrowdManifest clear the manifest on Leave venue
Codex preflight: nulling _appliedManifestVenue before refresh skipped the
setManifest(null) cleanup branch, leaving the crowd playing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(career): refresh tailwind output
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>