# Dokumentasi Sentinel Nusantara

> Dokumen tunggal & lengkap.
> **Bagian 1 ditulis untuk pengguna umum (tanpa istilah teknis).**
> Bagian 2 dan seterusnya untuk tim teknis: arsitektur, menu, database, API,
> hak akses, operasional, dan temuan.
> **Dibangkitkan dari source code** — daftar menu, tabel, dan endpoint di bawah
> di-generate langsung dari kode, bukan diketik manual.

---

## 1. Penjelasan Sederhana — untuk Semua Orang

> Bagian ini ditulis **tanpa istilah teknis**. Bagian 2 dan seterusnya barulah untuk
> tim teknis. Kalau Anda pengguna biasa, cukup baca bagian ini.

### Aplikasi ini sebenarnya apa?

Bayangkan sebuah **ruang pemantauan**. Di dinding ada papan besar berisi peta Indonesia,
tumpukan koran hari ini, laporan dari petugas di lapangan, dan catatan tentang tokoh-tokoh
yang perlu diperhatikan.

Dulu semua itu tersebar: koran di satu meja, laporan di map lain, catatan di kepala orang.
Aplikasi ini **mengumpulkan semuanya di satu layar**, lalu membantu menjawab satu pertanyaan:

> *"Daerah mana yang sedang memanas, dan kenapa?"*

### Datanya dari mana?

Ada **empat sumber**, semuanya nyata — tidak ada yang dikarang aplikasi:

| Sumber | Penjelasan |
|---|---|
| 📰 **Berita** | Aplikasi berlangganan situs berita. Setiap hari ia otomatis mengambil berita baru — seperti tukang kliping yang bekerja sendiri. |
| 👮 **Petugas lapangan** | Petugas mengirim laporan dari lokasi lewat HP, lengkap dengan titik GPS dan foto. |
| 🌐 **Media sosial** | Percakapan publik di X/Threads ditarik untuk melihat isu yang sedang ramai. |
| 📊 **Data resmi** | Angka kemiskinan, pengangguran, ketimpangan per provinsi (data BPS). |

### Lalu apa yang dilakukan aplikasi?

Empat langkah, seperti cara kerja seorang analis:

**1. Membaca** — Setiap berita dibaca dan dinilai nadanya: positif, netral, atau negatif.

**2. Mengelompokkan** — Berita yang membahas **peristiwa yang sama** digabung. Kalau satu
kejadian diberitakan 5 media sekaligus, itu tanda kejadiannya besar.

**3. Menandai lokasi** — Aplikasi mencari nama daerah di dalam berita, lalu menaruh titik
di peta.

**4. Memberi skor** — Tiap daerah diberi angka risiko berdasarkan data resmi + kejadian
yang terjadi di sana.

### Cara membaca layar utama (Pusat Komando)

Ini halaman pertama yang Anda lihat. Isinya ringkasan seluruh Indonesia:

- **Kartu-kartu angka di atas** — jumlah wilayah dipantau, rata-rata tekanan sosial,
  berapa daerah berisiko tinggi.
- **Jarum ukur (gauge)** — nilai risiko nasional 0–100. Di bawah 40 tenang, 40–69 perlu
  perhatian, 70 ke atas serius.
- **Tren Risiko Nasional** — grafik naik/turun 30 hari terakhir. Panah ▲ berarti memburuk.
- **Prioritas Tindak Lanjut** — daftar daerah yang paling perlu diperhatikan minggu ini.
- **Peringatan** — kalau ada tanda merah "⚠ N peringatan", ada ambang batas yang terlewati.

> **Penting:** semua angka di halaman ini berasal dari **data indikator wilayah**, bukan
> dari tebakan AI. Halaman ini menyegarkan dirinya setiap 1 menit.

### Untuk apa menu-menu utama?

| Menu | Gunanya, sederhananya |
|---|---|
| **Pusat Komando** | Ringkasan nasional. "Bagaimana keadaan hari ini?" |
| **Peta Taktis** | Melihat sebaran risiko per provinsi dalam bentuk warna peta. |
| **Kejadian** | Daftar & peta peristiwa nyata (demo, bentrok, bencana) beserta tanggalnya. |
| **Berita** | Semua berita terkumpul + topik yang sedang ramai + berita yang diangkat banyak media. |
| **Peringatan** | Anda pasang batas ("kalau kejadian lebih dari 5 dalam sehari, beri tahu saya"), sistem yang mengawasi. |
| **Kasus** | Map investigasi. Ikat berita, sinyal, tokoh, dan kejadian yang berkaitan jadi satu berkas. |
| **Verifikasi** | Antrean periksa. Hasil olahan AI **harus disetujui manusia** dulu. |
| **Jaringan** | Peta hubungan antar tokoh & organisasi — siapa terhubung dengan siapa. |
| **Analisis Buzzer** | Mendeteksi akun-akun yang bergerak serempak (dugaan pendengung terkoordinasi). |
| **OSINT** | Kumpulan alat untuk menelusuri jejak digital sebuah target. |
| **Lapangan** | Petugas menerima tugas dan mengirim laporan dari lokasi. Ada **tombol darurat**. |
| **Laporan** | Menyusun laporan intelijen naratif, lengkap dengan sumber & catatan keterbatasan. |
| **Cari** | Satu kotak pencarian untuk semua data. |

### Isi layar tiap menu — bagian mana datanya dari mana

Penjelasan per bagian layar. Semua data di bawah **dibaca dari yang sudah tersimpan**;
penarikan data baru (berita, media sosial) dilakukan terjadwal atau lewat tombol khusus.

---

#### 📰 Berita

Layar terbagi dua: **kolom kiri (sempit) = alat bantu**, **kolom kanan (lebar) = isi berita**.

**Kolom kiri:**

| Panel | Isinya | Diambil dari |
|---|---|---|
| **🔥 Topik Ramai (7 hari)** | Kata/nama yang paling sering muncul di judul berita minggu ini | Judul berita tersimpan; sistem menghitung kata yang sering muncul, membuang kata umum seperti "Agustus" atau "Juta". **Bisa diklik** → langsung dilacak narasinya |
| **📈 Lacak Narasi** | Perjalanan sebuah isu: naik atau mereda, media mana saja yang mengangkat | Semua berita yang memuat kata yang Anda ketik, dihitung per hari |
| **📈 Trending Medsos** | Topik viral di X / Threads | Ditarik langsung dari media sosial saat tombol ditekan. Jika akun terblokir, sistem beralih ke daftar tren publik — **dan menuliskan sumbernya apa adanya** |
| **🕵 Deteksi Disinformasi** | Judul mirip yang muncul serempak di banyak media | Perbandingan kemiripan judul berita 72 jam terakhir |
| **Ringkasan Sentimen** | Berapa berita positif / netral / negatif | Hitungan dari berita tersimpan |
| **Tombol Ingest RSS** *(analis+)* | Menarik berita terbaru sekarang juga | Situs berita yang didaftarkan |

**Kolom kanan — 4 tab:**

| Tab | Isinya | Diambil dari |
|---|---|---|
| **📰 Feed** | Daftar berita satu per satu, terbaru di atas | Kumpulan berita tersimpan. Bisa disaring: sentimen, sumber, kata kunci, wilayah |
| **🔥 Viral** | Satu cerita digabung dari banyak media, berlabel "N media" | Berita berjudul mirip dikelompokkan. Makin banyak media memberitakan = makin viral |
| **🏷 Per Sumber** | Jumlah berita per media (CNN 176, Antara 153, …) + **total** | Hitungan seluruh berita per nama media. Klik nama media → feed tersaring ke media itu |
| **🤖 Pengelompokan AI** | Berita dikelompokkan ke kategori (korupsi, kriminal, bencana…) | Berita dicocokkan dengan **kata kunci kategori** yang Anda atur di menu Pengaturan |

> **Dari mana berita masuk?** Bukan saat Anda membuka halaman. Ada penarik otomatis yang
> berjalan **terjadwal setiap hari**, atau Anda tekan tombol **Ingest RSS**. Halaman ini
> hanya menampilkan yang sudah tersimpan.

---

#### ◈ Kejadian

Tersusun dari atas ke bawah:

| Bagian | Isinya | Diambil dari |
|---|---|---|
| **Grafik batang (timeline)** | Jumlah kejadian per hari | Hitungan seluruh kejadian tersimpan. **Klik satu hari** → peta & tabel di bawah ikut menampilkan hari itu saja |
| **Peta** | Titik lokasi kejadian | Kejadian yang punya koordinat GPS → titik presisi. Yang hanya punya nama wilayah → penanda kelompok di tengah provinsi |
| **Tabel kejadian** | Daftar lengkap + penyaring (jenis, keparahan, **sentimen**, wilayah, tanggal) | Data kejadian tersimpan. Penyaringan dikerjakan server, bukan sekadar menyembunyikan baris |
| **Titik Panas & Prediksi ML** | Perkiraan daerah rawan | Model terlatih — **saat ini ditandai "⚠ belum andal"**, jangan dijadikan dasar keputusan |
| **Decision Support AI** | Analisis "mengapa" + saran tindakan | Dihitung saat tombol ditekan; hasilnya tidak disimpan |
| **Tombol Impor** | Menambah kejadian dari berita / ACLED / GDELT | Sumber luar & berita negatif yang cocok kategori |

> **Kejadian datang dari 3 jalur:** dicatat manual, hasil konversi dari berita, atau impor
> dari basis data kejadian internasional (ACLED/GDELT).

---

#### ◎ Pusat Komando

| Bagian | Isinya | Diambil dari |
|---|---|---|
| **Kartu angka atas** | Total kasus, RFI terbuka, laporan disetujui, tugas lapangan aktif, aktor dipantau | Masing-masing dihitung dari datanya sendiri |
| **Gauge risiko** | Angka 0–100 | Rata-rata skor risiko seluruh provinsi |
| **Tren Risiko Nasional** | Grafik 30 hari + panah ▲▼ | Riwayat skor risiko harian |
| **Prioritas Tindak Lanjut** | 6 daerah paling perlu perhatian | Gabungan: skor risiko + jumlah kejadian 7 hari + porsi berita negatif |
| **Peringatan** | Badge "⚠ N peringatan" | Aturan ambang yang sudah terlewati |
| **Peta panas mini** | Sebaran risiko | Data indikator wilayah |

> Halaman ini **menyegar sendiri tiap 1 menit**. Semua angkanya dari data indikator —
> **bukan** hasil AI.

---

#### ⊛ Analisis Buzzer

Ada 4 tab: **Kumpulkan X · Kumpulkan Threads · Hasil · Arsip**.

Di tab **Hasil**:

| Bagian | Isinya | Diambil dari |
|---|---|---|
| **Kartu ringkas** | Jumlah akun, interaksi, komunitas, klaster mencurigakan, skor disinformasi | Hasil perhitungan jaringan |
| **Peta jaringan (kiri)** | Titik = akun, garis = interaksi. **Besar titik = skor**, **warna = kelompok** | Interaksi hasil pengumpulan. **Titik di tengah yang ramai garis = akun paling banyak dibalas**, belum tentu skornya tertinggi |
| **Top Tersangka (kanan)** | Daftar akun paling patut dicurigai + identitas (nama, follower, bio, tanggal gabung) | Skor per akun. Klik baris → peta **memfokus** ke akun itu |
| **Tabel Klaster (bawah)** | Analisis per kelompok: kepadatan, sinkron waktu, mirip konten | Perhitungan tiap kelompok |
| **Label "bukti lemah/kuat"** | Seberapa banyak data pendukungnya | Jumlah interaksi dalam kelompok — **skor tinggi tapi "bukti lemah" jangan dipercaya penuh** |

---

#### ▤ Kasus · ⬡ Jaringan · ▥ Laporan

| Menu | Kiri | Kanan |
|---|---|---|
| **Kasus** | Daftar kasus + tombol tambah | Detail kasus terpilih, item tertaut, dan **🔗 Kasus Berhubungan** (kasus lain yang mungkin terkait, lengkap dengan alasannya) |
| **Jaringan** | Peta hubungan tokoh & organisasi | Detail tokoh terpilih: skor pengaruh, radikalisme, kasus terkait |
| **Laporan** | Daftar laporan | Isi laporan + sitasi + tombol ekspor **PDF/DOCX** (berkop instansi, berklasifikasi, ada watermark nama pengunduh) |

---

#### ◉ Lapangan

Tampilannya **berbeda menurut peran**:

- **Petugas lapangan** melihat: tombol **🆘 Darurat** (kirim lokasi + tanda bahaya seketika),
  daftar tugas, dan formulir laporan (teks, GPS, foto/video).
  Bila **sinyal hilang**, laporan **disimpan dulu di HP** dan terkirim otomatis saat online
  kembali — *media perlu dilampirkan ulang*.
- **Analis/Admin** melihat: papan penugasan untuk membuat & memantau tugas.

---

#### ✓ Verifikasi · ▤ Audit

| Menu | Isinya | Gunanya |
|---|---|---|
| **Verifikasi** | Antrean hasil AI menunggu persetujuan + tombol **setujui/tolak massal** + **gabung duplikat** | Memastikan tak ada hasil mesin yang lolos tanpa diperiksa manusia |
| **Audit** | Catatan tiap perubahan: siapa, apa, kapan, dari IP mana | Bisa **memeriksa keaslian rantai catatan** (mendeteksi bila ada yang diutak-atik) dan **mendeteksi perilaku janggal** (akses tengah malam, percobaan akses berulang) |

---

### Siapa boleh mengakses apa?

Ada tingkatan, seperti pangkat:

| Peran | Boleh apa |
|---|---|
| **Pemantau** (viewer) | Hanya melihat. Tidak bisa mengubah apa pun. |
| **Petugas lapangan** (operative) | Menerima tugas, mengirim laporan dari lokasi. |
| **Analis** | Menganalisis, membuat laporan, mengelola kasus. |
| **Verifikator** | Memeriksa & menyetujui hasil olahan. |
| **Manajer** | Memberi penugasan & persetujuan. |
| **Pengambil Keputusan** | Mengajukan permintaan informasi & memutuskan. |
| **Admin** | Mengatur seluruh sistem & pengguna. |

Pangkat lebih tinggi otomatis boleh melakukan hal yang boleh dilakukan pangkat di bawahnya.

### Yang JUJUR perlu Anda ketahui

Aplikasi ini sengaja dibuat **tidak menyembunyikan keterbatasannya**. Beberapa hal yang
harus Anda pahami supaya tidak salah mengambil keputusan:

