ธีม
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.
| Migration | Purpose | Status |
|---|---|---|
| 0008–0015 | (prior session — applied) | applied |
| 0016_current_user_dept_helper.sql | security-definer current_user_dept() + vs_tickets policies | applied |
| 0017_announcement_order.sql | display_order column for drag-reorder | applied |
| 0018_shop_order_status_extras.sql | widens shop_orders.status check to include slip_mismatch / refund_pending / refunded / no_show | applied |
| 0019_shop_banners.sql | new shop_banners table + RLS | applied |
| 0020_shop_orders_buyer_slip_reupload.sql | adds slip_mismatch to buyer-update RLS | applied |
| 0021_guest_ticket_lookup_rpcs.sql | get_vs_ticket_by_id + get_pr_ticket_by_id RPCs | applied |
| 0022_shop_orders_exchange_status.sql | adds 'exchange' to shop_orders.status check | applied |
| 0023_shop_product_code.sql | adds shop_products.code (backfill SH-derived) — drives <CODE>NNNN order ids | ❌ pending |
| 0024_shop_product_production_status.sql | shop_products.production_status + apply_product_production_status RPC (cascades on product-status toggle) | ❌ pending |
| 0025_shop_orders_paid_cascade.sql | BEFORE-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
buildCurrentUseruses 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
tonecolour). - 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:
modalActionsnapshotsorderIdto 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 todisplay:table-cellso 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_newproducts, 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_mismatchso 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.writeTextwithexecCommand('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- Gridin 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:
100dvhheight + 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"withbi-box-arrow-up-right.
- Public tools launcher (Thai+English data-name for
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, noon 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
- Visit
samomdkkuweb.pages.dev(prod) andrefactorsamomdkkuweb.pages.dev(preview). Both return 200 at/,/pr,/vssound,/shop,/tools,/about,/news,/news/{id},/admin/. - 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.
- Sign in as a staff account on mobile Android Chrome:
- Type username + password, tap "เข้าสู่ระบบ". Modal closes, avatar appears. No more "spinner returns, no login".
- /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).
- After 0023 applied: place a new order; new id format
<CODE><NNNN>. - 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#announcementsGridand#emptyStateon its first line — those only exist on the public site. On admin the TypeError was swallowed bytryCreatorDeepLink's catch,globalAnnouncementsstayed empty, andeditAnnouncement(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: coverstill applies, but now the author picks the framing. - Featured card frame at 16:9 (was
4/3on desktop) so a 1600×900 upload renders edge-to-edge on the home featured card. - Delete that actually deletes: split
deleteCurrentAnnouncementintodeleteAnnouncement(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_orderPATCHes (top = highest int, descending). - Migration 0017_announcement_order.sql adds the
display_order intcolumn + index. Sort changed todisplay_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)
| Migration | Purpose | Status |
|---|---|---|
| 0008_announcements_excerpt.sql | excerpt column | applied (user confirmed) |
| 0016_current_user_dept_helper.sql | current_user_dept() security-definer helper + repoint vs_tickets policies | ❌ pending |
| 0017_announcement_order.sql | display_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.
Mobile sign-out + external-link surface (2026-05-29)
Mobile sign-out + external-link surface (2026-05-29)
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-rightinstead ofbi-arrow-up-right/bi-arrow-right. - Sidebar sign-out visibility is now safe-area-aware; the workspace sidebar uses
100dvh(with100vhfallback) 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-itemtext-decoration: none (anchor variants for external links)..workspace-side-item-exttrailing external-arrow icon style; also hidden in collapsed (icon-only) sidebar mode alongside the label spans.
src/css/launcher.css—.launcher-tooltext-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).
- Admin Dashboard link:
src/html/tab-tools.html— two new<a class="launcher-tool">cards in the public "สำหรับทุกคน" section, bothtarget="_blank". data-name covers Thai + English search terms so/-search finds them.admin/index.html— new "ลิงก์ภายนอก" sidebar section with the two external-link items. Nodata-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 inregisterWithPasswordwas 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.samomdkkuradiologybefore the admin seeded that VP. Switched to a prefix check onsamomdkku*- the legacy
sastaffliteral.
- the legacy
supabase/migrations/0016_current_user_dept_helper.sql(new) — addedpublic.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 onusers_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 deaddata-role-onlyattributes on the 5 sidebar buttons. The JS path that reads them (admin-main.js:419) explicitly skips elements that also havedata-admin-side, so those attributes never did anything but mislead future readers.src/js/db.js— 25-minsetInterval(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) isfor alland lets any user withpermissions[] @> '{pr}'rewrite the single-row global agent roster. Today onlysamomdkkudigitalhaspr— fine — but if a VP is grantedprfor read-only PR visibility (likely-future ask), they can wipe the roster too. Splitting intopr(read) andpr_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/_redirectsroutes/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 byusers.department. Extras viausers.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)
| Migration | Purpose | Status (per user) |
|---|---|---|
| 0009_vs_owner_reply.sql | VS owner can reply to own ticket | applied (vs_tickets_update_owner present) |
| 0010_vp_accounts_permissions.sql | users.permissions + per-dept VP RLS + 9 reservations | applied |
| 0011_vp_corrections.sql | media→mdi rename + corrected UPDATE block (final perms) | applied (UPDATE block run) |
| 0012_vs_delete.sql | DELETE policy for vs_staff/dev | superseded by 0015 (see note) |
| 0013_vs_vp_send_back_to_se.sql | WITH CHECK fix: VP can โอนคืน SE | likely applied (policy redefined; verify body) |
| 0014_permission_aware_rls.sql | pr_tickets/pr_agents/announcements/shop_* honor permissions[] | applied |
| 0015_vs_delete_for_all_vs_staff.sql | DELETE extends to VPs (own dept) + 'vs' perm | applied |
| 0016_current_user_dept_helper.sql | current_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»:
| ฝ่าย | Username | Password | Extra 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-togglecleaned out.
Real bugs found, NOT fixed (low impact, defer):
sessionStorage.vsViewPickedis now dead key (view toggle gone) but a stale entry in some users' browsers does nothing. Cleanup:delete sessionStorage.vsViewPickedonce on next admin entry, or just leave (storage quota is tiny).src/html/modal-announcement.htmlexists 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-mantraetc. 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
- Read this STATE.md top section first.
- Read
.claude/rules/mistakes.mdif touching auth.js / db.js / RLS. - Verify migration 0009 + 0012 + 0013 are applied (the SQL block above).
- Check
refactorsamomdkkuweb.pages.devis reachable and the routes/,/pr,/vssound,/news,/admin/all serve 200. - 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
- Sign in as
samomdkkuvssound→ confirm Kanban (ทุกฝ่าย) view shows all VPs' tickets with dept badges.
Navbar slim + tools launcher + premium UI pass (2026-05-28)
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.
Navbar — slimmed and never grows
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-tabfor footer + mobile-offcanvas links.เครื่องมือdropdown deleted — now a top-level pill that opens the new tools tab (#pills-tools).navbar-expand-mdinstead of-lgso 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.jsboth 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.
Footer (src/html/footer.html)
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-motionguard 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) — launchersrc/html/tab-home.html— tools section gets a "ดูเครื่องมือทั้งหมด" linkindex.html— includestab-tools.html; inline critical CSS updated to match new navbar glass looksrc/css/navbar.css— full rewrite using tokenssrc/css/launcher.css(new) — launcher stylessrc/css/footer.css(new) — footer stylessrc/css/base.css— tokens + new body gradient + reduced-motion guardsrc/main.css—@importlauncher + footer CSSsrc/js/main.js— launcher search/filter/role-gating; mobile-bell mirror; mobile-user strip wiring (newmobileUserPic/Name/DeptIDs +mobileSignOutItem);goToAboutno longer touches deletedaboutDropdown;shown.bs.tabhandler comments updatedsrc/js/projects/index.js— togglenavProjectsBellMobilealongside desktopsrc/js/projects/notifications.js— bind click + update count on both bells
Verification status
npm run build— greennpm 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 viamargin-left: autoon.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.
- Active section tracks scroll via IntersectionObserver in
- ประกาศทั้งหมด 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 theshown.bs.tabhandler 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, swappednav-center→nav-primary, updated mobile offcanvas with ประกาศ.src/css/navbar.css—.nav-primary(left-aligned),.samo-navbar-authgetsmargin-left: auto.src/html/tab-about.html— added.about-subnavblock.src/css/cards.css—.about-subnav,.about-subnav-link, mobile icon-only variant; bumpedscroll-margin-topto 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:
- Slim auth-CTA / welcome strip (no big hero)
- Featured story (1 large card, image left, eyebrow + title + excerpt right) — populated from the newest announcement
- News grid (up to 6 more cards in a clean grid, image top + meta)
- 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.
- Tools quick-grid (4 cards) — redundant with the navbar
- ฝ่ายในสโมสร moved to About tab — new
#about-departmentssection below นโยบาย, reuses the existing.dept-cardstyling. 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 cardrenderNewsCard(post)— secondary card; used by home grid AND archivepickCover(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-welcometo token-based, border-onlysrc/js/announcements.js—renderHomeAnnouncements()now emits featured+grid;loadAnnouncements()archive emits same news cards; shared helperspickCover/extractSnippet/formatEditorialDate/renderNewsFeatured/renderNewsCardsrc/js/main.js— removedwindow.scrollHomeAnnounce(carousel gone)src/html/tab-home.html— rewrite to editorial structuresrc/html/tab-announcements.html— rewrite to news-archive headersrc/html/tab-about.html— added ฝ่าย to sub-nav + new#about-departmentssection 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
excerptcolumn 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 texttopublic.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-articlereached via:- card click →
viewAnnouncement(id)activates tab + renders + pushes#article/{id}to the hash - direct URL →
handleArticleHash()runs on hashchange and afterloadAnnouncements()resolves
- card click →
- 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-motionalready 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()selectsexcerpt; maps toglobalAnnouncements.renderNewsFeatured(post)preferspost.excerptover 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()callscloseArticleView()then reloads.- POST uses
prefer: 'return=representation'so we get the new row id back and can auto-open it.
Cleanup:
modal-announcement.htmlinclude removed fromindex.html(file kept in tree as reference; safe to delete later).- Hidden
pills-article-tabbutton added to navbar.html for Bootstrap Tab API routing.
Manual steps to ship
- Apply
supabase/migrations/0008_announcements_excerpt.sqlin the Supabase SQL editor. - (No GAS redeploy.) (No env-var change.)
- 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.
- Reader: click any news card on home or archive → opens full-page article with sticky back bar, hero image, reading column. URL hash becomes
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 withexcerpt. On 400, retries withoutexcerptso 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__samoWarnedExcerptis set, stripsexcerptfrom 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-tabandpills-admin-tabbuttons 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/navAdminItemIDs 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 gatesrc/html/navbar.html— Admin/Projects out of top pills, into dropdown menu items; staff-section labels added (desktop + mobile); hidden tab buttons preservedsrc/css/navbar.css—.nav-user-section-label+.samo-offcanvas-section-labelsrc/js/main.js— toggle#navStaffSection/#mobileStaffSectionviaqueueMicrotaskafter both visibility paths resolvesrc/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.inputwithpublic: index.html+admin: admin/index.htmlhtmlPartialsplugin processes<include src="..." />in both entries- Single
npm run buildoutputsdist/index.htmlanddist/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 legacyopenAdminSection()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.htmldropped includes: tab-admin, tab-projects, tab-creator, modal-pr-staff, modal-agents, modal-vs-staff, modal-project-send, offcanvas-project-notifysrc/js/main.jsdropped 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.htmlincludes them inside its single canonical workspace shell tab-admin.html: dropped the inlineadminLandingcards (admin entry has its own canonical landing section); per-section "back to dashboard" buttons removed (sidebar drives navigation)tab-projects.html: kept the inline#projectsSubnavswitcher as the secondary in-pane switcher;setView()in projects/index.js now delegates bydata-projects-viewregardless 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-vitalsoundremoved (creator/admin/projects gone from public; vitalsound is now a visible pill).
Files touched in iteration 6
vite.config.js— multi-page inputadmin/index.html(new) — admin entry, workspace shell, boot/auth gates, includessrc/admin.css(new) — admin CSS bundle (base + cards + forms + modals + workspace + shop + projects)src/js/admin-main.js(new) — admin entry scriptindex.html— dropped admin/projects/creator includes + 5 admin-only modalssrc/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 buttonssrc/html/tab-admin.html— stripped workspace shell + tab-pane wrapper + landing cards + back-linkssrc/html/tab-projects.html— stripped workspace shell + tab-pane wrapper; restored inline subnav as secondary in-pane switchersrc/html/tab-creator.html— stripped tab-pane wrappersrc/html/tab-tools.html— dropped staff section + content section + their filter chipssrc/js/projects/index.js— broadersetView()selector + click delegation on#pills-projects
Manual steps to ship
- Apply migration
0008_announcements_excerpt.sql(still pending from iteration 4). - Cloudflare Pages should serve
dist/index.htmlat/anddist/admin/index.htmlat/admin/automatically. No build config change. - 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/#shopdeep-links to the shop pane- "กลับสู่หน้าหลัก" returns to
/ - Sign out from sidebar — returns to
/admin/showing the auth gate
- 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.htmlfile still insrc/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 tomdby 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
fitscolumn kept in DB but ignored in UI. - Presale → Preorder rename across labels (DB column
is_presalekept to avoid a backfill — only the UI text changed). - New
stock_statuscolumn on products:available|sold_out|production_closed. Storefront shows OOS ribbon + grays the card + disables Add-to-Cart. Independent ofis_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_fulljsonb[{date, hours}]lets each pickup date carry its own time window. Backfilled from legacydates[]+ sharedhoursby 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
readyorder, expand to per-item checklist. - Tick → prompts for recipient name, writes
shop_pickup_recordsrow (one perorder_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.
- One card per
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 viabatchDateEntries.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
- Apply
supabase/migrations/0007_shop_refactor.sql(Supabase SQL editor). - Existing products auto-migrate
source→md. Re-tag them (RT/MDI/Sittikao) via admin. - Existing pickup batches auto-build
dates_fullfromdates[]+ sharedhours. Open each in admin to add per-date times. - Smoke test:
- Shop tab: carousel scrolls; arrow disabled at ends; multi-batch banner renders if 2+ active.
- Mark a product
sold_outin 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.
- 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, yellowis-low (≤3), greenis-ok, greyis-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-protectactive — direct push tomainrequires 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):
| Commit | What |
|---|---|
8459c65 | Mobile/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) |
b3c9bfd | Bell notifications — proper kind per action (file_added, resent separated from file_replaced / sent); 60s → 20s poll; bell refresh on visibilitychange + shown.bs.tab |
180ccc7 | File-level "ใหม่"/"แทนที่ใหม่" pills + orange row background on files uploaded after viewer's last action. Skipped for VPA (they uploaded them) |
a3078f6 | VPA ส่งใหม่อีกครั้ง 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) |
c85d208 | Card 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 |
81a389a | Two-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 |
f3245d1 | Doc-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-plusicon → 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:
- "เปลี่ยนแปลงล่าสุด" banner (orange for uni viewing a
sentdoc; red for VPA viewing areturneddoc) listing other-side actions since viewer's last move - 4-step stepper (ส่งแล้ว → รับเรื่อง → ดำเนินการ → เสร็จสิ้น) with "ตีกลับ" overlay on step 0 if returned, or grayed/strikethrough if cancelled
- doc.note (if present)
- 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.
- Action row — role-gated buttons. VPA on
returneddoc gets the green "ส่งใหม่อีกครั้ง" button. - 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:
- 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)
- 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.
- 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)
- File highlights: VPA adds/replaces a file on a doc sastaff is working on → sastaff opens → file row has orange background + pill
- Deep links still work: open
#projects/PRJ-xxx/doc/DOC-yyyin a fresh tab → drills in to Level 2 with that doc expanded
Caveats / open questions
- The Drive folder
Projects/allow-list in GAS usesuploadProjectFile. GAS redeploy already happened in the original ship — no further redeploy needed for this session's frontend-only changes. settings.uni_staff_labelinproject_settingsstill 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):
- 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) - Drag-and-drop file upload — drop onto the files panel uploads with per-file progress. Same on the send modal. (M effort)
- Undo toast for status changes — 8s grace period before commit + notification fires. Saves face-palms on misclicks. (M effort)
- 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) - Auto-create project when sending the first doc (combo box "เลือก/ สร้างโครงการ"). Currently 2-step. (S-M effort)
- 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-bellscoped to.samo-navbar).
Edited files:
appscript/prform.gs— three new actions:uploadProjectFile(allow-listed toProjects/...),notifyProjectEmail(MailApp),notifyProjectDiscord(webhook URL from Script PropertiesPROJECT_DISCORD_WEBHOOK_URL). GAS redeploy required — seeskills/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();, addedvp_admin+uni_stafftoroleLabel/roleBadgeClass.src/js/auth.js— addedsamomdkkuvpaandsastaffto the reserved-usernames list (frontend mirror of 0006).README.md"Key features" + roles list,docs/CONTEXT.mdrequest 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>.pdfHash 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:
- Apply
0005_project_tracking_schema.sql+0006_seed_project_accounts.sqlin the Supabase SQL editor (in that order). - Supabase Dashboard → Authentication → Add user:
samomdkkuvpa@samomdkku.app(pick a strong password — you'll use it)sastaff@samomdkku.appwith a weak password «redacted» Then run:
sqlupdate public.users set role='vp_admin' where email='samomdkkuvpa@samomdkku.app'; update public.users set role='uni_staff' where email='sastaff@samomdkku.app'; - In the
prformGAS project → Project Settings → Script Properties, addPROJECT_DISCORD_WEBHOOK_URL= (thenotify-samodocumentwebhook URL — given in chat; do NOT commit it). - Redeploy
prformGAS so the three new actions go live (seeskills/deploy-gas.md). - Sign in as
samomdkkuvpa→ "หนังสือโครงการ" tab → "การตั้งค่า" sub-tab → fill in p'nick's real email + adjust labels if needed. - Smoke test: create a test project, send a doc with 1 file, sign in as
sastaffin 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; newshop_adminrole; helpercurrent_user_is_shop_admin().supabase/migrations/0004_seed_shop_admin.sql— reservessamomdkkushopusername (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— newuploadShopFileaction withfolderPathparam, walks/creates nested folders under My Drive, allow-listed toSAMO_Shop/.... GAS redeploy required — seeskills/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 } …; broadenedisStaffRoleto includeshop_admin; admin auto-route handlesshop_admin;openAdminSection('shop')callsopenShopAdmin().src/js/auth.js— addedsamomdkkushopto 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>.pngManual steps to ship:
- Apply
0003_samoshop_schema.sql+0004_seed_shop_admin.sqlin the Supabase SQL editor (in that order). - Create
samomdkkushopin Supabase Dashboard → Authentication → Add user (synthetic emailsamomdkkushop@samomdkku.app), thenupdate public.users set role='shop_admin' where email='samomdkkushop@samomdkku.app';. - Redeploy the
prformGAS project souploadShopFileis live (seeskills/deploy-gas.md). - 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
notifyPROnlyshape. - 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-menuopacity/visibility rule to#toolsDropdown/#aboutDropdownonly. The original blanket selector hid the signed-in user-profile dropdown (still on Bootstrap's.showclass 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: #2C8F8Atoken; replaced three hardcoded hex copies in navbar.html + tab-home.html (VS keeps its teal identity even though--dept-qualityshifted to a light green). - Fixed yellow-on-white contrast on
.policy-category-header.creative— header text and count badge now usevar(--brand-primary)on yellow. - Closed two missing
;in tab-home.html dept-card style attrs. - Removed dead
aboutOpened/toolsOpenedrefs fromresetDropdownStates()(never declared anywhere). - Trailing newlines on
src/css/navbar.cssandsrc/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(theshow-dropdownclass,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.showclass. The existingshown.bs.tabhandler was upgraded to also strip.showfrom the dropdown-toggle (Bootstrap leaves it attached after a tab-JS-driven activation) and to clear the.activewe set on#aboutDropdownwhen a non-about tab takes over. goToAboutis back to its compact pre-PR form plus an explicit.activeadd on#aboutDropdown(because#pills-about-tabis hidden — Bootstrap can't visibly mark it).- Replaced
.show-dropdownselectors innavbar.csswith.show; restored.dropdown-toggle.showto 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 inlinestyle=""and into scoped CSS inbase.cssunder.vs-taband.an-tab.forms.csscarries the pink defaults viavar(..., 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 numbered01, 02, 03…prefix already shows the size implicitly. - Fixed a stray
</div>near the end of the policy section intab-about.htmland lifted the<h3>policy title to<h2>to match the other about-sections' heading level. - Declared
.about-hero-title { font-weight: 700 }instead of800— only weights 300–700 are loaded from Google Fonts, so 800 was silently falling back. - Consolidated the duplicate
.policy-ordered-listrule incards.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.
- Closed six RLS-silent-success sites + announcement button label + VS ticket-ID collision + fragile selector.
- Cleanup pass: partial-upload state in error message,
fileInput.value=''after reset (latent),decodeJwtResponseinput guards,escHtmlhelper in utils.js applied to announcement renderers (title/dept/snippet only —post.contentstays raw Quill HTML), two stale "sendBeacon" comments, one unused import. - Dead-code removal: deleted
supabase/functions/notify-pr/andnotify-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. - 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. Removednpm run migratefrom package.json + the unuseddotenvdep. Updated 6 doc files to remove the now-dead references.
Recent fixes (latest first, last ~10 commits)
| Commit | What |
|---|---|
| (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 |
f309955 | Remove one-shot migration tool: tools/migrate-from-sheets.mjs, sheetexample/, two skills, dotenv dep, npm run migrate, 11 references |
49d4ca1 | Remove dead Edge Function source (notify-pr, notify-vs) + 8 stale doc references. Discord stays on GAS by design |
a91fa17 | Audit 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 |
6a8193e | Audit 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 |
acc3ef1 | Docs pass 2: rewrite stale README.md, add Developer workflows section to docs/CONTEXT.md, add conditional rule 4 to CLAUDE.md auto-update loop |
ca20e10 | Memory system: CLAUDE.md router + STATE.md + .claude/rules/ + skills/ + docs/CONTEXT.md + CI build |
edaacc1 | Sort PR/VS tickets by timestamp (not created_at) — avoids needing a backfill |
5df7f65 | Migrate script writes created_at from CSV timestamp (defense in depth) |
92c039b | Gate auth-subscriber side-effects (showAdminLanding, modal close, VS form autofill) on real transitions only — fixes "kanban resets when switching tabs" |
4779c88 | Migrate other_platforms + other_platform_reason (silently dropped, CSV cols 21/22 have empty headers) |
074d653 | Migrate assignees from CSV col 20 via positional _raw[20] access |
5493c11 | THE big one — wrap onAuthStateChange body in setTimeout(0) to escape supabase-js auth-lock deadlock (issue #762) |
d97756f | Add dbRest() raw-fetch helper in db.js; convert PR tracking calls |
58d1ead | Bypass 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/notifyVSConsultactions. Both prod GAS deployments must have the slimappscript/*.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-minsetIntervalcallsrefreshSession()instead. Don't re-enableautoRefreshTokenwithout reading.claude/rules/mistakes.mdfirst.
Reproducible smoke tests
After any deploy:
- Sign in with Google + a fresh password account
- Submit a PR ticket — Discord pings, row in Supabase
- Submit a second PR ticket without reloading — must succeed (regression test for the deadlock)
- Submit a VS ticket — Discord pings target dept
- Admin → PR Management → kanban shows tickets in correct chrono order, dept filter works
- Edit an announcement (as
samomdkkuprorsamomdkkudev) — changes persist