Skip to content

feat(admin): staff override for project kind (software/hardware) - #655

Open
dhamariT wants to merge 10 commits into
mainfrom
hardware-project-conversion-job
Open

feat(admin): staff override for project kind (software/hardware)#655
dhamariT wants to merge 10 commits into
mainfrom
hardware-project-conversion-job

Conversation

@dhamariT

@dhamariT dhamariT commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

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_stage rake 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 existing OneTime::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:

bin/rails backfill:hardware_stage              # dry run, prints the projects it would convert
bin/rails backfill:hardware_stage DRY_RUN=false  # apply

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 Hardware AI 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 via resolve_open_ship_reviews! so nothing stays in the reviewer queue.

Software reviewer auto-assignment now applies excluding_hardware on Certification::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-update events. New backfill:hardware_stage rake task (dry-run default) wraps OneTime::BackfillHardwareStageJob.

Reviewed by Cursor Bugbot for commit 47773b9. Bugbot is set up for automated code reviews on this repo. Configure here.

dhamariT added 4 commits June 23, 2026 15:18
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.
@dhamariT
dhamariT marked this pull request as ready for review June 24, 2026 14:04
Comment thread app/controllers/admin/projects_controller.rb Outdated
Comment thread app/models/certification/ship.rb
Comment thread app/controllers/admin/projects_controller.rb Outdated
dhamariT added 2 commits June 24, 2026 10:12
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.
Comment thread app/controllers/admin/projects_controller.rb Outdated
dhamariT added 2 commits June 24, 2026 11:51
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

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.
Comment thread app/controllers/admin/projects_controller.rb Fixed

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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).

Fix All in Cursor

❌ 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

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?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

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.
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.

2 participants