AAIVA Daily Updates pushed to production อัปเดตขึ้นโปรดักชัน
Open dashboard →เปิดแดชบอร์ด →
30 commits · 3e9cbf6 → 48b391d · custom RFM segments + multi-RFM membershipsRFM กำหนดเอง + ผู้เล่นอยู่หลายกลุ่ม
Cross-cutting ข้ามทุก slot

Changes that touch the whole site

การเปลี่ยนแปลงที่กระทบทั้งไซต์

  • Auto-reload on stale deploy — GitLab Pages forces Cache-Control: max-age=600 on every response and ignores <meta http-equiv> overrides, so operators were seeing cached HTML for up to 10 minutes after a deploy. The CI build now sed-injects $CI_COMMIT_SHA into a <meta name="build-version"> tag and writes a sibling public/version.txt. A small script at the top of <head> fetches that file with cache: 'no-store' on every page load; if the SHA differs, it location.replace()s with ?v=<sha> as a cache-buster so the browser fetches fresh HTML. sessionStorage guards against reload loops.
  • Roster row click resilience — wrapped the brittle inline onclick="loadPlayer(uid);switchView('profile')" in openPlayerFromRoster(uid) which switches the view first and wraps the load in try/catch. Previously if any earlier step in the loadPlayer pipeline threw, navigation silently failed and the row click looked dead.
  • Fix init crash on procedural players — the procedural-player IIFE was emitting p.games.mostPlayed and p.games.top: [], but renderPlayer reads p.games.mostType and p.games.top5.map(...). The undefined top5 threw and killed the entire init render chain, so the dashboard appeared blank until any tab click independently triggered a render via switchView. Only manifested when localStorage saved a procedural UID as the current player (default P-20481 is hand-crafted and unaffected). Two-sided fix: procedural IIFE now emits the canonical key names, and renderPlayer reads p.games defensively (mostType||mostPlayed, top5||top, fallbacks for missing fields).
  • Pre-load the segments view — reorder the init render chain so renderSegments + renderRoster fire BEFORE the heavier loadPlayer / renderPayouts work. Segments is the default landing tab (the only .view-content without a [hidden] attribute on load), so populating its grid + custom card first means operators see real content the moment the page paints instead of staring at empty cards while the hidden profile view fills.
  • โหลดอัตโนมัติเมื่อ deploy ใหม่ — GitLab Pages บังคับ Cache-Control: max-age=600 ทุก response และไม่สนใจ <meta http-equiv>, ทำให้ operator เห็น HTML ที่แคชไว้นานสุด 10 นาทีหลัง deploy · CI build ตอนนี้ใส่ $CI_COMMIT_SHA ลงใน meta tag build-version และเขียนไฟล์ public/version.txt คู่กัน · script เล็ก ๆ ใน <head> ดึงไฟล์นั้นแบบ cache: 'no-store' ทุกครั้งที่โหลดหน้า; ถ้า SHA ต่างกันจะ location.replace() ด้วย ?v=<sha> เป็น cache-buster เพื่อให้เบราว์เซอร์ดึง HTML ใหม่ · ใช้ sessionStorage ป้องกัน loop การ reload
  • คลิกแถว Roster ทนทาน — ห่อ inline onclick="loadPlayer(uid);switchView('profile')" เดิมไว้ใน openPlayerFromRoster(uid) ซึ่งสลับ view ก่อนแล้วห่อ load ใน try/catch · ก่อนหน้านี้ถ้า step ใน pipeline ของ loadPlayer โยน error การ navigate จะล้มเงียบ ๆ และคลิกแถวเหมือนไม่ทำงาน
  • แก้ init crash ของผู้เล่น procedural — IIFE ที่สร้างผู้เล่น procedural ส่งคีย์ p.games.mostPlayed และ p.games.top: [], แต่ renderPlayer อ่าน p.games.mostType และ p.games.top5.map(...) · top5 ที่เป็น undefined ทำให้ throw และหยุด chain การ render ทั้งหมดใน init · dashboard ดูเปล่า ๆ จนกว่าจะคลิก tab อะไรสักอย่างที่ trigger render ผ่าน switchView โดยอิสระ · เกิดเฉพาะเมื่อ localStorage บันทึก UID ของผู้เล่น procedural เป็น current (default P-20481 เป็น hand-crafted ไม่กระทบ) · แก้สองทาง: IIFE ส่งคีย์ตามมาตรฐาน และ renderPlayer อ่าน p.games แบบกันพลาด (mostType||mostPlayed, top5||top, fallback สำหรับฟิลด์ที่หาย)
  • โหลด segments ก่อน — เรียงใหม่ลำดับ render ใน init ให้ renderSegments + renderRoster ทำงานก่อน loadPlayer / renderPayouts ที่หนักกว่า · Segments เป็น tab landing เริ่มต้น (เป็น .view-content เดียวที่ไม่มี [hidden] ตอนโหลด), ดังนั้นการเติม grid + custom card ก่อนหมายความว่า operator เห็นเนื้อหาทันทีที่หน้า paint แทนที่จะมองการ์ดเปล่าระหว่างที่ view profile (ซ่อนอยู่) กำลังเติม
01

Custom RFM segments — operator-built groups beyond the 11 AIVA archetypes

RFM กำหนดเอง — กลุ่มที่ operator สร้างเอง นอกเหนือจาก 11 archetype ของ AIVA

A new dashed + Custom segment tile sits at the end of the Segmentation grid. Clicking opens a centered modal where the operator picks only two things: a name and ten threshold inputs that mirror the inputs to AIVA's player-ranking formula (aismsDesirability). The card's icon, colour, and recommended-action line are AIVA-generated at save time — operators don't choose them.

มี tile + Custom segment เส้นประที่ท้าย grid ของ Segmentation · กดเปิด modal จัดกลางที่ operator เลือก เพียงสองอย่าง: ชื่อและค่าขั้นต่ำ 10 ช่อง ที่สะท้อนอินพุตของสูตรจัดอันดับผู้เล่นของ AIVA (aismsDesirability) · ไอคอน, สี, และบรรทัด action บนการ์ดถูกสร้างอัตโนมัติโดย AIVA ตอนบันทึก — operator ไม่ต้องเลือก

The ten signals

สิบสัญญาณ

  • Min AIVA score (0–99) · Active within last N days · Min recent net loss (THB) · Min lifetime turnover (THB) · Min avg deposit size (THB) · Min frequency rating (%) · Min monetary rating (%) · Min loyalty rating (%) · Min engagement rating (%) · Max risk rating (%).
  • Nine floors and one ceiling (risk). Defaults match everyone (70 of 70 players); the live counter at the top updates as the operator tightens the criteria.
  • คะแนน AIVA ขั้นต่ำ (0–99) · ใช้งานภายใน N วันล่าสุด · ยอดเสียสุทธิล่าสุดขั้นต่ำ (บาท) · ยอดเล่นรวมต่ำสุด (บาท) · ขนาดฝากเฉลี่ยขั้นต่ำ (บาท) · คะแนนความถี่ขั้นต่ำ (%) · คะแนนการเงินขั้นต่ำ (%) · คะแนนความภักดีขั้นต่ำ (%) · คะแนนความผูกพันขั้นต่ำ (%) · คะแนนความเสี่ยงสูงสุด (%)
  • เป็นเกณฑ์ขั้นต่ำเก้าตัวและขั้นสูงสุดหนึ่งตัว (ความเสี่ยง) · ค่าเริ่มต้นจะตรงทุกคน (70 จาก 70 คน); ตัวนับล่าสุดบนหัวจะอัปเดตทันทีที่ operator เข้มงวดขึ้น

AIVA-generated chrome

หน้าตาที่ AIVA สร้างให้

  • autoGenIcon(name) — deterministic hash of the name picks from a curated emoji pool (🏆⭐💎🎯🚀💡🌟…).
  • autoGenColor(name) — same hash picks from the 8-colour palette RFM badges already use (pri / green / cyan / amber / red / purple / blue / gy).
  • autoGenAction(criteria) — compact summary of whichever criteria depart from defaults, e.g. score ≥ 70 · loss ≥ ฿20k · risk ≤ 50%. Capped at 80 chars with an ellipsis; falls back to "Custom RFM segment" when nothing is tightened.
  • All three are regenerated on every save so the card stays in sync with the current name + criteria.
  • autoGenIcon(name) — hash ของชื่อแบบ deterministic เลือกจาก pool emoji ที่คัดไว้ (🏆⭐💎🎯🚀💡🌟…)
  • autoGenColor(name) — hash เดียวกันเลือกจาก palette 8 สีที่ RFM badge ใช้อยู่แล้ว (pri / green / cyan / amber / red / purple / blue / gy)
  • autoGenAction(criteria) — สรุปย่อเกณฑ์ที่ไม่ใช่ค่าเริ่มต้น เช่น score ≥ 70 · loss ≥ ฿20k · risk ≤ 50% · ตัดที่ 80 ตัวอักษรด้วย ellipsis; ถ้าไม่ได้เข้มงวดเลยจะใช้ "Custom RFM segment"
  • ทั้งสามถูกสร้างใหม่ทุกครั้งที่บันทึก เพื่อให้การ์ดตรงกับชื่อ + เกณฑ์ปัจจุบัน

Persistence

การจัดเก็บ

  • localStorage key pps75_custom_segments. Shape: { id, name, createdAt, criteria, icon, color, action, manualIncludes, manualExcludes }.
  • Clicking a custom card filters the roster identically to an RFM card via state.rosterFilter = 'custom:<id>'; viewCustomSegment(id) sets the filter and switches view.
  • คีย์ใน localStorage: pps75_custom_segments · รูปร่าง: { id, name, createdAt, criteria, icon, color, action, manualIncludes, manualExcludes }
  • คลิก card กำหนดเองจะกรอง roster เหมือนคลิก RFM card ผ่าน state.rosterFilter = 'custom:<id>'; viewCustomSegment(id) ตั้งค่า filter และสลับ view
02

Multi-RFM memberships — players can sit in more than one archetype

สมาชิก RFM หลายกลุ่ม — ผู้เล่นอยู่ได้มากกว่าหนึ่ง archetype

AIVA's default is still one archetype per player. The operator can now extend membership: a player can belong to any number of RFM archetypes simultaneously (≥1 required). The primary stays in p.period30dBase.archetype — so the roster badge, profile head, and insight generation all keep their single-archetype display paths. Additional memberships go in p.extraArchetypes:[].

ค่าเริ่มต้นของ AIVA ยังเป็นหนึ่ง archetype ต่อผู้เล่น · ตอนนี้ operator ขยายสมาชิกได้: ผู้เล่นอยู่ใน RFM archetype ได้พร้อมกันหลายกลุ่ม (ต้องมีอย่างน้อย 1) · primary ยังอยู่ใน p.period30dBase.archetype — badge ใน roster, head ของ profile, การสร้าง insight ทุกอย่างยังใช้เส้นทาง archetype เดียวเหมือนเดิม · สมาชิกเพิ่มเติมอยู่ใน p.extraArchetypes:[]

  • playerArchetypes(p) returns [primary, ...extras]; playerInArchetype(p, arch) checks both. renderSegments, the roster filter, and aismsFilteredPlayers all walk the full membership set so a multi-tagged player counts in every segment they belong to (total segment population can exceed roster size).
  • Unchecking the primary while extras exist promotes the first extra to primary. Unchecking the last archetype is refused with a toast.
  • localStorage pps75_archetype_overrides. Legacy single-archetype string values are auto-promoted to single-element arrays on load. p.aivaArchetype is lazy-stored on the first override so the modal can tag AIVA's pick with a blue AIVA badge and the "revert to AIVA's pick" link collapses back to that single archetype.
  • playerArchetypes(p) คืน [primary, ...extras]; playerInArchetype(p, arch) ตรวจทั้งสอง · renderSegments, filter ของ roster, และ aismsFilteredPlayers เดินผ่านชุดสมาชิกทั้งหมด ผู้เล่นที่ถูกแท็กหลายกลุ่มจึงถูกนับในทุก segment ที่อยู่ (จำนวนรวมของทุก segment อาจมากกว่าจำนวนผู้เล่น)
  • ยกเลิกเช็ค primary ขณะมี extras จะเลื่อน extra แรกขึ้นเป็น primary · ยกเลิกเช็ค archetype สุดท้ายจะถูกปฏิเสธพร้อม toast
  • localStorage pps75_archetype_overrides · ค่า string archetype เดียวแบบเก่าจะถูกแปลงเป็น array หนึ่งสมาชิกตอนโหลด · p.aivaArchetype ถูกบันทึก lazy ตอนแก้ครั้งแรก เพื่อให้ modal ใส่ badge สีฟ้า AIVA ตรง archetype ที่ AIVA เลือก และลิงก์ "revert to AIVA's pick" คืนค่าเป็น archetype เดียวนั้นได้
03

Per-row + per-profile 🏷️ assignment modal

Modal มอบหมายด้วย 🏷️ ในแต่ละแถวและ profile

Every row in the Player Roster has a small 🏷️ button next to the username (with a count badge — e.g. 🏷️ 3 — when the player already belongs to N groups). The same icon appears in the player-profile action bar. Clicking either opens one centered modal with two checkbox sections:

ทุกแถวใน Player Roster มีปุ่ม 🏷️ เล็ก ๆ ติดอยู่ข้างชื่อผู้ใช้ (มี badge นับ — เช่น 🏷️ 3 — เมื่อผู้เล่นอยู่แล้ว N กลุ่ม) · ไอคอนเดียวกันมีบน action bar ของ profile · คลิกที่ไหนก็ได้จะเปิด modal จัดกลางเดียวที่มี checkbox สองส่วน:

  • RFM archetypes — all 11 archetypes as checkboxes (multi-membership, ≥1 required). The originally-assigned one carries a small blue AIVA badge; when you've checked extras, the current primary shows a primary tag.
  • Custom segments — one checkbox per custom RFM you've created, with a reason badge per row (criteria in blue when the player matches the thresholds; manual in amber when you added them by hand; excluded when they match but you removed them).
  • Round-trip from "+ New custom segment" — clicking that button inside the assign modal remembers the player's uid so saving (or cancelling) the segment editor pops you back into the assign modal with the new segment in the checkbox list.
  • RFM archetypes — ทั้ง 11 archetype เป็น checkbox (อยู่ได้หลายกลุ่ม, ต้องมีอย่างน้อย 1) · archetype ที่ AIVA เลือกแต่แรกมี badge สีฟ้า AIVA; เมื่อเช็ค extras แล้ว primary ปัจจุบันจะมี tag primary
  • Custom segments — checkbox ต่อ RFM กำหนดเองที่สร้างไว้ พร้อม reason badge แต่ละแถว (criteria สีฟ้า เมื่อผู้เล่นตรงเกณฑ์; manual สีเหลือง เมื่อเพิ่มเข้ามือ; excluded เมื่อตรงเกณฑ์แต่ถูกถอดออก)
  • วนกลับจาก "+ New custom segment" — กดปุ่มนั้นใน modal มอบหมายจะจดจำ uid ของผู้เล่น เมื่อบันทึก (หรือยกเลิก) editor segment จะกลับมา modal มอบหมายพร้อมเห็น segment ใหม่อยู่ใน list checkbox
04

Player Identity card lists every RFM segment + live-refreshes

การ์ด Player Identity แสดงทุก segment RFM + อัปเดตสด

A new RFM segments row at the top of the Player Identity card renders every archetype the player belongs to as a colour-coded badge — primary first, then extras. Custom RFM segments the player belongs to (whether they matched via criteria or were added manually) flow inline on the same row, each in its own palette colour with its auto-generated icon. The row pulls from playerArchetypes(p) + playerSegmentMemberships(p.uid) so it always reflects the current membership across both axes.

มีแถวใหม่ RFM segments ที่ด้านบนของการ์ด Player Identity แสดงทุก archetype ที่ผู้เล่นอยู่เป็น badge ตามสี — primary ก่อน, ต่อด้วย extras · ตามด้วย segment RFM กำหนดเองที่ผู้เล่นอยู่ (ไม่ว่าจะตรงเกณฑ์หรือถูกเพิ่มเข้ามือ) ในแถวเดียวกัน แต่ละตัวใช้สีจาก palette และไอคอนที่ AIVA สร้างให้ · แถวดึงข้อมูลจาก playerArchetypes(p) + playerSegmentMemberships(p.uid) จึงสะท้อนสมาชิกปัจจุบันทั้งสองแกนเสมอ

Live-refresh. A new refreshIfCurrent(uid) helper is called from togglePlayerArchetype, clearPlayerArchetype, and togglePlayerInSegment — when the uid matches state.player.uid, the profile re-renders, so the Identity row updates the moment you toggle a checkbox in the 🏷️ modal. saveCustomSegment, deleteCustomSegmentFromModal, and the reset flow all call renderPlayer() when a player is loaded so segment creation, edits, and deletions reflect immediately too.

อัปเดตสด · helper ใหม่ refreshIfCurrent(uid) ถูกเรียกจาก togglePlayerArchetype, clearPlayerArchetype, และ togglePlayerInSegment — เมื่อ uid ตรงกับ state.player.uid, profile จะ render ใหม่ทันที แถว Identity จึงอัปเดตทันทีที่กดเช็คใน modal 🏷️ · saveCustomSegment, deleteCustomSegmentFromModal, และ flow ของ reset ก็เรียก renderPlayer() เมื่อ profile เปิดอยู่ การสร้าง, แก้ไข, และลบ segment จึงสะท้อนผลทันทีเช่นกัน

05

↻ Reset to AIVA default — one click to undo everything

↻ คืนค่า AIVA — คลิกเดียวยกเลิกทุกอย่าง

A small button sits next to the Segment Overview card title. Clicking opens a centered confirm modal that previews the impact ("3 archetype changes + 2 extra archetypes will be cleared · 5 custom RFM segments will be deleted") and asks Cancel / Yes-reset. Confirming does two things:

ปุ่ม เล็ก ๆ ติดอยู่ข้างหัวการ์ด Segment Overview · กดเปิด modal ยืนยันจัดกลางที่แสดงผลกระทบ ("3 archetype changes + 2 extra archetypes will be cleared · 5 custom RFM segments will be deleted") และถาม Cancel / Yes-reset · ยืนยันแล้วจะทำสองอย่าง:

  • Every per-player archetype override is cleared — primary collapses back to p.aivaArchetype and p.extraArchetypes is dropped.
  • Every custom RFM segment is deleted — names, criteria, manual member assignments all gone (CUSTOM_SEGMENTS = [] + localStorage.removeItem(KEY_CUSTOM_SEGMENTS)). The roster filter clears if it was pointing at a now-deleted segment. The confirm modal warns the action can't be undone.
  • ทุกการ override archetype ของผู้เล่นถูกล้าง — primary คืนค่าเป็น p.aivaArchetype และ p.extraArchetypes ถูกลบ
  • ทุก segment RFM กำหนดเองถูกลบ — ชื่อ, เกณฑ์, สมาชิกที่กำหนดเอง หายหมด (CUSTOM_SEGMENTS = [] + localStorage.removeItem(KEY_CUSTOM_SEGMENTS)) · filter ของ roster จะถูกล้างถ้าชี้ไปยัง segment ที่เพิ่งถูกลบ · modal ยืนยันเตือนว่าทำแล้วยกเลิกไม่ได้
06

Layout split — custom RFM gets its own card + the detail table extends

แยก layout — RFM กำหนดเองมี card ของตัวเอง + ตารางรายละเอียดขยาย

Custom segments now render in their own card — Custom RFM Segments — directly below the canonical Segment Overview grid. The 11 AIVA-assigned cards stay in the original card untouched; the new card holds operator-built segments plus the + Custom segment entry tile. Header shows "N custom segments" or "No custom segments yet".

Segment กำหนดเองตอนนี้ render ในการ์ดของตัวเอง — Custom RFM Segments — อยู่ใต้ grid Segment Overview ของ AIVA โดยตรง · 11 archetype ที่ AIVA กำหนดยังอยู่ในการ์ดเดิมไม่เปลี่ยน · การ์ดใหม่เก็บ segment ที่ operator สร้าง พร้อม tile + Custom segment · หัวการ์ดแสดง "N custom segments" หรือ "No custom segments yet"

Per-Segment Detail table picks up the same separation — RFM rows first, then a divider row ("🏷️ Custom RFM Segments · N segment(s) · M total memberships"), then one row per custom segment with its own icon, palette colour, criteria-derived action line, and revenue stats, sorted by revenue inside the custom block.

ตาราง Per-Segment Detail ใช้การแยกเดียวกัน — แถว RFM ก่อน ตามด้วยแถวคั่น ("🏷️ Custom RFM Segments · N segment(s) · M total memberships") แล้วต่อด้วยแถวต่อ segment กำหนดเอง ใช้ไอคอน, สี palette, บรรทัด action ที่สร้างจากเกณฑ์, และสถิติรายได้ของตัวเอง เรียงตามรายได้ในกลุ่ม custom

Icon polish

ปรับไอคอน

Throughout the day the segment-assign affordance moved from 🛠 (wrench) to 🏷️ (tag) — fits the "label / classify a player" semantic better. Swapped in the roster row count badge, player-profile head action, assign modal title, custom-segment card icon (now AIVA-generated from the emoji pool, but 🏷️ remains the fallback), filter-chip label, and across the assign-modal segment rows.

ระหว่างวันเปลี่ยนไอคอนของการมอบหมาย segment จาก 🛠 (ประแจ) เป็น 🏷️ (แท็ก) — เข้ากับความหมาย "ติดป้าย / จัดกลุ่มผู้เล่น" มากกว่า · เปลี่ยนใน badge นับของแถว roster, action ของ profile head, หัว modal มอบหมาย, ไอคอนของการ์ด custom segment (ตอนนี้ AIVA สร้างจาก pool emoji แต่ 🏷️ ยังเป็น fallback), label ของ filter chip, และทุกแถว segment ใน modal มอบหมาย

07

🏷️ count on roster rows = total RFM segments

จำนวนข้าง 🏷️ บนแถว Roster = RFM segment ทั้งหมด

The 🏷️ badge next to each player's name used to count only custom segments — so every default player showed 🏷️ with no number. It now reflects the same union the Identity card row renders: playerArchetypes(p).length (primary + any extra RFM archetypes) + playerSegmentMemberships(p.uid).length (custom RFM segments via criteria or manual). Always ≥1 since every player carries an AIVA-assigned archetype. The highlighted .has style only kicks in when the count exceeds 1 — i.e. the operator has deliberately grouped the player beyond the default single archetype. Title attribute now reads "N RFM segments · click to manage".

