# 🔗 MASTER INTEGRASI — Jobseeker Agent (A–Z, lengkap dari nol)

> Dokumen tunggal integrasi: sisi mesin (Hermes), sisi backend (BE Golang),
> sisi frontend (FE Nuxt), Centrifugo, dan seluruh ENV / database / endpoint /
> aliran data / gotcha. Versi 4.0 — sinkron dengan PROMPT-FE-NUXT4.md &
> PROMPT-BE-GOLANG.md & skill jobseeker-agent.

---

## 0. Peta sistem

```
┌─────────────┐  HTTP    ┌──────────────┐  HTTP(SSE)   ┌──────────────────────────┐
│ FE (Nuxt 4) │ ───────► │ BE (Golang)  │ ───────────► │ Mesin Hermes (per user)  │
│             │          │              │              │  • API server  :8642     │
│             │  WS ◄─── │              │              │  • noVNC/ws    :6080     │
│             │  Centrifugo (channel    │              │  • Xvnc :5901 (internal) │
│             │  "user:<id>")           │              │  • Chrome CDP :9222 (int)│
└─────────────┘          │  ┌────────┐  │              │  • scheduler harian      │
                         │  │ DB     │  │              │  • tooling /opt/data/... │
                         │  └────────┘  │              └──────────────────────────┘
                         └──────────────┘
```

**Aturan emas:** 1 mesin Hermes = 1 user. Semua secret per mesin (bukan global).

**Sumber file provisioning (S3):** `https://s3-autoapply-public.s3.ap-southeast-1.amazonaws.com/provision`
— semua skrip & dokumen mesin baru (bootstrap, platform, tooling, skill, panduan; 29 file, checksum `MANIFEST.sha256`).
Web root mesin utama (`-6080-`) hanya fallback.

---

## 1. Komponen & port

| Komponen | Port | Akses | Dipakai untuk |
|---|---|---|---|
| Xvnc (TigerVNC) | 5901 | lokal (internal) | Desktop headless |
| Chromium CDP | 9222 | lokal (internal) | Agent (browser tools) |
| websockify + noVNC | 6080 | **tunnel publik** `-6080-` | Viewer layar (VNC, JWT auth) |
| API server Hermes | 8642 | **tunnel publik** `-8642-` | Chat BE→agent (OpenAI-compatible) |
| Centrifugo | 8000 | internal BE | Realtime WS FE |
| BE (Golang) | — | internal | API platform |
| FE (Nuxt) | — | publik | UI user |
| S3 (provisioning) | — | publik | Sumber file mesin baru (bootstrap/platform/tooling/skill/dokumen, `--acl public-read`) |

---

## 2. ENV — MATRIKS LENGKAP

### 2.1 ENV BACKEND (BE) — WAJIB

| Variabel | Wajib? | Isi | Contoh |
|---|---|---|---|
| `DATABASE_URL` | ✅ | Koneksi DB | `postgres://...` |
| `PORT` | ✅ | Port HTTP BE sendiri | `8080` |
| `CENTRIFUGO_URL` | ✅ | Alamat Centrifugo | `http://localhost:8000` |
| `CENTRIFUGO_API_KEY` | ✅ | Sama dgn `api_key` config Centrifugo (publish) | `cent-api-...` |
| `CENTRIFUGO_TOKEN_SECRET` | ✅ | Sama dgn `token_hmac_secret_key` Centrifugo (JWT WS) | `cent-secret-...` |
| `REALTIME_CHANNEL_PREFIX` | ✅ | Prefix channel | `user` |
| `HERMES_API_URL` | ✅ | Default base URL mesin (fallback) | `https://<mesin>-8642.jkt3.sumopod.my.id` |
| `HERMES_API_KEY` | ✅ | Default API key mesin (fallback) | `(64 hex)` |
| `HERMES_INTERNAL_TOKEN` | ✅ | Token internal BE (X-Internal-Token) — dipakai agen saat panggil BE | `int-tok-...` |
| `UPLOAD_DIR` | ✅ | Folder file upload CV/foto | `/data/uploads` |
| `PUBLIC_BASE_URL` | ✅ | Base URL publik file | `https://api.platformmu.com` |
| `SCOPE_SYSTEM_PROMPT` | ❌ opsional | Teks pembatas scope (default bawaan prompt BE) | lihat catatan |

