diff --git a/package-lock.json b/package-lock.json index 4b4579c..c3d1f98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,6 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-hook-form": "^7.62.0", - "redis": "^5.8.0", "tailwind-merge": "^3.3.1", "uuid": "^14.0.0", "zod": "^4.0.16" @@ -1785,66 +1784,6 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, - "node_modules/@redis/bloom": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.8.0.tgz", - "integrity": "sha512-kpKZzAAjGiGYn88Bqq6+ozxPg6kGYWRZH9vnOwGcoSCbrW14SZpZVMYMFSio8FH9ZJUdUcmT/RLGlA1W1t0UWQ==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.8.0" - } - }, - "node_modules/@redis/client": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.8.0.tgz", - "integrity": "sha512-ywZjKGoSSAECGYOd9bJpws6d4867SN686obUWT/sRmo1c/Q8V+jWyInvlqwKa0BOvTHHwYeB2WFUEvd6PADeOQ==", - "license": "MIT", - "dependencies": { - "cluster-key-slot": "1.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@redis/json": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.8.0.tgz", - "integrity": "sha512-xPBpwY6aKoRzMSu67MpwrBiSliON9bfHo9Y/pSPBjW8/KoOm1MzGqwJUO20qdjXpFoKJsDWwxIE1LHdBNzcImw==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.8.0" - } - }, - "node_modules/@redis/search": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.8.0.tgz", - "integrity": "sha512-lF9pNv9vySmirm1EzCH6YeRjhvH6lLT7tvebYHEM7WTkEQ/7kZWb4athliKESHpxzTQ36U9UbzuedSywHV6OhQ==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.8.0" - } - }, - "node_modules/@redis/time-series": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.8.0.tgz", - "integrity": "sha512-kPTlW2ACXokjQNXjCD8Pw9mHDoB94AHUlHFahyjxz9lUJUVwiva2Dgfrd2k3JxHhSBqyY2PREIj9YwIUSTSSqQ==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.8.0" - } - }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -5392,15 +5331,6 @@ "node": ">=6" } }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -8413,22 +8343,6 @@ } } }, - "node_modules/redis": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/redis/-/redis-5.8.0.tgz", - "integrity": "sha512-re0MHm1KHbiVIUPDGoUM3jldvjH5EM/wGZ3A33gyUYoC/UnVNKNnZHM5hcJVry7L2O2eJU3nflSXTliv10BTKg==", - "license": "MIT", - "dependencies": { - "@redis/bloom": "5.8.0", - "@redis/client": "5.8.0", - "@redis/json": "5.8.0", - "@redis/search": "5.8.0", - "@redis/time-series": "5.8.0" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", diff --git a/package.json b/package.json index 5fc9731..635dfab 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-hook-form": "^7.62.0", - "redis": "^5.8.0", "tailwind-merge": "^3.3.1", "uuid": "^14.0.0", "zod": "^4.0.16" diff --git a/plans/260510-0204-cleanup-upstash-migration/phase-01-cleanup.md b/plans/260510-0204-cleanup-upstash-migration/phase-01-cleanup.md new file mode 100644 index 0000000..be0fa40 --- /dev/null +++ b/plans/260510-0204-cleanup-upstash-migration/phase-01-cleanup.md @@ -0,0 +1,63 @@ +--- +phase: 1 +title: "Cleanup" +status: pending +priority: P2 +effort: "15m" +dependencies: [] +--- + +# Phase 1: Cleanup + +## Overview + +Single-pass cleanup of Upstash migration scaffolding. No code logic changes, only file/dependency removal and plan-status update. + +## Related Code Files + +- Modify: `package.json` (remove `redis` dep) — **highlight to user** per File Modification Policy +- Modify: `package-lock.json` (regenerated by `npm uninstall`) — **highlight to user** +- Delete: `scripts/migrate-upstash.js` +- Delete: `.env.migrate` (local only, gitignored) +- Update: `plans/260509-2234-upstash-db-migration-with-prefix/phase-03-cutover-and-cleanup.md` (mark complete via `ck plan check 3`) + +## Implementation Steps + +1. **User runs**: `npm uninstall redis` + - Verify: `npm ls redis` returns empty. + - This regenerates `package-lock.json`. +2. **Agent deletes**: `scripts/migrate-upstash.js`. + - Recoverable from git history (commit `53ee795`) if ever needed. + - The directory `scripts/` will be left empty; remove the directory too. +3. **Agent deletes**: local `.env.migrate`. + - Contains old DB credentials no longer needed; sensitive even if gitignored. +4. **Agent runs**: `cd plans/260509-2234-upstash-db-migration-with-prefix && ck plan check 3` to mark Phase 3 of the migration plan complete. +5. **Agent runs**: `npm run build` to confirm nothing else depends on `redis`. +6. **Agent commits + pushes** the deletion changes (single commit). + +## What stays untouched + +- Vercel env vars `REDIS_URL`, `KV_URL`, `KV_REST_API_READ_ONLY_TOKEN` — auto-provisioned by the Vercel Marketplace Upstash integration. Removing causes drift. +- `src/lib/mapillary.js` console.error lines — kept-on-purpose error observability, not temporary instrumentation. +- The migration plan files themselves (kept as record). + +## Success Criteria + +- [ ] `npm ls redis` empty +- [ ] `scripts/migrate-upstash.js` not present +- [ ] `.env.migrate` not present locally +- [ ] `npm run build` passes +- [ ] Original migration plan Phase 3 marked completed +- [ ] Single commit pushed to main + +## Risk Assessment + +- **Risk**: deleting the migration script loses the bridge if a re-migration becomes necessary. **Mitigation**: git history preserves it (commit `53ee795`). Restoration is `git checkout 53ee795 -- scripts/migrate-upstash.js`. +- **Risk**: removing `redis` dep breaks something we missed. **Mitigation**: `npm run build` step after uninstall surfaces any stray imports. Grep already confirmed no `from 'redis'` or `require('redis')` references in `src/`. +- **Risk**: `.env.migrate` deletion is irreversible. **Mitigation**: file just contains creds copy-pastable from Vercel dashboard if ever needed; no original-source data. + +## Out of Scope + +- Removing Vercel auto-provisioned env vars. +- Refactoring observability in `mapillary.js`. +- Any change to the new Upstash integration or its credentials. diff --git a/plans/260510-0204-cleanup-upstash-migration/plan.md b/plans/260510-0204-cleanup-upstash-migration/plan.md new file mode 100644 index 0000000..2ca8936 --- /dev/null +++ b/plans/260510-0204-cleanup-upstash-migration/plan.md @@ -0,0 +1,60 @@ +--- +title: "Cleanup Upstash migration artifacts" +description: "Remove leftover artifacts from the completed Upstash migration: legacy redis npm dep, one-shot migration script, local .env.migrate, and close out the original migration plan's Phase 3." +status: pending +priority: P2 +effort: 15m +created: 2026-05-10 +related: 260509-2234-upstash-db-migration-with-prefix (closes Phase 3) +blockedBy: [] +blocks: [] +--- + +# Cleanup Upstash migration artifacts + +## Goal + +The Upstash migration is complete and verified in production (15 leaderboard users, 425 sorted-set members migrated, all 4 cities returning panoramas). Old Upstash integration is disconnected from Vercel. This plan retires the migration scaffolding. + +## Current state (verified) + +| Artifact | State | +|---|---| +| `redis` npm dep | still in `package.json` ^5.8.0 (last needed by migration script) | +| `scripts/migrate-upstash.js` | committed; one-shot job done | +| `.env.migrate` | exists locally (gitignored) with old + new DB credentials | +| `OLD_STORAGE_REDIS_URL` env in Vercel | already removed by user | +| `REDIS_URL` env in Vercel | auto-provisioned by Marketplace integration; unused by app | +| `KV_URL` env in Vercel | auto-provisioned; unused | +| `KV_REST_API_READ_ONLY_TOKEN` env in Vercel | auto-provisioned; unused | +| `KV_REST_API_URL` / `KV_REST_API_TOKEN` env in Vercel | **in active use** — keep | +| Mapillary diagnostic logging | already removed (viguessr rewrite replaced it; current logs are kept-on-purpose error observability) | +| Plan `260509-2234-upstash-db-migration-with-prefix` Phase 3 | marked in-progress in tracker; needs close | + +## What this plan does + +| Action | Reason | +|---|---| +| `npm uninstall redis` | App uses `@upstash/redis` exclusively; node-redis no longer needed | +| Delete `scripts/migrate-upstash.js` | One-shot done; recoverable from git history if ever needed | +| Delete local `.env.migrate` | Contains old DB credentials no longer in use; sensitive even if gitignored | +| Mark migration plan Phase 3 complete | Operational status accuracy | + +## What this plan does NOT do + +- **Don't touch `REDIS_URL`, `KV_URL`, `KV_REST_API_READ_ONLY_TOKEN`** — they're auto-provisioned by the Vercel Marketplace Upstash integration. Removing them via CLI causes drift; integration may re-sync them. They're harmless when unused. +- Don't change the diagnostic-but-still-useful console.error lines in `src/lib/mapillary.js` — those provide error observability for the Mapillary 500s and aren't temporary instrumentation. + +## Phases + +| # | Phase | Effort | Status | +|---|---|---|---| +| 01 | [Cleanup](./phase-01-cleanup.md) | 15m | pending | + +## Success Criteria + +- `npm ls redis` returns nothing +- `scripts/` directory empty (or removed entirely) +- `.env.migrate` not present locally +- `npm run build` passes +- Original migration plan reports Phase 3 completed diff --git a/scripts/migrate-upstash.js b/scripts/migrate-upstash.js deleted file mode 100644 index 0caf6e3..0000000 --- a/scripts/migrate-upstash.js +++ /dev/null @@ -1,121 +0,0 @@ -// One-shot migration: copy leaderboard sorted-set data from the OLD Redis DB -// to the NEW Upstash DB, prepending KEY_PREFIX (default 'vngeoguessr:') on -// write. The two DBs use different protocols on Vercel: -// -// OLD (source) → TCP protocol via OLD_STORAGE_REDIS_URL (node-redis) -// NEW (destination) → REST API via KV_REST_API_URL/_TOKEN (@upstash/redis) -// -// Sessions are NOT migrated (30-min TTL, ephemeral). -// -// Usage: -// 1. Pull env into .env.migrate: vercel env pull .env.migrate -// (gives you OLD_STORAGE_REDIS_URL, KV_REST_API_URL, KV_REST_API_TOKEN) -// 2. Optionally set KEY_PREFIX in .env.migrate (defaults to 'vngeoguessr:') -// 3. Dry run: node --env-file=.env.migrate scripts/migrate-upstash.js --dry-run -// 4. Live: node --env-file=.env.migrate scripts/migrate-upstash.js - -import { createClient } from 'redis'; -import { Redis } from '@upstash/redis'; - -const DEFAULT_KEY_PREFIX = 'vngeoguessr:'; -const SCAN_PATTERNS = ['leaderboard:*', 'distance:*']; - -const dryRun = process.argv.includes('--dry-run'); -const PREFIX = process.env.KEY_PREFIX ?? DEFAULT_KEY_PREFIX; - -function requireEnv(name) { - const v = process.env[name]; - if (!v) { - console.error(`Missing required env var: ${name}`); - process.exit(1); - } - return v; -} - -const oldUrl = requireEnv('OLD_STORAGE_REDIS_URL'); -const newRestUrl = process.env.UPSTASH_REDIS_REST_URL ?? requireEnv('KV_REST_API_URL'); -const newRestToken = process.env.UPSTASH_REDIS_REST_TOKEN ?? requireEnv('KV_REST_API_TOKEN'); - -const newClient = new Redis({ url: newRestUrl, token: newRestToken }); - -console.log('Upstash leaderboard migration'); -console.log(` source: OLD_STORAGE_REDIS_URL (TCP / node-redis)`); -console.log(` dest: ${newRestUrl}`); -console.log(` prefix: ${PREFIX} (must match Vercel runtime KEY_PREFIX)`); -console.log(` mode: ${dryRun ? 'DRY RUN (no writes)' : 'LIVE'}`); -console.log(''); - -// Normalize NEW DB SDK zrange withScores response into [{ score, member }, ...]. -// The migration script also uses this shape for ZADD args. -function reshapeWithScores(raw) { - if (!Array.isArray(raw) || raw.length === 0) return []; - if (typeof raw[0] === 'object' && raw[0] !== null && 'member' in raw[0]) { - return raw.map((entry) => ({ score: Number(entry.score), member: entry.member })); - } - const out = []; - for (let i = 0; i < raw.length; i += 2) { - out.push({ member: raw[i], score: Number(raw[i + 1]) }); - } - return out; -} - -async function main() { - const oldClient = await createClient({ url: oldUrl }).connect(); - - // Discover source keys via SCAN — node-redis exposes scanIterator. - const sourceKeys = new Set(); - for (const pattern of SCAN_PATTERNS) { - for await (const key of oldClient.scanIterator({ MATCH: pattern, COUNT: 200 })) { - // node-redis v5 yields strings (or arrays of strings depending on version). - if (Array.isArray(key)) key.forEach((k) => sourceKeys.add(k)); - else sourceKeys.add(key); - } - } - - const sortedKeys = Array.from(sourceKeys).sort(); - console.log(`Discovered ${sortedKeys.length} source keys:`); - for (const k of sortedKeys) console.log(` - ${k}`); - console.log(''); - - let totalMembers = 0; - let migratedKeys = 0; - - for (const key of sortedKeys) { - // node-redis v5 returns [{ value, score }, ...] for zRangeWithScores. - const raw = await oldClient.zRangeWithScores(key, 0, -1); - if (!raw || raw.length === 0) { - console.log(` [skip] ${key} (empty)`); - continue; - } - const entries = raw.map((e) => ({ score: Number(e.score), member: e.value })); - - const physicalKey = `${PREFIX}${key}`; - console.log(` [${dryRun ? 'dry' : 'write'}] ${physicalKey} (${entries.length} members)`); - totalMembers += entries.length; - migratedKeys += 1; - - if (dryRun) continue; - - // ZADD against NEW DB via REST. Chunk to keep request sane. - const CHUNK = 500; - for (let i = 0; i < entries.length; i += CHUNK) { - const chunk = entries.slice(i, i + CHUNK); - await newClient.zadd(physicalKey, ...chunk); - } - } - - await oldClient.quit(); - - console.log(''); - console.log('Summary:'); - console.log(` source keys discovered: ${sortedKeys.length}`); - console.log(` keys migrated: ${migratedKeys}`); - console.log(` members copied: ${totalMembers}`); - console.log(` prefix applied: ${PREFIX}`); - if (dryRun) console.log(' (dry run — no writes performed)'); -} - -main().catch((err) => { - console.error('Migration failed:', err); - process.exit(1); -});