# 📜 WepAppOpusApi — Project Rules

> **"กฎเหล็ก" ของโปรเจค** — ต้องอ่านก่อนแก้ไข/เพิ่มฟีเจอร์ใดๆ
>
> Last updated: 2026-07-31

---

## 🎨 1. UI/UX Rules (สำคัญที่สุด)

### 1.1 Theme Consistency
- **ทุกหน้าใหม่ต้องใช้ pink/pastel theme เดียวกับ `live.html`**
- ห้ามสร้างหน้า dark theme ใหม่ (ยกเว้น admin tools เช่น `admin-control.html`)
- ใช้ CSS variables จาก `live.css`:

```css
:root {
  --bg: #fdf2f8;        /* Pink background */
  --bg-warm: #fce7f3;   /* Warm pink */
  --surface: #ffffff;
  --card: #ffffff;
  --card-soft: #fff5f9;
  --border: #fbcfe8;
  --border-strong: #f9a8d4;
  --text: #500724;      /* Dark maroon */
  --text-2: #831843;
  --text-muted: #9d174d;
  --accent: #ec4899;    /* Hot pink */
  --accent-soft: #f9a8d4;
  --gradient: linear-gradient(135deg, #f9a8d4 0%, #ec4899 100%);
  --gradient-soft: linear-gradient(135deg, #fce7f3 0%, #fbcfe8 100%);
  --shadow: 0 4px 16px rgba(236, 72, 153, 0.10);
  --radius: 16px;
}
```

### 1.2 ห้ามแก้ไขหน้าที่มีอยู่เดิม โดยไม่ถาม
- **NEVER inject banners, CSS, or HTML เข้าไปในหน้าเดิม** (เช่น `live.html`)
- ถ้าจะเพิ่ม feature → **สร้างหน้าใหม่** แทน (เช่น `app.html`)
- หน้าเดิมที่ห้ามแตะ:
  - `public/live.html` (pink/pastel public dashboard)
  - `public/analytics.html`
  - `public/scale.html`
  - `public/manual.html`
  - `public/changelog.html`
  - `public/dashboard.html`

### 1.3 สร้าง Workspace แยก (App Shell Pattern)
- ถ้าจะทำ unified UI → สร้าง `public/app.html` (มีอยู่แล้ว)
- ใช้ hash-based routing: `#/overview`, `#/live`, `#/scale`, etc.
- ใช้ Sidebar + Topbar + Content shell
- Auto-pause status อยู่ใน sidebar (compact)

### 1.4 Mobile-First
- Bottom nav สำหรับ mobile
- Sidebar เป็น drawer (toggle ด้วย hamburger)
- Tables ต้อง scroll ได้
- Touch targets ≥ 40px

### 1.5 Command Palette
- ทุกหน้าต้องรองรับ `⌘K` (Mac) / `Ctrl+K` (Win)
- Sections: Pages / Commands / Campaigns
- Commands: `/pause`, `/resume`, `/scale`, `/refresh`, `/goto`

---

## 🤖 2. Auto-Pause Rules

### 2.1 Master Switch (Global Pause Block)
- **Config**: `app_config.pause_disabled_globally` (boolean)
- **เมื่อ `true`**: ระบบหยุดการปิดแคมเปญอัตโนมัติทั้งหมด (cron, auto-scale rules, schedules)
- **เช็คที่**: `live/scaleGuards.ts`, `cron/autoPause.ts`, `cron/scaleScheduler.ts`, `routes/auto-scale.ts`, `routes/scale-schedules.ts`
- **Toggle URL**: https://adsfb.namnan.co.th/admin-control.html

### 2.2 Per-Rule min_spend_thb Gate
- **Column**: `auto_scale_rules.min_spend_thb` (default 100 THB)
- **Logic**: ข้าม rule ถ้า "วันล่าสุดที่มี spend" < `min_spend_thb`
- **SQL pattern** (ใน `routes/auto-scale.ts`):
```sql
SELECT m.campaign_id, m.date, m.spend, m.revenue, m.conversions
FROM campaign_daily_metrics m
INNER JOIN (
  SELECT campaign_id, MAX(date) AS max_date
    FROM campaign_daily_metrics
   WHERE date >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
     AND campaign_id IN (?)
   GROUP BY campaign_id
) latest ON latest.campaign_id = m.campaign_id
        AND latest.max_date = m.date
```

