# PRD — Jobseeker Agent (alur lengkap, v2)

> Versi final setelah keputusan bersama (kuota per-user di DB, retry teknis saja,
> anti-duplikat lintas portal, pemicu cron + manual, profile di-pull dari BE).

## 1. Ringkasan alur

```
User daftar + bayar membership
   → Platform provisioning mesin Hermes dedicated (1 user = 1 mesin)
   → User set preferensi (level, kota, kata kunci) + upload CV & foto
   → User connect job portal (login sendiri via VNC viewer di FE)
   → Agent jalan HARIAN (cron) + bisa dipicu manual via chat
   → Agent cari & lamar s.d. kuota harian, lintas portal
   → Setiap hasil → notifikasi ke BE → tampil di FE user
```

## 2. Membership & provisioning mesin

1. User daftar & bayar membership di website.
2. Platform setup mesin dedicated (bootstrap-mesin.sh): Chromium, noVNC (+JWT auth), browser-harness, API server (8642), VNC stack.
3. Set env di mesin (manual):

| Env / File | Isi | Arah |
|---|---|---|
| `BE_API_URL` | Base URL BE platform | manual |
| `BE_INTERNAL_TOKEN` | Token internal (X-Internal-Token) | manual |
| `USER_ID` | ID user di platform | manual |
| `VNC_JWT_SECRET` | Auto-generated `start-vnc.sh` (`run/vnc-jwt-secret`) | **mesin → DB** |
| `API_SERVER_KEY` | Auto-generated saat setup (key API server) | **mesin → DB** |

4. Platform mencatat ke database **`user_services`** (satu baris per user):

| Kolom | Isi |
|---|---|
| `base_url` | URL API server mesin (HERMES_API_URL) |
| `api_key` | API_SERVER_KEY mesin |
| `vnc_url` | URL WebSocket noVNC mesin |
| `vnc_jwt_secret` | Secret JWT VNC mesin |
| `daily_quota` | Kuota lamaran per hari (default 5) |
| `max_quota` | Kuota maksimum periode (default 150 = 5×30) |
| (kolom user_service yang sudah ada) | dst. |

> Secret `VNC_JWT_SECRET` & `API_SERVER_KEY` **lahir di mesin**, disalin ke DB — bukan sebaliknya.

5. BE push preferensi awal user ke mesin via **pull**: `sync-profile.py` di mesin memanggil `GET {BE_API_URL}/v1/agent/user-profile` → simpan `/opt/data/jobseeker/user-profile.json`.

## 3. Data user

- **Preferensi** (disimpan di BE, di-pull ke mesin):
  - Lowongan/kata kunci yang diinginkan
  - Level pekerjaan: Staff / Manager / Direktur
  - Kota yang diinginkan
  - Opsional (bisa dikosongkan): gaji minimum, tipe kerja (full-time/kontrak/magang), exclude perusahaan
  - Target lamaran per hari (dari `daily_quota`)
- **Priority jobs**: link lowongan spesifik yang user kirim via chat — antrian di BE (`priority_jobs`), diproses agen **LEBIH DULU** daripada hasil pencarian otomatis.
- **CV** (PDF/DOC/DOCX) & **foto profil** (JPG/PNG) — upload via FE → BE → URL publik → agent unduh saat diproses.
- **Akun portal** — user login sendiri (tidak pernah kasih kredensial ke agent):
  - Portal didukung: JobStreet, Glints, LinkedIn, Indeed
  - Sesi login persist di profile Chromium mesin
  - **Daftar portal dari BE** (tabel `portals`, fetch via `GET /v1/agent/portals` → cache `portals.json`) — admin tambah portal cukup INSERT di DB, tanpa edit kode mesin.
  - **Status otomatis** (`portal-check.py`, cron 10 mnt, baca cookie browser): login/logout terdeteksi sendiri → accounts.json + DB ter-update.
  - **Trigger manual**: user chat "aku sudah login jobstreet" / tombol "🔄 Perbarui status login" → agent jalankan `portal-check.py` (cepat) → sync SEKARANG.
  - Status per portal: `connected | connecting | expired | disconnected` (accounts.json lokal + **sync ke DB** via `POST /v1/agent/portal-status` → user lihat status di FE)
  - "Register" portal baru: agent isi formulir, verifikasi email — pakai **email user** (bukan email random); user buka link verifikasi/OTP via VNC viewer.

## 4. Run harian agent (scheduler BE + manual)

Pemicu: **scheduler BE — waktu RANDOM 08:00–16:00 WIB per user** (`user_service.next_run_at`, ticker 1 mnt) + **manual via chat** (user minta langsung). BE kirim pesan "Jalankan DAILY RUN..." via **chat proxy** (sesi chat user) → **SEMUA proses terlihat LIVE di chat FE**. Waktu random supaya pola aktivitas tidak seragam/bot-like.

