ธีม
STATE archive — 2026-07-25 — PR ฝ่าย source-of-truth + VitalSound service desk
Pruned out of STATE.md to keep it near the ~200-line budget. Both areas are SHIPPED, DEPLOYED and stable — nothing here is in flight. Kept verbatim because the VS section documents load-bearing confidentiality invariants (submitter column allow-lists, the duplicate-id capability rule) that a future change in that area must not re-derive.
Chronology: git log --oneline. Architecture/RLS: docs/CONTEXT.md. Bug post-mortems: .claude/rules/mistakes.md.
PR — ฝ่าย list is now one source of truth (src/js/pr-depts.js)
- นายกสโม added to the PR submit form (ข้อมูลผู้ส่งงาน → ฝ่าย) and the admin staff dept filter. Decision: NO special-casing — it behaves exactly like any other ฝ่าย (all three ช่องทางการโพสต์ options, not project mode). Verified in a real browser against
npm run preview. - Same pass: fixed the long-standing typo
ฝ่ายคุณภาพขีวิต…→ชีวิตand added the missingฝ่ายรังสีเทคนิค. 8 livepr_ticketsrows carry the old spelling; nothing rewrites the DB —canonicalPrDept()aliases it at the row → view-model boundary inpr-staff.js/pr-tracking.js, so those tickets stay findable under the corrected filter option and display the corrected name. - Why the module: the list was hand-written twice (
tab-pr.html,tab-admin.html) and had drifted — same typo in both,ฝ่ายรังสีเทคนิคin neither,โครงการอื่นๆin only the admin one. Both selects are now filled fromPR_DEPARTMENTSviafillPrDeptSelect()(which preserves the current filter across a refill).docs/CONTEXT.md"When you add a new department or role" rewritten to match.
VITALSOUND — service-desk system (all DEPLOYED + migrations APPLIED through 0080)
VS = confidential service desk + curated public "Problem" board. 9 internal statuses = source of truth; students see a 4-phase stepper. This session shipped migrations 0073–0078 (all applied to live DB) + the UI slices:
- 0073 resolution-on-close: closing (เสร็จสิ้น) requires a reason (fixed/forwarded/wont_do+note;
MANUAL_VS_RESOLUTIONS); student sees a "ผลการดำเนินการ" outcome card. Shared vocabsrc/js/vs-resolution.js. - 0074 duplicate = linked progress-mirror: merge links B→A; trigger mirrors A's status (+resolution enum on close, never the note) onto open duplicates; submitter reads use
SUBMITTER_COLSallow-list (NEVERduplicate_of— the id is a lookup capability; generatedis_duplicateis the only exposed signal). "duplicate" is NOT a manual close reason — merge only. - 0075 linked context:
get_vs_linked_context(p_id)— canonical PUBLIC → returns public_id+title (tracking view deep-links to board viavsOpenBoardProblem); confidential → only{linked, related_count}. - 0076 publish consent: report-form switch →
vs_tickets.public_consent; explicit decline is server-enforced invs_set_public(null = legacy, SE judgment). - 0077:
updated_at(touch trigger; kanban dual chips 📥เข้ามา + ↻อัปเดต) + status split "กำลังดำเนินการ" → สโมกำลังดำเนินการ / คณะกำลังดำเนินการ (phase maps match substring 'ดำเนินการ' — unchanged; legacy value maps to the สโม column). - 0078 staff-only comments: board composer "ส่งถึงเจ้าหน้าที่เท่านั้น" →
vs_public_comments.staff_only; served ONLY to staff/author (badge เฉพาะเจ้าหน้าที่); board counts exclude them. Old 2-argvs_post_public_commentDROPPED (3-arg default). - 0079 internal per-dept tags (migration APPLIED to live; frontend committed
5b082f2+ DEPLOYED to the VM):vs_tags(id/dept/label/color/sort_order/is_active) +vs_tickets.tags text[](loose, no FK, GIN idx). SECOND axis, orthogonal to the ONE public category taxonomy — tags are INTERNAL, staff-only, NEVER on the public board / guest RPCs, and OWNED BY A DEPT (each dept classifies its own workload; SE triage ≠ อุปนายก triage). RLS: read =current_user_is_staff(); write = vs_staff/dev/perm('vs') any dept, vp_admin OWN dept only. UI (admin entry only): kanban tag FACET beside the category facet (scoped to the acting dept, grouped by dept on the "all" view, hidden when the dept has no tags); per-ticket toggle-chip editor scoped to the ticket'starget_dept(save MERGES this dept's selection with the ticket's other-dept tags — a save never drops another dept's tags); card chips coloured per owning dept; per-dept จัดการแท็ก manager (modal-vs-tags.html; VP locked to own dept, super users get a dept picker; 10-colour dot paletteTAG_COLORS). Written via the same staffvs_ticketsPATCH path ascategory(staff-only log remarkinternal:true). NOT public-board related — the vs0072 isolation invariants are untouched. - 0080 guest-lookup tag leak fix (DB-only; APPLIED to live, no redeploy): 0079's new
vs_tickets.tagswas auto-exposed toanonbecauseget_vs_ticket_by_idisreturns setof vs_ticketsviaselect *. 0080 blanksr.tags := '{}'(beside the 0071duplicate_of := null). Verified anon RPC returnstags:[]even with a real tag set; isolation proof 23/23. Lesson in mistakes.md: any ALTER ofvs_ticketsmust audit thatselect *guest RPC per-column. Known residual (low, pre-existing class, NOT fixed): the owner-update RLS lets a submitter PATCH their OWNvs_tickets.tags(same exposure ascategory) — non-confidential opaque ids, a triage-integrity nuisance only, not a data leak. Fix only if it ever matters. - UI now live: staff modal in 5 purpose-sections; duplicate cluster TREE + nested kanban dups ("ซ้ำ N เรื่อง" expand strip; a dup whose canonical is outside the current filter renders top-level so it never vanishes); dashboard SEARCH + หมวดหมู่ FACET (
__none__= untagged); category = ONE taxonomy (internal + board; 🔒 assignable internally, never publishable) with TWO synced selects (section-2 + publish panel) + จัดการหมวดหมู่ manager (SE-only; add/rename/confidential-toggle with double-confirm/hide); public board: showcase strip "ผลงานที่แก้ไขสำเร็จ" (resolved problems leave the grid; hidden during search), ONE comment composer (me-too tap focuses it; button ส่งความคิดเห็น). - Invariants (breaking any re-exposes confidential complaints):
- public reads = curated projections via SECURITY DEFINER RPCs only (never raw problem/submitter/remarks/duplicate_of); 2. SE writes
public_title; 3. confidential categories hard-excluded from every public surface (category join re-checked in RPCs); 4. a submitter never receives another ticket's id; 5. an explicit consent decline cannot be published. Proof:tools/vs0072-isolation.mjs(23/23) — re-run after ANY change touching vs_categories or the board RPCs; it catches CONFIG regressions too (a toggle once flippedpersonalpublishable).
- public reads = curated projections via SECURITY DEFINER RPCs only (never raw problem/submitter/remarks/duplicate_of); 2. SE writes
- Live-data notes: test category
cat_mryxyw97"หมวดหมู่ลับเอิง" exists (hide via the manager if unwanted); test ticket VS-260724-1612-5N6 soft-deleted (restorable). - NEXT (roadmap): slice 4 = transition guards (status dropdown offers only valid next states). Slice 3 (per-person assignee) DROPPED — depts use one shared account (memory: depts-use-shared-accounts). OPEN: "post public update" button for staff (curated update → board thread) — recommended over ever exposing the raw internal timeline (PDPA +
internal:truecross-refs). Human e2e worth doing: merge two tickets → track the duplicate as its submitter → watch the mirror/banner.