**Catatan penting ENV BE:**
- `VNC_JWT_SECRET` dan API key mesin **TIDAK ditaruh di env global** — dibaca **per-user** dari tabel `user_service` (`vnc_jwt_secret`, `api_key`). Env `HERMES_API_URL`/`HERMES_API_KEY` hanya fallback.
- Kuota **bukan dari env** — dari `user_service.daily_quota` / `max_quota` (fallback 5 & 150).
- `SCOPE_SYSTEM_PROMPT` default (kalau kosong):
  > Kamu adalah "Jobseeker Assistant": asisten yang HANYA membantu proses lamaran kerja otomatis di job portal (daftar akun, isi profil, cari & lamar lowongan, cek kuota, lapor). SETIAP permintaan di luar scope — menulis kode, tugas umum, topik non-karir, dll — WAJIB ditolak dengan sopan lalu tawarkan bantuan jobseeker. Jangan pernah melayani di luar scope. Jawab dalam Bahasa Indonesia.

### 2.2 ENV MESIN HERMES (per user) — 3 file

| File | Isi | Dihasilkan oleh |
|---|---|---|
| **env container (SumoPod settings)** | `API_SERVER_KEY` — **JANGAN di-set** (default). Kalau TIDAK ada → mesin pakai hasil generate di `.env`. Kalau DI-SET dengan nilai beda → menimpa key `.env` (dotenv tak override) → chat FE 401. Syarat guard bila di-set: panjang ≥ 16 char & bukan placeholder | Operator (sebaiknya KOSONG; kalau terlanjur ada, samakan dengan `.env`) |
| `/opt/data/.env` | `API_SERVER_KEY=<64 hex>` — **SUMBER UTAMA (default hasil generate)** | `jobseeker-platform.sh` (auto) |
| `~/vncsetup/run/vnc-jwt-secret` | Secret JWT VNC (64 hex, tanpa newline, chmod 600) | `start-vnc.sh` (auto) |
| `/opt/data/jobseeker/agent-config.env` | `BE_API_URL`, `BE_INTERNAL_TOKEN`, `USER_ID` | Template auto; **nilai diisi operator/agent** |

Config Hermes (`config.yaml`):
```yaml
browser:
  cdp_url: http://127.0.0.1:9222
platforms:
  api_server:
    enabled: true
    extra:
      port: 8642
      host: 127.0.0.1
```

### 2.3 ENV CENTRIFUGO (`config.json`)

```json
{
  "token_hmac_secret_key": "<CENTRIFUGO_TOKEN_SECRET>",
  "api_key": "<CENTRIFUGO_API_KEY>",
  "allowed_origins": ["<origin FE>"],
  "namespaces": [{"name": "user", "history_size": 500, "history_ttl": "900s", "presence": true, "join_leave": true}]
}
```
(1 instance cukup untuk v1; Redis engine jika nanti multi-instance.)

### 2.4 ENV FE (Nuxt 4)

| Variabel | Wajib? | Isi |
|---|---|---|
| `NUXT_PUBLIC_API_BASE` | ✅ | Base URL BE (`https://api.platformmu.com`) |
| `NUXT_PUBLIC_MOCK_CHAT` | ❌ | `true` = mode mock (tanpa BE, untuk develop FE) |
| `NUXT_PUBLIC_CENTRIFUGO_URL` | ✅ | URL WS Centrifugo publik (`wss://<host-centrifugo>/connection/websocket`) |

---

## 3. DATABASE — tabel & kolom lengkap

