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
- 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.
- 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.
- 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.
- As a visitor on
/team/[slug], I want only selected repos listed, so that the profile stays curated.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- As a developer, I want a pure plan function for select/deselect → publish/unpublish/noop, so that rules are unit-tested without the database.
- 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.
- 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.
- 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.
- 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.
- 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).
- As a stakeholder, I want ISR/public cache revalidated after a successful sync, so that
/projects updates promptly.
- As a QA engineer, I want E2E covering deselect → profile hide + portfolio hide, so that the dual-surface contract cannot regress.
- As a QA engineer, I want browser smoke on an affected slug after toggle, so that Playwright alone is not the only FE gate.
- As a maintainer, I want bilingual GitHub evidence for the epic and slices, so that AFK agents can continue.
- 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.
- 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.
- As an admin, I want MangaDock and other team imports untouched by member deselect storms, so that the agency flagship stays stable.
- As a developer, I want human-owned showcase editorial fields preserved across unpublish/republish, so that sync only changes publication status.
- 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.
- 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)
- Pure showcase-sync planner (select/deselect + member identity + candidate projects → unpublish | republish | noop) — highest seam for rules.
- Authenticated toggle+sync write (selected flip + planned status change + revalidate) — one integration seam.
- 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
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 separateprojectstable 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
/team/[slug], I want only selected repos listed, so that the profile stays curated./projects, I want not to see personal projects that the owning member has deselected, so that the portfolio matches current member intent./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.published_atrestored or kept, so that rank/history are not wiped needlessly.selectedand apply the showcase plan in one intentional flow, so that profile and portfolio cannot diverge mid-request.selectedflipped alone without feedback, so that operators can retry.owner_type = personalrows tied to that member’s GitHub identity, so that team projects cannot be unpublished via this path./admin/projects/from-members, I want import-all to remain the way net-new repos enterprojects, 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)./projectsupdates promptly.member_projects.selectedis false, so that historical drift from before this feature is cleaned./projects, I want deselected personal work excluded from counts and filter facets after revalidation, so that empty shells do not linger in filters.selected=falseon new rows to continue as today, so that automatic sync does not suddenly publish everything.Implementation Decisions
member_projects.selectedremains the source of truth for profile display; matching personalprojectsrows follow publish/unpublish.(gh_owner, gh_repo)case-insensitive, fallbackgh_html_urlnormalized; never match on title alone.projects.source = 'github'ANDowner_type = 'personal'AND identity belongs to the member whose row was toggled. Never touchowner_type = 'team'/ Slow-Inc org imports.draft/hiddenvocabulary in schema; do not hard-delete). Re-select sets back topublished.projectsrow exists yet, select only shows on profile; admin import (from-members) remains the on-ramp to ผลงาน. Document in UI.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.selected=false).Testing Decisions
member-repo-importunit tests, admin project actions, dual FE gate (suite + browser smoke).Out of Scope
projectswithout adminfrom-members.Further Notes
from-membersimport; ADR 0005 / 0011.Proposed test seams (confirm)
/projectsand 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