**1. "Skor" bukan vonis, tapi petunjuk arah.**
Angka risiko atau skor kecurigaan buzzer artinya *"ini pantas diperiksa"*, **bukan**
*"ini sudah terbukti"*. Keputusan tetap di tangan manusia.

**2. Kelompok yang terdeteksi belum tentu bersalah.**
Di Analisis Buzzer, akun yang saling mendukung akan terlihat sebagai satu kelompok.
Tapi **kelompok aktivis, penggemar K-pop, dan pendengung bayaran sama-sama terlihat
seperti itu**. Anda yang menilai mana yang manipulatif.

**3. Perhatikan label "bukti lemah".**
Kalau skornya tinggi tapi datanya cuma sedikit, sistem akan menulis **"bukti lemah"**.
Artinya: jangan terlalu percaya angka itu.

**4. Prediksi ML belum bisa dipakai.**
Ada fitur perkiraan titik rawan, tapi mutunya masih setara tebak-tebakan — dan sistem
**menandainya sendiri** dengan "⚠ belum andal". Sebabnya: riwayat kejadian belum cukup
panjang. Jangan jadikan dasar keputusan.

**5. Pemeriksaan foto mendeteksi *suntingan*, bukan *deepfake*.**
Alat forensik bisa melihat bekas editing, tapi **wajah palsu buatan AI modern sering
lolos**. Sebaliknya, foto asli dari WhatsApp sering ikut tertandai karena WhatsApp
memang mengompres ulang setiap gambar.

**6. Laporan AI wajib diperiksa manusia.**
Setiap laporan otomatis masuk antrean **Verifikasi** dan berstatus "menunggu" sampai
seorang analis menyetujuinya.

### Istilah yang sering muncul

| Istilah | Artinya sehari-hari |
|---|---|
| **OSINT** | Mencari informasi dari sumber terbuka (yang bisa diakses siapa saja). |
| **Sentimen** | Nada berita: positif, netral, atau negatif. |
| **Klaster** | Kelompok — bisa kelompok berita mirip, atau kelompok akun yang saling terhubung. |
| **Buzzer / CIB** | Sekumpulan akun yang digerakkan bersama untuk mendorong isu tertentu. |
| **Sinyal** | Satu potong temuan hasil penelusuran. |
| **Geotag** | Menandai lokasi pada peta. |
| **Audit** | Catatan otomatis: siapa melakukan apa dan kapan. |
| **Chain of custody** | Rantai bukti — riwayat perlakuan sebuah barang bukti agar sah. |

---

## 2. Gambaran Umum

**Sentinel Nusantara** adalah platform intelijen prediktif untuk memantau tekanan
sosial di Indonesia. Ia menggabungkan indikator sosial-ekonomi wilayah, jaringan
aktor, sinyal OSINT, simulasi berbasis agen (ABM), dan laporan naratif yang disusun
pipeline AI — dengan prinsip **anti-halusinasi**: tiap klaim ter-grounding pada data,
disertai sitasi dan disclaimer.

| Aspek | Fakta | Sumber |
|---|---|---|
| Tujuan | Deteksi dini & analisis tekanan sosial | `README.md:3` |
| Bahasa | Python 3.11 · TypeScript | `pyproject.toml` |
| Framework | FastAPI · Next.js (App Router) | `app/main.py:52` |
| Database | PostgreSQL (asyncpg) · Neo4j · Redis | `app/core/db.py:15-20` |
| Object storage | MinIO (S3-compatible) | `app/core/config.py:26-31` |
| Real-time | LiveKit (war room) · SSE firehose | `app/core/livekit.py` |

**Siklus intelijen 4 fase:** Koleksi → Fondasi Data → Analisis → Diseminasi.

---

## 3. Arsitektur

```
        Pengguna (browser / PWA)
                 │ HTTPS
        ┌────────▼─────────┐
        │  Next.js (web)   │  37 halaman · src/app/(app)/
        └────────┬─────────┘
                 │ REST + Bearer JWT
        ┌────────▼─────────────────────────────┐
        │  FastAPI (api)                        │
        │  main.py:52 · AuditMiddleware:64      │
        │  routers → services/repo → models     │
        │  CommitRoute (commit sebelum response)│
        └──┬──────┬──────┬──────┬──────┬────────┘
           │      │      │      │      │
      PostgreSQL Neo4j Redis  MinIO  LiveKit
           ▲
           │ HTTP
   ┌───────┴────────────────────────────┐
   │ osint-runner (scraping X/Threads,  │
   │ SpiderFoot, SearXNG)               │
   └────────────────────────────────────┘
   GPU services: vision (gemma4) · Whisper STT · InsightFace
```

### Rantai request (setiap panggilan API)

```
1. src/lib/client.ts            apiGet/apiPost + Bearer
2. app/main.py:52               FastAPI
3. app/core/audit.py            AuditMiddleware — catat semua mutasi
4. app/api/routers/<domain>.py  @router.<method>
5. app/api/deps.py:14           get_current_user   → 401
6. app/api/deps.py:73           require_role       → 403
7. app/core/db.py:23            get_db (async session)
8. app/services/<x>_repo.py     query SQLAlchemy
9. app/models/<x>.py            mapping ORM
10. app/core/route.py:32        CommitRoute — commit SEBELUM response
11. app/schemas/                Pydantic → JSON → React
```

> **Penting bagi pengembang:** router baru **wajib** memakai `route_class=CommitRoute`.
> Dependency `yield` bawaan FastAPI commit *setelah* response, sehingga klien yang
> langsung memuat ulang membaca data lama — inilah sebab keluhan "harus refresh".

> **Jebakan urutan route:** rute statis harus dideklarasikan **sebelum** `/{id}`.
> Contoh: `/cases/correlations` sebelum `/cases/{case_id}`; `regions.py:24`
> `/history/national` sebelum `/{code}`.

---

## 4. Struktur Project

```
sentinel-backend/          FastAPI + PostgreSQL + pipeline AI
  app/
    main.py                entry point, registrasi 42 router
    core/                  config, db, security, audit, route, redis, neo4j
    api/routers/           42 router HTTP
    services/              logika bisnis + repository
    models/                34 tabel SQLAlchemy
    schemas/               kontrak Pydantic
    pipeline/              LangGraph: ingest→analyst→forecaster→reporter
  alembic/versions/        55 migrasi (head 0055)
  osint-runner/server.py   scraping X/Threads, alat OSINT
sentinel-web/              Next.js
  src/app/(app)/           37 halaman menu
  src/components/          komponen UI
  src/lib/                 client API, helper, tipe
sentinel-abm/              simulasi berbasis agen
docs/                      dokumentasi
```

---

## 5. Menu Aplikasi

