Skip to content

feat(seed): seed-data tagging & wipe infrastructure (Pages reference) - #42

Merged
danielheene merged 13 commits into
developfrom
seed-data-tagging
Aug 20, 2026
Merged

feat(seed): seed-data tagging & wipe infrastructure (Pages reference)#42
danielheene merged 13 commits into
developfrom
seed-data-tagging

Conversation

@danielheene

Copy link
Copy Markdown
Owner

Summary

Adds a general-purpose seed-data tagging mechanism, plus a fully-built reference implementation for the Pages collection: create/wipe fixture data from the CLI or the admin panel, tagged so it can coexist with real content and be found/removed independently.

  • Extends the existing GeneratorFlagsField with a seeded-dummy flag, rolled out to 12 collections (all except Users; Media collections already had the field).
  • Shared, transport-agnostic seedPages/cleanPages logic (src/lib/seed/pages.ts) — the single source of truth for both callers below. cleanPages cascade-deletes the seeded media a page's hero referenced, gated by a live per-document flag re-check so a page that got its hero manually pointed at a real image never has that image deleted.
  • CLI: pnpm seed:pages [-- --count N] / pnpm seed:pages:clean, synchronous, calls the shared logic directly.
  • Admin panel: a "⋯" list-view action on Pages — "Seed…" opens a count-input modal, "Clean" opens a confirmation — both run async via Payload's jobs queue with live progress streamed over SSE and rendered as toasts (mirrors the existing TranslateControls.tsx/bilingualTranslateChannel pattern).

Design doc and implementation plan are local-only (docs/ is gitignored in this repo) — happy to share the content directly if useful for review.

Scope

This PR builds infrastructure + the Pages reference implementation only. The other 11 seedable collections (BlogTopics, 6 Resume collections) are explicit non-goals here, to follow later using Pages as the template.

Process notes

Built via subagent-driven development: fresh implementer + reviewer per task, human-escalated one design decision (a spec-mandated safety check in cleanPages that a plan-authoring bug had accidentally undermined — restored per your call), then a final whole-branch review (opus) that caught three cross-cutting issues no single task's review could see:

  • cleanPages/seedPages didn't account for Pages' trash: true — a soft-deleted seeded page was invisible to Clean, permanently orphaning its media, and broke re-Seed's idempotency check. Fixed.
  • A real (if narrow) SSE timing race: a very fast cleanPages run could complete before the client's SSE subscription opened, leaving a stuck toast and a stale list view. Fixed with a client-side timeout fallback.
  • The admin-panel modal referenced CSS classes with no stylesheet defining them (would render unstyled) — never caught by typecheck, only by knowing to check for it. Added the missing stylesheet.

All three fixes independently re-reviewed and confirmed resolved. 6 Minor findings were logged but intentionally left unfixed (no auth check on the destructive server action — confirmed pre-existing pattern across every server action in this repo, not a regression; a few small robustness/consistency nits) — noted below as candidates for follow-up.

Verification

  • pnpm typecheck — 0 errors
  • pnpm vitest run — 617/617 passing (+12 new tests)
  • pnpm build — succeeds
  • CLI seed→idempotency-check→clean cycle run three times against a real dev database (Tasks 4, 9, and post-fix), each time with exact expected counts and independently-verified DB state before/after

Follow-ups (not blocking, not in this PR)

  • No auth check on enqueueSeedCollection (matches every other server action in src/lib/actions/ today — a repo-wide gap, not new here, but worth a dedicated pass since this one action is destructive)
  • seedCollection.ts dispatches on a bare 'pages' string rather than the CollectionSlug enum — fine for one entry, worth a lookup map once a second collection lands
  • CLI --count doesn't validate a non-numeric/out-of-range value (silently seeds 0)
  • A mid-loop seedPages failure can orphan an already-created hero image (tagged, so still manually findable)

🤖 Generated with Claude Code

danielheene and others added 13 commits August 20, 2026 09:12
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
…ages

Pages is configured with trash: true, but Payload's local API find/delete
default trash: false, which excludes/skips soft-deleted documents. This
meant:

- seedPages's idempotency find couldn't see a trashed seeded page, so
  re-running Seed would try to create a page with the same slug and collide
  with the still-present (trashed) row's unique constraint.
- cleanPages's find for seeded-dummy-flagged pages skipped trashed pages,
  never collecting their hero media ids, permanently orphaning that media.
- cleanPages's delete call for pages needed trash: true to permanently
  delete rather than no-op on an already-trashed page.

MediaImages calls are left unchanged; that collection has no trash config.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
SeedActions.tsx opens its SSE subscription only after enqueueSeedCollection
resolves and triggers a re-render. The job itself is triggered via
after(() => payload.jobs.runByID(...)) and can start running immediately,
so a fast cleanPages run (few/zero pages) can publish its terminal
success/error message before the client has subscribed. Redis pub/sub has
no replay, so that message is lost: the toast stays stuck on "Queued…"
forever and router.refresh() never fires.

Add a client-side fallback: after enqueueing, arm a 5s timeout that, if no
terminal SSE message clears jobId first, performs the same cleanup a
success message would (toast, clear jobId, router.refresh()). The timeout
is cleared whenever a real terminal message arrives, so it never fires in
the normal case.

Also add the SeedActions modal stylesheet: SeedActions.tsx rendered
seed-actions-modal / __body / __footer classes with no stylesheet defining
them anywhere in the repo. Added SeedActions.styles.css following the
co-located .styles.css convention (see MediaScopeTabs.styles.css) and the
Payload theme custom properties already used under AdminPanel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
@danielheene
danielheene marked this pull request as ready for review August 20, 2026 08:53
@danielheene
danielheene merged commit b44d3d4 into develop Aug 20, 2026
5 checks passed
@danielheene
danielheene deleted the seed-data-tagging branch August 20, 2026 08:57
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