Skip to content

feat(career): career plugin — stars, venue tiers, pack downloads (career mode 2/3)#907

Merged
byrongamatos merged 8 commits into
mainfrom
feat/career-plugin
Jul 12, 2026
Merged

feat(career): career plugin — stars, venue tiers, pack downloads (career mode 2/3)#907
byrongamatos merged 8 commits into
mainfrom
feat/career-plugin

Conversation

@byrongamatos

@byrongamatos byrongamatos commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Second slice of the career-mode epic (follows #905, lands independently — the two share no files and each degrades without the other):

  • plugins/career/ (new bundled plugin):
    • Stars: per song, best accuracy across arrangements from the existing song_stats table → 60/75/85 % = 1/2/3 ★ (thresholds data-driven in venues.json), with the existing-song filter so orphaned stats don't count.
    • Venue tiers: bar (0 ★) → club (15 ★) → arena (40 ★), unlocked by cumulative stars.
    • Pack downloads: venue packs (UE-rendered crowd loops, PR 3/3) stream on a background thread to CONFIG_DIR/plugin_uploads/career/venues/<id>/ with sha256 verification, zip-slip-guarded extraction, manifest validation, and atomic swap-in; served back via FileResponse with the highway_3d no-cache/nosniff recipe. venues.json ships pack: null until PR 3 publishes release assets.
    • Career screen (promoted sidebar entry via PROMOTED_PLUGINS): star total, next-unlock progress bar, per-venue cards with download/remove/play-here, unlock toasts via fbNotify.
    • Pushes the active venue's manifest into the crowd video layer (v3VenueCrowd.setManifest, from feat(venue): reactive crowd video layer behind the 3D highway (career mode 1/3) #905), guarded so either side being absent is a clean no-op.

Testing

  • tests/plugins/career/ (11 pytest): star math, threshold unlocks, orphaned-stats exclusion, 403/404/409 paths, traversal guards, and the download worker end-to-end (real zip via file://, sha256 verify, corrupt-hash rejection).
  • tests/js/career_plugin.test.js: manifest/venues.json shape, shell promotion wiring, crowd-manifest push degradation.
  • Full JS suite 974/974; pyflakes clean; tailwind.min.css rebuilt — no diff (plugin ships its own career.css for classes outside the core glob).
  • Codex preflight: 4 rounds, 4 findings fixed, final round clean.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a Career mode with star-based progression, accuracy thresholds, and venue unlocks.
    • Added Career navigation and a dedicated Career screen with star collection details and per-venue cards.
    • Added venue pack download/install progress, removal, and venue playback selection with crowd experience updates.
  • Bug Fixes

    • Improved safety for served pack assets and installation (path traversal protection, stricter validation).
  • Style

    • Added responsive Career plugin styling, including progress and star-list UI.
  • Tests

    • Added coverage for progression logic, downloads/installation, file serving behavior, and navigation integration.

byrongamatos and others added 4 commits July 12, 2026 01:48
…k 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>
…or 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>
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>
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>
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR introduces a bundled Career plugin with configurable venue tiers, star-based progression, downloadable venue packs, a browser UI, crowd manifest integration, sidebar navigation, and backend and frontend test coverage.

Changes

Career plugin

Layer / File(s) Summary
Plugin contract and shell integration
.gitignore, plugins/career/plugin.json, plugins/career/venues.json, plugins/career/screen.html, plugins/career/assets/career.css, static/v3/shell.js, static/tailwind.min.css
Defines the Career plugin metadata, venue thresholds, screen markup, responsive styling, generated stylesheet bundle, and promoted sidebar navigation.
Career state and pack lifecycle
plugins/career/routes.py
Computes stars, reports venue state, manages background pack downloads and installation, validates archives, removes packs, and serves installed assets.
Progression UI and crowd synchronization
plugins/career/screen.js
Renders progression and venue actions, polls downloads, announces unlocks, selects installed venues, and updates the crowd manifest.
Packaging and route validation
tests/js/career_plugin.test.js, tests/plugins/career/*
Tests plugin metadata, venue ordering, shell integration, crowd hooks, state calculations, authorization, archive installation, progress tracking, and traversal protection.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Player
  participant CareerScreen
  participant CareerAPI
  participant VenuePackStorage
  participant VenueCrowd
  Player->>CareerScreen: Open Career screen
  CareerScreen->>CareerAPI: Fetch career state
  CareerAPI-->>CareerScreen: Return stars and venue states
  CareerScreen->>CareerAPI: Request unlocked venue pack
  CareerAPI->>VenuePackStorage: Download and install pack
  VenuePackStorage-->>CareerAPI: Report installation status
  CareerAPI-->>CareerScreen: Return updated state
  CareerScreen->>VenueCrowd: Set selected venue manifest
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change set: the new career plugin with stars, tier progression, and pack downloads.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/career-plugin

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (4)
plugins/career/routes.py (3)

114-115: 🔒 Security & Privacy | 🔵 Trivial

Verify the configured pack URL trust boundary.

pack["url"] comes from bundled venues.json, not directly from venue_id, so the current endpoint is not itself request-controlled SSRF. Before release assets populate these fields, confirm the metadata is immutable/trusted and enforce HTTPS or an allowed-host policy before calling urlopen.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/career/routes.py` around lines 114 - 115, Validate the configured URL
in the download flow before constructing the request or calling urlopen: ensure
pack["url"] is sourced from immutable trusted metadata and enforce HTTPS or the
project’s allowed-host policy. Update the logic around the visible
urllib.request.Request/urlopen calls while preserving the existing download
behavior for approved URLs.

Source: Linters/SAST tools


163-163: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the supplied plugin logger unconditionally.

context.get("log") or _state["log"] falls back to a module logger for this non-CLI plugin, bypassing centralized host logging when the context logger is absent or falsey. Use context["log"] according to the setup contract.

As per path instructions, “Use context["log"] for all backend plugin output and never use print(); CLI entry points may configure a standard-library logging fallback.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/career/routes.py` at line 163, Update the logger assignment in the
plugin setup flow to use context["log"] unconditionally instead of
context.get("log") or _state["log"]. Preserve the setup contract that backend
plugin output uses the supplied context logger and does not fall back to the
module logger.

Source: Path instructions


42-49: 🩺 Stability & Availability | 🔵 Trivial

Verify download coordination across server workers.

_state, _lock, and the daemon threads are process-local. With multiple FastAPI/Uvicorn workers, two requests can both see idle and download or remove the same venue concurrently, bypassing the 409 guard. Confirm single-process deployment or move coordination to a shared lock/state store.

Also applies to: 200-208

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/career/routes.py` around lines 42 - 49, Update the career download
coordination around the process-local _state, _lock, and daemon-thread workflow
to use coordination shared across FastAPI/Uvicorn workers, including shared
status and locking for download and removal operations so concurrent requests
cannot bypass the 409 guard. If the implementation intentionally requires
single-process deployment instead, enforce and document that deployment
constraint rather than relying on process-local state.
tests/plugins/career/test_routes.py (1)

127-131: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert that a checksum failure preserves the installed pack.

The comment promises “nothing installed over the good pack,” but the test only checks the error status. A regression that deletes or replaces the existing installation could still pass. Snapshot the known-good manifest (or at least assert the pack remains installed) after the corrupt download.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/plugins/career/test_routes.py` around lines 127 - 131, Extend the
checksum-failure test around career_routes._download_pack to capture the
existing good pack state before the corrupt download, then assert that the
installed pack and its manifest remain unchanged afterward. Keep the existing
error-status and sha256-message assertions, and compare against the pre-download
snapshot rather than only checking installation presence.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugins/career/routes.py`:
- Around line 109-112: Move staging initialization in the worker flow around
mkdtemp into the existing try block, starting with staging = None before it.
Ensure allocation failures are caught by the worker’s exception handling and
update the venue’s progress state from running to a failed/completed state so
polling and deletion do not remain blocked.
- Around line 94-102: Update the validation around REQUIRED_LOOPS and stingers
to use a media-only filename rule that rejects .json and accepts only supported
video files; continue checking that each referenced file exists in pack_dir.
Keep PACK_FILENAME_RE available for manifest.json validation, but do not use it
for loop or stinger entries.
- Around line 117-124: Protect all progress mutations in the download worker
with the existing _lock, including bytes_total, bytes_done, status, and error
updates. Add or reuse a lock-protected helper for updating progress fields, then
use it in the loop around resp.read, digest/write progress updates, and the
status/error paths referenced near lines 146-150 so get_state() never observes a
partial update.
- Around line 115-124: The download loop around urllib.request.urlopen must
enforce a maximum archive size while reading, rather than only recording
Content-Length or pack["bytes"] in progress. Add extraction quotas in the ZIP
handling flow as well, covering compressed size, total uncompressed size, file
count, and each file’s uncompressed size; reject the asset before writing beyond
any limit or filling CONFIG_DIR, while preserving existing digest, progress, and
verification behavior.
- Around line 143-145: Update the venue install and DELETE flows around
final_dir and extract_dir to use a per-venue removing state, preventing
concurrent installs while removal is in progress. Install into a new versioned
directory and atomically swap it into place only after extraction completes;
remove the old version afterward, preserving the previous pack if the swap
fails. Ensure DELETE removes the files before clearing the download state so a
concurrent POST cannot install content that the deletion then removes.

In `@plugins/career/screen.html`:
- Line 6: Update the instructional paragraph near the career screen markup to
derive all star accuracy percentages from state.star_accuracy_thresholds instead
of hard-coding 60%, 75%, and 85%. Preserve the existing explanatory text while
interpolating the configured thresholds in the correct star-order.

In `@plugins/career/screen.js`:
- Around line 96-100: Update venueCardHTML() to avoid calling
localStorage.getItem() directly when calculating isActive; reuse the existing
safe storage accessor used by the other storage paths, while preserving the
current override-key and venue-id comparison.
- Around line 133-137: Update the refresh error-handling path near schedulePoll
so transient fetch failures still schedule another poll when the previous state
contains a venue with download status "running". Reuse schedulePoll with the
last known state from the catch path, ensuring the existing timer behavior and
normal successful-refresh flow remain unchanged.
- Around line 180-188: Update the download and delete fetch chains in the career
screen action handler to validate response.ok before calling refresh, surface
non-2xx responses, and catch network failures with the existing visible error
mechanism. Keep refresh limited to successful actions and apply the same
handling to both dlBtn and delBtn branches.
- Around line 162-173: Update refresh() to use a state-request generation
counter like _manifestReqGen: increment it for each invocation, capture the
current generation before await fetchState(), and discard the response if a
newer refresh has started. Only apply _state, announceUnlocks, render,
schedulePoll, and pushCrowdManifest for the latest generation, while preserving
the existing retry behavior for fetch failures.

In `@tests/plugins/career/conftest.py`:
- Around line 5-13: Update the Career route loading in the test configuration to
avoid sys.path mutation, sys.modules.pop, and bare import routes; load the
Career routes module under a unique, plugin-specific module name while
preserving the career_routes reference used by the tests.

---

Nitpick comments:
In `@plugins/career/routes.py`:
- Around line 114-115: Validate the configured URL in the download flow before
constructing the request or calling urlopen: ensure pack["url"] is sourced from
immutable trusted metadata and enforce HTTPS or the project’s allowed-host
policy. Update the logic around the visible urllib.request.Request/urlopen calls
while preserving the existing download behavior for approved URLs.
- Line 163: Update the logger assignment in the plugin setup flow to use
context["log"] unconditionally instead of context.get("log") or _state["log"].
Preserve the setup contract that backend plugin output uses the supplied context
logger and does not fall back to the module logger.
- Around line 42-49: Update the career download coordination around the
process-local _state, _lock, and daemon-thread workflow to use coordination
shared across FastAPI/Uvicorn workers, including shared status and locking for
download and removal operations so concurrent requests cannot bypass the 409
guard. If the implementation intentionally requires single-process deployment
instead, enforce and document that deployment constraint rather than relying on
process-local state.

In `@tests/plugins/career/test_routes.py`:
- Around line 127-131: Extend the checksum-failure test around
career_routes._download_pack to capture the existing good pack state before the
corrupt download, then assert that the installed pack and its manifest remain
unchanged afterward. Keep the existing error-status and sha256-message
assertions, and compare against the pre-download snapshot rather than only
checking installation presence.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8aacb978-aad2-4c6f-afcc-ad0e23b883b0

📥 Commits

Reviewing files that changed from the base of the PR and between db3ca34 and 99b6d3c.

📒 Files selected for processing (12)
  • .gitignore
  • plugins/career/assets/career.css
  • plugins/career/plugin.json
  • plugins/career/routes.py
  • plugins/career/screen.html
  • plugins/career/screen.js
  • plugins/career/venues.json
  • static/v3/shell.js
  • tests/js/career_plugin.test.js
  • tests/plugins/career/__init__.py
  • tests/plugins/career/conftest.py
  • tests/plugins/career/test_routes.py

Comment thread plugins/career/routes.py
Comment on lines +94 to +102
for state in REQUIRED_LOOPS:
name = loops.get(state)
if not name or not PACK_FILENAME_RE.match(name):
raise ValueError(f"manifest is missing the '{state}' loop")
if not (pack_dir / name).is_file():
raise ValueError(f"loop file '{name}' missing from pack")
for name in (manifest.get("stingers") or {}).values():
if name and (not PACK_FILENAME_RE.match(name) or not (pack_dir / name).is_file()):
raise ValueError(f"stinger file '{name}' invalid or missing")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Reject non-video files for loop and stinger entries.

PACK_FILENAME_RE allows .json for every manifest value, so a malformed pack can point loops.bored at manifest.json and still install. The frontend then treats JSON as video. Use a media-only filename rule for loop and stinger values, reserving .json for manifest.json.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/career/routes.py` around lines 94 - 102, Update the validation around
REQUIRED_LOOPS and stingers to use a media-only filename rule that rejects .json
and accepts only supported video files; continue checking that each referenced
file exists in pack_dir. Keep PACK_FILENAME_RE available for manifest.json
validation, but do not use it for loop or stinger entries.

Comment thread plugins/career/routes.py
Comment on lines +109 to +112
staging = Path(tempfile.mkdtemp(prefix=f"career-{venue_id}-",
dir=str(_state["venues_dir"])))
zip_path = staging / "pack.zip"
try:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep staging allocation inside the guarded worker path.

mkdtemp() runs before try; if the config directory is unavailable or disk-full, the thread exits with the download permanently marked running. The UI will poll forever and DELETE will remain 409. Initialize staging = None, create it inside try, and ensure failures update the progress state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/career/routes.py` around lines 109 - 112, Move staging initialization
in the worker flow around mkdtemp into the existing try block, starting with
staging = None before it. Ensure allocation failures are caught by the worker’s
exception handling and update the venue’s progress state from running to a
failed/completed state so polling and deletion do not remain blocked.

Comment thread plugins/career/routes.py
Comment on lines +115 to +124
with urllib.request.urlopen(req, timeout=60) as resp, open(zip_path, "wb") as out:
total = int(resp.headers.get("Content-Length") or pack.get("bytes") or 0)
progress["bytes_total"] = total
while True:
chunk = resp.read(DOWNLOAD_CHUNK)
if not chunk:
break
digest.update(chunk)
out.write(chunk)
progress["bytes_done"] += len(chunk)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound both download and extracted-pack sizes.

Content-Length and pack["bytes"] only update progress; the stream accepts unlimited bytes, and ZIP extraction has no compressed, uncompressed, file-count, or per-file limits. A bad or compromised release asset can fill CONFIG_DIR before verification completes. Enforce maximum download and extraction quotas.

Also applies to: 130-139

🧰 Tools
🪛 Ruff (0.15.20)

[error] 115-115: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/career/routes.py` around lines 115 - 124, The download loop around
urllib.request.urlopen must enforce a maximum archive size while reading, rather
than only recording Content-Length or pack["bytes"] in progress. Add extraction
quotas in the ZIP handling flow as well, covering compressed size, total
uncompressed size, file count, and each file’s uncompressed size; reject the
asset before writing beyond any limit or filling CONFIG_DIR, while preserving
existing digest, progress, and verification behavior.

Comment thread plugins/career/routes.py
Comment on lines +117 to +124
progress["bytes_total"] = total
while True:
chunk = resp.read(DOWNLOAD_CHUNK)
if not chunk:
break
digest.update(chunk)
out.write(chunk)
progress["bytes_done"] += len(chunk)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Lock progress mutations as well as reads.

get_state() snapshots progress under _lock, but the worker mutates bytes_total, bytes_done, status, and error without it. Readers can observe inconsistent progress or an error status before its message is written. Update these fields through a lock-protected helper.

Also applies to: 146-150

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/career/routes.py` around lines 117 - 124, Protect all progress
mutations in the download worker with the existing _lock, including bytes_total,
bytes_done, status, and error updates. Add or reuse a lock-protected helper for
updating progress fields, then use it in the loop around resp.read, digest/write
progress updates, and the status/error paths referenced near lines 146-150 so
get_state() never observes a partial update.

Comment thread plugins/career/routes.py
Comment on lines +143 to +145
if final_dir.exists():
shutil.rmtree(final_dir)
extract_dir.rename(final_dir)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Make install and removal atomic per venue.

rmtree(final_dir) followed by rename exposes a missing/partial pack and can lose the old pack if the rename fails. Separately, DELETE removes the download state before deleting files, allowing a concurrent POST to install a new pack that the old DELETE then removes. Use a per-venue removing state and an atomic versioned-directory swap.

Also applies to: 215-220

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/career/routes.py` around lines 143 - 145, Update the venue install
and DELETE flows around final_dir and extract_dir to use a per-venue removing
state, preventing concurrent installs while removal is in progress. Install into
a new versioned directory and atomically swap it into place only after
extraction completes; remove the old version afterward, preserving the previous
pack if the swap fails. Ensure DELETE removes the files before clearing the
download state so a concurrent POST cannot install content that the deletion
then removes.

Comment thread plugins/career/screen.js
Comment thread plugins/career/screen.js
Comment on lines +133 to +137
function schedulePoll(state) {
clearTimeout(_pollTimer);
if (state.venues.some((v) => (v.download || {}).status === 'running')) {
_pollTimer = setTimeout(refresh, POLL_MS);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Keep polling alive after transient fetch failures.

When a scheduled refresh fails, the catch returns before schedulePoll(state), so no new timer is installed even though the backend download may still be running. The UI then remains stale until another event. Schedule a retry while the previous state indicates an active download.

Also applies to: 162-168

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 135-135: Avoid using the initial state variable in setState
Context: setTimeout(refresh, POLL_MS)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)


[error] 135-135: React's useState should not be directly called
Context: setTimeout(refresh, POLL_MS)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(usestate-direct-usage)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/career/screen.js` around lines 133 - 137, Update the refresh
error-handling path near schedulePoll so transient fetch failures still schedule
another poll when the previous state contains a venue with download status
"running". Reuse schedulePoll with the last known state from the catch path,
ensuring the existing timer behavior and normal successful-refresh flow remain
unchanged.

Comment thread plugins/career/screen.js
Comment on lines +162 to +173
async function refresh() {
let state;
try {
state = await fetchState();
} catch (_) {
return; // server restarting; next trigger retries
}
_state = state;
announceUnlocks(state);
render(state);
schedulePoll(state);
pushCrowdManifest(state);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Discard stale state responses.

refresh() can run concurrently from polling, stats:recorded, and click handlers. Without a request generation or abort check, an older response can overwrite newer install/download/unlock state and schedule polling from stale data. Add a state-request generation similar to _manifestReqGen.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/career/screen.js` around lines 162 - 173, Update refresh() to use a
state-request generation counter like _manifestReqGen: increment it for each
invocation, capture the current generation before await fetchState(), and
discard the response if a newer refresh has started. Only apply _state,
announceUnlocks, render, schedulePoll, and pushCrowdManifest for the latest
generation, while preserving the existing retry behavior for fetch failures.

Comment thread plugins/career/screen.js
Comment on lines +180 to +188
if (dlBtn) {
fetch(`${API}/packs/${dlBtn.dataset.careerDownload}/download`, { method: 'POST' })
.then(refresh);
} else if (delBtn) {
// Do NOT null _appliedManifestVenue here: pushCrowdManifest()
// clears/replaces the crowd manifest precisely by seeing that the
// applied venue is no longer among the installed ones.
fetch(`${API}/packs/${delBtn.dataset.careerDelete}`, { method: 'DELETE' })
.then(refresh);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle non-2xx action responses visibly.

Download and delete call .then(refresh) regardless of res.ok, and network failures are unhandled. A 403, 404, or 409 therefore appears as a silent no-op. Check the response status, catch failures, and surface the error before refreshing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/career/screen.js` around lines 180 - 188, Update the download and
delete fetch chains in the career screen action handler to validate response.ok
before calling refresh, surface non-2xx responses, and catch network failures
with the existing visible error mechanism. Keep refresh limited to successful
actions and apply the same handling to both dlBtn and delBtn branches.

Comment on lines +5 to +13
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / 'plugins' / 'career'))

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

# Drop a sibling 'routes' cached by another plugin's tests (bare-name collision).
sys.modules.pop('routes', None)
import routes as career_routes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP '^\s*(from|import)\s+routes\b' tests

Repository: got-feedBack/feedBack

Length of output: 689


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant test fixture files first
ast-grep outline tests/plugins/career/conftest.py --view expanded || true
ast-grep outline tests/plugins/tuner/conftest.py --view expanded || true
ast-grep outline tests/plugins/achievements/conftest.py --view expanded || true

# Show the relevant line ranges with line numbers
for f in tests/plugins/career/conftest.py tests/plugins/tuner/conftest.py tests/plugins/achievements/conftest.py tests/plugins/career/test_routes.py tests/plugins/achievements/test_routes.py tests/plugins/achievements/test_datamin.py tests/plugins/tuner/test_config.py; do
  echo "===== $f ====="
  wc -l "$f"
  cat -n "$f" | sed -n '1,120p'
done

Repository: got-feedBack/feedBack

Length of output: 28775


Avoid bare routes imports here

sys.path.insert(0, ...) plus import routes makes the module name process-global, so other test modules that do import routes can resolve to the wrong plugin during collection. Load Career under a unique module name instead of mutating sys.path/sys.modules this way.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/plugins/career/conftest.py` around lines 5 - 13, Update the Career
route loading in the test configuration to avoid sys.path mutation,
sys.modules.pop, and bare import routes; load the Career routes module under a
unique, plugin-specific module name while preserving the career_routes reference
used by the tests.

byrongamatos and others added 4 commits July 12, 2026 21:14
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>
…atch 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>
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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugins/career/screen.js`:
- Around line 88-95: Update the venue rendering logic around the active
predicate and the existing isActive check to reuse a single guarded localStorage
lookup, following the protection already used in pushCrowdManifest. Ensure
storage exceptions fall back safely without aborting render(), and remove the
duplicated unguarded VENUE_OVERRIDE_KEY access.
- Around line 246-257: Update the venue-unselect branch handling the
PREV_VIZ_KEY so that when no previous visualization is stored, it explicitly
restores the default non-venue visualization instead of leaving the current
'venue' selection active. Continue restoring the stored value through
localStorage and window.setViz when prev exists, then call refresh() as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5d97beb2-b979-439b-9707-1ae9bce1ed90

📥 Commits

Reviewing files that changed from the base of the PR and between 99b6d3c and 36d984f.

📒 Files selected for processing (8)
  • plugins/career/assets/career.css
  • plugins/career/routes.py
  • plugins/career/screen.html
  • plugins/career/screen.js
  • plugins/career/venues.json
  • static/tailwind.min.css
  • tests/plugins/career/conftest.py
  • tests/plugins/career/test_routes.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/plugins/career/conftest.py
  • plugins/career/routes.py
  • tests/plugins/career/test_routes.py

Comment thread plugins/career/screen.js
Comment on lines +88 to +95
const active = localStorage.getItem(VENUE_OVERRIDE_KEY) === v.id;
const main = active
? `<button data-career-unselect="1" class="career-btn career-btn-ghost">Leave venue</button>`
: `<button data-career-play="${esc(v.id)}" class="career-btn career-btn-primary">Play here</button>`;
action = `<div class="flex items-center gap-2">
${main}
<button data-career-delete="${esc(v.id)}" class="career-btn career-btn-ghost">Remove pack</button>
</div>`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Unguarded localStorage.getItem reintroduced (and duplicated).

Line 88 calls localStorage.getItem(VENUE_OVERRIDE_KEY) directly, unlike the guarded lookup added in pushCrowdManifest (lines 51-56). This is the same failure mode previously flagged for this file: in blocked/private/sandboxed storage contexts this throws and render() fails entirely. The existing isActive check at line 106-107 has the same unguarded call and computes the identical predicate — worth consolidating into a single guarded lookup.

🩹 Proposed fix
 function venueCardHTML(v, state) {
     const locked = !v.unlocked;
     const dl = v.download || { status: 'idle' };
     const pct = dl.bytes_total > 0
         ? Math.round((dl.bytes_done / dl.bytes_total) * 100) : 0;
+    let override = null;
+    try { override = localStorage.getItem(VENUE_OVERRIDE_KEY); } catch (_) { /* ok */ }
     let action = '';
     if (locked) {
         ...
     } else if (v.installed) {
-            const active = localStorage.getItem(VENUE_OVERRIDE_KEY) === v.id;
+            const active = override === v.id;
     ...
-        const isActive = !locked && v.installed &&
-            localStorage.getItem(VENUE_OVERRIDE_KEY) === v.id;
+        const isActive = !locked && v.installed && override === v.id;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/career/screen.js` around lines 88 - 95, Update the venue rendering
logic around the active predicate and the existing isActive check to reuse a
single guarded localStorage lookup, following the protection already used in
pushCrowdManifest. Ensure storage exceptions fall back safely without aborting
render(), and remove the duplicated unguarded VENUE_OVERRIDE_KEY access.

Comment thread plugins/career/screen.js
Comment on lines +246 to +257
} else if (e.target.closest('[data-career-unselect]')) {
try {
localStorage.setItem(VENUE_OVERRIDE_KEY, NO_VENUE);
const prev = localStorage.getItem(PREV_VIZ_KEY);
if (prev) {
localStorage.setItem('vizSelection', prev);
if (typeof window.setViz === 'function') window.setViz(prev);
}
} catch (_) { /* ok */ }
// keep _appliedManifestVenue: pushCrowdManifest clears the crowd
// manifest precisely by seeing it is still set with no venue left
refresh();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

"Leave venue" can leave the UI stuck in venue viz.

If PREV_VIZ_KEY was never written (e.g. first-ever "Play here" click with no prior explicit vizSelection), prev is falsy here and neither vizSelection nor window.setViz is restored — the visualization stays on 'venue' indefinitely with no venue selected.

🩹 Proposed fix
             try {
                 localStorage.setItem(VENUE_OVERRIDE_KEY, NO_VENUE);
-                const prev = localStorage.getItem(PREV_VIZ_KEY);
-                if (prev) {
-                    localStorage.setItem('vizSelection', prev);
-                    if (typeof window.setViz === 'function') window.setViz(prev);
-                }
+                const prev = localStorage.getItem(PREV_VIZ_KEY) || 'grid';
+                localStorage.setItem('vizSelection', prev);
+                if (typeof window.setViz === 'function') window.setViz(prev);
             } catch (_) { /* ok */ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if (e.target.closest('[data-career-unselect]')) {
try {
localStorage.setItem(VENUE_OVERRIDE_KEY, NO_VENUE);
const prev = localStorage.getItem(PREV_VIZ_KEY);
if (prev) {
localStorage.setItem('vizSelection', prev);
if (typeof window.setViz === 'function') window.setViz(prev);
}
} catch (_) { /* ok */ }
// keep _appliedManifestVenue: pushCrowdManifest clears the crowd
// manifest precisely by seeing it is still set with no venue left
refresh();
} else if (e.target.closest('[data-career-unselect]')) {
try {
localStorage.setItem(VENUE_OVERRIDE_KEY, NO_VENUE);
const prev = localStorage.getItem(PREV_VIZ_KEY) || 'grid';
localStorage.setItem('vizSelection', prev);
if (typeof window.setViz === 'function') window.setViz(prev);
} catch (_) { /* ok */ }
// keep _appliedManifestVenue: pushCrowdManifest clears the crowd
// manifest precisely by seeing it is still set with no venue left
refresh();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/career/screen.js` around lines 246 - 257, Update the venue-unselect
branch handling the PREV_VIZ_KEY so that when no previous visualization is
stored, it explicitly restores the default non-venue visualization instead of
leaving the current 'venue' selection active. Continue restoring the stored
value through localStorage and window.setViz when prev exists, then call
refresh() as before.

@byrongamatos
byrongamatos merged commit ea0ca94 into main Jul 12, 2026
5 checks passed
@byrongamatos

Copy link
Copy Markdown
Contributor Author

Note for reviewers: the feedBack-venue-crowd-sfx key written by the new settings panel is consumed by static/v3/venue-crowd.js in #905 — the two PRs are designed to land together (each degrades gracefully alone; the toggle is inert without #905, invisible-but-harmless).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant