Skip to content

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 live pr_tickets rows carry the old spelling; nothing rewrites the DB — canonicalPrDept() aliases it at the row → view-model boundary in pr-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 from PR_DEPARTMENTS via fillPrDeptSelect() (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 vocab src/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_COLS allow-list (NEVER duplicate_of — the id is a lookup capability; generated is_duplicate is 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 via vsOpenBoardProblem); confidential → only {linked, related_count}.
  • 0076 publish consent: report-form switch → vs_tickets.public_consent; explicit decline is server-enforced in vs_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-arg vs_post_public_comment DROPPED (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's target_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 palette TAG_COLORS). Written via the same staff vs_tickets PATCH path as category (staff-only log remark internal: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.tags was auto-exposed to anon because get_vs_ticket_by_id is returns setof vs_tickets via select *. 0080 blanks r.tags := '{}' (beside the 0071 duplicate_of := null). Verified anon RPC returns tags:[] even with a real tag set; isolation proof 23/23. Lesson in mistakes.md: any ALTER of vs_tickets must audit that select * guest RPC per-column. Known residual (low, pre-existing class, NOT fixed): the owner-update RLS lets a submitter PATCH their OWN vs_tickets.tags (same exposure as category) — 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):
    1. 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 flipped personal publishable).
  • 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:true cross-refs). Human e2e worth doing: merge two tickets → track the duplicate as its submitter → watch the mirror/banner.

Working docs. STATE.md is the status file and lives at the repo root, not here.