| Grup | Menu | Path | Peran |
|---|---|---|---|
| Pemantauan | Mulai di sini | `/mulai` | ALL |
| Pemantauan | Pusat Komando | `/` | MON |
| Pemantauan | Peta Taktis | `/peta` | ALL |
| Pemantauan | Kejadian | `/kejadian` | MON |
| Pemantauan | Peringatan | `/peringatan` | MON |
| Investigasi | Kasus | `/kasus` | ANALYSIS |
| Investigasi | Verifikasi | `/verifikasi` | ANALYSIS |
| Investigasi | Jaringan | `/jaringan` | ANALYSIS |
| Investigasi | Knowledge Graph | `/graf` | ANALYSIS |
| Investigasi | Notebook Analis | `/notebook` | ANALYSIS |
| Investigasi | Analisis Buzzer | `/buzzer` | OPS |
| Investigasi | Pemantauan Kata Kunci | `/pemantauan` | OPS |
| Investigasi | Perburuan Ancaman | `/perburuan` | ANALYSIS |
| Investigasi | Indikator Ancaman | `/indikator` | ANALYSIS |
| Investigasi | Agen Investigasi | `/agen` | ANALYSIS |
| Investigasi | OSINT | `/osint` | OPS |
| Investigasi | Cari | `/cari` | OPS |
| Koleksi | Sumber Koleksi | `/sumber` | OPS |
| Koleksi | Sinyal | `/sinyal` | OPS |
| Koleksi | Berita | `/berita` | MON |
| Koleksi | Lapangan | `/lapangan` | OPS |
| Koleksi | Token | `/token` | OPS |
| Analisis | Permintaan Informasi | `/rfi` | OPS |
| Analisis | Laporan | `/laporan` | MON |
| Analisis | Analisis Media | `/analisis` | OPS |
| Analisis | Simulasi | `/simulasi` | ANALYSIS |
| Analisis | Sentinel AI | `/asisten` | ANALYSIS |
| Data & Tata Kelola | Wilayah | `/wilayah` | ANALYSIS |
| Data & Tata Kelola | Ruang | `/ruang` | ANALYSIS |
| Data & Tata Kelola | Pengguna | `/pengguna` | ADMIN |
| Data & Tata Kelola | Watchlist Wajah | `/watchlist` | ADMIN |
| Data & Tata Kelola | Pengaturan | `/pengaturan` | ADMIN |
| Data & Tata Kelola | LLM | `/llm` | ADMIN |
| Data & Tata Kelola | Ontologi | `/ontologi` | ANALYSIS |
| Data & Tata Kelola | Audit | `/audit` | ADMIN |
| Data & Tata Kelola | Akun | `/akun` | ALL |

**Total:** 36 menu.

---

## 6. Cara Kerja Tiap Menu — dari klik sampai data tampil

Setiap baris ditelusuri dari kode: **halaman → endpoint → router**.

> Pola umum: halaman memanggil `apiGet/apiPost` (`src/lib/client.ts`, menyisipkan Bearer JWT)
> → router memvalidasi peran (`deps.py`) → service/repository menjalankan query →
> hasil dipetakan schema Pydantic → React merender.

| Menu | Berkas halaman | Endpoint yang dipanggil | Router backend |
|---|---|---|---|
| **Pusat Komando** `/` | `app/(app)/page.tsx` | `actors` · `alerts` · `alerts/count` · `audit` · `cases` · `events` · `field/tasks/count` _(+8)_ | `actors.py`, `alerts.py`, `audit.py`, `cases.py`, `events.py` |
| **Peta Taktis** `/peta` | `app/(app)/peta/page.tsx` | `actors/region-metrics` · `regions` | `actors.py`, `regions.py` |
| **Kejadian** `/kejadian` | `app/(app)/kejadian/page.tsx` | `auth/me` · `events` · `events/geotag-missing` · `events/hotspots` · `events/hotspots/ml` · `events/hotspots/train` · `events/nl-query` _(+2)_ | `auth.py`, `events.py`, `regions.py` |
| **Berita** `/berita` | `app/(app)/berita/page.tsx` | `auth/me` · `keyword-intel/trends/threads` · `keyword-intel/trends/x` · `news` · `news/by-category` · `news/by-source` · `news/disinfo` _(+6)_ | `auth.py`, `keyword_intel.py`, `news.py`, `regions.py` |
| **Peringatan** `/peringatan` | `app/(app)/peringatan/page.tsx` | `alerts` · `alerts/count` · `alerts/rules` · `auth/me` · `regions` | `alerts.py`, `auth.py`, `regions.py` |
| **Kasus** `/kasus` | `app/(app)/kasus/page.tsx` | `auth/me` · `cases` · `cases/correlations` · `regions` | `auth.py`, `cases.py`, `regions.py` |
| **Verifikasi** `/verifikasi` | `app/(app)/verifikasi/page.tsx` | `auth/me` · `verification` · `verification/bulk-review` · `verification/merge-duplicates` | `auth.py`, `verification.py` |
| **Jaringan** `/jaringan` | `app/(app)/jaringan/page.tsx` | `actors` · `actors/graph` · `auth/me` · `organizations` · `regions` | `actors.py`, `auth.py`, `organizations.py`, `regions.py` |
| **Knowledge Graph** `/graf` | `app/(app)/graf/page.tsx` | `auth/me` · `kg` · `kg/entities` · `kg/graph` · `kg/ingest-osint` · `kg/path` · `kg/relationships` _(+1)_ | `auth.py`, `kg.py` |
| **Analisis Buzzer** `/buzzer` | `app/(app)/buzzer/page.tsx` | `auth/me` · `buzzer/analyses` · `buzzer/analyze` · `buzzer/save` | `auth.py`, `buzzer.py` |
| **Pemantauan Kata Kunci** `/pemantauan` | `app/(app)/pemantauan/page.tsx` | `keyword-intel/analyze` · `keyword-intel/trends/threads` · `keyword-intel/trends/x` | `keyword_intel.py` |
| **Perburuan Ancaman** `/perburuan` | `app/(app)/perburuan/page.tsx` | `auth/me` · `hunts` | `auth.py`, `hunts.py` |
| **Indikator Ancaman** `/indikator` | `app/(app)/indikator/page.tsx` | `auth/me` · `indicators` · `indicators/ingest` · `indicators/stats` | `auth.py`, `indicators.py` |
| **Agen Investigasi** `/agen` | `app/(app)/agen/page.tsx` | `agent/investigate` | `agent.py` |
| **OSINT** `/osint` | `app/(app)/osint/page.tsx` | `auth/me` · `regions` · `signals` | `auth.py`, `regions.py`, `signals.py` |
| **Cari** `/cari` | `app/(app)/cari/page.tsx` | `auth/me` · `search` · `search/federated` · `search/reindex` · `search/semantic` · `search/semantic/status` | `auth.py`, `search.py` |
| **Notebook Analis** `/notebook` | `app/(app)/notebook/page.tsx` | `auth/me` · `notebooks` | `auth.py`, `notebooks.py` |
| **Sinyal** `/sinyal` | `app/(app)/sinyal/page.tsx` | `auth/me` · `regions` · `signals` | `auth.py`, `regions.py`, `signals.py` |
| **Sumber Koleksi** `/sumber` | `app/(app)/sumber/page.tsx` | `auth/me` · `regions` · `sources` | `auth.py`, `regions.py`, `sources.py` |
| **Lapangan** `/lapangan` | `app/(app)/lapangan/page.tsx` | `auth/me` | `auth.py` |
| **Token** `/token` | `app/(app)/token/page.tsx` | — | — |
| **RFI** `/rfi` | `app/(app)/rfi/page.tsx` | `auth/me` · `regions` · `rfi` · `users/assignable` | `auth.py`, `regions.py`, `rfi.py`, `users.py` |
| **Laporan** `/laporan` | `app/(app)/laporan/page.tsx` | `auth/me` · `regions` · `reports` · `reports/briefing` | `auth.py`, `regions.py`, `reports.py` |
| **Analisis Media** `/analisis` | `app/(app)/analisis/page.tsx` | — | — |
| **Simulasi** `/simulasi` | `app/(app)/simulasi/page.tsx` | `regions` · `simulations` | `regions.py`, `simulations.py` |
| **Sentinel AI** `/asisten` | `app/(app)/asisten/page.tsx` | — | — |
| **Wilayah** `/wilayah` | `app/(app)/wilayah/page.tsx` | `auth/me` · `regions` | `auth.py`, `regions.py` |
| **Ruang** `/ruang` | `app/(app)/ruang/page.tsx` | `rooms` | `rooms.py` |
| **Pengguna** `/pengguna` | `app/(app)/pengguna/page.tsx` | `auth/me` · `users` | `auth.py`, `users.py` |
| **Watchlist Wajah** `/watchlist` | `app/(app)/watchlist/page.tsx` | `watchlist` | `watchlist.py` |
| **Pengaturan** `/pengaturan` | `app/(app)/pengaturan/page.tsx` | `admin/demo-seed` · `events/categories` · `mlops/dataset/summary` · `mlops/eval` · `mlops/redteam` · `settings` · `settings/sentiment-basis` | `admin.py`, `events.py`, `mlops.py`, `settings.py` |
| **LLM** `/llm` | `app/(app)/llm/page.tsx` | `auth/me` · `llm-configs` · `llm-configs/presets` | `auth.py`, `llm.py` |
| **Ontologi** `/ontologi` | `app/(app)/ontologi/page.tsx` | `ontology` | — |
| **Audit** `/audit` | `app/(app)/audit/page.tsx` | `audit` · `audit/anomalies` · `audit/verify-chain` · `auth/me` | `audit.py`, `auth.py` |
| **Akun** `/akun` | `app/(app)/akun/page.tsx` | `auth/2fa/disable` · `auth/2fa/enable` · `auth/2fa/setup` · `auth/change-password` · `auth/me` | `auth.py` |
| **Mulai** `/mulai` | `app/(app)/mulai/page.tsx` | `auth/me` | `auth.py` |
| **Dokumentasi** `/dokumentasi` | `app/(app)/dokumentasi/page.tsx` | — | — |

