feat(admin): staff override for project kind (software/hardware) - #655
feat(admin): staff override for project kind (software/hardware)#655dhamariT wants to merge 10 commits into
Conversation
Adds Admin::ProjectsController#update_hardware_stage so staff can convert a project between Software / Hardware-design / Hardware-build on a submitter's behalf — the case where a hardware project was shipped as software, returned, and the owner has no self-serve path to fix it. - Requires an audit reason and records a PaperTrail version with old->new stage, project-kind labels, the reason, and whether the funding lock was bypassed. - Bypasses the funding-request stage lock via save(validate: false); the funding-approval design->build transition does the same so it isn't blocked by that lock. - Drops the has_any_funding_request? memoization so the admin re-check is fresh. - Admin show page gains a "Project Kind" row + change modal; surfaces the AI classifier type separately.
Adds Admin::ProjectsController#clear_latest_ship so staff can fully un-stick a project that was shipped under the wrong kind: it destroys the most recent ship event (its post, ledger entries, vote assignments and mission submission cascade via dependents; votes are nullified), removes the matching review, and resets the project to an un-shipped draft. - Requires an audit reason; logged to PaperTrail (admin_clear_latest_ship) with the old->new ship status, a snapshot of the cleared ship, and the reason. - Refuses to clear a ship that has already paid out. - Admin show page gains a "Clear Latest Ship" action with a confirm + reason modal.
Hardware projects belong in the hardware review flow (funding / build), not the software ship-review queue, but they were getting auto-assigned to reviewers. Certification::Ship.available_for (which drives next_eligible / reviewer auto-assignment) now excludes projects that are hardware — either by the canonical hardware_stage or by the AI classifier's "Hardware" project_type. Uses IS DISTINCT FROM so unclassified (project_type NULL) ships stay in. The index listing still uses policy_scope, so admins can find hardware ships via the existing project-type filter — they're only removed from the reviewer feed.
Adds a backfill:hardware_stage rake task wrapping OneTime::BackfillHardwareStageJob so we can promote AI-classified hardware projects (project_type "Hardware", hardware_stage nil) into the hardware flow at the design stage. Dry-run by default (prints the project ids/titles it would convert); pass DRY_RUN=false to persist. All scope/audit logic stays in the job.
clear_latest_ship destroyed the ship event and its review, but an approved ship also has a Certification::Ysws review FK'd to both (non-cascading, no dependent association) — so clearing an approved-but-unpaid ship raised InvalidForeignKey and rolled back with a 500. Destroy the YSWS reviews (which cascade their devlog reviews) first. Adds a regression test.
excluding_hardware keeps any project_type == "Hardware" out of the review queue, but update_hardware_stage only touched hardware_stage — so an admin overriding a project to Software left it stuck out of auto-assignment because the AI classifier label still said Hardware. When an admin sets Software, clear a project_type == "Hardware" classifier so the human override actually takes effect (and the cleared label is recorded in the PaperTrail audit entry). Stays inside the admin-only action.
The previous software-override fix computed the classifier clear *after* the old_stage == new_stage no-op guard, so a project that was already Software (hardware_stage nil) but mislabeled project_type "Hardware" — the actual limbo case keeping projects out of the review queue — hit "already Software" and returned before clearing the classifier. Compute clear_classifier before the guard and let the action proceed when there is a stale classifier to clear even if the stage is unchanged. Adds a test for the already-software case.
The "clear latest ship" action permanently destroyed the ship event, its review, votes and YSWS links — irreversible and corrupting of payout/vote/audit history. Reworked into a non-destructive soft reset (renamed reset_latest_ship): it flips the project back to draft, resets the ship event's certification to pending so it no longer blocks re-shipping, and clears shipped_at — while preserving every record. Refuses to reset a paid-out ship. Button/modal/tests updated to reflect that nothing is deleted.
|
|
||
| ApplicationRecord.transaction do | ||
| ship_event.update_columns(certification_status: "pending") | ||
| @project.update_columns(ship_status: "draft", shipped_at: nil) |
There was a problem hiding this comment.
Reset blocks owner re-shipping
High Severity
reset_latest_ship moves the project to draft and keeps the latest Post::ShipEvent, but Project#shippable? still treats that event as the previous ship without a payout. Owners lose the needs_changes recert path and hit the payout gate on a new ship, so the admin reset does not let them actually re-ship.
Reviewed by Cursor Bugbot for commit 11b8248. Configure here.
Staff actions changed a project's reviewability without reconciling its open ship review: reset_latest_ship left the pending review in the queue after sending the project back to draft, and converting a project to hardware left a now-misrouted software review sitting pending (excluded from auto-assignment but never resolved). Add resolve_open_ship_reviews! — when a staff action makes the open review moot it's marked returned, unclaimed, and stamped with an internal note via update_all (skips the verdict/notify/stardust callbacks). Non-destructive: the review row is preserved and the change is recorded in the audit entry. Wired into reset_latest_ship and the hardware conversion, with tests proving the review leaves the queue and is kept.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
There are 3 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a7d2e09. Configure here.
|
|
||
| ApplicationRecord.transaction do | ||
| ship_event.update_columns(certification_status: "pending") | ||
| # Don't leave a pending review behind for a project that's no longer |
There was a problem hiding this comment.
Reset skips mission sync
Medium Severity
reset_latest_ship sets the latest ship’s certification_status to pending via update_columns, which skips Post::ShipEvent’s after_update hook that syncs Mission::Submission when certification changes. Resetting an approved (but unpaid) mission ship can leave the submission in a post-cert state while the ship cert is pending again.
Reviewed by Cursor Bugbot for commit a7d2e09. Configure here.
| end | ||
| # A now-hardware project's open software review is misrouted — close it so | ||
| # it doesn't sit stale (it's already filtered out of auto-assignment). | ||
| resolved_review_ids = resolve_open_ship_reviews!("Converted to hardware by staff; software ship review closed: #{reason}") if new_stage.present? |
There was a problem hiding this comment.
Hardware convert skips verdict side effects
Medium Severity
When staff convert a project to hardware, resolve_open_ship_reviews! marks open ship reviews returned via update_all, bypassing the normal verdict callbacks that set needs_changes and align the ship event’s certification_status. The project can stay submitted with a returned review and a still-pending ship cert, so owner-facing ship state no longer matches the closed review.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit a7d2e09. Configure here.
CodeQL's "clear-text storage of sensitive information" flagged the local variable old_cert as credential/certificate material by name. The value is a non-secret ship-event status enum (pending/approved/returned), so this is a false positive. Rename the variable to old_ship_status to drop the bogus sensitive-data classification; audit content is unchanged.


