Skip to content

PR 7 — Account settings + vote reminders + email compliance footer#7

Merged
sidhujag merged 6 commits intomainfrom
account-settings-pr7
Apr 21, 2026
Merged

PR 7 — Account settings + vote reminders + email compliance footer#7
sidhujag merged 6 commits intomainfrom
account-settings-pr7

Conversation

@sidhujag
Copy link
Copy Markdown
Member

Summary

Adds three Account-screen features and the backend infrastructure for governance vote reminder emails. All password / vault mutations are atomic and round-trippable; account deletion is GDPR-complete; reminder emails link to the frontend host (not the backend API) and every outbound email now carries a transparency footer.

Frontend companion: syscoin/sysnode-info#account-settings-pr7

What's in it

Change password (atomic, vault-aware)

  • POST /auth/change-password accepts an optional vault:{blob,etag} alongside the new authHash. One DB transaction updates users.stored_auth and vaults together — both land or neither does.
  • Returns 409 vault_rewrap_required if the client omits the rewrapped blob while a vault exists, so we can never leave a vault the user can no longer decrypt.

Notification preferences

  • users.notification_prefs JSON blob. GET + PUT /auth/prefs with zod-validated shape. Default {voteReminders:{enabled:true}} — opt-out, not opt-in.

Delete account (GDPR erasure)

  • DELETE /auth/account requires session auth and fresh password re-proof (oldAuthHash). Runs in a single transaction:
    1. pendingRegistrations.purgeForEmail(email) — the one table keyed by email rather than user_id; without this a stale verification token could silently re-mint the account.
    2. users.deleteById(id)ON DELETE CASCADE handles sessions, vaults, email_verifications, tracked_masternodes, vote_reminder_log, vote_receipts.
  • Clears sid + csrf cookies, returns 204.

Governance vote reminders

  • Schema: vote_reminder_log.proposal_hashscope_key (stores cycle:<voting_deadline_unix>). ON DELETE CASCADE audited on every user-scoped table.
  • New reminderLog module (idempotent has / insert).
  • New reminderDispatcher.tick(): cycle derived from the earliest active proposal's end_epoch; walks verified opted-in users; for each bucket (days_before, final_24h) skips both if the user has any relayed receipt this cycle (voteReceipts.hasAnyRelayedInCycle). Injectable now for deterministic tests.
  • mailer.sendVoteReminder: generic, bucket-aware subject + body; no per-proposal hashes leaked.
  • server.js wires the dispatcher behind an hourly setInterval.

