Previously, saveGroups() used DELETE FROM d1_groups followed by re-insertion,
which triggered ON DELETE CASCADE and wiped all routes whenever any group was updated.
Changes:
- Use INSERT...ON CONFLICT DO UPDATE (upsert) instead of delete-then-insert
- Only delete groups that are actually being removed
- Delete routes first, then delete groups (proper cascade order)
- Load existing group IDs before saving to detect deletions
This fixes the critical bug where updating one group's metadata would
delete all routes across all groups.
Data recovery: Used D1 Time Travel to restore from bookmark
000018f9-00000002-000050de-f23b7bfe5ccdc00e9c9dec3a4de8bc81 (before
the problematic group update), recovering 18 routes.
- Move oversized queue payloads from KV to R2 (PAYLOAD binding, webhooks/YYYY/MM/DD/*.json, KV queue:payload:* fallback)
- Persist routes/groups to D1 (d1_routes/d1_groups) with memory -> KV -> D1 three-tier cache, seeded from legacy KV config keys
- Move webhook dedup (dedup_keys), delivery state (delivery_state) and message tracking (message_tracking) to D1 via canUseD1 probe with automatic KV fallback
- Batch send_logs inserts (recordSendBatch) and add group_id/ts index
- Add storage-prune scheduled task for expired dedup/state/tracking rows
- Add TTL to invite:group:{id} index and audit all ephemeral KV keys
- Add D1 indexes for the new tables
- Sync AGENTS.md, README.md/zh and docs/ (en/zh) with the new storage layout