### Telusur mendalam — Pusat Komando (`/`)

Halaman ini **tidak** punya satu endpoint tunggal; ia menggabungkan ±15 panggilan
paralel lalu menghitung ringkasannya **di sisi klien**.

```
1. Buka "/"                     app/(app)/page.tsx  → refresh() (useCallback)
2. Ambil data pokok             GET /regions                (regions.py)
3. Paralel (Promise, .catch aman):
     GET /alerts/count          → badge "⚠ N peringatan"    (alert_repo.unacknowledged_count)
     GET /alerts?limit=8        → daftar peringatan terbaru
     GET /events?limit=10       → kejadian terbaru
     GET /cases?limit=500       → hitung "Total Kasus"
     GET /rfi?limit=500         → hitung "RFI Terbuka"
     GET /reports?limit=500     → hitung "Laporan Disetujui"
     GET /field/tasks/count     → "Tugas Lapangan Aktif"
     GET /actors?limit=200      → "Aktor Dipantau" + Top Aktor
     GET /stats/collection?days=30 → grafik pengumpulan
     GET /regions/history/national?days=30 → TREN RISIKO NASIONAL
     GET /regions/risk-ranking?limit=6     → PRIORITAS TINDAK LANJUT
4. Query DB                     region_repo.py:12  select(Region).order_by(Region.code)
                                alert_repo.py:50   select(Alert)
5. Hitung di klien              rata-rata SPI/Gini, distribusi risiko, gauge
6. Render                       MetricTile · RiskGauge · MiniHeatMap · MultiLine
7. Auto-refresh                 useInterval(refresh, 60000)  → tiap 60 detik
```

**Sumber angka di layar:** semua kartu metrik berasal dari tabel `regions`
(indikator sosial-ekonomi) — **bukan** hasil AI. Gauge = rata-rata `risk_score`.

---

### Telusur mendalam — Berita (`/berita`)

```
1. Buka "/berita"               app/(app)/berita/page.tsx
2. Feed (paginasi)              GET /news?limit&offset&sentiment&source&q&region
      → news.py list_news → news_repo.list_items (news_repo.py:110)
      → select(NewsItem) + _filtered(...) order_by created_at desc limit/offset
      → tabel `news_items`
3. Tab lain memanggil endpoint berbeda:
      🔥 Viral        GET /news/viral        → news_disinfo.viral_clusters (klaster judul-mirip)
      🏷 Per Sumber   GET /news/by-source    → news_repo.source_counts (GROUP BY source)
      🤖 Kategori AI  GET /news/by-category  → news_to_event.group_by_category (aturan kata kunci)
4. Panel samping:
      Topik Ramai     GET /news/trending     → news_repo.trending_terms (regex + stoplist)
      Lacak Narasi    GET /news/narrative?q= → news_disinfo.narrative_evolution
      Trending Medsos GET /keyword-intel/trends/x|threads → osint-runner
      Disinformasi    GET /news/disinfo      → news_disinfo.detect
5. Render                       NewsFeed · SourceGroups · CategoryGroups · ViralClusters
```

**Dari mana berita masuk?** Bukan saat halaman dibuka. RSS ditarik oleh
**penjadwal harian** di dalam container api (`core/scheduler.py` → `news_ingest.ingest_feeds`)
atau manual lewat tombol **Ingest RSS**. Halaman hanya **membaca** `news_items`.

---

### Telusur mendalam — Kejadian (`/kejadian`)

```
1. Buka "/kejadian"             app/(app)/kejadian/page.tsx
2. Daftar + peta                GET /events?limit=500&offset=…&event_type&severity
                                     &sentiment&region&from&to
      → events.py list_events → event_repo.list_events (event_repo.py:78)
      → select(Event) + _apply_filters(...) order_by occurred_at desc
      → tabel `events`
3. Timeline                     GET /events/stats  → agregat per hari (by_day)
4. Klik hari di timeline        set from/to → FETCH ULANG ke server
      (bukan menyaring halaman termuat — supaya hari lama tetap muncul)
5. Peta                         EventMap: titik presisi bila lat/lon ada;
                                bila hanya region_id → penanda klaster per wilayah
6. Prediksi ML                  GET /events/hotspots/ml → hotspot_ml.predict
7. Render                       EventTimeline · EventMap/ReplayMap · EventManager
```

**Dari mana kejadian berasal?** Tiga jalur, semuanya menulis ke `events`:
`POST /events` (manual) · `POST /events/import-news` (berita → kejadian, terklasifikasi
kategori) · `POST /events/import-acled` & `import-gdelt` (sumber luar).
Yang tanpa koordinat dapat ditandai wilayahnya lewat `POST /events/geotag-missing`.

---

### Pola yang berlaku di semua menu lain

| Pola | Contoh |
|---|---|
| **Daftar berpaginasi** `usePagedList(fetchPage)` → `GET <domain>?limit&offset` | Kasus, Verifikasi, Sinyal, Laporan, Berita, Kejadian |
| **Detail saat dipilih** → `GET <domain>/{id}` | Kasus, Laporan, Media |
| **Aksi tulis** → `POST/PATCH/DELETE` lalu `reload()` | semua menu CRUD |
| **Panel AI** → endpoint khusus, hasil tak disimpan (dihitung saat diminta) | Decision Support, Kategori AI, Narasi |
| **Filter dikirim ke server**, bukan disaring di klien | Kejadian, Berita, Kasus |


---

