Living list of improvements, nits, and observations that agents notice while working on unrelated issues. Items here are not yet GitHub issues — they are promoted via /promote-backlog after the user confirms.
Use /log-improvement <short description> or append manually with this template:
## [YYYY-MM-DD] <short title>
- **Severity**: nit | minor | major
- **File(s)**: path:line (or "multiple" with list)
- **Observed during**: issue #N / <task description>
- **Suggestion**: one or two sentences describing the change, not the fix
/promote-backlog reads this file, asks which entries to promote, creates GitHub issues with the claude-suggested label, and appends - **Promoted**: #<issue> to the entry.
Do not manually edit - **Promoted** lines.
- Severity: minor
- File(s):
client/src/components/admin/ProgramFormModal.tsx; rendererclient/src/components/program/ProgramContent.tsx; typeApiProgram.contentinclient/src/lib/api.ts - Observed during: adding templatable
programs.contentJSONB + section renderer (Dogfooding Denver rich content) - Suggestion: the
contentcolumn + server validation (validateProgramContent) + renderer ship now, but admins can't author it in the UI — prod content is set by migration/seed and previews use the mock fixture. Add a per-section editor (or a validated raw-JSON textarea gated byvalidateProgramContent) toProgramFormModalso events can be authored without a redeploy.
- Severity: minor
- File(s):
server/api/middleware/auth.middleware.js(requireTeamMemberOrAdmin) - Observed during: PR #73 review — fixing Blocker 1 (domain check added to
requireOwnWallet) - Suggestion:
requireAdminandrequireOwnWalletboth gate onDISABLE_SIWS_DOMAIN_CHECK/EXPECTED_DOMAINimmediately after statement validation;requireTeamMemberOrAdminskips this check entirely. Add the same domain-check block in a dedicated hardening pass.
- Severity: nit
- File(s):
.claude/commands/*.md - Observed during: switching
stadium-testerfrom MCP/subagent to a project-scoped Skill - Suggestion: per the Claude Code skills doc,
.claude/commands/foo.mdand.claude/skills/foo/SKILL.mdcreate the same/fooslash command, but skills add auto-invocation, supporting files, subagent forking, and ecosystem sharing. When time allows, lift each ofship-issue,triage-issue,address-review,log-improvement,promote-backlog,pre-pr-check,verify-testerinto aSKILL.mddirectory. No behavior change required, just packaging.
- Severity: minor
- File(s):
client/src/pages/AdminPage.tsx(develop has two; workflow branches may have more) - Observed during: PR #29 review — found an infinite
useEffectfetch loop that the disabled lint rule would have caught - Suggestion: Walk each
eslint-disable-next-line react-hooks/exhaustive-depsin the file, justify or fix it. Prefer the fix: either include the missing dep (and memoize callbacks if needed) or move the logic so the dep isn't required. Leaving the suppression in place masks real bugs like the one PR #29 had to patch.
- Severity: minor
- File(s):
.claude/skills/stadium-tester/SKILL.md,client/src/lib/siwsUtils.ts,server/api/middleware/auth.middleware.js - Observed during: building the
stadium-testerskill - Suggestion:
stadium-testercurrently skips SIWS-gated live flows because there's no way to sign a SIWS message programmatically from a headless browser. Options: (a) mount a test wallet via a Playwright extension harness, (b) add a preview-only signed-request bypass keyed to an ephemeral secret, (c) stub the SIWS verification in preview-mode server. None are trivial — pick one when SIWS coverage becomes load-bearing.
- Severity: minor
- File(s):
client/src/pages/AdminPage.tsx:305,351,client/src/components/admin/M2BuildingProjectsTable.tsx:64 - Observed during: wiring VITE_USE_MOCK_DATA preview flag
- Suggestion: three admin-side
fetch('/api/m2-program/.../confirm-payment')calls bypassclient/src/lib/api.ts. They're SIWS-gated so they can't execute in preview without a wallet signature (low preview risk), but they also dodge the mock-mode plumbing and any future cross-cutting concerns (retry, logging, error mapping). Refactor them to callapi.confirmPayment()which already has a mock branch.
- Severity: minor
- File(s):
client/src/pages/AdminPage.tsx,client/src/pages/HomePage.tsx,client/src/pages/ProjectDetailsPage.tsx,client/tailwind.config.ts - Observed during: repo cleanup pass (running
npm run lintas part of verification) - Suggestion:
npm run lintfails pre-existing — mostly@typescript-eslint/no-explicit-any, a fewno-useless-escapein a regex, and arequire()in tailwind config.CLAUDE.md§6 says lint must pass for PRs, but baseline is broken. Clean these in a dedicated PR before the first/ship-issuerun touches one of those files (otherwise/pre-pr-checkwill block work that didn't cause the errors).
- Severity: minor
- File(s):
server/db.js,server/api/repositories/project.repository.js,server/scripts/*.js,server/models/*.js - Observed during: repo cleanup pass (CLAUDE.md correction)
- Suggestion: the API runs on Supabase while
server/scripts/still use Mongo/Mongoose as an offline staging layer. This split is a footgun — future agents may reach for the wrong layer. Consider porting the remaining useful scripts (seed-dev, migration.js, fix-bounty-amounts, list-winners-zero-paid, set-live-urls, set-m2-final-submissions) to Supabase directly, then droppingmongooseandserver/models/. Not urgent, but worth flagging before the next large refactor.
- Severity: minor
- File(s):
supabase/migrations/*,server/api/repositories/project.repository.js,client/src/components/admin/WinnersTable.tsx - Observed during: fixing issue #27 (Plata Mia bounty split). Source of truth in
server/migration-data/prizes-symbiosis-2025.csvdifferentiates USDC vs xx-token bounties. The schema has no currency column, so xx-network-denominated bounties get stored as bare NUMERIC amounts and misread as USDC when totalled alongside real USDC rows. - Suggestion: add
bounty_prizes.currency(CHECKIN 'USDC','DOT','xx','other'), backfill existing rows toUSDC(the de-facto denomination today), updatetransformProjectin the repository, and teach WinnersTable to render the currency per row. Not urgent — Phase 1 of the revamp explicitly defers this per the "additive only" principle. Revisit alongside issue #26.
- Severity: minor
- File(s):
client/src/components/admin/WinnersTable.tsx:186,211 - Observed during: fixing issue #27 (Plata Mia bounty split). After splitting her concatenated row into three, the table display renders all three (the component iterates, see line 547), but
openManageModalprefills its state frombountyPrize?.[0]only — so the Manage modal loses access to bounties 2..N. - Suggestion: rework the Manage modal to either (a) let the admin pick which bounty row to edit, or (b) show all bounties in an editable list. Not urgent — affects admin UX only, on projects with multiple bounty rows (Plata Mia is currently the only such row once #27 lands). Log once Phase 1 rehearsal exposes whether this actually blocks anything operational.
- Severity: minor
- File(s):
server/scripts/*.js,CLAUDE.md§4 - Observed during: writing
server/scripts/fix-plata-mia-bounties.jsfor issue #27. - Suggestion:
CLAUDE.mdsaysserver/scripts/is Mongo-only, but precedent for Supabase-touching scripts exists (deploy-all.jsand the since-deletedfix-bounty-amounts-supabase.js). The rule is therefore already inaccurate. Either (a) update CLAUDE.md to acknowledge the mixed layer and specify when each is appropriate, or (b) move Supabase scripts into a sibling dir likeserver/scripts/supabase/and enforce the original rule. Paired with the existing "Flatten the Supabase↔Mongo dual data layer" entry above.
- Severity: minor
- File(s):
server/migration-data/payouts.csv,server/migration-data/prizes-symbiosis-2025.csv - Observed during: writing
reconcile-bounty-amounts-supabase.jsfor issue #28. The symbiosis-2025 CSV is the only per-bounty source of truth in the repo.payouts.csvhas a singleTotal Prize (USDC)column for synergy-2025 and symmetry-2024, which is not enough to reconcile multi-track winners or verify individual bounty names/amounts. - Suggestion: ask WebZero for the same-shaped screenshot / spreadsheet for synergy-2025 and symmetry-2024 (project, track, total prize, currency). Transcribe to
prizes-synergy-2025.csvandprizes-symmetry-2024.csv, register them inCSV_PATHSin the reconciliation script, re-run. Until then, those two events are accepted as-is (their existing DB rows may be inaccurate in ways we can't verify).
- Severity: minor
- File(s): Supabase
projectstable,server/migration-data/prizes-symbiosis-2025.csv - Observed during: reconciliation for issue #28. The CSV lists five Marketing 1 / Marketing 2 winners (Right of the DOTty, Khoj, Pointillism, Connected by the Dots, Crypto Therapy | Polkadot) that have no
projectsrow at all — not just missing bounty rows. Reconciliation script skipped them because the project records don't exist. - Suggestion: open a follow-up issue to add these projects. Needs project-level data (name, description, team wallets, categories) beyond what's in the prize CSV — likely a separate source-of-truth fetch from WebZero before implementation. Not blocking for Phase 1 alpha (Marketing projects aren't M2 incubator candidates).
- Severity: nit
- File(s):
server/api/services/notification.service.js:9 - Observed during: issue #68 (revamp-P2-02 notifications dispatcher) — reviewer flagged
- Suggestion:
notify()collapses two distinct audit states intoerror='no_contact'— (a) wallet has never registered an email, (b) wallet had a row that was cleared. Thenotificationsaudit log loses this distinction. If Phase 3 adds retry logic or analytics over the table, the two states want separate reasons (e.g.no_contact_rowvsno_email_set). Not blocking for P2-02; the spec does not mandate splitting them.
- Severity: nit
- File(s):
server/api/repositories/notification.repository.js:20 - Observed during: issue #68 (revamp-P2-02 notifications dispatcher) — reviewer flagged
- Suggestion: caller-supplied
statusis written straight to Supabase; the DBCHECKconstraint catches illegal values but the caller gets an opaque Postgres error rather than anError('invalid_status'). Consider an in-process whitelist (queued | sent | failed | skipped) at the repo entry point. Consistent with the existingwallet-contact.repository.jspattern (no in-process validation), so deferring is fine; revisit when Issue 3 wires Resend.
- Severity: nit
- File(s):
server/api/services/notification.service.js - Observed during: issue #68 (revamp-P2-02 notifications dispatcher) — reviewer flagged
- Suggestion: the four-argument signature and the "writes status=queued, does NOT call the provider yet" contract are implied. P2-04 implementers wiring
notify(...)into admin controllers will benefit from a one-line JSDoc above the method documenting (args, return shape, idempotency via(recipient, eventType, sourceId)).
- Severity: nit
- File(s):
server/api/services/email-transport.js,server/server.js - Observed during: issue #69 (revamp-P2-03 Resend integration) — reviewer flagged
- Suggestion: if
RESEND_API_KEYis configured butRESEND_FROM_EMAILis unset,notify()passesfrom: undefinedto Resend, which returns a 4xx that is caught and marks the rowfailed. The send silently fails with an opaque reason. Add a one-time startup check (or a guard inside_trySend) that surfaces a clearfrom_email_not_configurederror instead of an opaque provider 4xx.
- Severity: nit
- File(s):
server/api/services/notification-templates/m2-changes-requested.js - Observed during: issue #69 (revamp-P2-03 Resend integration) — reviewer flagged
- Suggestion:
payload.feedbackis rendered into the email body verbatim (now HTML-escaped). Very long admin feedback produces an oversized email. Consider truncating with a "see the full feedback on your project page" link once P2-05 ships the project-page feedback surface.
- Severity: nit
- File(s):
server/api/controllers/project.controller.js:75,server/api/controllers/project.controller.js:188 - Observed during: issue #70 (revamp-P2-04 notify trigger wiring) — reviewer flagged
- Suggestion: two pre-existing
console.logcalls (a debug payload preview inupdateProject, and an M2-agreement confirmation log) predate Phase 2 and were left untouched. Server code elsewhere uses theloggerutility (server/api/utils/logger.js). Convert these tologger.debug/logger.info(or remove the debug preview) in a dedicated cleanup pass — out of scope for #70's minimal diff.
- Severity: minor
- File(s):
client/src/components/TeamPaymentSection.tsx:210 - Observed during: full user-journey test pass (team-member journeys, Team & Payments tab)
- Suggestion: team-member cards render with
key={member.walletAddress || index}. Wallet address is not guaranteed unique across a project's team members — in the mock dataset two Plata Mia members share the redacted address5MockWalletAddressRedactedForPreviewPurposes00000, so React logs "Encountered two children with the same key" (3× per Team & Payments render). The|| indexfallback only triggers on a falsy address, so a non-empty-but-duplicate address still collides. Production addresses are normally distinct so it does not surface there, but keying on a non-unique field is fragile. Key by a stable unique member id, or always fold in the index (e.g.key={`${member.walletAddress}-${index}`}).
- Severity: minor
- File(s):
server/server.js,server/api/routes/admin-session.routes.js - Observed during: security audit after #121 (substrate verifier P0)
- Suggestion:
POST /api/admin/sessiondoes SIWS signature verification (~50–200ms per call) and is unauthenticated. A burst of a few hundred req/sec could noticeably load the server. Addexpress-rate-limitwith tight per-IP limits on/api/admin/session(e.g. 10/min) and a looser app-wide default (e.g. 200/min/IP). No new env vars needed. - Promoted: #127
- Severity: nit
- File(s):
server/server.js - Observed during: security audit after #121
- Suggestion: server doesn't set
X-Frame-Options,X-Content-Type-Options, or CSP. Railway terminates TLS and may set some at the edge, but defense-in-depth at the app layer is cheap. Dropapp.use(helmet())after the CORS middleware. No CSP customisation needed since the server only serves JSON, not HTML. - Promoted: #128
- Severity: nit
- File(s):
server/api/auth/sessionToken.js:30,server/server.js - Observed during: security audit after #121
- Suggestion:
getSecret()throws on the firstissueSessionToken/verifySessionTokencall if the env var is missing or under 32 chars. The server still boots successfully with a misconfigured secret, and the first admin to try logging in sees a 500. Move the check intoserver.jsstartup so misconfiguration fails fast and visibly in the deploy logs. - Promoted: #129
- Severity: nit
- File(s):
server/api/middleware/auth.middleware.js(requireAdmin,requireTeamMemberOrAdmin) - Observed during: security audit after #121
- Suggestion:
requireProgramAdminandrequireAppAdminboth stampchainontoreq.user. The other two middlewares don't. DownstreamauditLog.logSafe({ actor: { chain: req.user?.chain, … } })calls then recordactor_chain = nullfor any admin or team-member action — a forensics gap, not a security flaw. Addchain: auth.chainto bothreq.userassignments. - Promoted: #130
- Severity: minor
- File(s):
server/api/controllers/project.controller.js(approveM2,requestChanges,confirmPayment,createContinuation) - Observed during: security audit after #121
- Suggestion: program-level mutations (
program.update,sponsor.*,signups.*,admin.*,application.<status>) already log viaauditLog.logSafe. Project-level mutations don't —confirmPaymentin particular is high-value because it records on-chain payout proof. WireauditLog.logSafeAFTERres.jsonfor each, withprogramId = project.program_id || null. Continuations also worth auditing (post-M2 follow-up trail). - Promoted: #131
- Severity: minor
- File(s):
client/tsconfig.app.json - Observed during: bug investigation in #120 — missing api methods typechecked as
anybecausenoImplicitAny: falseandstrict: false - Suggestion: turn on
strict: true+noImplicitAny: trueafter a sweep cleanup. Wide blast radius — likely hundreds of errors. Phase: first ratchet justnoImplicitAny: true, fix everything, then enablestrictNullChecks, thenstrict: true. Each phase its own PR. - Promoted: #132
- Severity: nit
- File(s):
server/tests/(new) - Observed during: investigating #117's bad merge that broke prod for ~10 min
- Suggestion: vitest currently tests handlers, repositories, and verifiers in isolation. None of the 243+ tests imports
server.jsend-to-end, so a route mounting an undefined controller method passes CI but crashes Railway at boot. Add a 1-test file that doesawait import('../server.js')with a 2s timeout, asserts the server starts listening, then closes. Would have caught #117 before merge. - Promoted: #133
- Severity: nit
- File(s):
server/api/services/program-signup.service.js(Luma CSV importer) - Observed during: hardening CSV export (#122) for formula injection
- Suggestion: #122 sanitises the inbox EXPORT. The Luma CSV IMPORT reads attacker-controllable rows from a third-party file and persists them. The values aren't re-emitted to a spreadsheet from a different surface (yet), so import-side sanitisation isn't urgent — but if any future feature exports a CSV that includes signup names/emails from this table, the same defense must run. Either: apply the prefix at import time (defense in depth, alters stored data), or factor
csvCellfromprogram-inbox.service.jsinto a sharedcsv.util.jsand call it everywhere CSV is generated. - Promoted: #134