### 2.3 Condition Types & Logic
| condition_type | Logic | min_spend gate |
|---|---|---|
| `roas_above` | `roas >= condition_value` | ✅ Yes |
| `roas_below` | `roas < condition_value` | ✅ Yes |
| `cpa_above` | `cpa > condition_value` | ✅ Yes |
| `cpa_below` | `cpa <= condition_value` | ✅ Yes |
| `spend_no_conv` | `conversions === 0` | ✅ Yes |

### 2.4 Auto-Pause Cron
- **File**: `src/cron/autoPause.ts`
- **Schedule**: every 1 hour
- **Skip if**: `pause_disabled_globally !== false`

---

## 💬 3. Chat Reply API Rules

### 3.1 Default to DRY-RUN
- **ทุกการ reply ต้องเริ่มด้วย `dryRun: true`**
- User ต้องกดปุ่ม "⚡ เปิด LIVE mode" เพื่อส่งจริง
- เหตุผล: ป้องกันการส่งข้อความผิดพลาดให้ลูกค้าจริง

### 3.2 Validation
- `text` ต้องไม่ว่าง
- `text` ≤ 2000 ตัวอักษร
- `conversationId` หรือ `pageId+recipientId` ต้องมีอย่างใดอย่างหนึ่ง

### 3.3 Store Sent Messages
- หลังส่งสำเร็จ INSERT เข้า `chat_messages`:
  - `is_from_page = 1`
  - `source = 'admin-reply'`
  - `message_id` = response.message_id จาก Meta

### 3.4 Page Access Token
- **Cache**: in-memory `Map`, TTL 6 ชั่วโมง
- **Fetch**: จาก `GET /me/accounts?fields=id,access_token` ใช้ User token
- **Use**: `POST /{page-id}/messages?access_token={page_token}`

### 3.5 messaging_type
- `"RESPONSE"` — ตอบกลับภายใน 24 ชม. หลัง user ส่งข้อความ
- `"MESSAGE_TAG"` + tag — สำหรับข้อความนอก 24 ชม. window (ต้อง approved tag)

---

## 🔌 4. API Conventions

### 4.1 URL Prefixes
| Prefix | Auth | Purpose |
|---|---|---|
| `/auth/*` | Public (login) | Authentication |
| `/public/*` | Public (read) | Public read endpoints |
| `/live/*` | Session required | Authenticated data |
| `/admin/*` | Admin only | Admin operations |
| `/webhook/*` | Public (verify) | Webhook receivers |

### 4.2 Response Format
**Success:**
```json
{ "ok": true, "data": ..., "count": N, ... }
```

**Error:**
```json
{ "ok": false, "error": "human message", "errorCode": "MACHINE_CODE", "statusCode": 400 }
```

### 4.3 Required Routes (HTML pages)
ต้องมี route ใน `src/index.ts` สำหรับทุก HTML file ใน `public/`:
- `/app.html`
- `/scale.html`
- `/live.html`
- `/analytics.html`
- `/ads-chats.html`
- `/admin-control.html`
- `/manual.html`
- `/changelog.html`

---

## 🔐 5. Token & Auth Rules

### 5.1 Meta Tokens
- **User Token** (long-lived): เก็บใน `.env` เช่น `FACEBOOK_API_KEY99`
- **Page Access Token**: ไม่เก็บใน env, cache in-memory 6h
- **Marketing API Token** (สำหรับ ads read): `API_ACCESS_TOKEN_ENC` (encrypted)

### 5.2 Test Tokens Safely
- ใช้ SAFE read endpoints เท่านั้น: `/me/accounts`, `/me`, `/{page-id}?fields=...`
- **NEVER test-send to real customers** (use placeholder PSIDs like `USER_12345`)
- **NEVER delete production data** ตอน test

### 5.3 Encryption
- ทุก token ใน DB ใช้ envelope encryption (`auth_user_api_sources.encrypted_value`)
- Key derivation: `config.tokenEncryptionKey` (from env)
- **ห้าม log token** — ใช้ `redactTokenLikeText()` ก่อน log

---

## 🛠 6. Code Style Rules

### 6.1 TypeScript
- `strict: true` (assumed)
- ไม่ใช้ `any` ถ้าไม่จำเป็น
- Interface สำหรับ request/response bodies
- Export types ที่ใช้ข้ามไฟล์

### 6.2 Fastify Routes
```typescript
app.post("/path", async (request, reply) => {
  const session = await requireSession(request, reply, "json", "admin");
  if (!session) return;  // session guard
  try {
    // logic
    return { ok: true, ...data };
  } catch (e: any) {
    return sendServiceError(reply, e, "context message");
  }
});
```

### 6.3 Database
- ใช้ `withMysql(async (pool) => { ... })` ไม่ใช้ pool directly
- Parameterized queries เสมอ (no string interpolation)
- Migrations ใน `src/db/sql.ts` → `mysqlStatements` และ `sqliteStatements`
- รัน migrations: `npx tsx src/db/run-migrations.ts`

### 6.4 Logging
- `console.log` สำหรับ info
- `console.error` สำหรับ errors
- Format: `[component] message`
- **NEVER log tokens or PII** without redaction

---

## 🚀 7. Deployment Rules

### 7.1 Server
- **Host**: `103.22.183.111` (root / `2$3Z7Gf1#9hv`)
- **Port**: `22222` (not 3000)
- **Process manager**: `pm2` with `pm2.config.cjs`
- **Runtime**: `tsx` (not tsc compile)
- **Restart**: `pm2 restart 0 --update-env` หลัง env changes
- **Start fresh**: `pm2 delete 0; pm2 start pm2.config.cjs`

### 7.2 Git Commits
- Conventional Commits: `feat:`, `fix:`, `revert:`, `refactor:`, `chore:`
- Commit message ต้องมี body อธิบายว่าทำไม ไม่ใช่แค่ "what"
- ห้าม commit:
  - `.env`, `.env.bak*` (secrets)
  - `*.bak` (backup files)
  - `public/*.bak-*` (old versions)
  - `*.log`

### 7.3 Public URLs
- `https://adsfb.namnan.co.th/<page>.html` — production
- `http://localhost:22222/<page>.html` — local dev

---

## 🧪 8. Testing Rules

### 8.1 Token Tests
- ใช้ SAFE read endpoints เสมอ
- ตัวอย่าง:
  - `GET /me?fields=id,name`
  - `GET /me/accounts?fields=id,name,access_token`
  - `GET /{page-id}?fields=id,name,fan_count`

### 8.2 API Tests
- ใช้ Python `urllib.request` + cookies (ไม่ใช้ requests library เพราะ environment)
- หรือ Playwright async + headless Chromium

### 8.3 UI Tests
- Playwright + Chromium headless
- Viewport: 1400×900 (desktop) และ 390×844 (mobile)
- **Screenshots**: ใช้ `full_page=False` เสมอ (full_page=True มี bug!)
- ดู [Playwright full_page bug](#) ใน memory

### 8.4 ทดสอบ End-to-End
- ใช้ test data ที่ seed ใน DB (USER_12345, USER_99999)
- **NEVER test กับ real customer PSID**

---

## 🚨 9. Safety Rules (CRITICAL)

### 9.1 ห้ามทำ
- ❌ Inject banners/CSS/HTML เข้าหน้าเดิมโดยไม่ถาม
- ❌ Test-send messages to real customers
- ❌ Delete production data ตอน test
- ❌ Log tokens/PII without redaction
- ❌ Pause campaign with low spend (< 100 THB/day)
- ❌ Send chat replies without DRY-RUN mode (default)
- ❌ Commit `.env`, `.bak`, `*.log` files
- ❌ Modify existing pages (live.html, scale.html, etc.) — สร้างใหม่แทน

### 9.2 ต้องทำ
- ✅ ถามก่อนแก้ design/structure ของหน้าที่มีอยู่
- ✅ ใช้ pink/pastel theme สำหรับหน้าใหม่
- ✅ ทุก auto-pause rule ต้องมี min_spend_thb gate
- ✅ ทุก chat reply เริ่มต้นด้วย dryRun: true
- ✅ ใช้ parameterized SQL queries
- ✅ เช็ค `pause_disabled_globally` ก่อน pause ใดๆ
- ✅ Restart ด้วย `pm2 restart 0 --update-env` หลัง env changes
- ✅ ทดสอบกับ test data เท่านั้น (ไม่ใช่ real customer)
- ✅ ทำ dry-run ก่อน apply ทุกครั้ง

---

## 📋 10. Pre-Commit Checklist

ก่อน `git commit`:

- [ ] ไม่มี `.env`, `.bak`, `*.log` ใน diff
- [ ] ทดสอบ API endpoints ที่เปลี่ยน
- [ ] ทดสอบ UI (Playwright screenshot)
- [ ] อัปเดต `docs/SYSTEM.md` ถ้าเปลี่ยน API
- [ ] อัปเดต `docs/PROJECT_RULES.md` ถ้าเปลี่ยน rule
- [ ] Commit message มี context ว่าทำไม
- [ ] `pm2 restart 0 --update-env` หลัง deploy

---

## 📚 11. Related Docs

- `docs/SYSTEM.md` — System architecture
- `docs/API-REFERENCE.md` — All API endpoints
- `docs/PROJECT_RULES.md` — This file
- `README.md` — Project intro
- `CLAUDE.md` / `AGENTS.md` — (optional) AI agent instructions

---



---

## 🐛 12. JS Date Equality Gotcha (CRITICAL — กฎใหม่ 2026-07-31)

> ⚠️ **Lesson learned 2026-07-31:** Sort comparator `a.date_only !== b.date_only` ทำให้ sort กลายเป็น stable เพราะ Date !== Date เป็น true เสมอ (reference comparison) → comparator fall through → getTime() - getTime() = 0 → sort ไม่ทำงาน → heatmap delta_spend เป็นลบทั้งหมด
>
> Commits: `bdb814b` (Date reference → value fix) + `3edfebd` (cross-day prev reset fix)
>
> QA Report: `docs/reports/heatmap_hourly_dashboard_qa_20260731_140800/`

### กฎ (mandatory)

1. **ห้ามใช้ `===` / `!==` ระหว่าง Date objects** — เปรียบเทียบ reference ไม่ใช่ value
   - `new Date('2026-07-30') === new Date('2026-07-30')` → **false** (คนละ reference)
   - ใช้ `getTime() === getTime()` หรือ `isSameDay()` จาก date-fns แทน

2. **Sort comparator ที่ใช้ Date field ต้อง compare by value เท่านั้น**
   ```ts
   // ❌ BAD - Date !== Date เป็น true เสมอ
   if (a.date_only !== b.date_only) return new Date(a.date_only).getTime() - new Date(b.date_only).getTime();

   // ✅ GOOD
   const _ddA = new Date(a.date_only).getTime(), _ddB = new Date(b.date_only).getTime();
   if (_ddA !== _ddB) return _ddA - _ddB;
   return Number(a.hour_of_day) - Number(b.hour_of_day);
   ```

3. **Per-period delta calculation ต้อง reset prev เมื่อ period เปลี่ยน**
   - ถ้าเก็บ `prevByKey.set(key, row)` แล้วลืม reset เมื่อ date เปลี่ยน → hour แรกของวันใหม่จะลบกับ hour สุดท้ายของวันเก่า = false negative
   ```ts
   const _rowDate = new Date(row.date_only).getTime();
   const _prev = prevByKey.get(key);
   const prev = _prev && new Date(_prev.date_only).getTime() === _rowDate ? _prev : null;
   ```

4. **ESLint rule** (TODO #988): add `no-date-equality-operator` rule เพื่อ catch `===`/`!==` ระหว่าง Date

### สาเหตุของ incident
- User complaint: "ทำไมยอดไม่ลงตามช่วงเวลา" (heatmap shows negative deltas)
- ดู `src/routes/hourlyMetrics.ts` line 92-127
- Original SQL: `ORDER BY hour_start DESC, campaign_id ASC` → rows in DESC order
- Sort intent: ASC by (campaign_id, date_only, hour_of_day) เพื่อให้ delta = current - previous
- Bug: `a.date_only !== b.date_only` → Date !== Date เป็น true เสมอ → comparator fall through ไป `getTime() - getTime() = 0` → sort ไม่กลับด้าน → rows คงอยู่ใน DESC → delta = (older - newer) = negative

### การแก้ไข
1. `bdb814b` — เปลี่ยน comparator เป็น compare by `getTime()` โดยตรง
2. `3edfebd` — เพิ่ม day-boundary check: ถ้า date เปลี่ยน → reset prev → delta = current spend (no subtraction)

### Verification
- 24/24 hour values ตรงกันระหว่าง UI กับ API
- 22 positive deltas, 0 negative, 0 first-hour-of-day false negatives
- Runaway campaign 1 [buy]tsm 250W: h15→h23 = +46.32, +2.61, ..., +2.64; h1 next day = +0.03 (not -91.9)

---

## 🔄 Changelog

| Date | Change |
|------|--------|
| 2026-07-12 | Initial rules doc — created from chat learnings |
| 2026-07-31 | Section 12: JS Date Equality gotcha (heatmap fix) |
| | • Date !== Date compares by reference, not value |
| | • Sort comparator with date_only must compare getTime() |
| | • Per-period delta must reset prev on date change |
| | • Commits: bdb814b + 3edfebd, QA: docs/reports/heatmap_hourly_dashboard_qa_20260731_140800/ |
| | • Theme: pink/pastel only for new pages |
| | • Don't modify existing pages |
| | • Auto-pause min_spend_thb gate (100 THB default) |
| | • DRY-RUN default for chat replies |
| | • Master switch `pause_disabled_globally` |