## 7. Database

| Tabel | Model | Kolom | Foreign Key |
|---|---|---|---|
| `ai_feedback` | `ai_feedback.py` | 8 | — |
| `alert_rules` | `alert.py` | 21 | `alert_rules.id`, `users.id` |
| `alerts` | `alert.py` | 21 | `alert_rules.id`, `users.id` |
| `app_settings` | `app_setting.py` | 16 | — |
| `audit_logs` | `audit.py` | 11 | — |
| `buzzer_analyses` | `buzzer.py` | 12 | — |
| `case_links` | `case.py` | 17 | `cases.id`, `users.id` |
| `cases` | `case.py` | 17 | `cases.id`, `users.id` |
| `collection_sources` | `collection_source.py` | 11 | — |
| `custody_events` | `field.py` | 55 | `field_reports.id`, `field_tasks.id`, `media_attachments.id`, `regions.id`, `users.id` |
| `embeddings` | `embedding.py` | 7 | — |
| `events` | `event.py` | 14 | `regions.id`, `users.id` |
| `face_watchlist` | `face_watchlist.py` | 6 | `users.id` |
| `field_reports` | `field.py` | 55 | `field_reports.id`, `field_tasks.id`, `media_attachments.id`, `regions.id`, `users.id` |
| `field_tasks` | `field.py` | 55 | `field_reports.id`, `field_tasks.id`, `media_attachments.id`, `regions.id`, `users.id` |
| `hunts` | `hunt.py` | 7 | — |
| `ingest_tokens` | `ingest_token.py` | 6 | `users.id` |
| `llm_configs` | `llm_config.py` | 9 | `users.id` |
| `media_analyses` | `media_analysis.py` | 9 | `media_attachments.id` |
| `media_attachments` | `field.py` | 55 | `field_reports.id`, `field_tasks.id`, `media_attachments.id`, `regions.id`, `users.id` |
| `ml_models` | `ml_model.py` | 5 | — |
| `news_items` | `news.py` | 13 | `users.id` |
| `notebooks` | `notebook.py` | 9 | — |
| `osint_signals` | `signal.py` | 9 | `regions.id`, `users.id` |
| `region_risk_history` | `risk_history.py` | 5 | `regions.id` |
| `regions` | `region.py` | 13 | — |
| `reports` | `report.py` | 19 | `llm_configs.id`, `regions.id`, `users.id` |
| `rfis` | `rfi.py` | 22 | `reports.id`, `rfis.id`, `users.id` |
| `saved_views` | `saved_view.py` | 5 | `users.id` |
| `simulations` | `simulation.py` | 8 | `regions.id`, `users.id` |
| `threat_indicators` | `indicator.py` | 10 | — |
| `users` | `user.py` | 9 | — |
| `verification_items` | `verification.py` | 12 | `users.id` |
| `war_rooms` | `war_room.py` | 4 | `users.id` |

**Total:** 34 tabel.

### Relasi utama

- **26 FK menuju `users.id`** — atribusi "siapa melakukan apa".
- `regions` → `osint_signals`, `events`, `reports`, `simulations`, `region_risk_history` (1:N).
- `cases` → `case_links` → sinyal/aktor/laporan/kejadian/peringatan.
- `alert_rules` → `alerts`; `field_tasks` → `field_reports` → `media_attachments` → `media_analyses`.
- `regions` bersifat **snapshot**; dimensi waktu ada di `region_risk_history` & `events.occurred_at`.


---

## 8. API

