chore(notifications): remove temp migration surface (post-PR-3461 cleanup)#3468
Conversation
…anup) The (realtime, all) backfill ran successfully — 29 rows migrated to digestMode='daily', recipient list captured, courtesy emails sent. Removing the temp admin-secret-gated migration surface added in #3461 and #3463 now that it's served its purpose. Removed: - convex/alertRules.ts: _countRealtimeAllRules, _migrateRealtimeAllPage, _listAffectedUserEmailsPage, assertMigrationAdmin helper, and the TEMP MIGRATION FUNCTIONS comment block. - 3 driver scripts: scripts/migrate-discover-realtime-all.mjs, scripts/migrate-realtime-all-to-daily.mjs, scripts/migrate-list-affected-emails.mjs. - The corresponding test describe() block in convex/__tests__/alertRules.test.ts (the _listAffectedUserEmailsPage cases — admin-secret gate, channel-type filtering, enabled-flag preservation, pagination contract). Production-logic tests preserved unchanged: cross-field invariant enforcement, insert-only defaults, atomic-mutation pair flips, partial-update re-validation, omitted-sensitivity preservation. 12/12 passing post-trim. After this PR merges, run on prod: npx convex env remove --prod MIGRATION_ADMIN_SECRET The secret value was visible in Convex dashboard function-call logs during the migration window — treat as exposed, do NOT reuse for any other admin path. Generate a fresh value if a future migration needs the same admin-gate pattern. Pure delete: 500 lines removed, 0 added. TS dual typecheck clean, biome clean, vitest green.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis is a pure cleanup PR (500 lines deleted, 0 added) removing temporary admin-gated migration surfaces that were introduced in #3461/#3463 to backfill Confidence Score: 5/5Safe to merge — pure deletion of confirmed-complete migration scaffolding with no production logic changes. All changes are deletions of temporary code whose purpose is explicitly confirmed complete. No logic is modified, imports left dangling ( No files require special attention; the only remaining action is the post-merge Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Admin Script] -->|calls| B[_countRealtimeAllRules]
A -->|calls| C[_migrateRealtimeAllPage]
A -->|calls| D[_listAffectedUserEmailsPage]
B & C & D --> E[assertMigrationAdmin\nMIGRATION_ADMIN_SECRET]
E -->|verified| F[(alertRules DB)]
style A fill:#f66,color:#fff
style B fill:#f66,color:#fff
style C fill:#f66,color:#fff
style D fill:#f66,color:#fff
style E fill:#f66,color:#fff
G[✅ Migration Complete] --> H[This PR removes all red nodes]
H --> I[Production alertRules.ts\nonly retains invariant-enforcement\nand CRUD functions]
Reviews (1): Last reviewed commit: "chore(notifications): remove temp migrat..." | Re-trigger Greptile |
…time, high) rows → (daily, high) (#3481) * chore(notifications): post-#3474 migration script + audit trail (5 rows migrated to (daily, high)) Run-time discovery + migrate driver for the latent grandfathered population that PR #3474 explicitly created: rows with `digestMode: undefined, sensitivity: "high"` were exactly the population whose effective pair (per relay's `rule.digestMode ?? "realtime"`) is `(realtime, high)` — the combo PR #3474 forbids. The validator only checks on write, so existing rows weren't auto-migrated. Population at run time on tacit-curlew-777: - 18 daily/all (untouched, valid) - 5 <undefined>/high (MIGRATED to daily/high) - 2 twice_daily/all (untouched, valid) - 2 <undefined>/critical (untouched, valid: effective realtime/critical) - 1 twice_daily/critical (untouched, valid) Result: 5/5 migrated, 0 failed. Post-run discovery confirms 0 in the affected set. Implementation notes: - Filter mirrors relay semantics (`rule.digestMode ?? "realtime"`); a bare `digestMode === "realtime"` filter MISSES rows with undefined digestMode — the silent third case from PR #3461 review. First run of this script with the bare filter showed 0 matches; the corrected filter found 5. Captured as a discovery-time breakdown in the script so any future run shows the population shape, not just the matching subset. - Uses `setNotificationConfigForUser` (atomic pair-update from PR #3461), not `setDigestSettingsForUser` (which has no `sensitivity` arg in its validator). - Dry-run by default. `--apply` required to mutate. Per-row failures logged + counted; exit 1 if any failed. - Convex deploy key auth via `npx convex run` shell-out (since `setNotificationConfigForUser` is internalMutation and unreachable from `ConvexHttpClient`; cf. skill `convex-internal-functions-unreachable-via-httpclient`). Idempotent — re-running after migration finds 0 affected and no-ops. Kept committed for audit trail (matches PR #3463/#3468 pattern) and as a reusable harness if a future tightening creates a similar grandfathered population. * docs(migration-script): fix stale mutation name in header (Greptile P2) The header docstring's 'Apply' usage block and 'Safety' paragraph both referenced setDigestSettingsForUser, but the actual spawnSync call (and inline comment + commit message) correctly invoke setNotificationConfigForUser. Update both header references and add a note explaining why setDigestSettingsForUser would NOT work (its validator has no sensitivity arg) so a future operator reading the header doesn't get confused.
…ertRules rows (#3486) * chore(notifications): cleanup script for 7 grandfathered free-tier alertRules rows Companion to PR #3483 (server-side mutation gate, layer 2) and PR #3485 (relay fail-closed, layer 3). Disables notifications for the 7 free-tier users that have grandfathered alertRules rows from before the layer-2 gate existed. Mechanism: calls internal.alertRules.setAlertRulesForUser (the INTENTIONALLY-ungated operator path — see PR #3483's contract test) with enabled=false. Preserves sensitivity/channels/eventTypes; only the on/off toggle flips. Affected userIds (verified via 2026-04-28 dry-run): user_3BwtmadXVR2XLnGfIUndMvCNn3S variant=full daily/high planKey=free user_3BybzNDxo0OIm1Q0wseyVPyZZI6 variant=full daily/high planKey=free user_3Bzq0tV0AqS1NGkGfAwkGiloBUW variant=full daily/all planKey=free user_3C0GSJz7gpTcqWdujingRCxxmMs variant=full daily/high planKey=free user_3C2rpobAryOk10hR2BKopVwaupC variant=full daily/all planKey=free user_3C4Y8NV7cVZlysodXrfOQufFBRE variant=full daily/all planKey=free user_3C6K7V9lEzPIH81TdSgthOmf2ZQ variant=full daily/all planKey=free DO NOT MERGE / RUN UNTIL #3483 + #3485 ARE DEPLOYED. Otherwise the same users could re-enable through the still-open mutation surface tomorrow. The user explicitly stated: "close it first, before doing anything to free users." Tier breakdown at time of script authoring: tier 0 (free): 7 users (25%) tier 1 (pro): 18 users tier 2 (pro+): 3 users Dry-run by default. --apply required to mutate. Per-row failures logged + counted; exit 1 on any failure. Idempotent: re-running after apply finds 0 free-tier rows in the enabled set. Audit trail kept committed (matches PR #3463 / #3468 / #3481 pattern). * fix(disable-free-notif): preserve eventTypes+channels, fail-closed on lookup errors (Greptile P1+P1) Two P1 findings on PR #3486 round 1 — both real, both fixed. P1 #1 (line 142): apply mode wiped eventTypes + channels. setAlertRulesForUser PATCHES (does NOT preserve) eventTypes and channels when they're supplied in args. The previous version sent `eventTypes: [], channels: []` — which would have wiped any user's saved subscriptions/channels, even though the script claimed to "only flip enabled". A user later upgrading to PRO would have to reconfigure from scratch. Concrete impact in the current population: user_3Bzq0tV0AqS1NGkGfAwkGiloBUW has channels=[1] (one configured channel). Old version would have wiped it. Fix: pass through `row.eventTypes ?? []` and `row.channels ?? []` from the existing row (which we already have in scope from getByEnabled). P1 #2 (line 87): silent failure on entitlement lookup. Previous version did `tier: tierMatch ? parseInt(tierMatch[1]) : -1` and then `filter(r => r.tier === 0)` — so a failed `npx convex run`, timeout, or output-format change would silently mark every user as tier=-1, get filtered out as "not free", and exit 0 with "no free users to disable." A regression in the lookup path could mask actual free users behind a green-status script run. Fix: fail-closed on three signals: - result.status !== 0 → FATAL exit code 4 - tier regex no match → FATAL exit code 4 - planKey regex no match → FATAL exit code 4 Includes the last few lines of stdout/stderr in the error so the operator can see the actual failure without re-running. Latent bug also fixed: the previous dedupe-by-userId-during-iteration would have caused multi-variant free users to only have ONE variant disabled. New version: dedupe entitlement LOOKUPS by userId (per-user data, no need to look up twice), but iterate ALL rows for the cleanup target list. Multi-variant users get every variant disabled in one pass. Dry-run on prod still shows the expected 7 rows. New diagnostic surface prints `eventTypes=[N] channels=[N]` per row so the operator sees what's preserved.
…anup) (koala73#3468) The (realtime, all) backfill ran successfully — 29 rows migrated to digestMode='daily', recipient list captured, courtesy emails sent. Removing the temp admin-secret-gated migration surface added in koala73#3461 and koala73#3463 now that it's served its purpose. Removed: - convex/alertRules.ts: _countRealtimeAllRules, _migrateRealtimeAllPage, _listAffectedUserEmailsPage, assertMigrationAdmin helper, and the TEMP MIGRATION FUNCTIONS comment block. - 3 driver scripts: scripts/migrate-discover-realtime-all.mjs, scripts/migrate-realtime-all-to-daily.mjs, scripts/migrate-list-affected-emails.mjs. - The corresponding test describe() block in convex/__tests__/alertRules.test.ts (the _listAffectedUserEmailsPage cases — admin-secret gate, channel-type filtering, enabled-flag preservation, pagination contract). Production-logic tests preserved unchanged: cross-field invariant enforcement, insert-only defaults, atomic-mutation pair flips, partial-update re-validation, omitted-sensitivity preservation. 12/12 passing post-trim. After this PR merges, run on prod: npx convex env remove --prod MIGRATION_ADMIN_SECRET The secret value was visible in Convex dashboard function-call logs during the migration window — treat as exposed, do NOT reuse for any other admin path. Generate a fresh value if a future migration needs the same admin-gate pattern. Pure delete: 500 lines removed, 0 added. TS dual typecheck clean, biome clean, vitest green.
…ve realtime, high) rows → (daily, high) (koala73#3481) * chore(notifications): post-koala73#3474 migration script + audit trail (5 rows migrated to (daily, high)) Run-time discovery + migrate driver for the latent grandfathered population that PR koala73#3474 explicitly created: rows with `digestMode: undefined, sensitivity: "high"` were exactly the population whose effective pair (per relay's `rule.digestMode ?? "realtime"`) is `(realtime, high)` — the combo PR koala73#3474 forbids. The validator only checks on write, so existing rows weren't auto-migrated. Population at run time on tacit-curlew-777: - 18 daily/all (untouched, valid) - 5 <undefined>/high (MIGRATED to daily/high) - 2 twice_daily/all (untouched, valid) - 2 <undefined>/critical (untouched, valid: effective realtime/critical) - 1 twice_daily/critical (untouched, valid) Result: 5/5 migrated, 0 failed. Post-run discovery confirms 0 in the affected set. Implementation notes: - Filter mirrors relay semantics (`rule.digestMode ?? "realtime"`); a bare `digestMode === "realtime"` filter MISSES rows with undefined digestMode — the silent third case from PR koala73#3461 review. First run of this script with the bare filter showed 0 matches; the corrected filter found 5. Captured as a discovery-time breakdown in the script so any future run shows the population shape, not just the matching subset. - Uses `setNotificationConfigForUser` (atomic pair-update from PR koala73#3461), not `setDigestSettingsForUser` (which has no `sensitivity` arg in its validator). - Dry-run by default. `--apply` required to mutate. Per-row failures logged + counted; exit 1 if any failed. - Convex deploy key auth via `npx convex run` shell-out (since `setNotificationConfigForUser` is internalMutation and unreachable from `ConvexHttpClient`; cf. skill `convex-internal-functions-unreachable-via-httpclient`). Idempotent — re-running after migration finds 0 affected and no-ops. Kept committed for audit trail (matches PR koala73#3463/koala73#3468 pattern) and as a reusable harness if a future tightening creates a similar grandfathered population. * docs(migration-script): fix stale mutation name in header (Greptile P2) The header docstring's 'Apply' usage block and 'Safety' paragraph both referenced setDigestSettingsForUser, but the actual spawnSync call (and inline comment + commit message) correctly invoke setNotificationConfigForUser. Update both header references and add a note explaining why setDigestSettingsForUser would NOT work (its validator has no sensitivity arg) so a future operator reading the header doesn't get confused.
…ertRules rows (koala73#3486) * chore(notifications): cleanup script for 7 grandfathered free-tier alertRules rows Companion to PR koala73#3483 (server-side mutation gate, layer 2) and PR koala73#3485 (relay fail-closed, layer 3). Disables notifications for the 7 free-tier users that have grandfathered alertRules rows from before the layer-2 gate existed. Mechanism: calls internal.alertRules.setAlertRulesForUser (the INTENTIONALLY-ungated operator path — see PR koala73#3483's contract test) with enabled=false. Preserves sensitivity/channels/eventTypes; only the on/off toggle flips. Affected userIds (verified via 2026-04-28 dry-run): user_3BwtmadXVR2XLnGfIUndMvCNn3S variant=full daily/high planKey=free user_3BybzNDxo0OIm1Q0wseyVPyZZI6 variant=full daily/high planKey=free user_3Bzq0tV0AqS1NGkGfAwkGiloBUW variant=full daily/all planKey=free user_3C0GSJz7gpTcqWdujingRCxxmMs variant=full daily/high planKey=free user_3C2rpobAryOk10hR2BKopVwaupC variant=full daily/all planKey=free user_3C4Y8NV7cVZlysodXrfOQufFBRE variant=full daily/all planKey=free user_3C6K7V9lEzPIH81TdSgthOmf2ZQ variant=full daily/all planKey=free DO NOT MERGE / RUN UNTIL koala73#3483 + koala73#3485 ARE DEPLOYED. Otherwise the same users could re-enable through the still-open mutation surface tomorrow. The user explicitly stated: "close it first, before doing anything to free users." Tier breakdown at time of script authoring: tier 0 (free): 7 users (25%) tier 1 (pro): 18 users tier 2 (pro+): 3 users Dry-run by default. --apply required to mutate. Per-row failures logged + counted; exit 1 on any failure. Idempotent: re-running after apply finds 0 free-tier rows in the enabled set. Audit trail kept committed (matches PR koala73#3463 / koala73#3468 / koala73#3481 pattern). * fix(disable-free-notif): preserve eventTypes+channels, fail-closed on lookup errors (Greptile P1+P1) Two P1 findings on PR koala73#3486 round 1 — both real, both fixed. P1 #1 (line 142): apply mode wiped eventTypes + channels. setAlertRulesForUser PATCHES (does NOT preserve) eventTypes and channels when they're supplied in args. The previous version sent `eventTypes: [], channels: []` — which would have wiped any user's saved subscriptions/channels, even though the script claimed to "only flip enabled". A user later upgrading to PRO would have to reconfigure from scratch. Concrete impact in the current population: user_3Bzq0tV0AqS1NGkGfAwkGiloBUW has channels=[1] (one configured channel). Old version would have wiped it. Fix: pass through `row.eventTypes ?? []` and `row.channels ?? []` from the existing row (which we already have in scope from getByEnabled). P1 #2 (line 87): silent failure on entitlement lookup. Previous version did `tier: tierMatch ? parseInt(tierMatch[1]) : -1` and then `filter(r => r.tier === 0)` — so a failed `npx convex run`, timeout, or output-format change would silently mark every user as tier=-1, get filtered out as "not free", and exit 0 with "no free users to disable." A regression in the lookup path could mask actual free users behind a green-status script run. Fix: fail-closed on three signals: - result.status !== 0 → FATAL exit code 4 - tier regex no match → FATAL exit code 4 - planKey regex no match → FATAL exit code 4 Includes the last few lines of stdout/stderr in the error so the operator can see the actual failure without re-running. Latent bug also fixed: the previous dedupe-by-userId-during-iteration would have caused multi-variant free users to only have ONE variant disabled. New version: dedupe entitlement LOOKUPS by userId (per-user data, no need to look up twice), but iterate ALL rows for the cleanup target list. Multi-variant users get every variant disabled in one pass. Dry-run on prod still shows the expected 7 rows. New diagnostic surface prints `eventTypes=[N] channels=[N]` per row so the operator sees what's preserved.
Summary
The `(realtime, all)` backfill from #3461 ran successfully — 29 forbidden rows migrated to `digestMode='daily'`, recipient list captured via the temp helper from #3463, courtesy emails sent. Removing the temp admin-secret-gated migration surface now that it's served its purpose.
What's removed (pure delete: 500 lines, 0 added)
`convex/alertRules.ts` — temp Convex functions block:
Driver scripts — `scripts/migrate-discover-realtime-all.mjs`, `scripts/migrate-realtime-all-to-daily.mjs`, `scripts/migrate-list-affected-emails.mjs`.
Test cases — the `_listAffectedUserEmailsPage (TEMP migration helper)` describe() block in `convex/tests/alertRules.test.ts`. Production-logic tests preserved unchanged.
What stays
All production-logic test coverage:
12/12 passing after trim.
Action required after merge
```bash
npx convex env remove --prod MIGRATION_ADMIN_SECRET
```
The secret value was visible in Convex dashboard function-call logs during the migration window — treat as exposed, do NOT reuse for any other admin path. Generate a fresh random value if a future migration needs the same admin-gate pattern.
Test plan