Found and fixed multiple critical bugs with similar patterns:
1. routeStatements() used DELETE FROM d1_routes before re-inserting
- Could cause data loss if interrupted or if logic changes
- Now uses DELETE with WHERE clause + INSERT...ON CONFLICT (upsert)
2. saveFragments() used DELETE FROM d1_fragments before re-inserting
- Same pattern as routes, fixed with targeted deletes + upsert
- Added loadFragments() call to determine what to delete
3. adminGroupRename() didn't update fragment groupId
- When renaming a group, fragments were left pointing to old groupId
- Now updates fragments alongside routes, secrets, and invites
All changes follow the same safe pattern:
- Load existing data to identify what needs deletion
- Delete only removed items with WHERE clauses
- Use INSERT...ON CONFLICT DO UPDATE for upserts
- Never use bare DELETE FROM table
Testing: All 339 tests pass.
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.
Providers that emit a fresh check run id per phase (e.g. Cloudflare Pages)
no longer post a new message for each status change; same name + commit
SHA now edits the existing message. Falls back to the run id when name or
head_sha is absent. Docs updated in sync.
check_suite payloads carry no details_url; the build log lives on the
underlying check_run. For failed suites, fetch the suite's check runs with
an installation token and inject the first run's details_url (preferring
the Cloudflare run) as a Build Log field.
The v4 bump moved the PostCSS plugin to @tailwindcss/postcss, breaking
the production build while the project still uses a v3-style
tailwind.config.ts and @apply components.
- add feishu_links D1 table and link store helpers
- implement X-Lark-Signature verification, url_verification, /gh login|logout|comment|merge|close and card.action.trigger Merge/Close
- render interactive cards with clickable title link, inline links and callback buttons (no whole-card card_link)
- bind Feishu account in OAuth callback
- document event subscription and required scopes
Add a field filter type reading any payload value by JSONPath with array expansion, 12 comparison operators (eq/ne/contains/startsWith/endsWith/regex/gt/gte/lt/lte/in/exists), a visual AST builder (all/any/not) in the route editor, chip-based multi-value input, a stateless POST /admin/api/test-match dry-run, and named filter fragments stored in D1 (d1_fragments, migration 0010) inlined into route ASTs on insert.
- dispatch: attach per-send detail (title/url/description/match/provider)
so the admin log view shows message context, not just a bare ok flag
- idempotency: compare dedup expiry against claimed_at instead of the new
expires_at so non-expired duplicates are rejected and expired keys re-claim
- 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
Add LICENSE (MIT), an IdempotencyStore abstraction with a KV implementation and provider-scoped delivery keys, optional replay protection for custom webhooks (X-WebHooker-Timestamp + X-WebHooker-Nonce), and per-request correlation ids in webhook responses and logs.