Skip to content

PRD: sync member profile repo selection with public ผลงาน #178

Description

@xenodeve

EN

Problem Statement

Member (or admin) deselection of a GitHub repo from a member profile only flips member_projects.selected. The public portfolio (/projects) is a separate projects table filled by a one-way admin import. After a repo is taken off a profile, visitors can still see it under ผลงาน — the two surfaces disagree, which breaks the expectation that “what I show on my profile” and “what the agency showcases for me” stay aligned.

Solution

Keep profile selection and the public showcase in sync for member-promoted personal GitHub projects: deselecting a repo removes it from ผลงาน; selecting it again restores it when a matching showcase row already exists. Org/team imports (Slow-Inc) are unaffected. After a toggle, public pages refresh so the change is visible without waiting for a redeploy (revalidation — not a live websocket requirement).

User Stories

  1. As a member, I want a repo I remove from my profile to disappear from ผลงาน, so that visitors do not see work I no longer endorse on my page.
  2. As a member, I want a repo I re-select on my profile to return to ผลงาน if it was previously imported, so that I do not need an admin to re-import every time.
  3. As an admin editing a member, I want the same select/deselect toggle to drive showcase sync, so that admin and member intent share one rule.
  4. As a visitor on /team/[slug], I want only selected repos listed, so that the profile stays curated.
  5. As a visitor on /projects, I want not to see personal projects that the owning member has deselected, so that the portfolio matches current member intent.
  6. As a visitor on /projects/[slug], I want a deselected personal project to 404 (or otherwise not be publicly listed), so that deep links do not keep promoting withdrawn work.
  7. As an admin, I want Slow-Inc / team-owned showcase projects to stay published when any individual member toggles selection, so that org portfolio is not accidentally emptied by one person.
  8. As an admin, I want sync to match projects by GitHub identity (owner/repo or html URL), so that slug renames do not orphan the link.
  9. As an operator, I want deselect to unpublish (not hard-delete) the showcase row, so that enrichment (category, content, snapshot) is not destroyed and can return on re-select.
  10. As an operator, I want re-select of a previously unpublished row to set it published again with published_at restored or kept, so that rank/history are not wiped needlessly.
  11. As a developer, I want a pure plan function for select/deselect → publish/unpublish/noop, so that rules are unit-tested without the database.
  12. As a developer, I want the toggle write path to update selected and apply the showcase plan in one intentional flow, so that profile and portfolio cannot diverge mid-request.
  13. As a developer, I want failed showcase writes to surface an error and not leave selected flipped alone without feedback, so that operators can retry.
  14. As a security reviewer, I want only authorized actors (member-own or admin per existing RLS/flat-authz) to toggle, so that strangers cannot unpublish others’ work.
  15. As a security reviewer, I want unpublish limited to owner_type = personal rows tied to that member’s GitHub identity, so that team projects cannot be unpublished via this path.
  16. As an admin on /admin/projects/from-members, I want import-all to remain the way net-new repos enter projects, so that sync does not invent showcase rows on first select without the existing import gate (or document if first-select auto-imports — decided: no auto-create on first select; only sync existing rows).
  17. As a stakeholder, I want ISR/public cache revalidated after a successful sync, so that /projects updates promptly.
  18. As a QA engineer, I want E2E covering deselect → profile hide + portfolio hide, so that the dual-surface contract cannot regress.
  19. As a QA engineer, I want browser smoke on an affected slug after toggle, so that Playwright alone is not the only FE gate.
  20. As a maintainer, I want bilingual GitHub evidence for the epic and slices, so that AFK agents can continue.
  21. As an operator, I want a one-shot reconcile (optional ops slice) that unpublishes personal showcase rows whose matching member_projects.selected is false, so that historical drift from before this feature is cleaned.
  22. As a member, I want the UI copy on the selector to mention that toggling also affects ผลงาน, so that the new behavior is not surprising.
  23. As an admin, I want MangaDock and other team imports untouched by member deselect storms, so that the agency flagship stays stable.
  24. As a developer, I want human-owned showcase editorial fields preserved across unpublish/republish, so that sync only changes publication status.
  25. As a visitor using filters on /projects, I want deselected personal work excluded from counts and filter facets after revalidation, so that empty shells do not linger in filters.
  26. As an operator, I want cron/member-sync that sets selected=false on new rows to continue as today, so that automatic sync does not suddenly publish everything.