Urutan tiap run:
1. `sync-profile.py` → ambil preferensi terbaru + antrian `priority_jobs` dari BE (user-profile.json).
2. **Proses `priority_jobs`** (lowongan spesifik dari chat) DULU — sampai habis atau kuota hari ini penuh.
3. `check-quota.py` → cek sisa kuota hari ini (`GET /v1/agent/quota`, limit dari `user_services.daily_quota`). Kuota habis → notifikasi `quota.exhausted` → selesai.
4. Verifikasi sesi portal (`accounts.py status`):
   - `expired`/`disconnected` → **lewati portal itu**, notifikasi `portal.session_expired` ke BE (user login ulang via VNC).
   - `connected` → lanjut.
5. Cari lowongan cocok (kriteria: kata kunci + level + kota + preferensi opsional) di tiap portal connected.
6. **Anti-duplikat**: sebelum melamar, cek `GET /v1/agent/applications` (riwayat BE) — lewati yang sudah dilamar (termasuk lintas portal).
7. Lamar sampai `daily_quota` terpenuhi (atau kehabisan lowongan cocok):
   - Sukses → `report-application.py` (`POST /v1/agent/applications`, `submitted`) → BE hitung kuota → notifikasi `application.submitted`.
   - Gagal teknis (form error, timeout) → retry lowongan sama maks **3×** → tetap gagal → `application.failed` + lanjut lowongan lain.
8. Akhir run → `POST /v1/agent/daily-report` (applied, failed, quota_left, portals_ok, portals_expired, no_match, priority_processed) → tampil di FE; BE kosongkan `priority_jobs` yang sudah diproses.

### Retry policy (keputusan)

| Kasus | Aksi |
|---|---|
| Gagal teknis (form/timeout/portal error) | Retry lowongan sama, maks 3×, lalu lapor `application.failed` |
| Ditolak employer | Catat, **tidak** retry |
| Tidak direspons | **Tidak** retry otomatis |
| "Coba lagi setiap hari" | Berarti **cari lowongan baru cocok** tiap hari, bukan lamar ulang yang sama |

## 5. Kuota (sumber kebenaran: BE + DB)

- Limit harian dari **`user_services.daily_quota`** (default 5).
- Limit periode dari **`user_services.max_quota`** (default 150 = 5×30).
- Agent cek sebelum melamar (`GET /v1/agent/quota`), BE enforce saat insert (`POST /v1/agent/applications` → 403 `quota_exceeded` jika penuh).
- Kuota hanya menghitung `status=submitted` (sukses); `failed`/`duplicate` tercatat tapi tidak memakai jatah.

## 6. Notifikasi ke BE (event set)

| Event | Kapan |
|---|---|
| `application.submitted` | Lamaran sukses (via POST /applications) |
| `application.failed` | Gagal teknis setelah 3× retry |
| `quota.exhausted` | Kuota habis (awal run atau ditolak BE) |
| `portal.session_expired` | Butuh login ulang (portal dilewati) |
| `portal.connected` | User berhasil connect (via portal-status) |
| `jobsearch.no_match` | Tidak ada lowongan cocok hari ini |
| `daily_report` | Ringkasan akhir run |

## 7. Komponen teknis per mesin (sudah dibangun)

| Komponen | Path |
|---|---|
| API server Hermes (8642, OpenAI-compatible) | aktif |
| noVNC + JWT auth (websockify JWTTokenApi) | `run/vnc-jwt-secret` |
| `accounts.py` (registry portal + sync ke BE) | `/opt/data/jobseeker/` |
| `check-quota.py` | `/opt/data/jobseeker/` |
| `report-application.py` | `/opt/data/jobseeker/` |
| `vnc-token.py` (tes/ref Go) | `/opt/data/jobseeker/` |
| `sync-profile.py` (pull preferensi dari BE) | `/opt/data/jobseeker/` |
| Scheduler daily run (BE) — random 08–16 WIB, trigger via chat proxy | **diimplementasikan BE** |

## 8. Keputusan (sudah dikunci)

- Waktu scheduler BE: **random di golden hour HR 08:00–16:00 WIB** (timezone Jakarta), per user (`next_run_at`); trigger via chat proxy agar live di chat FE.
- Preferensi tambahan (opsional): gaji minimum, tipe kerja, exclude perusahaan — bisa diisi user, tidak wajib.
- Lowongan spesifik via chat (`priority_jobs`) → **prioritas di atas hasil pencarian otomatis**.