| Tabel | Kolom | Keterangan |
|---|---|---|
| `users` | (yang sudah ada) | User platform |
| `user_service` | `user_id`, `base_url`, `api_key`, `vnc_url`, `vnc_jwt_secret`, `daily_quota`, `max_quota`, `next_run_at` | Pemetaan user → mesin; `next_run_at` = jadwal daily run (di-set BE scheduler, random 08–16 WIB) |
| `applications` | `id`, `user_id`, `portal`, `job_title`, `job_url`, `company`, `status`, `applied_at`, `created_at` | Riwayat lamaran; `status` ∈ submitted/duplicate/failed |
| `portal_accounts` | `user_id`, `portal`, `status`, `display_name`, `verified_at` | Status akun portal; `status` ∈ connected/disconnected/connecting/expired |
| `portals` | `id`, `slug`, `name`, `domain`, `url`, `marker_type`, `marker_name`, `marker_value`, `enabled`, `sort_order`, `created_at`, `updated_at` | **Registry portal (sumber kebenaran daftar portal)** — dikelola admin; mesin fetch via `/v1/agent/portals` |
| `daily_reports` | `user_id`, `date`, `applied`, `failed`, `quota_left`, `portals_ok`, `portals_expired`, `no_match`, `priority_processed`, `created_at` | Ringkasan daily run |
| `priority_jobs` | `user_id`, `job_url`, `title`, `source`, `status`, `created_at` | Antrian lowongan spesifik dari chat (diproses duluan) |
| `uploads` | (sesuai pola codebase) | File CV/foto |

---

## 4. ENDPOINT — kontrak lengkap

### 4.1 FE → BE (auth user)

| Method & Path | Deskripsi | Catatan |
|---|---|---|
| `POST /api/v1/chat` | Kirim pesan; balas **202** `{accepted,run_id}`; lock per user → 429 `agent_busy` | Body `{"message":"..."}` |
| `POST /api/v1/chat/session` | Buat sesi baru | Reset context agent |
| `POST /api/v1/chat/stop` | Hentikan turn berjalan | Opsional tapi disarankan |
| `GET /api/v1/session` | Info sesi user | |
| `POST /api/v1/upload` | Upload CV/foto (multipart `file`, maks 10 MB) | PDF/DOC/DOCX, JPG/JPEG/PNG |
| `GET /api/v1/files/{name}` | Serving file | |
| `GET /api/v1/quota` | Kuota user | `{used_today,limit_per_day,remaining_today,window_days,total_used,total_limit}` |
| `GET /api/v1/applications` | Riwayat lamaran | |
| `GET /api/v1/portal-status` | Status akun portal | `[{portal,status,display_name,verified_at}]` |
| `GET /api/v1/daily-reports` | Histori daily report (terbaru duluan) | |
| `GET /api/v1/realtime/token` | JWT Centrifugo FE | `{sub:user_id, channels:[user:<id>], exp:+1h}` |
| `GET /api/v1/realtime/vnc` | URL viewer VNC + JWT pendek | `{"url":"wss://.../websockify?token=<jwt>"}` |

### 4.2 Mesin → BE (internal, header `X-Internal-Token: <HERMES_INTERNAL_TOKEN>`)

| Method & Path | Deskripsi | Respon penting |
|---|---|---|
| `GET /v1/agent/quota?user_id=` | Cek kuota (limit dari `user_service`) | 200 `{...}`; `remaining_today<=0` → agent berhenti |
| `GET /v1/agent/applications?user_id=` | Riwayat lamaran (anti-duplikat agent) | list `{portal,job_title,job_url,status,applied_at}` |
| `POST /v1/agent/applications` | Lapor lamaran `{user_id,portal,job_title,job_url,company,status}` | 201 lanjut · 403 `quota_exceeded` berhenti · 401 token salah |
| `GET /v1/agent/portals` | Daftar portal enabled (dari tabel `portals`) | `{"portals":[{slug,name,url,marker_type,marker_name,marker_value}]}` — dipakai portal-check.py |
| `POST /v1/agent/portal-status` | Sync status akun portal `{user_id,portal,status,display_name?}` | 422 status tidak valid |
| `GET /v1/agent/user-profile` | Preferensi + antrian `priority_jobs` | `{profile:{level,city,keywords,salary_min?,work_type?,exclude_companies?}, priority_jobs:[{job_url,title,source}]}` |
| `POST /v1/agent/daily-report` | Laporan akhir daily run (termasuk `priority_processed`) | BE simpan + publish event `daily_report` + kosongkan priority yang diproses |