Badge 🏷️ ข้างชื่อผู้เล่นเคยนับเฉพาะ custom segment — ผู้เล่นค่าเริ่มต้นจึงเห็น 🏷️ โดยไม่มีตัวเลข · ตอนนี้สะท้อน การรวมเดียวกับแถวของการ์ด Identity: playerArchetypes(p).length (primary + RFM archetype เพิ่มเติม) + playerSegmentMemberships(p.uid).length (custom RFM segment จากเกณฑ์หรือเพิ่มเข้ามือ) · เป็น ≥1 เสมอ เพราะผู้เล่นทุกคนมี archetype ที่ AIVA กำหนด · สไตล์เน้น .has ทำงานเฉพาะเมื่อจำนวนเกิน 1 — กล่าวคือ operator ได้จัดกลุ่มผู้เล่นเพิ่มจากค่าเริ่มต้น · Title อ่านว่า "N RFM segments · click to manage"

08

Player Roster — sortable by P&L and Lifetime turnover

Player Roster — จัดเรียงตาม P&L และเทิร์นโอเวอร์ตลอดชีพ

The 30d P&L and Lifetime turnover column headers are now clickable sort toggles. Same column flips direction; switching columns picks a sensible default — P&L asc (biggest loser first, matching the prior fixed behaviour) and turnover desc (biggest spender first). Active column header goes green with a / indicator. Sort state lives in state.rosterSort ({col, dir}) and defaults to P&L asc on first render.

หัวคอลัมน์ 30d P&L และ เทิร์นโอเวอร์ตลอดชีพ ตอนนี้กดเพื่อสลับการจัดเรียงได้ · กดที่เดิมจะกลับทิศทาง; เปลี่ยนคอลัมน์จะเลือกทิศทางเริ่มต้นที่เหมาะ — P&L asc (ผู้เสียมากที่สุดอยู่บน เหมือนพฤติกรรมเดิม) และเทิร์นโอเวอร์ desc (ผู้ใช้จ่ายสูงสุดอยู่บน) · หัวคอลัมน์ที่ active เป็นสีเขียวพร้อมตัวบ่งชี้ / · สถานะ sort อยู่ใน state.rosterSort ({col, dir}) ค่าเริ่มต้นคือ P&L asc

Implementation note: header text is split between a .label span (translatable, hits the LANG_TH exact-match) and a .sort-arrow span (data-no-i18n), so the TH translation of "Lifetime turnover" still resolves cleanly with the arrow appended.

หมายเหตุการ implement: ข้อความ header แยกเป็น span .label (แปลได้, ตรงกับ exact-match ใน LANG_TH) และ span .sort-arrow (data-no-i18n) ดังนั้นการแปล "Lifetime turnover" เป็นภาษาไทยยังทำงานปกติเมื่อมีลูกศรต่อท้าย

09

"across N segments" subline includes custom RFM count

บรรทัด "across N segments" รวม custom RFM ด้วย

The Total Players cell on the seg-stats qstrip and the seg-summary on the Segment Overview card header both used a hardcoded "11 segments." Both are now SEGMENT_LIST.length + CUSTOM_SEGMENTS.length, so saving a custom RFM segment bumps the visible total — 1 custom → "across 12 segments", 3 customs → "across 14 segments". Reset deletes customs and the number falls back to 11. The TH dictionary still hits exact-match in the common no-custom case ("across 11 segments" → "ใน 11 กลุ่ม"); once a custom exists, the number passes through untranslated.

เซลล์ Total Players บน qstrip ของ seg-stats และ seg-summary บนหัวการ์ด Segment Overview เคย hardcode เป็น "11 segments" · ตอนนี้ทั้งคู่ใช้ SEGMENT_LIST.length + CUSTOM_SEGMENTS.length · บันทึก custom RFM segment 1 อัน → "across 12 segments", 3 อัน → "across 14 segments" · Reset ลบ custom และตัวเลขกลับเป็น 11 · LANG_TH ยังตรงในกรณีไม่มี custom ("across 11 segments" → "ใน 11 กลุ่ม"); เมื่อมี custom ตัวเลขจะไม่ถูกแปล

No changes in slot 8.2 today — work focused on 8.1.

ไม่มีการเปลี่ยนแปลงใน slot 8.2 วันนี้ — โฟกัสที่ 8.1

No changes in slot 8.3 today — work focused on 8.1.

ไม่มีการเปลี่ยนแปลงใน slot 8.3 วันนี้ — โฟกัสที่ 8.1

15 commits · f9a3c71 → 8edcc58 · live at askmeaiva.comขึ้น askmeaiva.com แล้ว
Cross-cutting ข้ามทุก slot

Changes that touch every slot

การเปลี่ยนแปลงที่กระทบทุก slot

  • Cashback → Bonus rename — every customer-facing string that previously said "Cashback" now says "Bonus": campaign display names (Weekly / Monthly / Pre-Monthly / Post-Monthly Bonus), promo titles, AI-insight bodies, segment action lines, AISMS promo catalog, the 8.2 page subtitle, comm-log entries, and the matching LANG_TH entries. Internal IDs (weekly-cashback, monthly-cashback) kept as persistence keys; the boot IIFE backfills stale localStorage names.
  • Thai localization audit — translated the new AI-recommend popout body, every segment description in the Segment Guide popout, all SEGMENT_META action lines, the chip-label icon-prefixed forms (👑 Champions … 💤 Hibernating; 👑 Whale … 🥉 Bronze), the "Browsing" heatmap state, and a regex for the AI-recommend toast.
  • Default language → EN — every fresh page load now opens in English. The topbar pill still flips the session to ภาษาไทย, but the choice no longer persists across reloads (localStorage pps75_lang cleared on boot).
  • เปลี่ยนชื่อ Cashback → Bonus — ทุกข้อความที่ผู้ใช้เห็นซึ่งเคยใช้คำว่า "Cashback" เปลี่ยนเป็น "Bonus" แล้ว · ทั้งชื่อแคมเปญที่แสดง (Weekly / Monthly / Pre-Monthly / Post-Monthly Bonus), หัวข้อโปรโม, เนื้อหา AI insight, บรรทัด action ของ segment, แค็ตตาล็อก AISMS promo, subtitle หน้า 8.2, รายการ comm-log, และ LANG_TH ที่เกี่ยวข้อง · ID ภายใน (weekly-cashback, monthly-cashback) ยังคงไว้เป็น persistence key; boot IIFE จะแก้ชื่อใน localStorage ที่ค้างให้อัตโนมัติ
  • ตรวจสอบการแปลภาษาไทย — แปลเนื้อหา AI-recommend popout, ทุก segment description ในคู่มือ Segment, ทุกบรรทัด action ของ SEGMENT_META, รูปแบบ chip ที่มีไอคอนนำหน้า (👑 Champions … 💤 Hibernating; 👑 Whale … 🥉 Bronze), สถานะ "Browsing" ใน heatmap, และ regex สำหรับ toast ของ AI-recommend
  • ค่าเริ่มต้นภาษา → EN — ทุกครั้งที่โหลดหน้าใหม่จะเริ่มเป็นภาษาอังกฤษ · ปุ่ม pill ในแถบบนยังสามารถเปลี่ยนเป็น ภาษาไทย ได้ในรอบการใช้งานนั้น แต่จะไม่บันทึกข้ามการรีโหลด (ลบ localStorage pps75_lang ตอนบูต)
01

New RFM segment — Unconverted Leads

RFM Segment ใหม่ — Unconverted Leads

The 11th segment lives between New Customers and Promising. It captures registered accounts that haven't made a first deposit yet — the most conversion-sensitive cohort in any operator's roster. Action is direct: call + SMS within 24 hours to drive the first deposit.

