ธีม
2026-08-18 (daytime) — archived from STATE.md
Moved out of STATE.md on 2026-08-19 to keep the handoff readable. This is the session narrative as it stood; the rules that are still LIVE were left in STATE.md (the ปีงบ default, the sastaff/saprof purge, the file-highlight baseline). git log --oneline is the chronology.
What the 2026-08-18 DAYTIME session was — THREE things, all deployed
- The LAST two shared logins are gone.
sastaff(roleuni_staff) andsaprof(rolesa_prof) were deleted bytools/purge-shared-project-accounts.mjsafter 161 + 65 rows were reassigned to the named เจ้าหน้าที่คณะ (woratho@kku.ac.th,staffseat) and อาจารย์ (prakasa@kku.ac.th,profseat) who already held the seat. Both had signed in with Google before, so nothing was staged for them.public.usersnow holds NOuni_staffand NOsa_profrow — the role branches survive only as the pre-seat path in the helpers. The only shared password account left issamomdkkudev. ⚠️email like '%@samomdkku.app'is NOT an audit for shared accounts — it returns 48, and 46 of them are ordinary students who registered with a username (that domain is the synthetic email every password signup gets). Counting it looks alarming and means nothing. Audit by GRANT instead: addand (role <> 'user' or permissions <> '{}' or managed_permissions <> '{}' or managed_project_seats <> '{}' …)— which returns exactly two,samomdkkudev(dev) andclaude-reporter(holdsclaude, machine account). Verified 2026-08-18. ⚠️ The role LITERALS are still all oversrc/js/projects/*(~28role === 'dev'/'uni_staff'/'sa_prof'branches). They still work, becauseprojectSeatRole()maps a seat to the role STRING before anything branches. Do not "clean them up" without re-reading that function. - ปีงบประมาณ is now a fact you can correct (
projects.fiscal_year_be, 0165). NULL = derive fromcreated_at; a number = a human moved it. The ONE implementation issrc/js/projects/fiscal-year.js, andfiscal-year.test.js§4 is a RATCHET that greps every projects module for a second one. ⚠️ It asserts the พ.ศ. offset paired with a month comparison — a bare/543/flagsfmtDate, which is correct code. Who may move it is NOT a new gate:projects_updateis alreadycurrent_user_is_project_actor()= ผู้ส่งหนังสือ + เจ้าหน้าที่คณะ. - Each person picks the ปีงบ their inbox opens on (
project_user_prefs, 0165):'all' | 'current' | '<year>', own-row-only RLS both directions.'current'resolves at OPEN time, so it rolls over on 1 ต.ค. by itself. An ABSENT row means'all'— nobody's behaviour changed until they opt in.
A /scrutinize pass on the same session's work found four defects (2f35068) — worth reading, because three are shapes that recur here:
- The auto-move lost the viewer's filter. The move handler follows the ปีงบ filter so a project does not vanish; it followed only when a NUMBER was written, and clearing an override writes NULL. Fixed by asking
projectFiscalYear({ ...p, fiscal_year_be: next })— the SAME function the grid filter uses. A "where does it end up" question answered locally will drift from the filter that answers it for real. - A half-done reset.
applyDefaultFiscalYearresetdefaultFYPrefon a uid change but returned early for'all', leavingfilterFYon the previous account's year. Unreachable only becauseadmin-main.jshard-reloads on an account switch. A guard that depends on an unrelated module's reload is not a guard. - The purge script audited 10 of 23 FK columns, and the 13 it missed included three
ON DELETE CASCADEones. The run lost nothing (verified) but could not have said so. Now read from the catalog — which immediately caughtproject_user_prefs.user_id, added by 0165 the same day. - Two proof assertions asserted the WRONG PROPERTY.
B5counted overrides equal to their derived year (vacuous today, red the first time someone pins a project to the year its date implies, and its SQL rule used UTC months where the app uses ICT).B6("no trigger exists") found two that do —projects_public_flag_guardis the column guard keepingis_publicsender-only even thoughprojects_updatehas no WITH CHECK. Worth knowing: astaff-seat actor can therefore write any other column onprojects; onlyis_publicis separately guarded.
Two guard lessons paid for in this session, both worth remembering:
proj0165's first draft could not tell a working policy from no policy:projects_read_publicisusing(is_public)and 27 of 28 projects are public, so "the staff seat reads every project" was ALSO true of a user with no grant. It now CREATES a hidden project inside its own transaction as the discriminator. When a probe's ALLOW and its DENY would give the same answer, the subject is wrong.prof0095diffed the seat against the sharedsaprofaccount. Deleting that account would NOT have reddened it —subresolves to null, both reads return 0, and0 == 0scores as parity. It now diffs against ground truth computed as superuser. A comparison against a deleted subject fails silently in the PASS direction.