### 4.3 BE → Hermes (per turn chat)

```
POST {user_service.base_url}/v1/chat/completions
Authorization: Bearer {user_service.api_key}
X-Hermes-Session-Id: <session user>
{
  "model": "hermes-agent",
  "stream": true,
  "messages": [
    {"role": "system", "content": "<SCOPE_SYSTEM_PROMPT>"},   ← WAJIB (lihat gotcha #2)
    {"role": "user", "content": "<pesan user>"}
  ]
}
```
- Streaming SSE; event custom `hermes.tool.progress`.
- **Ask user**: agent mengakhiri turn dengan marker `<<<ASK_USER:{"question":"...","choices":[...]}>>>` → BE parse → publish event `agent.ask_user` → jawaban user dikirim sebagai turn baru `{"message":"Jawaban user: <teks>"}` (session sama).

---

## 5. Aliran data (4 alur utama)

### 5.1 Chat interaktif
1. FE `POST /api/v1/chat` → BE balas 202 → goroutine.
2. BE resolve `user_service` user → panggil Hermes (4.3, dengan system scope).
3. BE baca SSE Hermes → publish ke Centrifugo `user:<id>` event typed (`agent.status`, `agent.activity`, `agent.text_delta`, `agent.text_done`, `agent.ask_user`, `agent.finished`, `agent.error`, `quota.updated`).
4. FE render (bubble, activity feed, kartu ask_user, badge kuota).

