ธีม
STATE archive — 2026-06-08
Detailed past-session narratives pruned from STATE.md to keep it under the ~200-line budget. All of this is SHIPPED + applied; authoritative chronology is git log. Kept here for the "what exactly did that feature do" lookups.
SAMO Team management — SHIPPED to main (0046–0049 applied)
New admin section ทีม SAMO (sidebar data-admin-side="team"), gated to vp_admin + dev via userCanAccess('team') (vp_admin default now ['vs','team']; dev all). Org tree: divisions → departments → roles → subroles (unlimited depth via team_nodes.parent_id), people under each node.
- Two toolbar modes (segmented toggle) so the page stays uncluttered:
จัดการทีม(roles + people) andจัดการสิทธิ์(permissions only). Permission chips + the perm editor only appear in perms mode; the node edit modal in team mode is just name + kind. - Add / edit / move / delete nodes + members. Two move paths: drag-and-drop for fine reorder (SortableJS; cycle guard via
onMove+isAncestor) AND an explicit searchable destination picker (openPickerin index.js +#teamPickerModal) — type-to-filter list of candidate parents/roles, select, confirm; excludes own subtree, "— ระดับบนสุด —" promotes to a root. Replaced the clunky 200-option<select>. The member modal's ตำแหน่ง field and the per-row member "ย้าย" both open the same picker. - ชั้นปี stored as a bare number; the year chip renders "ปี N".
- Multi-select ("เลือกหลายรายการ" toolbar toggle): checkboxes on node + member rows, a sticky bulk bar → ย้าย (one picker moves all selected: nodes reparent, members reassign; excludes selected nodes' own subtrees) or ลบ (bulk delete; nodes cascade, members not under a deleted node deleted individually). Drag is disabled in select mode. Multi-drag was intentionally NOT used (nested-tree multi-drag is unreliable) — bulk move is via the picker.
- Member rows now show kkumail inline (no need to open the editor).
- Per-node app permissions (
pr/vs/samoshop/projects/creator/team) with an inherit toggle, edited in the separate perms mode. v1 = org metadata only — NOT wired into live login access yet (chips: own=solid, inherited=dashed). Live-auth wiring is the scoped follow-up if wanted. - Files:
src/js/team/{index,api}.js,src/html/tab-team.html,src/css/team.css, registered inadmin/index.html+admin-main.js(SECTION_META/SIDE_FEATURE/initTeam/enterTeamWorkspace). - Seed pipeline:
tools/extract-team-seed.pyreadsexternaldata/roledata.xlsx(10 division tabs) +previousroledata.json(tree order) →externaldata/team-seed.json+0047. 37 xlsx roles with no JSON match (RT/MDI have no JSON roles + minor spelling variants) land as loose role nodes under their division — drag into place in the UI. Re-run:python3 tools/extract-team-seed.py(needs openpyxl). - Mutations are optimistic (update model + render, then persist; reload+toast on write failure).
- Live multi-editor sync (
src/js/team/realtime.js, migration 0048): Supabase Realtime postgres_changes on both tables → remote edits merge into the model and re-render (debounced 120ms; deferred while a drag is in progress to avoid cancelling the gesture). Last-write-wins, NOT character-level OT. Socket re-auths every 20 min (client autoRefresh off). Realtime node rows normalizepermissionsin case it arrives as a PG array literal. (No presence indicator — the channel stays open across admin sections, so a count would include people who left; removed as misleading.) - Every team-mode node is expandable (even an empty leaf role) and an empty leaf role shows a dashed drop placeholder so you can drag a person into a role that has no members yet (previously impossible — no drop target existed).
- Perf:
attachSortablesskips lists inside collapsed (d-none) bodies (inCollapsedBody) — only visible drop targets get a SortableJS instance, not all ~2×N lists every render (was iPad jank on the 218-node tree). - Import / export (
src/js/team/io.js, pure + unit-tested): export full tree+people as JSON or members as CSV (BOM for Excel Thai); import JSON (append, new ids, parents-first) or members CSV (Thai header aliases; resolvespath"ฝ่าย / แผนก / ตำแหน่ง", auto-creating missing roles when toggled). Import is additive (never deletes); sequential creates, so a big import is slow but safe. Robust to messy input: trims/collapses whitespace, normalizes ชั้นปี to a number, accepts looseconfirmedspellings (true/TRU/yes/ใช่/เข้าแล้ว…) and flags genuinely ambiguous ones, validates JSON shape (aborts with a clear message; orphan nodes go to root with a warning), de-dupes within the file AND against existing rows (by kkumail, else name+student_id per node), validates email format — and shows a per-import report (added / updated / skipped-with-reasons / warnings) instead of failing on the first bad row. The import modal stays open so the report is reviewable. The "เมื่อพบข้อมูลซ้ำ" select offers three modes: เลือกทีละรายการ (default) opens a git-merge-style per-conflict resolver — each duplicate shows a เดิม→ใหม่ field diff with a เก็บเดิม/ใช้ใหม่ toggle (+ bulk all-keep / all-replace); ข้ามทั้งหมด / อัปเดตทับทั้งหมด are the non-interactive shortcuts. CSV import is a read-only plan pass (planMembersCsv→ creates/conflicts/identical/skipped) then apply (applyPlan); path resolution only mutates at apply time. Path separator is " / " with spaces — a slash touching letters (e.g.Art/Graphic) is part of the name, not a level break (splitPathsplits on/\s+\/\s+/). - The destination picker + import modal are
modal-fullscreen-sm-downwith a flex body (sticky search, the list scrolls) so they're clean on iPad/phone.
President account + นายกสโม VS dept
- New account
samomdkkupresident/«disabled 2026-08-17», role=dev (full access, "permission like dev"),department='นายกสโม'. Provisioned viatools/president-account.mjs seed(CONFIRM=1) against real project. The service-role.from('users').update({role})is blocked byusers_self_update_guard(auth.uid()=null → not staff), so the script re-seeds the row via select→delete→insert (no INSERT guard exists). Seemistakes.md. vp-accounts.mjs has the same latent block if re-run. - นายกสโม added as a VS target dept everywhere: form select (
tab-vitalsound.html), dashboard filter (tab-admin.html), transfer + Discord-notify selects (modal-vs-staff.html), andDEPT_METAcolor/badge invs-staff.js. Discord: aนายกสโมwebhook was added toNOTIFY_DISCORD_VS_WEBHOOKSin.env.localand pushed to BOTH Cloudflare Pages projects viatools/set-notify-secrets.mjs(12 dept entries now).notifyVSConsult/notifyVSOnlyroute to it viafunctions/_discord.js. - Cleanup: removed dead
VP_DEPTS, deadisSuper, and the never-passedroleArgparam invs-staff.js. - VS dashboard: a super user (vs_staff/dev) with a
departmentset now defaults its dept filter to that dept on first entry (president → นายกสโม) while the picker stays visible so they can still browse ทุกฝ่าย / any dept. One-shot viastaffDashboardEntered; existing dev/SE have empty department → still default to ทุกฝ่าย (no behavior change).vs-staff.jsenterVSStaffDashboard.