Repository navigation
feat: harden feedback→Notion sync (central DB, outbox, reconciler, screenshots, after()) - #137
Merged
Merged
Conversation
…reenshots, after()) Rework the in-app feedback → Notion mirror onto the shared central "App Feedback" database with an outbox + reconciler so submissions are never silently lost. - schema/migration: add Feedback.notionPageId/notionSyncedAt/notionSyncAttempts/ notionLastError outbox columns + partial unsynced index; backfill notionSyncedAt for rows that already have a notionUrl. - notion sync: rewrite for the central schema; extract pure, env-free buildFeedbackNotionPayload (BUG→Bug, IDEA→Request, OTHER→Feedback) with unit tests. - submit: move the Notion create out of the request path via next/server after(); record attempts/last-error on failure instead of blocking the user. - reconciler: lib/notion/feedback-reconcile.ts queries Notion by App Row ID before creating (idempotent), backfills existing pages, caps attempts at 5; exposed at CRON_SECRET-guarded /api/cron/reconcile-feedback, wired into scripts/cron.mjs (30m). - screenshots: public GET /api/feedback/screenshots/[id]/[index] with a raster-only mime allowlist, nosniff + locked-down CSP, so Notion can fetch external image URLs. - rate limit: give IP-less (anon) clients a larger shared 30/5min bucket instead of starving a single 10/5min one. - docs: add FEEDBACK.md (canonical contract + HessFest per-app specifics).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Reworks the in-app feedback → Notion mirror onto the shared central "📥 App Feedback" database, with a durable outbox + reconciler so a submission is never silently lost, and moves the Notion write out of the request path.
20260703000000_feedback_notion_outbox): addsnotionPageId,notionSyncedAt,notionSyncAttempts,notionLastErrortoFeedback; partial index on unsynced rows; backfillsnotionSyncedAt = createdAtfor rows that already carry anotionUrl. Not applied to any live DB here.lib/notion/feedback-payload.ts(env-free, unit-tested), per the repo's pure-core convention (rate-limit-core.ts). Central schema:Title({emoji} {title}),App=HessFest,Type(BUG→Bug, IDEA→Request, OTHER→Feedback),Status=New,Environment,App Row ID, conditionalPage URL/User Email/Browser/Screenshot.lib/notion/feedback-sync.tsrewritten for the central schema; returns{ pageId, url };TIMEOUT_MS8s (out of request path); never throws.lib/feedback/submit.ts— Notion create now runs vianext/serverafter(); success writesnotionPageId/notionUrl/notionSyncedAt, failure recordsnotionSyncAttempts+notionLastError. Submission no longer waits on Notion.lib/notion/feedback-reconcile.ts— scans unsynced, under-cap, aged rows; queries Notion byApp Row IDbefore creating (idempotent — backfills an existing page instead of duplicating); caps attempts at 5. Exposed atPOST /api/cron/reconcile-feedback(CRON_SECRET-guarded, same guard aspoll-scores), wired intoscripts/cron.mjsat a 30-min cadence.GET /api/feedback/screenshots/[id]/[index]— raster-only mime allowlist (png/jpeg/webp, no SVG),X-Content-Type-Options: nosniff,Content-Security-Policy: default-src 'none'; sandbox,Content-Disposition: inline,Cache-Control: private, max-age=3600; 404 on missing row/index/parse. Public so Notion's servers can fetch external file URLs.app/api/feedback/route.ts) — IP-less (anon) clients now share one largerfeedback:anon-globalbucket at 30/5min instead of starving a single 10/5min bucket; resolvable IPs keep 10/5min; signed-in users keep 10/5min.FEEDBACK.md(canonical contract + HessFest per-app specifics).Test plan
npx vitest run— 733 passed (94 files), incl. newfeedback-payload.test.ts(13) +feedback-reconcile.test.ts(7, mocked prisma/fetch/env: create→writeback, query-hit backfill/no-dupe, attempts-cap query shape, query-failure no-create, screenshot-URL build, idempotent double-run).npm run typecheck— clean.npm run build— compiled successfully;/api/cron/reconcile-feedbackand/api/feedback/screenshots/[id]/[index]registered.Cutover TODO (NOT done here — supervised)
NOTION_FEEDBACK_DB_IDon Railway toc153f1e7-b65d-49d2-a2d8-5e57d1f22b70(central DB) and confirmNOTION_API_KEY's integration is shared with it.20260703000000_feedback_notion_outboxmigration to prod (prisma migrate deploy).Notes
.env.examplecould not be edited (dotfile-protection hook blocks read+write). The two vars (NOTION_API_KEY,NOTION_FEEDBACK_DB_ID) are already declared inlib/env.tswith a comment and are documented inFEEDBACK.md.🤖 Generated with Claude Code
https://claude.ai/code/session_015dvV6WQgHXA7rPCYT7mtZn