Skip to content

STATE — current task & latest known state ​

Last updated: 2026-05-30 (long shop polish session — see top section)

SESSION SNAPSHOT (2026-05-30) — resume point after /clear ​

Branch main at b0b8756. refactor/modular synced to the same commit. Build green, 53 tests pass (3 test files: utils, uploads, ticket-ids; plus shop/data.test.js for the new ticket-id + status contracts). Cloudflare auto-rebuilds both production (samomdkkuweb.pages.dev → main) and preview (refactorsamomdkkuweb.pages.dev → refactor/modular) on push.

Migrations applied vs pending (verify before next ship) ​

The user confirmed mid-session that 0016–0022 are applied to prod Supabase (fheueuowbchsnsvbcgil). 0023–0025 ship NEW columns + a trigger and likely haven't been run yet — every JS call site that touches them has a graceful fallback (warn-then-degrade) so the site keeps working without them.

MigrationPurposeStatus
0008–0015(prior session — applied)applied
0016_current_user_dept_helper.sqlsecurity-definer current_user_dept() + vs_tickets policiesapplied
0017_announcement_order.sqldisplay_order column for drag-reorderapplied
0018_shop_order_status_extras.sqlwidens shop_orders.status check to include slip_mismatch / refund_pending / refunded / no_showapplied
0019_shop_banners.sqlnew shop_banners table + RLSapplied
0020_shop_orders_buyer_slip_reupload.sqladds slip_mismatch to buyer-update RLSapplied
0021_guest_ticket_lookup_rpcs.sqlget_vs_ticket_by_id + get_pr_ticket_by_id RPCsapplied
0022_shop_orders_exchange_status.sqladds 'exchange' to shop_orders.status checkapplied
0023_shop_product_code.sqladds shop_products.code (backfill SH-derived) — drives <CODE>NNNN order ids❌ pending
0024_shop_product_production_status.sqlshop_products.production_status + apply_product_production_status RPC (cascades on product-status toggle)❌ pending
0025_shop_orders_paid_cascade.sqlBEFORE-UPDATE trigger: order moves INTO 'paid' → auto-advances per product production_status❌ pending

Until 0023 is applied, new orders fall back to SH<NNNN> (the default code). Until 0024 is applied, the product editor's "สถานะผลิตสินค้านี้" dropdown surfaces "ติดตั้ง migration 0024 ก่อน" and the product save still completes. Until 0025 is applied, admin must toggle the product's production_status to push existing 'paid' orders forward — newly approved 'paid' orders don't auto-cascade until the trigger exists.

Architecture — unchanged from prior snapshot ​

  • Two SPAs from one repo (/ public, /admin/ operator). Same Supabase, same Cloudflare. Vite multi-page input.
  • Per-VP accounts via users.permissions text[] + RLS helpers.
  • File uploads still on Drive via GAS uploadShopFile / uploadImageToDrive.
  • Auth fixes from this session: signInWithPassword explicitly populates currentUser + notify() after the supabase-js call resolves, so Android Chrome's stale-listener case doesn't leave the modal open. Profile fetch in buildCurrentUser uses dbRest (not supabase-js .from) for the same reason.

What shipped this session (newest first, by domain) ​

Shop admin order modal (final shape)

  • Two-group status picker: "เปลี่ยนสถานะ" (6 happy-path chips — pending/review/paid/produce/ready/done) + "สถานะปัญหา" (issue chips: slip_mismatch / exchange / refund_pending / no_show / cancel / refunded, each in its tone colour).
  • Selection state: outlined when off, SOLID FILLED + checkmark prefix when on. Tone families: warning amber, info blue, neutral gray, danger red. (Pastel-on-pastel was unreadable.)
  • Confirm-button workflow: chip click STAGES the change locally (modalPendingStatus); the new "อัปเดต" footer button writes it. Disabled gray when no change, green + status label when dirty. "ยกเลิกการเปลี่ยนแปลง" link reverts.
  • "ลบคำสั่งซื้อ" button (red, left-aligned in footer) hits shop_orders_delete_admin RLS via deleteOrder() in api.js.
  • Modal no longer carries the old approve / reject / "สถานะอื่น" dropdown — chips replaced them.
  • Order item rendering: shows product NAME from state.products (not the raw internal id like "p-shirtttest-685"). Internal id shows as small grey code reference. Defensive null filtering on the items array.
  • Defensive: modalAction snapshots orderId to a local before the await, so a downstream null doesn't surface as the "Cannot read properties of null (reading 'id')" toast.

Shop admin order LIST

  • New product filter dropdown next to the status filter.
  • AND/OR radio toggle ("ทั้งหมด (และ)" / "อย่างน้อยหนึ่ง (หรือ)") — predicates are collected then evaluated with every() / some(). Default stays AND.
  • Old per-product "1 คำสั่งซื้อ · สถานะปัจจุบัน 'X' → ทำเครื่องหมาย เป็น 'Y'" bulk-advance bar REMOVED. Replaced by the per-product production_status cascade.

Shop products

  • shop_products.code (0023) is the prefix for new order ids: <CODE><NNNN> (e.g. SH1234, TS5678). Editor exposes it as "รหัสนำหน้า Order". sanitizeOrderCode() is the SSoT — uppercase alnum, max 5 chars, fallback "SH". genOrderId(code) updated.
  • Color picker rebuilt: was a JSON-array textbox, now a row-based picker (native color input + label + optional id + × remove + "+ เพิ่มสี"). Empty rows dropped on save.
  • Hue input rebuilt: still 0–360, now flanked by a live hsl() swatch in an input-group.
  • shop_products.production_status (0024) — pending / produced / announced. Dropdown in the product editor. Save calls apply_product_production_status RPC which both updates the product AND cascades to every eligible happy-path order. The cascade ONLY moves: paid→produce (on produced), paid/produce→ ready (on announced). Off-path orders (slip_mismatch, refund_pending, refunded, cancel, no_show, exchange, done, pending, review) are NEVER touched.
  • 0025 trigger plugs the gap: when a NEW customer's order transitions INTO 'paid' (slip approval) AFTER the product is already at produced/announced, the trigger auto-advances on the spot. Multi-product orders need every item at the target rank.

Shop product list + stock view

  • Stock matrix table: <td> restored to display:table-cell so column widths track the size headers. The interactive cluster (− / input / +) moved into an inner <div> wrap.

Shop banners (admin)

  • Full admin section ("แบนเนอร์" sub-tab): upload image (Drive), drag-reorder (SortableJS), inline caption + link inputs (save on blur), active toggle, delete. Customer landing carousel prefers banners; falls back to is_new products, then to the 5 most recently added.

Shop hero carousel

  • One-slide-per-view 21:9 (4:3 on phones) banner format. Image fills, overlay with caption + price. Pagination dots, arrows scroll by viewport width.

Shop checkout / orders

  • Removed misleading "ค่าจัดส่ง · รับเอง · ฟรี" — replaced with "การรับสินค้า · รับที่คณะแพทย์" + explainer.
  • Removed the "หมายเหตุเพิ่มเติม" panel (admin announces pickup via batches; the freeform note was rarely used).
  • Dev accounts (role='dev') can checkout without uploading a slip — order lands as 'pending'.
  • Customer orders view: filter chip row REMOVED (per user request — scroll-to-find beats tap-to-filter for ≤20 orders).
  • Customer order card on slip_mismatch: inline "อัปโหลดสลิปใหม่" action calls setOrderSlip() which atomically PATCHes slip_url + slip_uploaded_at + flips status back to 'review'. Migration 0020 widens the buyer-update RLS to include slip_mismatch.
  • Customer status labels (final wording): pending → "สั่งซื้อแล้ว" review → "รอการตรวจสอบสลิป" paid → "ยืนยันการชำระเงิน" produce → "สินค้าผลิตเสร็จแล้ว" ready → "ประกาศรอบรับสินค้า" done → "ได้รับสินค้าแล้ว" Off-path: slip_mismatch / refund_pending / refunded / no_show ("ยังไม่ได้รับสินค้า") / cancel / exchange.

Shop verify queue

  • Reject button no longer cancels (terminal). Sets slip_mismatch so the buyer can re-upload. Outline-warning styled "สลิปไม่ถูกต้อง".

Auth / login

  • Android Chrome login hang root-caused: supabase-js's onAuthState- Change occasionally drops SIGNED_IN after a signOut→signIn cycle. signInWithPassword now explicitly fetches the profile + notifies subscribers itself (belt-and-braces) after db.auth.signInWith- Password resolves. buildCurrentUser uses dbRest (not supabase-js) for the profile query — mistakes.md guidance for any reliable read.

Guest ticket lookup (VS + PR)

  • 0021 added security-definer RPCs get_vs_ticket_by_id / get_pr_ticket_by_id (returns SETOF the row matching p_id). Granted to anon+authenticated. Anyone with the id can read their own ticket; RLS still gates direct table reads.

VS + PR forms — success card

  • Native alert() replaced with persistent inline success card on both forms. Readonly monospace input + full-width "คัดลอก Ticket ID" button + "ปิด" link. Card sits AFTER the form (right under the submit button), so the user doesn't scroll up. Copy logic: navigator.clipboard.writeText with execCommand('copy') fallback. window.scrollTo({top:0}) moved from finally to catch so error visibility still works but success stays put.
  • src/js/ticket-ids.js (new shared module): generators generatePRTicketId / generateVSTicketId + regex contracts PR_TICKET_ID_REGEX / VS_TICKET_ID_REGEX. Both forms import. src/js/ticket-ids.test.js (11 tests) locks format + uniqueness + generator-regex round-trip.