Segment ตัวที่ 11 อยู่ระหว่าง New Customers และ Promising · เก็บผู้เล่นที่ลงทะเบียนแล้วแต่ยังไม่ฝากครั้งแรก — กลุ่มที่ไวต่อการ convert มากที่สุดในรายชื่อของ operator · action ตรงไปตรงมา: โทร + SMS ภายใน 24 ชั่วโมง เพื่อกระตุ้นการฝากครั้งแรก

📞
Unconverted LeadsRFM [5,1,1]
Unconverted LeadsRFM [5,1,1]
Registered but no first deposit yet. AI insight generator weight 90 (between At Risk 92 and About To Sleep 88). Action: "Follow-up — call + SMS to drive 1st deposit".
ลงทะเบียนแล้วแต่ยังไม่ฝากครั้งแรก · AI insight generator น้ำหนัก 90 (ระหว่าง At Risk 92 และ About To Sleep 88) · Action: "Follow-up — call + SMS to drive 1st deposit"

10 hand-crafted exemplars

ผู้เล่นตัวอย่างทำมือ 10 คน

Player UIDs P-77001 through P-77010 populate the segment with realistic variety — different lead sources (Facebook Ads, TikTok Ads, Google Ads, LINE OA, friend referral, affiliate, direct), days since registration (1–8), and follow-up states (hot leads online now, calls unanswered, follow-up overdue, email engaged). Roster total grows 60 → 70.

UID ผู้เล่น P-77001 ถึง P-77010 เติม segment ด้วยความหลากหลายจริง — แหล่งลีดต่างกัน (Facebook Ads, TikTok Ads, Google Ads, LINE OA, friend referral, affiliate, direct), จำนวนวันตั้งแต่ลงทะเบียน (1–8 วัน), และสถานะการติดตาม (hot lead ออนไลน์อยู่, โทรไม่รับ, ติดตามค้าง, อ่านอีเมลแล้ว) · รายชื่อรวมเพิ่มจาก 60 → 70 คน

  • SEGMENT_LIST, SEGMENT_META, RFM mapping, INSIGHT_GENERATORS, the procedural-gen TEMPLATES table, and LANG_TH all updated.
  • api/_lib/defaults.js MOCK_PLAYERS extended with the 10 new entries so the backend mirrors the frontend.
  • CLAUDE.md, README.md, USER_MANUAL.md all synced — segment table, hand-crafted player list, distribution note.
  • อัปเดต SEGMENT_LIST, SEGMENT_META, RFM mapping, INSIGHT_GENERATORS, ตาราง TEMPLATES สำหรับ procedural gen และ LANG_TH ครบทุกที่
  • api/_lib/defaults.js MOCK_PLAYERS เพิ่ม 10 entry ใหม่เพื่อให้ backend ตรงกับ frontend
  • CLAUDE.md, README.md, USER_MANUAL.md อัปเดตให้ตรง — ตาราง segment, รายชื่อผู้เล่นทำมือ, หมายเหตุการกระจาย
02

Segment Guide popout

ป๊อปอัปคู่มือ Segment

A small ⓘ button now sits next to the "Players" page title on slot 8.1. Click it to open a single-screen Segment Guide popout — every segment listed with its icon, plain-language description, recommended action, and live player count.