Mailer URL fix

  • Every templated link now derives from publicBaseUrl (FRONTEND_URLCORS_ORIGINhttp://localhost:3000). Reminder emails land on sysnode.info/governance and /account, not the old hardcoded syscoin.dev/* (the backend API host). Tests assert the frontend host is used and syscoin.dev is absent.

Compliance footer on every outbound email

  • New buildFooter({kind,to,accountUrl}) in three flavors:
    • vote_reminder — opt-in, manage link visible
    • account_security — transactional, cannot disable, review link
    • account_verification — no account yet, safe-ignore path
  • Every template declares footerKind; buildMessage throws if missing, so a new email template cannot ship without a compliance footer.
  • Footer always surfaces the recipient address (HTML-escaped) so forwarded/misrouted mail is recognizable. Attribution line marks every message as automated, do-not-reply.

Test plan

  • Full backend suite: 451/451 pass (23 suites).
    • New coverage: delete-account route (session required, CSRF required, wrong password rejected, invalid body, cascade audit, email-keyed pending-registration purge), reminder dispatcher (cycle gate, bucket suppression on any relayed vote, idempotency, per-user isolation), reminderLog, deleteById, hasAnyRelayedInCycle, listWithRemindersEnabled, mailer URL assertions, buildFooter by kind, unknown-kind throws, every sendXxx carries its footer, every template declares footerKind.
  • Manual: change password → reload → vault still decrypts with new password.
  • Manual: opt into reminders; leave one active proposal unvoted; confirm the days_before email arrives with links to sysnode.info/governance and /account, not syscoin.dev, and contains the compliance footer.
  • Manual: vote on any active proposal, then force a dispatcher tick; confirm neither bucket fires for that user in that cycle.
  • Manual: delete account; confirm /auth/me returns 401, the vault is gone, and re-registering the same email works.

Notes for review

  • pending_registrations is intentionally not in the FK cascade chain (it's keyed by email, not user_id), so deleteById's contract explicitly delegates that row to the caller — documented in the deleteById comment in lib/users.js so nobody wires a "just call deleteById" path around it later and silently leaks re-registration tokens.
  • Out of scope (flagged, not doing here):
    • List-Unsubscribe / List-Unsubscribe-Post (RFC 8058) one-click unsubscribe — Gmail/Yahoo enforce this for bulk senders. Needs a signed unsubscribe endpoint + route + spec review; belongs in a small follow-up.
    • Physical postal address for CAN-SPAM/CASL — we don't have one yet; not fabricating it. Drops into attributionText as a one-liner once there is an official address.

Made with Cursor

…footer (PR 7)

Adds three Account-screen features and the backend infrastructure for
governance vote reminder emails. All password / vault mutations are
atomic and round-trippable; account deletion is GDPR-complete; reminder
emails link to the frontend (not the backend API) and carry a
transparency footer on every send.

Change password (atomic, vault-aware)
- POST /auth/change-password accepts an optional {vault:{blob,etag}}
  alongside the new authHash. Single DB transaction updates
  users.stored_auth and vaults together — both land or neither does.
- Returns 409 vault_rewrap_required if the client omits the rewrapped
  blob while a vault exists, so we can never leave a vault the user
  can no longer decrypt.

Notification preferences
- users.notification_prefs JSON blob; GET + PUT /auth/prefs with
  zod-validated shape. Default {voteReminders:{enabled:true}} — opt-out,
  not opt-in.

Delete account (GDPR erasure)
- DELETE /auth/account requires session auth AND fresh password re-proof
  (oldAuthHash). Runs atomically:
    1. pendingRegistrations.purgeForEmail(email) — the one table keyed
       by email rather than user_id; without this a stale verification
       token could silently re-mint the account.
    2. users.deleteById(id) — ON DELETE CASCADE handles sessions,
       vaults, email_verifications, tracked_masternodes,
       vote_reminder_log, vote_receipts.
- Clears sid + csrf cookies, returns 204.

Governance vote reminders
- Schema: vote_reminder_log.proposal_hash -> scope_key (stores
  cycle:<voting_deadline_unix>). ON DELETE CASCADE audited on every
  user-scoped table.
- New reminderLog module (idempotent has/insert).
- New reminderDispatcher.tick(): cycle derived from earliest active
  proposal's end_epoch; walks verified opted-in users; for each bucket
  (days_before, final_24h) skips BOTH if the user has any relayed
  receipt this cycle (voteReceipts.hasAnyRelayedInCycle). Injectable
  `now` for deterministic tests.
- Mailer sendVoteReminder: generic, bucket-aware subject + body; no
  per-proposal hashes leaked.
- server.js wires the dispatcher behind an hourly setInterval.

Mailer URL fix
- Every templated link now derives from publicBaseUrl
  (FRONTEND_URL -> CORS_ORIGIN -> http://localhost:3000). Reminder
  emails land on sysnode.info/governance and /account, not the old
  hardcoded syscoin.dev/* (the backend API host). Tests assert the
  frontend host is used and syscoin.dev is absent.

Compliance footer on every outbound email
- New buildFooter({kind,to,accountUrl}) in three flavors:
    vote_reminder         — opt-in, manage link visible
    account_security      — transactional, cannot disable, review link
    account_verification  — no account yet, safe-ignore path
- Every template declares footerKind; buildMessage throws if missing,
  so a new email template cannot ship without a compliance footer.
- Footer always surfaces the recipient address (HTML-escaped) so
  forwarded/misrouted mail is recognizable. Attribution line marks
  every message as automated, do-not-reply.

Made-with: Cursor
@sidhujag
Copy link
Copy Markdown
Member Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e54f7016f9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
proposal_hash TEXT NOT NULL,
scope_key TEXT NOT NULL,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add forward migration for vote_reminder_log column rename

Adding scope_key only by editing 001_init.sql breaks upgrades for any database that already applied the old 001_init.sql (which still has proposal_hash). lib/db.js records applied migrations by filename, so 001_init.sql will not be re-run, and those instances keep the old schema; then startup code that prepares queries on scope_key (via createReminderLog in server.js) will fail with no such column: scope_key, preventing the service from booting after deploy. This needs a new migration file that renames/rebuilds vote_reminder_log for existing databases.

Useful? React with 👍 / 👎.

…und 1 P1)

Move the proposal_hash -> scope_key rename out of 001_init.sql and
into a dedicated 002_vote_reminder_log_scope_key.sql. lib/db.js tracks
applied migrations by filename, so an in-place edit to 001 only
reaches fresh databases; any existing deployment would keep
proposal_hash and startup would blow up on reminderLog's `scope_key`
prepared statements.

- 001_init.sql: reverted to the shipped schema (proposal_hash). No
  semantic change for fresh databases — 002 renames the column
  immediately afterward — but 001 on disk now matches 001 as applied
  on every existing production row, which is what the migration
  tracker assumes.
- 002_vote_reminder_log_scope_key.sql: single ALTER TABLE RENAME
  COLUMN. SQLite >= 3.25 updates the UNIQUE(user_id, *, bucket)
  constraint in place so no further DDL is needed.
- db.test.js: regression test exercises the upgrade path explicitly
  — simulate a pre-PR-7 DB (proposal_hash + no 002 row) and assert
  migrate() renames the column while preserving data and the
  UNIQUE constraint.

Made-with: Cursor
@sidhujag
Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@sidhujag
Copy link
Copy Markdown
Member Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c6a1f83bf1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread routes/auth.js Outdated
Comment on lines +551 to +556
const existingVault =
vaults && typeof vaults.get === 'function'
? vaults.get(req.user.id)
: null;
if (existingVault && !parsed.data.vault) {
return res.status(409).json({ error: 'vault_rewrap_required' });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Enforce vault-presence check inside the transaction

The /auth/change-password handler checks whether a vault row exists before runAtomic, then updates stored_auth inside the transaction. In a multi-worker/process deployment sharing the same DB, another request can create the user's first vault row after this precheck but before the password rotation commits, so a request without vault still succeeds and leaves the vault wrapped under the old key. That reintroduces the lockout state this 409 guard is intended to prevent; the existence check should be performed in the same transaction as the auth update (or guarded via a conditional write).

Useful? React with 👍 / 👎.

…schema (Codex round 2)

Backend Codex PR 7 round-2 findings addressed:

P2 routes/auth.js:556 — vault-presence check now runs inside the same
runAtomic() transaction as the auth rotation. Previously the check
ran as a standalone SELECT before runAtomic, which in a multi-worker
deployment opens a TOCTOU window where a peer request can create
the user's first vault row between our pre-check and our COMMIT.
The resulting /auth/change-password would succeed with a new authHash
that cannot open the peer-wrapped vault → permanent lockout. Moving
the check into the transaction collapses that window to zero
(better-sqlite3 serializes writes in-process; SQLite's write lock
serializes across processes). Added a regression test that spies on
vaults.get and asserts every call during /change-password happens
with db.inTransaction === true.

db/migrations revert — per pre-production lifecycle, edits to the
v1 schema belong in 001_init.sql directly; there is no deployed
database to migrate from. The scope_key column is now defined
in-place and the previous 002_vote_reminder_log_scope_key.sql
forward migration + its regression test have been removed. Added
a reviewer-facing note in the 001_init.sql header (and a pointer
at the vote_reminder_log table) explaining the rule: edit in place
until v1 ships, new files thereafter.

Made-with: Cursor
@sidhujag
Copy link
Copy Markdown
Member Author

@codex review

Round-2 P2 findings addressed in ec511e2:

  • routes/auth.js:556 — vault-presence check now lives inside the runAtomic(...) block, so the auth rotation and the vault-existence read commit as one atomic unit. Regression test in tests/auth.routes.test.js spies on vaults.get and asserts db.inTransaction === true at every call during /auth/change-password.

Review guidance — sysnode-backend is pre-production. There is no deployed database that has applied 001_init.sql yet, so forward migrations are not relevant until v1 ships. In-place edits to 001_init.sql are the correct schema-lifecycle move right now; the previous 002_vote_reminder_log_scope_key.sql and its regression test have been removed. This rule is documented in the header of 001_init.sql (and cross-referenced at the vote_reminder_log table) — please treat pre-launch schema edits as in-place only and do not re-propose forward ALTERs. Once v1 ships, the rule inverts and every change becomes a new 002+ file.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ec511e2f1b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread routes/auth.js
// invalid_blob / blob_too_large — all of which roll back
// the transaction here before we touch auth state. Caught
// below and translated into HTTP status codes.
const putOut = vaults.put(req.user.id, {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Guard vault writes when vault repo is optional

createAuthRouter documents vaults as optional for auth-only mounts, but this path unconditionally dereferences vaults.put when a client sends a vault payload. In configurations where vaults is intentionally omitted (or a harness only mounts auth), /auth/change-password will throw a TypeError and surface a 500 instead of a deterministic API error, which makes the endpoint brittle in the exact mode the router claims to support. Add an explicit guard/rejection for vault-bearing requests when no vault repo is configured.

Useful? React with 👍 / 👎.

…ent (Codex round 2 P3)

createAuthRouter documents `vaults` as optional (auth-only harnesses
may omit it), but /auth/change-password previously dereferenced
`vaults.put` unconditionally when the client sent a vault payload,
surfacing a TypeError → 500 instead of a deterministic API error in
the exact mode the factory claims to support.

Fail fast with 503 server_misconfigured (the same code used for
other config-level faults, e.g. a missing KDF pepper) when the
client sends a vault body but no repo is wired. The auth-only path
(no vault in body) continues to work without a repo, matching the
optional contract.

Tests added in tests/auth.routes.test.js:
- vault-bearing request with no vault.put → 503 server_misconfigured
  and auth is NOT rotated (old password still logs in).
- auth-only rotation still succeeds when vault.put/.get are absent,
  proving the guard is scoped to the vault-bearing branch only.

Made-with: Cursor
@sidhujag
Copy link
Copy Markdown
Member Author

@codex review

Round-2 P3 addressed in 97af6c5:

  • routes/auth.js/auth/change-password now fails fast with 503 server_misconfigured when the client sends a vault payload but vaults.put is not wired (the optional-vaults contract case). Auth-only rotation (no vault body, no vault repo) still succeeds. Tests added for both branches in tests/auth.routes.test.js.

Schema reviewer guidance (restating from previous ping): sysnode-backend is pre-production, so forward migrations are not relevant — in-place edits to 001_init.sql are the correct schema-lifecycle move until v1 ships. This rule is documented in the 001_init.sql header.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 97af6c50ca

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread routes/auth.js Outdated
Comment on lines +112 to +113
if (vaults && typeof vaults.put !== 'function') {
throw new Error('createAuthRouter: vaults.put must be a function');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Require vaults.get when wiring change-password vault support

/auth/change-password relies on vaults.get to enforce vault_rewrap_required, but createAuthRouter only validates vaults.put during setup. In deployments/tests that pass a partial vault repo with put but no get, existingVault is forced to null and an auth-only password change can succeed even when a vault row exists, which can leave the account unable to decrypt its vault. This should fail fast as misconfiguration (or require vaults.get) whenever vault-backed password rotation is enabled.

Useful? React with 👍 / 👎.

…iring change-password (Codex round 2 P3 followup)

/auth/change-password uses BOTH `vaults.put` (to rewrap the blob)
and `vaults.get` (to enforce the vault_rewrap_required 409 inside
the atomic rotation). The previous factory guard only validated
`.put`. A partial repo with `put` but no `get` would make the
in-transaction `existingVault` fall through to `null`, silently
bypassing the 409 guard — a vault-bearing user could rotate their
password without rewrapping and permanently lock themselves out of
their vault.

Strengthen the factory: when `vaults` is provided, both `.put` and
`.get` must be functions. `vaults: undefined` (auth-only mount) is
still accepted per the optional contract. The in-transaction call
site is simplified accordingly — the defensive `typeof vaults.get
=== 'function'` check is redundant once the factory enforces the
invariant at construction.

Tests reworked in tests/auth.routes.test.js:
- Dropped two monkey-patch tests that modeled post-construction
  `delete ctx.vaults.put/.get` — states the factory now prevents.
- Added a focused `createAuthRouter factory contract` describe
  block with pure-input tests: partial-repo shapes throw at
  construction, complete repo accepted, `vaults: undefined`
  accepted, `runAtomic` still required regardless of vaults.

Made-with: Cursor
@sidhujag
Copy link
Copy Markdown
Member Author

@codex review

Round-2 P3 followup addressed in 803e9f8:

  • routes/auth.js:112createAuthRouter now requires a complete vault repo (both .put AND .get) when vaults is provided, per the /change-password usage pattern. Partial repos are rejected at construction so the 409 vault_rewrap_required guard can never be silently bypassed. vaults: undefined (auth-only mount) is still accepted per the optional contract.
  • routes/auth.js (inside runAtomic) — defensive typeof vaults.get === 'function' check simplified to vaults ? vaults.get(...) : null, now that the factory invariant is enforced at construction.
  • tests/auth.routes.test.js — replaced two monkey-patch tests (modeling post-construction mutation states the factory now prevents) with a focused createAuthRouter factory contract describe block: partial-repo shapes throw, complete repo accepted, undefined accepted, runAtomic still required regardless of vaults.

Pre-production reviewer reminder (repeated for completeness): forward migrations are not relevant yet — in-place edits to 001_init.sql are the correct schema-lifecycle move until v1 ships.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 803e9f8d02

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/reminderDispatcher.js Outdated
}

const scopeKey = `cycle:${earliestEndEpoch}`;
const proposalHashes = proposals.map((p) => p.hash);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Limit vote suppression to proposals in the active cycle

tick() defines the reminder cycle from the earliest proposal deadline, but proposalHashes is built from all active proposals. When active proposals have mixed endEpoch values, a user who voted only on a later-deadline proposal is treated as already-voted and gets suppressed for the earlier-deadline cycle, so required reminders are skipped. This also conflicts with the cycle semantics documented in this file (new earlier deadline should form a new cycle). Filter hashes to the proposals in the chosen cycle before calling hasAnyRelayedInCycle.

Useful? React with 👍 / 👎.

…ent cycle (Codex round 2 P2)

lib/reminderDispatcher.js — `tick()` computed the cycle from the
earliest end_epoch but then built `proposalHashes` from ALL active
proposals. When active proposals spanned multiple cycles (e.g. a
proposal for superblock N+1 was submitted while superblock N voting
is still open), a user who voted only on the later-cycle proposal
was treated as already-voted for the earlier cycle and had their
reminder silently suppressed — the exact opposite of the product
intent, which is "if user voted in THIS cycle, skip; otherwise
remind".

Fix: compute `cycleProposals` as the set of proposals whose
deadlineMs matches the earliest deadline (Syscoin governance rule:
proposals voting for the same superblock share the same voting-
deadline block, hence the same end_epoch). Use that filtered set
for (a) the `hasAnyRelayedInCycle` gate, (b) the email body's
`proposalCount`, and (c) the tick result's `proposalCount` metric.

Tests (lib/reminderDispatcher.test.js):
- Updated the `proposalsEndingIn` fixture so P1 and P2 share the
  same end_epoch, matching the semantic meaning of "same cycle"
  the fixture has always intended (the prior +1h offset was a
  test-only fiction).
- Added a targeted regression test: with P1 ending in 48h (current
  cycle, days_before bucket) and P2 ending in 96h (future cycle,
  too_early on its own), a user who voted on P2 must still get the
  reminder for P1's cycle, with proposalCount=1 rather than 2.

Made-with: Cursor
@sidhujag
Copy link
Copy Markdown
Member Author

@codex review

Round-2 P2 addressed in a7387da:

  • lib/reminderDispatcher.jstick() now defines the cycle as the proposal set whose deadlineMs matches the earliest deadline (Syscoin governance rule: proposals voting for the same superblock share the same end_epoch). hasAnyRelayedInCycle, the email's proposalCount, and the tick-result metric all use this cycle-scoped set, so a vote on a later-cycle proposal no longer suppresses the current cycle's reminder.
  • lib/reminderDispatcher.test.js — fixture updated so P1/P2 share the same end_epoch (true same-cycle). Added a targeted regression test for the flagged scenario: P1 ending in 48h + P2 ending in 96h, user voted on P2 → P1 reminder still fires with proposalCount=1.

Pre-production reviewer reminder (again): forward migrations are not relevant — in-place edits to 001_init.sql are the correct schema-lifecycle move until v1 ships.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@sidhujag sidhujag merged commit 38c5d4b into main Apr 21, 2026
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