You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Execution checklist for docs/roadmap.md R5 + docs/prd.md R5. Single issue for the whole release — do not split DB/API/UI into separate branches. Branch: feat/r5-moderate-enrich.
Outcome
One admin can turn minimal R4 submissions into complete published listings. This is the launch gate.
Context
project_submission (packages/db/src/schemas/submission.ts) already has rejectionReason, moderatedAt, moderatedBy — reuse them, do not add moderation fields to project for launch.
adminProcedure already exists (packages/api/src/procedures.ts, 401 no session / 403 non-admin) — use it for every moderation mutation.
submissionRouter (packages/api/src/routers/submission.ts) currently has only create + list on protectedProcedure — extend with admin + resubmit procedures below.
/_panel shell (apps/web/src/routes/_panel/route.tsx, my-submissions.tsx) + sidebar/data-table from PR Enhance sidebar and submission data table components #51 already exist. Roadmap text says /dashboard — reuse /_panel as the admin surface unless there is a reason to rename; document the choice.
R4 canonicalisation helper: canonicalizeGithubUrl in @altstack/shared/lib/github. Reuse it in approve/reject/resubmit paths.
Work
Keep moderation fields on project_submission (rejectionReason, moderatedAt, moderatedBy); do not add them to project for launch.
Add minimal audit_log: actor ID, action, target submission/project ID, optional metadata/reason, timestamp. Records moderation mutations only (no auth-activity events).
Add protected admin contracts: list pending submissions, approveSubmission, rejectSubmission. Keep remove as an internal admin operation on project if needed before launch.
approveSubmission input is the admin enrich step: tagline, short description, logo URL, optional website (prefilled from submission, editable), optional Markdown content, slug (prefilled from projectName, editable), and 1–3 existing category slugs.
approveSubmission handler re-checks canonical duplicate against bothproject.repositoryUrl and project_submission.repositoryUrl, refetches fresh GitHub metadata, then in one transaction inserts the published project + repository metadata + category links, marks the submission approved, and writes exactly one audit event. Slug collision resolves with a numeric suffix or 409 if unresolvable; document the choice in the issue/PR.
Enforce admin authorization in each mutation via adminProcedure. Invalid state transitions (e.g. approve an already-approved, reject a non-pending) return 409.
Build the smallest usable admin surface in the protected /_panel route (see Context on /dashboard naming): paginated pending-submissions list (reuse data-table), approve form with enrich fields, reject dialog requiring a reason.
Let the original submitter correct a rejected submission by editing only name/repository/website. Its narrow resubmitSubmission procedure (owner-only, protectedProcedure + submitter check) atomically clears the rejection reason and sets status back to pending. General editing of published projects remains R6.
Add moderation and audit tests: approve happy path, reject with reason, non-admin 403, unauthenticated 401, invalid transition 409, duplicate canonical on approve 409, resubmit rejected → pending (+ non-owner 403), exactly-one-audit-event per mutation in the same transaction. Run the complete smoke journey: seed → browse → search → sign in → submit (3 fields) → enrich/approve/reject → public/private assertions.
Record deployment notes: required GitHub OAuth callback URL, admin bootstrap process (how the first admin role is assigned), and known limitations.
Done when
An admin enriches + approves a submission and it immediately appears in browse/search as a complete listing.
An admin rejects with a reason; the submitter sees it privately (never public).
A valid re-submission moves rejected back to pending for another review.
Every moderation mutation creates exactly one audit event in the same transaction.
vp check, vp run -r test, and vp run -r build pass on the release branch.
Explicitly not in R5
Full dashboard CRUD, audit events for auth activity, featured projects, category management, analytics, queues, or scheduled sync.
Conventions
Branch: feat/r5-moderate-enrich
vp install before starting; vp check, vp run -r test, vp run -r build before merging.
Error codes: 400 invalid input, 401 no session, 403 no permission, 404 unavailable public data, 409 duplicate/conflict. Public list/detail always applies status = 'published' on the server.
Lifecycle: pending → approve → published / reject → rejected → valid resubmit → pending; published → remove → removed (terminal). No draft for user submissions in R4/R5.
R5 — Moderate, enrich, and launch
Outcome
One admin can turn minimal R4 submissions into complete published listings. This is the launch gate.
Context
project_submission(packages/db/src/schemas/submission.ts) already hasrejectionReason,moderatedAt,moderatedBy— reuse them, do not add moderation fields toprojectfor launch.adminProcedurealready exists (packages/api/src/procedures.ts,401no session /403non-admin) — use it for every moderation mutation.submissionRouter(packages/api/src/routers/submission.ts) currently has onlycreate+listonprotectedProcedure— extend with admin + resubmit procedures below./_panelshell (apps/web/src/routes/_panel/route.tsx,my-submissions.tsx) + sidebar/data-table from PR Enhance sidebar and submission data table components #51 already exist. Roadmap text says/dashboard— reuse/_panelas the admin surface unless there is a reason to rename; document the choice.canonicalizeGithubUrlin@altstack/shared/lib/github. Reuse it in approve/reject/resubmit paths.Work
project_submission(rejectionReason,moderatedAt,moderatedBy); do not add them toprojectfor launch.audit_log: actor ID, action, target submission/project ID, optional metadata/reason, timestamp. Records moderation mutations only (no auth-activity events).approveSubmission,rejectSubmission. Keep remove as an internal admin operation onprojectif needed before launch.approveSubmissioninput is the admin enrich step: tagline, short description, logo URL, optional website (prefilled from submission, editable), optional Markdown content, slug (prefilled fromprojectName, editable), and 1–3 existing category slugs.approveSubmissionhandler re-checks canonical duplicate against bothproject.repositoryUrlandproject_submission.repositoryUrl, refetches fresh GitHub metadata, then in one transaction inserts thepublishedproject + repository metadata + category links, marks the submissionapproved, and writes exactly one audit event. Slug collision resolves with a numeric suffix or409if unresolvable; document the choice in the issue/PR.adminProcedure. Invalid state transitions (e.g. approve an already-approved, reject a non-pending) return409./_panelroute (see Context on/dashboardnaming): paginated pending-submissions list (reuse data-table), approve form with enrich fields, reject dialog requiring a reason.resubmitSubmissionprocedure (owner-only,protectedProcedure+ submitter check) atomically clears the rejection reason and sets status back topending. General editing of published projects remains R6.403, unauthenticated401, invalid transition409, duplicate canonical on approve409, resubmitrejected→pending(+ non-owner403), exactly-one-audit-event per mutation in the same transaction. Run the complete smoke journey: seed → browse → search → sign in → submit (3 fields) → enrich/approve/reject → public/private assertions.adminrole is assigned), and known limitations.Done when
rejectedback topendingfor another review.vp check,vp run -r test, andvp run -r buildpass on the release branch.Explicitly not in R5
Full dashboard CRUD, audit events for auth activity, featured projects, category management, analytics, queues, or scheduled sync.
Conventions
feat/r5-moderate-enrichvp installbefore starting;vp check,vp run -r test,vp run -r buildbefore merging.beforeLoadis UX guard only.400invalid input,401no session,403no permission,404unavailable public data,409duplicate/conflict. Public list/detail always appliesstatus = 'published'on the server.pending→ approve →published/ reject →rejected→ valid resubmit →pending;published→ remove →removed(terminal). Nodraftfor user submissions in R4/R5.Close-out (fill on merge)