Skip to content

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 ​

  1. The LAST two shared logins are gone. sastaff (role uni_staff) and saprof (role sa_prof) were deleted by tools/purge-shared-project-accounts.mjs after 161 + 65 rows were reassigned to the named เจ้าหน้าที่คณะ (woratho@kku.ac.th, staff seat) and อาจารย์ (prakasa@kku.ac.th, prof seat) who already held the seat. Both had signed in with Google before, so nothing was staged for them. public.users now holds NO uni_staff and NO sa_prof row — the role branches survive only as the pre-seat path in the helpers. The only shared password account left is samomdkkudev. ⚠️ 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: add and (role <> 'user' or permissions <> '{}' or managed_permissions <> '{}' or managed_project_seats <> '{}' …) — which returns exactly two, samomdkkudev (dev) and claude-reporter (holds claude, machine account). Verified 2026-08-18. ⚠️ The role LITERALS are still all over src/js/projects/* (~28 role === 'dev' / 'uni_staff' / 'sa_prof' branches). They still work, because projectSeatRole() maps a seat to the role STRING before anything branches. Do not "clean them up" without re-reading that function.
  2. ปีงบประมาณ is now a fact you can correct (projects.fiscal_year_be, 0165). NULL = derive from created_at; a number = a human moved it. The ONE implementation is src/js/projects/fiscal-year.js, and fiscal-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/ flags fmtDate, which is correct code. Who may move it is NOT a new gate: projects_update is already current_user_is_project_actor() = ผู้ส่งหนังสือ + เจ้าหน้าที่คณะ.
  3. 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. applyDefaultFiscalYear reset defaultFYPref on a uid change but returned early for 'all', leaving filterFY on the previous account's year. Unreachable only because admin-main.js hard-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 CASCADE ones. The run lost nothing (verified) but could not have said so. Now read from the catalog — which immediately caught project_user_prefs.user_id, added by 0165 the same day.
  • Two proof assertions asserted the WRONG PROPERTY. B5 counted 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_guard is the column guard keeping is_public sender-only even though projects_update has no WITH CHECK. Worth knowing: a staff-seat actor can therefore write any other column on projects; only is_public is 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_public is using(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.
  • prof0095 diffed the seat against the shared saprof account. Deleting that account would NOT have reddened it — sub resolves to null, both reads return 0, and 0 == 0 scores as parity. It now diffs against ground truth computed as superuser. A comparison against a deleted subject fails silently in the PASS direction.

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