News / announcements editor

  • Drag-to-reorder via SortableJS in admin creator's collapsible "ลำดับการแสดงประกาศ" panel.
  • 16:9 Cropper.js for cover images.
  • Edit pre-populates the form (the previous "blank form on edit" bug was loadAnnouncements throwing on missing #announcements- Grid in admin — fixed with null guards + re-throw).
  • Featured card frame: 16:9 everywhere (was 4:3 on desktop).
  • Delete works: split into deleteAnnouncement(id?) + deleteCurrentAnnouncement + deleteEditingAnnouncement. New "ลบประกาศนี้" button in the admin creator appears when editing.

Mobile navbar (public)

  • Hamburger moved LEFT (matches /admin/). Drawer slides from LEFT.
  • Brand absolutely-centered on mobile (Twitter / X pattern).
  • Auth pill HIDDEN on mobile — drawer carries sign in / out / admin / external links.
  • Admin sidebar mobile drawer: 100dvh height + safe-area-inset- bottom padding so iOS Safari chrome doesn't bury the sign-out button.

External links surface

  • ระบบจองห้องสโม (Apps Script) + SAMO Passport (samomdkkupassport.pages.dev) added to:
    • Public tools launcher (Thai+English data-name for /-search)
    • Public navbar avatar dropdown (signed-in)
    • Mobile offcanvas (always)
    • Admin sidebar "ลิงก์ภายนอก" section All target="_blank" rel="noopener" with bi-box-arrow-up-right.

VS kanban

  • Sorts newest-first in every column (matches PR). Overdue colour on the card still flags stale tickets via age-bucket class.

SPA back button

  • Tab activations now history.pushState (was replaceState), so the browser back button unwinds /pr → / instead of leaving the site entirely.

Open items / not built ​

  • Editable internal product id: still disabled for existing products. Would need a FK cascade migration on shop_order_items.product_id (currently on delete restrict, no on update cascade). Defer until there's real demand.
  • 0023–0025 not yet applied on prod — user needs to run them in Supabase SQL editor before testing the new features end-to-end.
  • 9arm-skills install: still open from prior snapshot — optional.
  • Discord nudge for VP idle tickets: still deferred.

Reproducible smoke after Cloudflare redeploy ​

  1. Visit samomdkkuweb.pages.dev (prod) and refactorsamomdkkuweb.pages.dev (preview). Both return 200 at /, /pr, /vssound, /shop, /tools, /about, /news, /news/{id}, /admin/.
  2. Public:
    • Submit a VS ticket as guest. Success card shows; copy button copies the id; pasting into "ค้นหาสถานะ" finds the ticket (RPC).
    • Submit a PR ticket as guest. Same card, same flow.
    • Read a news article. Hero is 16:9, image fills.
  3. Sign in as a staff account on mobile Android Chrome:
    • Type username + password, tap "เข้าสู่ระบบ". Modal closes, avatar appears. No more "spinner returns, no login".
  4. /admin/ as dev:
    • Orders tab: filter by status / product / search. Toggle AND/OR. Open an old order. Status chips show product NAMES (not raw ids). Change status to a new chip → "อัปเดต" turns green → click → toast.
    • Hit "ลบคำสั่งซื้อ" on a test order. Confirms + deletes.
    • Banners sub-tab: upload an image, drag to reorder.
    • Products sub-tab: edit a product, set "รหัสนำหน้า Order" to "TS", change "สถานะผลิตสินค้านี้" to "ประกาศรอบรับสินค้า" → confirm dialog → toast says "อัปเดต N คำสั่งซื้อตามสถานะ ผลิตใหม่" (assumes 0024 applied).
  5. After 0023 applied: place a new order; new id format <CODE><NNNN>.
  6. After 0025 applied: have the product's production_status at 'announced', approve a new buyer's slip → order auto-lands at 'ready'.

News editor polish + drag-reorder + merge to main (2026-05-29) ​

User-driven iteration on the announcements / news experience plus a production merge.

Shipped on refactor/modular ​

  • Edit-blank bug finally root-caused: loadAnnouncements() was writing to #announcementsGrid and #emptyState on its first line — those only exist on the public site. On admin the TypeError was swallowed by tryCreatorDeepLink's catch, globalAnnouncements stayed empty, and editAnnouncement(id) found nothing. Wrapped every DOM-write in a null guard; re-throw on error so callers see real failures.
  • Publish self-heals on missing excerpt column the same way the read path does (gate by response, not by a flag set elsewhere).
  • 16:9 cover-image cropper via Cropper.js. Picking a file opens a modal; the crop box is locked at 16:9; confirm exports max 2000×1125 JPEG @ 0.9 and uploads. Cancel leaves the existing cover untouched. The display-side aspect-ratio: 16/9 + object-fit: cover still applies, but now the author picks the framing.
  • Featured card frame at 16:9 (was 4/3 on desktop) so a 1600×900 upload renders edge-to-edge on the home featured card.
  • Delete that actually deletes: split deleteCurrentAnnouncement into deleteAnnouncement(id?), deleteCurrentAnnouncement, deleteEditingAnnouncement. Public reader's "ลบ" no longer hops to /admin — it confirms + deletes inline. New "ลบประกาศนี้" button in the admin creator appears when editing.
  • Drag-to-reorder via SortableJS in the admin creator's new collapsible "ลำดับการแสดงประกาศ" panel. Drag handle on the left, thumb + title + dept/date middle, edit-pencil right. Drop persists via display_order PATCHes (top = highest int, descending).
  • Migration 0017_announcement_order.sql adds the display_order int column + index. Sort changed to display_order desc nulls last, created_at desc. Read path cascades graceful fallback (no excerpt → no display_order → minimal) so the site keeps working pre-migration.

Pending migrations to apply on prod (fheueuowbchsnsvbcgil) ​

MigrationPurposeStatus
0008_announcements_excerpt.sqlexcerpt columnapplied (user confirmed)
0016_current_user_dept_helper.sqlcurrent_user_dept() security-definer helper + repoint vs_tickets policies❌ pending
0017_announcement_order.sqldisplay_order column + index❌ pending — apply before next ship to enable reorder

Without 0017 applied, the reorder UI still renders (using the fallback sort by created_at) but the PATCH on drop will 400 and the toast shows "บันทึกลำดับไม่สำเร็จ".

Merge to main ​

Merging refactor/modular → main per user request. Both branches build green, 26/26 tests pass. Branch ruleset main-protect is active; we have Bypass.


User reported (a) the admin sidebar sign-out button was hidden behind iOS Safari's bottom chrome on mobile, (b) admin / room-booking / passport shortcuts should open in new tabs, and asked where to put the new external links for best UX across desktop / iPad / mobile.

Decisions ​

  • Two complementary surfaces for the new external tools (ระบบจองห้องสโม, SAMO Passport): the public tools launcher (canonical discovery for students/visitors) and the admin sidebar under a new "ลิงก์ภายนอก" section (so staff don't have to leave admin to reach them). Also mirrored in the public navbar's avatar dropdown + mobile offcanvas so signed-in users get a one-click shortcut from any tab.
  • All external links open in a new tab (target="_blank" rel="noopener"), including the existing "ไปยัง Admin Dashboard" entries. Visual cue: bi-box-arrow-up-right instead of bi-arrow-up-right / bi-arrow-right.
  • Sidebar sign-out visibility is now safe-area-aware; the workspace sidebar uses 100dvh (with 100vh fallback) so iOS Safari's collapsible bottom chrome doesn't steal the bottom of the drawer.

Changes ​

  • src/css/workspace.css —
    • .workspace-side (mobile drawer): height: 100vh; height: 100dvh; so the drawer matches the actually-visible viewport on iOS Safari.
    • .workspace-side-foot (mobile): padding-bottom: max(0.85rem, calc(env(safe-area-inset-bottom) + 0.6rem)) so the sign-out button sits above the home-indicator inset.
    • .workspace-side-item text-decoration: none (anchor variants for external links).
    • .workspace-side-item-ext trailing external-arrow icon style; also hidden in collapsed (icon-only) sidebar mode alongside the label spans.
  • src/css/launcher.css — .launcher-tool text-decoration: none so the new <a>-based tool cards don't render with underlines.
  • src/html/navbar.html —
    • Admin Dashboard link: target="_blank" rel="noopener" + box-arrow icon (desktop dropdown + mobile offcanvas).
    • New external-tool links (ระบบจองห้องสโม + SAMO Passport) in the avatar dropdown (signed-in users; below the admin link).
    • Same two links also added as standalone offcanvas items (no auth gate; visible to everyone on mobile).
  • src/html/tab-tools.html — two new <a class="launcher-tool"> cards in the public "สำหรับทุกคน" section, both target="_blank". data-name covers Thai + English search terms so /-search finds them.
  • admin/index.html — new "ลิงก์ภายนอก" sidebar section with the two external-link items. No data-admin-side (these don't switch panes) and no role gate.
  • src/js/admin-main.js — mobile drawer auto-close handler now fires for any .workspace-side-item (was [data-admin-side] only), so clicking an external link closes the drawer too.

Verify on device ​

  • iPhone Safari, signed in as a VP at /admin/:
    • Open the sidebar drawer → scroll if needed → sign-out button is fully visible above the Safari URL bar AND above the home-indicator inset.
    • Tap ระบบจองห้องสโม or SAMO Passport in "ลิงก์ภายนอก" → opens in a new tab, drawer closes in the current tab.
  • Public site (/):
    • เครื่องมือ tab → "สำหรับทุกคน" section shows 5 cards now (PR / VS / Shop + the two new external ones). Search "ห้อง" or "passport" filters them.
    • Signed in → avatar dropdown shows Admin Dashboard + the two external links, each opening in a new tab.

/scrutinize fix pass (2026-05-29) ​

End-to-end review of refactor/modular vs main surfaced two majors plus four minors. All fixed in-tree on this branch. Build + tests still green (26/26). One new migration not yet applied (0016 below).

Changes ​

  • src/js/auth.js — staff-username blocklist in registerWithPassword was a literal list of 6 names and didn't include the 9 VP accounts added in 0010/0011. A public visitor could squat e.g. samomdkkuradiology before the admin seeded that VP. Switched to a prefix check on samomdkku*
    • the legacy sastaff literal.
  • supabase/migrations/0016_current_user_dept_helper.sql (new) — added public.current_user_dept() (security definer, set search_path = public) and repointed the three vs_tickets policies that embedded (select department from public.users where id = auth.uid()) inline (read / update / delete). The inline form silently depended on users_read_all (0001) staying permissive — tightening that would silently zero out every VP's dashboard. Helper removes that cross-table RLS coupling. Behavior unchanged.
  • src/js/admin-main.js — admin boot-gate's 4-second timeout used to swap straight to the "เฉพาะเจ้าหน้าที่" deny copy, which a staff user on a slow network would read as access denied right before the dashboard popped in. Now it just updates the spinner copy to "โหลดช้ากว่าปกติ — ลองรีเฟรชหากค้างนาน".
  • admin/index.html — dropped dead data-role-only attributes on the 5 sidebar buttons. The JS path that reads them (admin-main.js:419) explicitly skips elements that also have data-admin-side, so those attributes never did anything but mislead future readers.
  • src/js/db.js — 25-min setInterval(refreshSession,…) ran forever including after sign-out, emitting a warn every 25 min on long-lived signed-out tabs. Now gated on the supabase session storage key existing.
  • STATE.md — the previous note "Apply 0012 first thing next session" was wrong: 0015 drops and recreates the same policy with a broader predicate, so 0012 is not a prerequisite. Migration table updated; the misleading "apply 0012" call-out replaced.

Apply before next ship ​

supabase/migrations/0016_current_user_dept_helper.sql — paste in the Supabase SQL editor. It re-creates 3 policies on vs_tickets (read / update / delete). Verify with:

sql
select pg_get_functiondef('public.current_user_dept()'::regprocedure);
-- should exist and be SECURITY DEFINER

select policyname, qual from pg_policies
where schemaname='public' and tablename='vs_tickets'
order by policyname;
-- read / update / delete should reference current_user_dept()

Not fixed (deferred — design call needed) ​

  • pr_agents_write (0014) is for all and lets any user with permissions[] @> '{pr}' rewrite the single-row global agent roster. Today only samomdkkudigital has pr — fine — but if a VP is granted pr for read-only PR visibility (likely-future ask), they can wipe the roster too. Splitting into pr (read) and pr_admin (manage agents) needs a product call on intent.

SESSION SNAPSHOT (2026-05-28) — resume point after /clear ​

Branch refactor/modular is at 4be1a16 (post-kanban-only + scroll affordances + dead CSS cleanup). Build green, 26/26 tests pass. Cloudflare preview refactorsamomdkkuweb.pages.dev auto-rebuilds on push.

This session shipped 10 iterations across UI/UX, architecture (public+admin split), per-VP accounts with permissions, VS kanban+filter, URL routing, and several RLS fixes. Multiple migrations still need to be applied to the prod Supabase project (fheueuowbchsnsvbcgil) — confirm 0009–0014 are all applied before considering this session "shipped".

Architecture as it stands now ​

  • Two SPAs from one repo (Vite multi-page, one Cloudflare project):
    • / → public site bundle. Tabs: home, ประกาศ, แจ้งปัญหา (VS form), ร้านค้า, เครื่องมือ, เกี่ยวกับเรา.
    • /admin/ → operator app bundle. Workspace shell (sidebar + top-bar + collapsible/drawer). Sections: ภาพรวม, PR Management, VitalSound, SAMO Shop, หนังสือโครงการ, เขียนประกาศ.
    • Same Supabase, same Cloudflare. public/_redirects routes /admin/* → /admin/index.html, /* → /index.html.
  • URL routing for public: /news /news/{id} /pr /vssound/shop /tools /about. Path↔tab mirror via shown.bs.tab. Article view uses /news/{id} (legacy #article/{id} auto-redirects).
  • Editorial article view (full-page reader at /news/{id}, not modal).
  • Per-VP accounts (10 อุปนายก): all role=vp_admin, distinguished by users.department. Extras via users.permissions text[]. userCanAccess(feature, user) in auth.js combines role default + perms.
  • VS dashboard: list + kanban (9 per-status columns). Per-VP filter dropdown drives both views. Hide-empty-columns toggle persisted in localStorage. VPs see their own dept only (RLS-enforced); SE/dev default to ทุกฝ่าย + kanban.

Migrations applied vs pending (verify before shipping) ​

MigrationPurposeStatus (per user)
0009_vs_owner_reply.sqlVS owner can reply to own ticketapplied (vs_tickets_update_owner present)
0010_vp_accounts_permissions.sqlusers.permissions + per-dept VP RLS + 9 reservationsapplied
0011_vp_corrections.sqlmedia→mdi rename + corrected UPDATE block (final perms)applied (UPDATE block run)
0012_vs_delete.sqlDELETE policy for vs_staff/devsuperseded by 0015 (see note)
0013_vs_vp_send_back_to_se.sqlWITH CHECK fix: VP can โอนคืน SElikely applied (policy redefined; verify body)
0014_permission_aware_rls.sqlpr_tickets/pr_agents/announcements/shop_* honor permissions[]applied
0015_vs_delete_for_all_vs_staff.sqlDELETE extends to VPs (own dept) + 'vs' permapplied
0016_current_user_dept_helper.sqlcurrent_user_dept() helper; repoints vs_tickets read/update/delete to use it (removes RLS cross-table coupling on users_read_all)❌ not yet applied — apply before next ship

0012 is not a prerequisite for 0015. 0015 does its own drop policy if exists "vs_tickets_delete_staff" and recreates the policy with the broader predicate, so if 0015 is in place the delete button works whether or not 0012 ever ran. (Earlier note here said "apply 0012 first" — that was wrong.) Verify with the pg_policies query below.

Verify 0013/0014 bodies (policy names alone don't prove the body is the new version):

sql
-- 0013: with_check should contain 'SE' in the IN(...) list
select with_check from pg_policies
where schemaname='public' and tablename='vs_tickets'
  and policyname='vs_tickets_update_staff';

-- 0014: pr_tickets read should mention current_user_has_permission
select qual from pg_policies
where schemaname='public' and tablename='pr_tickets'
  and policyname='pr_tickets_read';

-- 0014: shop helper should include perm OR
select pg_get_functiondef('public.current_user_is_shop_admin()'::regprocedure);

User confirmed 0010 + 0014 applied; should sanity-check 0009 + 0012 + 0013 because they fix specific bugs (VS reply, VS delete, VP→SE transfer). Quick check:

sql
select policyname from pg_policies
where schemaname='public' and tablename='vs_tickets'
order by policyname;
-- Should include:
--   vs_tickets_delete_staff         (0012)
--   vs_tickets_insert_anyone
--   vs_tickets_read                  (0010 expanded)
--   vs_tickets_update_owner          (0009)
--   vs_tickets_update_staff          (0013 expanded)

VP account credentials (for the boss) ​

10 accounts; all sign in via username + password (no @suffix needed). Username pattern samomdkku<short> / password «pattern retired»:

ฝ่ายUsernamePasswordExtra perms
อุปนายกฝ่ายบริหารองค์กรsamomdkkuvpa«disabled 2026-08-17»projects, samoshop
ฝ่ายดิจิทัลและสื่อสารองค์กรsamomdkkudigital«disabled 2026-08-17»pr, creator
ฝ่ายกิจการภายในsamomdkkuinternal«disabled 2026-08-17»—
ฝ่ายกิจการภายนอกsamomdkkuexternal«disabled 2026-08-17»—
ฝ่ายกิจการมหาวิทยาลัยsamomdkkuuniversity«disabled 2026-08-17»—
ฝ่ายวิชาการsamomdkkuacademic«disabled 2026-08-17»—
ฝ่ายยุทธศาสตร์ฯsamomdkkustrategy«disabled 2026-08-17»—
ฝ่ายคุณภาพชีวิตฯsamomdkkuquality«disabled 2026-08-17»—
ฝ่ายเวชนิทัศน์samomdkkumdi«disabled 2026-08-17»—
ฝ่ายรังสีเทคนิคsamomdkkuradiology«disabled 2026-08-17»—

All VPs see: VS for their own dept + nothing else by default. Perms stack on top: e.g. samomdkkudigital also sees PR Management + เขียนประกาศ.

Existing super-account samomdkkuvssound (role=vs_staff) sees all VS.

Automation script ​

tools/vp-accounts.mjs — drives the Supabase Admin API to create/delete VP auth users. Two modes (cleanup / seed), requires CONFIRM=1 to proceed. See script header for env vars. Used earlier this session to clean up a misplaced batch in the "passport" Supabase project.

Files added this session ​

admin/index.html                                    (new admin entry)
public/_redirects                                   (Cloudflare SPA routing)
src/admin.css                                       (admin CSS bundle)
src/js/admin-main.js                                (admin entry script)
src/css/article.css                                 (editorial article)
src/css/footer.css                                  (4-col footer)
src/css/launcher.css                                (tools launcher)
src/css/news.css                                    (editorial cards)
src/css/workspace.css                               (admin shell)
src/css/vs-admin.css                                (VS kanban + chips)
src/html/tab-article.html                           (article reader)
src/html/tab-tools.html                             (tools launcher)
supabase/migrations/0008_announcements_excerpt.sql  (subhead column)
supabase/migrations/0009_vs_owner_reply.sql         (VS owner reply RLS)
supabase/migrations/0010_vp_accounts_permissions.sql (perms + RLS)
supabase/migrations/0011_vp_corrections.sql         (rename + UPDATE)
supabase/migrations/0012_vs_delete.sql              (VS delete RLS)
supabase/migrations/0013_vs_vp_send_back_to_se.sql  (WITH CHECK fix)
supabase/migrations/0014_permission_aware_rls.sql   (perms-aware RLS)
tools/vp-accounts.mjs                               (auth admin automation)

Bug scan results (end of session — final, post 4be1a16) ​

Bugs found + fixed across this session:

  • VS kanban exact-string status match silently dropped legacy tickets → added "อื่นๆ" catch-all column.
  • Kanban empty state was blank when hide-empty=on + 0 tickets → added centered inbox-icon placeholder with a nudge to change filter.
  • "Kanban (ทุกฝ่าย)" misleading label → removed with the now-dropped view toggle.
  • VS list view dropped per user request — kanban-only now.
  • VS delete originally only vs_staff/dev → extended via 0015 to VPs (own dept) and anyone with 'vs' perm.
  • VP couldn't โอนคืน SE → 0013 fixed WITH CHECK clause.
  • PR staff dashboard blank for VPs with 'pr' perm → 0014 extended pr_tickets/pr_agents/announcements/shop_* policies.
  • VS "media" account renamed to "mdi" via 0011 + script.
  • About sub-nav redundancy removed.
  • Navbar wrap on iPad portrait → switched back to navbar-expand-lg.
  • Dead CSS .vs-dept-* chips and .vs-view-toggle cleaned out.

Real bugs found, NOT fixed (low impact, defer):

  • sessionStorage.vsViewPicked is now dead key (view toggle gone) but a stale entry in some users' browsers does nothing. Cleanup: delete sessionStorage.vsViewPicked once on next admin entry, or just leave (storage quota is tiny).
  • src/html/modal-announcement.html exists but no entry includes it (replaced by /news/{id} article reader). Safe to delete file.

Mobile/desktop UX additions in this round:

  • Kanban gets edge-fade gradients (Linear / App Store pattern) + column peek + visible thin scrollbar. Mobile users now know they can swipe horizontally without text instructions.
  • "อื่นๆ" catch-all column gets dashed border + italic title so it reads as "non-canonical statuses worth reviewing" instead of another normal column.

RLS / DB state:

  • 0008–0015 all confirmed applied to the real db (fheueuowbchsnsvbcgil) per user's verify queries during the session.

Verified clean (final scan):

  • No orphan window.* handlers — all HTML onclick targets are wired.
  • No public-app imports of admin-only modules (public bundle stays small).
  • setVsView, renderList, vsViewToggle, vsKanbanHideEmptyWrap, staffTicketList, vsKanbanToggleBtn — all removed from JS, HTML, and exports/imports consistently.
  • Scroll-affordance binding is idempotent (flag guard + dual call).
  • Path routing: /, /pr, /vssound, /shop, /tools, /about, /news, /news/{id}, /admin/ all return 200 in dev.

Open items / known not-yet-done ​

  • 9arm-skills install: optional — user asked about it. Install via npx skills add thananon/9arm-skills. Memory notes about when to invoke /debug-mantra etc. can be added on demand.
  • Discord nudge for VP idle tickets (Phase 3 from earlier triage discussion): not built. Would be a cron / scheduled Edge Function pinging Discord when a ticket sits in รออุปนายก >3 days.
  • VP "Mark received" explicit ack: discussed but not built; would add friction without clear payoff.
  • Per-tab visual polish for VS form / Shop / Projects content (the global token pass touched the chrome, not the form internals).
  • Dead CSS in vs-admin.css for the removed dept-chips. Cosmetic cleanup; not breaking.
  • modal-announcement.html file still in src/html/ but not included by any entry. Safe to delete.

How to resume after /clear ​

  1. Read this STATE.md top section first.
  2. Read .claude/rules/mistakes.md if touching auth.js / db.js / RLS.
  3. Verify migration 0009 + 0012 + 0013 are applied (the SQL block above).
  4. Check refactorsamomdkkuweb.pages.dev is reachable and the routes /, /pr, /vssound, /news, /admin/ all serve 200.
  5. Sign in as samomdkkudigital (a VP with extra perms) and confirm:
    • Public site → avatar dropdown shows "ไปยัง Admin Dashboard ↗"
    • /admin/ → sidebar shows ภาพรวม + VitalSound + PR Management + เขียนประกาศ
    • /admin/#pr → PR kanban populates (this needed 0014 — confirm)
    • /admin/#vs → VS kanban with 9 per-status columns, hide-empty toggle
  6. Sign in as samomdkkuvssound → confirm Kanban (ทุกฝ่าย) view shows all VPs' tickets with dept badges.

Substantial IA + visual polish pass. Build green, tests 26/26 pass. Manual browser verification still pending across breakpoints.

What changed (intent) ​

User asked for a more premium / professional / modern UI and noted the navbar had too many things — with the constraint that ~100 tools will be added later (some dept-scoped, some shared). The fix is to keep the nav permanently minimal and route tool discovery through a dedicated launcher.

Old: หน้าหลัก · ประกาศ · ร้านค้า · เครื่องมือ▾ · เกี่ยวกับเรา▾ + role items + auth. New: Brand · ร้านค้า · เครื่องมือ · (หนังสือโครงการ) · (Admin) + bell + avatar/sign-in.

  • ประกาศ dropped from nav — home page already carries the announcement carousel + "ดูทั้งหมด" link to the full archive tab (hidden tab button).
  • เกี่ยวกับเรา dropdown deleted — 4 sections moved to the new footer. goToAbout() still activates the hidden #pills-about-tab for footer + mobile-offcanvas links.
  • เครื่องมือ dropdown deleted — now a top-level pill that opens the new tools tab (#pills-tools).
  • navbar-expand-md instead of -lg so iPad portrait (768px+) keeps the horizontal nav (the slim shape fits). Pills tighten at md via media query.
  • New translucent glass-light navbar: rgba(255,255,255,0.92) + backdrop-filter: saturate(180%) blur(14px). Token-driven shadows.
  • Mobile cluster (<768px): hamburger + a separate mobile bell that mirrors the desktop bell (click, visibility, count) via mirrorMobileBell() helper in main.js. projects/index.js + projects/notifications.js both teach about the mobile bell too.

Tools launcher tab (src/html/tab-tools.html) ​

Designed to scale to 100+ tools without further layout changes. Each tool is a .launcher-tool button carrying:

  • data-name="..." — searchable text (Thai + English + aliases)
  • data-cats="..." — chip-filter categories (public / pr / vs / content / staff)
  • data-roles="..." — role gate; only matching roles see the card

Search input filters live. Chip row above the grid (ทั้งหมด / สาธารณะ / PR / VitalSound / เนื้อหา / เจ้าหน้าที่). Empty state shown when search has 0 matches. Adding a new tool = drop a button in. No JS changes required.

/ keyboard shortcut focuses the search field when the tools tab is open.

Replaces the old 1-line copyright stamp. 4-column grid:

  • Brand + tagline
  • เกี่ยวกับเรา (4 about-tab anchors via goToAbout())
  • เครื่องมือ (PR / VS / Announcements / Shop / All tools — all via activateTab())
  • ติดต่อ (Facebook / IG / email) Plus a copyright bar below. Stacks to 2-col at 991px, 1-col at 575px.

Global tokens (src/css/base.css) ​

New CSS variable scales used everywhere from this commit on:

  • --ink-50..900 (slate scale)
  • --surface (#ffffff)
  • --shadow-xs..xl + --shadow-focus-ring (layered, premium feel)
  • --radius-xs..2xl + --radius-pill
  • --ease, --t-fast/base/slow (motion tokens)
  • prefers-reduced-motion guard at the bottom of base.css
  • Body bg upgraded to dual radial-gradient over linear (subtle green + orange wash, much less flat than the old plain gray)

Files touched ​

  • src/html/navbar.html — rewritten (slim shape, mobile cluster, user pill)
  • src/html/footer.html — rewritten (4-col)
  • src/html/tab-tools.html (new) — launcher
  • src/html/tab-home.html — tools section gets a "ดูเครื่องมือทั้งหมด" link
  • index.html — includes tab-tools.html; inline critical CSS updated to match new navbar glass look
  • src/css/navbar.css — full rewrite using tokens
  • src/css/launcher.css (new) — launcher styles
  • src/css/footer.css (new) — footer styles
  • src/css/base.css — tokens + new body gradient + reduced-motion guard
  • src/main.css — @import launcher + footer CSS
  • src/js/main.js — launcher search/filter/role-gating; mobile-bell mirror; mobile-user strip wiring (new mobileUserPic/Name/Dept IDs + mobileSignOutItem); goToAbout no longer touches deleted aboutDropdown; shown.bs.tab handler comments updated
  • src/js/projects/index.js — toggle navProjectsBellMobile alongside desktop
  • src/js/projects/notifications.js — bind click + update count on both bells

Verification status ​

  • npm run build — green
  • npm test — 26/26 pass
  • Manual browser verification — pending. To check:
    • Desktop (≥1200): navbar reads Brand · ร้านค้า · เครื่องมือ + auth; glass-light bg; pills tighten on hover, active = green pill with shadow.
    • iPad landscape (1024) + portrait (768): horizontal nav still shows; user-name text hidden (d-none d-lg-flex), only avatar pill visible.
    • Mobile (<768): hamburger + bell visible right; offcanvas drawer has user-strip on top when signed in.
    • Tools tab: search filters live; chips switch sections; empty state works; / key focuses search; sign in as staff role → staff section
      • chip appear.
    • Footer: 4-col on desktop, 2-col on iPad, 1-col on mobile. About links jump to the right section in #pills-about.

Iteration 2 — same session (2026-05-28) ​

User feedback after first pass:

  • Navbar had weird empty space (pills were centered with flex: 1).
  • Wants ประกาศ + เกี่ยวกับเรา back in the navbar.
  • ประกาศ shouldn't live in the tools launcher — visitors flip between news a lot, it's content not a tool.

Adjustments:

  • Navbar pills switched from centered cluster to left-aligned next to brand (.nav-primary { margin-left: 0.5rem }), with auth pushed right via margin-left: auto on .samo-navbar-auth. Matches Stripe / Linear / Vercel pattern. Empty space gone.
  • ประกาศ is back as a top pill (uses existing #pills-announcements-tab, no longer hidden).
  • เกี่ยวกับเรา is back as a single top pill (uses existing #pills-about-tab, no longer hidden) — not a dropdown.
  • Inside #pills-about, added a sticky sub-nav (pill row, glass-light) with 4 sections (ทีมงาน / วิสัยทัศน์ / พันธกิจ / นโยบาย).
    • Active section tracks scroll via IntersectionObserver in initAboutSubnav() (main.js).
    • On mobile (≤575px), sub-nav becomes icon-only to fit all 4.
    • .about-section { scroll-margin-top: 160px } clears both the global navbar and the sub-nav on anchor jumps.
  • ประกาศทั้งหมด tool card removed from the launcher and from the home quick-tools grid (replaced with ร้านค้า + เครื่องมือทั้งหมด tiles). Launcher chip "เนื้อหา" → "เขียนเนื้อหา" (it now only filters the creator tool).
  • Content tabs scroll-to-top on activation: when the user switches to about / tools / announcements via the navbar, window.scrollTo(0) fires from the shown.bs.tab handler so the visitor sees the hero, not whatever scroll position they were at on the previous tab. Admin/projects keep their own hash-routed scroll behavior.

Files touched in iteration 2:

  • src/html/navbar.html — added ประกาศ + เกี่ยวกับเรา pills, swapped nav-center → nav-primary, updated mobile offcanvas with ประกาศ.
  • src/css/navbar.css — .nav-primary (left-aligned), .samo-navbar-auth gets margin-left: auto.
  • src/html/tab-about.html — added .about-subnav block.
  • src/css/cards.css — .about-subnav, .about-subnav-link, mobile icon-only variant; bumped scroll-margin-top to 160px.
  • src/js/main.js — initAboutSubnav(), scroll-to-top on content tab activation.
  • src/html/tab-tools.html — removed ประกาศทั้งหมด tool card.
  • src/html/tab-home.html — replaced two redundant cards with ร้านค้า + เครื่องมือทั้งหมด tiles.

Build green, tests 26/26.

Iteration 3 — editorial redesign (2026-05-28) ​

User feedback after iteration 2:

  • Announcement cards "look childish" — wants what professional web does.
  • เครื่องมือยอดนิยม and ฝ่ายในสโมสร on the home page are redundant — they should live where they belong.
  • "Should main page show what else?"

Decisions:

  • Editorial news style (Stripe / FT / Linear changelog pattern): border-based cards, no jumpy hover, no heavy drop shadows, tight typography (eyebrow → title → meta), subtle image-zoom on hover only. Dropped the scroll-snap carousel + arrow nav (felt app-y / playful).
  • Home is now an editorial news index:
    1. Slim auth-CTA / welcome strip (no big hero)
    2. Featured story (1 large card, image left, eyebrow + title + excerpt right) — populated from the newest announcement
    3. News grid (up to 6 more cards in a clean grid, image top + meta)
    4. Restrained "เริ่มต้นกับ SAMO" quick-action strip — 3 inline items (PR / VS / Shop), not a card grid
  • Removed from home:
    • Tools quick-grid (4 cards) — redundant with the navbar เครื่องมือ pill and the launcher tab.
    • ฝ่ายในสโมสร (10 dept cards) — they were all disabled "เร็วๆ นี้" placeholders. Real per-dept tools belong in the launcher; the departments-as-org-info is About content.
  • ฝ่ายในสโมสร moved to About tab — new #about-departments section below นโยบาย, reuses the existing .dept-card styling. About sub-nav now has 5 items (added ฝ่าย).
  • Announcement archive (#pills-announcements) rebuilt with the same editorial card style + a refined page header (eyebrow + h1 + lead + refresh button).

Date formatting: introduced formatEditorialDate(post) that returns the restrained "28 พ.ค. 2569" format (BE year) instead of the previous full dd/mm/yyyy HH:MM:SS stamp.

Renderer helpers extracted from loadAnnouncements:

  • renderNewsFeatured(post) — large featured card
  • renderNewsCard(post) — secondary card; used by home grid AND archive
  • pickCover(post) / extractSnippet(content, max) — shared helpers

Files touched in iteration 3:

  • src/css/news.css (new) — editorial card system (.news-featured, .news-card, .news-grid, .news-eyebrow, .news-meta, .news-archive-head, .news-empty)
  • src/main.css — @import './css/news.css'
  • src/css/cards.css — deleted .announce-card, .home-announce-*, .home-hero, .home-section-*; new .home-quick-* strip CSS; refined .home-auth-cta + .home-welcome to token-based, border-only
  • src/js/announcements.js — renderHomeAnnouncements() now emits featured+grid; loadAnnouncements() archive emits same news cards; shared helpers pickCover / extractSnippet / formatEditorialDate / renderNewsFeatured / renderNewsCard
  • src/js/main.js — removed window.scrollHomeAnnounce (carousel gone)
  • src/html/tab-home.html — rewrite to editorial structure
  • src/html/tab-announcements.html — rewrite to news-archive header
  • src/html/tab-about.html — added ฝ่าย to sub-nav + new #about-departments section with the 10 dept cards

Build green, tests 26/26.

Iteration 4 — Harvard-Gazette reader + slot creator (2026-05-28) ​

User reference: Harvard Gazette home + article pages. Two questions: "shouldn't the news look like this" + "how can creators do it easily".

Architecture decisions:

  • Reader: dedicated full-page article tab, NOT a modal.
  • Creator: 5 slots; no design choices. Live preview reuses the same renderer the reader uses, so authors see exactly what visitors see.
  • Schema: additive nullable excerpt column on announcements (migration 0008). Old rows fall back to extracted snippet — no breaking change.

What shipped ​

Schema (supabase/migrations/0008_announcements_excerpt.sql):

  • Add excerpt text to public.announcements. Nullable. No backfill.
  • Apply via Supabase SQL editor before re-pulling from the preview branch; the renderers tolerate the column not existing (defaults to empty string on the JS side) but PostgREST would error on the select-list if the column was missing.

Reader (src/html/tab-article.html + src/css/article.css + JS in announcements.js):

  • New tab #pills-article reached via:
    • card click → viewAnnouncement(id) activates tab + renders + pushes #article/{id} to the hash
    • direct URL → handleArticleHash() runs on hashchange and after loadAnnouncements() resolves
  • Layout: sticky back-bar (back button + staff edit/delete) → eyebrow → big headline (clamp 1.8–2.8rem) → subhead → byline (dept + date) → wide hero image (max 1100px, rounded) → reading body column (max 720px, 1.08rem / 1.75 line-height) → after-read CTA back to archive.
  • Editorial reading typography: Harvard / Medium / Substack pattern. prefers-reduced-motion already handled globally in base.css.
  • closeArticleView() pops the hash and returns to #pills-announcements.

Creator (src/html/tab-creator.html + announcements.js):

  • Slim head with title/desc + an Edit / ดูตัวอย่าง pill-toggle
  • 5 slots: หัวเรื่อง · ฝ่าย · คำโปรย (NEW, 220 char limit with live counter) · ภาพปก (now REQUIRED — Harvard pattern; publish blocks without one) · เนื้อหา (Quill body)
  • Preview pane mounts renderArticleView(formSnapshot, {isPreview}) — same renderer as the public reader, so what authors see is what visitors get. No second-renderer drift possible.
  • After successful publish, the creator auto-opens the new article (viewAnnouncement(publishedId)) so the author lands on the rendered editorial page instead of the archive grid.

Renderers in src/js/announcements.js:

  • loadAnnouncements() selects excerpt; maps to globalAnnouncements.
  • renderNewsFeatured(post) prefers post.excerpt over the auto- extracted body snippet. Cards across home + archive show the real subhead now.
  • renderArticleView(post, {isPreview}) — single source of truth for the article HTML, used by reader and creator preview.
  • viewAnnouncement(id) switches to article tab + renders + syncs hash.
  • closeArticleView() symmetric exit.
  • setCreatorMode('edit'|'preview') toggles the creator panes.
  • editCurrentAnnouncement() no longer touches a modal; pulls post, fills form (incl. excerpt), navigates to creator.
  • deleteCurrentAnnouncement() calls closeArticleView() then reloads.
  • POST uses prefer: 'return=representation' so we get the new row id back and can auto-open it.

Cleanup:

  • modal-announcement.html include removed from index.html (file kept in tree as reference; safe to delete later).
  • Hidden pills-article-tab button added to navbar.html for Bootstrap Tab API routing.

Manual steps to ship ​

  1. Apply supabase/migrations/0008_announcements_excerpt.sql in the Supabase SQL editor.
  2. (No GAS redeploy.) (No env-var change.)
  3. Smoke test:
    • Reader: click any news card on home or archive → opens full-page article with sticky back bar, hero image, reading column. URL hash becomes #article/{id}. Reload the page → opens the same article on cold load.
    • Creator (publish): เขียนประกาศ → fill title + คำโปรย + pick a cover image + body → กดดูตัวอย่าง → confirm preview matches what you'll see → เผยแพร่ → success → auto-lands on the new article.
    • Creator (edit): open an existing article → edit button → form populated incl. excerpt → save → article updated, lands back on the article view.
    • Creator (image required): try to publish without picking a cover image → red alert "กรุณาเลือกภาพปกของบทความ".
    • Pre-0008 posts: render without an excerpt should fall back to the auto-snippet under the headline.

Iteration 5 — graceful loader + staff workspace in avatar (2026-05-28) ​

User feedback after iteration 4:

  • 400 error on the local dev when loading announcements (excerpt column missing — migration 0008 hadn't been applied to local Supabase yet).
  • "Should Admin and หนังสือโครงการ be on different page? What's best practice?"

Fixes:

1. Graceful announcements loader (src/js/announcements.js):

  • loadAnnouncements() now tries the select with excerpt. On 400, retries without excerpt so the site keeps working pre-migration.
  • Logs one console.warn pointing at the pending migration so devs notice it but the UI never breaks.
  • publishAnnouncement() mirrors the gate: if __samoWarnedExcerpt is set, strips excerpt from the POST body so insert/update don't 400 on the missing column either.
  • Renderers were already excerpt-tolerant (empty falls back to extracted snippet) — no further change needed.

2. Staff items move to the avatar dropdown (src/html/navbar.html):

  • Followed the GitHub / Linear / Vercel pattern: public navbar stays clean (ประกาศ · ร้านค้า · เครื่องมือ · เกี่ยวกับเรา); staff workspace links live inside the user-profile dropdown.
  • Avatar dropdown now has a "เจ้าหน้าที่" section heading above the staff items, separated by dividers from profile info and from Sign Out at the bottom.
  • Mobile offcanvas mirrors with a "เจ้าหน้าที่" section heading.
  • pills-projects-tab and pills-admin-tab buttons remain in the tablist as hidden controls (so Bootstrap Tab API can still activate the panes from anywhere) — only the visible top-pill buttons were removed.
  • Existing role-gating logic (main.js + projects/index.js) carries over unchanged because navProjectsItem / navAdminItem IDs moved but didn't rename.
  • New #navStaffSection (dropdown) and #mobileStaffSection (offcanvas) toggle visible when ANY staff-only item is visible. Computed in both auth subscribers via the resolved DOM state to cover the projects-role and global-role paths.
  • Bell stays in the navbar — notifications are time-sensitive and belong at the surface, not buried in a dropdown.

Net effect:

  • Visitor navbar: 4 pills + auth button.
  • Signed-in staff navbar: 4 pills + bell + avatar dropdown (which now expands to expose Admin Dashboard + หนังสือโครงการ + Sign Out under "เจ้าหน้าที่" heading).
  • Tools launcher still carries the same staff items (different access pattern: browse vs. quick-jump from avatar).

Files touched in iteration 5:

  • src/js/announcements.js — graceful loader + publish gate
  • src/html/navbar.html — Admin/Projects out of top pills, into dropdown menu items; staff-section labels added (desktop + mobile); hidden tab buttons preserved
  • src/css/navbar.css — .nav-user-section-label + .samo-offcanvas-section-label
  • src/js/main.js — toggle #navStaffSection / #mobileStaffSection via queueMicrotask after both visibility paths resolve
  • src/js/projects/index.js — toggle same staff-section roots when the projects role-visibility runs

Build green, tests 26/26.

Iteration 6 — Multi-page split: public + admin app (2026-05-28) ​

User asked the bigger architecture question: should admin be a separate page like real products do (Stripe Dashboard, Vercel, Linear), sharing the same Supabase + Cloudflare. Confirmed yes — Option B (path-based separation in same repo). Also: หนังสือโครงการ moves INTO the admin app.

Result:

  • Two entries from one repo: / (public site) and /admin/ (operator app)
  • Two CSS + JS bundles — public visitors no longer download admin code
    • Before: 487 KB JS (one monolith)
    • After: 51 KB public + 161 KB admin + shared chunk 283 KB
  • Same Supabase, same Cloudflare Pages project, same git workflow
  • VitalSound now a public top-nav pill ("แจ้งปัญหา") since it's a report-a-problem system (user feedback during this iteration)

Vite multi-page (vite.config.js):

  • build.rollupOptions.input with public: index.html + admin: admin/index.html
  • htmlPartials plugin processes <include src="..." /> in both entries
  • Single npm run build outputs dist/index.html and dist/admin/index.html

Admin entry (admin/index.html + src/js/admin-main.js + src/admin.css):

  • Permanent body.workspace-mode — no public chrome
  • Workspace shell: sidebar (Overview / PR / VS / Shop / Projects / Creator
    • sign out) + top bar (workspace title, project bell when role allows)
  • Sidebar items drive section switching via showAdminSide(which) — hides/shows [data-admin-pane] and triggers the legacy openAdminSection() for PR/VS/Shop
  • Hash routes: /admin/#pr, /admin/#vs, /admin/#shop, /admin/#projects, /admin/#creator — bookmarkable, shareable
  • Boot gate: spinner → auth resolves → app or sign-in gate
  • Auth gate (signed-out / non-staff): "เฉพาะเจ้าหน้าที่" with "กลับสู่หน้าหลัก" + sign-in modal
  • Inherits the same Supabase session (cookies/localStorage are origin-scoped)

Public entry trimmed:

  • index.html dropped includes: tab-admin, tab-projects, tab-creator, modal-pr-staff, modal-agents, modal-vs-staff, modal-project-send, offcanvas-project-notify
  • src/js/main.js dropped imports: announcements creator side (kept read-only viewAnnouncement / loadAnnouncements / closeArticleView), pr-staff, vs-staff, projects, openShopAdmin. Creator Quill init removed.
  • Avatar dropdown: "เจ้าหน้าที่" section → single "ไปยัง Admin Dashboard ↗" link to /admin/
  • Mobile offcanvas: same — single link
  • Project bell removed from public navbar (lives in admin top bar)
  • Tools launcher: dropped "เนื้อหา" + "เจ้าหน้าที่" sections; เขียนประกาศ moved to admin
  • Editor's "edit"/"delete" action on a published article now redirects to /admin/#creator (since admin owns those flows)
  • All window.* shims for admin handlers redirect to /admin/#<section> so any stray onclick that survives doesn't 404 — it just navigates

Stripped-content files (tab-admin.html, tab-projects.html, tab-creator.html):

  • Removed the <div class="tab-pane fade" id="pills-*" role="tabpanel"> and the workspace-shell wrappers I added in iteration 5
  • They're now bare content blocks — admin/index.html includes them inside its single canonical workspace shell
  • tab-admin.html: dropped the inline adminLanding cards (admin entry has its own canonical landing section); per-section "back to dashboard" buttons removed (sidebar drives navigation)
  • tab-projects.html: kept the inline #projectsSubnav switcher as the secondary in-pane switcher; setView() in projects/index.js now delegates by data-projects-view regardless of parent ID

Public navbar (src/html/navbar.html):

  • New top pill: แจ้งปัญหา (VitalSound, with clipboard-pulse icon). User asked for it because VS is a report-a-problem system — it deserves to be visible alongside ประกาศ / ร้านค้า.
  • Hidden tab buttons for pills-admin / pills-projects / pills-creator / pills-vitalsound removed (creator/admin/projects gone from public; vitalsound is now a visible pill).

Files touched in iteration 6 ​

  • vite.config.js — multi-page input
  • admin/index.html (new) — admin entry, workspace shell, boot/auth gates, includes
  • src/admin.css (new) — admin CSS bundle (base + cards + forms + modals + workspace + shop + projects)
  • src/js/admin-main.js (new) — admin entry script
  • index.html — dropped admin/projects/creator includes + 5 admin-only modals
  • src/js/main.js — heavy trim (admin/projects/creator imports + handlers gone, workspace toggle gone, exitWorkspace gone, mirrorMobileBell gone)
  • src/html/navbar.html — added แจ้งปัญหา pill; replaced เจ้าหน้าที่ group with single /admin/ link (dropdown + offcanvas); removed bell from desktop + mobile; cleared dead hidden tab buttons
  • src/html/tab-admin.html — stripped workspace shell + tab-pane wrapper + landing cards + back-links
  • src/html/tab-projects.html — stripped workspace shell + tab-pane wrapper; restored inline subnav as secondary in-pane switcher
  • src/html/tab-creator.html — stripped tab-pane wrapper
  • src/html/tab-tools.html — dropped staff section + content section + their filter chips
  • src/js/projects/index.js — broader setView() selector + click delegation on #pills-projects

Manual steps to ship ​

  1. Apply migration 0008_announcements_excerpt.sql (still pending from iteration 4).
  2. Cloudflare Pages should serve dist/index.html at / and dist/admin/index.html at /admin/ automatically. No build config change.
  3. Smoke test:
    • / loads with: ประกาศ · แจ้งปัญหา · ร้านค้า · เครื่องมือ · เกี่ยวกับเรา + sign-in
    • Sign in as a staff account → avatar dropdown shows "ไปยัง Admin Dashboard ↗" link
    • Click → navigate to /admin/ — boot gate spins briefly, then workspace shell appears (sidebar + top bar)
    • Each sidebar item navigates: PR / VS / Shop / Projects / Creator
    • /admin/#shop deep-links to the shop pane
    • "กลับสู่หน้าหลัก" returns to /
    • Sign out from sidebar — returns to /admin/ showing the auth gate
  4. Bundle sizes are smaller for public visitors:
    • Before: 487 KB JS for everyone
    • After: 51 KB public + 283 KB shared (still smaller than monolith)
    • admin-only code (161 KB) is only fetched when an operator navigates to /admin/

Known not-in-scope ​

  • Home page is news-first as agreed; per-tab visual polish (PR / VS / Shop / Projects / Admin) still pending a dedicated session.
  • Multi-image gallery / inline pull-quotes / related-articles list at article foot — all Harvard Gazette features we could add later if authors want richer storytelling tools.
  • modal-announcement.html file still in src/html/ but no longer included — delete in a cleanup pass.
  • Tabs (PR / VS / Shop / Projects / Admin) not visually polished this round — only the global token shift + body bg affects them. Per-tab pass is its own session.
  • Cmd+K command palette deliberately skipped (user picked dedicated /tools page pattern). / shortcut added as a lightweight power-user affordance.

SAMO Shop refactor (2026-05-27) ​

Substantial UX + schema change pass on the SAMO Shop module. Build + tests green (26/26). Not yet deployed — needs schema migration + manual smoke test before merge.

What changed ​

  • Sources reshaped: md, rt, mdi, sittikao (replaces project/fund/merch). Legacy rows auto-migrated to md by 0007 — admin should re-tag them.
  • Types: dropped accessory (ของแถม).
  • Fit dimension removed everywhere (modal, cart, checkout, admin editor, order detail). All items default to unisex on insert; old fits column kept in DB but ignored in UI.
  • Presale → Preorder rename across labels (DB column is_presale kept to avoid a backfill — only the UI text changed).
  • New stock_status column on products: available | sold_out | production_closed. Storefront shows OOS ribbon + grays the card + disables Add-to-Cart. Independent of is_active (soft-archive).
  • Stock matrix UI: editable size × color number grid in the admin product editor. Empty cell = unspecified; 0 = OOS for that combo.
  • เปิดตัวล่าสุด is now a horizontal "big show" carousel with prev/next arrows + scroll-snap (mobile: swipe).
  • ประกาศการรับสินค้า now stacks multiple active batches on the storefront (was single hero before). Closed batches editable + re-openable in admin.
  • Per-date hours: dates_full jsonb [{date, hours}] lets each pickup date carry its own time window. Backfilled from legacy dates[] + shared hours by the migration.
  • Checkout pickup-radio block removed: location/time come from the admin's pickup announcement instead.
  • Delivery workflow (new admin tab "การส่งมอบ"):
    • One card per ready order, expand to per-item checklist.
    • Tick → prompts for recipient name, writes shop_pickup_records row (one per order_item_id, unique).
    • Issue button → captures issue_type (wrong_size/damaged/missing/other) + free-text note.
    • Resolve button on issues → adds resolution text + resolved_at.
    • When all items ticked, "ปิดคำสั่งซื้อ → รับสินค้าแล้ว" advances the order to done. Issues block the auto-complete.

Files touched ​

  • supabase/migrations/0007_shop_refactor.sql (new): source enum, stock_status, dates_full, shop_pickup_records + RLS.
  • src/js/shop/data.js: new constants (sources, types, stock-status meta, stockKey helper, batchDateEntries helper).
  • src/js/shop/api.js: pickup-record CRUD (listPickupRecords*, upsertPickupRecord, resolvePickupIssue, deletePickupRecord).
  • src/js/shop/products.js: drop fit options, preorder labels, stock-status banner, big-card carousel, multi-batch banner stack.
  • src/js/shop/checkout.js: removed pickup-location radio block, drop fit from variant display.
  • src/js/shop/cart.js: drop fit from variant display.
  • src/js/shop/orders.js: per-date hours via batchDateEntries.
  • src/js/shop/admin.js: full rewrite of products editor (stock matrix, drop fits, stock_status), batches (edit anytime, per-date hours + remove rows), new delivery tab.
  • src/html/tab-shop.html: launch carousel + arrows.
  • src/html/tab-admin.html: new "การส่งมอบ" tab + refresh button.
  • src/html/modal-shop-product.html: drop fit group, preorder label, stock-status banner.
  • src/css/shop.css: launch carousel, multi-batch banner styles, preorder/sold-out ribbons, OOS card grayscale, stock matrix grid, delivery checklist styling, batch-date chips.

Manual steps to ship ​

  1. Apply supabase/migrations/0007_shop_refactor.sql (Supabase SQL editor).
  2. Existing products auto-migrate source → md. Re-tag them (RT/MDI/Sittikao) via admin.
  3. Existing pickup batches auto-build dates_full from dates[] + shared hours. Open each in admin to add per-date times.
  4. Smoke test:
    • Shop tab: carousel scrolls; arrow disabled at ends; multi-batch banner renders if 2+ active.
    • Mark a product sold_out in admin → OOS ribbon shows, card grayscaled, Add-to-Cart disabled.
    • Set stock_matrix entry to 0 for a size/color → variant OOS warning in modal.
    • Place an order → admin Verify → Approve → Produce → Ready.
    • Delivery tab: tick item → enter recipient name → row turns green. Mark another item as issue (wrong_size) → row turns yellow. Resolve → green-strikethrough. Tick all → "ปิดคำสั่งซื้อ" appears → order goes to done.
  5. No GAS redeploy needed (only DB + frontend changed).

Follow-up: search-first delivery + standalone stock tab (same day) ​

Iterated on the delivery UX and added a dedicated stock-only tab after user feedback ("fast easy access, like search customer and tick").

Delivery tab (rewrite):

  • Big sticky search bar at top: customer name / order ID / email — narrows live.
  • Filter chips: รอส่งมอบ (default) / มีปัญหาค้าง / เสร็จสิ้นวันนี้ / ทั้งหมด.
  • Each row leads with the buyer's avatar + name (order ID secondary).
  • Progress pill with mini bar (e.g. "2/3").
  • No more prompt() popups: tick auto-fills recipient = buyer_label; pencil icon reveals an inline override input.
  • Issue button opens an inline form (type dropdown + note input + save), not a window.prompt chain.
  • When all items ticked → inline green banner with "ปิดคำสั่งซื้อ" button (no confirm).

Stock tab (new):

  • Search by product name; per-product card with thumb + name + total-stock pill + status select.
  • Inline size × color grid with −/+ steppers + direct input.
  • Cells colour-coded: red is-zero, yellow is-low (≤3), green is-ok, grey is-unset (empty).
  • Per-card "บันทึก" + "ยกเลิก"; dirty card highlighted yellow.
  • PATCH only stock_matrix + stock_status — image stays untouched.

Not in this round (deferred) ​

  • Discord/email notification when batch published (could reuse existing GAS notify actions).
  • Stock auto-decrement on order placement (currently admin updates by hand — fine for low volume).
  • Product image multi-shot gallery (today: one image_url).
  • Customer-facing pickup-record badge in "คำสั่งซื้อของฉัน" — would let buyer see "marked picked up by admin on 28 พ.ค." inline.

Branches ​

main at 3fc7cd4 (PR #7 merge). refactor/modular HEAD at 8459c65 — mobile/iPad FAB fix. Working tree clean, everything pushed. Build

  • tests green (26/26). Branch ruleset main-protect active — direct push to main requires Bypass list membership.
  • main → samomdkkuweb.pages.dev (production)
  • refactor/modular → refactorsamomdkkuweb.pages.dev (preview)

Previous big merge ​

refactor/modular was merged to main (d91a32a) as the Supabase cutover. Two conflicts resolved: .gitignore (kept both branches' rules) and index.html (took the slim refactor version over main's 2700-line monolith). functions/api/submit.js deleted — refactor talks to Supabase directly.

Phase 1.x — Project Tracking UX polish round (2026-05-26 session) ​

Took the initial project-tracking ship (c8584e9) through ~7 iterations of UX feedback in one session. Final shape locked in. Commits in this session (newest first):

CommitWhat
8459c65Mobile/iPad — adaptive FAB (folder-plus on grid, file-plus inside a project) + เพิ่มไฟล์ promoted to primary-soft. FAB now visible up to lg (covers iPad portrait/landscape)
b3c9bfdBell notifications — proper kind per action (file_added, resent separated from file_replaced / sent); 60s → 20s poll; bell refresh on visibilitychange + shown.bs.tab
180ccc7File-level "ใหม่"/"แทนที่ใหม่" pills + orange row background on files uploaded after viewer's last action. Skipped for VPA (they uploaded them)
a3078f6VPA ส่งใหม่อีกครั้ง button on returned docs (status → sent + clears return_reason + notifies uni). "เปลี่ยนแปลงล่าสุด" banner at top of expand listing other-side actions since viewer's last move. Default labels: "พี่นิค" → "เจ้าหน้าที่" (settings.uni_staff_label override still wins). Owner pill dropped from doc card head (redundant with status pill)
c85d208Card simplification — one big attention badge per role: "X ใหม่" (uni) / "X ตีกลับ" (vp). Dropped six per-status mini-chips and the project-status pill from the card head
81a389aTwo-level drill-down (Drive/Outlook/Notion pattern). Final IA. Level 1 = project grid; Level 2 = project detail with breadcrumb back, project header, list of doc cards, click to expand. Replaces the table approach below
35145b5(superseded) Spreadsheet/table inbox — flat table of all docs with group-by toggle. Felt "too messy" per user, replaced 50 min later by 81a389a
f3245d1Doc-header chevron toggle + navbar active-pill green-on-green text fix

Final UX shape ​

Level 1 — project grid:

  • Toolbar: search + 4 filter chips (ของฉัน / รออีกฝ่าย / เสร็จสิ้น / ทั้งหมด) with per-bucket counts. Buckets computed via projectBucket(p, role).
  • Each card: folder icon + name + id + clamped description + one attention badge (orange "X ใหม่" for uni, red "X ตีกลับ" for VPA) + "X หนังสือ" + relative time. Left-border colour encodes bucket.
  • Mobile FAB bottom-right: green circle, bi-folder-plus icon → opens create-project modal. VPA + dev only.

Level 2 — project detail:

  • Breadcrumb back ("← หนังสือโครงการทั้งหมด")
  • Project header with id/date, name, description, status pill, action row (เพิ่มหนังสือ / status menu / delete / copy link — all VPA-gated)
  • Doc cards stacked vertically. Each card head: mine-dot + #seq + title/type + "อัปเดต" pill (when applicable) + status pill + time
    • chevron. Click → expands.

Expanded doc card:

  1. "เปลี่ยนแปลงล่าสุด" banner (orange for uni viewing a sent doc; red for VPA viewing a returned doc) listing other-side actions since viewer's last move
  2. 4-step stepper (ส่งแล้ว → รับเรื่อง → ดำเนินการ → เสร็จสิ้น) with "ตีกลับ" overlay on step 0 if returned, or grayed/strikethrough if cancelled
  3. doc.note (if present)
  4. Files block — each file row shows "ใหม่" (orange pill) or "แทนที่ใหม่" (deeper amber) if uploaded after the viewer's last action. VPA gets a green "เพิ่มไฟล์" button here.
  5. Action row — role-gated buttons. VPA on returned doc gets the green "ส่งใหม่อีกครั้ง" button.
  6. Timeline (collapsible)

Mobile FAB inside Level 2 flips to bi-file-earmark-plus → adds doc to current project. Same FAB element, adaptive icon + aria.

What was preserved from the original ship ​

  • Hash routing: #projects / #projects/PRJ-… / #projects/PRJ-…/doc/DOC-…
  • All notify pipelines (Discord webhook, GAS MailApp, in-app bell)
  • All action handlers (status, return, comment, delete, add files, replace file). Notification recipients: uni gets vp's actions, vp gets uni's actions — you don't see your own actions in your own bell
  • DB schema, RLS policies, GAS deployment — untouched

Manual verification pending (next session) ​

Build + tests are green; I did not click through the running app this session. To verify on refactorsamomdkkuweb.pages.dev:

  1. Two sessions needed: VPA in one browser, sastaff in incognito (notifications go to the other side; you can't see your own in your own bell)
  2. Mobile/iPad: FAB visible bottom-right on the grid; tap → create project. Drill into a project → FAB flips to file-plus icon → tap → add doc to this project. The green "เพิ่มไฟล์" button inside an expanded doc should now be obvious in the files panel.
  3. Resend flow: VPA sends → sastaff ตีกลับ with reason → VPA opens → sees red "เจ้าหน้าที่ตีกลับ" banner → clicks green "ส่งใหม่อีกครั้ง" → enters change summary → sastaff's bell pops within 20s (or instantly if sastaff focuses the tab)
  4. File highlights: VPA adds/replaces a file on a doc sastaff is working on → sastaff opens → file row has orange background + pill
  5. Deep links still work: open #projects/PRJ-xxx/doc/DOC-yyy in a fresh tab → drills in to Level 2 with that doc expanded

Caveats / open questions ​

  • The Drive folder Projects/ allow-list in GAS uses uploadProjectFile. GAS redeploy already happened in the original ship — no further redeploy needed for this session's frontend-only changes.
  • settings.uni_staff_label in project_settings still overrides the "เจ้าหน้าที่" default — if a deployment wants a specific person's name, set it in the manage screen.
  • Phase 2 candidates list below — none implemented this session.

Phase 2 candidates — discussed but not built ​

Brief shortlist of workflow improvements from end-of-session brainstorm, ordered by ROI (drop these into a future session by name):

  1. Inline comment thread — replace native prompt() with a real reply box + chat-bubble thread at bottom of expand. Comments are the most-frequent interaction and the worst UX right now. (M effort)
  2. Drag-and-drop file upload — drop onto the files panel uploads with per-file progress. Same on the send modal. (M effort)
  3. Undo toast for status changes — 8s grace period before commit + notification fires. Saves face-palms on misclicks. (M effort)
  4. Due date column on project_documents (nullable). VPA sets it on send. Overdue red flag on the card. Needs 1-line migration. (M-L effort)
  5. Auto-create project when sending the first doc (combo box "เลือก/ สร้างโครงการ"). Currently 2-step. (S-M effort)
  6. Inline PDF/image preview in expand (Drive iframe). Today every file opens in a new tab. (M-L effort)

Skipped: bulk actions, "read but not acting yet" state, fuzzy search, mobile push, reminder/nudge — all premature for current volume.

Original Project tracking module (2026-05-26 — pre-polish ship) ​

Brand-new workflow between SAMO VP-Administration (sender) and a single designated university officer "พี่นิค" (receiver). Each "โครงการ" (project) contains one or more "หนังสือ" (documents); each document has N attached files (Word/PDF/Excel/etc.) on Drive. Sender can send, edit, replace files (non-destructive — old versions kept), cancel, or delete. Receiver can mark received → in_progress → completed, return for fixes, or comment. Notifications fan out to in-app bell + email (uni) and in-app bell + Discord (vp).

New roles (CHECK constraints in 0005): vp_admin (samomdkkuvpa) + uni_staff (sastaff / weak pw — «redacted, still live 2026-08-17»). Both are seat-style, mirroring samomdkkupr / samomdkkushop. current_user_is_project_actor() helper gates RLS.

New files:

  • supabase/migrations/0005_project_tracking_schema.sql — six tables (project_doc_types, projects, project_documents, project_files, project_notifications, project_settings) + RLS + role expansion + 4 seeded doc types.
  • supabase/migrations/0006_seed_project_accounts.sql — reserves the two usernames.
  • src/js/projects/{data,api,uploads,notify,index,inbox,send,manage,notifications}.js — feature lives in one folder, lazy-loaded on first tab show.
  • src/html/tab-projects.html, modal-project-send.html, offcanvas-project-notify.html.
  • src/css/projects.css — all rules scoped under .projects-tab (plus .nav-projects-bell scoped to .samo-navbar).

Edited files:

  • appscript/prform.gs — three new actions: uploadProjectFile (allow-listed to Projects/...), notifyProjectEmail (MailApp), notifyProjectDiscord (webhook URL from Script Properties PROJECT_DISCORD_WEBHOOK_URL). GAS redeploy required — see skills/deploy-gas.md.
  • src/html/navbar.html — added "หนังสือโครงการ" pill (desktop + mobile) and a bell icon in the auth area, both role-gated.
  • index.html — included the new partials.
  • src/main.css — @import './css/projects.css';.
  • src/js/main.js — import { initProjects } …; initProjects();, added vp_admin + uni_staff to roleLabel / roleBadgeClass.
  • src/js/auth.js — added samomdkkuvpa and sastaff to the reserved-usernames list (frontend mirror of 0006).
  • README.md "Key features" + roles list, docs/CONTEXT.md request flow, module map, schema section, migrations list.

Drive layout (created lazily on first upload, allow-listed to Projects/...):

My Drive/
├── PR_Submissions/                  ← unchanged
├── SAMO_Shop/...                    ← unchanged
└── Projects/
    └── PRJ-2605-0001_<safe-name>/
        └── DOC-260526-1430-XXXX_<type>/
            └── <file>.pdf

Hash routing (new behaviour in main.js / projects/index.js):

  • #projects — open the tab
  • #projects/PRJ-2605-0001 — open + auto-open that project
  • #projects/PRJ-2605-0001/doc/DOC-… — open + jump to that doc
  • A "คัดลอกลิงก์" button on every project detail head exposes the URL.

Manual steps to ship:

  1. Apply 0005_project_tracking_schema.sql + 0006_seed_project_accounts.sql in the Supabase SQL editor (in that order).
  2. Supabase Dashboard → Authentication → Add user:
    • samomdkkuvpa@samomdkku.app (pick a strong password — you'll use it)
    • sastaff@samomdkku.app with a weak password «redacted» Then run:
    sql
    update public.users set role='vp_admin'  where email='samomdkkuvpa@samomdkku.app';
    update public.users set role='uni_staff' where email='sastaff@samomdkku.app';
  3. In the prform GAS project → Project Settings → Script Properties, add PROJECT_DISCORD_WEBHOOK_URL = (the notify-samodocument webhook URL — given in chat; do NOT commit it).
  4. Redeploy prform GAS so the three new actions go live (see skills/deploy-gas.md).
  5. Sign in as samomdkkuvpa → "หนังสือโครงการ" tab → "การตั้งค่า" sub-tab → fill in p'nick's real email + adjust labels if needed.
  6. Smoke test: create a test project, send a doc with 1 file, sign in as sastaff in another browser/incognito, mark received → check Discord channel for the webhook ping + check VP-Admin's in-app bell.

Security note: the Discord webhook URL was exposed once in chat. Rotate it after smoke testing (Discord channel → Integrations → Webhooks → Regenerate) and update the GAS Script Property.

Not in scope this round (deferred to Phase 2 UI pass):

  • Holistic nav/IA refresh across the whole portal.
  • "My bookmarks/favorites" personal home panel for staff.
  • Real-time updates (uses refetch-on-open like the rest of the portal).
  • Mobile push / browser notifications.

Previously working — SAMO Shop feature (2026-05-26) ​

Ported the Claude Design SAMO Shop handoff bundle into the portal as a new tab + admin section. Vanilla JS + Bootstrap (matches the rest of the codebase), real Supabase backend, slip + product images uploaded to organised Drive folders via a new GAS action.

New files:

  • supabase/migrations/0003_samoshop_schema.sql — shop_products, shop_orders, shop_order_items, shop_pickup_batches, shop_settings; RLS policies; new shop_admin role; helper current_user_is_shop_admin().
  • supabase/migrations/0004_seed_shop_admin.sql — reserves samomdkkushop username (mirrors the 0002 pattern).
  • src/js/shop/{data,api,state,uploads,products,cart,checkout,orders,admin,index}.js — feature lives in one folder, lazy-loaded on first tab show.
  • src/html/tab-shop.html, modal-shop-product.html, offcanvas-shop-cart.html, modal-shop-order-detail.html.
  • src/css/shop.css — all rules scoped under .shop-tab.

Edited files:

  • appscript/prform.gs — new uploadShopFile action with folderPath param, walks/creates nested folders under My Drive, allow-listed to SAMO_Shop/.... GAS redeploy required — see skills/deploy-gas.md.
  • src/html/tab-admin.html — added SAMO Shop landing card + #adminShopSection (orders / verify / batches / products / QR).
  • src/html/navbar.html — added "ร้านค้า" pill (desktop + mobile).
  • index.html — included the new partials.
  • src/main.css — @import './css/shop.css';.
  • src/js/main.js — import { initShop, openShopAdmin } …; broadened isStaffRole to include shop_admin; admin auto-route handles shop_admin; openAdminSection('shop') calls openShopAdmin().
  • src/js/auth.js — added samomdkkushop to reserved usernames list.
  • README.md "Key features" + docs/CONTEXT.md (architecture, schema, RLS, Drive folder layout).

Drive layout (created lazily on first upload):

My Drive/
├── PR_Submissions/                 ← unchanged
└── SAMO_Shop/
    ├── Slips/YYYY-MM/<buyerId>_<ts>.jpg
    ├── Products/<productId>/<name>_<ts>.jpg
    └── QR/promptpay_<ts>.png

Manual steps to ship:

  1. Apply 0003_samoshop_schema.sql + 0004_seed_shop_admin.sql in the Supabase SQL editor (in that order).
  2. Create samomdkkushop in Supabase Dashboard → Authentication → Add user (synthetic email samomdkkushop@samomdkku.app), then update public.users set role='shop_admin' where email='samomdkkushop@samomdkku.app';.
  3. Redeploy the prform GAS project so uploadShopFile is live (see skills/deploy-gas.md).
  4. Sign in as samomdkkushop → Admin → SAMO Shop → set PromptPay name, id, instructions, and upload a QR image. Add a few products. Then smoke a guest flow (browse → add to cart → checkout → upload slip → appears in admin Verify queue).

Not in scope this round:

  • Discord notification on new order — easy to add later via the existing GAS notifyPROnly shape.
  • Real PromptPay EMVCo dynamic QR — admin uploads a static PNG instead; cheaper to maintain and matches the design.

Most recent merge ​

PR #9 (ui/font+color, by Kita) → refactor/modular as squash commit b4d7048 on 2026-05-25. Branch carried 11 iterative WIP commits; collapsed into one entry. Two best-practice passes were layered on top of her work before merge:

  • Critical: scoped the new .dropdown-menu opacity/visibility rule to #toolsDropdown/#aboutDropdown only. The original blanket selector hid the signed-in user-profile dropdown (still on Bootstrap's .show class which doesn't touch opacity).
  • Restored the font fallback chain so the body stays readable if Google Fonts is blocked: "Noto Sans Thai", "Prompt", system-ui, -apple-system, "Segoe UI", sans-serif.
  • De-duplicated --dept-hover-media (was identical to --dept-hover-external); media now gets #176581.
  • Introduced --vs-accent: #2C8F8A token; replaced three hardcoded hex copies in navbar.html + tab-home.html (VS keeps its teal identity even though --dept-quality shifted to a light green).
  • Fixed yellow-on-white contrast on .policy-category-header.creative — header text and count badge now use var(--brand-primary) on yellow.
  • Closed two missing ; in tab-home.html dept-card style attrs.
  • Removed dead aboutOpened / toolsOpened refs from resetDropdownStates() (never declared anywhere).
  • Trailing newlines on src/css/navbar.css and src/html/footer.html.

Second pass on the same branch — closed the items I previously left in place:

  • Deleted ~100 lines of bespoke dropdown JS in main.js (the show-dropdown class, closeAllDropdowns / resetDropdownStates, the about/tools click+touch handlers, the global outside-click handler, the DOMContentLoaded #pills-tab handler). The dropdowns now rely on Bootstrap's native .show class. The existing shown.bs.tab handler was upgraded to also strip .show from the dropdown-toggle (Bootstrap leaves it attached after a tab-JS-driven activation) and to clear the .active we set on #aboutDropdown when a non-about tab takes over.
  • goToAbout is back to its compact pre-PR form plus an explicit .active add on #aboutDropdown (because #pills-about-tab is hidden — Bootstrap can't visibly mark it).
  • Replaced .show-dropdown selectors in navbar.css with .show; restored .dropdown-toggle.show to the green-pill highlight rule so the trigger lights up while the menu is open. Fade is now a simple opacity transition on .show, scoped to the two managed menus only.
  • Moved per-tab shadow tokens (--form-shadow, --btn-shadow, --btn-hover-shadow) out of inline style="" and into scoped CSS in base.css under .vs-tab and .an-tab. forms.css carries the pink defaults via var(..., default) fallback, so the PR form (no tab class) renders the default without any inline override.
  • Removed the hardcoded 5 ข้อ / 4 ข้อ / 3 ข้อ policy badges + their CSS — counts were drift-prone and the numbered 01, 02, 03… prefix already shows the size implicitly.
  • Fixed a stray </div> near the end of the policy section in tab-about.html and lifted the <h3> policy title to <h2> to match the other about-sections' heading level.
  • Declared .about-hero-title { font-weight: 700 } instead of 800 — only weights 300–700 are loaded from Google Fonts, so 800 was silently falling back.
  • Consolidated the duplicate .policy-ordered-list rule in cards.css (the PR had two — declarations and counter-reset separated).

New about-page copy + 3-card 3C mission + policy section content are Kita's authored decisions and are accepted as-is.

Previously: The multi-project engine refactor proposed in docs/PROJECT-ARCHITECTURE.md is deferred — the user wants readable/maintainable improvements opportunistically (as we touch each module) rather than a multi-week planned refactor. The proposal doc stays as future reference.

Last small feature: delete-announcement button (modal-announcement.html

  • announcements.js deleteCurrentAnnouncement, RLS-gated, dbRest with return=representation + length check).

Collaboration scaffold: added CONTRIBUTING.md (branch model, touch-zone table for what a colleague can self-merge vs. what needs review, hard "don'ts" mirrored from mistakes.md). README + CLAUDE.md cross-link to it.

Unit tests (Vitest, 26 tests across src/js/utils.test.js and src/js/uploads.test.js) cover the security-critical pure helpers: escHtml, safeUrl, convertDriveUrl, formatThaiDate, decodeJwtResponse. npm test now runs in CI before npm run build. ~150 ms total. No DOM mocking, no network — pure functions only.

Most recent change: second audit pass closed XSS class across ticket renderers + dead-code admin auto-routing bug. See 2nd audit row below.

  1. Closed six RLS-silent-success sites + announcement button label + VS ticket-ID collision + fragile selector.
  2. Cleanup pass: partial-upload state in error message, fileInput.value='' after reset (latent), decodeJwtResponse input guards, escHtml helper in utils.js applied to announcement renderers (title/dept/snippet only — post.content stays raw Quill HTML), two stale "sendBeacon" comments, one unused import.
  3. Dead-code removal: deleted supabase/functions/notify-pr/ and notify-vs/ (~300 LOC of Deno code that was returning 502 — Discord stays on GAS by design now). Trimmed 8 stale references across docs and agent rules; net -411 LOC.
  4. Migration-tool removal: deleted tools/migrate-from-sheets.mjs (529 LOC), sheetexample/ (~800 KB student data dumps; already gitignored so never on GitHub), skills/migrate-data.md, skills/recover-ticket.md. Removed npm run migrate from package.json + the unused dotenv dep. Updated 6 doc files to remove the now-dead references.

Recent fixes (latest first, last ~10 commits) ​

CommitWhat
(this commit)2nd audit pass: close XSS class across 6 ticket renderers (escHtml + safeUrl applied), fix admin auto-route reading stale localStorage('samoUser'), fix convertDriveUrl regex on no-trailing-slash URLs, consolidate the duplicate escapeHtml helper in pr-staff into utils.js, strip one more stale "sendBeacon" comment
f309955Remove one-shot migration tool: tools/migrate-from-sheets.mjs, sheetexample/, two skills, dotenv dep, npm run migrate, 11 references
49d4ca1Remove dead Edge Function source (notify-pr, notify-vs) + 8 stale doc references. Discord stays on GAS by design
a91fa17Audit cleanup pass: partial-upload state in pr-form error msg; fileInput.value='' after reset; decodeJwtResponse guards; escHtml helper + applied to announcement renderers; stale comments; unused import
6a8193eAudit pass: close 6 RLS-silent-success sites (pr-staff status/delete/agents, vs-staff status, vs-tracking remarks, auth.setDepartment); fix announcement publish-btn label after edit; VS ticket-ID collision; selector
acc3ef1Docs pass 2: rewrite stale README.md, add Developer workflows section to docs/CONTEXT.md, add conditional rule 4 to CLAUDE.md auto-update loop
ca20e10Memory system: CLAUDE.md router + STATE.md + .claude/rules/ + skills/ + docs/CONTEXT.md + CI build
edaacc1Sort PR/VS tickets by timestamp (not created_at) — avoids needing a backfill
5df7f65Migrate script writes created_at from CSV timestamp (defense in depth)
92c039bGate auth-subscriber side-effects (showAdminLanding, modal close, VS form autofill) on real transitions only — fixes "kanban resets when switching tabs"
4779c88Migrate other_platforms + other_platform_reason (silently dropped, CSV cols 21/22 have empty headers)
074d653Migrate assignees from CSV col 20 via positional _raw[20] access
5493c11THE big one — wrap onAuthStateChange body in setTimeout(0) to escape supabase-js auth-lock deadlock (issue #762)
d97756fAdd dbRest() raw-fetch helper in db.js; convert PR tracking calls
58d1eadBypass supabase-js for PR/VS inserts using raw fetch + AbortController

Open / deferred ​

  • Phase 4 file storage: deliberately staying on Drive (2 TB) instead of Supabase Storage (1 GB free tier). Documented in docs/SUPABASE-MIGRATION.md.

Known caveats ​

  • Discord notifications travel through GAS notifyPROnly / notifyVSOnly / notifyVSConsult actions. Both prod GAS deployments must have the slim appscript/*.gs (104 + 154 lines) code live.
  • Drive uploads also go through GAS uploadPRFile. Same redeploy requirement.
  • Synthetic email domain is samomdkku.app (NOT .local). Supabase Auth rejects RFC-reserved TLDs. Don't switch back.
  • Supabase auto-refresh is disabled in db.js; a 25-min setInterval calls refreshSession() instead. Don't re-enable autoRefreshToken without reading .claude/rules/mistakes.md first.

Reproducible smoke tests ​

After any deploy:

  1. Sign in with Google + a fresh password account
  2. Submit a PR ticket — Discord pings, row in Supabase
  3. Submit a second PR ticket without reloading — must succeed (regression test for the deadlock)
  4. Submit a VS ticket — Discord pings target dept
  5. Admin → PR Management → kanban shows tickets in correct chrono order, dept filter works
  6. Edit an announcement (as samomdkkupr or samomdkkudev) — changes persist

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