# API Reference — WepAppOpusApi

> Auto-generated reference for all 96 endpoints.
> Base URL: `https://adsfb.namnan.co.th`
> Live updated: 2026-07-31

## Table of Contents
- [Auth](#-auth)
- [Public](#-public-no-auth)
- [Live](#-live-session-required)
- [Admin](#-admin)
- [Webhooks](#-webhooks)
- [Demo Pages](#-demo-pages)

---

## 🔐 Auth

### `POST /auth/login`
Login with username/password.

**Request body:**
```json
{
  "username": "admin",
  "password": "oSu86wowB10fZT6rjOc3Q7Ap"
}
```

**Response 200:**
```json
{
  "ok": true,
  "user": { "id": 1, "username": "admin", "role": "admin" }
}
```

**Errors:**
- `401 Invalid credentials`

---

### `GET /auth/session`
Check current session (used by frontend to detect login state).

**Response 200:**
```json
{
  "ok": true,
  "session": { "userId": 1, "username": "admin", "role": "admin" }
}
```

**Response 401:** No active session

---

### `POST /auth/change-password`
Change own password.

**Request body:**
```json
{ "oldPassword": "...", "newPassword": "..." }
```

**Response 200:** `{ "ok": true }`

---

## 🌐 Public (no auth)

### `POST /public/live-dashboard`
Full dashboard data — KPIs, time series, campaign list, funnel, heatmap, distribution.

**Request body:** `{}` (empty for default)

**Response:**
```json
{
  "ok": true,
  "kpis": { "spend": 610, "revenue": 58210, "roas": 95.4, ... },
  "daily": [{ "date": "2026-07-10", "spend": 152, "revenue": 400, "roas": 2.6 }, ...],
  "campaigns": [...],
  "reports": {
    "campaignDecision": { "winners": [...], "losers": [...] },
    "funnel": [...],
    "heatmap": [...],
    "distribution": [...],
    ...
  }
}
```

---

### `GET /public/campaigns`
List all campaigns (no PII).

**Response:**
```json
{
  "ok": true,
  "accountId": "act_2807991909289645",
  "campaigns": [
    {
      "id": "120230799572200590",
      "name": "[buy]tsm ไฟตุ้มโซล่าเซลล์",
      "status": "ACTIVE",
      "effectiveStatus": "ACTIVE",
      "dailyBudgetThb": 80,
      "objective": "OUTCOME_ENGAGEMENT"
    }
  ]
}
```

---

### `GET /public/chats?days=7`
Aggregate Messenger conversations across all pages.

**Query params:**
- `days` (default 7, max 30)

**Response:**
```json
{
  "ok": true,
  "publicMode": true,
  "days": 7,
  "totalConversations": 42,
  "totalMessages": 156,
  "totalUnread": 3,
  "perPage": [
    { "pageId": "408865435647368", "pageName": "เฮียหนวด การเกษตร", "count": 12, "unread": 0 }
  ],
  "perCampaign": [
    { "campaignId": "120208734886640590", "campaignName": "...", "count": 8, "messages": 23, "lastTime": "..." }
  ],
  "recentConversations": [
    { "id": "...", "pageName": "...", "participantName": "User 1234", "snippet": "...", "updatedTime": "..." }
  ]
}
```

---

### `GET /public/chat-stats?days=30`
Daily chat aggregates.

**Response:**
```json
{
  "ok": true,
  "days": 30,
  "totals": { "total_conversations": 156, "total_messages": 482, "total_unread": 5 },
  "perDay": [{ "date": "2026-07-10", "conversations": 12, "messages": 34 }],
  "perCampaign": [
    { "campaign_id": "1202...", "page_name": "...", "conversations": 8, "last_seen": "..." }
  ]
}
```

---

## 🔒 Live (session required)

### `POST /live/campaigns/bulk`
Bulk scale (with safety guards).

**Auth:** admin

**Request body:**
```json
{
  "actions": [
    { "campaignId": "120230799572200590", "action": "scale_percent", "percent": 30 },
    { "campaignId": "120230799572200590", "action": "set_budget", "newBudget": 80, "currentBudget": 60 },
    { "campaignId": "120230799572200590", "action": "resume" }
  ],
  "reason": "End of weekend push",
  "skipCapCheck": false
}
```

**Response 200:**
```json
{
  "ok": true,
  "total": 3,
  "successCount": 3,
  "errorCount": 0,
  "results": [
    { "campaignId": "...", "action": "scale_percent", "ok": true, "before": {...}, "after": {...}, "percent": 30 }
  ],
  "bulkId": "bulk-1783792100000-abc123"
}
```

**Response 400 (Safety Guard Blocked):**
```json
{
  "ok": false,
  "errorCode": "SAFETY_GUARD_BLOCKED",
  "error": "1 action(s) blocked by safety guards",
  "violations": [
    { "campaignId": "...", "action": "pause", "error": "PAUSE_DISABLED_GLOBALLY", "message": "Pause is globally disabled" }
  ],
  "config": { "minSafeBudget": 39, "blockPause": true, "blockScaleDown": true, "pauseDisabledGlobally": true }
}
```

**Error codes:**
- `SAFETY_GUARD_BLOCKED` (paused globally, paused at min, or scale-down)
- `DAILY_TOTAL_CAP_EXCEEDED`
- `PRODUCT_DAILY_CAP_EXCEEDED`
- `BUDGET_INVALID`
- `campaignId + action required`

---

### `GET /live/scale-recommendations`
AI recs based on 7d vs 7d trend.

**Response:**
```json
{
  "ok": true,
  "scaleUp": [
    { "id": "120...", "name": "...", "roas": 34.5, "trend": "new", "trendPct": 100, "suggestedBudget": 80, "suggestion": "resume" }
  ],
  "scaleDown": [],
  "resume": [...],
  "totalAnalyzed": 1,
  "trendWindow": "recent 7d vs older 7d"
}
```

---

### `POST /live/scale-forecast`
Calculate forecast impact for proposed actions.

**Request body:**
```json
{
  "actions": [
    { "campaignId": "120...", "newBudget": 80, "currentBudget": 60 }
  ]
}
```

**Response:**
```json
{
  "ok": true,
  "totalCurrent": 60,
  "totalNew": 80,
  "netBudgetDelta": 20,
  "totalEstimatedDailySpend": 49,
  "totalEstimatedDailyRevenue": 1691,
  "estimatedROAS": 34.5,
  "perCampaign": [{ "campaignId": "...", "monthlyImpact": { "daysRemaining": 20, "extraSpend": 210, "extraRevenue": 7265 } }]
}
```

---

### `GET /live/scale-history?limit=20`
Recent scale actions (last N).

**Response:**
```json
{
  "ok": true,
  "history": [
    { "id": 42, "campaign_id": "120...", "action_type": "set_budget", "before_budget_thb": 76, "after_budget_thb": 80, "actor": "admin", "source": "manual", "created_at": "..." }
  ]
}
```

---

### `POST /live/scale-history/:id/undo`
Revert a scale action.

**Response 200:** `{ "ok": true, "undone": true }`

---

### `GET /live/scale-schedules?status=pending`
List scheduled changes.

**Response:**
```json
{
  "ok": true,
  "schedules": [
    { "id": 5, "campaign_id": "120...", "action_type": "set_budget", "scheduled_at": "2026-07-15T10:00:00", "expires_at": "2026-07-22T10:00:00", "status": "pending", "recurring_pattern": "weekly" }
  ]
}
```

---

### `POST /live/scale-schedules`
Create a scheduled change. **Blocks pause action.**

**Request body:**
```json
{
  "campaignId": "120...",
  "actionType": "set_budget",
  "newBudget": 100,
  "scheduledAt": "2026-07-15T10:00:00",
  "expiresAt": "2026-07-22T10:00:00",
  "revertAction": { "action": "set_budget", "newBudget": 60 },
  "recurringPattern": "weekly",
  "reason": "Weekend boost"
}
```

**Response 200:** `{ "ok": true, "id": 5 }`
**Response 403:** `PAUSE_DISABLED_GLOBALLY` (if actionType="pause")

---

### `GET /live/auto-scale-rules`
List automation rules.

**Response:**
```json
{
  "ok": true,
  "rules": [
    { "id": 1, "name": "Pause low-ROAS", "scope_type": "campaign", "scope_id": "120...", "condition_type": "roas_below", "condition_value": 1, "action_type": "pause", "enabled": 1, "last_evaluated_at": "..." }
  ]
}
```

---

### `POST /live/auto-scale-rules`
Create rule. **Blocks `actionType: "pause"`.**

**Request body:**
```json
{
  "name": "Scale high-ROAS",
  "scopeType": "campaign",
  "scopeId": "120...",
  "conditionType": "roas_above",
  "conditionValue": 5,
  "conditionDays": 3,
  "actionType": "scale_percent",
  "actionValue": 30,
  "maxPerDay": 1,
  "maxPctChange": 50
}
```

**Valid condition types:** `roas_above`, `roas_below`, `cpa_above`, `cpa_below`, `spend_no_conv`
**Valid action types:** `scale_percent`, `set_budget`, `pause`, `resume`
**Valid scopes:** `campaign`, `product`

---

### `GET /live/forecast/accuracy?days=30`
Accuracy summary for forecast predictions.

**Response:**
```json
{
  "ok": true,
  "days": 30,
  "stats": {
    "total_measurements": 15,
    "avg_spend_drift": 12.4,
    "avg_revenue_drift": 18.7,
    "avg_roas_drift": 15.3,
    "within_10pct": 5,
    "within_25pct": 8,
    "beyond_25pct": 7
  }
}
```

---

### `GET /live/forecast/measurements?limit=20`
Recent measurements (predicted vs actual).

**Response:**
```json
{
  "ok": true,
  "count": 15,
  "measurements": [
    { "prediction_id": 1, "campaign_id": "120...", "campaign_name": "...", "predicted_spend": 80, "actual_spend": 75, "predicted_revenue": 4000, "actual_revenue": 3500, "revenue_drift_pct": -12.5, "predicted_roas": 50, "actual_roas": 46.7 }
  ]
}
```

---

### `GET /live/chat-stream` (SSE)
Real-time chat updates via Server-Sent Events.

**Query params:** `pages=id1,id2,id3` (optional filter)

**Event types:**
- `connected` — initial connection
- `message` — new chat message
- `heartbeat` — every 30s (keep-alive)

**Example event:**
```
event: message
data: {"conversationId":"...","pageId":"...","pageName":"...","senderId":"USER_123","messageText":"สวัสดี","timestamp":"2026-07-12T01:30:00Z","campaignId":null}
```

**Browser usage:**
```js
const evt = new EventSource('/live/chat-stream');
evt.addEventListener('message', e => {
  const data = JSON.parse(e.data);
  console.log('New message:', data.messageText);
});
```

---

## 🔧 Admin

### `GET /admin/safety-guards`
Current safety config.

**Response:**
```json
{
  "ok": true,
  "guards": {
    "minSafeBudget": 39,
    "blockPause": true,
    "blockScaleDown": true,
    "pauseDisabledGlobally": true
  },
  "pauseGloballyDisabled": true,
  "midnightReset": { "enabled": true, "resetBudget": 39 }
}
```

---

### `POST /admin/midnight-reset/run`
Manually trigger midnight reset.

**Response:**
```json
{ "ok": true, "resetCount": 27, "resetBudget": 39, "errors": 17 }
```

---

### `GET /admin/webhooks`
List all webhooks.

**Response:**
```json
{
  "ok": true,
  "events": ["scale.applied", "scale.undone", "auto_rule.executed", "forecast.drift", ...],
  "webhooks": [
    { "id": 1, "name": "Slack", "url": "https://hooks.slack.com/...", "events": ["scale.applied"], "enabled": 1, "total_deliveries": 42, "successful_deliveries": 41 }
  ]
}
```

---

### `POST /admin/webhooks`
Create webhook.

**Request body:**
```json
{
  "name": "Slack Production",
  "url": "https://hooks.slack.com/services/T00/B00/xxx",
  "events": ["scale.applied", "auto_rule.executed", "forecast.drift"],
  "secret": "my-secret-key",
  "description": "Notify on scale events"
}
```

**Valid events:** `scale.applied`, `scale.undone`, `scale.failed`, `auto_rule.matched`, `auto_rule.executed`, `schedule.executed`, `schedule.failed`, `cap.violated`, `backfill.completed`, `forecast.drift`

---

### `POST /admin/webhooks/:id/test`
Send a test event to webhook.

**Response:**
```json
{ "ok": true, "status": 200 }
```

---

### `POST /admin/metrics/backfill`
Backfill historical daily metrics.

**Request body:**
```json
{ "days": 30 }
```

**Response:**
```json
{
  "ok": true,
  "days": 30,
  "runs": 30,
  "results": [{ "filteredCampaigns": 115, "insertedCount": 115, "durationMs": 234, ... }]
}
```

**Note:** days capped at 30. Runs in foreground (~5-7 min for 30d).

---

## 📡 Webhooks (incoming from Facebook)

### `GET /webhook/facebook`
Facebook webhook verification.

**Query params:**
- `hub.mode` must be `subscribe`
- `hub.verify_token` must match `FB_WEBHOOK_VERIFY_TOKEN` env var
- `hub.challenge` returned as plain text

---

### `POST /webhook/facebook`
Facebook event delivery.

**Headers:**
- `X-Hub-Signature: sha1=<hmac>` (if `FB_APP_SECRET` set)

**Body (Messenger):**
```json
{
  "object": "page",
  "entry": [{
    "id": "408865435647368",
    "messaging": [{
      "sender": { "id": "USER_12345" },
      "recipient": { "id": "408865435647368" },
      "timestamp": 1700000000000,
      "message": {
        "mid": "m_abc",
        "text": "สวัสดี",
        "referral": { "ad_id": "120208734886640590" }
      }
    }]
  }]
}
```

**Response:** `{ "ok": true, "processed": 1, "webhookVerified": true }`

---

## 🌐 Demo Pages

All served as static HTML from `/opt/adsfb/public/`:

| URL | File | Auth |
|-----|------|------|
| `/` | `index.html` | public |
| `/live` | `live.html` | public (live dashboard) |
| `/demo/login` | `login.html` | public |
| `/demo/live-dashboard` | `dashboard.html` | session |
| `/demo/scale` | `scale.html` | session |
| `/demo/analytics` | `analytics.html` | session |
| `/manual.html` | `manual.html` | public |
| `/api-guide.html` | `api-guide.html` | public |
| `/changelog.html` | `changelog.html` | public |

---

## 📊 Status Codes

| Code | Meaning | Used For |
|------|---------|----------|
| 200 | OK | Successful request |
| 302 | Redirect | Unauth → /demo/login |
| 400 | Bad Request | Validation error, cap exceeded, safety guard blocked |
| 401 | Unauthorized | No session / invalid credentials |
| 403 | Forbidden | Safety guard (pause globally disabled) |
| 404 | Not Found | Unknown endpoint |
| 500 | Internal Error | Server error (check logs) |

---

## 🧪 Testing

### cURL Examples

```bash
# Login
curl -X POST https://adsfb.namnan.co.th/auth/login \
  -H "Content-Type: application/json" \
  -c /tmp/cookies.txt \
  -d '{"username":"admin","password":"oSu86wowB10fZT6rjOc3Q7Ap"}'

# Get recommendations
curl -b /tmp/cookies.txt https://adsfb.namnan.co.th/live/scale-recommendations

# Scale up
curl -b /tmp/cookies.txt -X POST https://adsfb.namnan.co.th/live/campaigns/bulk \
  -H "Content-Type: application/json" \
  -d '{"actions":[{"campaignId":"120230799572200590","action":"scale_percent","percent":30}]}'

# Subscribe to chat stream
curl -N https://adsfb.namnan.co.th/live/chat-stream
```

---

**Last Updated:** 2026-07-31

## v1.1 (2026-07-31) Updates
- **Heatmap Hourly Metrics** — per-hour `delta_spend` fixed (was always negative)
- See `docs/CHANGELOG-2026-07-31.md` for full details
- See `docs/reports/heatmap_hourly_dashboard_qa_20260731_140800/` for QA evidence