what's this do?
Right now, if someone builds a hardware project but ships it as a software project, a reviewer sends it back and tells them to switch it to hardware in their settings. The catch: they can't actually do that once it's been sent back — so the project gets stuck and keeps bouncing around the review queue. On top of that, hardware projects are landing in the software reviewers' queue, which slows everyone down.
This adds four things:
1. Change the project kind (admin). A button to switch a project between Software, Hardware (design), and Hardware (build) on the owner's behalf. Requires a reason (saved to the audit log), works even if the project already requested funding (which normally locks the type), and the page shows the project's real kind next to what our AI classifier guessed. If staff override a project to Software, any stale "Hardware" AI label is cleared too, so the project actually re-enters the review queue.
2. Reset the latest ship (admin). A button that soft-resets a project's most recent ship back to an un-shipped draft — for when a project shipped under the wrong kind and we want to give the owner a clean slate to fix and re-ship. Nothing is deleted: the ship event, its review, votes and YSWS links are all kept for history/audit. It just flips the project back to draft, resets the ship's certification to pending so it no longer blocks re-shipping, and clears the shipped timestamp. Asks for confirmation + a reason, logs it, and won't touch a ship that's already paid out.
3. Keep hardware out of the software review queue. Hardware projects no longer get auto-assigned to software reviewers — they're meant for the hardware flow (funding/build). "Hardware" means either the real hardware setting or anything our AI classifier tagged as Hardware. Unclassified projects still show up normally, and admins can still find hardware ships in the queue list via the type filter; they're just out of the auto-assignment feed.
4. A backfill runner. A
backfill:hardware_stagerake task (dry-run by default) that promotes AI-tagged hardware projects that never entered the hardware flow into it (at the design stage), so they actually land in the hardware flow instead of sitting in limbo. Wraps the existingOneTime::BackfillHardwareStageJob.Letting users fix this themselves (instead of staff) is a separate thing for later.
show it works
Tests:
test/controllers/admin/projects_controller_test.rb— change kind through the funding lock with audit; software override clears a stale Hardware classifier (including when the project is already software); soft-reset the latest ship (project back to draft, ship/review/YSWS all preserved, certification reset, audit entry); refuse to reset a paid-out ship; both actions require a reason; helper role blocked.test/models/certification/ship_test.rb— the reviewer queue keeps software and unclassified ships but excludes both real-hardware and AI-tagged-hardware ships.Backfill runner:
Heads up: I couldn't run the tests or the backfill on my machine — the docker setup wouldn't build here (a platform/image mismatch unrelated to this change). The Ruby passes a syntax check; relying on CI for the suite, and the backfill should be dry-run'd in a real environment before applying.
ai?
Yep — Claude Code (Opus 4.8) helped with the rebase, conflict resolution, review, and building these.
Note
Medium Risk
Changes ship/review queue routing and staff overrides that bypass validations and funding locks; mistakes could misroute reviews or reset ships, though payouts are guarded and actions are audited.
Overview
Adds admin tools to fix stuck software/hardware misroutes: staff can change project kind (software vs hardware design/build) with a required audit reason, bypassing the funding lock; overriding to software also clears a stale
HardwareAI classifier so the project can re-enter the software review queue.Adds Reset Latest Ship, a non-destructive draft reset (project + latest ship certification, clears
shipped_at) with audit logging; blocks paid-out ships and closes pending ship reviews viaresolve_open_ship_reviews!so nothing stays in the reviewer queue.Software reviewer auto-assignment now applies
excluding_hardwareonCertification::Ship.available_for(hardware stage or AI-tagged Hardware), while admin listing filters are unchanged.Admin project show gains kind/classifier display and modals; ship/status admin actions share
log_admin_version. Audit badges humanize non-updateevents. Newbackfill:hardware_stagerake task (dry-run default) wrapsOneTime::BackfillHardwareStageJob.Reviewed by Cursor Bugbot for commit 47773b9. Bugbot is set up for automated code reviews on this repo. Configure here.