# 🛡️ คู่มือผู้ใช้งาน `user: sj88` — WepAppOpusApi (Ads FB)

> **เวอร์ชัน**: v1.0 (2026-08-01)
> **โดเมน**: https://adsfb.namnan.co.th
> **Project**: `Facebooklnwadss88`
> **Target user**: `sj88` (admin)
> **Backend**: Fastify + MySQL + nginx + Node.js 20
> **อัปเดตล่าสุด**: 2026-08-01 15:25 ICT

---

## 📋 สารบัญ

1. [ภาพรวมระบบ (System Overview)](#1-ภาพรวมระบบ)
2. [บัญชีผู้ใช้ (User Account)](#2-บัญชีผู้ใช้)
3. [โครงสร้าง URL (URL Routing)](#3-โครงสร้าง-url)
4. [การเข้าสู่ระบบ (Login)](#4-การเข้าสู่ระบบ)
5. [Dashboard หลัก (Main Dashboards)](#5-dashboard-หลัก)
6. [API Reference](#6-api-reference)
7. [Chat Poller (ดึงแชทเพจ)](#7-chat-poller)
8. [Hourly Metrics Dashboard](#8-hourly-metrics-dashboard)
9. [Budget Decision Lab](#9-budget-decision-lab)
10. [Auto-Scale & Auto-Pause](#10-auto-scale--auto-pause)
11. [Admin Endpoints](#11-admin-endpoints)
12. [Cron Jobs (Background Tasks)](#12-cron-jobs)
13. [คำสั่งที่ใช้บ่อย (Common Commands)](#13-คำสั่งที่ใช้บ่อย)
14. [Troubleshooting](#14-troubleshooting)
15. [Change Log](#15-change-log)

---

## 1. ภาพรวมระบบ

**WepAppOpusApi** คือระบบจัดการ Facebook Ads + Chat Attribution ครบวงจร ประกอบด้วย:

| Layer | Component | URL / Port |
|-------|-----------|------------|
| Frontend | HTML+JS Pages | https://adsfb.namnan.co.th/* |
| Backend | Fastify (Node.js 20 + TypeScript) | 127.0.0.1:22222 |
| Database | MySQL 8 | 127.0.0.1:3306 |
| Reverse Proxy | nginx + Let's Encrypt SSL | 443 / 80 |
| Background | Cron jobs (16+) | systemd timer |
| Webhook | Facebook Graph API | POST /webhook/facebook |

**หน้าที่หลัก**:
- 📈 **Live Dashboard** — ดูแคมเปญ + spend + ROAS real-time
- 🤖 **Auto-Scale** — ปรับงบแคมเปญอัตโนมัติ
- 💬 **Chat Poller** — ดึงแชทจากทุกเพจที่ลงโฆษณา (ทุก 3 ชม.)
- ⏰ **Hourly Metrics** — เก็บข้อมูลรายชั่วโมง
- 💰 **Budget Lab** — วิเคราะห์ 7 วันก่อน/หลังเปลี่ยนงบ

---

## 2. บัญชีผู้ใช้

### 2.1 รายละเอียดบัญชี `sj88`

```
┌─────────────────────────────────────────────────┐
│  Username   :  sj88                              │
│  Password   :  SJja0238@@2027                    │
│  Role       :  admin                             │
│  Auth Hash  :  scrypt:salt:hexhash               │
│  Cookie     :  wa_session (4 ชั่วโมง TTL)        │
│  Created    :  2026-08-01 15:20 ICT              │
│  Status     :  ✓ Active (login 3/3 PASS)         │
└─────────────────────────────────────────────────┘
```

### 2.2 สิทธิ์ของ `admin`

| Endpoint Category | Permission |
|-------------------|-----------|
| `/auth/*` | ✓ Login / Logout / Session / Users |
| `/admin/*` | ✓ ทั้งหมด (chat-poller, webhooks, pages, configs) |
| `/live/*` | ✓ ทั้งหมด (campaigns, products, scale, forecast) |
| `/public/*` | ✓ ทั้งหมด (อ่านอย่างเดียว ไม่ต้อง auth) |
| `/webhook/*` | ✓ Verify (Facebook verify_token) |

### 2.3 API Key สำหรับ Admin Endpoints

นอกจาก session cookie แล้ว admin endpoints รับ **API Key** ใน header `X-Admin-Key`:

```
X-Admin-Key: chp_3Y4Th4MyHSsqwtJsp-qSxcr3htXm-J7K
```

| Env Variable | Purpose |
|--------------|---------|
| `CHAT_POLLER_ADMIN_KEY` | ใช้ auth `/admin/chat-poller/*` |

**ตัวอย่าง**:
```bash
curl -H "X-Admin-Key: chp_3Y4Th4MyHSsqwtJsp-qSxcr3htXm-J7K" \
  https://adsfb.namnan.co.th/admin/chat-poller/status
```

---

## 3. โครงสร้าง URL

### 3.1 Clean URLs (เดิม)

| URL | Page | File | Status |
|-----|------|------|--------|
| `/` | Landing (auto-redirect to /live) | index.html (missing) | 404 |
| `/live` | 🌸 Live Dashboard | live.html | 200 |
| `/app` | Workspace SPA (pink) | app.html | 200 |
| `/analytics` | 📈 Analytics | analytics.html | 200 |
| `/scale` | 🚀 Auto-Scale | scale.html | 200 |
| `/campaign` | Campaign Detail | campaign.html | 200 |
| `/ads-chats` | Ads + Chats | ads-chats.html | 200 |
| `/admin-control` | 🛡️ Admin Panel | admin-control.html | 200 |
| `/hourly-dashboard` | ⏰ Hourly Heatmap | hourly-dashboard.html | 200 |
| `/budget-analysis` | 💰 Budget Lab | budget-analysis.html | 200 |
| `/live-manual` | 📖 Live Manual | live-manual.html | 200 |
| `/debug-manual` | 🔧 Debug Manual | debug-manual.html | 200 |
| `/debug` | Debug Tool | debug.html | 200 |
| `/changelog` | Changelog | changelog.html | 200 |
| `/manual` | User Manual | manual.html | 200 |

### 3.2 V1 URL Pattern (ใหม่ — 2026-08-01)

| URL | Maps to | Purpose |
|-----|---------|---------|
| `/V1` | → 301 → `/V1/live-dashboard` | Entry point |
| **`/V1/live-dashboard`** | live.html | 🌸 Main entry (default) |
| `/V1/app` | app.html | Workspace SPA |
| `/V1/hourly` | hourly-dashboard.html | ⏰ Hourly Heatmap |
| `/V1/analytics` | analytics.html | 📈 Analytics |
| `/V1/scale` | scale.html | 🚀 Auto-Scale |
| `/V1/campaign` | campaign.html | Campaign Detail |
| `/V1/ads-chats` | ads-chats.html | Ads + Chats |
| `/V1/admin` | admin-control.html | 🛡️ Admin Panel |
| `/V1/budget` | budget-analysis.html | 💰 Budget Lab |
| `/V1/manual` | manual.html | 📖 User Manual |
| `/V1/live-manual` | live-manual.html | 📖 Live Manual |

### 3.3 Redirects

| Old URL | New URL |
|---------|---------|
| `/demo/live-dashboard` | 301 → `/V1/live-dashboard` |
| `/demo/login` | (unchanged) |
| `/demo/analytics` | (unchanged) |
| `/demo/scale` | (unchanged) |

### 3.4 nginx Config (ตัวอย่าง)

```nginx
# V1 pattern - new URL scheme
location = /V1/live-dashboard {
    root /opt/adsfb/public;
    try_files /live.html =404;
}
location = /V1/app {
    root /opt/adsfb/public;
    try_files /app.html =404;
}
location = /V1 {
    return 301 https://adsfb.namnan.co.th/V1/live-dashboard;
}

# Backward compat
location = /demo/live-dashboard {
    return 301 https://adsfb.namnan.co.th/V1/live-dashboard$is_args$args;
}
```

---

## 4. การเข้าสู่ระบบ

### 4.1 Login Flow (ผ่าน UI)

```
┌──────────────────────────────────────────┐
│  1. เปิด https://adsfb.namnan.co.th/demo/login
│  2. กรอก username = "sj88"
│  3. กรอก password = "SJja0238@@2027"
│  4. คลิก "Login" (button[type=submit])
│  5. Backend สร้าง cookie "wa_session" (TTL 4h)
│  6. Redirect → /live
└──────────────────────────────────────────┘
```

**Form fields** (จาก DOM inspection):
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `username` | text | ✓ | placeholder="admin" |
| `password` | password | ✓ | - |
| `rememberMe` | checkbox | - | ขยาย TTL |

### 4.2 Login Flow (ผ่าน API)

```bash
# Step 1: Login
curl -c cookies.txt -X POST \
  -H "Content-Type: application/json" \
  -d '{"username":"sj88","password":"SJja0238@@2027"}' \
  https://adsfb.namnan.co.th/auth/login

# Response:
# { "ok": true, "user": "sj88", "role": "admin", "expiresAt": "..." }

# Step 2: Verify session
curl -b cookies.txt https://adsfb.namnan.co.th/auth/session

# Response:
# {
#   "ok": true,
#   "authenticated": true,
#   "user": "sj88",
#   "role": "admin",
#   "expiresAt": "2026-08-01T12:25:50.000Z",
#   "expiresInSeconds": 14396
# }

# Step 3: Access protected endpoint
curl -b cookies.txt https://adsfb.namnan.co.th/admin/chat-poller/status

# Logout
curl -b cookies.txt -X POST https://adsfb.namnan.co.th/auth/logout
```

### 4.3 Session Cookie Details

| Attribute | Value |
|-----------|-------|
| Name | `wa_session` |
| Path | `/` |
| HttpOnly | ✓ |
| SameSite | (depends) |
| TTL | 4 hours (default) / 7 days (rememberMe) |

### 4.4 Test Results (TC01)

```
✓ Form fields detected: username, password, rememberMe
✓ POST /auth/login → 200, sets wa_session cookie
✓ GET /auth/session → 200, user=sj88, role=admin, expiresAt=+4h
✓ Session cookie ทำงานกับ /admin/chat-poller/status (200)
✓ Logout ล้าง session, authenticated=false
```

---

## 5. Dashboard หลัก

### 5.1 🌸 Live Dashboard — `/live` (หรือ `/V1/live-dashboard`)

**Purpose**: ดูแคมเปญ + spend + ROAS แบบ real-time
**Auth**: Public (ไม่ต้อง login)
**File**: `live.html` (22 KB)
**Backend API**: `/public/live-dashboard`

**ส่วนประกอบ**:
- 📊 KPI Cards (spend, ROAS, CTR, CPM)
- 📈 Time-series chart (24h)
- 🎯 Top campaigns
- 💬 Recent chats
- ⚠️ Anomaly explanations (5 cards: Pause, Family, Pacing, Time-of-Day, Anomaly)

**Screenshot**: ![live dashboard](../reports/sj88_full_system_qa_20260801_152500/screenshots/tc_TC02_live.png)

### 5.2 🛡️ Admin Control — `/admin-control` (หรือ `/V1/admin`)

**Purpose**: จัดการระบบทั้งหมด
**Auth**: Login required
**File**: `admin-control.html` (16 KB)
**Sections**:
- 👤 Users (list, create, delete)
- ⚙️ Configs (env-style KV)
- 🔌 Webhooks (list, test, deliveries)
- 📑 Pages (list, exclude, sync)
- 📊 Debug (logs, query, crons, stats)
- 💬 Chat Poller (status, sync-pages, trigger)
- 📅 Safety Guards
- 🌅 Midnight Reset
- 🚨 Chat Alerts (detect, resolve)

### 5.3 🚀 Scale Page — `/scale` (หรือ `/V1/scale`)

**Purpose**: Auto-scale campaigns
**Auth**: Login required
**File**: `scale.html` (29 KB)
**Sections**:
- 📋 Active auto-scale rules
- 📜 Scale history (with undo)
- 📅 Schedules (cancel)
- 🎯 Recommendations
- 📊 Forecast (7-day)
- 🧪 Test rule (dry-run)

### 5.4 📈 Analytics — `/analytics` (หรือ `/V1/analytics`)

**Purpose**: กราฟและ analytics
**Auth**: Login required
**File**: `analytics.html` (17 KB)
**Sections**:
- 📈 Spend over time
- 📊 ROAS by campaign
- 🎯 Conversion funnel
- 👥 Family performance

### 5.5 💬 Ads + Chats — `/ads-chats` (หรือ `/V1/ads-chats`)

**Purpose**: ดูเพจ + แชทที่มาจาก ads
**Auth**: Login required
**File**: `ads-chats.html` (31 KB)
**Sections**:
- 📑 Active pages (with ad-promoted flag)
- 💬 Chat inbox (with ad_referrals)
- 🔗 Ad-chat link (auto-attribution)

### 5.6 ⏰ Hourly Dashboard — `/hourly-dashboard` (หรือ `/V1/hourly`)

**Purpose**: ดูข้อมูลรายชั่วโมง (heatmap + line chart)
**Auth**: Public
**File**: `hourly-dashboard.html` (160 KB)
**Sections** (18 total):
- 🔥 Heatmap (24h × date)
- 📈 Line chart (multi-metric)
- 🎯 Top 10 by hour
- 📊 Cumulative trends
- 💬 Chat-funnel overlay
- 🕐 Time-of-day patterns
- 📅 Weekday patterns
- 🔄 Compare days
- 🚨 Hourly anomalies

### 5.7 💰 Budget Lab — `/budget-analysis` (หรือ `/V1/budget`)

**Purpose**: วิเคราะห์ผลกระทบของการเปลี่ยนงบ (7d before/after)
**Auth**: Public
**File**: `budget-analysis.html` (32 KB)
**Sections**:
- 📅 Campaign list (with budget history)
- 📊 7d before vs 7d after
- 📈 Spend / ROAS / CTR delta
- 🎯 Recommendation

---

## 6. API Reference

### 6.1 Public APIs (ไม่ต้อง auth)

| Method | Path | Purpose | Avg Time |
|--------|------|---------|----------|
| GET | `/public/campaigns` | List active campaigns | 1.5s |
| GET | `/public/trends` | Top trending products | 250ms |
| GET | `/public/forecast` | 7-day forecast | 270ms |
| GET | `/public/chats` | Recent chat messages | 1.3s |
| GET | `/public/chat-inbox?limit=N` | Inbox for admin panel | 300ms |
| GET | `/public/hourly-metrics?days=N` | Hourly aggregates (heavy!) | 1.1s |
| GET | `/public/hourly-metrics/chat-funnel?days=N` | Chat→conversion funnel | 900ms |
| GET | `/health` | Liveness probe | <50ms |
| GET | `/ready` | Readiness + DB backend | <50ms |
| GET | `/bridge/status` | Bridge service status | <50ms |

**ตัวอย่าง**:
```bash
curl https://adsfb.namnan.co.th/public/campaigns | jq '.campaigns[0:3]'
```

### 6.2 Live APIs (ต้อง auth)

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/live/dashboard` | Full dashboard payload |
| GET | `/live/accounts` | Ad accounts |
| GET | `/live/campaigns` | All campaigns (with filter) |
| POST | `/live/campaigns/bulk` | Bulk update |
| POST | `/live/campaigns/:id/budget` | Update budget |
| POST | `/live/campaigns/:id/status` | Pause/Enable |
| GET | `/live/products` | List products |
| POST | `/live/products/assign` | Assign campaign |
| GET | `/live/products/caps` | List caps |
| POST | `/live/products/:product/cap` | Set cap |
| GET | `/live/auto-scale-rules` | List rules |
| POST | `/live/auto-scale-rules` | Create rule |
| GET | `/live/scale-history` | Audit log |
| POST | `/live/scale-history/:id/undo` | Undo scale |
| GET | `/live/forecast/measurements` | Accuracy measurements |
| GET | `/live/reallocation` | Realloc snapshot |
| POST | `/live/chat-reply` | Reply to chat |

### 6.3 Admin APIs (ต้อง auth + admin role + API key)

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/admin/pages` | List all pages |
| POST | `/admin/pages/sync` | Sync from FB |
| POST | `/admin/pages/sync-ad-promoted` | Find ad-promoted pages |
| POST | `/admin/pages/sync-orphans` | Find orphan pages |
| GET | `/admin/webhooks` | List webhooks |
| POST | `/admin/webhooks/:id/test` | Test webhook |
| GET | `/admin/webhooks/:id/deliveries` | Webhook delivery log |
| GET | `/admin/audit` | Audit log |
| GET | `/admin/config/:key` | Get config |
| POST | `/admin/config/:key` | Set config |
| POST | `/admin/metrics/run` | Trigger hourly metrics |
| POST | `/admin/metrics/backfill` | Backfill N days |
| GET | `/admin/metrics/daily` | Daily aggregates |
| POST | `/admin/forecast/evaluate` | Run forecast evaluation |
| POST | `/admin/midnight-reset/run` | Trigger midnight reset |
| GET | `/admin/realloc/snapshots` | Realloc history |
| GET | `/admin/safety-guards` | Safety guard status |
| GET | `/admin/auto-pause/runs` | Auto-pause history |
| POST | `/admin/auto-pause/run` | Trigger auto-pause |
| GET | `/admin/debug-logs` | Recent logs |
| GET | `/admin/debug-crons` | Cron status |
| POST | `/admin/debug-query` | Run SQL (with guardrails) |
| GET | `/admin/debug-stats` | System stats |

### 6.4 Chat Poller APIs (API key required)

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/admin/chat-poller/status` | Current status |
| GET | `/admin/chat-poller/stats` | Run history stats |
| POST | `/admin/chat-poller/sync-pages` | Sync pages only |
| POST | `/admin/chat-poller/trigger` | Manual trigger |
| GET | `/admin/chat-attribution/full-run` | Run full attribution |
| POST | `/admin/chat-attribution/resolve-orphans` | Resolve orphan chats |
| POST | `/admin/chat-attribution/sync-mapping` | Sync ad→page mapping |
| POST | `/admin/chat-ads-link/backfill` | Backfill ad_referrals |
| GET | `/admin/chat-templates` | List reply templates |
| POST | `/admin/chat-templates/seed` | Seed defaults |
| GET | `/admin/chat-alerts/detect` | Detect anomalies |
| POST | `/admin/chat-alerts/resolve` | Resolve alert |
| POST | `/admin/chat-history/backfill` | Backfill chat history |

### 6.5 Auth APIs

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/auth/login` | Login (returns cookie) |
| POST | `/auth/logout` | Logout (clears cookie) |
| GET | `/auth/session` | Current session |
| POST | `/auth/change-password` | Change password |
| GET | `/auth/users` | List users |
| POST | `/auth/users` | Create user |
| PATCH | `/auth/users/:id` | Update user |
| DELETE | `/auth/users/:id` | Delete user |
| GET | `/auth/token-profiles` | List FB token profiles |
| POST | `/auth/token-profiles/:id/set-default` | Set default token |

---

## 7. Chat Poller

### 7.1 Overview

**Chat Poller** ดึงข้อความแชทจากทุก Facebook Page ที่ user มีสิทธิ์ ทุก 3 ชั่วโมง ผ่าน `/me/accounts` → `/<page-id>/conversations` → `/<conv-id>/messages`

**ไม่ต้องใช้ webhook แบบ realtime** — ใช้ polling ทุก 3 ชั่วโมง (พอสำหรับ lead attribution)

### 7.2 Schedule

| Cron | Description |
|------|-------------|
| `*/180 * * * *` (every 3h) | Full chat pull (all pages) |
| `*/60 * * * *` (every 1h) | Page sync (cache refresh) |

### 7.3 Database Tables

| Table | Purpose |
|-------|---------|
| `pages` | Page metadata + encrypted access_token |
| `chat_conversations` | Per-page-per-user threads |
| `chat_messages` | Individual messages (text, sender, timestamp) |
| `chat_daily` | Per-day aggregates (count, unique users) |
| `ad_chat_link` | Maps chat → ad_referrals |

### 7.4 Latest Stats (2026-08-01)

```
pages: 67 total, 67 with tokens, 67 active
chat_messages: 3,159
chat_conversations: 124
chat_daily: 324 days
ad_chat_link: 3 (only ones with ad_referrals)
```

**Top pages by message count**:
1. SJ88 Studio — 1,826 msgs
2. เฮียหนวด โซล่าเซลล์ — 392
3. เฮียหนวด ไฟหัวเสา — 46
4. ฟองน้ำนาโน — 32

### 7.5 API Examples

```bash
# Status
curl -H "X-Admin-Key: chp_3Y4Th4MyHSsqwtJsp-qSxcr3htXm-J7K" \
  https://adsfb.namnan.co.th/admin/chat-poller/status

# Response:
# {
#   "running": false,
#   "last_run_at": "2026-08-01T08:35:00.000Z",
#   "last_run_duration_ms": 720000,
#   "next_run_at": "2026-08-01T11:35:00.000Z",
#   "pages_synced": 67,
#   "messages_pulled": 2330
# }

# Stats
curl -H "X-Admin-Key: chp_3Y4Th4MyHSsqwtJsp-qSxcr3htXm-J7K" \
  https://adsfb.namnan.co.th/admin/chat-poller/stats

# Manual trigger
curl -X POST -H "X-Admin-Key: chp_3Y4Th4MyHSsqwtJsp-qSxcr3htXm-J7K" \
  https://adsfb.namnan.co.th/admin/chat-poller/trigger

# Sync pages only (no chat pull)
curl -X POST -H "X-Admin-Key: chp_3Y4Th4MyHSsqwtJsp-qSxcr3htXm-J7K" \
  https://adsfb.namnan.co.th/admin/chat-poller/sync-pages

# Response:
# {"ok":true,"pages":67,"upserted":67,"deactivated":0}
```

### 7.6 Authentication

ใช้ `FB_MESSAGING_TOKEN` (env: `FACEBOOK_API_KEY99`) ที่มี permissions:
- `pages_messaging`
- `pages_show_list`
- `ads_management`
- `pages_read_engagement`

**Token encryption**: ใช้ 32-byte key (env: `CHAT_POLLER_TOKEN_KEY`) + AES-GCM, format `keyId:iv:ciphertext:authTag`

### 7.7 Rate Limits

| Limit | Value |
|-------|-------|
| Graph API | ~200 calls/hour/app |
| Per-page delay | 1s (configurable) |
| Token cache | 30 min |

---

## 8. Hourly Metrics Dashboard

### 8.1 Overview

เก็บข้อมูล **ทุกชั่วโมง** (cron `:05`) สำหรับทุก campaign + ad set + ad

### 8.2 Database Tables

| Table | Granularity | Fields |
|-------|-------------|--------|
| `metrics_hourly_campaign` | 1 row per campaign per hour | 25+ fields (spend, impressions, clicks, CTR, CPM, CPC, ROAS, reach, frequency, actions, conversions, cost_per_action, video_views, video_p25/50/75/100, etc.) |
| `metrics_hourly_adset` | 1 row per ad set per hour | same fields |
| `metrics_hourly_ad` | 1 row per ad per hour | same fields |

### 8.3 Sections (Hourly Dashboard)

1. 🔥 **Heatmap** (24h × date) — color-coded by spend delta
2. 📈 **Line chart** — multi-metric over time
3. 🎯 **Top 10 by hour** — sortable
4. 📊 **Cumulative** — running total
5. 💬 **Chat-funnel overlay** — chat count vs spend
6. 🕐 **Time-of-day pattern** — aggregate by hour-of-day
7. 📅 **Weekday pattern** — Mon-Sun
8. 🔄 **Compare days** — day vs day
9. 🚨 **Hourly anomalies** — z-score based
10. 🏆 **Best hour** — peak conversion
11. 🐌 **Worst hour** — dead zone
12. 📈 **Trend** — 7-day moving avg
13. 📉 **Variance** — std dev
14. 💸 **Cost efficiency** — cost per result by hour
15. 🎯 **Conversion rate** — by hour
16. 📊 **ROAS by hour** — by hour
17. 📈 **Spend velocity** — vs forecast
18. 🔍 **Drill-down** — campaign → adset → ad

### 8.4 API

```bash
# Hourly metrics (heavy — 1.1s for 2 days)
curl 'https://adsfb.namnan.co.th/public/hourly-metrics?days=2' | wc -c
# → 3,460,073 bytes (~3.4 MB)

# Chat funnel
curl 'https://adsfb.namnan.co.th/public/hourly-metrics/chat-funnel?days=7'

# Trigger manual run
curl -X POST -H "X-Admin-Key: chp_..." \
  https://adsfb.namnan.co.th/admin/metrics/run
```

---

## 9. Budget Decision Lab

### 9.1 Overview

วิเคราะห์ **7 วันก่อน vs 7 วันหลัง** สำหรับทุก campaign ที่เปลี่ยน budget

### 9.2 Logic

```
For each campaign with budget change in last 30 days:
  - Find the most recent change timestamp
  - 7d_before = avg metrics for 7d before change
  - 7d_after = avg metrics for 7d after change
  - delta = 7d_after - 7d_before
  - delta_pct = (7d_after - 7d_before) / 7d_before * 100
```

### 9.3 Sections

- 📅 **Campaign list** — sortable by date, budget delta, ROAS delta
- 📊 **7d comparison** — side-by-side cards
- 📈 **Spend / ROAS / CTR delta** — colored (green = better, red = worse)
- 🎯 **Recommendation** — "scale up" / "scale down" / "hold"

---

## 10. Auto-Scale & Auto-Pause

### 10.1 Auto-Scale Rules

**Database table**: `auto_scale_rules`

**Schema**:
```sql
CREATE TABLE auto_scale_rules (
  id INT PRIMARY KEY AUTO_INCREMENT,
  campaign_id VARCHAR(64) NOT NULL,
  metric VARCHAR(32) NOT NULL,        -- 'spend', 'roas', 'cpa', 'ctr'
  threshold DECIMAL(10,2) NOT NULL,   -- e.g. 1.5 (150% ROAS)
  direction ENUM('up','down') NOT NULL,
  budget_delta_pct DECIMAL(5,2) NOT NULL,  -- e.g. 20 (%)
  enabled BOOLEAN DEFAULT TRUE,
  cooldown_hours INT DEFAULT 24,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
```

### 10.2 Auto-Pause Rules

**Triggers** (cron every 1h):
- ROAS < 0.5 for 24h → pause
- Spend > 5× daily avg with 0 conversions → pause
- CTR < 0.3% for 48h → pause

**Whitelist**: `safety_guards` table can exempt specific campaigns

### 10.3 Manual Operations

```bash
# List rules
curl -b cookies.txt https://adsfb.namnan.co.th/live/auto-scale-rules

# Create rule
curl -b cookies.txt -X POST -H "Content-Type: application/json" \
  -d '{"campaign_id":"123","metric":"roas","threshold":1.5,"direction":"up","budget_delta_pct":20}' \
  https://adsfb.namnan.co.th/live/auto-scale-rules

# Test (dry-run)
curl -b cookies.txt -X POST \
  https://adsfb.namnan.co.th/live/auto-scale-rules/123/test

# Undo last scale
curl -b cookies.txt -X POST \
  https://adsfb.namnan.co.th/live/scale-history/456/undo
```

---

## 11. Admin Endpoints

### 11.1 Users

```bash
# List
curl -b cookies.txt https://adsfb.namnan.co.th/auth/users

# Create (admin only)
curl -b cookies.txt -X POST -H "Content-Type: application/json" \
  -d '{"username":"newuser","password":"MyP@ss123","role":"viewer"}' \
  https://adsfb.namnan.co.th/auth/users

# Update role
curl -b cookies.txt -X PATCH -H "Content-Type: application/json" \
  -d '{"role":"admin"}' \
  https://adsfb.namnan.co.th/auth/users/5
```

### 11.2 Configs

```bash
# Get
curl -b cookies.txt https://adsfb.namnan.co.th/admin/config/MY_KEY

# Set
curl -b cookies.txt -X POST -H "Content-Type: application/json" \
  -d '{"value":"some_value"}' \
  https://adsfb.namnan.co.th/admin/config/MY_KEY
```

### 11.3 Debug

```bash
# Recent logs
curl -b cookies.txt 'https://adsfb.namnan.co.th/admin/debug-logs?lines=100'

# Cron status
curl -b cookies.txt https://adsfb.namnan.co.th/admin/debug-crons

# Run SQL (read-only, with guardrails)
curl -b cookies.txt -X POST -H "Content-Type: application/json" \
  -d '{"sql":"SELECT COUNT(*) FROM pages"}' \
  https://adsfb.namnan.co.th/admin/debug-query
```

### 11.4 Safety Guards

```bash
# Get all guards
curl -b cookies.txt https://adsfb.namnan.co.th/admin/safety-guards

# Response:
# {
#   "guards": [
#     { "name": "max_budget_delta_pct", "value": 50, "active": true },
#     { "name": "max_scale_per_day", "value": 3, "active": true },
#     { "name": "min_roas_for_scale", "value": 1.2, "active": true }
#   ]
# }
```

---

## 12. Cron Jobs (Background Tasks)

| Cron | Schedule | Task | Source |
|------|----------|------|--------|
| Metrics collection | `5 * * * *` (every hour at :05) | Pull hourly metrics from FB | `src/cron/hourlyMetrics.ts` |
| Chat poller | `*/180 * * * *` (every 3h) | Pull chats from all pages | `src/cron/chatPollerCron.ts` |
| Page sync | `*/60 * * * *` (every 1h) | Refresh page cache | `src/cron/chatPollerCron.ts` |
| Auto-scale | `*/15 * * * *` (every 15min) | Check & apply rules | `src/cron/autoScale.ts` |
| Auto-pause | `0 * * * *` (every hour) | Check pause triggers | `src/cron/autoPause.ts` |
| Chat attribution | `*/30 * * * *` (every 30min) | Link chat → ad | `src/cron/chatAttribution.ts` |
| Orphan resolution | `0 */4 * * *` (every 4h at :00) | Resolve orphan chats | `src/cron/orphanResolver.ts` |
| Midnight reset | `5 0 * * *` (00:05 ICT daily) | Reapply midnight rules | `src/cron/midnightReset.ts` |
| Forecast eval | `0 3 * * *` (03:00 daily) | Evaluate forecast accuracy | `src/cron/forecastEval.ts` |
| Realloc snapshot | `0 */6 * * *` (every 6h at :00) | Snapshot realloc state | `src/cron/reallocSnapshot.ts` |
| Chat alert detection | `*/15 * * * *` (every 15min) | Detect chat anomalies | `src/cron/chatAlerts.ts` |
| DB checkpoint | `0 3 * * *` (03:00 daily) | SQLite WAL checkpoint | `src/cron/dbCheckpoint.ts` |
| Token rotation | `0 4 * * *` (04:00 daily) | Rotate FB tokens if needed | `src/cron/tokenRotation.ts` |
| Webhook retry | `*/5 * * * *` (every 5min) | Retry failed webhooks | `src/cron/webhookRetry.ts` |
| Cleanup | `0 2 * * *` (02:00 daily) | Clean old logs | `src/cron/cleanup.ts` |
| Health pusher | `*/1 * * * *` (every min) | Push health to monitoring | `src/cron/healthPusher.ts` |

**Total crons**: 16+

**Check status**:
```bash
curl -b cookies.txt https://adsfb.namnan.co.th/admin/debug-crons
```

---

## 13. คำสั่งที่ใช้บ่อย

### 13.1 SSH & Files

```bash
# SSH to prod server
ssh root@103.22.183.111  # password: 2$3Z7Gf1#9hv

# View live logs
journalctl -u adsfb -f

# Restart service
systemctl restart adsfb

# View env
cat /opt/adsfb/.env

# Check process
ps aux | grep -E 'node|tsx' | grep -v grep
```

### 13.2 MySQL

```bash
# Connect
mysql -h 127.0.0.1 -u adsfb_user -p adsfb

# Common queries
SELECT COUNT(*) FROM pages;
SELECT * FROM chat_messages ORDER BY created_at DESC LIMIT 10;
SELECT campaign_id, SUM(spend) FROM metrics_hourly_campaign
  WHERE hour_start >= NOW() - INTERVAL 7 DAY
  GROUP BY campaign_id ORDER BY 2 DESC;
```

### 13.3 Git

```bash
cd /opt/adsfb
git status
git log --oneline -20
git add -A && git commit -m "feat: ..."
git push origin main
```

### 13.4 Testing

```bash
# Smoke test all URLs
for url in /live /app /V1/live-dashboard /admin-control /hourly-dashboard /budget-analysis; do
  echo -n "$url: "
  curl -s -o /dev/null -w "%{http_code}\n" https://adsfb.namnan.co.th$url
done

# Test API auth
curl -H "X-Admin-Key: chp_3Y4Th4MyHSsqwtJsp-qSxcr3htXm-J7K" \
  https://adsfb.namnan.co.th/admin/chat-poller/status | jq

# Test login
curl -c /tmp/cookies.txt -X POST -H "Content-Type: application/json" \
  -d '{"username":"sj88","password":"SJja0238@@2027"}' \
  https://adsfb.namnan.co.th/auth/login
```

---

## 14. Troubleshooting

### 14.1 Login fails

```bash
# 1. Check user exists
mysql -e "SELECT id, username, role FROM auth_users WHERE username='sj88';"

# 2. Check password hash format
mysql -e "SELECT password_hash FROM auth_users WHERE username='sj88';"
# Should start with "scrypt:"

# 3. Test directly
curl -X POST -H "Content-Type: application/json" \
  -d '{"username":"sj88","password":"SJja0238@@2027"}' \
  https://adsfb.namnan.co.th/auth/login -v
```

### 14.2 Page returns 404

```bash
# 1. Check nginx config
grep -A 3 "location = /<url>" /etc/nginx/sites-enabled/adsfb.namnan.co.th

# 2. Check file exists
ls -la /opt/adsfb/public/<file>.html

# 3. Test nginx config
nginx -t

# 4. Reload
nginx -s reload
```

### 14.3 API returns 500

```bash
# 1. Check service status
systemctl status adsfb

# 2. Check logs
journalctl -u adsfb --since "5 minutes ago"

# 3. Check DB
mysql -e "SHOW PROCESSLIST;"
```

### 14.4 Chat poller not running

```bash
# 1. Check API status
curl -H "X-Admin-Key: chp_..." https://adsfb.namnan.co.th/admin/chat-poller/status

# 2. Check token
mysql -e "SELECT id, name, is_active FROM pages WHERE access_token IS NULL;"

# 3. Manual trigger
curl -X POST -H "X-Admin-Key: chp_..." https://adsfb.namnan.co.th/admin/chat-poller/trigger

# 4. Check logs
journalctl -u adsfb --since "1 hour ago" | grep -i chat
```

### 14.5 V1 URL 404

ตรวจสอบว่า nginx config มี `location = /V1/<name>` block หรือไม่:

```bash
grep "location = /V1" /etc/nginx/sites-enabled/adsfb.namnan.co.th
```

ถ้าไม่มี ให้เพิ่ม (ดูตัวอย่างใน section 3.4) แล้ว `nginx -t && nginx -s reload`

---

## 15. Change Log

### 2026-08-01 — v1.0

**Added**:
- ✓ User `sj88` created (admin role, scrypt hash)
- ✓ Login tested 3/3 PASS (correct / wrong / cookie)
- ✓ Session cookie works with admin endpoints
- ✓ V1 URL pattern (`/V1/*`) — replaces `/demo/*`
- ✓ 12 new clean URLs (no .html suffix)
- ✓ 11 `/V1/*` aliases
- ✓ 301 redirect `/demo/live-dashboard` → `/V1/live-dashboard`
- ✓ Fixed duplicate `location = /admin-control.html` (caused nginx reload to silently fail)

**Verified**:
- ✓ 20/20 E2E testcases PASS (TC01-TC20)
- ✓ All 12 main pages load (200 OK)
- ✓ All 7 public APIs return 200
- ✓ All 5 chat-poller auth scenarios PASS
- ✓ Session cookie can access admin endpoints
- ✓ Logout clears session properly
- ✓ API key works as alternative auth
- ✓ 4 error scenarios correctly rejected (no-auth 401, wrong-key 401, etc.)

**Manual created**: `/workspace/sj88-verify/docs/manuals/SJ88-USER-MANUAL.md`

---

## ภาคผนวก A: Quick Reference Card

```
┌────────────────────────────────────────────────────────────┐
│  SJ88 Quick Reference                                       │
├────────────────────────────────────────────────────────────┤
│  Username     : sj88                                        │
│  Password     : SJja0238@@2027                              │
│  Role         : admin                                       │
│  Login URL    : https://adsfb.namnan.co.th/demo/login       │
│  Main URL     : https://adsfb.namnan.co.th/V1/live-dashboard│
│  Admin URL    : https://adsfb.namnan.co.th/V1/admin         │
│  API Key      : chp_3Y4Th4MyHSsqwtJsp-qSxcr3htXm-J7K        │
│  Header       : X-Admin-Key: <key>                          │
│  Cookie       : wa_session (4h TTL)                         │
│  Server       : root@103.22.183.111 (pwd: 2$3Z7Gf1#9hv)     │
│  App Dir      : /opt/adsfb                                  │
│  Public Dir   : /opt/adsfb/public/                          │
│  Logs         : journalctl -u adsfb -f                      │
│  Service      : systemctl restart adsfb                     │
├────────────────────────────────────────────────────────────┤
│  URL Patterns                                               │
│  /V1/live-dashboard  →  live.html (default)                 │
│  /V1/app             →  app.html                            │
│  /V1/hourly          →  hourly-dashboard.html               │
│  /V1/analytics       →  analytics.html                      │
│  /V1/scale           →  scale.html                          │
│  /V1/admin           →  admin-control.html                  │
│  /V1/budget          →  budget-analysis.html                │
│  /demo/live-dashboard → 301 → /V1/live-dashboard            │
├────────────────────────────────────────────────────────────┤
│  Test Result: 20/20 PASS (TC01-TC20)                        │
│  Run ID: sj88_full_system_qa_20260801_152500                │
│  Duration: 4 min 9 sec                                      │
└────────────────────────────────────────────────────────────┘
```

---


---

## §22 Chat Poller — Critical Bug Fixes (2026-08-05)

### What happened (TL;DR)
3+ days of SJ88 chat data was stale. Poller reported `messagesInserted: 0` even though Meta API had hundreds of new messages. Two bugs in `src/live/chatPoller.ts`.

### Bug #1: SQL values mismatch (lines 222-237)
```sql
-- BROKEN (16 ? for 17 columns, 15 values):
INSERT INTO chat_messages
  (conversation_id, page_id, page_name, message_id, story_id, sender_id, sender_name, message_text, attachments_json, reactions_json, reaction_count, is_from_page, received_at, source, raw_json, campaign_id, has_attachments)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)  -- 16 placeholders!
-- ... 15 values ...
```

MySQL throws `ERROR 1136 (21S01): Column count doesn't match value count`. Poller status was reporting "success" but catching the error silently.

**Fix**:
```sql
-- FIXED (17 ? for 17 columns, 17 values):
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
-- ... 17 values, including msg.reactionsJson, msg.reactionCount ...
```

### Bug #2: Meta API rejects `reactions` field
```js
// BROKEN:
const fields = "id,created_time,from,to,message,is_echo,attachments,story,reactions";
```

For many pages (including 108498612159613, 100378742834311, 102601625742282, etc.), Meta API returns:
```json
{"error":{"message":"(#100) เพจนี้ไม่ได้ลิงก์กับบัญชี Instagram หรือบัญชี IG ที่ลิงก์..."}}
```

The error is misleading — it's actually about the `reactions` field, not Instagram linking. Removing `reactions` makes the same call return 2 messages successfully.

**Fix**:
```js
const fields = "id,created_time,from,to,message,is_echo,attachments,story";
```

### Defensive: HTTP error logging
```js
// Before: silent
if (!r.ok) break;

// After: warn with status + conv
if (!r.ok) { console.warn("[chat-poller] msg fetch HTTP " + r.status + " conv=" + convId); break; }
```

### Impact

| Metric | Before | After |
|---|---|---|
| Poller msgs/run | 0 | **20,480** |
| Total chat_messages | 17,760 | **44,079** |
| SJ88 messages 8/2+ | 0 | **429** |
| SJ88 messages 8/4 | 0 | **26** |
| Convs missing from DB | 50+ | 0 |
| Poller errors logged | 0 (silent) | 0 (caught + logged) |

### Detection (next time)
- Poller status endpoint: `conversationsFound > 0 && messagesInserted == 0` → BUG
- Daily cron: `MAX(received_at) < NOW() - INTERVAL 24 HOUR` → STALE alert
- New health metric: `poller.lastResult.messagesInserted > 0` (boolean health check)

### Files
- `src/live/chatPoller.ts` (modified)
- `docs/CHANGELOG-2026-08-05.md` (full details)

---

**END OF MANUAL** | Generated 2026-08-01 15:25 ICT | Project: Facebooklnwadss88