Implementation Decisions

  • Two surfaces, one rule for personal promoted work: member_projects.selected remains the source of truth for profile display; matching personal projects rows follow publish/unpublish.
  • Match key: GitHub identity — prefer (gh_owner, gh_repo) case-insensitive, fallback gh_html_url normalized; never match on title alone.
  • Scope guard: only projects.source = 'github' AND owner_type = 'personal' AND identity belongs to the member whose row was toggled. Never touch owner_type = 'team' / Slow-Inc org imports.
  • Unpublish = status change (e.g. to a non-public status already used by CMS — prefer existing draft/hidden vocabulary in schema; do not hard-delete). Re-select sets back to published.
  • No auto-create on first select: if no projects row exists yet, select only shows on profile; admin import (from-members) remains the on-ramp to ผลงาน. Document in UI.
  • Write path: extend the existing project-selection toggle (admin member edit today) to call a server-side sync after updating selected (prefer server action over browser-only Supabase update so secrets/revalidate and authz stay consistent). If member self-service later gains the same selector, reuse the same sync helper.
  • Revalidation: after successful status change, revalidate projects list/detail (existing revalidate helpers).
  • ADR note: ADR 0011 separates “which repos are chosen” from auto-publish of generated copy — this epic tightens the choice layer so profile choice also gates public listing for personal imports; it does not reopen the generation publish-gate. Consider a short additive ADR or ledger note if the product rule is considered hard-to-reverse.
  • Optional reconcile job: secret-guarded or admin-only one-shot to align historical drift (unpublish personal rows with selected=false).

Testing Decisions

  • Good tests assert external behavior: given fixtures of member repo + projects rows, deselect plans unpublish; team rows plan noop; re-select plans republish; missing project plans noop.
  • Prefer one pure planning seam + the toggle/sync public interface (server action or service) + Playwright for visitor-visible hide.
  • Prior art: member-repo-import unit tests, admin project actions, dual FE gate (suite + browser smoke).
  • Do not assert private SQL strings or mock internal RLS implementation details.

Out of Scope

  • Supabase Realtime websocket subscriptions on the public site.
  • Auto-importing a brand-new select into projects without admin from-members.
  • Removing or cascading Slow-Inc org imports when members toggle.
  • Hard-deleting enriched project rows.
  • Changing AI enrichment / ranking algorithms.
  • Member self-service project selector UI if it does not exist yet (reuse sync when added later).

Further Notes

Proposed test seams (confirm)

  1. Pure showcase-sync planner (select/deselect + member identity + candidate projects → unpublish | republish | noop) — highest seam for rules.
  2. Authenticated toggle+sync write (selected flip + planned status change + revalidate) — one integration seam.
  3. Public FE — Playwright + browser smoke: after deselect, slug absent from /projects and from profile; team projects unaffected.

TH

ปัญหา

การเอา repo ออกจากโปรไฟล์ member แค่สลับ selected แต่หน้าผลงาน (/projects) ยังโชว์โปรเจกต์ที่ import ไว้แล้ว ทำให้สองพื้นผิวไม่ตรงกัน

ทางแก้

ซิงก์เฉพาะผลงานส่วนตัวที่เคย promote แล้ว: deselect → unpublish จากผลงาน, select ใหม่ → publish กลับถ้ามีแถวอยู่แล้ว — ไม่แตะของ Slow-Inc/ทีม, ไม่ hard-delete, ไม่สร้างแถวใหม่ตอน select ครั้งแรก (ยังใช้ import แอดมิน)

นอกขอบเขต

Realtime websocket · auto-import ครั้งแรก · ลบ org import · hard-delete

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestepicTracks a multi-issue featureready-for-agentFully specified, ready for an AFK agent

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions