# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Nguyên tắc trả lời (đọc trước khi làm bất kỳ việc gì)

- Luôn trả lời bằng tiếng Việt.
- Trước khi sửa code: đưa ra kế hoạch ngắn gọn và hỏi lại người dùng; chỉ tiến hành thay đổi khi được xác nhận.
- Trả lời đúng trọng tâm, ngắn gọn, dễ hiểu, không lòng vòng.
- Luôn cân nhắc các trường hợp biên (edge case) và ngoại lệ liên quan đến thay đổi/đề xuất trước khi chốt giải pháp.

## Tổng quan dự án

Đây là ứng dụng nội bộ **giám sát domain / server / API** (Laravel 12, PHP 8.2, MySQL). Ba khối chức năng chính:

1. **API healthcheck**: command `apis:healthcheck` (chạy mỗi 10 phút qua scheduler) gọi từng API đang active, đo response time, so mã trả về với `success_status`, gửi cảnh báo Telegram nếu fail hoặc không kết nối được. Mỗi lần check đều lưu lịch sử (status/response time/lỗi) vào bảng `api_health_checks` để phục vụ thống kê.
2. **Metric ingest**: agent Python (`Monitor_agent/`, repo git riêng — xem mục riêng dưới) chạy trên server được giám sát, mỗi 5 phút POST CPU/RAM/DISK usage vào `POST /metrics` (xác thực bằng header `X-API-Token`), lưu vào bảng `server_metrics`. Vượt ngưỡng (RAM/CPU ≥ 80%, DISK ≥ 90%, một số IP đặc thù có ngưỡng RAM riêng qua `MONITORING_HIGH_RAM_IPS`) sẽ gửi cảnh báo Telegram. Agent còn tự phát hiện cao tải (ngưỡng riêng cấu hình trong `.env` của agent, độc lập với ngưỡng Telegram phía server) để thu thập tiến trình/log, gửi tới `POST /incidents` (lưu bảng `server_incidents`/`incident_processes`/`incident_logs`) — xem mục "Monitor_agent" dưới.
3. **Dashboard quản trị** (Blade, cần đăng nhập): CRUD Domain/Server/API, trang Overview (tổng quan hôm nay), Details (biểu đồ theo domain, filter theo ngày/domain), Tổng Quan API Health (bảng thống kê tổng check/tỉ lệ thành công/response time từng API, filter theo ngày trong retention window, kèm trang chi tiết theo giờ + breakdown lỗi cho từng API/ngày) và Cao Tải Server (danh sách sự cố cao tải theo ngày/domain/IP, kèm trang chi tiết top tiến trình + log liên quan).

Phân cấp dữ liệu: `Domain` → `Server` (IP) → `Api` (endpoint healthcheck, có `ApiHealthCheck` time-series riêng), → `ServerMetric` (time-series) và → `ServerIncident` (sự cố cao tải, có `IncidentProcess`/`IncidentLog` con).

## Hai môi trường — khác nhau về stack, không chỉ khác cấu hình

### Local (Docker Compose)

```
┌──────────────────────────┐
│  Browser (máy host)      │
└────────────┬─────────────┘
             │ http://localhost:8000
             ▼
┌────────────────────────────────────────────┐
│ Container: monitoring-app                  │
│ Image: php:8.2-apache                      │
│ (Apache + mod_php — KHÔNG có PHP-FPM riêng)│
│ DocumentRoot: /var/www/html/public         │
│ Volume: .:/var/www/html (code sync 2 chiều)│
└────────────┬────────────────────────────────┘
             │ TCP 3306 (docker network "monitoring")
             ▼
┌────────────────────────────────────────────┐
│ Container: mySqldb-monitoring              │
│ Image: mysql:8.0                           │
│ Port: host 3308 → container 3306           │
│ Volume data: mysql-monitoring-data         │
│ Volume init: database/init/monitor_db.sql  │
│   → /docker-entrypoint-initdb.d/init.sql   │
│   (chỉ import 1 LẦN khi volume data rỗng)  │
└────────────────────────────────────────────┘
```

⚠ Container app không có cron/supervisor nào → scheduler (`apis:healthcheck`, `metrics:cleanup`) **không tự chạy**; muốn test phải tự gọi `docker compose exec monitoring-app php artisan schedule:run` (hoặc gọi trực tiếp từng command).

### Staging (Linux thật, không Docker)

```
┌──────────────────────────┐
│  Browser / Agent ngoài   │
└────────────┬─────────────┘
             │ https://<domain>
             ▼
┌────────────────────────────────────────────┐
│ Apache (mod_proxy_fcgi)                    │
└────────────┬────────────────────────────────┘
             │ fastcgi (socket/TCP)
             ▼
┌────────────────────────────────────────────┐
│ PHP-FPM (pool riêng, tách process khỏi     │
│ Apache)                                    │
│ DocumentRoot: .../public                   │
└────────────┬────────────────────────────────┘
             ▼
┌────────────────────────────────────────────┐
│ MySQL (service cài trực tiếp trên OS)      │
└────────────────────────────────────────────┘

┌────────────────────────────────────────────┐
│ crontab: * * * * * php artisan schedule:run│
│  → apis:healthcheck  (mỗi 10 phút)         │
│  → metrics:cleanup   (CN hàng tuần, 00:00) │
└────────────────────────────────────────────┘
```

Ngoài luồng HTTP chính còn hai luồng độc lập chạy qua cùng PHP-FPM: agent trên server giám sát `POST /metrics` (header `X-API-Token`), và healthcheck/vượt ngưỡng gửi cảnh báo ra Telegram Bot API (ra internet, không qua Apache).

⇒ Khi sửa bất cứ thứ gì liên quan đến PHP process, php.ini, upload limit, opcache, hay hành vi worker/restart, nhớ rằng **hai môi trường có process model PHP khác nhau** (mod_php gộp trong Apache vs PHP-FPM tách riêng) — cấu hình/khắc phục sự cố ở môi trường này không nhất thiết áp dụng được cho môi trường kia. Cron thật trên staging phải trỏ đúng PHP-FPM/CLI binary đang dùng.

## Lệnh thường dùng

```bash
# Cài đặt lần đầu
composer install
cp .env.example .env && php artisan key:generate
php artisan migrate --seed          # seeder tạo admin user (AdminUserSeeder)

# Local qua Docker
docker compose up -d --build
docker compose exec monitoring-app php artisan migrate
docker compose exec monitoring-app php artisan <command>

# Chạy thủ công các artisan command của domain nghiệp vụ
php artisan apis:healthcheck   # healthcheck toàn bộ API, đo response time, lưu api_health_checks, gửi Telegram nếu fail
php artisan metrics:cleanup    # xoá server_metrics, api_health_checks VÀ server_incidents (kèm tiến trình/log) cũ hơn METRICS_RETENTION_DAYS (mặc định 7 ngày)
```

## Kiến trúc / luồng dữ liệu

Tầng chuẩn cho mọi CRUD:

```
Request → FormRequest (validate) → Controller → Service (business logic) → Repository (BaseRepository) → Model
```

**Luồng healthcheck** (scheduler mỗi 10 phút):

```
apis:healthcheck → ApiHealthCheckService.checkAll()
  → mỗi API active: đo thời gian (microtime) quanh request, gọi HTTP (ghim IP qua
    CURLOPT_RESOLVE, đổi Host/SNI sang domain — né chặn gọi trực tiếp bằng IP / lỗi
    SSL Cloudflare Origin CA)
    ├─ status ∈ success_status → OK
    └─ lỗi / không khớp → TelegramService.send() (fail-soft, không throw)
  → luôn lưu 1 record vào ApiHealthCheck (http_status, is_success, response_time_ms,
    error_message, checked_at UTC) — fail-soft, lỗi ghi DB không chặn luồng check/Telegram
```

**Luồng thống kê API Health** — 2 trang (`resources/views/api-health-stats/{index,day}.blade.php`), route `api-health-stats`, `api-health-stats.day`:

```
index  → ApiHealthStatsService.getStatsForDate($date) — bảng TẤT CẢ API cho ĐÚNG 1 NGÀY (filter chọn
  ngày, mặc định hôm nay, chỉ cho chọn trong đúng services.metrics.retention_days ngày gần nhất)
  → eager-load Domain → Server → Api (tránh N+1)
  → 1 aggregate query DÙNG CHUNG cho mọi API trong khoảng [đầu ngày, cuối ngày] ($date theo
    display_timezone, convert UTC khi query): tổng số check, tỉ lệ thành công, avg/min/max
    response_time_ms (bảng — không có chart). Nút "Chi tiết" mỗi dòng dẫn thẳng sang `day` cho
    đúng API + đúng ngày đang filter; disable nếu API đó không có check nào trong ngày.

day    → ApiHealthStatsService.getDayDetailForApi($api, $date) — chi tiết 1 ngày: tỉ lệ thành công
  + avg response time theo TỪNG GIỜ (0h-23h), breakdown lỗi (gồm lỗi kết nối, http_status=null gộp
  1 nhóm qua groupBy) kèm % trên tổng check và % trên tổng lỗi. Data bound theo 1 API × 1 ngày nên
  luôn nhỏ, không cần lo N+1 dù domain/server/API tăng nhiều.
```

Mốc ngày (cả filter ở `index` và query ở `day`) luôn lấy ở ĐẦU/CUỐI NGÀY HIỂN THỊ (`display_timezone`),
KHÔNG dùng "now() - N ngày" theo giờ tuyệt đối — tránh lặp lại lỗi cắt ngang ngày do lệch UTC vs
display_timezone (đã gặp ở Details, xem lịch sử sửa).

**Luồng ingest metric** (agent trên server giám sát gọi vào):

```
Agent → POST /metrics (header X-API-Token, middleware VerifyApiToken)
  → MetricIngestService.ingest()
    ├─ firstOrCreate Domain/Server theo domain/ip (auto-provision nếu chưa có)
    ├─ lưu ServerMetric (recorded_at lưu UTC)
    └─ vượt ngưỡng RAM/CPU/DISK → TelegramService.send()
```

**Luồng ingest incident cao tải** (agent gửi khi CPU/RAM/DISK vượt ngưỡng riêng của agent — xem mục
"Monitor_agent" dưới; độc lập với ngưỡng Telegram ở luồng metric trên):

```
Agent → POST /incidents (header X-API-Token, middleware VerifyApiToken)
  → IncidentIngestService.ingest()
    ├─ firstOrCreate Domain/Server theo domain/ip (auto-provision, giống MetricIngestService)
    ├─ lưu ServerIncident (status: started/ongoing/recovered, cpu/ram/disk_usage_pct,
    │  breached_metrics JSON, recorded_at UTC)
    └─ nếu status ≠ recovered: bulk insert IncidentProcess (top tiến trình) + IncidentLog
       (log tail, lines nối thành 1 chuỗi content, kèm line_count/truncated/error) —
       payload "recovered" không kèm 2 phần này (agent không gửi vì không còn cần thiết)
```

**Trang "Cao Tải Server"** (`resources/views/incidents/{index,show}.blade.php`, route
`incidents.index`, `incidents.show`):

```
index → IncidentStatsService.getListForDate($date, $domainId, $serverId) — danh sách sự cố
  trong ĐÚNG 1 NGÀY (filter ngày/domain/IP, mặc định hôm nay, giới hạn trong
  services.metrics.retention_days ngày gần nhất, theo display_timezone giống api-health-stats)
  → eager-load Domain → Server (tránh N+1) + thống kê nhỏ: đếm số lần vượt ngưỡng theo từng
    chỉ số (cpu/ram/disk) và top 5 server cao tải nhiều nhất trong ngày đang filter.
  Nút "Chi tiết" disable khi status = recovered (không có process/log để xem).

show   → IncidentStatsService.getIncidentDetail($incident) — bảng top tiến trình (sort theo
  cpu_pct + mem_pct giảm dần) + log snippet từng file (path, số dòng, có bị cắt bớt/lỗi đọc hay
  không, nội dung). Data bound theo 1 incident nên luôn nhỏ, không lo N+1.
```

## Monitor_agent — agent Python cài trên server được giám sát

`Monitor_agent/` là **repo git riêng** (nested — không phải submodule, chỉ đơn giản là 1 thư mục
có `.git` con), deploy độc lập bằng `git clone`/`git pull` trực tiếp lên từng server BE cần giám
sát (xem `Monitor_agent/Readme.md`). Không phải code Laravel, không chạy trong container
`monitoring-app`.

`monitor_agent.py` chạy qua systemd timer mỗi 5 phút (`Monitor_agent/setup_monitor.sh` cài đặt
service + timer + logrotate), mỗi lần chạy:

```
1. Đo CPU/RAM/DISK, luôn POST /metrics (không đổi, như mục "Metric ingest" trên).
2. Nếu CPU/RAM/DISK vượt ngưỡng riêng trong .env của agent (MONITORING_*_THRESHOLD_PCT) —
   coi là cao tải:
   → thu thập top N tiến trình tốn tài nguyên nhất (psutil) + đọc đoạn CUỐI (tail, không
     phải nguyên file) các file log cấu hình trong MONITORING_LOG_FILES (Apache/Nginx
     access/error log...)
   → gửi tới MONITORING_INCIDENT_API_URL (endpoint RIÊNG, khác /metrics; để trống = tắt
     hẳn tính năng này, agent chỉ gửi metrics như cũ)
   → chống "bão" dữ liệu: lưu trạng thái vào monitor_agent_state.json (cùng thư mục agent)
     — chỉ gửi ngay khi MỚI chuyển bình thường→cao tải, các lần sau chỉ gửi lại cách nhau
     tối thiểu MONITORING_INCIDENT_COOLDOWN_MINUTES phút, gửi 1 lần "recovered" khi hết cao tải.
```

Endpoint nhận `MONITORING_INCIDENT_API_URL` phía Laravel là `POST /incidents` (xem luồng
"ingest incident cao tải" ở trên) — điền URL này (vd `https://<domain>/api/incidents`) vào
`.env` của agent để bật tính năng. Payload cụ thể agent gửi xem trong `Monitor_agent/Readme.md`,
mục "Payload gửi tới MONITORING_INCIDENT_API_URL".

Do server chạy agent thường hạn chế tài nguyên: mọi logging trong `monitor_agent.py` để dưới
dạng comment (`# LOGGER.info(...)`) — chỉ bật tạm khi debug rồi comment lại, không để log chạy
thường trực; 2 file `monitor-agent.log`/`.err` (đặt ngay trong `Monitor_agent/`, không phải
`/var/log`) có logrotate tự dọn hàng tuần.

**Luồng dashboard** (Overview/Details):

```
Controller → DashboardService
  → lấy ServerMetric theo domain/server (UTC)
  → gom vào slot 30' bằng MAX-aggregation (sub-slot ±2')
  → convert sang display_timezone (env APP_DISPLAY_TIMEZONE) khi render Blade
```

Ghi chú ngắn:

- `Domain.position` quyết định thứ tự ưu tiên hiển thị, chỉnh qua kéo-thả (SortableJS) → `DomainController::reorder`.
- Frontend không dùng Vite/npm (di sản Laravel skeleton, không blade nào gọi `@vite`) — toàn bộ UI là Blade + AdminLTE/jQuery load qua CDN, Chart.js/Select2/SortableJS cũng vậy. Sửa UI = sửa trực tiếp `.blade.php`.
- Không dùng FK constraint ở DB (`server_id`/`domain_id`/`api_id`/`server_incident_id` chỉ là index) — cascade xoá được cài ở tầng Service qua các method `deleteByXIds()` của Repository liên quan (ví dụ `DomainService::delete()` → cascade `ApiHealthCheck` → `Api` → `ServerMetric` → `IncidentProcess`/`IncidentLog` → `ServerIncident` → `Server` → `Domain`, theo đúng thứ tự phụ thuộc — `ServerService::delete()` làm tương tự cho 1 server lẻ).
- Nhiều canvas Chart.js trên 1 trang (Overview/Details) đều dùng chung pattern: build dataset ở PHP → `@json` xuống JS → `IntersectionObserver` chỉ `new Chart()` khi canvas lọt/sắp lọt khung nhìn (tránh đơ trình duyệt khi domain/server tăng nhiều). Trang "Tổng Quan API Health" và "Cao Tải Server" chỉ hiển thị dạng bảng, không có chart.