| Router | Prefix | Endpoint |
|---|---|---|
| `actors.py` | `/actors` | `GET /actors/` · `GET /actors/graph` · `GET /actors/graph-analytics` · `GET /actors/path` · `GET /actors/{actor_id}/cases` · `GET /actors/region-metrics` · `GET /actors/{actor_id}` · `GET /actors/{actor_id}/link-suggestions` · `POST /actors/` · `POST /actors/propose` · `POST /actors/extract-profile` · `POST /actors/from-osint` · `PATCH /actors/{actor_id}/accounts` · `PATCH /actors/{actor_id}` · `DELETE /actors/{actor_id}` · `POST /actors/{actor_id}/connections` · `DELETE /actors/{actor_id}/connections/{target_id}` · `POST /actors/{actor_id}/affiliations` · `DELETE /actors/{actor_id}/affiliations/{org_id}` |
| `admin.py` | `/admin` | `POST /admin/demo-seed` · `DELETE /admin/demo-seed` |
| `agent.py` | `/agent` | `POST /agent/investigate` |
| `alerts.py` | `/alerts` | `POST /alerts/rules` · `GET /alerts/rules` · `PATCH /alerts/rules/{rule_id}` · `DELETE /alerts/rules/{rule_id}` · `POST /alerts/evaluate` · `GET /alerts/count` · `GET /alerts/` · `GET /alerts/{alert_id}` · `PATCH /alerts/{alert_id}` |
| `analyze.py` | `/analyze` | `POST /analyze/media` |
| `assistant.py` | `/assistant` | `POST /assistant/chat` · `POST /assistant/copilot` |
| `audit.py` | `/audit` | `GET /audit/` · `GET /audit/verify-chain` · `GET /audit/anomalies` · `GET /audit/count` · `GET /audit/export.csv` |
| `auth.py` | `/auth` | `POST /auth/login` · `POST /auth/refresh` · `GET /auth/me` · `POST /auth/change-password` · `POST /auth/2fa/setup` · `POST /auth/2fa/enable` · `POST /auth/2fa/disable` |
| `buzzer.py` | `/buzzer` | `POST /buzzer/analyze` · `GET /buzzer/x/accounts` · `POST /buzzer/x/account` · `DELETE /buzzer/x/account/{username}` · `POST /buzzer/x/test` · `POST /buzzer/x/profile` · `POST /buzzer/threads/profile` · `POST /buzzer/x/collect` · `POST /buzzer/x/collect-start` · `GET /buzzer/x/collect-status/{job_id}` · `GET /buzzer/threads/session` · `POST /buzzer/threads/session` · `DELETE /buzzer/threads/session` · `POST /buzzer/threads/test` · `POST /buzzer/threads/collect` · `POST /buzzer/save` · `GET /buzzer/analyses` · `GET /buzzer/analyses/{analysis_id}` · `DELETE /buzzer/analyses/{analysis_id}` |
| `cases.py` | `/cases` | `POST /cases/` · `GET /cases/` · `GET /cases/correlations` · `GET /cases/{case_id}` · `PATCH /cases/{case_id}` · `DELETE /cases/{case_id}` · `POST /cases/{case_id}/links` · `DELETE /cases/{case_id}/links/{link_id}` · `POST /cases/{case_id}/ai-analysis` |
| `events.py` | `/events` | `POST /events/` · `POST /events/import-gdelt` · `POST /events/import-acled` · `POST /events/import-news` · `GET /events/categories` · `POST /events/geotag-missing` · `GET /events/` · `GET /events/stats` · `POST /events/hotspots/train` · `GET /events/hotspots/ml` · `GET /events/hotspots` · `GET /events/replay` · `POST /events/nl-query` · `GET /events/{event_id}/impact` · `GET /events/{event_id}` · `PATCH /events/{event_id}` · `DELETE /events/{event_id}` |
| `feedback.py` | `/feedback` | `POST /feedback/` · `GET /feedback/stats` |
| `field.py` | `/field` | `POST /field/panic` · `POST /field/tasks` · `POST /field/self-tasks` · `GET /field/tasks` · `GET /field/tasks/count` · `GET /field/tasks/{task_id}` · `PATCH /field/tasks/{task_id}` · `DELETE /field/tasks/{task_id}` · `POST /field/tasks/{task_id}/checkin` · `POST /field/tasks/{task_id}/checkout` · `GET /field/tasks/{task_id}/reports` · `GET /field/media/{media_id}/analysis` · `GET /field/media/{media_id}/verify` · `GET /field/media/{media_id}/forensics` · `GET /field/media/{media_id}/similar` · `GET /field/media/{media_id}/custody` · `GET /field/media/{media_id}` |
| `health.py` | `/` | `GET /health` · `GET /health/services` |
| `hunts.py` | `/hunts` | `GET /hunts/` · `POST /hunts/` · `POST /hunts/{hunt_id}/run` · `DELETE /hunts/{hunt_id}` |
| `indicators.py` | `/indicators` | `POST /indicators/ingest` · `GET /indicators/` · `GET /indicators/stats` · `DELETE /indicators/{indicator_id}` |
| `ingest.py` | `/ingest` | `POST /ingest/osint` |
| `ingest_tokens.py` | `/ingest-tokens` | `POST /ingest-tokens/` · `GET /ingest-tokens/` · `DELETE /ingest-tokens/{token_id}` |
| `keyword_intel.py` | `/keyword-intel` | `GET /keyword-intel/trends/x` · `GET /keyword-intel/trends/threads` · `POST /keyword-intel/analyze` |
| `kg.py` | `/kg` | `GET /kg/types` · `GET /kg/stats` · `GET /kg/graph` · `GET /kg/{node_id}/expand` · `GET /kg/path` · `POST /kg/entities` · `POST /kg/relationships` · `DELETE /kg/entities/{node_id}` · `POST /kg/ingest-osint` |
| `llm.py` | `/llm-configs` | `GET /llm-configs/presets` · `POST /llm-configs/list-models` · `GET /llm-configs/` · `POST /llm-configs/` · `PATCH /llm-configs/{config_id}` · `DELETE /llm-configs/{config_id}` · `POST /llm-configs/{config_id}/test` |
| `mlops.py` | `/mlops` | `GET /mlops/eval` · `GET /mlops/dataset/summary` · `GET /mlops/dataset/export.jsonl` |
| `news.py` | `/news` | `POST /news/` · `GET /news/` · `GET /news/sources` · `GET /news/feeds` · `POST /news/search-preview` · `POST /news/search-x-preview` · `POST /news/search-bluesky-preview` · `POST /news/save` · `POST /news/bulk-delete` · `GET /news/sentiment-summary` · `GET /news/disinfo` · `GET /news/trending` · `GET /news/stats` · `GET /news/by-source` · `GET /news/by-category` · `GET /news/narrative` · `GET /news/viral` · `GET /news/{item_id}` · `PATCH /news/{item_id}` · `POST /news/ingest-rss` · `POST /news/cleanup` · `POST /news/geotag-backfill` · `POST /news/normalize-sources` · `POST /news/rescore` · `POST /news/rescore-start` · `GET /news/rescore-status/{job_id}` · `DELETE /news/{item_id}` |
| `notebooks.py` | `/notebooks` | `GET /notebooks/` · `POST /notebooks/` · `GET /notebooks/{nb_id}` · `PATCH /notebooks/{nb_id}` · `DELETE /notebooks/{nb_id}` |
| `ontology.py` | `/` | `GET /ontology` · `GET /mlops/redteam` |
| `organizations.py` | `/organizations` | `GET /organizations/` · `POST /organizations/` · `PATCH /organizations/{org_id}` · `DELETE /organizations/{org_id}` |
| `osint_enum.py` | `/osint` | `GET /osint/runnable` · `GET /osint/intel-platforms` · `GET /osint/intel-status` · `POST /osint/intel-search` · `POST /osint/intel-analyze` · `POST /osint/run` · `POST /osint/username` · `POST /osint/email` · `POST /osint/exif` · `GET /osint/whois` · `GET /osint/wayback` · `GET /osint/phone` · `POST /osint/threads/replies` · `POST /osint/threads/user-posts` |
| `regions.py` | `/regions` | `GET /regions/` · `GET /regions/history/national` · `POST /regions/apply-bps` · `GET /regions/{code}/decision-support` · `GET /regions/situation` · `GET /regions/risk-ranking` · `POST /regions/history/snapshot` · `GET /regions/{code}/history` · `GET /regions/{code}` · `POST /regions/` · `PATCH /regions/{code}` · `DELETE /regions/{code}` |
| `reports.py` | `/reports` | `POST /reports/` · `POST /reports/from-case/{case_id}` · `PATCH /reports/{report_id}` · `GET /reports/` · `GET /reports/{report_id}` · `POST /reports/{report_id}/approval` · `GET /reports/{report_id}/signature/verify` · `PATCH /reports/{report_id}/classification` · `POST /reports/briefing` · `PATCH /reports/{report_id}/release` · `GET /reports/{report_id}/export.pdf` · `GET /reports/{report_id}/export.docx` · `GET /reports/{report_id}/lineage` |
| `rfi.py` | `/rfi` | `POST /rfi/` · `GET /rfi/` · `GET /rfi/assigned-count` · `GET /rfi/{rfi_id}` · `PATCH /rfi/{rfi_id}` · `POST /rfi/{rfi_id}/decision` · `POST /rfi/{rfi_id}/feedback` · `DELETE /rfi/{rfi_id}` |
| `rooms.py` | `/rooms` | `POST /rooms/token` · `GET /rooms/` · `POST /rooms/` |
| `saved_views.py` | `/saved-views` | `GET /saved-views/` · `POST /saved-views/` · `DELETE /saved-views/{view_id}` |
| `search.py` | `/search` | `GET /search/` · `GET /search/federated` · `GET /search/semantic` · `GET /search/semantic/status` · `POST /search/reindex` |
| `settings.py` | `/settings` | `GET /settings/sentiment-basis` · `GET /settings/` · `PATCH /settings/` |
| `signals.py` | `/signals` | `POST /signals/` · `GET /signals/` · `GET /signals/{signal_id}` · `DELETE /signals/{signal_id}` |
| `simulations.py` | `/simulations` | `POST /simulations/compare` · `POST /simulations/` · `GET /simulations/` · `GET /simulations/{simulation_id}` |
| `sources.py` | `/sources` | `POST /sources/{source_id}/yt-ingest` · `POST /sources/yt-ingest-all` · `GET /sources/` · `POST /sources/` · `PATCH /sources/{source_id}` · `DELETE /sources/{source_id}` · `POST /sources/{source_id}/snapshot` |
| `stats.py` | `/stats` | `GET /stats/collection` · `GET /stats/roles` |
| `stream.py` | `/stream` | `GET /stream/firehose` |
| `users.py` | `/users` | `POST /users/` · `GET /users/` · `GET /users/assignable` · `PATCH /users/{user_id}` · `DELETE /users/{user_id}` |
| `verification.py` | `/verification` | `GET /verification/duplicates` · `POST /verification/` · `GET /verification/` · `GET /verification/count` · `GET /verification/{item_id}` · `POST /verification/merge-duplicates` · `POST /verification/bulk-review` · `PATCH /verification/{item_id}/review` |
| `watchlist.py` | `/watchlist` | `GET /watchlist/` · `POST /watchlist/` · `DELETE /watchlist/{entry_id}` |

