ธีม
STATE archive — 2026-08-04
Sections pruned out of STATE.md when it passed 580 lines (CLAUDE.md caps it at ~200, and a bloated STATE.md is a direct tax on every cold start). All of this describes work that is DONE and stable; nothing here is in flight. Chronology lives in git log --oneline.
ประกาศ article cover — no longer cropped (2026-07-31)
.article-hero used to be a fixed 16:9 box with object-fit: cover, which center-cropped every cover. Covers are often PORTRAIT newsletter pages, so most of the page was cut away. The hero image is now rule-for-rule identical to .article-body img (width:100%; height:auto) inside the same 720px column.
Three things were tried and removed; src/css/article.css records each with the defect it caused, and none should be reintroduced: aspect-ratio (crops), a background colour (grey letterbox bars beside a portrait cover), and a vh-based max-height (made the cover NARROWER than the body pages once zoomed, because the height clamped and the width followed).
The board cards keep their 3:4 crop (.news-card-media) — a grid needs uniform tiles; a detail page needs the real image. Don't "unify" those.
APPS SCRIPT + DRIVE — all DONE 2026-07-31 (full detail in the archive)
Detail: docs/state-archive/2026-07-31-gas-drive-migration.md. Durable facts are also in the memory dir (gas-apps-script-topology, gas-is-an-unauthenticated-api).
THREE separate Apps Script projects — the names mislead. samoweb (standalone, 1lENmMdT…, deployment AKfycbwomKii…) serves samoweb; samopassport serves passport badges/certs; prformweb_backup_candelete is the retired Sheet-bound predecessor, still deployed only so bundles cached before the switch keep working. Deploying one cannot affect another.
- Both repos have
npm run deploy:gas. Diffs the remote, then create-version + update-deployment on the SAME id, then verifies over HTTP. Neverclasp deploy— new URL, reads as "uploads silently stopped". samoweb reads the endpoint fromsrc/js/config.js, passport fromVITE_GAS_UPLOAD_URL; each repo needs its ownGAS_SCRIPT_IDin.env.local. - Why samoweb was migrated: a bound script lives INSIDE its container, so trashing the unused
prformwebSheet would have taken the script and every deployment with it. The replacement is standalone. - Drive is now
My Drive/IT Database/—_Scripts/,PR/(was PR_Submissions),Projects/,Shop/(was SAMO_Shop),Team/(was SAMO_Team),Passport/{badges,certificates}. Every folder kept its original id through move+rename, so no stored URL changed.TOP_FOLDER_CANONaccepts legacy spellings; don't drop a legacy key while any deployed bundle can send it. New top-level folders go throughgetOrCreateTopFolder_, neverDriveApp.getRootFolder(). - Security review — three holes found and CLOSED, all years old, none caused by the migration: an unguarded delete-any-file, an open email relay, and an unconstrained upload folder. Uploads stay open because guests submit PR tickets without an account. A session gate on the deletes was built, deployed and REVERTED — it needed
UrlFetchApp, which widened the derived OAuth scopes, and a web app running as its owner throws until that owner re-consents; it broke every delete for ~1h. Deletes are folder-scoped only, as before. Re-enable ONLY in this order: owner re-consents first, then restore the gate (the frontend already sendsaccessToken). Proofs and the full post-mortem are in.claude/rules/mistakes.md. - ONLY REMAINING: delete
prformweb_backup_candelete+ its old deployment once the old endpoint is quiet. HTML isno-cache, so the drain window is open tabs only — hours, not weeks. Deleting early costs at most one failed upload on a stale tab; Drive trash is recoverable 30 days.
Shipped earlier, pruned to the archive
Full text: docs/state-archive/2026-07-30-pre-clear.md. All applied + deployed.
- ทีม SAMO is the grant engine (0081–0088). The tree issues real permissions via
managed_permissions/managed_vs_depts/managed_project_seats/managed_passport_*, recomputed by a statement-level trigger. Proofs:tools/team0089-manage.mjs,proj0086-seats.mjs,proj0092-seat-parity.mjs,prof0095-seat-parity.mjs,vs0083-scope.mjs. - VitalSound 0096–0099 — remark visibility ladder, unknown-category fail-closed, self-public context. Proof:
tools/vs0096-remark-vis.mjs. - Pre-/clear security scan (2026-07-29) — 4 real bugs, all fixed. The standing sweep is
tools/security-sweeps.mjs(run it after any RLS change).
READ THIS FIRST AFTER A /clear (2026-08-01 end of session)
Everything from this session is SHIPPED AND LIVE. main is at 28c757c, pushed; the KKU VM was deployed and verified against the SERVED bundles (buildId e74de393eebd); Apps Script is on v10; migration 0108 is applied. Nothing is in flight, nothing is half-done. Full reasoning: docs/state-archive/2026-08-01-team-identity.md; the identity rule is also a memory (team-identity-is-kkumail).
The one thing left undone, and it needs a human: none of the new UI has ever been rendered. The Chrome extension was not connected, so the crop dialog and the ตรวจสอบข้อมูล pane were built, unit tested and reasoned about but never LOOKED at. Open /admin/ → ทีม SAMO and check four things unit tests cannot see:
- the mode row has a fourth button, ตรวจสอบข้อมูล, badged 24;
- amber triangles on flagged member rows and amber counts on their ฝ่าย — ~38 rows of 404, most under ฝ่ายกิจการภายนอก (11);
- clicking one opens the pane filtered, with a "แสดงเฉพาะ …" banner;
- picking a photo in the member editor opens a pan/zoom 3:4 frame — check its proportions at PHONE width, and that the ตำแหน่ง picker now sits ABOVE the member editor rather than behind it.
What shipped
- Crop replaces จุดโฟกัสของรูป (
src/js/image-crop.js). Uploads are already 3:4, so every card takes lh3's server-side crop (~38 KB instead of ~78 KB).photo_focusstays in the DB — archived and legacy rows still carry top/bottom. - Stacked modals (
src/js/modal-stack.js, both entries). Bootstrap gives every.modalz-index 1055 and does not support stacking, so DOM order decided the painting order and the ตำแหน่ง picker rendered behind the member editor. - Team photos are deleted from Drive at last — GAS
deleteTeamFile(v10, proved both ways bytools/gas-team-delete-probe.mjs). It is a REFCOUNT:publish_team_termcopiesphoto_urlintoteam_archive_members, so a live portrait and an archived year's card are the same Drive file. - ตรวจสอบข้อมูล — a fourth ทีม SAMO mode listing every identity ambiguity, computed live from members already in memory. 24 findings today. จัดการทีม flags WHO: an amber triangle on each affected member row (tooltip names the reasons) plus a rolled-up count on every ancestor ตำแหน่ง — without the rollup the per-row flag would be invisible, since only the 14 root ฝ่าย are expanded on load. Both go through
issuesByMember(), computed once per render, and clicking either opens the pane ALREADY FILTERED to what was clicked — a member row focuses that person, a ตำแหน่ง's count focuses its whole branch, with a "แสดงเฉพาะ …" banner and a "ดูทั้งหมด" escape. Landing at the top of 24 findings and having to remember who you just clicked is the same work, moved. Using the mode BUTTON clears any focus, so the tab never silently shows a subset (which would read as "everything else is fixed"). Currently 38 of 404 rows, max 11 under any one ฝ่ายหลัก. - 0108
team_people— each person stored once. EXPAND ONLY: nothing reads it yet, all ten resolvers still jointeam_members.kkumail, and the proof asserts zero accounts whosemanaged_permissionswould change. 403 rows → 303 people.
The rule that governs all of it
kkumail is the identity; รหัสนักศึกษา is a field. It is the only key the user PROVES (Google login) rather than types, it is already what the permission engine resolves by, and 673070332-6 is one mistyped รหัส shared by two humans whose emails are correct and distinct. Never merge on name. Nothing non-empty is silently overwritten; two keys that disagree REFUSE rather than guess.
NEXT, in order
- Look at the UI (above). Nothing else should start before this.
- Contract step for 0108 — switch writes to the person, then drop the duplicated columns. Do NOT repoint one resolver without moving all ten. First thing it must do: close the INSERT gap.
createMemberand the CSV import writeperson_id = nullandbuildExportJsondoes not carry it — OBSERVED, not theoretical: a member added by hand minutes after 0108 applied is already unlinked (select count(*) from team_members where person_id is null). Nothing degrades today because nothing reads the column, and such a row is still fully visible and fixable in ตรวจสอบข้อมูล, which keys onteam_members. Fix with a BEFORE INSERT trigger resolving/creating the person by the same rule (it cannot loop with the mirror: that is AFTER UPDATE onteam_people), or simply re-run the 0108 backfill — it is idempotent and only considers unlinked rows. - Roles/permissions + photos — full design written 2026-08-04, nothing built:
docs/TEAM-ROLES-AND-PHOTOS.md. Measured on the live DB: 11 of 11 คณะกรรมการ slots have no portrait (1 photo exists org-wide) and 15 of 27 admin users can grant permissions ({team}onอุปนายกฯinherits down the branch). Also flagged there:หัวหน้าฝ่าย ITcarriesproject_seat='prof', which almost certainly is not intended. Build order + the five decisions the user must make are in §4/§5 of that doc. It subsumes and extends item 4 below. - Member profile page (design DECIDED, nothing built). kkumail is authentication, a
team_membersrow is authorization, the tree stays admin-only. Decisions the user made and that should not be re-litigated: a request form into an approval queue for people not on the roster (they explicitly rejected letting any kkumail self-add); self-uploaded photos go live with no moderation; a signed-in member MAY see other members' details. Self-edit must go through a SECURITY DEFINER RPC with a column allow-list — neverfor update using (user_id = auth.uid()), the class that has already bittenusers(0028),vs_tickets(0096) andshop_orders(0100). The durable fix for the 24 findings is this page: an admin cannot know whether วรวลัญช์'s ชื่อเล่น is ปรายฟ้า or ปลายฟ้า, but she can answer in one click.