ตอนนี้มี ปุ่ม ⓘ เล็ก ๆ ติดอยู่ข้างหัวเรื่อง "Players" บน slot 8.1 · กดเปิดป๊อปอัปคู่มือ Segment ที่แสดงครบในหน้าเดียว — ทุก segment พร้อมไอคอน, คำอธิบายภาษาคนปกติ, action ที่แนะนำ, และจำนวนผู้เล่นล่าสุด

  • Two columns on desktop (≥720px), one column on phones — no internal scrolling needed.
  • Hidden on slots 8.2 / 8.3 (segments aren't relevant there); shown only when the title reads "Players".
  • Close via × button, Esc key, or click outside the popout.
  • The body of every segment row pulls from SEGMENT_DESCRIPTIONS; the new Unconverted Leads description is included and Thai-translated.
  • แสดงสองคอลัมน์บนเดสก์ท็อป (≥720px), หนึ่งคอลัมน์บนมือถือ — ไม่ต้องเลื่อนภายในป๊อปอัป
  • ซ่อนไว้บน slot 8.2 / 8.3 (segment ไม่เกี่ยว); แสดงเฉพาะเมื่อหัวเรื่องเป็น "Players"
  • ปิดด้วยปุ่ม × · ปุ่ม Esc · หรือคลิกนอกป๊อปอัป
  • แต่ละแถวดึงข้อมูลจาก SEGMENT_DESCRIPTIONS; รวมคำอธิบายของ Unconverted Leads และมีภาษาไทยแล้ว
01

Campaign display names → "Bonus"

ชื่อแคมเปญที่แสดง → "Bonus"

The four scheduled campaigns now read as Bonus across every surface they show up on:

ทั้ง 4 แคมเปญที่ตั้งเวลาไว้แสดงเป็น "Bonus" ในทุกที่ที่ปรากฏ:

  • Weekly Bonus — was "Weekly Cashback"
  • Pre-Monthly Bonus · Month-End Push — was "Pre-Monthly Cashback · …"
  • Monthly Bonus — was "Monthly Cashback" (the independent formula anchor)
  • Post-Monthly Bonus · Catch-Up — was "Post-Monthly Cashback · …"
  • Weekly Bonus — เดิม "Weekly Cashback"
  • Pre-Monthly Bonus · Month-End Push — เดิม "Pre-Monthly Cashback · …"
  • Monthly Bonus — เดิม "Monthly Cashback" (สูตรหลักของแคมเปญ)
  • Post-Monthly Bonus · Catch-Up — เดิม "Post-Monthly Cashback · …"

The 8.2 page subtitle also moves from "cashbacks, claims, and reconciliation" to "bonuses, claims, and reconciliation". The promo template title and body, the Bonus-aware AI insight strings, and the AISMS promo catalog all swap to the new wording in lock-step.

subtitle ของหน้า 8.2 ก็เปลี่ยนจาก "cashbacks, claims, and reconciliation" เป็น "bonuses, claims, and reconciliation" · ทั้งหัวข้อและเนื้อหา promo template, ข้อความ AI insight ที่เกี่ยวกับ Bonus, และแค็ตตาล็อก AISMS promo เปลี่ยนคำพร้อมกันทั้งหมด

Persistence keys untouched

Persistence key ไม่แตะ

Internal campaign IDs stay as weekly-cashback, monthly-cashback, etc. — they back localStorage pps75_campaigns and KV cron history, so renaming them would orphan saved state. Only the visible name field changes. The cashback formula (loss × 0.10 + wager × 0.0005) is also unchanged — the rename is display-layer only.

ID ภายในของแคมเปญยังคงเป็น weekly-cashback, monthly-cashback ฯลฯ — เป็น key ใน localStorage pps75_campaigns และ KV cron history · เปลี่ยนชื่อจะทำให้ข้อมูลที่บันทึกไว้ใช้ไม่ได้ · เปลี่ยนเฉพาะ field name ที่แสดง · สูตรการคิด cashback (loss × 0.10 + wager × 0.0005) ก็ไม่เปลี่ยน — เปลี่ยนเฉพาะการแสดงผลเท่านั้น

02

One-shot localStorage migration

Migration localStorage ครั้งเดียว

Users who loaded the dashboard before the rename had the old campaign names cached in pps75_campaigns. The boot IIFE's backfill() step now refreshes c.name from DEFAULT_CAMPAIGNS when the stored name still contains "Cashback" and the new default doesn't.

ผู้ใช้ที่โหลดแดชบอร์ดก่อนที่จะเปลี่ยนชื่อ จะมีชื่อแคมเปญเก่าเก็บไว้ใน pps75_campaigns · ขั้นตอน backfill() ใน boot IIFE จะอัปเดต c.name จาก DEFAULT_CAMPAIGNS เมื่อชื่อที่เก็บไว้ยังมีคำว่า "Cashback" แต่ default ใหม่ไม่มี

// CAMPAIGNS IIFE — refresh stale names
if(def
&&  typeof c.name === 'string'
&&  /cashback/i.test(c.name)
&&  !/cashback/i.test(def.name)) {
  c.name = def.name;
}

One-shot — quietly no-ops once everyone has loaded the dashboard once. Preserves user-renamed campaigns (e.g. "VIP Weekly Reload" won't match because the stored name lacks "Cashback").

ทำครั้งเดียว — จะไม่ทำอะไรอีกเมื่อทุกคนได้โหลดแดชบอร์ดผ่านแล้ว · ไม่กระทบแคมเปญที่ผู้ใช้ตั้งชื่อเอง (เช่น "VIP Weekly Reload" จะไม่เข้าเงื่อนไขเพราะไม่มีคำว่า "Cashback")

01

3-button AI-recommend selector

ตัวเลือก AI-recommend แบบ 3 ปุ่ม

The old "🤖 AI recommends top 10% players" single checkbox is now a three-button pill row inside the AISMS Filters popout. Click a bucket to engage; click the active bucket again to turn the recommendation off.

เช็คบ็อกซ์เดิม "🤖 AI recommends top 10% players" เปลี่ยนเป็นแถวปุ่ม pill 3 ปุ่มใน AISMS Filters popout · กดปุ่มเพื่อเปิดกลุ่มนั้น; กดปุ่มที่เปิดอยู่อีกครั้งเพื่อปิด

What changed underneath

มีอะไรเปลี่ยนใต้ฮูด

  • State carries the chosen percentage on aismsAIRecommendPct (10 / 25 / 50 / null).
  • The legacy aismsAIRecommend boolean stays in sync as !!aismsAIRecommendPct so any code path that only checks "is AI recommend on?" still works.
  • aismsTopSet(pct) builds the top-N set from the desirability score for any percentage. aismsTop10Set() remains as a back-compat shim returning aismsTopSet(10).
  • The toast on activation reports the actual count: "AI recommends top 25% · 17 players · intersected with chip filters."
  • state เก็บเปอร์เซ็นต์ที่เลือกไว้ใน aismsAIRecommendPct (10 / 25 / 50 / null)
  • boolean เดิม aismsAIRecommend ยัง sync เป็น !!aismsAIRecommendPct เพื่อให้โค้ดที่เช็คแค่ "AI recommend เปิดอยู่หรือไม่" ใช้ได้เหมือนเดิม
  • aismsTopSet(pct) สร้างชุด top-N จากคะแนน desirability สำหรับเปอร์เซ็นต์ใดก็ได้ · aismsTop10Set() ยังอยู่เป็น back-compat shim ที่ return aismsTopSet(10)
  • toast ตอนเปิดบอกจำนวนจริง: "AI recommends top 25% · 17 players · intersected with chip filters"
02

AI-recommend ⓘ info popout

ป๊อปอัปข้อมูล ⓘ ของ AI-recommend

The header line for the selector now carries an ⓘ button. Clicking it opens a nested popout that explains exactly how players are picked — formula, signal weights, and the on/off flow.

บรรทัดหัวของตัวเลือกตอนนี้มีปุ่ม ⓘ · กดเปิดป๊อปอัปซ้อนที่อธิบายวิธีคัดผู้เล่นแบบละเอียด — สูตร, น้ำหนักของสัญญาณ, และวิธีเปิด/ปิด

The score formula

สูตรคะแนน

desirability = AIVA-score             · 1.2
             + recency                · 35
             + log₁₀(recent loss)       · 8
             + log₁₀(lifetime turnover) · 4

Why each weight

ทำไมน้ำหนักแต่ละตัวถึงเป็นแบบนี้

AIVA score·1.2×
คะแนน AIVA·1.2×
Engagement-quality composite the model assigns each player (0–100).
คะแนนรวมคุณภาพการมีส่วนร่วม 0–100 ที่โมเดลกำหนดให้ผู้เล่นแต่ละคน
Recency·35×
ความใหม่·35×
How recently the player was active. Heaviest weight in the formula — recent players respond best.
ผู้เล่นแอคทีฟล่าสุดเมื่อใด · น้ำหนักสูงที่สุดในสูตร — ผู้เล่นที่เพิ่งแอคทีฟตอบสนองได้ดีที่สุด
📉
Recent loss·8× (log)
ขาดทุนล่าสุด·8× (log)
Net loss in the current period. Log-scaled so a Whale (฿100k loss) outranks a Bronze (฿1k loss) without dominating linearly.
ขาดทุนสุทธิช่วงเวลาปัจจุบัน · ใช้ log scale เพื่อให้ Whale (ขาดทุน ฿100k) อยู่เหนือ Bronze (ขาดทุน ฿1k) โดยไม่ครอบงำเชิงเส้น
💰
Lifetime turnover·4× (log)
เทิร์นโอเวอร์ตลอดชีพ·4× (log)
Cumulative wager since signup. Surfaces long-term high-value players even when the current period is quiet.
ยอดเดิมพันสะสมตั้งแต่สมัคร · ทำให้ผู้เล่นมูลค่าสูงระยะยาวขึ้นมาแม้ช่วงนี้จะเงียบ

The kept set is AND-intersected with the chip filters below — so a player only stays in the send queue if they pass the AI rank and every chip filter.

ชุดที่เก็บไว้จะถูก AND-intersect กับตัวกรอง chip ด้านล่าง — ผู้เล่นจะอยู่ในคิวส่งก็ต่อเมื่อผ่านอันดับ AI และ ผ่านตัวกรอง chip ทุกตัว

03

Nested ⓘ for Segments + VIP tiers

ป๊อปอัปข้อมูล ⓘ ซ้อนสำหรับ Segments + VIP tiers

Earlier today the AISMS Filters labels "Segments to include" and "VIP tiers" each got a small ⓘ that opens a nested popout above the AISMS Filters card. Both use the same viewport-fixed pattern so they aren't clipped by the parent card's transform.

ก่อนหน้านี้วันนี้ ป้าย "Segments to include" และ "VIP tiers" ใน AISMS Filters ทั้งคู่มี ⓘ เล็ก ๆ ที่กดเปิดป๊อปอัปซ้อนเหนือ AISMS Filters card · ใช้ pattern viewport-fixed เดียวกัน เพื่อไม่ให้โดน transform ของ card แม่ตัด

Segments popout

ป๊อปอัป Segments

Reuses SEGMENT_DESCRIPTIONS — every one of the 11 RFM segments listed with its plain-language description.

นำกลับมาใช้ SEGMENT_DESCRIPTIONS — ทุก 11 RFM segment พร้อมคำอธิบายภาษาคนปกติ

VIP tier popout

ป๊อปอัป VIP tier

Each tier paired with its Bonus multiplier (applied once at the source for derived campaigns via composition):

แต่ละ tier จับคู่กับตัวคูณ Bonus (ใช้ครั้งเดียวที่ต้นทางสำหรับแคมเปญที่ derive ผ่าน composition):

🥉
Bronze×1.0
Default tier — new or low-activity players.
ระดับเริ่มต้น — ผู้เล่นใหม่หรือกิจกรรมน้อย
🥈
VIP Silver×1.1
First step up — sustained low-to-mid volume.
ระดับถัดไป — กิจกรรมต่ำถึงปานกลางอย่างสม่ำเสมอ
🥇
VIP Gold×1.25
Active mid-volume regulars.
ผู้เล่นประจำระดับกลาง
💎
VIP Platinum×1.5
High-volume players · personal host eligible.
ผู้เล่นกิจกรรมสูง · มี VIP host เฉพาะ
👑
VIP Diamond×2.0
Top tier — whales / Champions.
ระดับสูงสุด — Whale / Champions

Closing behavior

พฤติกรรมการปิด

  • Click the × button or anywhere outside the popout to close.
  • Esc closes the nested popout first; only after both nested popouts are closed does Esc close the parent AISMS Filters card.
  • Closing the parent AISMS card calls closeAismsInfoPopouts() so the nested popouts don't get orphaned on screen.
  • กดปุ่ม × หรือคลิกนอกป๊อปอัปเพื่อปิด
  • Esc ปิดป๊อปอัปซ้อนก่อน · หลังจากป๊อปอัปซ้อนทั้งสองปิดแล้ว Esc จึงปิด AISMS Filters card หลัก
  • การปิด AISMS card หลักจะเรียก closeAismsInfoPopouts() เพื่อไม่ให้ป๊อปอัปซ้อนค้างหน้าจอ
04

Thai translations for AISMS Filters

การแปลภาษาไทยสำหรับ AISMS Filters

Chip labels (icon-prefixed forms)

ข้อความบน chip (แบบมีไอคอนนำหน้า)

The walker matches on trimmed text, so even though plain "Champions" and "Whale (≥฿100k)" were already in LANG_TH, the chip form "👑 Champions" / "👑 Whale (≥฿100k)" wasn't matching and the chip text fell back to English. Added every icon-prefixed combination.

walker เทียบจาก trimmed text · ดังนั้นแม้ "Champions" และ "Whale (≥฿100k)" จะอยู่ใน LANG_TH แล้ว · แต่รูปแบบบน chip "👑 Champions" / "👑 Whale (≥฿100k)" ไม่เข้าคู่ ทำให้ข้อความ chip ตกกลับเป็นภาษาอังกฤษ · เพิ่มทุกรูปแบบที่มีไอคอนนำหน้าครบแล้ว

EN
👑 Champions · ⭐ Loyal Customers · 🌿 Potential Loyalists · 🌱 New Customers · 📞 Unconverted Leads · ✨ Promising · 🔔 Need Attention · 😴 About To Sleep · ⚠ At Risk · 🔥 Cant Loose · 💤 Hibernating
TH
👑 แชมเปียน · ⭐ ลูกค้าประจำ · 🌿 ลูกค้าประจำที่กำลังจะมา · 🌱 ลูกค้าใหม่ · 📞 ลีดยังไม่ฝาก · ✨ มีแววดี · 🔔 ต้องการดูแล · 😴 กำลังจะหายไป · ⚠ มีความเสี่ยง · 🔥 เสียไม่ได้ · 💤 จำศีล

AI-recommend toast

Toast ของ AI-recommend

Added a regex pattern in LANG_TH_PATTERNS that catches the whole sentence including the dynamic count and percentage:

เพิ่ม regex pattern ใน LANG_TH_PATTERNS ที่จับทั้งประโยครวมตัวเลขและเปอร์เซ็นต์ที่เปลี่ยนแปลง:

EN
AI recommends top 25% · 17 players · intersected with chip filters
TH
AI แนะนำ 25% แรก · ผู้เล่น 17 คน · ตัดร่วมกับตัวกรอง

AI-recommend popout body

เนื้อหาในป๊อปอัป AI-recommend

The intro sentence, the "Score formula" eyebrow, all four signal name + description pairs, and the closing instruction line all translated. The formula text itself stays English (math + variable names are universal).

ประโยคบทนำ, eyebrow "Score formula", ทั้ง 4 คู่ของชื่อสัญญาณ + คำอธิบาย, และบรรทัดคำสั่งปิดท้าย แปลครบแล้ว · ตัวสูตรเองยังเป็นภาษาอังกฤษ (สัญลักษณ์คณิตศาสตร์และชื่อตัวแปรใช้สากล)

11 commits · 762602b → 38f46be · AISMS hardening dayวันเสริมความแข็งแกร่ง AISMS
Cross-cutting ข้ามทุก slot

Notes

หมายเหตุ

  • Almost the whole day went into hardening 8.3 AI SMS — pill state machine, Filters UX, History population, and behaviour at zero credits. CLAUDE.md, README.md, and USER_MANUAL.md all synced.
  • ใช้เวลาเกือบทั้งวันเสริมความแข็งแกร่งของ 8.3 AI SMS — state machine ของ pill, UX ของ Filters, การเติม History, และพฤติกรรมเมื่อเครดิตเป็น 0 · CLAUDE.md, README.md, USER_MANUAL.md อัปเดตครบ

No changes in slot 8.1 today — work focused on 8.3.

ไม่มีการเปลี่ยนแปลงใน slot 8.1 วันนี้ — โฟกัสที่ 8.3

No changes in slot 8.2 today — work focused on 8.3.

ไม่มีการเปลี่ยนแปลงใน slot 8.2 วันนี้ — โฟกัสที่ 8.3

01

Filters: collapsible card → centered popout

Filters: card แบบ collapsible → popout จัดกลาง

The AISMS Filters block used to be an inline collapsible card that pushed everything below it down when expanded. It's now a centered modal popout with a dimmed backdrop — opened via the 🎯 Filters button on the credit card, closed via × / Esc / backdrop click. Body scroll locks while open.

บล็อก AISMS Filters เคยเป็น card แบบ collapsible ที่ดันเนื้อหาด้านล่างเลื่อนลงเมื่อขยาย · ตอนนี้เป็น modal popout จัดกลางพร้อม backdrop ทึบ — เปิดด้วยปุ่ม 🎯 Filters บน card เครดิต, ปิดด้วย × / Esc / คลิก backdrop · scroll ของ body ถูกล็อกขณะเปิด

02

"AI recommends top 10% players" option

ตัวเลือก "AI แนะนำผู้เล่น 10% แรก"

First cut of the AI-recommend filter — a single checkbox at the top of the Filters popout. When on, AIVA ranks every player by a desirability score (AIVA score · 1.2 + recency · 35 + log10(recent loss) · 8 + log10(lifetime turnover) · 4) and keeps only the top 10%, then intersects with the chip filters below. (Superseded the next day by a 10 / 25 / 50 three-button selector.)

เวอร์ชันแรกของตัวกรอง AI-recommend — checkbox เดียวที่ด้านบนของ popout Filters · เมื่อเปิด AIVA จะจัดอันดับผู้เล่นทุกคนด้วยคะแนน desirability (AIVA score · 1.2 + recency · 35 + log10(recent loss) · 8 + log10(lifetime turnover) · 4) แล้วเก็บไว้แค่ 10% แรก ตัดร่วมกับ chip filter ด้านล่าง · (วันถัดมาถูกแทนด้วย selector 3 ปุ่ม 10 / 25 / 50)

03

AI SMS History: 120 generated entries + pagination + live updates

AI SMS History: 120 entry · แบ่งหน้า · อัปเดตสด

The History table now seeds with ~120 procedurally generated entries (varied promos, statuses, outcomes, dates) and paginates at 50 rows per page. While the AI Auto SMS ticker is sending, each tick prepends a new row with live:true; those rows render with a pulsing green left border for 4 seconds before reverting to standard styling. state.aismsHistoryPage persists across renders so the user keeps their place during live updates.

ตาราง History ตอนนี้มีข้อมูลเริ่มต้นประมาณ 120 entry (โปรโม, สถานะ, ผลลัพธ์, วันที่ หลากหลาย) และแบ่งหน้า 50 แถวต่อหน้า · ระหว่างที่ ticker AI Auto SMS กำลังส่ง แต่ละ tick จะเพิ่มแถวใหม่พร้อม live:true; แถวเหล่านั้นแสดงด้วยขอบซ้ายสีเขียวกระพริบ 4 วินาทีก่อนกลับเป็นสไตล์ปกติ · state.aismsHistoryPage รักษาไว้ข้ามการ render ผู้ใช้จึงยังอยู่ที่หน้าเดิมขณะมีการอัปเดตสด

Auto-start on Filters Confirm

เริ่มอัตโนมัติเมื่อกด Confirm บน Filters

Pressing Confirm in the Filters popout now closes the popout AND engages AI Auto SMS via toggleAISMS() if the master toggle was off — one-click "set filters and start sending".

การกดปุ่ม Confirm ใน popout Filters จะปิด popout และเปิด AI Auto SMS ผ่าน toggleAISMS() ถ้า master toggle ยังปิดอยู่ — "ตั้งค่าและเริ่มส่ง" ในคลิกเดียว

04

Pill state: Off / Idle / On with colour coding

สถานะ Pill: Off / Idle / On พร้อมรหัสสี

The AI Auto SMS card status pill is now three-state:

Pill สถานะของ card AI Auto SMS ตอนนี้มี 3 สถานะ:

  • Off (grey) — the master toggle is off.
  • On (green, --pri) — toggle on AND credit unlocked AND credits ≥ tick cost AND queue non-empty (= actually sending).
  • Idle (yellow, --amber) — toggle on AND anything else (queue drained, credit locked, or credits below tick cost).
  • Off (เทา) — master toggle ปิดอยู่
  • On (เขียว, --pri) — toggle เปิด, เครดิตปลดล็อก, เครดิต ≥ ราคา tick, queue ไม่ว่าง (= กำลังส่งจริง)
  • Idle (เหลือง, --amber) — toggle เปิด แต่กรณีอื่น (queue หมด, เครดิตล็อก, หรือเครดิตไม่พอ tick)

The yellow is driven by the .idle class on .autopilot-card, mirrored onto the SMS Credit pill so both pills move together. Slot 8.2 (AIVA Rewards autopilot) stays in the two-colour (off / on) model. The earlier "turn master toggle OFF when queue drains" approach was reverted in favour of staying engaged-but-idle — that way a chip toggle that brings a fresh untexted player into the set automatically resumes sending.

สีเหลืองมาจากคลาส .idle บน .autopilot-card, สะท้อนไปที่ pill เครดิต SMS เพื่อให้ทั้งสอง pill เคลื่อนไหวพร้อมกัน · Slot 8.2 (autopilot AIVA Rewards) ยังคงใช้แบบสองสี (off / on) · แนวคิดเดิม "ปิด master toggle เมื่อ queue หมด" ถูกย้อนกลับเป็นการรักษาสถานะ engaged-but-idle — แบบนั้นการเปิด chip ที่ดึงผู้เล่นใหม่เข้าสู่กลุ่มจะกลับมาส่งต่อเองโดยอัตโนมัติ

05

Credit balance starts at 0 + engage with zero credits

ยอดเครดิตเริ่มที่ 0 + เปิดได้แม้เครดิตเป็น 0

Initial SMS credit balance now starts at 0 (was 1,000). AI Auto SMS engages regardless of balance — the ticker just won't fire until credits cover the next tick; the pill stays Idle (yellow) with the description "credit balance is empty · top up". topUpCredit() calls syncAISMSTicker() so sending kicks in automatically the moment credits land.

ยอดเครดิต SMS เริ่มต้นที่ 0 แล้ว (เดิม 1,000) · AI Auto SMS เปิดได้แม้ไม่มียอด — ticker จะยังไม่ยิงจนกว่าเครดิตจะครอบคลุม tick ถัดไป; pill อยู่ Idle (เหลือง) พร้อมคำอธิบาย "เครดิตว่าง · เติม" · topUpCredit() เรียก syncAISMSTicker() เพื่อให้การส่งเริ่มเองทันทีที่เครดิตเข้า

4 commits · e35828e → 57bdd6f · bilingual launch + autopilot gatingเริ่มสองภาษา + ผูก autopilot
Cross-cutting ข้ามทุก slot

Bilingual EN / TH UI

UI สองภาษา EN / TH

First cut of the dashboard's Thai mode. A topbar pill (top-right) lets the operator flip between English and ภาษาไทย; a post-processor walks the DOM and substitutes labels from a LANG_TH dictionary on a MutationObserver, so dynamic content like newly-rendered cards and toasts pick up the translation too. Domain enum values (segment names, VIP tiers) stay English internally — only their display is translated.

เวอร์ชันแรกของโหมดภาษาไทยบนแดชบอร์ด · ปุ่ม pill บนแถบด้านขวาให้ผู้ดูแลสลับระหว่าง English และ ภาษาไทย; post-processor เดิน DOM แล้วแทนข้อความจาก dictionary LANG_TH ผ่าน MutationObserver จึงรองรับเนื้อหาที่ render ใหม่ทั้ง card และ toast · ค่า enum ของ domain (ชื่อ segment, ระดับ VIP) ยังคงเป็นภาษาอังกฤษภายใน — แปลเฉพาะการแสดงผล

01

Scheduled Campaigns card gated on AIVA Rewards

card Scheduled Campaigns ผูกกับ AIVA Rewards

The "Scheduled Campaigns" card in the right column of the player profile (slot 8.1) now hides when the master AIVA Rewards autopilot is off — gated via card.hidden = !state.aiAutopilot inside renderCampaigns(). toggleAutopilot() calls renderCampaigns() so flipping the master switch on slot 8.2 immediately updates the player profile too. Historical reward info (Bonus received lifetime, comm-log promo entries, AI insight recommendations) is intentionally NOT gated — it's data, not a description of the active rewards engine.

card "Scheduled Campaigns" ในคอลัมน์ขวาของ player profile (slot 8.1) ตอนนี้ซ่อนเมื่อ autopilot AIVA Rewards ปิด — ผูกผ่าน card.hidden = !state.aiAutopilot ใน renderCampaigns() · toggleAutopilot() เรียก renderCampaigns() เพื่อให้การสลับ master บน slot 8.2 อัปเดต player profile ทันที · ข้อมูล reward ในอดีต (Bonus ที่เคยรับ, รายการ promo ใน comm-log, AI insight) ตั้งใจไม่ผูก — เป็นข้อมูลย้อนหลัง ไม่ใช่คำอธิบายเอนจินที่ทำงานอยู่

No standalone changes in slot 8.2 today — see 8.1 for the cross-cutting gating change driven by the AIVA Rewards master toggle.

ไม่มีการเปลี่ยนแปลงเฉพาะ slot 8.2 วันนี้ — ดู 8.1 สำหรับการผูกแบบข้าม slot ที่ขับด้วย master toggle ของ AIVA Rewards

01

AISMS turns off when queue drains + TH coverage

AISMS ปิดเมื่อ queue หมด + เพิ่มเนื้อหาภาษาไทย

When the pending-queue empties (every targeted player has received their auto-SMS), the master toggle flips OFF and a toast confirms completion. Thai-language coverage expanded across AISMS components — toast prefixes, status descriptions, and the auto-SMS card's secondary lines were translated. (The toggle-off-on-drain behaviour was reverted the next day in favour of the Idle state that's live today.)

เมื่อ pending-queue หมด (ผู้เล่นเป้าหมายได้รับ auto-SMS หมดแล้ว), master toggle จะปิดและมี toast ยืนยัน · ภาษาไทยใน AISMS ครอบคลุมเพิ่ม — toast prefix, คำอธิบายสถานะ, และบรรทัดรองของ card auto-SMS ถูกแปล · (พฤติกรรม toggle-off-on-drain ถูกย้อนกลับในวันถัดไปเพื่อใช้สถานะ Idle ที่เห็นวันนี้)