**Total:** 279 endpoint di 42 router.

**Autentikasi:** JWT HS256 (`app/core/security.py:25`), access 15 menit / refresh 7 hari,
password bcrypt (`security.py:9`). Semua endpoint memerlukan `Authorization: Bearer <token>`
kecuali `/auth/login`, `/auth/refresh`, dan `/health`.

**Pola galat:** `401` autentikasi gagal · `403` peran kurang · `404` tak ditemukan ·
`422` input tak sah · `502` layanan hulu menolak · `503` layanan hulu tak terjangkau.

---

## 9. Hak Akses (RBAC berjenjang)

RBAC memakai **peringkat**, bukan daftar kaku (`app/api/deps.py:73-85`):

```python
threshold = min(role_rank(r) for r in roles)   # deps.py:78
if role_rank(user.role) < threshold: 403       # deps.py:82
```

| Peran | Rank | Wewenang |
|---|---|---|
| `viewer` | 0 | baca saja |
| `operative` | 1 | intel lapangan: tugas, laporan GPS+media, panic |
| `analyst` | 2 | analisis, laporan, kasus, jaringan |
| `verifikator` | 2 | verifikasi & dedup produk (setara analyst) |
| `manager` | 3 | tasking & persetujuan |
| `decision_maker` | 4 | RFI & keputusan |
| `admin` | 99 | superuser — selalu lolos |

> ⚠️ **Konsekuensi penting:** karena ambang = rank terendah yang dicantumkan,
> `require_role(analyst, admin)` berambang 2 sehingga **verifikator ikut lolos**.
> Ini disengaja (`deps.py:74-77`) agar daftar lama otomatis mengizinkan peran lebih
> senior — tetapi **daftar peran pada dekorator bukan whitelist eksak**. Bila sebuah
> endpoint harus eksklusif untuk satu peran, `require_role` **tidak cukup**.

---

## 10. Alur Bisnis Utama

```
Login (JWT)
  ↓
OSINT — investigasi target, alat langsung-pakai, Case Tray
  ↓
Ekspor temuan → Sinyal  (atau via Token oleh intel lapangan)
  ↓
Jaringan — petakan aktor, skor radikalisme & mobilisasi
  ↓
Laporan — pipeline LangGraph:
     ingest → actor_analyst → analyst → forecaster → consensus → reporter
     → narasi + forecast + sitasi + disclaimer
  ↓
Verifikasi — antrian human-in-the-loop (approve/reject, bisa massal)
  ↓
Pusat Komando / Peta — posisi risiko nasional (auto-refresh 60 dtk)
  ↓
Ruang (War Room) · Cari · Audit (semua mutasi tercatat)
```

**Otomasi (rules engine, `services/automation.py`):** kondisi
(`news_negative_share` / `event_count` / `region_risk`) → aksi
`alert` · `rfi` · `field_task` · `hunt`. Aksi `field_task` memilih petugas via
`assign_to` atau **pemerataan beban** ke operative dengan tugas aktif paling sedikit.
Cooldown 12 jam per aturan.

---

## 11. Operasional

### Deployment

Stack berjalan via docker-compose. Sinkronisasi source memakai rsync, lalu build ulang
service yang berubah:

```bash
# web saja
scripts/deploy.sh web
# backend (menjalankan alembic upgrade head saat start)
scripts/deploy.sh api
# osint-runner (tidak dicakup skrip — manual)
rsync -az --exclude __pycache__ -e "ssh -p <port>" ./sentinel-backend/osint-runner/ \
  <user>@<host>:/home/<user>/sentinel/sentinel-backend/osint-runner/
ssh <host> "cd .../sentinel-backend && docker compose build osint-runner && \
  docker compose up -d --force-recreate osint-runner"
```

> **Penting:** rebuild `api` **tanpa** menyinkronkan `app/` lebih dulu akan menyajikan
> route lama. Selalu rsync `app/` sebelum build.
> Host berjalan `ENV=production`, sehingga guard rahasia aktif — semua secret bawaan
> harus diganti di `.env` host atau container gagal start (`config.py:100-114`).

### Maintenance

| Tugas | Perintah / lokasi |
|---|---|
| Cek kesehatan | `GET /health`, `GET /health/services` |
| Migrasi | `alembic current` / `alembic upgrade head` (otomatis saat api start) |
| Backup & DR | lihat `docs/BACKUP-DR.md` |
| Skala & HA | lihat `docs/SCALING-HA.md` |
| Integritas audit | `GET /audit/verify-chain` (hash-chain) |
| Anomali orang-dalam | `GET /audit/anomalies` |
| Retensi berita | `POST /news/cleanup` (bawaan 90 hari) |

---

## 12. Temuan Teknis & Rekomendasi

### Keamanan

| Temuan | Bukti | Nilai |
|---|---|---|
| Default rahasia dev ada di kode | `config.py:4,7,39` | 🟡 |
| **Guard produksi menolak default** saat `ENV != dev` | `config.py:100-114` | ✅ mitigasi nyata |
| Kunci Fernet divalidasi saat start (gagal cepat) | `config.py:109-113` | ✅ |
| IP GPU ter-hardcode sebagai *default* (dapat di-override env) | `config.py:73-76` | 🟡 |
| bcrypt · JWT HS256 | `security.py:9,25` | ✅ |
| Biometrik wajah **mati secara bawaan** menunggu dasar hukum | `config.py:44` | ✅ sikap etis |

### Performa

| Temuan | Bukti | Solusi |
|---|---|---|
| **N+1**: query per item dalam loop | `api/routers/verification.py:88-89` | satu `select().where(id.in_(ids))` lalu proses di memori |
| Klaster berita O(n²) | `services/news_disinfo.py` | sudah dibatasi 1200 baris; naikkan hati-hati |
| Media default tersimpan di DB | `config.py:26` | set `MEDIA_STORAGE=minio` di produksi |

### Technical debt

- **9 tes web gagal (pra-ada)** — `SavedViews` memanggil `useRouter` tanpa router
  ter-mount di jsdom (`src/components/SavedViews.tsx:16`). Perlu mock `next/navigation`.
- **Model ML hotspot belum andal** — AUC ≈ 0.50 karena riwayat kejadian bukan deret
  waktu kontinu (ada lubang ~9 bulan). UI sudah menandainya "⚠ AUC rendah — belum andal".
  Perbaikan sebenarnya: backfill riwayat nyata (ACLED/GDELT), bukan tuning model.
- **Dokumen lama sebagian usang** — `docs/SAD-SDD.md` & `docs/SDD-Sentinel-Nusantara.md`
  belum memuat endpoint & migrasi terbaru; dokumen ini yang paling mutakhir.

### Batas yang perlu diketahui pengguna

- **Forensik media** mendeteksi *penyuntingan*, **bukan deepfake AI generatif**.
- **Trending X** memakai agregator publik bila endpoint akun mengembalikan kosong —
  sumber selalu ditampilkan apa adanya di UI.
- **Outbox lapangan** mengantre teks+GPS saat offline; **media tidak ikut** (kuota
  penyimpanan browser) dan harus dilampirkan ulang.

---

*Dokumen ini dibangkitkan dari source code. Bila menu/tabel/endpoint berubah, bangkitkan
ulang bagian 4–6 agar tetap sinkron.*
