Skip to content

chore(notifications): remove temp migration surface (post-PR-3461 cleanup)#3468

Merged
koala73 merged 1 commit into
mainfrom
notifications-temp-migration-cleanup
Apr 27, 2026
Merged

chore(notifications): remove temp migration surface (post-PR-3461 cleanup)#3468
koala73 merged 1 commit into
mainfrom
notifications-temp-migration-cleanup

Conversation

@koala73

@koala73 koala73 commented Apr 27, 2026

Copy link
Copy Markdown
Owner

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:

  • `_countRealtimeAllRules` (paginated discovery query)
  • `_migrateRealtimeAllPage` (paginated migration mutation)
  • `_listAffectedUserEmailsPage` (paginated recipient-list query)
  • `assertMigrationAdmin` helper
  • `TEMP MIGRATION FUNCTIONS` comment 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:

  • Cross-field invariant enforcement (`(realtime, all)` rejected at every writer)
  • Insert-only default flip (`'high'` on fresh insert, preserves on patch)
  • Atomic-mutation pair flips via `setNotificationConfigForUser`
  • Partial-update re-validation
  • Omitted-sensitivity preservation (the patch-vs-insert subtlety)

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

  • `npx vitest run convex/tests/alertRules.test.ts` — 12/12 passing post-trim
  • `npm run typecheck` + `npm run typecheck:api` — both clean
  • `npx biome lint` on changed files — clean
  • No remaining references to removed functions: `grep -rn '_countRealtimeAllRules\|_migrateRealtimeAllPage\|_listAffectedUserEmailsPage\|assertMigrationAdmin\|migrate-discover-realtime-all\|migrate-realtime-all-to-daily\|migrate-list-affected-emails' .` returns nothing in tree
  • Post-merge: run `npx convex env remove --prod MIGRATION_ADMIN_SECRET`. Verify with `npx convex env list --prod | grep MIGRATION` (expected: no match).

…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.
@vercel

vercel Bot commented Apr 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment Apr 27, 2026 3:07pm

Request Review

@greptile-apps

greptile-apps Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This is a pure cleanup PR (500 lines deleted, 0 added) removing temporary admin-gated migration surfaces that were introduced in #3461/#3463 to backfill digestMode='daily' for forbidden (realtime, all) alertRules rows. The migration has completed successfully, and this PR removes the three Convex functions, their three driver scripts, and their associated test block. All production-logic tests (12/12) are preserved unchanged.

Confidence Score: 5/5

Safe 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 (ConvexError is still used by production assertCompatibleDeliveryMode), or tests removed for production paths. The one remaining action item (removing the env var) is a post-merge manual step that is clearly documented in the PR description.

No files require special attention; the only remaining action is the post-merge npx convex env remove --prod MIGRATION_ADMIN_SECRET step noted in the PR description.

Important Files Changed

Filename Overview
convex/alertRules.ts Removes the TEMP MIGRATION FUNCTIONS block (_countRealtimeAllRules, _migrateRealtimeAllPage, _listAffectedUserEmailsPage, assertMigrationAdmin); all remaining production code and imports are intact.
convex/tests/alertRules.test.ts Removes the _listAffectedUserEmailsPage (TEMP migration helper) describe block (4 tests); all 12 production-logic test cases are preserved and unmodified.
scripts/migrate-discover-realtime-all.mjs File deleted — temporary discovery driver that paginated _countRealtimeAllRules; no longer needed post-migration.
scripts/migrate-realtime-all-to-daily.mjs File deleted — temporary migration driver that called _migrateRealtimeAllPage; no longer needed post-migration.
scripts/migrate-list-affected-emails.mjs File deleted — temporary recipient-list driver that called _listAffectedUserEmailsPage; no longer needed post-migration.

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]
Loading

Reviews (1): Last reviewed commit: "chore(notifications): remove temp migrat..." | Re-trigger Greptile

@koala73
koala73 merged commit bd84883 into main Apr 27, 2026
12 checks passed
@koala73
koala73 deleted the notifications-temp-migration-cleanup branch April 27, 2026 16:08
koala73 added a commit that referenced this pull request Apr 28, 2026
…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.
koala73 added a commit that referenced this pull request Apr 28, 2026
…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.
fuleinist pushed a commit to fuleinist/worldmonitor that referenced this pull request May 9, 2026
…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.
fuleinist pushed a commit to fuleinist/worldmonitor that referenced this pull request May 9, 2026
…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.
fuleinist pushed a commit to fuleinist/worldmonitor that referenced this pull request May 9, 2026
…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.
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