### 5.2 Viewer VNC (auth JWT)
1. FE `GET /api/v1/realtime/vnc` → BE tanda tangani JWT HS256:
   - claims `{host:"localhost", port:5901, exp: now+1h}`
   - secret = `user_service.vnc_jwt_secret` → **kunci = base64url_decode(secret)** (gotcha #1)
2. FE `new RFB(el, url, {})` dengan `url = vnc_url + "?token=" + jwt`.
3. websockify (JWTTokenApi) validasi → 101 + RFB. Token tanpa/rusak/kedaluwarsa → ditolak.
4. Token exp → FE ambil URL baru & reconnect (maks 2×). Viewer kontainer 16:9; zoom via `rfb._display.scale` (lihat PROMPT-FE).
5. **Beban server — SATU TAB**: browser mesin dijalankan dengan 1 tab saja. Agent WAJIB navigasi di tab yang sama (skill aturan #12 — "menyesuaikan alamat situsnya", bukan tab baru), popup/tab tak sengaja ditutup via `Target.closeTarget`, dan `portal-check.py` baca cookie via `Network.getCookies` TANPA buka tab. Cek: `Target.getTargets` → 1 page.

### 5.3 Daily run (pemicu: scheduler BE — via chat proxy, LIVE di chat FE)

> **Alur detail**: `~/prd/alur-antrian.md` (status queued → submitted; antrian DIISI BE, agent hanya pick & update).

1. **Scheduler BE** (goroutine ticker tiap 1 mnt) menemukan `user_service.next_run_at <= now` (waktu acak 08:00–16:00 WIB per user) → kirim pesan trigger via **chat proxy** (sesi chat user) → set `next_run_at` = waktu acak besok. Gagal call Hermes → retry 5 mnt maks 3× → skip hari ini.
2. Agent (mode OTOMATIS, di sesi chat user — ikuti skill §4): `sync-profile.py` → `check-quota.py` → verifikasi portal (`accounts.py`) → **ambil lowongan ACAK dari antrian queued** (`report-application.py pick` — antrian diisi BE: priority dari chat, import, dll) → anti-duplikat (riwayat `status=submitted`) → buka `job_url` & lamar (satu tab) → `report-application.py update` (queued → submitted/failed/duplicate, by id atau url+user_id).
3. **SEMUA aktivitas agent ter-stream ke FE** (`agent.status`/`agent.activity`/`agent.text_delta`) — proses daily run LIVE di chat user.
4. Akhir run: `POST /v1/agent/daily-report` → BE publish `daily_report` → FE kartu; `priority_processed` dikeluarkan dari antrian.
5. Manual: user chat "jalankan daily run" → alur yang sama (BE proxy → agent → live di FE).

**Konfigurasi cron & scheduler:**

| Komponen | Jadwal | Jenis | Keterangan |
|---|---|---|---|
| `vnc-stack-watchdog` (cron Hermes) | tiap 10 menit | `--no-agent` · script `cron-vnc-check.sh` · `--deliver local` | VNC stack (`start-vnc.sh --quiet`) + vnc-watchdog + **portal-check.py** (deteksi login/logout portal → sync BE). Silent saat sehat. Dibuat OTOMATIS oleh `bootstrap-mesin.sh` (check-then-create, idempotent) |
| `portal-check.py` | ikut cron di atas (±10 mnt) | script | baca cookie browser → `accounts.json` + `POST /v1/agent/portal-status` |
| **Scheduler BE** (di BE, bukan di mesin) | acak 08:00–16:00 WIB per user (`user_service.next_run_at`) | goroutine ticker 1 mnt | pilih waktu acak → trigger daily run via chat proxy (sesi chat user, LIVE di FE) → `next_run_at` = besok. **Menggantikan daemon mesin** (`jobseeker-scheduler.py`/`run-daily.sh` di mesin TIDAK dipakai) |

**Aturan:** TIDAK ada cron harian Hermes untuk daily run (pemicu harian = scheduler BE + manual chat). Satu-satunya cron Hermes = `vnc-stack-watchdog`. Cek: `hermes cron list` → hanya 1 job.
**Daily report = kesimpulan SETIAP run** (bukan jadwal terpisah): setiap kali daily run selesai — dipicu scheduler BE ATAU manual chat — agent SELALU mengakhiri dengan `POST /v1/agent/daily-report` (tetap terkirim walau 0 lamaran / kuota habis / portal expired / tidak ada cocok). Tidak ada mekanisme report terpisah yang perlu dijadwalkan.
**Daily run LIVE di chat FE:** karena di-trigger lewat chat proxy di sesi chat user, SEMUA proses (agent.status/activity/text_delta) tampil langsung di chat FE — bukan sesi terpisah lagi.

### 5.4 Connect account (handoff user)
1. User minta connect portal / status portal `expired`.
2. Agent buka halaman login di browser (VNC) → kirim `ask_user` ("login sendiri ya, ketik 'sudah'").
3. User login di viewer → balas "sudah" → agent verifikasi → `accounts.py set <portal> --status connected --display-name "<email-mask>"` → sync otomatis ke BE (`POST /v1/agent/portal-status`) → FE status "Terhubung ✓".
4. **Deteksi otomatis (watchdog):** `portal-check.py` (cron 10 mnt via `cron-vnc-check.sh`) membaca cookie browser (jobstreet: `is.authenticated`/`appSession`; linkedin: `li_at`; indeed: `USER`) → perubahan login/logout otomatis meng-update `accounts.json` + sync ke BE — tanpa perlu user/agent memicu.
5. **Trigger manual (user non-teknis):** user chat "aku sudah login jobstreet" / "cek status portal saya" (atau tombol "🔄 Perbarui status login" di FE) → agent jalankan `python3 portal-check.py` (jalur cepat ±5 dtk, baca cookie + sync) → balas konfirmasi status. Browser-verification hanya fallback bila hasil belum sesuai.

### 5.5 Menambah portal baru (di masa depan) — TANPA edit kode

Daftar portal bersumber dari **tabel `portals` di BE** (bukan kode). Mesin fetch tiap run (`GET /v1/agent/portals` → cache `portals.json` → fallback bawaan). Prosedur:

1. **INSERT/isi via admin panel** tabel `portals` (atau endpoint admin CRUD):
   ```sql
   INSERT INTO portals (slug, name, domain, url, marker_type, marker_name, marker_value, enabled, sort_order)
   VALUES ('glints', 'Glints', 'glints.com', 'https://glints.com/id/', 'name', '', '', true, 4);
   ```
   `marker_name` bisa dikosongkan dulu (mesin otomatis skip portal itu sampai marker diisi).
2. **Cari marker cookie sesi** (setelah user login di browser):
   ```bash
   browser-harness <<'PY'
   ck = cdp("Network.getCookies", urls=["https://<domain-portal>/"])["cookies"]
   for c in ck: print(c["name"], "=", (c.get("value") or "")[:30])
   PY
   ```
3. **Update `marker_type`/`marker_name`/`marker_value`** di tabel → run berikutnya (≤10 mnt) langsung aktif. Tanpa deploy, tanpa restart.
4. Tes: logout → status `expired`; login → `connected` (otomatis sync ke BE).

---

## 6. Setup mesin baru (dari nol)

> **Update mesin EXISTING** (sudah ter-provisioning): refresh tooling + venv-cdp + skill + cron dari S3 tanpa re-provision —
> `SRC_BASE=https://s3-autoapply-public.s3.ap-southeast-1.amazonaws.com/provision bash update-mesin.sh` (idempotent; secret tidak disentuh), lalu `hermes gateway restart`.

### 6.1 Prasyarat
- Mesin SumoPod baru (Hermes terpasang, chat TUI bisa dibuka).
- 3 data: nama unik (`joko-jobseeker`), `ENDPOINT_BACKEND`, `API_KEY` (token internal BE).

### 6.2 Eksekusi (tempel ke chat mesin baru — PAKET PERINTAH)
```
TUGAS: Siapkan mesin ini agar identik dengan mesin utama (noVNC + API server + tooling jobseeker + skill). Kerjakan SEMUA dengan tool TERMINAL.

LANGKAH 1 — Unduh 2 skrip (sumber: S3 provisioning; fallback: web root mesin utama):
  curl -fsSL https://s3-autoapply-public.s3.ap-southeast-1.amazonaws.com/provision/bootstrap-mesin.sh -o /tmp/bootstrap-mesin.sh
  curl -fsSL https://s3-autoapply-public.s3.ap-southeast-1.amazonaws.com/provision/jobseeker-platform.sh -o /tmp/jobseeker-platform.sh
  chmod +x /tmp/bootstrap-mesin.sh /tmp/jobseeker-platform.sh

LANGKAH 2 — Jalankan bootstrap dasar (TANPA argumen — webhook sudah tidak dipakai):
  bash /tmp/bootstrap-mesin.sh

LANGKAH 3 — Jalankan platform jobseeker (tooling diunduh dari SRC_BASE — default web root mesin utama; untuk S3):
  SRC_BASE=https://s3-autoapply-public.s3.ap-southeast-1.amazonaws.com/provision bash /tmp/jobseeker-platform.sh

LANGKAH 4 — Isi agent-config.env (nilai dari operator/BE):
  Tulis file /opt/data/jobseeker/agent-config.env dengan: BE_API_URL=<alamat BE>, BE_INTERNAL_TOKEN=<token internal>, USER_ID=<user_id platform>

LANGKAH 5 — Verifikasi: CDP 9222 · noVNC 6080 (200) · API 8642 (/health) · tooling /opt/data/jobseeker/ · skill jobseeker-agent · config api_server enabled=true. Lalu LAPORKAN: hasil tiap verifikasi (OK/gagal), URL noVNC publik, isi run/vnc-jwt-secret, dan baris API_SERVER_KEY= dari /opt/data/.env. Untuk 2 nilai terakhir: tampilkan di layar saja (jangan tempel di chat publik).
```
- Waktu ±15–20 mnt; chat putus 1–2 mnt saat gateway restart = normal.
- `bootstrap-mesin.sh` (tanpa argumen): VNC stack (JWT auth, 1600×900 = 16:9) + CDP + watchdog + cron — **TANPA webhook**.
- `jobseeker-platform.sh`: API server 8642 + `API_SERVER_KEY` + tooling + skill + template `agent-config.env` + restart gateway (+180 dtk, hanya bila config berubah).
- Semua file provisioning tersedia di **S3** (`s3-autoapply-public/provision/`, 29 file, verifikasi `curl -s <BASE>/MANIFEST.sha256`) — fallback: web root mesin utama `https://hermes-3s1llz-6080.jkt3.sumopod.my.id`.

### 6.3 Tunnel (panel SumoPod) — 2 entri
| Nama | Port | Alamat publik |
|---|---|---|
| `<nama>-6080` | 6080 | `https://<nama>-6080.jkt3.sumopod.my.id` |
| `<nama>-8642` | 8642 | `https://<nama>-8642.jkt3.sumopod.my.id` |

### 6.4 Isi `agent-config.env` di mesin
```
BE_API_URL=https://api.platformmu.com
BE_INTERNAL_TOKEN=<HERMES_INTERNAL_TOKEN dari BE>
USER_ID=<user_id platform>
```
(Bisa dikerjakan agen: kirim chat "Isi /opt/data/jobseeker/agent-config.env dengan ...".)

---

## 7. Registrasi ke BE (`user_service`) — 6 kolom

| Kolom | Nilai |
|---|---|
| `base_url` | `https://<nama>-8642.jkt3.sumopod.my.id` |
| `api_key` | `API_SERVER_KEY` mesin itu |
| `vnc_url` | `wss://<nama>-6080.jkt3.sumopod.my.id/websockify` |
| `vnc_jwt_secret` | isi `run/vnc-jwt-secret` mesin itu |
| `daily_quota` | 5 (default) |
| `max_quota` | 150 (5 × 30 hari) |

Aktifkan scheduler: chat mesin → `python3 /opt/data/jobseeker/jobseeker-scheduler.py` (+ `--preview`, `--status`).

---

## 8. Verifikasi end-to-end (checklist teknis)

1. **Mesin**: `curl -s http://localhost:8642/health` → `{"status":"ok"}` · noVNC 200 · CDP 9222.
2. **Tunnel**: `curl -s https://<nama>-8642.../health` → 200 · `https://<nama>-6080.../vnc.html` → 200.
3. **Chat E2E**: FE kirim "Cek kuota" → BE 202 → Centrifugo events → bubble jawaban.
4. **Scope**: FE kirim "buat script python" → agent MENOLAK (verifikasi role:system terkirim).
5. **VNC**: `GET /api/v1/realtime/vnc` → URL + token → connect → 101 + RFB (pola probe: token valid = RFB, tanpa/expired = ditolak).
6. **Quota BE**: `GET /v1/agent/quota?user_id=` (X-Internal-Token) → JSON benar; POST aplikasi ke-6 saat daily_quota=5 → 403.
7. **Daily report**: `POST /v1/agent/daily-report` → tersimpan + event `daily_report` ter-publish + priority diproses hilang dari antrian.
8. **Portal status**: `accounts.py set jobstreet --status connected` → `GET /api/v1/portal-status` menampilkannya.

---

## 9. GOTCHA — jebakan yang sudah terbukti (WAJIB dibaca)

| # | Jebakan | Fakta | Solusi |
|---|---|---|---|
| 1 | Secret JWT VNC | websockify (jwcrypto) memaknai secret sebagai **base64url** → kunci = `base64url_decode(hex)` (48 byte) | Go: `base64.RawURLEncoding.DecodeString(secret)`, JANGAN `[]byte(secret)` |
| 2 | Pembatas scope | Field `system_prompt` di body request **TIDAK efektif** | Kirim `{"role":"system",...}` sebagai pesan pertama di `messages` |
| 3 | Secret file VNC | Trailing newline bikin `import_key` gagal ("Failed to correctly parse key data") | Tulis tanpa newline (`printf '%s'`) |
| 4 | Cron container | Tidak ada cron/systemd (PID 1 = s6-svscan) | Pakai `jobseeker-scheduler.py` (daemon) |
| 5 | Kill websockify | PID file menunjuk wrapper, bukan proses asli | `kill -9 $(pgrep -f websockify)` |
| 6 | Secret per mesin | `API_SERVER_KEY` & `vnc_jwt_secret` lahir per mesin | Jangan salin antar mesin; daftarkan per user di `user_service` |
| 7 | Resolusi VNC | Rasio ditentukan framebuffer Xvnc (`-geometry`), bukan client; jangan paksa rasio di sisi client | 1600×900 (16:9) di `start-vnc.sh`; FE menyesuaikan (`scaleViewport`) |
| 8 | API server bind | Config `host: 127.0.0.1` tapi listener di `0.0.0.0:8642` | Tidak dikejar; tunnel tetap jalan |
| 9 | Kuota env | Dulu dari env, sekarang **per-user di DB** | `user_service.daily_quota/max_quota` |
| 10 | Gateway restart dobel | Bootstrap restart +60 dtk, platform +180 dtk | Platform hanya restart bila config berubah |
| 11 | **$HOME vs Hermes home** | `$HOME` = `/opt/data/home`, tapi Hermes home = `/opt/data` (`HERMES_HOME`). Script lama pakai `${HOME:-/opt/data}` → tooling/skill/.env salah tempat (`/opt/data/home/*`) | `jobseeker-platform.sh` kini pakai `$HERMES_HOME`/fallback; mesin yang terlanjur salah → `fix-platform-paths.sh` |
| 12 | MANIFEST self-hash | `sha256sum *` menghitung MANIFEST saat masih kosong → hash file kosong (`e3b0c442...`) masuk manifest | Generate tanpa self-entry: `sha256sum $(ls \| grep -v '^MANIFEST') \| sort > MANIFEST` |
| 13 | IAM S3 ter-scope | User IAM `autoapply` TIDAK bisa list/delete bucket lain (`s3:ListAllMyBuckets` ditolak); hanya put/get di bucket tertentu | Upload ke bucket yang benar (`s3-autoapply-public`); jangan asumsi bisa list semua bucket |
| 14 | **API_SERVER_KEY: env container bisa MENIMPA .env** | Hermes baca `os.getenv("API_SERVER_KEY")` (api_server.py:1199); dotenv TIDAK menimpa env yang sudah ada → container settings yang beda nilai = 401 terus | **Default = hasil generate di `.env`**; env container sebaiknya KOSONG; kalau terlanjur ada harus SAMA dengan `.env`; `user_service.api_key` = nilai dari `.env` |
| 15 | **`api_mode` wajib `chat_completions`** | Provider SumoPod (`ai.sumopod.com`) TIDAK menyediakan `/responses`; Hermes default `responses` → `404 litellm.NotFoundError: model not found` (padahal model ADA di `/chat/completions`) | `hermes config set model.api_mode chat_completions` + `hermes gateway restart` — sudah di-hardcode di `jobseeker-platform.sh` (mesin baru) & `update-mesin.sh` (existing) |

---

## 10. Checklist go-live (ringkas)

- [ ] BE: ENV 2.1 lengkap · tabel 3 dibuat · endpoint 4.1–4.3 implement (kontrak prompt BE v4)
- [ ] FE: ENV 2.4 · fitur sesuai PROMPT-FE v4 (mock dulu, lalu live)
- [ ] Centrifugo: config 2.3 + `GET /api/v1/realtime/token`
- [ ] Mesin user: bootstrap + platform (Bagian 6, sumber S3) · 3 tunnel · `agent-config.env` terisi
- [ ] S3 provisioning: 29 file ter-upload `s3-autoapply-public/provision/` · checksum 29/29 cocok `MANIFEST.sha256` (verifikasi `curl -s <BASE>/MANIFEST.sha256`)
- [ ] `user_service` terisi 6 kolom (Bagian 7)
- [ ] Scheduler aktif · daily run test · scope test · VNC test (Bagian 8)
