Browse Source

docs: dokumentasi lengkap GeoNetAgent (docs/, AGENTS.md, README.md, CHANGELOG.md, .cursor/rules/)

master
Budi Setiawan 3 weeks ago
parent
commit
ba77706d0e
  1. 14
      GeoNetAgent/.archive/README.md
  2. 0
      GeoNetAgent/.archive/collector-README.md
  3. 37
      GeoNetAgent/.archive/infra-host.md
  4. 9
      GeoNetAgent/.cursor/adr/001-agent-asset-identity.md
  5. 38
      GeoNetAgent/.cursor/adr/002-asset-split-by-hardware.md
  6. 192
      GeoNetAgent/.cursor/adr/003-asset-documents-qnap.md
  7. 13
      GeoNetAgent/.cursor/agent-playbook.md
  8. 112
      GeoNetAgent/.cursor/api-contracts.md
  9. 3
      GeoNetAgent/.cursor/blueprint.md
  10. 495
      GeoNetAgent/.cursor/context-index.md
  11. 112
      GeoNetAgent/.cursor/db-schema.md
  12. 178
      GeoNetAgent/.cursor/progress.md
  13. 18
      GeoNetAgent/.cursor/repo-structure.md
  14. 52
      GeoNetAgent/.cursor/rules/agent-release-deploy.mdc
  15. 35
      GeoNetAgent/.cursor/rules/documentation.mdc
  16. 3
      GeoNetAgent/.cursor/rules/project-context.mdc
  17. 40
      GeoNetAgent/.cursor/rules/security.mdc
  18. 6
      GeoNetAgent/.cursor/server.md
  19. 84
      GeoNetAgent/AGENTS.md
  20. 102
      GeoNetAgent/CHANGELOG.md
  21. 120
      GeoNetAgent/README.md
  22. 163
      GeoNetAgent/agent/Collect-GeoNetReport.ps1
  23. 5
      GeoNetAgent/agent/Install-GeoNetAgent.ps1
  24. 11
      GeoNetAgent/agent/README.md
  25. 92
      GeoNetAgent/agent/Send-GeoNetReport.ps1
  26. 2
      GeoNetAgent/agent/UserAgent.ps1
  27. 2
      GeoNetAgent/agent/VERSION
  28. 3
      GeoNetAgent/agent/install/GeoNetAgent-Install.ps1
  29. 3
      GeoNetAgent/agent/install/bootstrap.ps1
  30. 5
      GeoNetAgent/agent/install/manifest.json
  31. 207
      GeoNetAgent/agent/lib/Build-GeoNetReportPayload.ps1
  32. 60
      GeoNetAgent/agent/lib/Get-CpuInfo.ps1
  33. 49
      GeoNetAgent/agent/lib/Get-GpuInfo.ps1
  34. 44
      GeoNetAgent/agent/lib/Get-MemoryInfo.ps1
  35. 100
      GeoNetAgent/agent/lib/Get-PhysicalDiskInfo.ps1
  36. 93
      GeoNetAgent/agent/lib/Get-SystemInfo.ps1
  37. 2
      GeoNetAgent/collector/docs/API.md
  38. 24
      GeoNetAgent/collector/scripts/run_split.py
  39. 50
      GeoNetAgent/collector/scripts/split_asset_by_hardware.py
  40. 61
      GeoNetAgent/collector/tests/test_asset_identity.py
  41. 19
      GeoNetAgent/collector/tests/test_asset_split.py
  42. 69
      GeoNetAgent/docs/adr/ADR-001-agent-asset-identity.md
  43. 60
      GeoNetAgent/docs/adr/ADR-002-asset-split-by-hardware.md
  44. 100
      GeoNetAgent/docs/adr/ADR-003-asset-documents-qnap.md
  45. 13
      GeoNetAgent/docs/adr/README.md
  46. 60
      GeoNetAgent/docs/api/error-codes.md
  47. 109
      GeoNetAgent/docs/api/overview.md
  48. 184
      GeoNetAgent/docs/api/service-collector.md
  49. 90
      GeoNetAgent/docs/architecture/authentication-flow.md
  50. 95
      GeoNetAgent/docs/architecture/database-flow.md
  51. 124
      GeoNetAgent/docs/architecture/deployment-architecture.md
  52. 199
      GeoNetAgent/docs/architecture/folder-structure.md
  53. 77
      GeoNetAgent/docs/architecture/request-flow.md
  54. 124
      GeoNetAgent/docs/architecture/system-overview.md
  55. 143
      GeoNetAgent/docs/context-index.md
  56. 204
      GeoNetAgent/docs/database.md
  57. 72
      GeoNetAgent/docs/decisions/naming-convention.md
  58. 78
      GeoNetAgent/docs/decisions/technology-stack.md
  59. 117
      GeoNetAgent/docs/deployment.md
  60. 165
      GeoNetAgent/docs/development.md
  61. 37
      GeoNetAgent/docs/diagrams/agent-report-flow.mmd
  62. 23
      GeoNetAgent/docs/diagrams/deployment.mmd
  63. 41
      GeoNetAgent/docs/diagrams/system.mmd
  64. 69
      GeoNetAgent/docs/environment.md
  65. 35
      GeoNetAgent/docs/glossary.md
  66. 80
      GeoNetAgent/docs/issues.md
  67. 90
      GeoNetAgent/docs/monitoring.md
  68. 105
      GeoNetAgent/docs/onboarding/coding-standard.md
  69. 94
      GeoNetAgent/docs/onboarding/first-day.md
  70. 99
      GeoNetAgent/docs/onboarding/local-setup.md
  71. 93
      GeoNetAgent/docs/project-status.md
  72. 80
      GeoNetAgent/docs/roadmap.md
  73. 65
      GeoNetAgent/docs/security.md
  74. 90
      GeoNetAgent/docs/server.md
  75. 80
      GeoNetAgent/docs/todo.md
  76. 136
      GeoNetAgent/docs/troubleshooting.md
  77. 1
      GeoNetAgent/infra/collector/Dockerfile
  78. 82
      GeoNetAgent/infra/collector/publish_agent_install.ps1
  79. 19
      GeoNetAgent/infra/collector/publish_agent_install.sh
  80. 49
      GeoNetAgent/infra/collector/push_collector.ps1
  81. 47
      GeoNetAgent/infra/collector/push_collector.sh
  82. 18
      GeoNetAgent/infra/db/README.md
  83. 6
      GeoNetAgent/infra/db/migrations/003_drop_machine_id_unique.sql
  84. 108
      GeoNetAgent/infra/db/migrations/004_v_asset_hardware_latest.sql
  85. 111
      GeoNetAgent/infra/db/migrations/005_asset_documents.sql
  86. 2
      GeoNetAgent/infra/nginx/geonetagent-collector-public.conf
  87. 51
      GeoNetAgent/infra/qnap/README.md
  88. 62
      GeoNetAgent/perintah.md
  89. 404
      GeoNetAgent/samples/agent-reports/DESKTOP-RRJ9G01_20260622_132706.json
  90. 1650
      GeoNetAgent/samples/dxdiag/DxDiag.txt

14
GeoNetAgent/.archive/README.md

@ -0,0 +1,14 @@
# Archive
Folder ini berisi file yang tidak digunakan atau duplikat dari dokumentasi utama.
## File yang Diarsipkan
| File Asli | Lokasi Arsip | Alasan |
|-----------|--------------|--------|
| `collector/README.md` | `collector-README.md` | Tidak direferensikan di dokumentasi manapun |
| `infra/host.md` | `infra-host.md` | Duplikat dari `.cursor/server.md` |
## Catatan
File-file ini dipindahkan ke sini untuk menjaga struktur GeoNetAgent tetap rapi dan mudah dianalisa. Jika diperlukan kembali, file dapat dipindahkan kembali ke lokasi aslinya.

0
GeoNetAgent/collector/README.md → GeoNetAgent/.archive/collector-README.md

37
GeoNetAgent/.archive/infra-host.md

@ -0,0 +1,37 @@
Mikrotik :
ssh -m hmac-sha1 admin@10.100.1.1 -p 255
ssh -i "$env:USERPROFILE\.ssh\id_rsa_mikrotik" -o IdentitiesOnly=yes -m hmac-sha1 -p 255 admin@10.100.1.1
ssh -m hmac-sha1 admin@192.168.0.1 -p 255
ssh -i "$env:USERPROFILE\.ssh\id_rsa_mikrotik" -o IdentitiesOnly=yes -m hmac-sha1 -p 255 admin@192.168.0.1
# Reverse_proxy:
ssh -m hmac-sha1 root@10.100.1.24 -p 22
# Main_Database:
ssh -m hmac-sha1 root@10.100.1.25 -p 22
Main_Webserver:
ssh -m hmac-sha1 Administrator@10.100.1.41 -p 22
AD-LDAP:
ssh -m hmac-sha1 Administrator@10.100.1.40 -p 22
zabbix:
ssh -m hmac-sha1 root@10.100.1.42 -p 22
Ollama (aiopr, Ubuntu 22.04) — POC & inference CPU:
ssh ollama
.\scripts\ssh-ollama.ps1
ssh -i "$env:USERPROFILE\.ssh\id_rsa_geonet_laptop" -o IdentitiesOnly=yes -m hmac-sha1 root@10.100.1.26 -p 22
ssh -i "$env:USERPROFILE\.ssh\id_rsa_geonet_laptop" -o IdentitiesOnly=yes -m hmac-sha1 geonet@10.100.1.26 -p 22
# Wajib PowerShell (bukan CMD) agar $env:USERPROFILE terbaca. Jangan pakai password — login key only.
# setup key geonet (sekali): type $env:USERPROFILE\.ssh\id_rsa_geonet_laptop.pub | ssh -m hmac-sha1 geonet@10.100.1.26 -p 22 "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
# NAS QNAP (TS-932X):
ssh -i "$env:USERPROFILE\.ssh\id_ed25519" -o IdentitiesOnly=yes admin@10.100.1.10 -p 22
.\scripts\ssh-qnap.ps1
ssh -m hmac-sha1 admin@10.100.1.10 -p 22

9
GeoNetAgent/.cursor/adr/001-agent-asset-identity.md

@ -34,6 +34,14 @@ Urutan di agent (`Get-AssetIdentity.ps1`):
- `smbios_uuid` = firmware SMBIOS — **biasanya tetap** pada reinstall OS saja - `smbios_uuid` = firmware SMBIOS — **biasanya tetap** pada reinstall OS saja
- Jangan gunakan DXDiag `Machine Id` sebagai pengganti WMI — bisa berbeda (contoh Dell) - Jangan gunakan DXDiag `Machine Id` sebagai pengganti WMI — bisa berbeda (contoh Dell)
### Disk clone / pindah storage ke mesin lain
- `machine_id` **ikut terbawa** (identitas instalasi OS di disk)
- `smbios_uuid` **berubah** (motherboard baru)
- Collector **tidak** merge jika `smbios_uuid` (atau serial hardware) bertentangan, meskipun `machine_id` atau hostname sama
- Asset baru dengan hostname bentrok: `hostname` unik + suffix SMBIOS, `display_name` = hostname asli agent
- Kolom `machine_id` di DB **bukan** UNIQUE (boleh sama sementara pada dua mesin fisik hasil clone)
## Dampak ## Dampak
- Payload agent v0.1.2+ menyertakan `smbios_uuid`, `serial_number`, `asset_identity` - Payload agent v0.1.2+ menyertakan `smbios_uuid`, `serial_number`, `asset_identity`
@ -44,4 +52,5 @@ Urutan di agent (`Get-AssetIdentity.ps1`):
- `agent/lib/Get-AssetIdentity.ps1` - `agent/lib/Get-AssetIdentity.ps1`
- `api-contracts.md` §1 Agent Ingress - `api-contracts.md` §1 Agent Ingress
- `adr/002-asset-split-by-hardware.md` — koreksi snapshot campur
- `samples/agent-reports/*.json` - `samples/agent-reports/*.json`

38
GeoNetAgent/.cursor/adr/002-asset-split-by-hardware.md

@ -0,0 +1,38 @@
# ADR 002 — Split Asset by Hardware Profile
**Status:** Accepted
**Tanggal:** 2026-06-22
**Fase:** 1 (Foundation)
## Konteks
Riwayat snapshot bisa tercampur pada satu asset jika disk OS dipindah antar mesin (hostname + `machine_id` sama, `smbios_uuid` beda) sebelum perbaikan merge rule (ADR 001). Contoh: `DESKTOP-RRJ9G01` — 18 snapshot laptop Dell + 23 snapshot PC Gigabyte.
## Keputusan
1. **Pencegahan (ingest):** Collector menolak merge jika `smbios_uuid` / serial hardware bertentangan — lihat ADR 001.
2. **Koreksi data historis:** `split_asset_by_hardware()` di `collector/app/services/asset_split_service.py`
- Kelompokkan snapshot by fingerprint: `smbios:{uuid}` atau `product:{manufacturer}|{model}`
- **Primary** = profil dengan snapshot terbaru — pertahankan `hostname` asli
- Profil lain → asset baru: `hostname-{smbios8}`, `display_name` = `{hostname} · Laptop|PC`, status `inactive`
- Relink `asset_snapshots` + `software_installations`
## Operasional
```bash
# Dry-run
docker exec geonet-collector python /app/scripts/split_asset_by_hardware.py <asset-uuid> --dry-run
# Apply
docker exec geonet-collector python /app/scripts/split_asset_by_hardware.py <asset-uuid>
```
## Dampak
- UI menampilkan dua baris asset (mis. `DESKTOP-RRJ9G01 · PC` + `DESKTOP-RRJ9G01 · Laptop`)
- Snapshot lama tidak hilang; masing-masing mengikuti hardware profile-nya
## Referensi
- `collector/scripts/split_asset_by_hardware.py`
- ADR 001 — merge rule disk clone

192
GeoNetAgent/.cursor/adr/003-asset-documents-qnap.md

@ -0,0 +1,192 @@
# ADR 003 — Dokumen Asset & Tiket di QNAP Object Storage
**Status:** Accepted
**Tanggal:** 2026-06-22
**Fase:** 3 (Infra sync & CMDB) — desain disetujui; implementasi portal belum
## Konteks
IT perlu melampirkan file non-struktur ke CMDB:
| Domain | Contoh file |
|--------|-------------|
| **Procurement** | PDF invoice, PO, foto unit, garansi |
| **Service** | BA perbaikan, foto kerusakan, bukti spare part |
| **Complain** | Form keluhan, screenshot error, foto kondisi dari user |
File biner **tidak** disimpan di PostgreSQL. Infrastruktur existing: **QNAP TS-932X** (`10.100.1.10`) dengan **Object Storage** (S3-compatible API) sudah ready (`project-context.mdc`).
Identitas hardware (agent) dan dokumen (manual IT/user) harus terpisah:
- **Agent** → hardware snapshot, merge via ADR 001
- **Portal** → upload dokumen, bind ke `assets.id` atau tiket
## Keputusan
### 1. Pola penyimpanan
```
[Laravel Portal] ──upload──► QNAP S3 bucket
└── metadata ──► PostgreSQL (invoice / ticket / document_attachments)
```
| Lapisan | Peran |
|---------|--------|
| **QNAP Object Storage** | File biner (PDF, JPG, PNG, …) |
| **PostgreSQL** | Metadata, relasi, audit, workflow tiket |
| **FastAPI collector** | **Tidak** menulis file; tetap ingress agent saja |
| **Laravel portal** | Upload, download (presigned URL atau proxy), CRUD |
### 2. Tiga domain data
| Domain | Tabel utama | Lampiran via |
|--------|-------------|--------------|
| Procurement | `asset_invoices` | `document_attachments` (`invoice_id`) |
| Service | `tickets` (`ticket_type = service`) | `document_attachments` (`ticket_id`) |
| Complain | `tickets` (`ticket_type = complaint`) | `document_attachments` (`ticket_id`) |
Procurement punya tabel terstruktur (nomor invoice, harga, vendor) karena field finansial. Service & complain berbagi **satu tabel tiket** + workflow status.
### 3. Tabel `document_attachments` (bersama)
Satu tabel untuk semua file di object storage. Constraint: minimal satu FK parent (`asset_id`, `invoice_id`, atau `ticket_id`).
| Kolom penting | Keterangan |
|---------------|------------|
| `storage_provider` | `qnap_s3` (default) |
| `bucket`, `object_key` | Lokasi di object storage |
| `domain` | `procurement` \| `service` \| `complaint` |
| `kind` | Semantik file — lihat tabel di bawah |
| `original_filename`, `content_type`, `file_size_bytes` | Metadata file |
| `checksum_sha256` | Opsional — deduplikasi / integritas |
| `uploaded_by`, `uploaded_at` | Audit |
| `deleted_at` | Soft delete (file di S3 diarsip/hapus terpisah) |
**Nilai `kind` (awal):**
| kind | Domain |
|------|--------|
| `invoice_pdf` | procurement |
| `po_pdf` | procurement |
| `unit_photo` | procurement |
| `warranty_doc` | procurement |
| `service_report` | service |
| `service_photo` | service |
| `parts_receipt` | service |
| `complaint_form` | complaint |
| `complaint_photo` | complaint |
| `complaint_screenshot` | complaint |
| `other` | semua |
Lampiran bisa juga mengacu langsung ke `asset_id` tanpa invoice/tiket (mis. foto unit umum).
### 4. Struktur object key (QNAP)
Bucket: `geonetagent-documents` (buat sekali di QNAP).
```
{domain}/{yyyy}/{mm}/{attachment_id}/{sanitized_original_filename}
```
Contoh:
```
procurement/2026/06/8f3a2c1e-....-uuid/INV-2024-00123.pdf
service/2026/06/9a1b....../ba-service-signed.pdf
complaint/2026/06/7c2d....../screenshot-error.png
```
`attachment_id` = UUID baris `document_attachments` — hindari bentrok nama file.
### 5. Pengikat ke Laptop/PC (tidak berubah saat reinstall / upgrade komponen)
| Pengikat dokumen | Stabil reinstall? | Stabil ganti CPU/GPU/RAM/disk? |
|------------------|-------------------|--------------------------------|
| **`assets.id`** (UUID CMDB) | ✅ | ✅ — **anchor utama invoice & tiket** |
| `asset_invoices.asset_id` | ✅ | ✅ |
| `tickets.asset_id` | ✅ | ✅ |
| `serial_number` / `smbios_uuid` | Verifikasi agent (ADR 001) | Berubah jika motherboard diganti |
Dokumen **tidak** dikait ke `cpu_model`, `gpu`, atau `physical_disks` — itu inventori snapshot, bukan identitas asset.
### 6. Akses & keamanan
| Aturan | Detail |
|--------|--------|
| Endpoint S3 | LAN internal (`10.100.1.10`); tidak expose publik |
| Kredensial | IAM/bucket user khusus `geonetagent-portal` — least privilege |
| Download | Presigned URL TTL pendek (15–60 menit) atau stream via Laravel |
| Complain | User hanya lihat tiket/file milik sendiri; IT lihat semua |
| Ukuran upload | Batas aplikasi: PDF ≤ 10 MB, gambar ≤ 5 MB (konfigurasi portal) |
| Retensi | Kebijakan IT (mis. complain 2 th, service 5 th) — fase implementasi |
### 7. Batas MVP implementasi
- **Fase 3** — migration DB + Laravel upload/download
- Collector UI (`/ui/`) tetap **read-only**; tidak upload file
- Agent PowerShell **tidak** mengunggah dokumen
- `portal_users` (auth IT) prerequisite untuk upload
## Alternatif yang ditolak
| Alternatif | Alasan ditolak |
|------------|----------------|
| File di kolom `bytea` PostgreSQL | DB membengkak, backup lambat |
| Satu folder SMB share tanpa metadata | Sulit audit, link ke asset, permission |
| Tabel lampiran terpisah per domain | Duplikasi schema; satu `document_attachments` cukup |
| Tiket terpisah `service_tickets` + `complaints` | Workflow sama; cukup `ticket_type` |
## Dampak
- Migration: `infra/db/migrations/005_asset_documents.sql` (draft — jalankan saat portal scaffold)
- Schema: `.cursor/db-schema.md` — tabel `asset_invoices`, `tickets`, `document_attachments`
- API kontrak: endpoint upload Laravel (bukan FastAPI agent) — `api-contracts.md` § portal (TODO)
- Infra: bucket QNAP + env `S3_ENDPOINT`, `S3_BUCKET`, `S3_ACCESS_KEY` di portal `.env`
## Diagram
```mermaid
flowchart TB
subgraph portal [Laravel Portal]
UP[Upload handler]
UI[Asset / Ticket UI]
end
subgraph storage [QNAP 10.100.1.10]
S3[(Object Storage bucket)]
end
subgraph db [PostgreSQL main-db]
AST[assets]
INV[asset_invoices]
TIX[tickets]
ATT[document_attachments]
end
subgraph agent [Agent - unchanged]
AG[POST report]
end
UP --> S3
UP --> ATT
INV --> AST
TIX --> AST
ATT --> INV
ATT --> TIX
ATT --> AST
AG --> AST
UI --> ATT
```
## Referensi
- ADR 001 — identitas hardware agent
- `.cursor/db-schema.md` — definisi tabel
- `infra/db/migrations/005_asset_documents.sql`
- `infra/host.md` — SSH QNAP
- View hardware: `v_asset_hardware_latest` (migration 004)
## Checklist implementasi (nanti)
- [ ] Buat bucket `geonetagent-documents` + user S3 di QNAP
- [ ] Jalankan migration 005 di `geonetagent_dev`
- [ ] Laravel: model + storage driver S3-compatible
- [ ] UI: tab Dokumen di detail asset; form tiket service/complain
- [ ] Presigned download + audit log

13
GeoNetAgent/.cursor/agent-playbook.md

@ -90,6 +90,19 @@
| Keputusan monitoring | `monitoring-architecture.md` | | Keputusan monitoring | `monitoring-architecture.md` |
| Infra/stack user berubah | `me.md` | | Infra/stack user berubah | `me.md` |
| Keputusan arsitektur major | `blueprint.md` + `monitoring-architecture.md` | | Keputusan arsitektur major | `blueprint.md` + `monitoring-architecture.md` |
| **ADR baru** (identitas, split, dokumen QNAP) | `.cursor/adr/*.md` + `db-schema.md` + migration `infra/db/migrations/` |
| **Bump versi agent** | `progress.md` (Riwayat milestone), `perintah.md`, `project-context.mdc`, `agent/README.md`; deploy via `publish_agent_install.ps1` + `push_collector.ps1` — lihat `agent-release-deploy.mdc` |
---
## Release agent (wajib saat bump `agent/VERSION`)
1. Publish paket install → `.\infra\collector\publish_agent_install.ps1`
2. Rebuild collector/UI → `.\infra\collector\push_collector.ps1` (jika payload, schema, atau `collector/static/` berubah)
3. Tambah baris di **Riwayat milestone** (`progress.md`) — contoh: `Agent v0.1.11 — printer, deploy server`
4. Sync `perintah.md` + versi di `project-context.mdc`
Jangan nyatakan task agent selesai hanya karena kode di repo sudah commit; endpoint user harus sudah menyajikan versi baru.
--- ---

112
GeoNetAgent/.cursor/api-contracts.md

@ -54,6 +54,19 @@ Content-Type: application/json
"gpu_model": "NVIDIA GeForce GTX 850M", "gpu_model": "NVIDIA GeForce GTX 850M",
"last_boot_at": "2026-06-15T08:42:51+07:00" "last_boot_at": "2026-06-15T08:42:51+07:00"
}, },
"cpu": {
"model": "Intel(R) Core(TM) i7-4710HQ CPU @ 2.50GHz",
"manufacturer": "GenuineIntel",
"cores": 4,
"logical_processors": 8,
"max_clock_mhz": 2500,
"current_clock_mhz": 2494,
"socket": "SOCKET 0",
"socket_count": 1,
"l2_cache_kb": 1024,
"l3_cache_kb": 6144,
"architecture": "x64"
},
"gpus": [ "gpus": [
{ {
"name": "NVIDIA GeForce GTX 850M", "name": "NVIDIA GeForce GTX 850M",
@ -200,17 +213,67 @@ Content-Type: application/json
| `connection` | `PNPDeviceID` / `PNPClass` | `usb`, `usb_hid`, `bluetooth`, `bluetooth_radio`, `audio_jack`, `bluetooth_audio`, `display` | | `connection` | `PNPDeviceID` / `PNPClass` | `usb`, `usb_hid`, `bluetooth`, `bluetooth_radio`, `audio_jack`, `bluetooth_audio`, `display` |
| `device_role` | — | `adapter`, `radio`, `playback`, `capture`, `input`, `peripheral` | | `device_role` | — | `adapter`, `radio`, `playback`, `capture`, `input`, `peripheral` |
| `vid` / `pid` | Parse dari `PNPDeviceID` | Vendor/Product ID hex, jika ada | | `vid` / `pid` | Parse dari `PNPDeviceID` | Vendor/Product ID hex, jika ada |
| `is_external` | — | `true` untuk USB/BT headset; `null` untuk jack audio (status colok tidak diketahui WMI) | | `is_external` | — | `true` USB/BT/HID/monitor eksternal; `false` panel laptop/internal; `null` jack audio & beberapa radio BT |
Scope agent (`Get-PeripheralsInfo.ps1`):
- **USB/HID**`USB\`, `HID\` (kecuali hub infrastruktur); `is_external: true`
- **Bluetooth dongle/headset**`BTH*\`, `BTHENUM\`, `SWD\RADIO\BLUETOOTH*`; headset `is_external: true`
- **Headphone jack 3.5mm**`AudioEndpoint`; `is_external: null` (status colok tidak diketahui WMI)
- **Monitor** — EDID via `WmiMonitorID`; built-in jika nama *Integrated Monitor* / *Laptop Display*`is_external: false`
**Belum dalam scope:** printer; klasifikasi keyboard vs mouse; flag upgrade RAM/disk/GPU aftermarket.
> Contoh lengkap: `samples/agent-reports/*.json` · Implementasi: `agent/lib/Build-GeoNetReportPayload.ps1` v0.1.10-phase0
#### Payload extensions (v0.1.8+)
**`system`** — tambahan v0.1.9:
Scope agent: | Field | Keterangan |
- **USB/HID**`USB\`, `HID\` (kecuali hub infrastruktur) |-------|------------|
- **Bluetooth dongle/headset**`USB\`+class Bluetooth, `BTH*\`, `BTHENUM\`, `SWD\RADIO\BLUETOOTH*` | `form_factor` | `laptop` \| `desktop` \| `server` \| `other` (SMBIOS + PCSystemType) |
- **Headphone jack 3.5mm**`AudioEndpoint` playback (`SWD\MMDEVAPI\{0.0.0.*`) nama mengandung Speaker/Headphone | `chassis_label` | e.g. Notebook, Desktop |
- **Headphone Bluetooth**`AudioEndpoint`/`MEDIA` dengan `BTHENUM\` atau nama mengandung Bluetooth (hanya saat terhubung) | `pc_system_label` | e.g. Mobile, Desktop |
- **Monitor eksternal**`Win32_PnPEntity` class Monitor + `WmiMonitorID` (EDID); `resolution`, `refresh_hz` dari `EnumDisplaySettings` per layar aktif **`cpu`** — v0.1.10 (detail prosesor; `system.cpu_model` tetap untuk kompatibilitas):
> Contoh lengkap: `samples/agent-reports/*.json` · Implementasi: `agent/Collect-GeoNetReport.ps1` v0.1.6-phase0 | Field | Keterangan |
|-------|------------|
| `model` | Nama CPU (Win32_Processor.Name) |
| `manufacturer` | e.g. GenuineIntel, AuthenticAMD |
| `cores` | Total physical cores (jumlah socket) |
| `logical_processors` | Total threads |
| `max_clock_mhz` | MaxClockSpeed |
| `current_clock_mhz` | CurrentClockSpeed |
| `socket` | SocketDesignation |
| `socket_count` | Jumlah prosesor terdeteksi |
| `l2_cache_kb` / `l3_cache_kb` | Cache size (KB) |
| `architecture` | `x64`, `ARM64`, dll. |
**`gpus[]`** — tambahan v0.1.9: `adapter_compatibility`, `video_processor`, `driver_version`, `driver_date`, `current_resolution`, `status`, `pnp_device_id`
**`memory_modules[]`** — tambahan v0.1.9: `memory_type_label` (DDR3/DDR4/DDR5), `configured_speed_mhz`, `serial_number`, `rank`
**`memory_summary`** — tambahan: `ddr_generation`
**`physical_disks[]`** — v0.1.9 (disk fisik, bukan volume):
```json
{
"disk_number": 0,
"model": "ADATA SU650",
"friendly_name": "ADATA SU650",
"media_type": "SSD",
"operational_status": "OK",
"health_status": "Healthy",
"total_gb": 447.1,
"partition_style": "MBR",
"bus_type": "SATA",
"serial_number": "2N29292H8BN9"
}
```
Sumber agent: `Get-PhysicalDisk` + `Get-Disk` (modul Storage); fallback WMI jika gagal.
**Response 201 Created:** **Response 201 Created:**
```json ```json
@ -228,11 +291,16 @@ Scope agent:
**Server behavior:** **Server behavior:**
1. Validate token → `agent_tokens` 1. Validate token → `agent_tokens`
2. **Upsert `assets`** — urutan match (lihat [adr/001-agent-asset-identity.md](./adr/001-agent-asset-identity.md)): 2. **Upsert `assets`** — urutan match (lihat [adr/001-agent-asset-identity.md](./adr/001-agent-asset-identity.md)):
`serial_number``smbios_uuid``primary_physical_mac` (dari `asset_identity`) → `machine_id``hostname` `serial_number``smbios_uuid``primary_physical_mac``machine_id``hostname`
3. Insert `asset_snapshots` + child disks/network/memory - Match `machine_id` / `hostname` **ditolak** jika `smbios_uuid` atau serial hardware bertentangan (disk clone)
4. Upsert `users` from `logged_in_user``asset_user_assignments` - Hostname bentrok → asset baru `hostname-{smbios8}`, `display_name` = hostname asli
5. Replace `software_installations` for this snapshot (MVP: insert linked to snapshot) 3. Insert `asset_snapshots` (payload JSON lengkap termasuk `physical_disks`, `form_factor`)
6. Update `assets.last_seen_at`, `primary_physical_mac`, `smbios_uuid` jika ada di payload 4. Insert child rows: `asset_disks` (volume), `asset_network_adapters`
5. Upsert `users` from `logged_in_user``asset_user_assignments`
6. Insert `software_installations` linked to snapshot
7. Update `assets.last_seen_at`, identity fields, `metadata.form_factor`
**Koreksi data:** split snapshot by hardware — [adr/002-asset-split-by-hardware.md](./adr/002-asset-split-by-hardware.md)
--- ---
@ -310,7 +378,23 @@ Laravel primarily serves **Blade/Livewire/Inertia UI**. REST API optional fase 2
| GET | `/alerts` | AlertController@index | from `alerts` table | | GET | `/alerts` | AlertController@index | from `alerts` table |
| GET | `/licenses` | LicenseController@index | CRUD fase 2 | | GET | `/licenses` | LicenseController@index | CRUD fase 2 |
### `GET /assets` query params ### Asset documents & tickets (Fase 3 — ADR 003)
> File di QNAP Object Storage; metadata di PostgreSQL. Implementasi Laravel, bukan FastAPI agent.
| Method | Route | Keterangan |
|---|---|---|
| GET | `/assets/{id}/invoices` | Daftar invoice procurement |
| POST | `/assets/{id}/invoices` | Buat invoice + upload PDF (multipart) |
| GET | `/tickets` | List tiket service/complaint (filter `type`, `status`) |
| POST | `/tickets` | Buat tiket |
| GET | `/tickets/{id}` | Detail + lampiran |
| POST | `/tickets/{id}/attachments` | Upload lampiran ke QNAP |
| GET | `/attachments/{id}/download` | Presigned URL atau stream |
**Tabel DB:** `asset_invoices`, `tickets`, `document_attachments` — lihat `db-schema.md`, migration `005_asset_documents.sql`.
---
| Param | Type | Example | | Param | Type | Example |
|---|---|---| |---|---|---|

3
GeoNetAgent/.cursor/blueprint.md

@ -78,6 +78,7 @@
| Hardware snapshot history | PostgreSQL `asset_snapshots` | FastAPI only | Laravel UI | | Hardware snapshot history | PostgreSQL `asset_snapshots` | FastAPI only | Laravel UI |
| Software inventory | PostgreSQL `software_installations` | FastAPI (scan) | Laravel UI | | Software inventory | PostgreSQL `software_installations` | FastAPI (scan) | Laravel UI |
| License entitlements | PostgreSQL `licenses` | Laravel manual/import | Laravel UI | | License entitlements | PostgreSQL `licenses` | Laravel manual/import | Laravel UI |
| Asset documents (invoice, service, complain) | PostgreSQL metadata + QNAP S3 | Laravel upload | Laravel UI (presigned download) |
| VM inventory | PostgreSQL `assets` type=vm | FastAPI Proxmox sync | Laravel UI | | VM inventory | PostgreSQL `assets` type=vm | FastAPI Proxmox sync | Laravel UI |
| User ↔ device mapping | PostgreSQL | FastAPI (agent user) + AD sync (later) | Laravel UI | | User ↔ device mapping | PostgreSQL | FastAPI (agent user) + AD sync (later) | Laravel UI |
| GIS server registry | PostgreSQL | Laravel manual | Laravel UI | | GIS server registry | PostgreSQL | Laravel manual | Laravel UI |
@ -258,7 +259,7 @@ agent_tokens (auth)
- [x] PostgreSQL schema migrated (`geonetagent_dev`) - [x] PostgreSQL schema migrated (`geonetagent_dev`)
- [x] FastAPI `/agent/report` + health + read API + UI `/ui/` - [x] FastAPI `/agent/report` + health + read API + UI `/ui/`
- [x] PowerShell agent v0.1.6 + POST ke collector (publik `agent.gisportal.id`) - [x] PowerShell agent v0.1.10 + POST ke collector (publik `agent.gisportal.id`)
- [x] Reverse proxy nginx + web install `agent.gisportal.id/install/` - [x] Reverse proxy nginx + web install `agent.gisportal.id/install/`
- [ ] Laravel asset index (read-only) - [ ] Laravel asset index (read-only)

495
GeoNetAgent/.cursor/context-index.md

@ -0,0 +1,495 @@
# Context Index (AI Entry Point)
> **Versi Dokumen:** 1.0.0
> **Terakhir Diperbarui:** 2026-06-23
> **Status:** Aktif
> **Pemilik:** Tim Pengembang GeoNetAgent
---
# Tujuan
Dokumen ini merupakan **titik masuk utama (Single Entry Point)** bagi seluruh AI Agent dan Developer GeoNetAgent.
Setiap AI **WAJIB** membaca dokumen ini terlebih dahulu sebelum melakukan analisis, implementasi, perubahan kode, refactoring, penghapusan, deployment, maupun pembuatan dokumentasi.
Dokumen ini **bukan** tempat menyimpan seluruh informasi proyek, melainkan **peta navigasi** menuju seluruh dokumentasi yang tersedia.
---
# Filosofi Proyek
Proyek ini menerapkan prinsip **AI Native Development**.
Artinya:
* Dokumentasi adalah bagian dari source code.
* Setiap perubahan wajib terdokumentasi.
* Repository merupakan sumber kebenaran utama (Single Source of Truth).
* AI tidak boleh mengandalkan riwayat percakapan.
* Seluruh pengetahuan proyek harus tersimpan dalam repository.
* Developer sering berpindah perangkat (PC, Laptop, Workstation) — AI Agent harus tetap bisa melanjutkan tanpa riwayat chat.
---
# Aturan Utama AI
Sebelum melakukan pekerjaan apa pun, AI wajib:
1. Membaca file ini (`context-index.md`).
2. Membaca dokumentasi yang berkaitan dengan tugas.
3. Memahami arsitektur yang ada.
4. Memastikan tidak ada konflik dengan keputusan arsitektur sebelumnya (cek ADR).
5. Memperbarui dokumentasi setelah implementasi selesai.
6. Mengikuti protokol sync di `perintah.md` sebelum menyatakan pekerjaan selesai.
Implementasi tanpa dokumentasi dianggap **belum selesai**.
---
# Urutan Membaca Dokumentasi
AI wajib membaca dokumen sesuai urutan berikut:
1. `context-index.md` (file ini)
2. `blueprint.md` — Master blueprint, visi, arsitektur, modul
3. `progress.md` — Fase aktif & snapshot progress
4. Dokumen spesifik modul (lihat tabel Struktur Dokumentasi di bawah)
5. `agent-playbook.md` — Workflow multi-tab & koordinasi
6. `perintah.md` — Protokol sync dokumentasi (wajib akhir sesi)
Jangan membaca seluruh repository apabila tidak diperlukan.
---
# Struktur Dokumentasi
## 1. Blueprint
**Lokasi:** `.cursor/blueprint.md`
**Berisi:**
* Visi produk & masalah yang diselesaikan
* Non-goals (sengaja TIDAK dibangun)
* Boundary System — ownership matrix (Zabbix, FastAPI, Laravel)
* Alur data utama (agent, Zabbix, Proxmox, user dashboard)
* Modul aplikasi & prioritas (M1-M9)
* Keputusan teknis locked (PostgreSQL, monorepo, API versioning)
* Entity domain ringkas
* Integrasi eksternal (Zabbix, Proxmox, MikroTik, QNAP, AD)
* Security blueprint
* Deployment target
* Roadmap & Definition of Done per fase
* Glossarium
**Baca saat:** Selalu — cek visi & batas modul sebelum coding.
---
## 2. Progress
**Lokasi:** `.cursor/progress.md`
**Berisi:**
* Snapshot status (fase aktif, agent version, collector status)
* Progress ringkas per fase (0-4)
* Checklist detail per fase dengan DoD (Definition of Done)
* Agent feature log (versi & fitur per versi)
* Backlog agent (fitur yang belum diimplementasi)
* Riwayat milestone (tanggal & milestone penting)
**Baca saat:** Selalu — cek fase aktif sebelum coding. Jangan implement fase 3 saat fase 1 belum DoD.
---
## 3. Agent Playbook
**Lokasi:** `.cursor/agent-playbook.md`
**Berisi:**
* Prinsip koordinasi multi-tab Cursor Agent
* Workflow multi-tab (DB, API, UI, Infra, Zabbix)
* Assignment tab → scope & folder
* Checklist sebelum & sesudah coding
* Dokumen — kapan diupdate (mapping event → file)
* Release agent protocol (publish + deploy + milestone log)
* Handling deviasi blueprint
* Conflict prevention
* Prompt template untuk user
* Status tracking
* Protokol sync dokumentasi
**Baca saat:** Saat paralel development atau koordinasi antar tab.
---
## 4. Database Schema
**Lokasi:** `.cursor/db-schema.md`
**Berisi:**
* Konvensi (PK UUID, timestamptz, snake_case)
* ERD (Mermaid)
* Tabel detail (`locations`, `assets`, `asset_snapshots`, `asset_disks`, `users`, `software_installations`, `licenses`, `asset_invoices`, `tickets`, `document_attachments`, `alerts`, `agent_tokens`, `sync_logs`)
* View `v_asset_hardware_latest`
* Laravel auth tables (`portal_users`)
* Sample queries
* Migration order
**Baca saat:** Saat kerja database/migration, schema change, atau query data.
---
## 5. API Contracts
**Lokasi:** `.cursor/api-contracts.md`
**Berisi:**
* Overview service & base URL
* FastAPI — Agent Ingress (`POST /api/v1/agent/report`)
* FastAPI — Read API CMDB (GET assets, snapshots, software)
* FastAPI — Proxmox sync (`POST /api/v1/sync/proxmox`)
* Laravel — Portal API (pages, asset documents, tickets)
* Laravel — Zabbix Webhook (`POST /webhooks/zabbix`)
* Laravel → Zabbix API (read-only host status)
* Proxmox API (FastAPI sync job)
* PowerShell Agent — Local Contract
* Error format standar
* Versioning policy
**Baca saat:** Saat kerja backend/API, integrasi agent, atau webhook.
---
## 6. Repository Structure
**Lokasi:** `.cursor/repo-structure.md`
**Berisi:**
* Root layout (`.cursor/`, `agent/`, `collector/`, `portal/`, `infra/`, `samples/`)
* Keputusan migration DB (Alembic vs Laravel)
* Naming conventions (Python, PHP, PowerShell)
* Environment variables
* Docker Compose dev
* Git branch strategy
* File ownership per agent tab
* README.md root target content
**Baca saat:** Saat scaffold/buat file baru atau struktur folder.
---
## 7. Profil & Infrastruktur
**Lokasi:** `.cursor/me.md`
**Berisi:**
* Profil user (IT end-to-end: network, server, storage, GIS, licensing)
* Stack & aset yang sudah dimiliki (Laravel, FastAPI, Zabbix, AD, Proxmox, QNAP, MikroTik)
* Lingkungan development (workspace, OS dev, contoh endpoint)
* Ruang lingkup tanggung jawab
* Preferensi kerja (Bahasa Indonesia, jangan duplikasi Zabbix, PostgreSQL untuk baru)
* File konteks terkait
* Status proyek
* URL operasional
**Ringkasan Profil User:**
User adalah **IT Administrator** yang mengelola infrastruktur end-to-end:
- **Network & Security:** MikroTik CCR2004 (gateway, routing, firewall)
- **Compute & Virtualization:** Proxmox (VM host), PostgreSQL/SQL Server (database), Docker (containers)
- **Identity:** Windows Server 2012 R2 Active Directory (domain user management, GPO deploy)
- **Storage:** QNAP TS-932X (NAS, Object Storage untuk dokumen)
- **Monitoring:** Zabbix (alerting real-time)
- **Endpoint:** Banyak laptop user (Windows, domain-joined)
- **GIS & Licensing:** Software licensing tracking
**Preferensi Kerja:**
- Bahasa komunikasi: **Bahasa Indonesia**
- Jangan duplikasi fungsi Zabbix — Zabbix tetap pusat alerting real-time
- PostgreSQL untuk aplikasi/CMDB baru; SQL Server untuk legacy/GIS
- Minimalkan scope perubahan; ikuti konvensi existing code
- Jangan commit kecuali diminta eksplisit
- Sering berpindah perangkat (PC, Laptop, Workstation) — gunakan `.cursor/` sebagai shared memory
**Baca saat:** Selalu — konteks lingkungan & preferensi user.
---
## 8. Server & SSH
**Lokasi:** `.cursor/server.md`
**Berisi:**
* SSH commands ke seluruh server (MikroTik, reverse proxy, main database, webserver, AD-LDAP, Zabbix, Ollama, QNAP)
* Port & credential hints
* Service per server
**Baca saat:** Saat deploy / akses infra.
---
## 9. Architecture Decision Records (ADR)
**Lokasi:** `.cursor/adr/`
**Berisi:**
* `001-agent-asset-identity.md` — Strategi identitas asset (serial → smbios_uuid → MAC → machine_id → hostname)
* `002-asset-split-by-hardware.md` — Split asset dengan hardware campur (laptop + PC dalam 1 chassis)
* `003-asset-documents-qnap.md` — Dokumen procurement/service/complain di QNAP Object Storage
**Baca saat:** Saat keputusan desain baru atau implementasi fitur yang terdampak ADR.
**Catatan:** ADR tidak boleh dihapus. Jika berubah, buat ADR baru.
---
## 10. Perintah (Sync Protocol)
**Lokasi:** `perintah.md` (root repo)
**Berisi:**
* Indeks cepat operasional (progress, SSH, API, QNAP, install agent, nginx, deploy)
* URL produksi (domain `gisportal.id`)
* Install agent (PowerShell commands)
* Admin — deploy & DB commands
* Agent versi saat ini
* Pertanyaan sync (checklist AI wajib akhir sesi)
* Sync terakhir (changelog singkat)
**Baca saat:** Akhir sesi / sebelum nyatakan done (wajib).
---
## 11. Rules
**Lokasi:** `.cursor/rules/`
**Berisi:**
* `project-context.mdc` — Rule Cursor: muat konteks proyek otomatis
* Rules tambahan (coding standard, security, dll) — akan ditambah sesuai kebutuhan
**Baca saat:** Saat perlu rules spesifik atau konvensi coding.
---
## 12. Monitoring Architecture
**Lokasi:** `.cursor/monitoring-architecture.md`
**Berisi:**
* Layer Zabbix/FastAPI/Laravel
* DXDiag integration
* Roadmap fase monitoring
**Baca saat:** Saat kerja monitoring/integrasi Zabbix.
---
## 13. Collector API Documentation
**Lokasi:** `collector/docs/API.md`
**Berisi:**
* Panduan konsumsi Collector API
* Contoh curl/PowerShell/Python
* Detail endpoint & response
**Baca saat:** Integrasi agent / aplikasi lain ke collector.
---
# Informasi Infrastruktur
Seluruh informasi server berada di `.cursor/server.md`.
## Audit Server (OS & Aplikasi)
| Server | IP | OS / Versi | Digunakan di GeoNetAgent? | Fungsi |
|--------|-----|------------|---------------------------|--------|
| MikroTik CCR2004 | 10.100.1.1 / 192.168.0.1 | RouterOS | ☐ | Gateway, routing, firewall |
| Reverse Proxy (nginx) | 10.100.1.24 | CentOS Linux 8 | ☑ YES | GeoNetAgent collector, agent.gisportal.id, collector.gisportal.id |
| Main Database | 10.100.1.25 | Ubuntu 18.04.6 LTS | ☑ YES | PostgreSQL 12 — `geonetagent_dev` |
| Main Webserver | 10.100.1.41 | Windows Server 2012 R2 (6.3.9600) | ☐ | Web server |
| AD-LDAP | 10.100.1.40 | Windows Server 2012 R2 (6.3.9600) | ☐ | Active Directory (GPO deploy agent - fase 2) |
| Zabbix | 10.100.1.42 | Ubuntu 22.04.1 LTS | ☐ | Monitoring Server (integrasi webhook - fase 2) |
| Ollama | 10.100.1.26 | Ubuntu 22.04.1 LTS | ☐ | AI inference (opsional - fase 4) |
| QNAP TS-932X | 10.100.1.10 | QTS 5.2.9 (ARM64) | ☑ YES | NAS / Object Storage (dokumen ADR 003) |
**Catatan:**
- ☑ = Sudah dikonfirmasi digunakan di GeoNetAgent
- ☐ = Belum dikonfirmasi atau belum digunakan di fase saat ini
- User dapat mengisi checkbox ini sesuai kebutuhan
AI tidak boleh mengubah konfigurasi server tanpa memperbarui dokumentasi.
---
# Informasi API
Semua API wajib memiliki dokumentasi di `.cursor/api-contracts.md`.
**FastAPI Collector:**
* Base URL dev: `http://localhost:8000`
* Base URL prod: `https://collector.gisportal.id`
* Auth: Bearer token
* Endpoint: `POST /api/v1/agent/report`, GET `/api/v1/assets`, dll
**Laravel Portal:**
* Base URL dev: `http://localhost:8080`
* Base URL prod: `https://portal.geonet.internal`
* Auth: Session / Sanctum
---
# Informasi Database
Seluruh perubahan database wajib memperbarui `.cursor/db-schema.md`.
**Database:** `geonetagent` (prod) · `geonetagent_dev` (dev/testing)
**Engine:** PostgreSQL 12+
**Ownership:** FastAPI = write ingress otomatis | Laravel = read + manual CRUD terbatas
**Migration tool:** Alembic (core schema) + Laravel migrations (portal-specific)
Tidak boleh ada perubahan database tanpa dokumentasi.
---
# Checklist Sebelum Implementasi
AI wajib memastikan:
☐ Sudah membaca `context-index.md`
☐ Sudah membaca `blueprint.md` — cek fase & DoD
☐ Sudah membaca `progress.md` — cek fase aktif
☐ Sudah membaca dokumentasi spesifik modul (db-schema, api-contracts, dll)
☐ Sudah memahami ADR yang relevan
☐ Sudah memahami infrastruktur di `me.md` & `server.md`
☐ Sudah memastikan dependency tab lain sudah ada di disk (jika paralel)
---
# Checklist Setelah Implementasi
AI wajib memperbarui:
`progress.md` — centang item selesai, update snapshot, tambahkan milestone jika perlu
☐ ADR baru (jika ada perubahan arsitektur/desain)
☐ Dokumentasi spesifik (db-schema.md, api-contracts.md, repo-structure.md, dll)
`blueprint.md` — jika modul/fase baru
`agent-playbook.md` — jika workflow berubah
`perintah.md` — sync terakhir (wajib akhir sesi)
`me.md` — jika infrastruktur berubah
`context-index.md` — jika ada dokumen baru
☐ Deploy agent (jika bump versi): `publish_agent_install.ps1` + `push_collector.ps1`
---
# Prinsip Pengembangan
Setiap perubahan harus memenuhi prinsip berikut:
* Mudah dipahami developer baru.
* Mudah dipahami AI Agent lain (tanpa riwayat chat).
* Mudah diaudit.
* Mudah di-maintain.
* Memiliki rollback plan.
* Memiliki dokumentasi lengkap.
* Tidak menghasilkan technical debt yang tidak terdokumentasi.
* Mengikuti fase di `progress.md` — jangan loncat fase.
* Repository adalah sumber kebenaran utama.
---
# Quick Reference
## URL Produksi
| Fungsi | URL |
|--------|-----|
| Agent POST (internet) | `https://agent.gisportal.id/api/v1/agent/report` |
| Install / update agent | `https://agent.gisportal.id/install/` |
| UI + read API HTTPS | `https://collector.gisportal.id/ui/` |
| UI + read API HTTP LAN | `http://10.100.1.24:8000/ui/` |
## Agent Version
**Current:** `v0.1.10-phase0``cpu{}` detail, `physical_disks[]`, RAM/GPU detail, `system.form_factor`, auto-update via manifest.
## Fase Aktif
**Fase 1 — Foundation** 🔄
DoD: Agent di 1 laptop test POST → data di DB → Laravel tampilkan halaman asset list.
Progress: 8 / 9 item selesai. Blocker: Laravel asset index belum scaffold.
## Deploy Commands
```powershell
# Publish agent
.\infra\collector\publish_agent_install.ps1
# Deploy collector
.\infra\collector\push_collector.ps1
# Deploy nginx
infra/collector/deploy_nginx.sh
```
---
# Penutup
Repository ini dirancang agar:
* Developer baru dapat memahami proyek dengan cepat.
* AI Agent mana pun dapat melanjutkan pekerjaan tanpa kehilangan konteks.
* Seluruh pengetahuan proyek tersimpan di dalam repository.
* Dokumentasi selalu selaras dengan implementasi.
Repository adalah sumber kebenaran utama.
Jika terjadi perbedaan antara implementasi dan dokumentasi, AI wajib memperbarui dokumentasi hingga keduanya konsisten.
---
# Bahasa & Gaya
* Komunikasi dengan user: **Bahasa Indonesia**
* Kode & komentar: **English** (konvensi industri)
* Dokumentasi `.cursor/`: **Bahasa Indonesia** (kecuali istilah teknis)

112
GeoNetAgent/.cursor/db-schema.md

@ -197,6 +197,44 @@ Normalized dari snapshot — memudahkan query "disk > 85%".
--- ---
### View `v_asset_hardware_latest`
Satu baris per asset aktif (`deleted_at IS NULL`), hardware dari **snapshot terbaru** (`collected_at` DESC).
Migration: `infra/db/migrations/004_v_asset_hardware_latest.sql`
| Kolom | Sumber | Keterangan |
|---|---|---|
| `asset_id`, `hostname`, identitas | `assets` | Master asset |
| `snapshot_id`, `snapshot_collected_at`, `agent_version` | `asset_snapshots` | Snapshot dipakai |
| `form_factor`, `chassis_label` | `payload.system` atau `assets.metadata` | `desktop` / `laptop` / … |
| `cpu_*` | `payload.cpu` (+ fallback `cpu_model`) | Model, cores, clock, cache, arch |
| `ram_gb`, `ram_*`, `memory_modules` | kolom + `payload.memory_*` | Total, DDR, layout, modul per slot |
| `gpu_primary`, `gpu_count`, `gpus` | kolom + `payload.gpus` | GPU utama + array lengkap |
| `display_count`, `display_external_count`, `display_internal_count` | agregat `payload.peripherals` | Monitor terdeteksi |
| `displays` | `jsonb` | Array monitor (`connection: display`, `is_external`) |
Contoh query:
```sql
-- Semua laptop + RAM
SELECT hostname, form_factor, ram_gb, ram_layout, cpu_model
FROM v_asset_hardware_latest
WHERE form_factor = 'laptop';
-- PC dengan monitor eksternal
SELECT hostname, display_external_count, displays
FROM v_asset_hardware_latest
WHERE form_factor = 'desktop' AND display_external_count > 0;
-- Expand GPU
SELECT v.hostname, g->>'name' AS gpu_name, (g->>'dedicated_memory_mb')::int AS vram_mb
FROM v_asset_hardware_latest v
CROSS JOIN LATERAL jsonb_array_elements(v.gpus) AS g;
```
---
### `asset_network_adapters` ### `asset_network_adapters`
| Kolom | Tipe | Nullable | Keterangan | | Kolom | Tipe | Nullable | Keterangan |
@ -299,6 +337,79 @@ Constraint: minimal salah satu `asset_id` atau `user_id` NOT NULL.
--- ---
### `asset_invoices` (Fase 3 — ADR 003)
Metadata pembelian laptop/PC. File PDF/foto di **QNAP Object Storage** via `document_attachments`.
| Kolom | Tipe | Nullable | Keterangan |
|---|---|---|---|
| id | uuid | NO | PK |
| asset_id | uuid | NO | FK → assets — **anchor dokumen** |
| invoice_number | varchar(100) | NO | UNIQUE |
| invoice_date | date | YES | |
| vendor | varchar(200) | YES | |
| purchase_price | numeric(14,2) | YES | |
| currency | varchar(3) | NO | default `IDR` |
| po_number | varchar(100) | YES | |
| warranty_until | date | YES | |
| serial_at_purchase | varchar(100) | YES | verifikasi vs agent |
| notes | text | YES | |
| created_by | varchar(100) | YES | portal user |
| deleted_at | timestamptz | YES | soft delete |
---
### `tickets` (Fase 3 — ADR 003)
Tiket **service** dan **complaint** berbagi satu tabel.
| Kolom | Tipe | Nullable | Keterangan |
|---|---|---|---|
| id | uuid | NO | PK |
| ticket_number | varchar(30) | NO | UNIQUE, e.g. `SRV-2026-00042` |
| ticket_type | varchar(20) | NO | `service` \| `complaint` |
| status | varchar(20) | NO | `open`, `in_progress`, `resolved`, … |
| priority | varchar(10) | NO | `low``urgent` |
| asset_id | uuid | YES | FK → assets (laptop/PC terkait) |
| reporter_user_id | uuid | YES | FK → users |
| assigned_to_user_id | uuid | YES | FK → users (IT) |
| subject | varchar(255) | NO | |
| description | text | YES | |
| resolution_notes | text | YES | |
| opened_at | timestamptz | NO | |
| resolved_at | timestamptz | YES | |
| closed_at | timestamptz | YES | |
---
### `document_attachments` (Fase 3 — ADR 003)
Metadata file di **QNAP S3**. Satu tabel untuk procurement, service, complain.
| Kolom | Tipe | Nullable | Keterangan |
|---|---|---|---|
| id | uuid | NO | PK |
| domain | varchar(20) | NO | `procurement` \| `service` \| `complaint` |
| kind | varchar(40) | NO | `invoice_pdf`, `unit_photo`, `service_report`, … |
| asset_id | uuid | YES | FK langsung ke asset |
| invoice_id | uuid | YES | FK → asset_invoices |
| ticket_id | uuid | YES | FK → tickets |
| storage_provider | varchar(20) | NO | `qnap_s3` |
| bucket | varchar(100) | NO | e.g. `geonetagent-documents` |
| object_key | varchar(500) | NO | path di bucket |
| original_filename | varchar(255) | NO | |
| content_type | varchar(100) | YES | MIME |
| file_size_bytes | bigint | YES | |
| checksum_sha256 | varchar(64) | YES | |
| uploaded_by | varchar(100) | YES | |
| uploaded_at | timestamptz | NO | |
Constraint: minimal satu dari `asset_id`, `invoice_id`, `ticket_id` NOT NULL.
**Object key pattern:** `{domain}/{yyyy}/{mm}/{attachment_id}/{filename}` — lihat ADR 003.
---
### `alerts` ### `alerts`
Denormalized dari Zabbix webhook (bukan source of truth — Zabbix tetap master). Denormalized dari Zabbix webhook (bukan source of truth — Zabbix tetap master).
@ -401,6 +512,7 @@ WHERE asset_type = 'endpoint'
11. `agent_tokens` 11. `agent_tokens`
12. `sync_logs` 12. `sync_logs`
13. `portal_users` (Laravel) 13. `portal_users` (Laravel)
14. `asset_invoices`, `tickets`, `document_attachments` (Fase 3 — ADR 003, migration `005`)
--- ---

178
GeoNetAgent/.cursor/progress.md

@ -1,239 +1,179 @@
# GeoNetAgent — Progress & Monitoring Fase # GeoNetAgent — Progress & Monitoring Fase
> **Update file ini** setiap milestone selesai. Ini sumber tunggal untuk cek *kita ada di fase mana*. > **Update file ini** setiap milestone selesai. Ini sumber tunggal untuk cek *kita ada di fase mana*.
**Terakhir diupdate:** 2026-06-22 (agent v0.1.10 · ADR 003 dokumen QNAP · view `v_asset_hardware_latest`)
**Terakhir diupdate:** 2026-06-17 (web install agent · domain gisportal.id · reverse proxy)
--- ---
## Snapshot (baca cepat) ## Snapshot (baca cepat)
| Item | Status | | Item | Status |
|------|--------| |------|--------|
| **Fase aktif** | **Fase 1 — Foundation** 🔄 | | **Fase aktif** | **Fase 1 — Foundation** 🔄 |
| Fase berikutnya | Laravel asset index · auth IT | | Fase berikutnya | Laravel asset index · auth IT |
| Agent | `v0.1.10-phase0` — collect + POST + auto-update + `cpu{}` detail |
| Agent POC | `v0.1.6-phase0` — collect + POST + web install | | Collector | `https://collector.gisportal.id/ui/` · merge/split asset · section CPU/RAM/GPU |
| Collector | `https://collector.gisportal.id` UI · `agent.gisportal.id` POST · `:8000` HTTP LAN |
| Monorepo | Partial — `agent/` + `collector/` + `samples/` + `infra/` ✅ · `portal/` ❌ | | Monorepo | Partial — `agent/` + `collector/` + `samples/` + `infra/` ✅ · `portal/` ❌ |
| Blocker utama | Laravel portal belum scaffold | | Blocker utama | Laravel portal belum scaffold |
### Progress ringkas ### Progress ringkas
| Fase | Status | Progress | | Fase | Status | Progress |
|------|--------|----------| |------|--------|----------|
| 0 — Blueprint | ✅ Selesai | 8 / 8 | | 0 — Blueprint | ✅ Selesai | 8 / 8 |
| 1 — Foundation | 🔄 Aktif | 8 / 9 |
| 1 — Foundation | 🔄 Aktif | 7 / 9 |
| 2 — Integrasi monitoring | ⏳ | 0 / 2 | | 2 — Integrasi monitoring | ⏳ | 0 / 2 |
| 3 — Infra sync & CMDB | ⏳ | 0 / 3 | | 3 — Infra sync & CMDB | ⏳ | 0 / 3 |
| 4 — Scale & intelligence | ⏳ | 1 / 3 | | 4 — Scale & intelligence | ⏳ | 1 / 3 |
--- ---
## Fase 0 — Blueprint ✅ **(SELESAI)** ## Fase 0 — Blueprint ✅ **(SELESAI)**
**DoD:** Dokumentasi lengkap, blueprint disetujui user, struktur monorepo ter-scaffold. **DoD:** Dokumentasi lengkap, blueprint disetujui user, struktur monorepo ter-scaffold.
- [x] Dokumentasi `.cursor/` lengkap (blueprint, db, API, repo, monitoring, playbook) - [x] Dokumentasi `.cursor/` lengkap (blueprint, db, API, repo, monitoring, playbook)
- [x] `server.md` + SSH key-only diverifikasi - [x] `server.md` + SSH key-only diverifikasi
- [x] Rule `project-context.mdc` (server, Dockge, QNAP Object Storage) - [x] Rule `project-context.mdc` (server, Dockge, QNAP Object Storage)
- [x] Scaffold partial — folder `agent/` + `samples/agent-reports/` - [x] Scaffold partial — folder `agent/` + `samples/agent-reports/`
- [x] Uji agent collect-only di laptop dev (`DESKTOP-RRJ9G01`) - [x] Uji agent collect-only di laptop dev (`DESKTOP-RRJ9G01`)
- [ ] Review & approval user terhadap blueprint - [ ] Review & approval user terhadap blueprint
- [ ] Scaffold monorepo penuh (`portal/`) - [ ] Scaffold monorepo penuh (`portal/`)
- [x] Pindahkan sample DXDiag ke `samples/dxdiag/` - [x] Pindahkan sample DXDiag ke `samples/dxdiag/`
### Sub-task: Agent POC ### Sub-task: Agent POC
- [x] `Collect-GeoNetReport.ps1` + modul `lib/` - [x] `Collect-GeoNetReport.ps1` + modul `lib/`
- [x] Payload: system, disk, memory, network, health, peripherals, monitor - [x] Payload: system, disk, memory, network, health, peripherals, monitor
- [x] `Send-GeoNetReport.ps1` — E2E POST ke collector - [x] `Send-GeoNetReport.ps1` — E2E POST ke collector
- [x] Web install `https://agent.gisportal.id/install/` — pilot `DESKTOP-RRJ9G01` - [x] Web install `https://agent.gisportal.id/install/` — pilot `DESKTOP-RRJ9G01`
- [ ] Uji collect di laptop kedua (`BKK-MINIPC`) - [ ] Uji collect di laptop kedua (`BKK-MINIPC`)
--- ---
## Fase 1 — Foundation 🔄 **(AKTIF)** ## Fase 1 — Foundation 🔄 **(AKTIF)**
**DoD:** Agent di 1 laptop test POST → data di DB → Laravel tampilkan halaman asset list. **DoD:** Agent di 1 laptop test POST → data di DB → Laravel tampilkan halaman asset list.
- [x] PostgreSQL `geonetagent_dev` + SQL migration `001_initial_schema` di main-db - [x] PostgreSQL `geonetagent_dev` + SQL migration `001_initial_schema` di main-db
- [x] Migration `002_disk_model`, `003_drop_machine_id_unique` (dev)
- [x] FastAPI `/api/v1/agent/report` + `/health` + read API assets - [x] FastAPI `/api/v1/agent/report` + `/health` + read API assets
- [x] PowerShell agent v0.1.10 + kirim ke collector (E2E verified)
- [x] PowerShell agent v0.1.6 + kirim ke collector (E2E verified)
- [x] UI uji read-only — `https://collector.gisportal.id/ui/` · `http://10.100.1.24:8000/ui/` - [x] UI uji read-only — `https://collector.gisportal.id/ui/` · `http://10.100.1.24:8000/ui/`
- [x] Nginx reverse proxy — POST agent `agent.gisportal.id` · HTTPS UI `collector.gisportal.id` - [x] Nginx reverse proxy — POST agent `agent.gisportal.id` · HTTPS UI `collector.gisportal.id`
- [x] DNS publik + split-DNS LAN `agent.gisportal.id` / `collector.gisportal.id` - [x] DNS publik + split-DNS LAN `agent.gisportal.id` / `collector.gisportal.id`
- [x] Firewalld port `8000` untuk subnet internal di `.24` - [x] Firewalld port `8000` untuk subnet internal di `.24`
- [x] Asset identity merge (ADR 001) + split hardware campur (ADR 002)
- [ ] Laravel asset index (read-only) - [ ] Laravel asset index (read-only)
- [ ] Auth IT user lokal (M9) - [ ] Auth IT user lokal (M9)
### Agent feature log (Foundation)
| Versi | Fitur |
|-------|--------|
| v0.1.6 | Peripherals, scheduled task 3 jam |
| v0.1.8 | `gpus[]`, disk model, auto-update, GPO scripts |
| v0.1.9 | `physical_disks[]`, RAM DDR detail, GPU detail, `form_factor` Laptop/PC, `Build-GeoNetReportPayload.ps1` |
| v0.1.10 | `cpu{}` (cores, clock, cache, arch), UI section CPU, deploy PS1 (`publish_agent_install.ps1`, `push_collector.ps1`) |
--- ### Backlog agent (diskusi, belum diimplementasi)
| Target | Fitur |
|--------|--------|
| v0.1.11+ | Printer (`Win32_Printer` / PnP PrintQueue) |
| v0.1.12+ | Klasifikasi HID keyboard vs mouse (saat ini generik `USB Input Device`) |
| Fase 3 | `is_factory` / baseline hardware vs upgrade aftermarket (RAM, disk, GPU) |
### Fase 3 — Dokumen & tiket (ADR 003, desain accepted)
## Fase 2 — Integrasi monitoring ⏳ | Item | Status |
|------|--------|
| ADR 003 | ✅ `003-asset-documents-qnap.md` |
| Migration 005 | ✅ Draft SQL — belum dijalankan di DB |
| QNAP bucket `geonetagent-documents` | ⏳ |
| Laravel upload S3 + UI invoice/tiket | ⏳ (portal belum scaffold) |
Domain: **procurement** (`asset_invoices`), **service** + **complaint** (`tickets`), file bersama **`document_attachments`** → QNAP Object Storage `10.100.1.10`.
### Periferal: bawaan vs eksternal (status saat ini)
**DoD:** Alert Zabbix di portal; status host dari Zabbix API. | Jenis | Field | Status |
|-------|-------|--------|
| Monitor eksternal / panel laptop | `peripherals[].is_external` + `connection: display` | ✅ |
| USB keyboard/mouse/dongle | `peripherals[]`, `is_external: true` | ✅ terdeteksi, ⚠ nama sering generik |
| NIC onboard | `network[].is_built_in` | ✅ |
| CPU/RAM/GPU/disk fisik | section terpisah (tanpa flag eksternal) | ✅ inventory, ❌ belum beda bawaan vs upgrade |
| Printer | — | ❌ belum dikumpulkan |
| Audio jack 3.5mm | `is_external: null` | ⚠ colokan tidak diketahui WMI |
## Fase 2 — Integrasi monitoring ⏳
- [ ] Zabbix webhook → Laravel **DoD:** Alert Zabbix di portal; status host dari Zabbix API.
- [ ] Zabbix webhook → Laravel
- [ ] Zabbix host status cache di dashboard - [ ] Zabbix host status cache di dashboard
--- ---
## Fase 3 — Infra sync & CMDB ⏳ ## Fase 3 — Infra sync & CMDB ⏳
**DoD:** VM Proxmox otomatis di CMDB; license & dokumen asset (invoice, service, complain) bisa di-assign.
**DoD:** VM Proxmox otomatis di CMDB; license manual bisa di-assign.
- [ ] Proxmox sync job (FastAPI) - [ ] Proxmox sync job (FastAPI)
- [ ] License module (Laravel) - [ ] License module (Laravel)
- [ ] User ↔ device assignment UI - [ ] User ↔ device assignment UI
- [x] ADR 003 — desain dokumen QNAP (`asset_invoices`, `tickets`, `document_attachments`)
- [ ] Migration 005 + bucket `geonetagent-documents` di QNAP
- [ ] Laravel upload/download dokumen (procurement, service, complain)
--- ---
## Fase 4 — Scale & intelligence ⏳ ## Fase 4 — Scale & intelligence ⏳
- [ ] GPO deploy agent ke semua laptop (web install siap; GPO opsional) - [ ] GPO deploy agent ke semua laptop (web install siap; GPO opsional)
- [x] Web install `https://agent.gisportal.id/install/` + `Install-GeoNetAgent.ps1` - [x] Web install `https://agent.gisportal.id/install/` + `Install-GeoNetAgent.ps1`
- [ ] Trend & compliance report - [ ] Trend & compliance report
- [ ] Ollama assistant (opsional) - [ ] Ollama assistant (opsional)
--- ---
## Cara update ## Cara update
1. Centang `[x]` item yang selesai 1. Centang `[x]` item yang selesai
2. Update **Terakhir diupdate** dan tabel **Snapshot** 2. Update **Terakhir diupdate** dan tabel **Snapshot**
3. Hitung ulang kolom **Progress** (item selesai / total item fase) 3. Hitung ulang kolom **Progress** (item selesai / total item fase)
4. Jika pindah fase: ubah **Fase aktif** di Snapshot + banner `(AKTIF)` pada section fase 4. Jika pindah fase: ubah **Fase aktif** di Snapshot + banner `(AKTIF)` pada section fase
5. Sinkronkan baris status singkat di `blueprint.md`, `me.md`, `monitoring-architecture.md`, `project-context.mdc`, `perintah.md` 5. Sinkronkan baris status singkat di `blueprint.md`, `me.md`, `monitoring-architecture.md`, `project-context.mdc`, `perintah.md`
6. **Bump versi agent** → wajib publish server + deploy UI (jika berubah) + **tambah baris Riwayat milestone** (lihat `.cursor/rules/agent-release-deploy.mdc`)
### Checklist release agent (ringkas)
```
agent/VERSION + manifest.json
→ publish_agent_install.ps1 (agent.gisportal.id/install/)
→ push_collector.ps1 (collector/UI — jika payload atau UI berubah)
→ progress.md Riwayat milestone + feature log
→ perintah.md + project-context.mdc
```
--- ---
## Riwayat milestone ## Riwayat milestone
| Tanggal | Milestone | | Tanggal | Milestone |
|---------|-----------| |---------|-----------|
| 2026-06-17 | Dokumentasi `.cursor/` + SSH server verified | | 2026-06-17 | Dokumentasi `.cursor/` + SSH server verified |
| 2026-06-17 | Agent POC `Collect-GeoNetReport.ps1` v0.1.2 — collect JSON lokal | | 2026-06-17 | Agent POC `Collect-GeoNetReport.ps1` v0.1.2 — collect JSON lokal |
| 2026-06-17 | File `progress.md` dibuat — tracking fase terpusat | | 2026-06-17 | File `progress.md` dibuat — tracking fase terpusat |
| 2026-06-17 | Agent v0.1.4 — `peripherals[]` USB/HID/BT/audio jack | | 2026-06-17 | Agent v0.1.4 — `peripherals[]` USB/HID/BT/audio jack |
| 2026-06-17 | Collector redeploy `.24` + UI peripherals di `/ui/` | | 2026-06-17 | Collector redeploy `.24` + UI peripherals di `/ui/` |
| 2026-06-17 | Nginx reverse proxy — domain `gisportal.id` | | 2026-06-17 | Nginx reverse proxy — domain `gisportal.id` |
| 2026-06-17 | Web install agent + pilot deploy `DESKTOP-RRJ9G01` | | 2026-06-17 | Web install agent + pilot deploy `DESKTOP-RRJ9G01` |
| 2026-06-22 | Agent v0.1.8 — GPU, disk model, merge disk-clone, fix nginx `$env:` truncate |
| 2026-06-22 | Split asset `DESKTOP-RRJ9G01` laptop vs PC (ADR 002) |
| 2026-06-22 | Agent v0.1.9 — `physical_disks[]`, RAM/GPU detail, badge Laptop/PC di UI |
| 2026-06-22 | Agent v0.1.10 — `cpu{}`, publish/deploy PS1, manifest 21 file di `agent.gisportal.id` |
| 2026-06-22 | View SQL `v_asset_hardware_latest` (migration 004, dev DB) |
| 2026-06-22 | ADR 003 — dokumen procurement/service/complain di QNAP + migration 005 draft |

18
GeoNetAgent/.cursor/repo-structure.md

@ -13,7 +13,7 @@ GeoNetAgent/
│ ├── blueprint.md # Master index │ ├── blueprint.md # Master index
│ ├── progress.md # Checklist fase & snapshot progress │ ├── progress.md # Checklist fase & snapshot progress
│ ├── server.md # SSH & server proyek │ ├── server.md # SSH & server proyek
│ ├── adr/ # Architecture Decision Records │ ├── adr/ # ADR 001–003 (identitas, split, dokumen QNAP)
│ ├── monitoring-architecture.md │ ├── monitoring-architecture.md
│ ├── db-schema.md │ ├── db-schema.md
│ ├── api-contracts.md │ ├── api-contracts.md
@ -79,18 +79,22 @@ GeoNetAgent/
│ │ ├── init_geonetagent_dev.sql │ │ ├── init_geonetagent_dev.sql
│ │ ├── deploy_dev.sh │ │ ├── deploy_dev.sh
│ │ └── migrations/ │ │ └── migrations/
│ │ └── 001_initial_schema.sql │ │ ├── 001_initial_schema.sql … 005_asset_documents.sql
│ ├── qnap/
│ │ └── README.md # Object Storage bucket dokumen (ADR 003)
│ ├── docker-compose.yml │ ├── docker-compose.yml
│ ├── docker-compose.dev.yml │ ├── docker-compose.dev.yml
│ ├── nginx/ │ ├── nginx/
│ │ ├── geonetagent-collector-public.conf # agent.gisportal.id POST + /install/ │ │ ├── geonetagent-collector-public.conf
│ │ ├── geonetagent-collector-https.conf # collector.gisportal.id UI/API │ │ ├── geonetagent-collector-https.conf
│ │ ├── geonetagent-collector-internal.conf # :8000 LAN │ │ ├── geonetagent-collector-internal.conf
│ │ └── README.md │ │ └── README.md
│ ├── collector/ │ ├── collector/
│ │ ├── deploy_nginx.sh │ │ ├── deploy_nginx.sh
│ │ ├── deploy_nginx_remote.sh │ │ ├── publish_agent_install.sh
│ │ └── publish_agent_install.sh │ │ ├── publish_agent_install.ps1
│ │ ├── push_collector.sh
│ │ └── push_collector.ps1
│ └── zabbix/ │ └── zabbix/
│ └── webhook-media-type.json │ └── webhook-media-type.json

52
GeoNetAgent/.cursor/rules/agent-release-deploy.mdc

@ -0,0 +1,52 @@
---
description: Wajib publish agent + deploy collector/UI + catat milestone saat bump versi agent
alwaysApply: true
---
# Agent Release — Deploy Server & Dokumentasi
Setiap perubahan agent yang **mem-bump versi** (`agent/VERSION`, `manifest.json`, atau string `AgentVersion` di skrip) **belum selesai** sampai ketiga langkah ini dilakukan.
## 1. Publish agent ke server (wajib)
Endpoint user/auto-update membaca `https://agent.gisportal.id/install/manifest.json`. Tanpa publish, installer dan `UserAgent.ps1` tetap mengunduh versi lama.
```powershell
.\infra\collector\publish_agent_install.ps1
```
Verifikasi: manifest HTTPS = versi baru; jumlah file manifest sesuai (mis. `Get-CpuInfo.ps1` ikut ter-upload).
## 2. Deploy collector + UI (wajib jika ada perubahan payload/schema/UI)
Perubahan payload, schema FastAPI, `asset_read_service`, atau `collector/static/` **wajib** rebuild container:
```powershell
.\infra\collector\push_collector.ps1
```
Jika hanya fix skrip agent tanpa perubahan collector, publish saja — tetapi **default asumsikan UI ikut deploy** bila field baru ditampilkan di UI.
## 3. Dokumentasi & riwayat milestone (wajib)
Sebelum nyatakan selesai, update:
| File | Isi |
|------|-----|
| `.cursor/progress.md` | Baris **Riwayat milestone** + feature log versi + **Terakhir diupdate** |
| `perintah.md` | Versi agent saat ini + blok sync terakhir |
| `.cursor/api-contracts.md` | Field/kontrak baru (jika ada) |
| `agent/README.md` | Tabel versi |
| `.cursor/rules/project-context.mdc` | Baris **Agent saat ini** |
Format milestone: `| YYYY-MM-DD | Agent vX.Y.Z — ringkasan fitur + deploy server |`
## Checklist AI (jangan skip)
- [ ] `agent/VERSION` = `install/manifest.json` version
- [ ] `publish_agent_install.ps1` sukses; manifest HTTPS cocok
- [ ] `push_collector.ps1` jika collector/UI berubah
- [ ] Riwayat milestone di `progress.md` ditambah
- [ ] Sync `perintah.md` sesuai protokol
Server target: `ssh reverse-proxy` (10.100.1.24). Detail SSH: `.cursor/server.md`.

35
GeoNetAgent/.cursor/rules/documentation.mdc

@ -0,0 +1,35 @@
---
description: Aturan dokumentasi — kapan harus update doc setelah perubahan
alwaysApply: true
---
# Documentation Rules
## Setelah Perubahan Apapun
| Jika berubah | Wajib update |
|--------------|-------------|
| Schema DB (migration baru) | `docs/database.md` + `.cursor/db-schema.md` |
| API endpoint baru/berubah | `docs/api/service-collector.md` + `.cursor/api-contracts.md` + `collector/docs/API.md` |
| Arsitektur berubah | `docs/architecture/system-overview.md` + ADR baru di `docs/adr/` |
| Keputusan teknologi baru | ADR baru di `docs/adr/ADR-00N-*.md` |
| Bug ditemukan/fix | `docs/issues.md` |
| Fase selesai (DoD) | `.cursor/progress.md` + `docs/project-status.md` |
| Deploy sukses | `perintah.md` (Sync terakhir) + `.cursor/progress.md` (Riwayat) |
| Variabel env baru | `docs/environment.md` + `.env.example` |
| Script server baru | `docs/server.md` |
## Aturan ADR
- ADR **tidak boleh dihapus** — buat ADR baru jika keputusan berubah
- Format file: `ADR-NNN-judul-kebab-case.md` di `docs/adr/`
- Update `docs/adr/README.md` setiap ada ADR baru
- Isi minimal: Konteks, Keputusan, Alternatif yang Ditolak, Konsekuensi
## Definition of Done
Implementasi belum selesai jika:
- [ ] Dokumentasi relevan belum diupdate
- [ ] `perintah.md` belum di-sync
- [ ] `.cursor/progress.md` belum mencatat milestone
- [ ] Secret baru belum didokumentasikan lokasinya di `docs/environment.md` (nilai tidak boleh di repo)

3
GeoNetAgent/.cursor/rules/project-context.mdc

@ -19,6 +19,7 @@ Sebelum asumsi tentang stack, arsitektur, atau implementasi, baca urutan ini:
- Folder/scaffold → `.cursor/repo-structure.md` - Folder/scaffold → `.cursor/repo-structure.md`
- Monitoring/Zabbix → `.cursor/monitoring-architecture.md` - Monitoring/Zabbix → `.cursor/monitoring-architecture.md`
- Multi-tab agent → `.cursor/agent-playbook.md` - Multi-tab agent → `.cursor/agent-playbook.md`
- Dokumen asset (QNAP) → `.cursor/adr/003-asset-documents-qnap.md`, `infra/qnap/README.md`
5. Server existing & SSH (key-only, wajib PowerShell): `.cursor/server.md` 5. Server existing & SSH (key-only, wajib PowerShell): `.cursor/server.md`
6. Server proyek GeoNetAgent — alias cepat (`~/.ssh/config`): 6. Server proyek GeoNetAgent — alias cepat (`~/.ssh/config`):
- `ssh reverse-proxy` — reverse proxy & Docker (10.100.1.24); simpan stack compose di `/opt/stacks/` agar ter-monitor via Dockge - `ssh reverse-proxy` — reverse proxy & Docker (10.100.1.24); simpan stack compose di `/opt/stacks/` agar ter-monitor via Dockge
@ -36,6 +37,8 @@ Sebelum asumsi tentang stack, arsitektur, atau implementasi, baca urutan ini:
- **Zabbix** = monitoring real-time; **FastAPI** = collector ingress; **Laravel** = portal/CMDB - **Zabbix** = monitoring real-time; **FastAPI** = collector ingress; **Laravel** = portal/CMDB
- **Monorepo**: `agent/` + `collector/` + `portal/` + `infra/` - **Monorepo**: `agent/` + `collector/` + `portal/` + `infra/`
- **URL produksi**: agent POST `https://agent.gisportal.id` · UI `https://collector.gisportal.id` · install `https://agent.gisportal.id/install/` - **URL produksi**: agent POST `https://agent.gisportal.id` · UI `https://collector.gisportal.id` · install `https://agent.gisportal.id/install/`
- **Agent saat ini**: v0.1.10-phase0 (`cpu{}`, `physical_disks[]`, RAM/GPU detail, `form_factor`, auto-update manifest)
- **Jangan** bangun monitoring engine baru; **jangan** duplikasi fungsi Zabbix - **Jangan** bangun monitoring engine baru; **jangan** duplikasi fungsi Zabbix
- **Bahasa**: Bahasa Indonesia untuk komunikasi dengan user - **Bahasa**: Bahasa Indonesia untuk komunikasi dengan user
- **Documentation First**: ikuti `perintah.md` — sync `.cursor/` sebelum nyatakan selesai - **Documentation First**: ikuti `perintah.md` — sync `.cursor/` sebelum nyatakan selesai
- **Agent release**: setiap bump versi agent → publish ke server + deploy collector/UI bila perlu + catat **Riwayat milestone** di `progress.md` (lihat rule `agent-release-deploy.mdc`)

40
GeoNetAgent/.cursor/rules/security.mdc

@ -0,0 +1,40 @@
---
description: Aturan keamanan — credential, token, SSH
alwaysApply: true
---
# Security Rules
## Larangan Mutlak
- ❌ Jangan commit `.env` ke repository
- ❌ Jangan commit password, API key, bearer token, private key ke repository
- ❌ Jangan tampilkan nilai secret di output percakapan/log
- ❌ Jangan hardcode credentials di kode Python atau PowerShell
## Token & Credential
| Secret | Lokasi yang Benar |
|--------|-------------------|
| DB credentials | `/root/geonetagent_dev.credentials` di server 10.100.1.25 |
| Agent bearer token | `.env` di server collector (10.100.1.24) |
| QNAP OSS credentials | `infra/qnap/README.md` (hanya referensi lokasi, nilai di server) |
| SSH keys | `~/.ssh/` di laptop developer (tidak di repo) |
## SSH Command Rules
- SSH command ke server wajib **minta persetujuan user** sebelum dijalankan
- Jangan auto-run SSH command yang bersifat destructive (rm, truncate, dll)
- Gunakan `scp -o "MACs=hmac-sha1"` untuk transfer file dari Windows
- Jangan heredoc `<< 'EOF'` via SSH dari PowerShell — `$` variabel terpotong
## Nginx Security
- `agent.gisportal.id/install/`: wajib ada `ssi off` — agar `$env:` di `.ps1` tidak terpotong
- Jangan expose port 18000 langsung ke internet
- SSL via certbot/Let's Encrypt — jangan matikan HTTPS
## Aturan untuk AI Agent
- Jika diminta membuat credential atau secret baru: catat **lokasi penyimpanan** di `docs/environment.md`, bukan nilainya
- Jika token bocor: instruksikan user untuk rotasi manual — jangan buat token baru di repo

6
GeoNetAgent/.cursor/server.md

@ -12,7 +12,7 @@ Reverse_proxy (proxy.lan — nginx + GeoNetAgent collector):
# UI/API HTTPS: https://collector.gisportal.id/ui/ # UI/API HTTPS: https://collector.gisportal.id/ui/
# Agent POST publik: https://agent.gisportal.id/api/v1/agent/report # Agent POST publik: https://agent.gisportal.id/api/v1/agent/report
# Install agent web: https://agent.gisportal.id/install/ # Install agent web: https://agent.gisportal.id/install/
# Publish paket agent: bash infra/collector/publish_agent_install.sh # Publish paket agent: .\infra\collector\publish_agent_install.ps1 (Windows) atau bash infra/collector/publish_agent_install.sh
# Deploy nginx: infra/collector/deploy_nginx.sh · lihat infra/nginx/README.md # Deploy nginx: infra/collector/deploy_nginx.sh · lihat infra/nginx/README.md
Main_Database: Main_Database:
@ -44,5 +44,5 @@ Ollama (aiopr, Ubuntu 22.04) — POC & inference CPU:
ssh -i "$env:USERPROFILE\.ssh\id_ed25519" -o IdentitiesOnly=yes admin@10.100.1.10 -p 22 ssh -i "$env:USERPROFILE\.ssh\id_ed25519" -o IdentitiesOnly=yes admin@10.100.1.10 -p 22
.\scripts\ssh-qnap.ps1 .\scripts\ssh-qnap.ps1
ssh -m hmac-sha1 admin@10.100.1.10 -p 22 ssh -m hmac-sha1 admin@10.100.1.10 -p 22
# Object Storage — dokumen invoice/service/complain (ADR 003): infra/qnap/README.md
# Bucket: geonetagent-documents

84
GeoNetAgent/AGENTS.md

@ -0,0 +1,84 @@
# AGENTS.md — AI Agent Rules
## Entry Point Wajib
Sebelum melakukan pekerjaan apa pun, baca berurutan:
1. `docs/context-index.md` — peta seluruh dokumentasi
2. `.cursor/blueprint.md` — visi, fase, batas modul
3. `.cursor/progress.md` — fase aktif & checklist DoD
4. Dokumen spesifik modul (db-schema, api-contracts, dll)
**Jangan implement fase N+1 jika fase N belum DoD.**
---
## Aturan Kode
### Python (FastAPI Collector)
- Gunakan `snake_case` untuk variable, function, file
- Type hints wajib di semua function signature
- Pydantic schema di `app/schemas/` — jangan inline model di routes
- Route di `app/api/` — satu file per resource
- DB session via dependency injection (`get_db`)
- Jangan commit `.env` — gunakan `.env.example`
### PowerShell (Agent)
- Module di `agent/lib/` — satu file per sensor (Get-CpuInfo.ps1, dll)
- Jangan hardcode credential di skrip
- Hindari literal `$env:` di file `.ps1` yang di-serve Nginx (nginx potong respons)
- Test lokal dulu sebelum publish ke `agent.gisportal.id/install/`
### SQL / Database
- PK: UUID (`gen_random_uuid()`)
- Timestamp: `timestamptz`
- Nama tabel: `snake_case` plural
- Setiap perubahan schema → update `docs/database.md` + `.cursor/db-schema.md`
- Migration order: lihat `infra/db/README.md`
---
## Aturan Deploy
**Wajib setelah bump versi agent:**
```powershell
.\infra\collector\publish_agent_install.ps1 # upload ke agent.gisportal.id/install/
.\infra\collector\push_collector.ps1 # rebuild UI/API jika berubah
```
- Update `agent/VERSION` + `agent/install/manifest.json` sebelum publish
- Update `Riwayat milestone` di `.cursor/progress.md`
- Update `Sync terakhir` di `perintah.md`
**File transfer ke server:** gunakan `scp -o "MACs=hmac-sha1"` dari Windows.
Jangan heredoc SSH dari PowerShell — `$` variabel terpotong.
---
## Aturan Dokumentasi
- Setiap perubahan arsitektur → buat ADR baru di `docs/adr/ADR-00N-*.md`
- ADR **tidak boleh dihapus** — buat ADR baru jika keputusan berubah
- Setelah implementasi: update `.cursor/progress.md` + `perintah.md`
- Implementasi tanpa dokumentasi = **belum selesai**
- Update `docs/context-index.md` jika ada dokumen baru
---
## Aturan Keamanan
- **Jangan commit secret**: password DB, bearer token, API key
- Credentials DB ada di `/root/geonetagent_dev.credentials` di server 10.100.1.25
- Token agent ada di `.env` server — lihat `docs/environment.md` untuk nama variabelnya
- SSH command butuh approval user sebelum dijalankan
- Nginx `agent.gisportal.id`: `ssi off` — jangan taruh `$env:` literal di file `.ps1` yang di-serve
---
## Batas Sistem (Non-goals)
Jangan implement hal-hal berikut — sudah diputuskan di ADR/blueprint:
- ❌ Engine monitoring time-series (pakai Zabbix)
- ❌ NMS pengganti MikroTik/Proxmox tools
- ❌ Duplikasi data GIS legacy dari SQL Server
- ❌ Remote desktop / RMM agent
- ❌ Ticketing enterprise penuh (fase awal)

102
GeoNetAgent/CHANGELOG.md

@ -0,0 +1,102 @@
# Changelog — GeoNetAgent
Format: [Semantic Versioning](https://semver.org) — Agent version / Doc version
---
## [Doc 1.0.0] — 2026-06-29
### Added (Dokumentasi)
- `README.md` — root README lengkap dengan quick start + struktur monorepo
- `AGENTS.md` — aturan AI agent untuk proyek ini
- `CHANGELOG.md` — file ini
- `docs/context-index.md` ⭐ — AI entry point utama, peta navigasi semua doc
- `docs/project-status.md` — status modul, backlog, technical debt
- `docs/roadmap.md` — fase dan milestone
- `docs/todo.md` — next actions berprioritas
- `docs/issues.md` — known bugs + limitations
- `docs/environment.md` — env vars (tanpa secret)
- `docs/server.md` — server list + SSH commands
- `docs/deployment.md` — step-by-step deploy
- `docs/development.md` — local setup + coding guide
- `docs/database.md` — schema PostgreSQL + migration history
- `docs/troubleshooting.md` — common issues + fixes
- `docs/monitoring.md` — Zabbix boundary + health check
- `docs/security.md` — credential management + rules
- `docs/glossary.md` — kamus istilah
- `docs/architecture/system-overview.md` — diagram sistem + boundary
- `docs/architecture/folder-structure.md` — struktur monorepo detail
- `docs/architecture/request-flow.md` — sequence diagram alur agent
- `docs/architecture/authentication-flow.md` — auth bearer token + Laravel (Fase 1)
- `docs/architecture/deployment-architecture.md` — topology + Nginx config
- `docs/architecture/database-flow.md` — write/read ownership + view
- `docs/api/overview.md` — base URL, auth, format response
- `docs/api/service-collector.md` — endpoint FastAPI lengkap + payload schema
- `docs/api/error-codes.md` — HTTP status codes
- `docs/adr/README.md` — index ADR
- `docs/adr/ADR-001-agent-asset-identity.md` — strategi identitas asset
- `docs/adr/ADR-002-asset-split-by-hardware.md` — split asset hardware campur
- `docs/adr/ADR-003-asset-documents-qnap.md` — dokumen di QNAP Object Storage
- `docs/diagrams/system.mmd` — Mermaid diagram sistem
- `docs/diagrams/agent-report-flow.mmd` — Mermaid sequence laporan agent
- `docs/diagrams/deployment.mmd` — Mermaid deployment topology
- `docs/onboarding/first-day.md` — panduan hari pertama
- `docs/onboarding/local-setup.md` — setup lokal developer
- `docs/onboarding/coding-standard.md` — standar kode
- `docs/decisions/technology-stack.md` — keputusan stack teknologi
- `docs/decisions/naming-convention.md` — keputusan naming convention
- `.cursor/rules/documentation.mdc` — aturan kapan harus update doc
- `.cursor/rules/security.mdc` — aturan keamanan + credential
---
## [Agent v0.1.10-phase0] — 2026-06-22
### Added
- `cpu{}` detail object: cores, clock, cache, arch
- Deploy script `push_collector.ps1` PowerShell
- Auto-update manifest check sebelum collect
### Changed
- Field CPU sebelumnya hanya ada di `system.cpu_model` — sekarang ada object terpisah `cpu{}`
---
## [Agent v0.1.9] — 2026-06-22
### Added
- `physical_disks[]` — disk fisik (model, serial, size, media_type, bus_type)
- RAM DDR type label (DDR3/DDR4/DDR5)
- GPU detail: driver version, current resolution, pnp_device_id
- `form_factor`: Laptop/Desktop/Server dari SMBIOS + PCSystemType
---
## [Agent v0.1.8] — 2026-06-22
### Fixed
- **Nginx `$env:` cut-off bug** — tambah `ssi off` di nginx config
### Changed
- Bump versi untuk publish ke server
---
## [Agent v0.1.6] — 2026-06-17
### Added
- Peripherals scan: USB/BT/audio/monitor (`is_external` flag)
- Scheduled Task setiap 3 jam (SYSTEM account)
- Web install `agent.gisportal.id/install/`
---
## [Collector v0.1.0] — 2026-06-17
### Added
- FastAPI: `POST /api/v1/agent/report` + upsert assets/snapshots/software
- FastAPI: `GET /api/v1/assets` + detail + snapshots + software
- FastAPI: `GET /health`
- UI: `/ui/` read-only CMDB
- PostgreSQL migrations 001-004
- Nginx reverse proxy: `agent.gisportal.id` + `collector.gisportal.id`

120
GeoNetAgent/README.md

@ -0,0 +1,120 @@
# GeoNetAgent — IT Asset Monitoring & CMDB
Portal IT + CMDB ringan yang melengkapi Zabbix. Mengumpulkan data hardware/software dari endpoint Windows via PowerShell agent, menyimpannya ke PostgreSQL melalui FastAPI collector, dan menampilkannya di portal Laravel.
> **AI Agent & Developer baru:** Mulai dari [`docs/context-index.md`](docs/context-index.md) — peta navigasi seluruh dokumentasi.
---
## Komponen
| Komponen | Stack | Lokasi | URL Produksi |
|----------|-------|--------|--------------|
| **PowerShell Agent** | PS 5.1+ | `agent/` | Install: `agent.gisportal.id/install/` |
| **FastAPI Collector** | Python 3.11 + FastAPI | `collector/` | `collector.gisportal.id` |
| **Infrastruktur** | Nginx, Docker, DB migrations | `infra/` | - |
| **Laravel Portal** | Laravel (belum scaffold) | `portal/` (planned) | `portal.geonet.internal` |
---
## Quick Start — Install Agent (User)
Jalankan PowerShell sebagai **Administrator**, dua baris berurutan:
```powershell
Invoke-WebRequest -Uri 'https://agent.gisportal.id/install/GeoNetAgent-Install.ps1' -OutFile "$env:TEMP\GeoNetAgent-Install.ps1" -UseBasicParsing
& "$env:TEMP\GeoNetAgent-Install.ps1"
```
Untuk update: perintah yang sama. `config.json` lokal dipertahankan.
---
## Quick Start — Developer
```bash
cd collector
cp ../.env.example .env
# Edit .env — isi DATABASE_URL dan AGENT_BEARER_TOKEN
pip install -e ".[dev]"
uvicorn app.main:app --reload --port 8000
```
---
## Deploy (Admin IT)
```powershell
# Publish agent ke server (setelah bump versi)
.\infra\collector\publish_agent_install.ps1
# Deploy collector (FastAPI)
.\infra\collector\push_collector.ps1
# Deploy nginx config
bash infra/collector/deploy_nginx.sh
```
---
## Status Proyek
| Fase | Status | Keterangan |
|------|--------|------------|
| Fase 0 — Blueprint | ✅ Selesai | Dokumentasi + scaffold |
| Fase 1 — Foundation | 🔄 Aktif (8/9) | Agent E2E → DB → UI read-only · Blocker: Laravel asset index |
| Fase 2 — Zabbix | ⏳ Pending | Webhook alert + host status |
| Fase 3 — CMDB Lengkap | ⏳ Pending | Proxmox sync, license, dokumen QNAP |
| Fase 4 — Scale | ⏳ Pending | GPO, trend, Ollama |
Agent versi saat ini: **v0.1.10-phase0**
---
## Struktur Monorepo
```
GeoNetAgent/
├── agent/ — PowerShell agent (install ke endpoint user)
│ ├── lib/ — Modul collect: CPU, disk, RAM, GPU, network, software, peripherals
│ ├── gpo/ — Script GPO untuk rollout massal
│ └── install/ — Entry point web install
├── collector/ — FastAPI service (terima laporan agent, read API)
│ ├── app/ — Source FastAPI
│ └── docs/API.md — API documentation
├── infra/ — Nginx, DB migrations, Docker, QNAP config
│ ├── collector/ — Deploy scripts
│ ├── db/ — SQL migrations
│ ├── nginx/ — Nginx config
│ └── qnap/ — QNAP Object Storage config
├── samples/ — Sample payload & DXDiag untuk testing
├── docs/ — Dokumentasi lengkap ⭐
└── .cursor/ — AI context (blueprint, schema, API, progress)
```
---
## Dokumentasi Lengkap
Lihat [`docs/context-index.md`](docs/context-index.md) untuk peta dokumentasi.
| Topik | File |
|-------|------|
| Status & progress | `docs/project-status.md` |
| Arsitektur sistem | `docs/architecture/system-overview.md` |
| API Collector | `docs/api/service-collector.md` |
| Database schema | `docs/database.md` |
| Deploy guide | `docs/deployment.md` |
| Environment vars | `docs/environment.md` |
| Troubleshooting | `docs/troubleshooting.md` |
| ADR | `docs/adr/` |
---
## ADR (Architecture Decision Records)
| ADR | Keputusan |
|-----|-----------|
| [ADR-001](docs/adr/ADR-001-agent-asset-identity.md) | Strategi identitas asset (serial → smbios_uuid → MAC) |
| [ADR-002](docs/adr/ADR-002-asset-split-by-hardware.md) | Split asset dengan hardware campur |
| [ADR-003](docs/adr/ADR-003-asset-documents-qnap.md) | Dokumen procurement di QNAP Object Storage |

163
GeoNetAgent/agent/Collect-GeoNetReport.ps1

@ -1,20 +1,6 @@
<# <#
.SYNOPSIS .SYNOPSIS
Kumpulkan inventaris endpoint GeoNetAgent dan simpan sebagai JSON. Kumpulkan inventaris endpoint GeoNetAgent dan simpan sebagai JSON.
.DESCRIPTION
Fase 0 collect-only (belum POST ke FastAPI).
Output mengikuti schema api-contracts.md POST /api/v1/agent/report
.PARAMETER OutputDir
Folder tujuan simpan JSON. Default: samples/agent-reports/
.PARAMETER MaxSoftware
Batas entri software (registry scan bisa lambat).
.EXAMPLE
.\Collect-GeoNetReport.ps1
.\Collect-GeoNetReport.ps1 -OutputDir D:\temp\geonet -MaxSoftware 50
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(
@ -24,7 +10,7 @@ param(
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$AgentVersion = '0.1.8-phase0' $AgentVersion = '0.1.10-phase0'
$SchemaVersion = '1.0' $SchemaVersion = '1.0'
$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path $scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
@ -33,14 +19,17 @@ $libPath = Join-Path $scriptRoot 'lib'
. (Join-Path $libPath 'Format-IsoDateTime.ps1') . (Join-Path $libPath 'Format-IsoDateTime.ps1')
. (Join-Path $libPath 'Format-Text.ps1') . (Join-Path $libPath 'Format-Text.ps1')
. (Join-Path $libPath 'Get-SystemInfo.ps1') . (Join-Path $libPath 'Get-SystemInfo.ps1')
. (Join-Path $libPath 'Get-CpuInfo.ps1')
. (Join-Path $libPath 'Get-GpuInfo.ps1') . (Join-Path $libPath 'Get-GpuInfo.ps1')
. (Join-Path $libPath 'Get-DiskInfo.ps1') . (Join-Path $libPath 'Get-DiskInfo.ps1')
. (Join-Path $libPath 'Get-PhysicalDiskInfo.ps1')
. (Join-Path $libPath 'Get-MemoryInfo.ps1') . (Join-Path $libPath 'Get-MemoryInfo.ps1')
. (Join-Path $libPath 'Get-NetworkInfo.ps1') . (Join-Path $libPath 'Get-NetworkInfo.ps1')
. (Join-Path $libPath 'Get-AssetIdentity.ps1') . (Join-Path $libPath 'Get-AssetIdentity.ps1')
. (Join-Path $libPath 'Get-HealthInfo.ps1') . (Join-Path $libPath 'Get-HealthInfo.ps1')
. (Join-Path $libPath 'Get-PeripheralsInfo.ps1') . (Join-Path $libPath 'Get-PeripheralsInfo.ps1')
. (Join-Path $libPath 'Get-SoftwareInfo.ps1') . (Join-Path $libPath 'Get-SoftwareInfo.ps1')
. (Join-Path $libPath 'Build-GeoNetReportPayload.ps1')
if (-not $OutputDir) { if (-not $OutputDir) {
$repoRoot = Split-Path -Parent $scriptRoot $repoRoot = Split-Path -Parent $scriptRoot
@ -54,152 +43,24 @@ if (-not (Test-Path $OutputDir)) {
Write-Host "GeoNetAgent — Collect-GeoNetReport v$AgentVersion" -ForegroundColor Cyan Write-Host "GeoNetAgent — Collect-GeoNetReport v$AgentVersion" -ForegroundColor Cyan
Write-Host "Mengumpulkan data endpoint..." -ForegroundColor Gray Write-Host "Mengumpulkan data endpoint..." -ForegroundColor Gray
$sys = Get-GeoNetSystemInfo
$disks = @(Invoke-GeoNetSafe { @(Get-GeoNetDiskInfo) } -Default @())
$memory = Invoke-GeoNetSafe { Get-GeoNetMemoryInfo } -Default @{ Modules = @(); Summary = $null }
$network = @(Invoke-GeoNetSafe { @(Get-GeoNetNetworkInfo) } -Default @())
$identity = Get-GeoNetAssetIdentity `
-SerialNumber (Convert-GeoNetOptionalString -Value $sys.SerialNumber) `
-SmbiosUuid (Convert-GeoNetOptionalString -Value $sys.SmbiosUuid) `
-MachineId (Convert-GeoNetOptionalString -Value $sys.MachineId) `
-Hostname (Convert-GeoNetOptionalString -Value $sys.Hostname) `
-Network $network
$health = Invoke-GeoNetSafe { Get-GeoNetHealthInfo } -Default @{
bsod_count_30d = 0
pending_reboot = $false
last_patch_installed = $null
}
$peripherals = @(Invoke-GeoNetSafe { @(Get-GeoNetPeripheralsInfo) } -Default @())
$software = @()
if ($IncludeSoftware) { if ($IncludeSoftware) {
Write-Host "Scanning software registry (max $MaxSoftware)..." -ForegroundColor Gray Write-Host "Scanning software registry (max $MaxSoftware)..." -ForegroundColor Gray
$software = @(Get-GeoNetSoftwareInfo -MaxItems $MaxSoftware)
} }
$collectedAt = Format-GeoNetIsoDateTime -Value (Get-Date) $payload = Build-GeoNetReportPayload `
-AgentVersion $AgentVersion `
$payload = [ordered]@{ -SchemaVersion $SchemaVersion `
schema_version = $SchemaVersion -CollectMode 'phase0-local' `
agent_version = $AgentVersion -IncludeSoftware:$IncludeSoftware `
collected_at = $collectedAt -MaxSoftware $MaxSoftware
hostname = (Convert-GeoNetOptionalString -Value $sys.Hostname)
machine_id = (Convert-GeoNetOptionalString -Value $sys.MachineId)
smbios_uuid = (Convert-GeoNetOptionalString -Value $sys.SmbiosUuid)
serial_number = (Convert-GeoNetOptionalString -Value $sys.SerialNumber)
asset_type = 'endpoint'
logged_in_user = (Convert-GeoNetOptionalString -Value $sys.LoggedInUser)
system = [ordered]@{
manufacturer = (Convert-GeoNetOptionalString -Value $sys.Manufacturer)
model = (Convert-GeoNetOptionalString -Value $sys.Model)
bios_version = (Convert-GeoNetOptionalString -Value $sys.BiosVersion)
os_name = (Convert-GeoNetOptionalString -Value $sys.OsName)
os_version = (Convert-GeoNetOptionalString -Value $sys.OsVersion)
os_build = (Convert-GeoNetOptionalString -Value $sys.OsBuild)
cpu_model = (Convert-GeoNetOptionalString -Value $sys.CpuModel)
ram_gb = $sys.RamGb
gpu_model = (Convert-GeoNetOptionalString -Value $sys.GpuModel)
last_boot_at = $sys.LastBootAt
}
gpus = @($sys.Gpus | ForEach-Object {
[ordered]@{
name = $_.name
dedicated_memory_mb = $_.dedicated_memory_mb
}
})
disks = @($disks | ForEach-Object {
[ordered]@{
drive_letter = $_.drive_letter
total_gb = $_.total_gb
free_gb = $_.free_gb
pct_used = $_.pct_used
file_system = $_.file_system
model = $_.model
}
})
memory_modules = @($memory.Modules | ForEach-Object {
[ordered]@{
slot = $_.slot
bank_label = $_.bank_label
capacity_gb = $_.capacity_gb
speed_mhz = $_.speed_mhz
manufacturer = $_.manufacturer
part_number = $_.part_number
form_factor = $_.form_factor
memory_type = $_.memory_type
}
})
memory_summary = if ($memory.Summary) {
[ordered]@{
total_gb = $memory.Summary.total_gb
module_count = $memory.Summary.module_count
slot_count = $memory.Summary.slot_count
empty_slots = $memory.Summary.empty_slots
layout = $memory.Summary.layout
configuration = $memory.Summary.configuration
upgradeable = $memory.Summary.upgradeable
}
} else {
$null
}
network = @($network | ForEach-Object {
[ordered]@{
adapter_name = $_.adapter_name
mac_address = $_.mac_address
ip_address = $_.ip_address
adapter_kind = $_.adapter_kind
status = $_.status
is_primary = $_.is_primary
is_built_in = $_.is_built_in
}
})
asset_identity = [ordered]@{
primary_physical_mac = (Convert-GeoNetOptionalString -Value $identity.PrimaryPhysicalMac)
primary_physical_adapter = (Convert-GeoNetOptionalString -Value $identity.PrimaryPhysicalAdapter)
primary_mac_built_in = $identity.PrimaryMacBuiltIn
primary_mac_source = (Convert-GeoNetOptionalString -Value $identity.PrimaryMacSource)
merge_key = (Convert-GeoNetOptionalString -Value $identity.MergeKey)
recommended_match_order = $identity.RecommendedMatchOrder
}
peripherals = @($peripherals | ForEach-Object {
[ordered]@{
name = $_.name
device_class = $_.device_class
manufacturer = $_.manufacturer
connection = $_.connection
device_role = $_.device_role
vid = $_.vid
pid = $_.pid
pnp_device_id = $_.pnp_device_id
device_serial = $_.device_serial
edid_product = $_.edid_product
resolution = $_.resolution
resolution_width = $_.resolution_width
resolution_height = $_.resolution_height
refresh_hz = $_.refresh_hz
status = $_.status
is_external = $_.is_external
}
})
health = [ordered]@{
bsod_count_30d = $health.bsod_count_30d
pending_reboot = $health.pending_reboot
last_patch_installed = $health.last_patch_installed
}
software = $software
metadata = [ordered]@{
collect_mode = 'phase0-local'
software_included = [bool]$IncludeSoftware
software_count = $software.Count
}
}
$safeHost = ((Convert-GeoNetOptionalString -Value $sys.Hostname) -replace '[^\w\-]', '_') $safeHost = ((Convert-GeoNetOptionalString -Value $payload.hostname) -replace '[^\w\-]', '_')
if (-not $safeHost) { $safeHost = 'UNKNOWN' } if (-not $safeHost) { $safeHost = 'UNKNOWN' }
$timestamp = (Get-Date).ToString('yyyyMMdd_HHmmss') $timestamp = (Get-Date).ToString('yyyyMMdd_HHmmss')
$fileName = "${safeHost}_${timestamp}.json" $fileName = "${safeHost}_${timestamp}.json"
$outputPath = Join-Path $OutputDir $fileName $outputPath = Join-Path $OutputDir $fileName
$json = $payload | ConvertTo-Json -Depth 6 $json = $payload | ConvertTo-Json -Depth 8
[System.IO.File]::WriteAllText($outputPath, $json, [System.Text.UTF8Encoding]::new($false)) [System.IO.File]::WriteAllText($outputPath, $json, [System.Text.UTF8Encoding]::new($false))
Write-Host "Disimpan: $outputPath" -ForegroundColor Green Write-Host "Disimpan: $outputPath" -ForegroundColor Green

5
GeoNetAgent/agent/Install-GeoNetAgent.ps1

@ -3,7 +3,7 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[string]$BaseUrl = 'https://agent.gisportal.id/install', [string]$BaseUrl = 'https://agent.gisportal.id/install',
[string]$InstallRoot = (Join-Path $env:ProgramData 'GeoNetAgent'), [string]$InstallRoot,
[string]$AgentToken, [string]$AgentToken,
[string]$SourcePath, [string]$SourcePath,
[switch]$SkipScheduledTask, [switch]$SkipScheduledTask,
@ -12,6 +12,9 @@ param(
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
if (-not $InstallRoot) {
$InstallRoot = Join-Path ([Environment]::GetFolderPath('CommonApplicationData')) 'GeoNetAgent'
}
$core = Join-Path $PSScriptRoot 'lib\Agent-InstallCore.ps1' $core = Join-Path $PSScriptRoot 'lib\Agent-InstallCore.ps1'
if (-not (Test-Path $core)) { if (-not (Test-Path $core)) {
$core = Join-Path $InstallRoot 'lib\Agent-InstallCore.ps1' $core = Join-Path $InstallRoot 'lib\Agent-InstallCore.ps1'

11
GeoNetAgent/agent/README.md

@ -57,7 +57,16 @@ bash infra/collector/publish_agent_install.sh
## Keamanan ## Keamanan
- Token di `config.json` install = site agent token (bukan kredensial admin). - Token di `config.json` install = site agent token (bukan kredensial admin).
- Path `/install/` nginx: `ssi off` (agar file .ps1 dengan `<#` tidak terpotong). - Path `/install/` nginx: `ssi off`; **jangan** pakai literal `$env:` di file `.ps1` yang di-serve (nginx bisa memotong respons).
## Versi agent (ringkas)
| Versi | Highlight |
|-------|-----------|
| v0.1.10 | `cpu{}` detail (cores, clock, cache, arch); `system.cpu_model` tetap |
| v0.1.9 | `physical_disks[]`, DDR label, GPU driver/resolusi, `form_factor` Laptop/PC |
| v0.1.8 | `gpus[]`, disk model, auto-update, interval task 3 jam |
| v0.1.6 | Peripherals USB/BT/audio, monitor EDID |
## GPO (opsional) ## GPO (opsional)

92
GeoNetAgent/agent/Send-GeoNetReport.ps1

@ -1,10 +1,6 @@
<# <#
.SYNOPSIS .SYNOPSIS
Kumpulkan inventaris endpoint dan POST ke GeoNetAgent Collector API. Kumpulkan inventaris endpoint dan POST ke GeoNetAgent Collector API.
.EXAMPLE
.\Send-GeoNetReport.ps1 -ConfigPath .\config.example.json
.\Send-GeoNetReport.ps1 -CollectorUrl http://localhost:8000/api/v1/agent/report -AgentToken dev-agent-token-change-me
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(
@ -22,14 +18,17 @@ $libPath = Join-Path $scriptRoot 'lib'
. (Join-Path $libPath 'Format-IsoDateTime.ps1') . (Join-Path $libPath 'Format-IsoDateTime.ps1')
. (Join-Path $libPath 'Format-Text.ps1') . (Join-Path $libPath 'Format-Text.ps1')
. (Join-Path $libPath 'Get-SystemInfo.ps1') . (Join-Path $libPath 'Get-SystemInfo.ps1')
. (Join-Path $libPath 'Get-CpuInfo.ps1')
. (Join-Path $libPath 'Get-GpuInfo.ps1') . (Join-Path $libPath 'Get-GpuInfo.ps1')
. (Join-Path $libPath 'Get-DiskInfo.ps1') . (Join-Path $libPath 'Get-DiskInfo.ps1')
. (Join-Path $libPath 'Get-PhysicalDiskInfo.ps1')
. (Join-Path $libPath 'Get-MemoryInfo.ps1') . (Join-Path $libPath 'Get-MemoryInfo.ps1')
. (Join-Path $libPath 'Get-NetworkInfo.ps1') . (Join-Path $libPath 'Get-NetworkInfo.ps1')
. (Join-Path $libPath 'Get-AssetIdentity.ps1') . (Join-Path $libPath 'Get-AssetIdentity.ps1')
. (Join-Path $libPath 'Get-HealthInfo.ps1') . (Join-Path $libPath 'Get-HealthInfo.ps1')
. (Join-Path $libPath 'Get-PeripheralsInfo.ps1') . (Join-Path $libPath 'Get-PeripheralsInfo.ps1')
. (Join-Path $libPath 'Get-SoftwareInfo.ps1') . (Join-Path $libPath 'Get-SoftwareInfo.ps1')
. (Join-Path $libPath 'Build-GeoNetReportPayload.ps1')
. (Join-Path $libPath 'Send-CollectorReport.ps1') . (Join-Path $libPath 'Send-CollectorReport.ps1')
if (-not $ConfigPath) { if (-not $ConfigPath) {
@ -54,86 +53,15 @@ if (-not $CollectorUrl -or -not $AgentToken) {
throw "CollectorUrl dan AgentToken wajib (parameter atau config.json)." throw "CollectorUrl dan AgentToken wajib (parameter atau config.json)."
} }
# Build payload (same as Collect-GeoNetReport) $AgentVersion = '0.1.10-phase0'
$AgentVersion = '0.1.8-phase0'
$SchemaVersion = '1.0' $SchemaVersion = '1.0'
$sys = Get-GeoNetSystemInfo $payload = Build-GeoNetReportPayload `
$disks = @(Invoke-GeoNetSafe { @(Get-GeoNetDiskInfo) } -Default @()) -AgentVersion $AgentVersion `
$memory = Invoke-GeoNetSafe { Get-GeoNetMemoryInfo } -Default @{ Modules = @(); Summary = $null } -SchemaVersion $SchemaVersion `
$network = @(Invoke-GeoNetSafe { @(Get-GeoNetNetworkInfo) } -Default @()) -CollectMode 'agent-upload' `
$identity = Get-GeoNetAssetIdentity ` -IncludeSoftware:$IncludeSoftware `
-SerialNumber (Convert-GeoNetOptionalString -Value $sys.SerialNumber) ` -MaxSoftware $MaxSoftware
-SmbiosUuid (Convert-GeoNetOptionalString -Value $sys.SmbiosUuid) `
-MachineId (Convert-GeoNetOptionalString -Value $sys.MachineId) `
-Hostname (Convert-GeoNetOptionalString -Value $sys.Hostname) `
-Network $network
$health = Invoke-GeoNetSafe { Get-GeoNetHealthInfo } -Default @{
bsod_count_30d = 0
pending_reboot = $false
last_patch_installed = $null
}
$peripherals = @(Invoke-GeoNetSafe { @(Get-GeoNetPeripheralsInfo) } -Default @())
$software = @()
if ($IncludeSoftware) {
$software = @(Get-GeoNetSoftwareInfo -MaxItems $MaxSoftware)
}
$collectedAt = Format-GeoNetIsoDateTime -Value (Get-Date)
$payload = [ordered]@{
schema_version = $SchemaVersion
agent_version = $AgentVersion
collected_at = $collectedAt
hostname = (Convert-GeoNetOptionalString -Value $sys.Hostname)
serial_number = (Convert-GeoNetOptionalString -Value $sys.SerialNumber)
machine_id = (Convert-GeoNetOptionalString -Value $sys.MachineId)
smbios_uuid = (Convert-GeoNetOptionalString -Value $sys.SmbiosUuid)
asset_type = 'endpoint'
logged_in_user = (Convert-GeoNetOptionalString -Value $sys.LoggedInUser)
system = [ordered]@{
manufacturer = (Convert-GeoNetOptionalString -Value $sys.Manufacturer)
model = (Convert-GeoNetOptionalString -Value $sys.Model)
bios_version = (Convert-GeoNetOptionalString -Value $sys.BiosVersion)
os_name = (Convert-GeoNetOptionalString -Value $sys.OsName)
os_version = (Convert-GeoNetOptionalString -Value $sys.OsVersion)
os_build = (Convert-GeoNetOptionalString -Value $sys.OsBuild)
cpu_model = (Convert-GeoNetOptionalString -Value $sys.CpuModel)
ram_gb = $sys.RamGb
gpu_model = (Convert-GeoNetOptionalString -Value $sys.GpuModel)
last_boot_at = $sys.LastBootAt
}
gpus = @($sys.Gpus | ForEach-Object {
[ordered]@{
name = $_.name
dedicated_memory_mb = $_.dedicated_memory_mb
}
})
disks = $disks
memory_modules = @($memory.Modules)
memory_summary = $memory.Summary
network = $network
asset_identity = [ordered]@{
primary_physical_mac = (Convert-GeoNetOptionalString -Value $identity.PrimaryPhysicalMac)
primary_physical_adapter = (Convert-GeoNetOptionalString -Value $identity.PrimaryPhysicalAdapter)
primary_mac_built_in = $identity.PrimaryMacBuiltIn
primary_mac_source = (Convert-GeoNetOptionalString -Value $identity.PrimaryMacSource)
merge_key = (Convert-GeoNetOptionalString -Value $identity.MergeKey)
recommended_match_order = $identity.RecommendedMatchOrder
}
peripherals = $peripherals
health = [ordered]@{
bsod_count_30d = $health.bsod_count_30d
pending_reboot = $health.pending_reboot
last_patch_installed = $health.last_patch_installed
}
software = $software
metadata = [ordered]@{
collect_mode = 'agent-upload'
software_included = [bool]$IncludeSoftware
software_count = $software.Count
}
}
Write-Host "Mengirim laporan ke $CollectorUrl ..." -ForegroundColor Cyan Write-Host "Mengirim laporan ke $CollectorUrl ..." -ForegroundColor Cyan
$result = Send-GeoNetCollectorReport -Payload $payload -CollectorUrl $CollectorUrl -AgentToken $AgentToken $result = Send-GeoNetCollectorReport -Payload $payload -CollectorUrl $CollectorUrl -AgentToken $AgentToken

2
GeoNetAgent/agent/UserAgent.ps1

@ -8,7 +8,7 @@
config.json lokal dipertahankan saat auto-update. config.json lokal dipertahankan saat auto-update.
.EXAMPLE .EXAMPLE
powershell -NoProfile -ExecutionPolicy Bypass -File "$env:ProgramData\GeoNetAgent\UserAgent.ps1" powershell -NoProfile -ExecutionPolicy Bypass -File "%ProgramData%\GeoNetAgent\UserAgent.ps1"
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(

2
GeoNetAgent/agent/VERSION

@ -1 +1 @@
0.1.8-phase0 0.1.10-phase0

3
GeoNetAgent/agent/install/GeoNetAgent-Install.ps1

@ -22,7 +22,8 @@ function Save-GeoNetInstallFile {
} }
} }
$stage = Join-Path $env:TEMP 'GeoNetAgent-install' $tempRoot = [System.IO.Path]::GetTempPath().TrimEnd('\')
$stage = Join-Path $tempRoot 'GeoNetAgent-install'
$installer = Join-Path $stage 'Install-GeoNetAgent.ps1' $installer = Join-Path $stage 'Install-GeoNetAgent.ps1'
$coreLib = Join-Path $stage 'lib\Agent-InstallCore.ps1' $coreLib = Join-Path $stage 'lib\Agent-InstallCore.ps1'

3
GeoNetAgent/agent/install/bootstrap.ps1

@ -25,7 +25,8 @@ function Save-GeoNetInstallFile {
} }
} }
$stage = Join-Path $env:TEMP 'GeoNetAgent-install' $tempRoot = [System.IO.Path]::GetTempPath().TrimEnd('\')
$stage = Join-Path $tempRoot 'GeoNetAgent-install'
$installer = Join-Path $stage 'Install-GeoNetAgent.ps1' $installer = Join-Path $stage 'Install-GeoNetAgent.ps1'
$coreLib = Join-Path $stage 'lib\Agent-InstallCore.ps1' $coreLib = Join-Path $stage 'lib\Agent-InstallCore.ps1'

5
GeoNetAgent/agent/install/manifest.json

@ -1,15 +1,18 @@
{ {
"version": "0.1.8-phase0", "version": "0.1.10-phase0",
"files": [ "files": [
"Send-GeoNetReport.ps1", "Send-GeoNetReport.ps1",
"Collect-GeoNetReport.ps1", "Collect-GeoNetReport.ps1",
"lib/Agent-InstallCore.ps1", "lib/Agent-InstallCore.ps1",
"lib/Update-GeoNetAgent.ps1", "lib/Update-GeoNetAgent.ps1",
"lib/Build-GeoNetReportPayload.ps1",
"lib/Format-IsoDateTime.ps1", "lib/Format-IsoDateTime.ps1",
"lib/Format-Text.ps1", "lib/Format-Text.ps1",
"lib/Get-AssetIdentity.ps1", "lib/Get-AssetIdentity.ps1",
"lib/Get-CpuInfo.ps1",
"lib/Get-DiskInfo.ps1", "lib/Get-DiskInfo.ps1",
"lib/Get-GpuInfo.ps1", "lib/Get-GpuInfo.ps1",
"lib/Get-PhysicalDiskInfo.ps1",
"lib/Get-DisplaySettings.ps1", "lib/Get-DisplaySettings.ps1",
"lib/Get-HealthInfo.ps1", "lib/Get-HealthInfo.ps1",
"lib/Get-MemoryInfo.ps1", "lib/Get-MemoryInfo.ps1",

207
GeoNetAgent/agent/lib/Build-GeoNetReportPayload.ps1

@ -0,0 +1,207 @@
function Build-GeoNetReportPayload {
param(
[Parameter(Mandatory)]
[string]$AgentVersion,
[Parameter(Mandatory)]
[string]$SchemaVersion,
[Parameter(Mandatory)]
[string]$CollectMode,
[switch]$IncludeSoftware,
[int]$MaxSoftware = 100
)
$sys = Get-GeoNetSystemInfo
$disks = @(Invoke-GeoNetSafe { @(Get-GeoNetDiskInfo) } -Default @())
$physicalDisks = @(Invoke-GeoNetSafe { @(Get-GeoNetPhysicalDiskInfo) } -Default @())
$memory = Invoke-GeoNetSafe { Get-GeoNetMemoryInfo } -Default @{ Modules = @(); Summary = $null }
$network = @(Invoke-GeoNetSafe { @(Get-GeoNetNetworkInfo) } -Default @())
$identity = Get-GeoNetAssetIdentity `
-SerialNumber (Convert-GeoNetOptionalString -Value $sys.SerialNumber) `
-SmbiosUuid (Convert-GeoNetOptionalString -Value $sys.SmbiosUuid) `
-MachineId (Convert-GeoNetOptionalString -Value $sys.MachineId) `
-Hostname (Convert-GeoNetOptionalString -Value $sys.Hostname) `
-Network $network
$health = Invoke-GeoNetSafe { Get-GeoNetHealthInfo } -Default @{
bsod_count_30d = 0
pending_reboot = $false
last_patch_installed = $null
}
$peripherals = @(Invoke-GeoNetSafe { @(Get-GeoNetPeripheralsInfo) } -Default @())
$software = @()
if ($IncludeSoftware) {
$software = @(Get-GeoNetSoftwareInfo -MaxItems $MaxSoftware)
}
$collectedAt = Format-GeoNetIsoDateTime -Value (Get-Date)
[ordered]@{
schema_version = $SchemaVersion
agent_version = $AgentVersion
collected_at = $collectedAt
hostname = (Convert-GeoNetOptionalString -Value $sys.Hostname)
machine_id = (Convert-GeoNetOptionalString -Value $sys.MachineId)
smbios_uuid = (Convert-GeoNetOptionalString -Value $sys.SmbiosUuid)
serial_number = (Convert-GeoNetOptionalString -Value $sys.SerialNumber)
asset_type = 'endpoint'
logged_in_user = (Convert-GeoNetOptionalString -Value $sys.LoggedInUser)
system = [ordered]@{
manufacturer = (Convert-GeoNetOptionalString -Value $sys.Manufacturer)
model = (Convert-GeoNetOptionalString -Value $sys.Model)
bios_version = (Convert-GeoNetOptionalString -Value $sys.BiosVersion)
os_name = (Convert-GeoNetOptionalString -Value $sys.OsName)
os_version = (Convert-GeoNetOptionalString -Value $sys.OsVersion)
os_build = (Convert-GeoNetOptionalString -Value $sys.OsBuild)
cpu_model = (Convert-GeoNetOptionalString -Value $sys.CpuModel)
ram_gb = $sys.RamGb
gpu_model = (Convert-GeoNetOptionalString -Value $sys.GpuModel)
last_boot_at = $sys.LastBootAt
form_factor = (Convert-GeoNetOptionalString -Value $sys.FormFactor)
chassis_label = (Convert-GeoNetOptionalString -Value $sys.ChassisLabel)
chassis_types = @($sys.ChassisTypes)
pc_system_type = $sys.PcSystemType
pc_system_label = (Convert-GeoNetOptionalString -Value $sys.PcSystemLabel)
}
cpu = if ($sys.Cpu) {
[ordered]@{
model = $sys.Cpu.model
manufacturer = $sys.Cpu.manufacturer
cores = $sys.Cpu.cores
logical_processors = $sys.Cpu.logical_processors
max_clock_mhz = $sys.Cpu.max_clock_mhz
current_clock_mhz = $sys.Cpu.current_clock_mhz
socket = $sys.Cpu.socket
socket_count = $sys.Cpu.socket_count
l2_cache_kb = $sys.Cpu.l2_cache_kb
l3_cache_kb = $sys.Cpu.l3_cache_kb
architecture = $sys.Cpu.architecture
}
} else {
$null
}
gpus = @($sys.Gpus | ForEach-Object {
[ordered]@{
name = $_.name
adapter_compatibility = $_.adapter_compatibility
video_processor = $_.video_processor
driver_version = $_.driver_version
driver_date = $_.driver_date
dedicated_memory_mb = $_.dedicated_memory_mb
pnp_device_id = $_.pnp_device_id
status = $_.status
current_resolution = $_.current_resolution
video_mode_description = $_.video_mode_description
}
})
disks = @($disks | ForEach-Object {
[ordered]@{
drive_letter = $_.drive_letter
total_gb = $_.total_gb
free_gb = $_.free_gb
pct_used = $_.pct_used
file_system = $_.file_system
model = $_.model
}
})
physical_disks = @($physicalDisks | ForEach-Object {
[ordered]@{
disk_number = $_.disk_number
device_id = $_.device_id
model = $_.model
friendly_name = $_.friendly_name
media_type = $_.media_type
operational_status = $_.operational_status
health_status = $_.health_status
total_gb = $_.total_gb
partition_style = $_.partition_style
bus_type = $_.bus_type
serial_number = $_.serial_number
}
})
memory_modules = @($memory.Modules | ForEach-Object {
[ordered]@{
slot = $_.slot
bank_label = $_.bank_label
capacity_gb = $_.capacity_gb
speed_mhz = $_.speed_mhz
configured_speed_mhz = $_.configured_speed_mhz
manufacturer = $_.manufacturer
part_number = $_.part_number
serial_number = $_.serial_number
form_factor = $_.form_factor
memory_type = $_.memory_type
memory_type_label = $_.memory_type_label
data_width = $_.data_width
total_width = $_.total_width
rank = $_.rank
configured_voltage_mv = $_.configured_voltage_mv
}
})
memory_summary = if ($memory.Summary) {
[ordered]@{
total_gb = $memory.Summary.total_gb
module_count = $memory.Summary.module_count
slot_count = $memory.Summary.slot_count
empty_slots = $memory.Summary.empty_slots
layout = $memory.Summary.layout
configuration = $memory.Summary.configuration
upgradeable = $memory.Summary.upgradeable
ddr_generation = $memory.Summary.ddr_generation
}
} else {
$null
}
network = @($network | ForEach-Object {
[ordered]@{
adapter_name = $_.adapter_name
mac_address = $_.mac_address
ip_address = $_.ip_address
adapter_kind = $_.adapter_kind
status = $_.status
is_primary = $_.is_primary
is_built_in = $_.is_built_in
}
})
asset_identity = [ordered]@{
primary_physical_mac = (Convert-GeoNetOptionalString -Value $identity.PrimaryPhysicalMac)
primary_physical_adapter = (Convert-GeoNetOptionalString -Value $identity.PrimaryPhysicalAdapter)
primary_mac_built_in = $identity.PrimaryMacBuiltIn
primary_mac_source = (Convert-GeoNetOptionalString -Value $identity.PrimaryMacSource)
merge_key = (Convert-GeoNetOptionalString -Value $identity.MergeKey)
recommended_match_order = $identity.RecommendedMatchOrder
}
peripherals = @($peripherals | ForEach-Object {
[ordered]@{
name = $_.name
device_class = $_.device_class
manufacturer = $_.manufacturer
connection = $_.connection
device_role = $_.device_role
vid = $_.vid
pid = $_.pid
pnp_device_id = $_.pnp_device_id
device_serial = $_.device_serial
edid_product = $_.edid_product
resolution = $_.resolution
resolution_width = $_.resolution_width
resolution_height = $_.resolution_height
refresh_hz = $_.refresh_hz
status = $_.status
is_external = $_.is_external
}
})
health = [ordered]@{
bsod_count_30d = $health.bsod_count_30d
pending_reboot = $health.pending_reboot
last_patch_installed = $health.last_patch_installed
}
software = $software
metadata = [ordered]@{
collect_mode = $CollectMode
software_included = [bool]$IncludeSoftware
software_count = $software.Count
}
}
}

60
GeoNetAgent/agent/lib/Get-CpuInfo.ps1

@ -0,0 +1,60 @@
$formatText = Join-Path $PSScriptRoot 'Format-Text.ps1'
if (Test-Path $formatText) { . $formatText }
function Get-GeoNetCpuArchitectureName {
param([uint16]$Code)
switch ($Code) {
0 { return 'x86' }
1 { return 'MIPS' }
2 { return 'Alpha' }
3 { return 'PowerPC' }
5 { return 'ARM' }
6 { return 'ia64' }
9 { return 'x64' }
12 { return 'ARM64' }
default { return "arch_$Code" }
}
}
function Get-GeoNetCpuInfo {
$processors = @(Invoke-GeoNetSafe {
Get-CimInstance Win32_Processor -ErrorAction Stop
} -Default @())
if (-not $processors.Count) { return $null }
$primary = $processors[0]
$cores = [int]($processors | Measure-Object -Property NumberOfCores -Sum).Sum
$logical = [int]($processors | Measure-Object -Property NumberOfLogicalProcessors -Sum).Sum
$maxClock = ($processors | Where-Object { $_.MaxClockSpeed -gt 0 } | Select-Object -ExpandProperty MaxClockSpeed | Measure-Object -Maximum).Maximum
$currentClock = ($processors | Where-Object { $_.CurrentClockSpeed -gt 0 } | Select-Object -ExpandProperty CurrentClockSpeed | Measure-Object -Maximum).Maximum
$l2 = ($processors | Where-Object { $_.L2CacheSize -gt 0 } | Select-Object -ExpandProperty L2CacheSize | Measure-Object -Sum).Sum
$l3 = ($processors | Where-Object { $_.L3CacheSize -gt 0 } | Select-Object -ExpandProperty L3CacheSize | Measure-Object -Sum).Sum
$sockets = @($processors | ForEach-Object {
Convert-GeoNetOptionalString -Value $_.SocketDesignation
} | Where-Object { $_ } | Select-Object -Unique)
$socketLabel = if ($sockets.Count -eq 1) { $sockets[0] }
elseif ($sockets.Count -gt 1) { ($sockets -join ', ') }
else { $null }
$archCode = if ($null -ne $primary.Architecture) { [uint16]$primary.Architecture } else { 0 }
[pscustomobject]@{
model = Convert-GeoNetOptionalString -Value $primary.Name
manufacturer = Convert-GeoNetOptionalString -Value $primary.Manufacturer
cores = if ($cores -gt 0) { $cores } else { $null }
logical_processors = if ($logical -gt 0) { $logical } else { $null }
max_clock_mhz = if ($maxClock -gt 0) { [int]$maxClock } else { $null }
current_clock_mhz = if ($currentClock -gt 0) { [int]$currentClock } else { $null }
socket = $socketLabel
socket_count = $processors.Count
l2_cache_kb = if ($l2 -gt 0) { [int]$l2 } else { $null }
l3_cache_kb = if ($l3 -gt 0) { [int]$l3 } else { $null }
architecture = Get-GeoNetCpuArchitectureName -Code $archCode
}
}

49
GeoNetAgent/agent/lib/Get-GpuInfo.ps1

@ -1,6 +1,9 @@
$formatText = Join-Path $PSScriptRoot 'Format-Text.ps1' $formatText = Join-Path $PSScriptRoot 'Format-Text.ps1'
if (Test-Path $formatText) { . $formatText } if (Test-Path $formatText) { . $formatText }
$isoDate = Join-Path $PSScriptRoot 'Format-IsoDateTime.ps1'
if (Test-Path $isoDate) { . $isoDate }
function Convert-GeoNetAdapterRamMb { function Convert-GeoNetAdapterRamMb {
param([AllowNull()]$AdapterRam) param([AllowNull()]$AdapterRam)
@ -15,6 +18,32 @@ function Convert-GeoNetAdapterRamMb {
} }
} }
function Convert-GeoNetDriverDateIso {
param($DriverDate)
if (-not $DriverDate) { return $null }
try {
return Format-GeoNetIsoDateTime -Value $DriverDate
} catch {
return $null
}
}
function Get-GeoNetGpuVendorFromPnp {
param([string]$PnpDeviceId)
if ($PnpDeviceId -match 'VEN_([0-9A-F]{4})') {
switch ($Matches[1]) {
'8086' { return 'Intel' }
'10DE' { return 'NVIDIA' }
'1002' { return 'AMD' }
'1414' { return 'Microsoft' }
default { return "VEN_$($Matches[1])" }
}
}
return $null
}
function Get-GeoNetGpuInfo { function Get-GeoNetGpuInfo {
$controllers = Invoke-GeoNetSafe { $controllers = Invoke-GeoNetSafe {
Get-CimInstance Win32_VideoController -ErrorAction Stop | Get-CimInstance Win32_VideoController -ErrorAction Stop |
@ -27,9 +56,29 @@ function Get-GeoNetGpuInfo {
$name = Convert-GeoNetOptionalString -Value $_.Name $name = Convert-GeoNetOptionalString -Value $_.Name
if (-not $name) { return } if (-not $name) { return }
$pnp = Convert-GeoNetOptionalString -Value $_.PNPDeviceID
$vendor = Convert-GeoNetOptionalString -Value $_.AdapterCompatibility
if (-not $vendor) { $vendor = Get-GeoNetGpuVendorFromPnp -PnpDeviceId $pnp }
$width = $null
$height = $null
if ($_.CurrentHorizontalResolution -gt 0 -and $_.CurrentVerticalResolution -gt 0) {
$width = [int]$_.CurrentHorizontalResolution
$height = [int]$_.CurrentVerticalResolution
}
$resolution = if ($width -and $height) { "${width}x${height}" } else { $null }
[pscustomobject]@{ [pscustomobject]@{
name = $name name = $name
adapter_compatibility = $vendor
video_processor = Convert-GeoNetOptionalString -Value $_.VideoProcessor
driver_version = Convert-GeoNetOptionalString -Value $_.DriverVersion
driver_date = Convert-GeoNetDriverDateIso -DriverDate $_.DriverDate
dedicated_memory_mb = Convert-GeoNetAdapterRamMb -AdapterRam $_.AdapterRAM dedicated_memory_mb = Convert-GeoNetAdapterRamMb -AdapterRam $_.AdapterRAM
pnp_device_id = $pnp
status = Convert-GeoNetOptionalString -Value $_.Status
current_resolution = $resolution
video_mode_description = Convert-GeoNetOptionalString -Value $_.VideoModeDescription
} }
}) })

44
GeoNetAgent/agent/lib/Get-MemoryInfo.ps1

@ -1,3 +1,29 @@
function Get-GeoNetMemoryTypeLabel {
param(
[uint16]$SmbiosType,
[uint16]$LegacyType = 0
)
switch ($SmbiosType) {
20 { return 'DDR' }
21 { return 'DDR2' }
22 { return 'DDR2 FB-DIMM' }
24 { return 'DDR3' }
26 { return 'DDR4' }
34 { return 'DDR5' }
}
switch ($LegacyType) {
20 { return 'DDR' }
21 { return 'DDR2' }
24 { return 'DDR3' }
}
if ($SmbiosType -gt 0) { return "Type_$SmbiosType" }
if ($LegacyType -gt 0) { return "Legacy_$LegacyType" }
return $null
}
function Get-GeoNetFormFactorName { function Get-GeoNetFormFactorName {
param([uint16]$Code) param([uint16]$Code)
@ -30,15 +56,26 @@ function Get-GeoNetMemoryInfo {
$moduleRows = foreach ($m in $modules) { $moduleRows = foreach ($m in $modules) {
$capacityGb = [math]::Round($m.Capacity / 1GB, 1) $capacityGb = [math]::Round($m.Capacity / 1GB, 1)
$smbiosType = if ($null -ne $m.SMBIOSMemoryType) { [uint16]$m.SMBIOSMemoryType } else { 0 }
$legacyType = if ($null -ne $m.MemoryType) { [uint16]$m.MemoryType } else { 0 }
$configuredSpeed = if ($m.ConfiguredClockSpeed -gt 0) { [int]$m.ConfiguredClockSpeed } elseif ($m.Speed -gt 0) { [int]$m.Speed } else { $null }
[pscustomobject]@{ [pscustomobject]@{
slot = if ($m.DeviceLocator) { $m.DeviceLocator } else { $m.BankLabel } slot = if ($m.DeviceLocator) { $m.DeviceLocator } else { $m.BankLabel }
bank_label = $m.BankLabel bank_label = $m.BankLabel
capacity_gb = $capacityGb capacity_gb = $capacityGb
speed_mhz = $m.Speed speed_mhz = if ($m.Speed -gt 0) { [int]$m.Speed } else { $null }
configured_speed_mhz = $configuredSpeed
manufacturer = ($m.Manufacturer -replace '\s+$', '').Trim() manufacturer = ($m.Manufacturer -replace '\s+$', '').Trim()
part_number = ($m.PartNumber -replace '\s+$', '').Trim() part_number = ($m.PartNumber -replace '\s+$', '').Trim()
serial_number = ($m.SerialNumber -replace '\s+$', '').Trim()
form_factor = Get-GeoNetFormFactorName -Code $m.FormFactor form_factor = Get-GeoNetFormFactorName -Code $m.FormFactor
memory_type = $m.SMBIOSMemoryType memory_type = $m.SMBIOSMemoryType
memory_type_label = Get-GeoNetMemoryTypeLabel -SmbiosType $smbiosType -LegacyType $legacyType
data_width = if ($m.DataWidth -gt 0) { [int]$m.DataWidth } else { $null }
total_width = if ($m.TotalWidth -gt 0) { [int]$m.TotalWidth } else { $null }
rank = if ($null -ne $m.Rank) { [int]$m.Rank } else { $null }
configured_voltage_mv = if ($m.ConfiguredVoltage -gt 0) { [int]$m.ConfiguredVoltage } else { $null }
} }
} }
@ -52,8 +89,10 @@ function Get-GeoNetMemoryInfo {
$layout = ($layoutParts -join ' + ') $layout = ($layoutParts -join ' + ')
$formFactors = @($moduleRows | Select-Object -ExpandProperty form_factor -Unique) $formFactors = @($moduleRows | Select-Object -ExpandProperty form_factor -Unique)
$ddrTypes = @($moduleRows | Select-Object -ExpandProperty memory_type_label -Unique | Where-Object { $_ -and $_ -notmatch '^(Type_|Legacy_)' })
$configuration = if ($layout) { $configuration = if ($layout) {
"$layout $(($formFactors -join '/'))".Trim() $ddrPart = if ($ddrTypes.Count) { ($ddrTypes -join '/') } else { '' }
"$layout $(($formFactors -join '/')) $ddrPart".Trim()
} else { $null } } else { $null }
$upgradeable = if ($null -ne $emptySlots) { $upgradeable = if ($null -ne $emptySlots) {
@ -72,6 +111,7 @@ function Get-GeoNetMemoryInfo {
layout = $layout layout = $layout
configuration = $configuration configuration = $configuration
upgradeable = $upgradeable upgradeable = $upgradeable
ddr_generation = if ($ddrTypes.Count -eq 1) { $ddrTypes[0] } elseif ($ddrTypes.Count -gt 1) { ($ddrTypes -join '/') } else { $null }
} }
[pscustomobject]@{ [pscustomobject]@{

100
GeoNetAgent/agent/lib/Get-PhysicalDiskInfo.ps1

@ -0,0 +1,100 @@
$formatText = Join-Path $PSScriptRoot 'Format-Text.ps1'
if (Test-Path $formatText) { . $formatText }
function Convert-GeoNetMediaTypeName {
param([object]$Value)
if ($null -eq $Value) { return $null }
$text = $Value.ToString()
switch ($text) {
'SSD' { return 'SSD' }
'HDD' { return 'HDD' }
'SCM' { return 'SCM' }
'Unspecified' { return $null }
default {
if ($Value -is [int] -or $Value -is [uint32]) {
switch ([int]$Value) {
3 { return 'HDD' }
4 { return 'SSD' }
5 { return 'SCM' }
}
}
return $text
}
}
}
function Convert-GeoNetBusTypeName {
param([object]$Value)
if ($null -eq $Value) { return $null }
$text = $Value.ToString()
switch ($text) {
'NVMe' { return 'NVMe' }
'SATA' { return 'SATA' }
'SAS' { return 'SAS' }
'USB' { return 'USB' }
'RAID' { return 'RAID' }
default {
if ($Value -is [int] -or $Value -is [uint32]) {
switch ([int]$Value) {
7 { return 'USB' }
11 { return 'NVMe' }
17 { return 'NVMe' }
3 { return 'SATA' }
}
}
return $text
}
}
}
function Convert-GeoNetPartitionStyleName {
param([object]$Value)
if ($null -eq $Value) { return $null }
$text = $Value.ToString()
switch ($text) {
'MBR' { return 'MBR' }
'GPT' { return 'GPT' }
default {
if ($Value -is [int] -or $Value -is [uint32]) {
switch ([int]$Value) {
1 { return 'MBR' }
2 { return 'GPT' }
}
}
return $text
}
}
}
function Get-GeoNetPhysicalDiskInfo {
Invoke-GeoNetSafe {
$physical = @(Get-PhysicalDisk -ErrorAction Stop)
$logical = @(Get-Disk -ErrorAction Stop)
$rows = @()
foreach ($pd in $physical) {
$disk = $logical | Where-Object { $_.Number -eq $pd.DeviceId } | Select-Object -First 1
$totalGb = if ($pd.Size -gt 0) { [math]::Round($pd.Size / 1GB, 1) } else { $null }
$rows += [pscustomobject]@{
disk_number = [int]$pd.DeviceId
device_id = [string]$pd.DeviceId
model = Convert-GeoNetOptionalString -Value $pd.Model
friendly_name = Convert-GeoNetOptionalString -Value $(if ($disk) { $disk.FriendlyName } else { $pd.FriendlyName })
media_type = Convert-GeoNetMediaTypeName -Value $pd.MediaType
operational_status = Convert-GeoNetOptionalString -Value $pd.OperationalStatus
health_status = Convert-GeoNetOptionalString -Value $pd.HealthStatus
total_gb = $totalGb
partition_style = Convert-GeoNetPartitionStyleName -Value $(if ($disk) { $disk.PartitionStyle } else { $null })
bus_type = Convert-GeoNetBusTypeName -Value $pd.BusType
serial_number = Convert-GeoNetOptionalString -Value $pd.SerialNumber
unique_id = Convert-GeoNetOptionalString -Value $pd.UniqueId
}
}
@($rows | Sort-Object disk_number)
} -Default @()
}

93
GeoNetAgent/agent/lib/Get-SystemInfo.ps1

@ -4,6 +4,9 @@ if (Test-Path $formatText) { . $formatText }
$gpuInfo = Join-Path $PSScriptRoot 'Get-GpuInfo.ps1' $gpuInfo = Join-Path $PSScriptRoot 'Get-GpuInfo.ps1'
if (Test-Path $gpuInfo) { . $gpuInfo } if (Test-Path $gpuInfo) { . $gpuInfo }
$cpuInfo = Join-Path $PSScriptRoot 'Get-CpuInfo.ps1'
if (Test-Path $cpuInfo) { . $cpuInfo }
function Test-GeoNetValidUuid { function Test-GeoNetValidUuid {
param($Uuid) param($Uuid)
@ -18,8 +21,68 @@ function Test-GeoNetValidUuid {
return ($invalid -notcontains $normalized) return ($invalid -notcontains $normalized)
} }
function Get-GeoNetChassisTypeName {
param([uint16]$Code)
switch ($Code) {
3 { return 'Desktop' }
4 { return 'Low Profile Desktop' }
5 { return 'Pizza Box' }
6 { return 'Mini Tower' }
7 { return 'Tower' }
8 { return 'Portable' }
9 { return 'Laptop' }
10 { return 'Notebook' }
11 { return 'Hand Held' }
12 { return 'Docking Station' }
13 { return 'All in One' }
14 { return 'Sub Notebook' }
30 { return 'Tablet' }
31 { return 'Convertible' }
32 { return 'Detachable' }
default { return "Chassis_$Code" }
}
}
function Get-GeoNetPcSystemTypeName {
param([uint16]$Code)
switch ($Code) {
1 { return 'Desktop' }
2 { return 'Mobile' }
3 { return 'Workstation' }
4 { return 'Enterprise Server' }
5 { return 'SOHO Server' }
6 { return 'Appliance PC' }
7 { return 'Performance Server' }
8 { return 'Maximum' }
default { return "PCSystemType_$Code" }
}
}
function Get-GeoNetDeviceFormFactor {
param(
[uint16[]]$ChassisTypes,
[uint16]$PcSystemType
)
$portableCodes = @(8, 9, 10, 11, 14, 30, 31, 32)
$desktopCodes = @(3, 4, 5, 6, 7, 13, 15, 16, 17, 24)
foreach ($code in @($ChassisTypes)) {
if ($portableCodes -contains $code) { return 'laptop' }
}
foreach ($code in @($ChassisTypes)) {
if ($desktopCodes -contains $code) { return 'desktop' }
}
if ($PcSystemType -eq 2) { return 'laptop' }
if ($PcSystemType -eq 1) { return 'desktop' }
if ($PcSystemType -in 4, 5, 7) { return 'server' }
return 'other'
}
function Get-GeoNetSystemInfo { function Get-GeoNetSystemInfo {
$hostname = Convert-GeoNetOptionalString -Value $env:COMPUTERNAME $hostname = Convert-GeoNetOptionalString -Value ([Environment]::MachineName)
if (-not $hostname) { if (-not $hostname) {
$hostname = Convert-GeoNetOptionalString -Value ([Environment]::MachineName) $hostname = Convert-GeoNetOptionalString -Value ([Environment]::MachineName)
} }
@ -29,7 +92,7 @@ function Get-GeoNetSystemInfo {
$cs = Invoke-GeoNetSafe { Get-CimInstance Win32_ComputerSystem -ErrorAction Stop } $cs = Invoke-GeoNetSafe { Get-CimInstance Win32_ComputerSystem -ErrorAction Stop }
$os = Invoke-GeoNetSafe { Get-CimInstance Win32_OperatingSystem -ErrorAction Stop } $os = Invoke-GeoNetSafe { Get-CimInstance Win32_OperatingSystem -ErrorAction Stop }
$cpu = Invoke-GeoNetSafe { Get-CimInstance Win32_Processor -ErrorAction Stop | Select-Object -First 1 } $cpu = Invoke-GeoNetSafe { Get-GeoNetCpuInfo }
$bios = Invoke-GeoNetSafe { Get-CimInstance Win32_BIOS -ErrorAction Stop } $bios = Invoke-GeoNetSafe { Get-CimInstance Win32_BIOS -ErrorAction Stop }
$enclosure = Invoke-GeoNetSafe { Get-CimInstance Win32_SystemEnclosure -ErrorAction Stop } $enclosure = Invoke-GeoNetSafe { Get-CimInstance Win32_SystemEnclosure -ErrorAction Stop }
$product = Invoke-GeoNetSafe { Get-CimInstance Win32_ComputerSystemProduct -ErrorAction Stop } $product = Invoke-GeoNetSafe { Get-CimInstance Win32_ComputerSystemProduct -ErrorAction Stop }
@ -69,7 +132,7 @@ function Get-GeoNetSystemInfo {
$lastBootAt = Invoke-GeoNetSafe { Format-GeoNetIsoDateTime -Value $os.LastBootUpTime } $lastBootAt = Invoke-GeoNetSafe { Format-GeoNetIsoDateTime -Value $os.LastBootUpTime }
} }
$loggedInUser = Convert-GeoNetOptionalString -Value ($env:USERDOMAIN + '\' + $env:USERNAME) $loggedInUser = Convert-GeoNetOptionalString -Value ([Environment]::UserDomainName + '\' + [Environment]::UserName)
if ($cs -and $cs.UserName) { if ($cs -and $cs.UserName) {
$loggedInUser = Convert-GeoNetOptionalString -Value $cs.UserName $loggedInUser = Convert-GeoNetOptionalString -Value $cs.UserName
} }
@ -83,6 +146,22 @@ function Get-GeoNetSystemInfo {
} }
} }
$chassisTypes = @()
$chassisLabel = $null
$pcSystemType = $null
$pcSystemLabel = $null
$formFactor = $null
if ($enclosure -and $enclosure.ChassisTypes) {
$chassisTypes = @($enclosure.ChassisTypes | ForEach-Object { [uint16]$_ })
$chassisLabel = (Get-GeoNetChassisTypeName -Code $chassisTypes[0])
}
if ($cs -and $null -ne $cs.PCSystemType) {
$pcSystemType = [uint16]$cs.PCSystemType
$pcSystemLabel = Get-GeoNetPcSystemTypeName -Code $pcSystemType
}
$formFactor = Get-GeoNetDeviceFormFactor -ChassisTypes $chassisTypes -PcSystemType $pcSystemType
[pscustomobject]@{ [pscustomobject]@{
Hostname = $hostname Hostname = $hostname
MachineId = $machineId MachineId = $machineId
@ -95,10 +174,16 @@ function Get-GeoNetSystemInfo {
OsName = Convert-GeoNetOptionalString -Value $os.Caption OsName = Convert-GeoNetOptionalString -Value $os.Caption
OsVersion = $osVersion OsVersion = $osVersion
OsBuild = $osBuild OsBuild = $osBuild
CpuModel = Convert-GeoNetOptionalString -Value $cpu.Name CpuModel = Convert-GeoNetOptionalString -Value $(if ($cpu) { $cpu.model } else { $null })
Cpu = $cpu
RamGb = $ramGb RamGb = $ramGb
GpuModel = Convert-GeoNetOptionalString -Value $gpu.name GpuModel = Convert-GeoNetOptionalString -Value $gpu.name
Gpus = $gpus Gpus = $gpus
LastBootAt = $lastBootAt LastBootAt = $lastBootAt
ChassisTypes = $chassisTypes
ChassisLabel = $chassisLabel
PcSystemType = $pcSystemType
PcSystemLabel = $pcSystemLabel
FormFactor = $formFactor
} }
} }

2
GeoNetAgent/collector/docs/API.md

@ -88,6 +88,8 @@ Menerima inventaris endpoint (hardware, disk, network, software, dll.) dan menyi
| `machine_id` | 4 | | `machine_id` | 4 |
| `hostname` | 5 | | `hostname` | 5 |
Match via `machine_id` atau `hostname` **ditolak** jika `smbios_uuid` / `serial_number` hardware bertentangan (skenario disk clone ke mesin lain). Hostname bentrok → asset baru dengan suffix SMBIOS; `display_name` menyimpan hostname asli agent.
Lihat [ADR 001](../../.cursor/adr/001-agent-asset-identity.md). Lihat [ADR 001](../../.cursor/adr/001-agent-asset-identity.md).
#### Response sukses — baru #### Response sukses — baru

24
GeoNetAgent/collector/scripts/run_split.py

@ -0,0 +1,24 @@
import sys
import uuid
from app.db.session import SessionLocal
from app.services.asset_split_service import split_asset_by_hardware
asset_id = uuid.UUID(sys.argv[1])
dry_run = "--dry-run" in sys.argv
db = SessionLocal()
try:
result = split_asset_by_hardware(db, asset_id, dry_run=dry_run)
finally:
db.close()
mode = "DRY-RUN" if result.dry_run else "APPLIED"
print(f"[{mode}] source asset {result.source_asset_id}")
print(f" primary: {result.primary_fingerprint}")
for group in result.groups:
marker = " *" if group.fingerprint == result.primary_fingerprint else ""
print(f" - {group.fingerprint}{marker}: {len(group.snapshot_ids)} · {group.label}")
for created in result.created_assets:
print(f" + {created['hostname']} ({created['display_name']}) ← {created['snapshot_count']}")
print(f" moved: {result.moved_snapshots}")

50
GeoNetAgent/collector/scripts/split_asset_by_hardware.py

@ -0,0 +1,50 @@
"""CLI: split merged asset snapshots by hardware profile."""
from __future__ import annotations
import argparse
import sys
import uuid
from app.db.session import SessionLocal
from app.services.asset_split_service import split_asset_by_hardware
def main() -> int:
parser = argparse.ArgumentParser(description="Split asset snapshots by hardware profile (SMBIOS / model).")
parser.add_argument("asset_id", help="Asset UUID to split")
parser.add_argument("--dry-run", action="store_true", help="Show plan only; do not write DB")
args = parser.parse_args()
try:
asset_uuid = uuid.UUID(args.asset_id)
except ValueError:
print(f"Invalid asset_id: {args.asset_id}", file=sys.stderr)
return 1
db = SessionLocal()
try:
result = split_asset_by_hardware(db, asset_uuid, dry_run=args.dry_run)
except ValueError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 1
finally:
db.close()
mode = "DRY-RUN" if result.dry_run else "APPLIED"
print(f"[{mode}] source asset {result.source_asset_id}")
print(f" hardware groups: {len(result.groups)} (primary: {result.primary_fingerprint})")
for group in result.groups:
marker = " *" if group.fingerprint == result.primary_fingerprint else ""
print(f" - {group.fingerprint}{marker}: {len(group.snapshot_ids)} snapshots · {group.label}")
for created in result.created_assets:
print(
f" + new asset {created['hostname']} ({created['display_name']}) "
f"{created['snapshot_count']} snapshots"
)
print(f" moved snapshots: {result.moved_snapshots}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

61
GeoNetAgent/collector/tests/test_asset_identity.py

@ -0,0 +1,61 @@
from unittest.mock import MagicMock
from app.schemas.agent_report import AgentReportRequest, HealthInfo, SystemInfo
from app.services.asset_identity import (
allocate_hostname,
hardware_identity_compatible,
normalize_smbios_uuid,
)
def _report(**overrides):
base = {
"schema_version": "1.0",
"agent_version": "0.1.8-phase0",
"collected_at": "2026-06-22T12:00:00+07:00",
"hostname": "DESKTOP-TEST",
"system": SystemInfo(),
"health": HealthInfo(),
}
base.update(overrides)
return AgentReportRequest(**base)
def test_normalize_smbios_uuid_strips_braces_and_uppercases():
assert normalize_smbios_uuid("{03d502e0-045e-0550-8e06-ba0700080009}") == "03D502E0-045E-0550-8E06-BA0700080009"
def test_hardware_compatible_when_smbios_matches():
asset = MagicMock(smbios_uuid="AAAA-BBBB", serial_number=None)
report = _report(smbios_uuid="{aaaa-bbbb}")
assert hardware_identity_compatible(asset, report) is True
def test_hardware_incompatible_when_smbios_differs():
asset = MagicMock(
smbios_uuid="4C4C4544-004C-5610-805A-B2C04F4A3432",
serial_number=None,
)
report = _report(smbios_uuid="03D502E0-045E-0550-8E06-BA0700080009")
assert hardware_identity_compatible(asset, report) is False
def test_hardware_compatible_when_asset_has_no_smbios_yet():
asset = MagicMock(smbios_uuid=None, serial_number=None)
report = _report(smbios_uuid="03D502E0-045E-0550-8E06-BA0700080009")
assert hardware_identity_compatible(asset, report) is True
def test_allocate_hostname_uses_smbios_suffix_on_collision():
db = MagicMock()
db.scalar.side_effect = [object(), None]
hostname, display_name = allocate_hostname(
db,
_report(
hostname="DESKTOP-TEST",
smbios_uuid="03D502E0-045E-0550-8E06-BA0700080009",
),
)
assert hostname == "DESKTOP-TEST-03D502E0"
assert display_name == "DESKTOP-TEST"

19
GeoNetAgent/collector/tests/test_asset_split.py

@ -0,0 +1,19 @@
from app.services.asset_split_service import hardware_fingerprint, hardware_label
def test_hardware_fingerprint_prefers_smbios():
payload = {
"smbios_uuid": "{4c4c4544-004c-5610-805a-b2c04f4a3432}",
"system": {"manufacturer": "Dell Inc.", "model": "Inspiron 7447"},
}
assert hardware_fingerprint(payload) == "smbios:4C4C4544-004C-5610-805A-B2C04F4A3432"
def test_hardware_fingerprint_falls_back_to_product():
payload = {"system": {"manufacturer": "Dell Inc.", "model": "Inspiron 7447"}}
assert hardware_fingerprint(payload) == "product:Dell Inc.|Inspiron 7447"
def test_hardware_label():
payload = {"system": {"manufacturer": "Dell Inc.", "model": "Inspiron 7447"}}
assert hardware_label(payload) == "Dell Inc. Inspiron 7447"

69
GeoNetAgent/docs/adr/ADR-001-agent-asset-identity.md

@ -0,0 +1,69 @@
# ADR-001 — Strategi Identitas Asset
**Status:** Accepted
**Tanggal:** 2026-06-17
**Penulis:** Tim IT GIS Portal
---
## Konteks
Agent PowerShell berjalan di berbagai endpoint Windows. Kita perlu menentukan cara mengidentifikasi aset secara unik agar:
- Laporan dari mesin yang sama selalu di-merge ke satu asset record
- Tidak membuat duplikat jika hostname berubah atau OS diinstall ulang
- Menangani kasus edge: komputer tanpa serial number, VM clone, dual-boot
---
## Keputusan
Gunakan **hierarki identitas fallback** untuk resolve `asset_id`:
```
1. serial_number (paling stabil, hardware-bound)
2. smbios_uuid (BIOS UUID, umumnya stabil)
3. primary_physical_mac (MAC NIC built-in utama, stabil kecuali ganti NIC)
4. machine_id (Registry MachineGuid, berubah jika clean install)
5. hostname (last resort — rentan berubah)
```
**Primary physical MAC** dipilih berdasarkan:
- NIC dengan route ke gateway default
- Filter: hanya physical (bukan virtual/VPN)
- Prefer `is_built_in: true`
Algoritma ada di `agent/lib/Get-AssetIdentity.ps1` + `agent/lib/Build-GeoNetReportPayload.ps1`.
---
## Implementasi
Field `asset_identity` di payload:
```json
{
"primary_physical_mac": "B8:2A:72:FA:17:FF",
"merge_key": "mac:B8:2A:72:FA:17:FF",
"recommended_match_order": ["serial_number", "smbios_uuid", "primary_physical_mac", "machine_id", "hostname"]
}
```
FastAPI Collector menggunakan `merge_key` untuk UPSERT ke tabel `assets`.
---
## Alternatif yang Ditolak
| Alternatif | Alasan Ditolak |
|-----------|---------------|
| Hanya hostname | Berubah jika komputer di-rename |
| Hanya machine_id | Berubah jika clean install OS |
| Generate UUID baru per install | Tidak bisa merge laporan lama |
| Kombinasi hostname+mac UNIQUE | Terlalu ketat — gagal jika ganti NIC |
---
## Konsekuensi
- Jika serial number berubah (ganti motherboard), bisa membuat asset baru — acceptable
- Jika MAC berubah (ganti NIC), fallback ke machine_id atau hostname
- VM clone bisa saling override jika MAC sama — perlu filter `asset_type=vm` terpisah

60
GeoNetAgent/docs/adr/ADR-002-asset-split-by-hardware.md

@ -0,0 +1,60 @@
# ADR-002 — Split Asset dengan Hardware Campur
**Status:** Accepted
**Tanggal:** 2026-06-22
**Penulis:** Tim IT GIS Portal
---
## Konteks
Ditemukan kasus di `DESKTOP-RRJ9G01`: satu username/hostname tapi laptop dan PC berbeda digunakan bergantian. Agent melaporkan hardware yang berbeda dari hostname yang sama, menghasilkan confusing asset record dengan data hardware yang terus berganti.
Masalah spesifik:
- `machine_id` UNIQUE constraint — jika mesin lain register hostname yang sama, constraint error
- Riwayat snapshot menunjukkan hardware yang inkonsisten (laptop Dell vs PC Gigabyte)
- CMDB tidak bisa menunjukkan "kamu punya 2 device: laptop + PC"
---
## Keputusan
1. **Hapus UNIQUE constraint `machine_id`** (migration 003) — `machine_id` bukan identifier yang reliable
2. **Buat script split** untuk memisahkan asset dengan hardware campur
3. **Gunakan `smbios_uuid` sebagai key pembeda** hardware fisik yang berbeda
**Script split:** `docker exec geonet-collector python /app/scripts/split_asset_by_hardware.py <uuid>`
Script ini:
- Deteksi snapshot dengan `smbios_uuid` berbeda dari satu asset
- Buat asset record baru untuk hardware lain
- Pindahkan snapshot yang relevan ke asset baru
- Set `hostname_alias` atau `merged_from` di metadata
---
## Implementasi
Pilot: `DESKTOP-RRJ9G01` — split menjadi:
- Asset A: Laptop Dell Inspiron (smbios_uuid: 4C4C4544...)
- Asset B: PC Gigabyte (smbios_uuid: berbeda)
Keduanya tetap terhubung ke user yang sama via `asset_user_assignments`.
---
## Alternatif yang Ditolak
| Alternatif | Alasan Ditolak |
|-----------|---------------|
| UNIQUE (hostname, smbios_uuid) | Terlalu ketat untuk edge cases lain |
| Biarkan saja (merge semua) | CMDB tidak akurat — tidak tahu user punya 2 device |
| Require manual entry per device | Tidak scalable, agent tidak tahu mana yang "utama" |
---
## Konsekuensi
- `machine_id` tidak lagi UNIQUE — tidak bisa dijadikan identifier tunggal
- Asset record yang lama dengan hardware campur perlu di-split manual (per kasus)
- Script split tersedia tapi perlu dijalankan manual oleh admin

100
GeoNetAgent/docs/adr/ADR-003-asset-documents-qnap.md

@ -0,0 +1,100 @@
# ADR-003 — Dokumen Asset di QNAP Object Storage
**Status:** Accepted
**Tanggal:** 2026-06-22
**Penulis:** Tim IT GIS Portal
---
## Konteks
Tim IT perlu melampirkan dokumen fisik ke asset CMDB:
- Invoice pembelian hardware
- Service report perbaikan
- Surat complain/garansi
- Certificate of authenticity
Dokumen-dokumen ini berupa file (PDF, gambar, Word) yang tidak cocok disimpan di PostgreSQL sebagai blob. Perlu penyimpanan file eksternal yang terintegrasi.
---
## Keputusan
Gunakan **QNAP QuObjects Object Storage (S3-compatible)** dengan bucket terpisah `geonetagent-documents`.
**Alasan memilih QNAP OSS:**
- Infrastruktur QNAP sudah ada (10.100.1.10)
- S3-compatible API — bisa pakai library standar (`boto3`, Laravel S3 driver)
- Tidak perlu bayar cloud storage
- Data on-premise — tidak keluar jaringan internal
- Bucket `super-apps` sudah terbukti berfungsi di proyek lain (mysupperapps_FE_PWA)
**Bucket:** `geonetagent-documents` (terpisah dari `super-apps` untuk isolasi)
**Struktur path di bucket:**
```
geonetagent-documents/
├── invoices/<asset_id>/<filename>
├── service-reports/<asset_id>/<filename>
└── complaints/<asset_id>/<filename>
```
---
## Schema DB (Migration 005 — Draft, Belum Apply)
```sql
CREATE TABLE asset_invoices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
asset_id UUID REFERENCES assets(id),
invoice_number VARCHAR,
vendor VARCHAR,
purchase_date DATE,
amount NUMERIC(12,2),
currency VARCHAR DEFAULT 'IDR',
warranty_until DATE,
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE document_attachments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
related_type VARCHAR, -- 'invoice', 'ticket', 'asset'
related_id UUID,
filename VARCHAR NOT NULL,
storage_path VARCHAR NOT NULL, -- path di QNAP bucket
file_size_bytes INTEGER,
mime_type VARCHAR,
uploaded_by VARCHAR,
uploaded_at TIMESTAMPTZ DEFAULT NOW()
);
```
---
## Implementasi (Fase 3)
1. Buat bucket `geonetagent-documents` di QNAP
2. Apply migration 005 ke DB
3. Laravel: upload via S3 driver → simpan path di `document_attachments`
4. Laravel: download via presigned URL atau proxy route
5. UI: attach file ke asset di halaman detail
---
## Alternatif yang Ditolak
| Alternatif | Alasan Ditolak |
|-----------|---------------|
| PostgreSQL bytea | Tidak scalable untuk file besar, backup berat |
| NFS mount Laravel | Coupling ke infrastruktur NFS, sudah migrasi dari NFS di proyek lain |
| Cloud storage (S3 AWS, GCS) | Data harus on-premise, ada biaya tambahan |
| File system lokal server | Tidak HA, coupling ke satu server |
---
## Konsekuensi
- Laravel butuh QNAP OSS credentials di `.env` — **jangan commit ke repo**
- Bucket perlu dikonfigurasi access control — private (presigned URL untuk download)
- Jika QNAP down: upload/download dokumen tidak tersedia, tapi CMDB tetap berfungsi

13
GeoNetAgent/docs/adr/README.md

@ -0,0 +1,13 @@
# Architecture Decision Records (ADR)
ADR mendokumentasikan keputusan arsitektur penting, alasan di baliknya, dan alternatif yang dipertimbangkan.
**Aturan:** ADR tidak boleh dihapus. Jika keputusan berubah, buat ADR baru.
---
| ADR | Judul | Status |
|-----|-------|--------|
| [ADR-001](ADR-001-agent-asset-identity.md) | Strategi Identitas Asset | Accepted |
| [ADR-002](ADR-002-asset-split-by-hardware.md) | Split Asset Hardware Campur | Accepted |
| [ADR-003](ADR-003-asset-documents-qnap.md) | Dokumen Asset di QNAP Object Storage | Accepted |

60
GeoNetAgent/docs/api/error-codes.md

@ -0,0 +1,60 @@
# API — Error Codes
> **Terakhir Diperbarui:** 2026-06-29
---
## HTTP Status Codes
| Code | Keterangan | Penyebab Umum |
|------|------------|---------------|
| 200 | OK | Request berhasil |
| 201 | Created | Asset baru dibuat |
| 422 | Unprocessable Entity | Payload tidak valid (field hilang/tipe salah) |
| 401 | Unauthorized | Token tidak ada atau salah |
| 403 | Forbidden | Token valid tapi tidak punya akses |
| 404 | Not Found | Resource tidak ditemukan |
| 503 | Service Unavailable | DB tidak bisa diakses |
---
## Format Error 422 (Validation)
```json
{
"detail": [
{
"loc": ["body", "hostname"],
"msg": "field required",
"type": "value_error.missing"
},
{
"loc": ["body", "system", "ram_gb"],
"msg": "value is not a valid float",
"type": "type_error.float"
}
]
}
```
---
## Format Error Lainnya
```json
{
"detail": "Error message string"
}
```
---
## PowerShell Agent Error Handling
Agent tidak retry otomatis jika gagal. Error di-log ke `%ProgramData%\GeoNetAgent\logs\`. Scheduled Task akan retry di jadwal berikutnya (3 jam).
Untuk debug manual:
```powershell
& "$env:ProgramData\GeoNetAgent\UserAgent.ps1"
# Cek output di terminal
```

109
GeoNetAgent/docs/api/overview.md

@ -0,0 +1,109 @@
# API Overview
> **Terakhir Diperbarui:** 2026-06-29
---
## Services
| Service | Base URL | Auth | Status |
|---------|----------|------|--------|
| FastAPI Collector | `https://collector.gisportal.id` | Bearer token | ✅ Running |
| FastAPI Collector (internal) | `http://10.100.1.24:8000` | Bearer token | ✅ Running |
| Laravel Portal | `https://portal.geonet.internal` | Session/Sanctum | ❌ Belum scaffold |
---
## Auth
Semua endpoint FastAPI memerlukan header:
```
Authorization: Bearer <agent_token>
```
Token dikonfigurasi di `.env` collector (`AGENT_BEARER_TOKEN`).
---
## Format Response Standard
**Success:**
```json
{
"status": "ok",
"data": { ... }
}
```
**Error:**
```json
{
"detail": "Error message"
}
```
**Validation Error (422):**
```json
{
"detail": [
{
"loc": ["body", "hostname"],
"msg": "field required",
"type": "value_error.missing"
}
]
}
```
---
## Endpoints Ringkas
| Method | Path | Keterangan | Auth |
|--------|------|------------|------|
| `POST` | `/api/v1/agent/report` | Laporan dari agent | Bearer |
| `GET` | `/api/v1/assets` | List asset | Bearer |
| `GET` | `/api/v1/assets/{id}` | Detail asset | Bearer |
| `GET` | `/api/v1/assets/{id}/snapshots` | History snapshot | Bearer |
| `GET` | `/api/v1/assets/{id}/software` | Software terinstall | Bearer |
| `GET` | `/health` | Health check | Tidak perlu |
| `GET` | `/ui/` | Web UI CMDB | Tidak perlu (LAN only) |
| `GET` | `/docs` | Swagger UI | Tidak perlu |
---
## Interactive Docs
Swagger UI tersedia di:
- `https://collector.gisportal.id/docs`
- `http://10.100.1.24:8000/docs`
OpenAPI JSON:
- `https://collector.gisportal.id/openapi.json`
---
## Contoh Request (cURL)
```bash
# Health check
curl https://collector.gisportal.id/health
# List assets
curl -H "Authorization: Bearer <token>" \
https://collector.gisportal.id/api/v1/assets
# Post laporan (dari file sample)
curl -X POST \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d @samples/agent-reports/sample-report-v0.1.10.json \
https://agent.gisportal.id/api/v1/agent/report
```
---
## Versioning
Semua endpoint menggunakan prefix `/api/v1/`. API v2 akan dibuat jika ada breaking changes — v1 tetap berjalan paralel.

184
GeoNetAgent/docs/api/service-collector.md

@ -0,0 +1,184 @@
# API — FastAPI Collector Service
> **Base URL:** `https://collector.gisportal.id` / `https://agent.gisportal.id`
> **Terakhir Diperbarui:** 2026-06-29
> **Auth:** `Authorization: Bearer <agent_token>` (semua endpoint kecuali /health dan /ui/)
---
## GET /health
Health check. Tidak butuh auth.
**Response 200:**
```json
{"status": "ok", "db": "connected"}
```
---
## POST /api/v1/agent/report
Menerima laporan hardware/software dari PowerShell agent.
**Header:** `Authorization: Bearer <agent_token>`
**Request body (ringkas):**
```json
{
"schema_version": "1.0",
"agent_version": "0.1.10-phase0",
"collected_at": "2026-06-22T09:00:00+07:00",
"hostname": "DESKTOP-RRJ9G01",
"machine_id": "3709baa5-5265-4e4d-9a81-fb8c8927bb3b",
"smbios_uuid": "4C4C4544-004C-5610-805A-B2C04F4A3432",
"serial_number": "2LVZJ42",
"asset_type": "endpoint",
"logged_in_user": "DOMAIN\\username",
"system": {
"manufacturer": "Dell Inc.",
"model": "Inspiron 7447",
"os_name": "Microsoft Windows 11 Pro",
"os_build": "10.0.26200",
"cpu_model": "Intel(R) Core(TM) i7-4710HQ CPU @ 2.50GHz",
"ram_gb": 15.9,
"gpu_model": "NVIDIA GeForce GTX 850M",
"form_factor": "Laptop",
"last_boot_at": "2026-06-20T08:42:51+07:00"
},
"cpu": {
"model": "Intel Core i7-4710HQ",
"cores": 4,
"logical_processors": 8,
"max_clock_mhz": 2500,
"l2_cache_kb": 1024,
"l3_cache_kb": 6144,
"architecture": "x64"
},
"gpus": [
{"name": "NVIDIA GeForce GTX 850M", "dedicated_memory_mb": 4054}
],
"disks": [
{"drive_letter": "C:", "total_gb": 188.7, "free_gb": 3.1, "pct_used": 98.3, "file_system": "NTFS", "model": "ADATA SU650"}
],
"physical_disks": [
{"model": "ADATA SU650", "serial": "2K3620065948", "size_gb": 223.6, "media_type": "SSD", "bus_type": "SATA"}
],
"memory_summary": {
"total_gb": 16,
"module_count": 2,
"layout": "2x8GB",
"upgradeable": false
},
"memory_modules": [
{"slot": "DIMM_A", "capacity_gb": 8, "speed_mhz": 1600, "memory_type": 24, "form_factor": "sodimm"}
],
"network": [
{"adapter_name": "Realtek PCIe GBE", "mac_address": "B8:2A:72:FA:17:FF", "ip_address": "192.168.0.33", "is_primary": true, "is_built_in": true}
],
"asset_identity": {
"primary_physical_mac": "B8:2A:72:FA:17:FF",
"merge_key": "mac:B8:2A:72:FA:17:FF",
"recommended_match_order": ["serial_number", "smbios_uuid", "primary_physical_mac", "machine_id", "hostname"]
},
"peripherals": [
{"name": "Dell BT Adapter", "device_class": "Bluetooth", "connection": "usb", "is_external": true}
],
"health": {
"bsod_count_30d": 0,
"pending_reboot": false,
"last_patch_installed": "2026-02-27"
},
"software": [
{"name": "Google Chrome", "version": "120.0.6099.130", "publisher": "Google LLC", "install_date": "2024-01-15"}
]
}
```
**Response 200:**
```json
{
"status": "ok",
"asset_id": "uuid-of-asset",
"action": "updated"
}
```
**Response 401:** Token salah
**Response 422:** Payload tidak valid (field wajib hilang atau tipe salah)
---
## GET /api/v1/assets
List asset dengan filter opsional.
**Query params:**
- `?hostname=<hostname>` — filter by hostname (partial match)
- `?status=active` — filter by status
- `?page=1&per_page=20` — pagination
**Response 200:**
```json
{
"total": 42,
"page": 1,
"per_page": 20,
"items": [
{
"id": "uuid",
"hostname": "DESKTOP-RRJ9G01",
"asset_type": "endpoint",
"status": "active",
"last_seen_at": "2026-06-22T09:00:00+07:00",
"cpu_model": "Intel Core i7-4710HQ",
"ram_gb": 15.9,
"os_name": "Microsoft Windows 11 Pro",
"form_factor": "Laptop"
}
]
}
```
---
## GET /api/v1/assets/{id}
Detail asset termasuk snapshot terbaru.
**Response 200:**
```json
{
"id": "uuid",
"hostname": "DESKTOP-RRJ9G01",
"asset_type": "endpoint",
"serial_number": "2LVZJ42",
"smbios_uuid": "4C4C...",
"status": "active",
"last_seen_at": "2026-06-22T09:00:00+07:00",
"latest_snapshot": { ... full payload ... }
}
```
---
## GET /api/v1/assets/{id}/snapshots
History snapshot untuk asset.
**Query params:** `?limit=10`
**Response 200:** Array snapshot objects dengan `collected_at` dan `agent_version`.
---
## GET /api/v1/assets/{id}/software
Software terinstall (dari snapshot terbaru).
**Response 200:**
```json
[
{"name": "Google Chrome", "version": "120.0.6099.130", "publisher": "Google LLC", "install_date": "2024-01-15"}
]
```

90
GeoNetAgent/docs/architecture/authentication-flow.md

@ -0,0 +1,90 @@
# Architecture — Authentication Flow
> **Terakhir Diperbarui:** 2026-06-29
---
## Auth Model per Komponen
| Komponen | Auth Method | Status |
|----------|-------------|--------|
| Agent → Collector API | Bearer token (statis per site) | ✅ Implemented |
| IT Admin → Collector UI | **Tidak ada auth** (read-only LAN) | ⚠ Fase 1 blocker |
| IT Admin → Laravel Portal | Session / Sanctum (lokal dulu, LDAP Fase 2) | ❌ Belum |
| Zabbix → Laravel webhook | Shared secret header | ❌ Fase 2 |
| Laravel → Zabbix API | API token | ❌ Fase 2 |
| Laravel → AD | LDAP bind | ❌ Fase 2 |
---
## Alur Auth Agent (Bearer Token)
```mermaid
sequenceDiagram
participant PS as PowerShell Agent
participant COL as FastAPI Collector
PS->>PS: Baca token dari %ProgramData%\GeoNetAgent\config.json
PS->>COL: POST /api/v1/agent/report\nAuthorization: Bearer <agent_token>
alt AGENT_TOKEN_CHECK_DB=false (default)
COL->>COL: Compare token dengan AGENT_BEARER_TOKEN env
alt Token cocok
COL-->>PS: 200 OK
else Token salah
COL-->>PS: 401 Unauthorized
end
else AGENT_TOKEN_CHECK_DB=true
COL->>COL: Hash token, lookup di tabel agent_tokens
alt Token valid dan is_active
COL-->>PS: 200 OK
else
COL-->>PS: 401 Unauthorized
end
end
```
---
## Token di `config.json` (Endpoint)
```json
{
"CollectorUrl": "https://agent.gisportal.id/api/v1/agent/report",
"AgentToken": "site-agent-token",
"ReportIntervalHours": 3
}
```
> Token ini adalah **site agent token** — bukan credentials admin. Tidak bisa dipakai untuk mengakses read API atau Laravel portal.
---
## Alur Auth Laravel Portal (Fase 1 — Lokal)
```mermaid
sequenceDiagram
participant IT as IT Admin
participant LRV as Laravel Portal
IT->>LRV: POST /login {username, password}
LRV->>LRV: Auth::attempt() — local users table
alt Valid
LRV->>LRV: Create session
LRV-->>IT: Redirect /assets
else Invalid
LRV-->>IT: 422 Unauthorized
end
IT->>LRV: GET /assets
LRV->>LRV: Check session / auth middleware
LRV-->>IT: Asset list dari PostgreSQL
```
---
## Keamanan Token
- Token agent disimpan di `config.json` di `%ProgramData%\GeoNetAgent\` — hanya admin local yang bisa baca
- Token **tidak** disimpan di repo source code
- Jika token bocor: ganti `AGENT_BEARER_TOKEN` di `.env` collector + regenerate `config.json` di semua endpoint
- Untuk multi-site dengan token berbeda: aktifkan `AGENT_TOKEN_CHECK_DB=true` + buat entry di `agent_tokens`

95
GeoNetAgent/docs/architecture/database-flow.md

@ -0,0 +1,95 @@
# Architecture — Database Flow
> **Terakhir Diperbarui:** 2026-06-29
---
## Ownership Write/Read
```
FastAPI Collector ──────────► PostgreSQL ◄──────── Laravel Portal
(write ingress otomatis) geonetagent (read + manual CRUD)
- agent/report upsert - asset list/detail
- Proxmox sync (Fase 3) - license manage
- software scan - document upload
```
**Aturan:** FastAPI = satu-satunya writer untuk data otomatis. Laravel = presentasi + manual entry.
---
## Alur Write — Laporan Agent
```mermaid
flowchart TD
A[Agent POST payload] --> B[FastAPI: validate schema]
B --> C{asset_id\nresolution\nADR-001}
C -->|serial_number| D[UPSERT assets\nWHERE serial_number=?]
C -->|smbios_uuid fallback| E[UPSERT assets\nWHERE smbios_uuid=?]
C -->|MAC fallback| F[UPSERT assets\nWHERE primary_mac=?]
C -->|machine_id fallback| G[UPSERT assets\nWHERE machine_id=?]
C -->|hostname last resort| H[INSERT/UPSERT assets\nWHERE hostname=?]
D & E & F & G & H --> I[UPDATE assets.last_seen_at]
I --> J[INSERT asset_snapshots\npayload=full JSON]
J --> K[UPSERT software_installations\nper collected_at]
```
---
## Alur Read — Collector UI
```
GET /api/v1/assets
→ SELECT a.*, v.cpu_model, v.ram_gb, v.gpu_model, v.os_name
FROM assets a
LEFT JOIN v_asset_hardware_latest v ON v.asset_id = a.id
WHERE a.deleted_at IS NULL
ORDER BY a.last_seen_at DESC
GET /api/v1/assets/{id}/snapshots
→ SELECT * FROM asset_snapshots WHERE asset_id = ?
ORDER BY collected_at DESC LIMIT 10
```
---
## Alur Read — Laravel (Fase 1)
```
Laravel Eloquent:
Asset::with(['latestSnapshot', 'location', 'primaryUser'])
->paginate(20)
```
Laravel menggunakan koneksi **read-only** ke DB (rekomendasi — belum diimplementasi). Saat ini masih credential yang sama.
---
## View `v_asset_hardware_latest`
View yang merangkum snapshot terbaru per asset (migration 004):
```sql
CREATE VIEW v_asset_hardware_latest AS
SELECT DISTINCT ON (asset_id)
asset_id,
payload->>'os_name' AS os_name,
(payload->'system'->>'cpu_model') AS cpu_model,
(payload->'system'->>'ram_gb')::float AS ram_gb,
(payload->'system'->>'gpu_model') AS gpu_model,
collected_at
FROM asset_snapshots
ORDER BY asset_id, collected_at DESC;
```
> **Catatan:** Field hardware ada di path `payload->system->*` di snapshot lama, dan di `payload->*` (root) untuk payload baru (v0.1.9+). View mungkin perlu diupdate sesuai schema terbaru.
---
## Transaction Boundary
FastAPI menggunakan single DB session per request dengan auto-commit. Tidak ada distributed transaction.
Jika partial write terjadi (mis. `assets` berhasil upsert tapi `asset_snapshots` gagal):
- Laporan berikutnya (3 jam kemudian) akan retry — idempotent
- Tidak ada rollback manual yang diperlukan

124
GeoNetAgent/docs/architecture/deployment-architecture.md

@ -0,0 +1,124 @@
# Architecture — Deployment Architecture
> **Terakhir Diperbarui:** 2026-06-29
---
## Topology
```
Endpoint Windows (Laptop/PC)
│ HTTPS POST /api/v1/agent/report
┌─────────────────────────────────────────────┐
│ Nginx Reverse Proxy (10.100.1.24) │
│ /etc/nginx/conf.d/production/ │
│ - collector.conf → agent.gisportal.id │
│ → collector.gisportal.id │
└──────────────────┬──────────────────────────┘
│ proxy_pass :18000
┌──────────────────────┐
│ FastAPI Collector │
│ Docker container │
│ geonet-collector │
│ Port: 18000 │
└──────────┬───────────┘
┌──────────────────────┐
│ PostgreSQL 12 │
│ 10.100.1.25 │
│ DB: geonetagent │
└──────────────────────┘
```
---
## Container Info
| Container | Image | Port | Stack Path |
|-----------|-------|------|------------|
| `geonet-collector` | Python 3.11 (FastAPI) | 18000 | `/opt/stacks/geonetagent-collector/` |
---
## Nginx Config (collector.conf)
```nginx
# agent.gisportal.id — agent POST + web install
server {
server_name agent.gisportal.id;
location /api/ {
proxy_pass http://127.0.0.1:18000;
}
location /install/ {
ssi off; # WAJIB — agar $env: tidak terpotong di file .ps1
alias /var/www/agent-install/;
}
}
# collector.gisportal.id — read UI + API
server {
server_name collector.gisportal.id;
location / {
proxy_pass http://127.0.0.1:18000;
}
}
```
---
## Web Install Static Files
File agent yang di-serve di `agent.gisportal.id/install/`:
```
/var/www/agent-install/
├── GeoNetAgent-Install.ps1 — Entry point user (2 baris PS)
├── bootstrap.ps1
├── Install-GeoNetAgent.ps1
├── manifest.json — Daftar file + versi (untuk auto-update)
├── config.json — Config default (token, URL)
└── lib/
├── Agent-InstallCore.ps1
├── Build-GeoNetReportPayload.ps1
└── ... (semua file lib/)
```
> File ini di-publish via `.\infra\collector\publish_agent_install.ps1`
---
## Docker Compose (Collector)
```yaml
services:
geonet-collector:
build: ./collector-src
ports:
- "18000:18000"
env_file: .env
restart: unless-stopped
```
---
## Firewall
- Port 443/80: public (HTTPS/HTTP via nginx)
- Port 18000: internal bind (127.0.0.1) — tidak expose langsung
- Port 8000: allow dari subnet 10.x dan 192.168.x (internal UI HTTP direct)
- PostgreSQL 5432: tidak expose, hanya dari container dan server 10.100.1.24
---
## Lihat Juga
- `infra/nginx/README.md` — detail nginx config
- `infra/collector/` — deploy scripts
- `docs/deployment.md` — step-by-step deploy
- `docs/server.md` — SSH commands

199
GeoNetAgent/docs/architecture/folder-structure.md

@ -0,0 +1,199 @@
# Architecture — Folder Structure
> **Terakhir Diperbarui:** 2026-06-29
---
## Root Monorepo
```
GeoNetAgent/
├── README.md — Ringkasan proyek + quick start
├── AGENTS.md — Aturan AI agent
├── .env.example — Template env vars
├── .gitignore — Git ignore rules
├── perintah.md — Protokol sync dokumentasi (wajib akhir sesi)
├── agent/ — PowerShell agent (deploy ke endpoint Windows)
├── collector/ — FastAPI collector service
├── infra/ — Infrastruktur (Nginx, DB, Docker, QNAP)
├── samples/ — Sample payload untuk testing
├── docs/ — Dokumentasi lengkap ⭐
├── .cursor/ — AI context (blueprint, schema, API, progress)
└── scripts/ — Utility scripts (opsional)
```
---
## `agent/` — PowerShell Agent
```
agent/
├── README.md — Cara install (user + admin)
├── Collect-GeoNetReport.ps1 — Entry point collect data
├── Send-GeoNetReport.ps1 — Entry point kirim laporan
├── UserAgent.ps1 — Script user: cek update + kirim laporan
├── Install-GeoNetAgent.ps1 — Installer: setup file + scheduled task
├── VERSION — Versi agent saat ini (mis. v0.1.10-phase0)
├── config.example.json — Template config agent
├── config.json — Config aktif (tidak di-commit ke repo)
├── lib/ — Modul sensor (satu file per sensor)
│ ├── Agent-InstallCore.ps1 — Inti proses instalasi
│ ├── Build-GeoNetReportPayload.ps1 — Assemble full JSON payload
│ ├── Get-AssetIdentity.ps1 — Resolve agent_id (ADR-001)
│ ├── Get-CpuInfo.ps1 — CPU: cores, clock, cache, arch
│ ├── Get-DiskInfo.ps1 — Logical disk (C:, D:, dll)
│ ├── Get-DisplaySettings.ps1 — Monitor EDID, resolusi
│ ├── Get-GpuInfo.ps1 — GPU model + dedicated memory
│ ├── Get-HealthInfo.ps1 — Disk usage %, battery
│ ├── Get-MemoryInfo.ps1 — RAM GB, DDR type, part no
│ ├── Get-NetworkInfo.ps1 — NIC, MAC, IP, is_built_in
│ ├── Get-PeripheralsInfo.ps1 — USB/BT/audio/monitor, is_external
│ ├── Get-PhysicalDiskInfo.ps1 — Physical disk: model, serial, type
│ ├── Get-SoftwareInfo.ps1 — Installed software list
│ ├── Get-SystemInfo.ps1 — OS, manufacturer, model, BIOS
│ ├── Send-CollectorReport.ps1 — HTTP POST ke collector
│ ├── Update-GeoNetAgent.ps1 — Auto-update dari manifest server
│ └── Format-*.ps1 — Utility formatting
├── gpo/ — GPO startup scripts (rollout massal)
│ └── README.md
└── install/ — File yang di-serve di agent.gisportal.id/install/
```
---
## `collector/` — FastAPI Service
```
collector/
├── pyproject.toml — Python project config + dependencies
├── app/
│ ├── main.py — FastAPI app init, lifespan, mount /ui/
│ ├── config.py — Settings dari .env (pydantic-settings)
│ ├── api/ — Route handlers
│ │ ├── agent.py — POST /api/v1/agent/report
│ │ ├── assets.py — GET /api/v1/assets (read)
│ │ └── health.py — GET /health
│ ├── schemas/ — Pydantic schemas
│ ├── services/ — Business logic (asset resolution, upsert)
│ ├── db/ — DB session + models
│ └── core/ — Middleware, dependencies
├── static/ — UI files (HTML/JS/CSS untuk /ui/)
├── tests/ — pytest
└── docs/
└── API.md — API documentation (konsumsi API)
```
---
## `infra/` — Infrastruktur
```
infra/
├── collector/ — Deploy scripts collector
│ ├── push_collector.ps1 — Deploy collector ke server (Windows)
│ ├── push_collector.sh — Deploy collector ke server (Linux)
│ ├── publish_agent_install.ps1 — Publish agent ke web install (Windows)
│ ├── publish_agent_install.sh — Publish agent ke web install (Linux)
│ └── deploy_nginx.sh — Deploy nginx config
├── db/ — SQL migration files
│ ├── README.md — Cara apply migration
│ ├── 001_initial_schema.sql
│ ├── 002_disk_model.sql
│ ├── 003_drop_machine_id_unique.sql
│ ├── 004_view_hardware_latest.sql
│ └── 005_asset_documents.sql — Draft, belum apply
├── nginx/ — Nginx config templates
│ └── README.md
└── qnap/ — QNAP Object Storage config
└── README.md
```
---
## `docs/` — Dokumentasi Lengkap
```
docs/
├── context-index.md ⭐ AI Entry Point
├── project-status.md
├── roadmap.md
├── todo.md
├── issues.md
├── environment.md
├── server.md
├── deployment.md
├── development.md
├── database.md
├── troubleshooting.md
├── monitoring.md
├── security.md
├── glossary.md
├── architecture/
│ ├── system-overview.md
│ ├── folder-structure.md (file ini)
│ ├── request-flow.md
│ ├── authentication-flow.md
│ ├── database-flow.md
│ └── deployment-architecture.md
├── api/
│ ├── overview.md
│ ├── service-collector.md
│ └── error-codes.md
├── adr/
│ ├── README.md
│ ├── ADR-001-agent-asset-identity.md
│ ├── ADR-002-asset-split-by-hardware.md
│ └── ADR-003-asset-documents-qnap.md
├── diagrams/
│ └── *.mmd
├── onboarding/
│ ├── first-day.md
│ ├── local-setup.md
│ └── coding-standard.md
└── decisions/
├── technology-stack.md
└── naming-convention.md
```
---
## `.cursor/` — AI Context (Detail Teknis)
```
.cursor/
├── context-index.md — Entry point lama (versi lama, masih relevan)
├── blueprint.md — Master blueprint + modul M1-M9
├── progress.md — Checklist fase + milestone history
├── db-schema.md — Schema detail + ERD Mermaid
├── api-contracts.md — Kontrak API FastAPI + Laravel + webhook
├── repo-structure.md — Konvensi monorepo
├── me.md — Profil user + inventaris infrastruktur
├── server.md — SSH commands semua server
├── monitoring-architecture.md — Layer monitoring Zabbix/FastAPI/Laravel
├── agent-playbook.md — Workflow multi-tab koordinasi
├── adr/ — ADR files (kosong — ada di docs/adr/)
└── rules/ — Cursor rules (kosong — perlu diisi)
```
---
## Konvensi Naming
| Tipe | Konvensi | Contoh |
|------|----------|--------|
| Python file | `snake_case.py` | `agent_report.py` |
| PS1 file | `Verb-Noun.ps1` | `Get-CpuInfo.ps1`, `Send-CollectorReport.ps1` |
| SQL migration | `NNN_description.sql` | `003_drop_machine_id_unique.sql` |
| DB tabel | `snake_case` plural | `asset_snapshots`, `software_installations` |
| FastAPI route | `/api/v1/resource` kebab-case | `/api/v1/agent/report` |
| Doc file | `kebab-case.md` | `system-overview.md` |

77
GeoNetAgent/docs/architecture/request-flow.md

@ -0,0 +1,77 @@
# Architecture — Request Flow
> **Terakhir Diperbarui:** 2026-06-29
---
## Alur Laporan Agent
```mermaid
sequenceDiagram
participant PS as PowerShell Agent
participant NGX as Nginx (10.100.1.24)
participant COL as FastAPI Collector
participant PG as PostgreSQL
PS->>PS: Scheduled Task setiap 3 jam
PS->>PS: Cek VERSION lokal vs manifest.json server
alt Versi server lebih baru
PS->>NGX: GET agent.gisportal.id/install/manifest.json
NGX-->>PS: manifest.json
PS->>NGX: Download file-file terbaru
PS->>PS: Replace file lokal
end
PS->>PS: Collect-GeoNetReport.ps1 (WMI/CIM scan)
PS->>NGX: POST agent.gisportal.id/api/v1/agent/report\nAuthorization: Bearer <token>
NGX->>COL: proxy_pass :18000
COL->>COL: Validate schema + bearer token
COL->>COL: Resolve asset_id (ADR-001:\nserial → smbios_uuid → MAC → machine_id)
COL->>PG: UPSERT assets (hostname, last_seen_at, dll)
COL->>PG: INSERT asset_snapshots (payload JSON)
COL->>PG: UPSERT software_installations
COL-->>NGX: 200 OK {asset_id, status}
NGX-->>PS: 200 OK
```
---
## Alur User Buka UI (Fase 1)
```mermaid
sequenceDiagram
participant IT as IT Admin (Browser)
participant NGX as Nginx
participant COL as FastAPI /ui/
participant PG as PostgreSQL
IT->>NGX: GET collector.gisportal.id/ui/
NGX->>COL: proxy_pass :18000
COL-->>IT: HTML/JS (static files)
IT->>NGX: GET collector.gisportal.id/api/v1/assets
NGX->>COL: proxy_pass :18000
COL->>PG: SELECT dari assets + v_asset_hardware_latest
PG-->>COL: asset data
COL-->>IT: JSON [{id, hostname, cpu_model, ram_gb, ...}]
```
---
## Idempotency
Laporan agent bersifat idempotent:
- `UPSERT assets` berdasarkan `asset_id` (resolve ADR-001)
- Duplikat snapshot `(asset_id, collected_at)` diabaikan
- Software installations di-replace per `collected_at`
---
## Error Handling
| Skenario | Behavior |
|----------|----------|
| Token salah (401) | Collector return 401, agent log error, tidak retry otomatis |
| Schema invalid (422) | Collector return 422 + detail field errors |
| DB tidak bisa diakses | Collector return 503, agent log error |
| Timeout | Agent tidak retry — akan coba lagi di jadwal berikutnya (3 jam) |
| Duplikat `collected_at` | Diabaikan (idempotent), return 200 |

124
GeoNetAgent/docs/architecture/system-overview.md

@ -0,0 +1,124 @@
# Architecture — System Overview
> **Terakhir Diperbarui:** 2026-06-29
---
## Visi
GeoNetAgent adalah **portal IT + CMDB ringan yang melengkapi Zabbix**, bukan menggantikannya.
- **Zabbix** = monitoring real-time, alerting, threshold
- **GeoNetAgent** = inventaris hardware/software point-in-time, manajemen aset, dokumen procurement
---
## Diagram Sistem
```mermaid
graph TB
subgraph ENDPOINTS["Endpoint Windows (Laptop/PC)"]
PS["PowerShell Agent\nScheduled Task 3 jam"]
end
subgraph INFRA["Infrastruktur Existing"]
ZBX["Zabbix\n10.100.1.42"]
AD["Active Directory\n10.100.1.40"]
QNAP["QNAP NAS\n10.100.1.10\nObject Storage"]
end
subgraph SERVER24["10.100.1.24 — App Server"]
NGX["Nginx\nSSL + Routing"]
COL["FastAPI Collector\n:18000"]
LRV["Laravel Portal\n(planned)"]
end
subgraph SERVER25["10.100.1.25 — DB Server"]
PG[("PostgreSQL 12\ngeonetagent DB")]
end
PS -->|"HTTPS POST /api/v1/agent/report\nBearer token"| NGX
NGX -->|"agent.gisportal.id"| COL
NGX -->|"collector.gisportal.id"| COL
COL --> PG
LRV --> PG
LRV -->|"Zabbix API (Fase 2)"| ZBX
ZBX -->|"webhook (Fase 2)"| LRV
LRV -->|"AD LDAP auth (Fase 2)"| AD
LRV -->|"S3 dokumen (Fase 3)"| QNAP
```
---
## Boundary System — Siapa Kerja Apa
| Komponen | Owner Data | Write | Read |
|----------|-----------|-------|------|
| Uptime, CPU live, RAM live | Zabbix | Zabbix agents | Laravel via Zabbix API |
| Alert firing | Zabbix | Zabbix triggers | Laravel via webhook + API |
| Asset inventory (hardware) | PostgreSQL `assets` | FastAPI (agent), Laravel (manual) | Laravel UI |
| Hardware snapshot history | PostgreSQL `asset_snapshots` | FastAPI only | Laravel UI |
| Software inventory | PostgreSQL `software_installations` | FastAPI (scan) | Laravel UI |
| License entitlements | PostgreSQL `licenses` | Laravel manual | Laravel UI |
| Asset documents | PostgreSQL + QNAP S3 | Laravel upload | Laravel (presigned) |
| VM inventory | PostgreSQL `assets` type=vm | FastAPI Proxmox sync | Laravel UI |
**Aturan emas:** FastAPI = **ingress & sync otomatis**. Laravel = **presentasi, enrichment manual, auth**.
---
## Alur Data Utama
### 1. Laporan Agent
```
[Scheduled Task SYSTEM, 3 jam]
→ PowerShell Collect-GeoNetReport.ps1
→ WMI/CIM + network + software scan
→ Build JSON payload
→ POST agent.gisportal.id/api/v1/agent/report (HTTPS + Bearer)
→ Nginx → FastAPI Collector
→ Validate + resolve asset_id (ADR-001)
→ Upsert assets + insert asset_snapshots
→ Insert software_installations
```
### 2. User Buka Dashboard (Fase 1)
```
IT Admin → Browser → collector.gisportal.id/ui/
→ FastAPI read API → PostgreSQL
→ Tampilkan list asset + hardware info
```
### 3. Zabbix Alert ke Portal (Fase 2)
```
Zabbix trigger PROBLEM
→ Media type webhook → POST Laravel /webhooks/zabbix
→ Normalize → insert tabel alerts
→ Notifikasi di portal IT
```
---
## Komponen Detail
| Komponen | File | Status |
|----------|------|--------|
| Agent PowerShell | `agent/` | ✅ v0.1.10 |
| Web install | `agent.gisportal.id/install/` | ✅ Running |
| FastAPI Collector | `collector/` | ✅ Running |
| Collector UI | `collector.gisportal.id/ui/` | ✅ Running |
| PostgreSQL | `infra/db/` | ✅ Running (dev) |
| Nginx config | `infra/nginx/` | ✅ Running |
| Laravel Portal | `portal/` (planned) | ❌ Belum scaffold |
---
## Lihat Juga
- [`folder-structure.md`](folder-structure.md) — struktur monorepo
- [`request-flow.md`](request-flow.md) — alur request detail
- [`deployment-architecture.md`](deployment-architecture.md) — server topology
- `.cursor/blueprint.md` — visi lengkap + modul M1-M9

143
GeoNetAgent/docs/context-index.md

@ -0,0 +1,143 @@
# Context Index — GeoNetAgent ⭐ AI Entry Point
> **Terakhir Diperbarui:** 2026-06-29
> **Versi Agent:** v0.1.10-phase0
> **Fase Aktif:** Fase 1 — Foundation 🔄
---
## Filosofi
Proyek ini menerapkan **AI Native Development**:
- Repository = sumber kebenaran utama (Single Source of Truth)
- AI tidak boleh mengandalkan riwayat percakapan
- Developer sering berpindah perangkat — konteks harus tersimpan di repo
- Implementasi tanpa dokumentasi = belum selesai
---
## Urutan Membaca
1. **File ini** — peta navigasi
2. `.cursor/blueprint.md` — visi, fase, batas sistem
3. `.cursor/progress.md` — fase aktif & DoD checklist
4. Dokumen spesifik task (lihat tabel di bawah)
5. `perintah.md` — wajib dibaca sebelum menyatakan selesai
---
## Peta Dokumentasi
### Dokumentasi di `docs/`
| File | Isi | Baca Saat |
|------|-----|-----------|
| `docs/project-status.md` | Status modul, progress, known issues | Selalu |
| `docs/roadmap.md` | Fase & milestone | Planning |
| `docs/todo.md` | Next actions berprioritas | Sebelum coding |
| `docs/issues.md` | Known bugs + root cause | Debug |
| `docs/environment.md` | Env vars (tanpa secret) | Setup, deploy |
| `docs/server.md` | Server list, SSH, deploy commands | Deploy, infra |
| `docs/deployment.md` | Step-by-step deploy | Deploy |
| `docs/development.md` | Setup lokal, coding standard | Onboarding |
| `docs/database.md` | Schema PostgreSQL, tabel, migration | DB change |
| `docs/troubleshooting.md` | Debug common problems | Debug |
| `docs/monitoring.md` | Zabbix, collector health | Monitoring |
| `docs/security.md` | Credential management, rules | Security |
| `docs/glossary.md` | Kamus istilah | Reference |
### Arsitektur (`docs/architecture/`)
| File | Isi |
|------|-----|
| `system-overview.md` | Diagram sistem, boundary, komponen |
| `folder-structure.md` | Struktur monorepo |
| `request-flow.md` | Alur agent → collector → DB |
| `authentication-flow.md` | Auth agent token, Laravel session |
| `database-flow.md` | Write FastAPI, read Laravel |
| `deployment-architecture.md` | Docker, Nginx, server topology |
| `storage.md` | QNAP Object Storage (ADR 003) |
### API (`docs/api/`)
| File | Isi |
|------|-----|
| `overview.md` | Base URL, auth, format response |
| `service-collector.md` | FastAPI: agent/report, assets, snapshots |
| `service-portal.md` | Laravel: portal pages, documents, tickets (planned) |
| `error-codes.md` | Error format standar |
### ADR (`docs/adr/`)
| ADR | Keputusan |
|-----|-----------|
| `ADR-001-agent-asset-identity.md` | Identitas asset: serial → smbios_uuid → MAC → machine_id |
| `ADR-002-asset-split-by-hardware.md` | Split asset hardware campur (laptop + PC) |
| `ADR-003-asset-documents-qnap.md` | Dokumen procurement di QNAP Object Storage |
### Konteks AI di `.cursor/`
| File | Isi |
|------|-----|
| `.cursor/blueprint.md` | Master blueprint, visi, fase, modul M1-M9 |
| `.cursor/progress.md` | Checklist fase + milestone history |
| `.cursor/db-schema.md` | Schema PostgreSQL detail + ERD Mermaid |
| `.cursor/api-contracts.md` | Kontrak API FastAPI, Laravel, webhook |
| `.cursor/repo-structure.md` | Struktur monorepo + konvensi |
| `.cursor/me.md` | Profil user + inventaris infrastruktur |
| `.cursor/server.md` | SSH commands semua server |
| `.cursor/monitoring-architecture.md` | Layer monitoring Zabbix/FastAPI/Laravel |
| `.cursor/agent-playbook.md` | Workflow multi-tab koordinasi |
---
## Quick Reference
### URL Produksi
| Fungsi | URL |
|--------|-----|
| Agent POST (internet) | `https://agent.gisportal.id/api/v1/agent/report` |
| Install agent | `https://agent.gisportal.id/install/` |
| Collector UI + read API | `https://collector.gisportal.id/ui/` |
| Internal HTTP | `http://10.100.1.24:8000/ui/` |
### Deploy Commands
```powershell
.\infra\collector\publish_agent_install.ps1 # publish agent
.\infra\collector\push_collector.ps1 # deploy collector
bash infra/collector/deploy_nginx.sh # deploy nginx
```
### Server
| Server | IP | SSH |
|--------|----|-----|
| Reverse Proxy + Collector | 10.100.1.24 | `ssh -m hmac-sha1 root@10.100.1.24 -p 22` |
| PostgreSQL | 10.100.1.25 | `ssh -m hmac-sha1 root@10.100.1.25 -p 22` |
| AD/LDAP | 10.100.1.40 | `ssh -m hmac-sha1 Administrator@10.100.1.40 -p 22` |
| Zabbix | 10.100.1.42 | `ssh -m hmac-sha1 root@10.100.1.42 -p 22` |
| QNAP NAS | 10.100.1.10 | `ssh -i "$env:USERPROFILE\.ssh\id_ed25519" -o IdentitiesOnly=yes admin@10.100.1.10 -p 22` |
---
## Fase & Status Saat Ini
| Fase | Status | Blocker |
|------|--------|---------|
| Fase 0 — Blueprint | ✅ Selesai | - |
| Fase 1 — Foundation | 🔄 Aktif | Laravel asset index belum scaffold |
| Fase 2 — Zabbix integrasi | ⏳ | Menunggu Fase 1 DoD |
| Fase 3 — CMDB + Dokumen | ⏳ | Menunggu Fase 2 |
| Fase 4 — Scale | ⏳ | Menunggu Fase 3 |
---
## Aturan Utama AI
1. Baca `context-index.md` ini terlebih dahulu
2. Cek `.cursor/progress.md` — jangan implement fase yang belum waktunya
3. Setelah selesai: update progress.md + perintah.md
4. ADR tidak boleh dihapus — buat baru jika keputusan berubah
5. **Jangan commit secret** ke repository

204
GeoNetAgent/docs/database.md

@ -0,0 +1,204 @@
# Database Documentation
> **Terakhir Diperbarui:** 2026-06-29
---
## Overview
| Item | Value |
|------|-------|
| DBMS | PostgreSQL 12+ |
| Host | 10.100.1.25 |
| DB Dev | `geonetagent_dev` |
| DB Prod | `geonetagent` |
| Timezone | `Asia/Jakarta` |
| PK Convention | UUID (`gen_random_uuid()`) |
| Timestamp | `timestamptz` |
| Credentials | `/root/geonetagent_dev.credentials` di server 10.100.1.25 |
**Ownership:**
- **FastAPI Collector** = write ingress otomatis (agent reports, Proxmox sync)
- **Laravel Portal** = read + manual CRUD terbatas
---
## ERD (MVP)
```mermaid
erDiagram
locations ||--o{ assets : has
assets ||--o{ asset_snapshots : has
assets ||--o{ software_installations : has
assets ||--o{ asset_user_assignments : has
users ||--o{ asset_user_assignments : assigned
users ||--o{ license_assignments : has
licenses ||--o{ license_assignments : has
assets ||--o{ license_assignments : assigned
assets ||--o{ alerts : triggers
assets ||--o{ assets : parent_host
```
---
## Tabel Detail
### `assets`
Entitas utama — setiap device/VM/server.
| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | UUID PK | `gen_random_uuid()` |
| `asset_type` | varchar | `endpoint`, `server`, `vm`, `proxmox_host`, `network_device`, `nas`, `gis_server` |
| `hostname` | varchar UNIQUE | Hostname device |
| `machine_id` | varchar | OS machine-id (tidak lagi UNIQUE sejak migration 003) |
| `smbios_uuid` | varchar | BIOS UUID |
| `serial_number` | varchar | Serial number hardware |
| `primary_physical_mac` | varchar | MAC address utama (untuk identity fallback) |
| `zabbix_host_id` | varchar nullable | ID host di Zabbix |
| `location_id` | UUID FK → locations | Lokasi fisik |
| `parent_asset_id` | UUID FK → assets | Untuk VM: parent Proxmox host |
| `status` | varchar | `active`, `inactive`, `unknown` |
| `metadata` | jsonb | Field fleksibel/rare |
| `last_seen_at` | timestamptz | Terakhir laporan masuk |
| `deleted_at` | timestamptz nullable | Soft delete |
---
### `asset_snapshots`
Point-in-time snapshot dari laporan agent.
| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | UUID PK | |
| `asset_id` | UUID FK → assets | |
| `payload` | jsonb | Full payload agent (system, cpu, gpu, disks, network, dll) |
| `agent_version` | varchar | Versi agent saat collect |
| `collected_at` | timestamptz | Waktu collect di endpoint |
> Idempotent berdasarkan `(hostname, collected_at)` — laporan duplikat diabaikan.
---
### `software_installations`
Daftar software terinstall dari scan agent.
| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | UUID PK | |
| `asset_id` | UUID FK | |
| `name` | varchar | Nama software |
| `version` | varchar nullable | |
| `publisher` | varchar nullable | |
| `install_date` | date nullable | |
| `collected_at` | timestamptz | |
---
### `users`
User/karyawan (dari AD atau manual entry Laravel).
| Kolom | Tipe |
|-------|------|
| `id` | UUID PK |
| `username` | varchar UNIQUE |
| `display_name` | varchar |
| `email` | varchar nullable |
| `department` | varchar nullable |
---
### `locations`
Lokasi fisik asset.
| Kolom | Tipe |
|-------|------|
| `id` | UUID PK |
| `name` | varchar |
| `code` | varchar |
| `address` | varchar nullable |
---
### `alerts`
Alert dari Zabbix webhook (Fase 2).
| Kolom | Tipe | Keterangan |
|-------|------|------------|
| `id` | UUID PK | |
| `asset_id` | UUID FK nullable | |
| `zabbix_event_id` | varchar UNIQUE | ID event Zabbix |
| `severity` | varchar | `info`, `warning`, `average`, `high`, `disaster` |
| `message` | text | |
| `status` | varchar | `problem`, `resolved` |
| `fired_at` | timestamptz | |
| `resolved_at` | timestamptz nullable | |
---
### `agent_tokens`
Token autentikasi agent (jika `AGENT_TOKEN_CHECK_DB=true`).
| Kolom | Tipe |
|-------|------|
| `id` | UUID PK |
| `token_hash` | varchar UNIQUE |
| `description` | varchar |
| `is_active` | boolean |
| `created_at` | timestamptz |
---
### `asset_invoices`, `tickets`, `document_attachments` (Fase 3 — Belum Apply)
Draft di `infra/db/005_asset_documents.sql`. Belum dijalankan di DB.
| Tabel | Domain |
|-------|--------|
| `asset_invoices` | Procurement / pembelian |
| `tickets` | Service request + complain |
| `document_attachments` | File di QNAP Object Storage (ADR-003) |
---
## View
### `v_asset_hardware_latest` (migration 004, dev DB)
View yang merangkum hardware terbaru per asset dari `asset_snapshots`.
```sql
-- Contoh query
SELECT hostname, cpu_model, ram_gb, gpu_model, os_name
FROM v_asset_hardware_latest
ORDER BY hostname;
```
---
## Migration History
| File | Tanggal | Keterangan |
|------|---------|------------|
| `001_initial_schema.sql` | 2026-06-17 | Schema awal: assets, snapshots, software, users, licenses, alerts, agent_tokens |
| `002_disk_model.sql` | 2026-06-17 | Tambah kolom disk_model |
| `003_drop_machine_id_unique.sql` | 2026-06-22 | Hapus UNIQUE constraint machine_id (asset campur hardware) |
| `004_view_hardware_latest.sql` | 2026-06-22 | Buat view `v_asset_hardware_latest` |
| `005_asset_documents.sql` | 2026-06-22 | Draft: asset_invoices, tickets, document_attachments — **belum apply** |
Cara apply migration: lihat `infra/db/README.md`
---
## Catatan Penting
- **Split asset:** Satu laptop bisa punya dua asset record (ADR-002). `smbios_uuid` tidak lagi UNIQUE per laptop dual-boot.
- **Snapshot flatten:** Field hardware di payload ada di root object sekarang, bukan nested `.system` lagi untuk data baru.
- **Soft delete:** Tabel master punya `deleted_at` — jangan `DELETE` langsung.

72
GeoNetAgent/docs/decisions/naming-convention.md

@ -0,0 +1,72 @@
# Decision — Naming Convention
> **Tanggal:** 2026-06-17
> **Status:** Finalized
---
## File & Folder
| Tipe | Konvensi | Alasan |
|------|----------|--------|
| Python | `snake_case.py` | PEP 8 standard |
| PowerShell | `Verb-Noun.ps1` | PowerShell naming standard |
| SQL migration | `NNN_description.sql` (NNN = 3 digit) | Urutan apply yang jelas |
| Markdown doc | `kebab-case.md` | URL-friendly, konsisten |
| ADR | `ADR-NNN-title.md` | Standard ADR format |
---
## Database
| Hal | Konvensi | Contoh |
|-----|----------|--------|
| Nama tabel | `snake_case` plural | `asset_snapshots`, `software_installations` |
| Nama kolom | `snake_case` | `last_seen_at`, `smbios_uuid` |
| Primary key | selalu `id` (UUID) | `id UUID PRIMARY KEY DEFAULT gen_random_uuid()` |
| Foreign key | `<table>_id` | `asset_id`, `location_id` |
| Timestamp | `<event>_at` | `created_at`, `last_seen_at`, `collected_at` |
| Soft delete | `deleted_at` | nullable, null = aktif |
| View | `v_<description>` | `v_asset_hardware_latest` |
---
## API Routes
| Hal | Konvensi | Contoh |
|-----|----------|--------|
| Prefix | `/api/v1/` | Versioned API |
| Resource | `kebab-case` plural | `/api/v1/assets`, `/api/v1/agent-tokens` |
| Sub-resource | `/{id}/sub-resource` | `/api/v1/assets/{id}/snapshots` |
| Action route | `POST /api/v1/resource/action` | `/api/v1/agent/report` |
---
## PowerShell Agent
| Hal | Konvensi | Contoh |
|-----|----------|--------|
| Function | `Verb-Noun` | `Get-CpuInfo`, `Send-CollectorReport` |
| File | `Verb-Noun.ps1` | sama dengan function |
| Variable | `$camelCase` | `$cpuInfo`, `$reportPayload` |
| Hashtable key | `snake_case` | `@{cpu_model = ...; ram_gb = ...}` |
---
## Python (FastAPI)
| Hal | Konvensi | Contoh |
|-----|----------|--------|
| File | `snake_case.py` | `asset_service.py` |
| Class | `PascalCase` | `AssetSnapshot`, `AgentReport` |
| Function | `snake_case` | `get_asset_by_id`, `upsert_asset` |
| Variable | `snake_case` | `asset_id`, `collected_at` |
| Constant | `UPPER_SNAKE` | `DATABASE_URL` |
---
## Commit Messages
Format: `<type>: <deskripsi singkat dalam Bahasa Indonesia atau Inggris>`
Tidak perlu body kecuali ada penjelasan penting yang tidak muat di subject.

78
GeoNetAgent/docs/decisions/technology-stack.md

@ -0,0 +1,78 @@
# Decision — Technology Stack
> **Tanggal:** 2026-06-17
> **Status:** Finalized
---
## Agent: PowerShell 5.1
**Dipilih karena:**
- Pre-installed di semua Windows 7+ — tidak perlu deploy runtime
- WMI/CIM API paling lengkap untuk hardware inventory di Windows
- Bisa berjalan sebagai SYSTEM account via Scheduled Task
- GPO support untuk deployment massal
**Alternatif yang ditolak:**
- Python agent: perlu install Python di setiap endpoint
- C# executable: perlu .NET runtime yang spesifik, lebih kompleks untuk update
- WMI via network: perlu port WMI terbuka, security risk
---
## Collector: FastAPI (Python 3.11)
**Dipilih karena:**
- Schema validation otomatis via Pydantic (payload agent complex)
- Auto-generated OpenAPI/Swagger docs
- Async support untuk concurrent agent reports
- Developer familiar dengan Python
**Alternatif yang ditolak:**
- Laravel saja: PHP kurang cocok untuk ingress agent yang high-frequency
- Go: learning curve lebih tinggi, tim tidak familiar
- Node.js Express: tim lebih familiar Python untuk data processing
---
## Portal: Laravel (Fase 1)
**Dipilih karena:**
- Tim familiar dengan PHP/Laravel
- Eloquent ORM cocok untuk relational data CMDB
- Laravel Sanctum untuk auth, Livewire/Blade untuk UI sederhana
- Framework yang sama dengan geonet-console (konsistensi)
---
## Database: PostgreSQL 12+
**Dipilih karena:**
- JSONB support untuk payload agent yang flexible
- UUID native support
- PostgreSQL sudah running di infra (10.100.1.25)
- View dan CTE yang powerful untuk query CMDB
**Alternatif yang ditolak:**
- MySQL: JSONB kurang mature di versi lama
- MongoDB: overkill untuk data yang mostly relational
- SQLite: tidak cocok untuk multi-service access
---
## Object Storage: QNAP QuObjects (S3-compatible)
**Dipilih karena:**
- On-premise — data tidak keluar jaringan internal
- S3-compatible API — bisa pakai library standard
- Infrastruktur QNAP sudah ada dan terbukti (sudah dipakai mysupperapps_FE_PWA)
- Tidak ada biaya tambahan
---
## Monitoring: Zabbix (existing)
**Dipilih karena:**
- Sudah deployed dan dikonfigurasi
- GeoNetAgent melengkapi, bukan menggantikan Zabbix
- Tidak perlu build monitoring stack baru

117
GeoNetAgent/docs/deployment.md

@ -0,0 +1,117 @@
# Deployment Guide
> **Terakhir Diperbarui:** 2026-06-29
---
## Deploy FastAPI Collector
### Step 1 — Buat archive di lokal (Windows PowerShell)
```powershell
tar -czf "$env:TEMP\collector.tar.gz" `
--exclude=".venv" `
--exclude="__pycache__" `
--exclude=".pytest_cache" `
--exclude="*.egg-info" `
-C "D:\Project\app on git\GeoNetAgent\collector" .
```
### Step 2 — Upload ke server
```powershell
scp -o "MACs=hmac-sha1" "$env:TEMP\collector.tar.gz" "root@10.100.1.24:/opt/stacks/geonetagent-collector/collector.tar.gz"
```
### Step 3 — Extract, rebuild, restart
```bash
ssh -m hmac-sha1 root@10.100.1.24 -p 22 "cd /opt/stacks/geonetagent-collector && tar -xzf collector.tar.gz -C collector-src && rm collector.tar.gz && docker compose build --no-cache && docker compose up -d"
```
Atau gunakan script:
```powershell
.\infra\collector\push_collector.ps1
```
### Step 4 — Verifikasi
```bash
ssh -m hmac-sha1 root@10.100.1.24 -p 22 "docker ps --filter name=geonet-collector && docker logs geonet-collector --tail 20"
curl https://collector.gisportal.id/health
```
---
## Publish Agent ke Server
Setelah bump versi agent (update `agent/VERSION` + `agent/install/manifest.json`):
```powershell
.\infra\collector\publish_agent_install.ps1
```
Script ini mengupload file agent ke `agent.gisportal.id/install/` sehingga:
- User baru mendapat versi terbaru saat install
- Agent yang sudah terpasang auto-update saat kirim laporan berikutnya (cek `manifest.json` vs `VERSION` lokal)
---
## Deploy Nginx Config
```bash
bash infra/collector/deploy_nginx.sh
```
Atau manual:
```powershell
scp -o "MACs=hmac-sha1" "D:\Project\app on git\GeoNetAgent\infra\nginx\collector.conf" "root@10.100.1.24:/etc/nginx/conf.d/production/collector.conf"
ssh -m hmac-sha1 root@10.100.1.24 -p 22 "nginx -t && nginx -s reload"
```
---
## Apply DB Migration
```powershell
# Upload migration file
scp -o "MACs=hmac-sha1" "D:\Project\app on git\GeoNetAgent\infra\db\00X_migration.sql" "root@10.100.1.25:/tmp/migration.sql"
# Apply di server DB
ssh -m hmac-sha1 root@10.100.1.25 -p 22 "psql -U geonet_collector -d geonetagent_dev -f /tmp/migration.sql"
```
---
## Install Agent ke Endpoint (User)
User menjalankan di PowerShell Admin:
```powershell
Invoke-WebRequest -Uri 'https://agent.gisportal.id/install/GeoNetAgent-Install.ps1' -OutFile "$env:TEMP\GeoNetAgent-Install.ps1" -UseBasicParsing
& "$env:TEMP\GeoNetAgent-Install.ps1"
```
Atau dengan `-SkipInitialRun` (install task saja, tanpa kirim laporan langsung):
```powershell
& "$env:TEMP\GeoNetAgent-Install.ps1" -SkipInitialRun
```
---
## Rollout Massal via GPO
Lihat `agent/gpo/README.md` untuk panduan script startup GPO.
---
## Checklist Deployment
- [ ] Update `agent/VERSION` (jika bump versi agent)
- [ ] Update `agent/install/manifest.json` (file list + versi)
- [ ] `publish_agent_install.ps1` — setelah bump versi agent
- [ ] `push_collector.ps1` — setelah perubahan collector/UI
- [ ] Verifikasi `docker ps` — container running
- [ ] Verifikasi `https://collector.gisportal.id/health` — 200 OK
- [ ] Verifikasi `https://agent.gisportal.id/install/` — accessible
- [ ] Update `Riwayat milestone` di `.cursor/progress.md`
- [ ] Update `Sync terakhir` di `perintah.md`

165
GeoNetAgent/docs/development.md

@ -0,0 +1,165 @@
# Development Guide
> **Terakhir Diperbarui:** 2026-06-29
---
## Setup Lokal — FastAPI Collector
### Prerequisites
- Python 3.11+
- PostgreSQL 12+ (atau koneksi ke 10.100.1.25)
### Install & Run
```bash
cd GeoNetAgent/collector
python -m venv .venv
.\.venv\Scripts\activate # Windows
# atau: source .venv/bin/activate # Linux/Mac
pip install -e ".[dev]"
cp ../.env.example .env
# Edit .env: isi DATABASE_URL dan AGENT_BEARER_TOKEN
uvicorn app.main:app --reload --port 8000
```
API tersedia di `http://localhost:8000`
Swagger UI: `http://localhost:8000/docs`
UI CMDB: `http://localhost:8000/ui/`
---
## Setup Lokal — PowerShell Agent
Agent berjalan di endpoint Windows. Untuk pengembangan:
```powershell
# Jalankan collect lokal (tanpa kirim ke server)
cd GeoNetAgent\agent
.\Collect-GeoNetReport.ps1
# Kirim laporan ke server dev
.\Send-GeoNetReport.ps1 -CollectorUrl "http://localhost:8000/api/v1/agent/report" -Token "dev-agent-token-change-me"
```
---
## Struktur Kode Collector
```
collector/
├── app/
│ ├── main.py — FastAPI app init, lifespan, mount UI
│ ├── config.py — Settings dari .env (pydantic-settings)
│ ├── api/ — Route handlers
│ │ ├── agent.py — POST /api/v1/agent/report
│ │ ├── assets.py — GET /api/v1/assets (read API)
│ │ └── health.py — GET /health
│ ├── schemas/ — Pydantic schemas (request/response)
│ ├── services/ — Business logic
│ ├── db/ — DB session, models
│ └── core/ — Middleware, dependencies
├── static/ — UI files (HTML/CSS/JS) untuk /ui/
└── tests/ — pytest tests
```
---
## Struktur Kode Agent
```
agent/
├── Collect-GeoNetReport.ps1 — Entry point collect (gather payload)
├── Send-GeoNetReport.ps1 — Entry point kirim laporan
├── UserAgent.ps1 — Script user: cek VERSION + kirim laporan
├── Install-GeoNetAgent.ps1 — Installer (setup files + scheduled task)
├── lib/
│ ├── Get-SystemInfo.ps1 — OS, manufacturer, model, BIOS
│ ├── Get-CpuInfo.ps1 — CPU cores, clock, cache, arch
│ ├── Get-MemoryInfo.ps1 — RAM detail, DDR type, part no
│ ├── Get-GpuInfo.ps1 — GPU dedicated memory
│ ├── Get-DiskInfo.ps1 — Logical disk info
│ ├── Get-PhysicalDiskInfo.ps1 — Physical disk model, serial
│ ├── Get-NetworkInfo.ps1 — NIC, MAC, IP, is_built_in
│ ├── Get-PeripheralsInfo.ps1 — USB/BT/audio/monitor, is_external
│ ├── Get-DisplaySettings.ps1 — Monitor EDID, resolusi
│ ├── Get-HealthInfo.ps1 — Disk usage, battery
│ ├── Get-SoftwareInfo.ps1 — Installed software list
│ ├── Get-AssetIdentity.ps1 — Resolve agent_id (ADR-001)
│ ├── Build-GeoNetReportPayload.ps1 — Assemble full JSON payload
│ ├── Agent-InstallCore.ps1 — Instalasi file + task
│ ├── Update-GeoNetAgent.ps1 — Auto-update dari manifest server
│ └── Send-CollectorReport.ps1 — HTTP POST ke collector
└── gpo/ — GPO startup scripts
```
---
## Coding Standard
### Python (FastAPI)
- `snake_case` untuk variabel, fungsi, file
- Type hints wajib di semua function signature
- Pydantic v2 untuk schema validasi
- Route handler tipis — logic di `services/`
- Gunakan `Depends(get_db)` untuk DB session
### PowerShell (Agent)
- Satu file per sensor di `lib/`
- Fungsi nama: `Get-<Sensor>Info` → return hashtable
- Jangan hardcode token/URL — baca dari `config.json`
- Jangan gunakan `$env:` literal di file yang di-serve Nginx
### SQL
- PK: UUID (`gen_random_uuid()`)
- Timestamp: `timestamptz`
- Soft delete: kolom `deleted_at`
- Nama tabel: `snake_case` plural
---
## Commit Convention
Format: `<type>: <deskripsi>`
| Type | Kapan |
|------|-------|
| `feat` | Fitur baru |
| `fix` | Bug fix |
| `agent` | Perubahan PowerShell agent |
| `collector` | Perubahan FastAPI collector |
| `infra` | Nginx, Docker, DB migration |
| `docs` | Dokumentasi |
| `chore` | Config, deps |
---
## Testing
```bash
# Run tests collector
cd collector
pytest tests/ -v
# Manual test agent report
curl -X POST https://agent.gisportal.id/api/v1/agent/report \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d @samples/agent-reports/sample-report-v0.1.10.json
```
---
## Menambah Sensor Baru ke Agent
1. Buat `agent/lib/Get-<Sensor>Info.ps1`
2. Import di `Build-GeoNetReportPayload.ps1`
3. Tambah field ke schema Pydantic di `collector/app/schemas/`
4. Update `collector/docs/API.md` + `docs/api/service-collector.md`
5. Bump versi di `agent/VERSION` + `agent/install/manifest.json`
6. Publish: `.\infra\collector\publish_agent_install.ps1`
7. Update `docs/project-status.md` + `.cursor/progress.md`

37
GeoNetAgent/docs/diagrams/agent-report-flow.mmd

@ -0,0 +1,37 @@
sequenceDiagram
participant TASK as Windows Scheduled Task
participant PS as PowerShell Agent
participant NGX as Nginx (10.100.1.24)
participant COL as FastAPI Collector
participant PG as PostgreSQL
TASK->>PS: Trigger setiap 3 jam (SYSTEM account)
PS->>PS: Baca config.json (URL, token)
PS->>NGX: GET /install/manifest.json
NGX-->>PS: {version, files[]}
alt Versi server lebih baru
PS->>NGX: Download file-file terbaru
PS->>PS: Replace file lokal
Note over PS: Auto-update selesai
end
PS->>PS: Get-SystemInfo, Get-CpuInfo, Get-GpuInfo
PS->>PS: Get-MemoryInfo, Get-DiskInfo, Get-PhysicalDiskInfo
PS->>PS: Get-NetworkInfo, Get-PeripheralsInfo
PS->>PS: Get-SoftwareInfo, Get-HealthInfo
PS->>PS: Get-AssetIdentity → resolve merge_key
PS->>PS: Build-GeoNetReportPayload → JSON
PS->>NGX: POST /api/v1/agent/report\nAuthorization: Bearer token\nContent-Type: application/json
NGX->>COL: proxy_pass :18000
COL->>COL: Validate schema (Pydantic)
COL->>COL: Validate bearer token
COL->>PG: UPSERT assets (merge_key)
PG-->>COL: asset.id
COL->>PG: INSERT asset_snapshots (payload)
COL->>PG: UPSERT software_installations
PG-->>COL: OK
COL-->>NGX: 200 {status: ok, asset_id}
NGX-->>PS: 200 OK
PS->>PS: Log sukses

23
GeoNetAgent/docs/diagrams/deployment.mmd

@ -0,0 +1,23 @@
graph LR
subgraph DEV["Developer (Windows Laptop)"]
REPO["GeoNetAgent Repo\nd:/Project/app on git"]
PS_SCRIPTS["PowerShell\nDeploy Scripts"]
end
subgraph SERVER24["10.100.1.24 — App Server (CentOS 8)"]
NGX_SVC["Nginx\n:80/:443"]
DOCKER["Docker\ngeonet-collector\n:18000"]
STATIC_DIR["/var/www/agent-install/\n(static web install)"]
end
subgraph SERVER25["10.100.1.25 — DB (Ubuntu 18.04)"]
PGSQL[("PostgreSQL 12\ngeonetagent_dev")]
end
REPO -->|"publish_agent_install.ps1\nSCP upload"| STATIC_DIR
REPO -->|"push_collector.ps1\nSCP + docker compose"| DOCKER
REPO -->|"deploy_nginx.sh\nSCP + nginx reload"| NGX_SVC
REPO -->|"SCP migration.sql\npsql apply"| PGSQL
DOCKER --> PGSQL
NGX_SVC -->|"proxy_pass :18000"| DOCKER
NGX_SVC --> STATIC_DIR

41
GeoNetAgent/docs/diagrams/system.mmd

@ -0,0 +1,41 @@
graph TB
subgraph ENDPOINTS["Endpoint Windows"]
PS["PowerShell Agent\nv0.1.10-phase0\nScheduled Task 3 jam"]
end
subgraph INTERNET["Internet / WAN"]
USER_IT["IT Admin\n(Browser)"]
end
subgraph SERVER24["10.100.1.24 — App Server"]
NGX["Nginx\nSSL termination"]
COL["FastAPI Collector\n:18000\n/api/ + /ui/"]
STATIC["/var/www/agent-install/\nStatic files agent"]
end
subgraph SERVER25["10.100.1.25 — DB Server"]
PG[("PostgreSQL 12\ngeonetagent_dev / geonetagent")]
end
subgraph INFRA["Infrastruktur Existing"]
ZBX["Zabbix\n10.100.1.42"]
AD["AD / LDAP\n10.100.1.40"]
QNAP["QNAP NAS\n10.100.1.10\nObject Storage"]
PROX["Proxmox\n(VM sync Fase 3)"]
end
PS -->|"HTTPS POST\nBearer token"| NGX
USER_IT -->|"HTTPS GET /install/"| NGX
USER_IT -->|"HTTPS /ui/"| NGX
NGX -->|"agent.gisportal.id"| COL
NGX -->|"collector.gisportal.id"| COL
NGX --> STATIC
COL --> PG
COL -.->|"Fase 3"| PROX
COL -.->|"Fase 2"| ZBX
ZBX -.->|"webhook Fase 2"| COL
style PS fill:#4CAF50,color:#fff
style COL fill:#2196F3,color:#fff
style PG fill:#FF9800,color:#fff
style NGX fill:#607D8B,color:#fff

69
GeoNetAgent/docs/environment.md

@ -0,0 +1,69 @@
# Environment Variables
> **Terakhir Diperbarui:** 2026-06-29
> ⚠ **Jangan commit nilai secret ke repository.**
---
## FastAPI Collector (`.env` di server)
File: `/opt/stacks/geonetagent-collector/.env` (atau path deploy di 10.100.1.24)
| Variable | Contoh | Keterangan |
|----------|--------|------------|
| `DATABASE_URL` | `postgresql://user:pass@10.100.1.25:5432/geonetagent` | **SECRET** — lihat `/root/geonetagent_dev.credentials` di server 10.100.1.25 |
| `COLLECTOR_SECRET_KEY` | (string random) | **SECRET** — untuk signing internal |
| `AGENT_BEARER_TOKEN` | `dev-agent-token-change-me` | **SECRET** — token yang dikirim agent di header Authorization |
| `AGENT_TOKEN_CHECK_DB` | `false` | `true` = validasi token ke DB, `false` = token statis |
| `API_BEARER_TOKEN` | (kosong = sama dengan AGENT) | Token untuk read API eksternal (FE) |
| `CORS_ORIGINS` | `*` atau `https://my.gisportal.id` | CORS allowed origins |
| `AGENT_TOKEN_SALT` | (string random) | Salt untuk hash token DB |
| `APP_TIMEZONE` | `Asia/Jakarta` | Timezone aplikasi |
---
## Laravel Portal (Planned)
File: `.env` di stack Laravel (belum scaffold)
| Variable | Keterangan |
|----------|------------|
| `APP_KEY` | Laravel encryption key |
| `DB_HOST` | 10.100.1.25 |
| `DB_DATABASE` | `geonetagent` |
| `DB_USERNAME` | **SECRET** |
| `DB_PASSWORD` | **SECRET** — lihat `/root/geonetagent_dev.credentials` |
| `ZABBIX_API_URL` | URL Zabbix API (fase 2) |
| `ZABBIX_API_TOKEN` | **SECRET** (fase 2) |
| `ZABBIX_WEBHOOK_SECRET` | **SECRET** (fase 2) |
---
## PowerShell Agent (`config.json` di endpoint)
File: `%ProgramData%\GeoNetAgent\config.json`
| Key | Keterangan |
|-----|------------|
| `CollectorUrl` | `https://agent.gisportal.id/api/v1/agent/report` |
| `AgentToken` | **SECRET** — site agent token (bukan kredensial admin) |
| `ReportIntervalHours` | Interval laporan (default: 3 jam) |
Template: `agent/config.example.json`
---
## Template File
Gunakan `.env.example` di root sebagai template. Format variabel sudah sesuai — hanya isi nilai yang kosong atau bertanda `CHANGE_ME`.
---
## Lokasi Secret
| Secret | Lokasi |
|--------|--------|
| DB credentials | `/root/geonetagent_dev.credentials` di 10.100.1.25 |
| Agent bearer token | `.env` di server deploy collector |
| QNAP Object Storage key | `infra/qnap/README.md` (referensi) |
| SSH private keys | `~/.ssh/` di laptop developer |

35
GeoNetAgent/docs/glossary.md

@ -0,0 +1,35 @@
# Glossary
> **Terakhir Diperbarui:** 2026-06-29
---
| Istilah | Definisi |
|---------|----------|
| **GeoNetAgent** | Nama proyek — portal IT + CMDB berbasis agent untuk inventaris endpoint Windows |
| **Agent** | PowerShell script yang berjalan di endpoint user (laptop/PC) via Scheduled Task setiap 3 jam |
| **Collector** | FastAPI service yang menerima laporan dari agent dan menyimpan ke PostgreSQL |
| **Portal** | Laravel UI untuk tim IT (belum scaffold — Fase 1 blocker) |
| **Snapshot** | Rekaman point-in-time data hardware/software dari laporan agent (bukan live metric) |
| **Asset** | Entitas terkelola: endpoint, server, VM, NAS, network device, dll |
| **CMDB** | Configuration Management Database — inventaris aset IT beserta relasi dan metadata |
| **ADR** | Architecture Decision Record — dokumen keputusan arsitektur permanen |
| **DoD** | Definition of Done — kriteria suatu fase dinyatakan selesai |
| **agent_id** | Identifier unik asset hasil resolusi ADR-001 (serial → smbios_uuid → MAC → machine_id → hostname) |
| **smbios_uuid** | UUID dari BIOS/SMBIOS — digunakan sebagai identitas asset utama |
| **machine_id** | OS-level machine identifier (bukan UNIQUE sejak migration 003) |
| **payload** | Data JSON yang dikirim agent ke collector: system, cpu, gpu, disks, network, peripherals, software |
| **v_asset_hardware_latest** | Database view yang merangkum hardware snapshot terbaru per asset |
| **manifest.json** | File di server listing semua file agent + versi — dipakai untuk auto-update |
| **auto-update** | Mekanisme agent mengunduh ulang file terbaru dari server sebelum kirim laporan (cek VERSION vs manifest) |
| **GPO** | Group Policy Object — mekanisme Windows untuk deploy script ke semua endpoint domain |
| **Web install** | Cara install agent via 2 baris PowerShell dari URL `agent.gisportal.id/install/` |
| **SSI** | Server-Side Includes — fitur Nginx yang memotong `$env:` di file `.ps1` (wajib `ssi off`) |
| **Zabbix** | Platform monitoring existing — GeoNetAgent melengkapi, bukan menggantikan |
| **Proxmox** | Platform virtualisasi — VM-nya akan di-sync ke CMDB di Fase 3 |
| **QNAP OSS** | QNAP QuObjects Object Storage — penyimpanan dokumen procurement/service/complain (ADR-003) |
| **Split asset** | ADR-002: satu chassis dengan hardware campur (laptop Dell + PC Gigabyte) dipecah jadi 2 asset record |
| **form_factor** | Field yang menentukan Laptop/PC/Desktop berdasarkan chassis type dari WMI |
| **physical_disks** | Array disk fisik (NVMe/HDD/SSD) — berbeda dari `disks` yang berisi logical partition |
| **is_external** | Flag peripheral yang menandakan apakah perangkat eksternal (USB) atau built-in |
| **is_built_in** | Flag NIC yang menandakan NIC onboard vs USB/adapter |

80
GeoNetAgent/docs/issues.md

@ -0,0 +1,80 @@
# Known Issues
> **Terakhir Diperbarui:** 2026-06-29
---
## ✅ Fixed
### [FIXED] Nginx memotong file `.ps1` di `/install/``$env:` hilang
- **Dampak:** File PowerShell yang diunduh dari `agent.gisportal.id/install/` terpotong. Kode setelah `$env:` hilang → agent gagal install.
- **Penyebab:** Nginx SSI menginterpretasikan `$env:` sebagai variabel SSI → potong respons.
- **Fix:** Tambah `ssi off` di nginx config block `location /install/`.
- **Tanggal Fix:** 2026-06-22
---
### [FIXED] `machine_id` UNIQUE constraint — split asset hardware campur
- **Dampak:** Satu laptop yang diinstall dua OS (dual-boot) atau satu chassis yang berisi dua hardware berbeda gagal masuk DB karena `machine_id` UNIQUE.
- **Penyebab:** Constraint terlalu ketat — `machine_id` bisa sama di hardware yang berbeda dalam chassis yang sama.
- **Fix:** Migration 003 — hapus UNIQUE constraint `machine_id`. Gunakan kombinasi `smbios_uuid + hostname` untuk identity. Lihat ADR-002.
- **Tanggal Fix:** 2026-06-22
---
## 🔶 Known Limitation
### [LIMITATION] Collector port 8000 expose ke internal network
- **Dampak:** `http://10.100.1.24:8000/ui/` bisa diakses tanpa auth dari subnet 10.x dan 192.168.x.
- **Workaround:** Hanya untuk internal LAN. UI read-only tidak mengekspos data sensitif.
- **Rencana Fix:** Tambah auth (Laravel session) di Fase 1.
---
### [LIMITATION] Nama USB device sering generik
- **Dampak:** Peripherals HID (keyboard, mouse) terdeteksi tapi nama sering `USB Input Device` bukan nama spesifik.
- **Penyebab:** WMI `Win32_PnPEntity` tidak selalu punya nama deskriptif untuk HID generic.
- **Workaround:** Tidak ada saat ini. Klasifikasi HID yang lebih baik ada di backlog (v0.1.12+).
---
### [LIMITATION] Printer belum dikumpulkan agent
- **Dampak:** Data printer tidak ada di inventory.
- **Rencana:** Tambah `Win32_Printer` / PnP PrintQueue di v0.1.11+.
---
### [LIMITATION] Laravel portal belum ada
- **Dampak:** UI hanya tersedia di FastAPI `/ui/` yang read-only tanpa auth.
- **Rencana:** Scaffold Laravel di Fase 1 (blocker untuk DoD Fase 1).
---
## 🔴 Open Issues
### [OPEN] Uji agent di laptop kedua `BKK-MINIPC`
- **Dampak:** Belum diverifikasi agent berjalan di PC non-laptop.
- **Prioritas:** Low
- **File:** `agent/` — tidak ada perubahan kode
---
## Cara Melaporkan Issue Baru
Tambahkan entri di file ini:
```markdown
### [OPEN] Judul Issue
- **Dampak:** Apa yang terasa salah dari perspektif user
- **Penyebab:** Root cause (jika diketahui)
- **Workaround:** Cara sementara
- **Prioritas:** High / Medium / Low
- **Ditemukan:** YYYY-MM-DD
```

90
GeoNetAgent/docs/monitoring.md

@ -0,0 +1,90 @@
# Monitoring
> **Terakhir Diperbarui:** 2026-06-29
---
## Stack Monitoring
| Service | URL | Digunakan GeoNetAgent |
|---------|-----|----------------------|
| **Zabbix** | `zabbix.gisportal.id` (10.100.1.42) | 🔶 Fase 2 — webhook + host status |
| **Collector Health** | `https://collector.gisportal.id/health` | ✅ Sudah ada |
| **Uptime Kuma** | `uptime.gisportal.id` | 🔶 Belum dikonfigurasi untuk GeoNetAgent |
---
## Boundary Zabbix vs GeoNetAgent
| Fungsi | Tool | Catatan |
|--------|------|---------|
| Uptime, CPU live, RAM live | **Zabbix** | Real-time metrics — jangan duplikasi |
| Alert firing (threshold) | **Zabbix** | Trigger + media type webhook → Laravel |
| Alert history di portal | **GeoNetAgent** (Laravel) | Dari webhook Zabbix → tabel `alerts` |
| Asset inventory (hardware) | **GeoNetAgent** (FastAPI) | Point-in-time snapshot, bukan live |
| Software inventory | **GeoNetAgent** | Zabbix tidak punya fitur ini |
| Host status badge (online/offline) | **Zabbix API** via Laravel | Cache 60 detik |
---
## Health Check Collector
```bash
# HTTP
curl https://collector.gisportal.id/health
# Expected response
{"status": "ok", "db": "connected"}
```
---
## Cek Status Container
```bash
ssh -m hmac-sha1 root@10.100.1.24 -p 22
docker ps --filter name=geonet-collector --format "{{.Names}} {{.Status}}"
docker logs geonet-collector --tail 50 -f
```
---
## Monitoring Agent Activity
Cek laporan terbaru masuk ke DB:
```sql
SELECT a.hostname, s.agent_version, s.collected_at
FROM assets a
JOIN asset_snapshots s ON s.asset_id = a.id
ORDER BY s.collected_at DESC
LIMIT 20;
```
Cek aset yang tidak lapor > 24 jam:
```sql
SELECT hostname, last_seen_at,
EXTRACT(EPOCH FROM (NOW() - last_seen_at))/3600 AS hours_since
FROM assets
WHERE status = 'active'
AND last_seen_at < NOW() - INTERVAL '24 hours'
ORDER BY last_seen_at;
```
---
## Integrasi Zabbix (Fase 2 — Belum Implementasi)
Rencana arsitektur:
```
Zabbix trigger PROBLEM
→ Media type webhook → POST Laravel /webhooks/zabbix
→ Normalize → insert tabel alerts
→ Tampil di portal IT
Laravel dashboard
→ Zabbix API host.get → status badge hijau/merah
→ Cache 60 detik
```
Lihat `.cursor/monitoring-architecture.md` untuk detail.

105
GeoNetAgent/docs/onboarding/coding-standard.md

@ -0,0 +1,105 @@
# Onboarding — Coding Standard
---
## Python (FastAPI Collector)
- `snake_case` untuk variabel, fungsi, file, folder
- Type hints wajib di semua function signature
- Pydantic v2 untuk schema validasi — model di `app/schemas/`, bukan inline di routes
- Route handler tipis — logic di `app/services/`
- `Depends(get_db)` untuk DB session — jangan buat session langsung di route
- Import order: stdlib → third-party → local
```python
# Benar
async def get_assets(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 20,
) -> list[AssetListItem]:
return asset_service.list_assets(db, skip=skip, limit=limit)
# Salah — logic di route, tanpa type hint
@router.get("/assets")
def assets(db=Depends(get_db)):
return db.query(Asset).all()
```
---
## PowerShell (Agent)
- Satu file per sensor di `agent/lib/`
- Nama fungsi/file: `Verb-Noun` format (`Get-CpuInfo`, `Send-CollectorReport`)
- Return hashtable dari setiap Get- function — gunakan `@{}` bukan PSObject
- Jangan hardcode URL atau token — baca dari `config.json`
- **Jangan gunakan `$env:` literal** di file `.ps1` yang akan di-serve Nginx
```powershell
# Benar
function Get-CpuInfo {
$cpu = Get-WmiObject Win32_Processor | Select-Object -First 1
return @{
model = $cpu.Name.Trim()
cores = [int]$cpu.NumberOfCores
threads = [int]$cpu.NumberOfLogicalProcessors
}
}
# Salah — jangan return object langsung
function Get-CpuInfo {
return Get-WmiObject Win32_Processor
}
```
---
## SQL / Database
- PK: UUID (`gen_random_uuid()`) — jangan integer auto-increment
- Timestamp: `timestamptz` — jangan `timestamp` tanpa timezone
- Nama tabel: `snake_case` plural (`asset_snapshots`, tidak `AssetSnapshot`)
- Soft delete: `deleted_at` nullable — jangan `DELETE` pada tabel master
- Setiap migration: file baru bernomor (`NNN_description.sql`)
---
## Commit Convention
```
<type>: <deskripsi singkat>
```
| Type | Kapan |
|------|-------|
| `feat` | Fitur baru |
| `fix` | Bug fix |
| `agent` | Perubahan agent PowerShell |
| `collector` | Perubahan FastAPI |
| `infra` | Nginx, Docker, DB migration |
| `docs` | Dokumentasi saja |
| `chore` | Config, deps, cleanup |
Contoh:
```
agent: tambah Get-PrinterInfo.ps1 untuk inventory printer
collector: fix upsert conflict pada asset dengan machine_id duplikat
infra: apply migration 003 drop machine_id unique constraint
docs: update ADR-002 split asset hardware campur
```
---
## Naming Convention
| Hal | Konvensi | Contoh |
|-----|----------|--------|
| Python file | `snake_case.py` | `asset_service.py` |
| PS1 file | `Verb-Noun.ps1` | `Get-CpuInfo.ps1` |
| SQL migration | `NNN_description.sql` | `003_drop_unique.sql` |
| DB tabel | `snake_case` plural | `asset_snapshots` |
| DB kolom | `snake_case` | `last_seen_at` |
| FastAPI route | `/api/v1/kebab-case` | `/api/v1/agent/report` |
| Doc file | `kebab-case.md` | `system-overview.md` |
| ADR | `ADR-NNN-title.md` | `ADR-001-agent-asset-identity.md` |

94
GeoNetAgent/docs/onboarding/first-day.md

@ -0,0 +1,94 @@
# Onboarding — Hari Pertama
> Untuk developer baru, IT admin baru, dan AI Agent yang baru masuk ke proyek ini.
---
## 1. Baca Dulu
Urutan wajib:
1. `docs/context-index.md` — peta navigasi seluruh proyek
2. `README.md` — ringkasan + quick start
3. `.cursor/blueprint.md` — visi, fase, modul
4. `.cursor/progress.md` — status fase aktif
---
## 2. Pahami Proyek
GeoNetAgent adalah **IT Asset CMDB ringan** yang:
- Mengumpulkan data hardware/software dari endpoint Windows via **PowerShell Agent** (berjalan otomatis setiap 3 jam)
- Menyimpan ke **PostgreSQL** via **FastAPI Collector**
- Menampilkan di UI CMDB (**Collector UI** saat ini, **Laravel Portal** nanti)
**Bukan monitoring real-time** — itu urusan Zabbix. GeoNetAgent melengkapi Zabbix dengan inventaris hardware/software.
---
## 3. Struktur Monorepo
```
agent/ ← PowerShell scripts (install ke endpoint Windows)
collector/ ← FastAPI Python (server penerima laporan)
infra/ ← Nginx, DB migration, Docker, QNAP
docs/ ← Dokumentasi (kamu di sini!)
.cursor/ ← AI context (blueprint, schema, API contracts)
```
---
## 4. URL yang Perlu Diketahui
| URL | Fungsi |
|-----|--------|
| `https://agent.gisportal.id/install/` | Web install agent untuk user |
| `https://agent.gisportal.id/api/v1/agent/report` | POST endpoint agent |
| `https://collector.gisportal.id/ui/` | UI CMDB (read-only) |
| `https://collector.gisportal.id/docs` | Swagger UI API |
---
## 5. Fase Yang Aktif Saat Ini
**Fase 1 — Foundation** (8 dari 9 selesai)
Yang tersisa: Laravel asset index page + auth IT user lokal.
Jangan mulai Fase 2 (Zabbix) sebelum Fase 1 DoD selesai.
---
## 6. Cara Kerja Agent
```
Endpoint Windows
→ PowerShell Scheduled Task (SYSTEM, 3 jam)
→ WMI scan (CPU, RAM, GPU, disk, software, peripherals)
→ POST https://agent.gisportal.id/api/v1/agent/report
→ FastAPI → PostgreSQL
```
---
## 7. Akses Server
Semua server butuh **SSH key** yang sudah dikonfigurasi. Lihat `docs/server.md` untuk command lengkap.
> SSH commands ke server **wajib minta persetujuan user** sebelum dijalankan.
---
## 8. Aturan Penting
- Jangan commit `.env` atau secret ke repo
- Jangan implementasi Fase N+1 jika Fase N belum DoD
- Setelah selesai: update `perintah.md` + `.cursor/progress.md`
- ADR tidak boleh dihapus — buat baru jika keputusan berubah
---
## Lanjutkan ke
- [`local-setup.md`](local-setup.md) — cara setup lokal untuk dev
- [`coding-standard.md`](coding-standard.md) — standar kode
- [`docs/development.md`](../development.md) — panduan pengembangan lengkap

99
GeoNetAgent/docs/onboarding/local-setup.md

@ -0,0 +1,99 @@
# Onboarding — Local Setup
---
## Prerequisites
- Python 3.11+
- PostgreSQL 12+ (lokal atau koneksi ke 10.100.1.25)
- PowerShell 5.1+ (Windows, untuk test agent)
- Git
---
## Setup FastAPI Collector
```bash
cd GeoNetAgent/collector
# Buat virtual environment
python -m venv .venv
# Aktivasi
.\.venv\Scripts\activate # Windows PowerShell
source .venv/bin/activate # Linux/Mac
# Install dependencies
pip install -e ".[dev]"
# Copy env template
cp ../.env.example .env
```
Edit `.env`:
```env
DATABASE_URL=postgresql://user:pass@localhost:5432/geonetagent_dev
AGENT_BEARER_TOKEN=dev-agent-token-change-me
CORS_ORIGINS=*
```
```bash
# Jalankan dev server
uvicorn app.main:app --reload --port 8000
```
Buka `http://localhost:8000/docs` untuk Swagger UI.
---
## Setup DB Lokal (opsional)
Jika tidak mau koneksi ke server 10.100.1.25:
```bash
# Buat DB lokal
createdb geonetagent_dev
# Apply semua migration
psql geonetagent_dev -f infra/db/001_initial_schema.sql
psql geonetagent_dev -f infra/db/002_disk_model.sql
psql geonetagent_dev -f infra/db/003_drop_machine_id_unique.sql
psql geonetagent_dev -f infra/db/004_view_hardware_latest.sql
```
---
## Test Agent Lokal
```powershell
# Kirim sample payload ke server lokal
curl -X POST http://localhost:8000/api/v1/agent/report `
-H "Authorization: Bearer dev-agent-token-change-me" `
-H "Content-Type: application/json" `
-d (Get-Content "samples/agent-reports/sample-report-v0.1.10.json" -Raw)
```
---
## Verifikasi
```bash
# Health check
curl http://localhost:8000/health
# Expected: {"status": "ok", "db": "connected"}
# List assets (setelah ada laporan masuk)
curl -H "Authorization: Bearer dev-agent-token-change-me" \
http://localhost:8000/api/v1/assets
```
---
## Tools Direkomendasikan
| Tool | Fungsi |
|------|--------|
| VS Code | IDE utama |
| Thunder Client / Postman | Test API |
| pgAdmin / DBeaver | DB GUI |
| Git Graph extension | Visualisasi branch |

93
GeoNetAgent/docs/project-status.md

@ -0,0 +1,93 @@
# Project Status
> **Terakhir Diperbarui:** 2026-06-29
---
## Snapshot Cepat
| Item | Status |
|------|--------|
| **Fase aktif** | Fase 1 — Foundation 🔄 |
| **Agent versi** | v0.1.10-phase0 |
| **Collector** | Running di `collector.gisportal.id` |
| **Database** | `geonetagent_dev` PostgreSQL 12 di 10.100.1.25 (dev) |
| **Portal Laravel** | ❌ Belum di-scaffold |
| **Blocker utama** | Laravel asset index page |
---
## Modul yang Sudah Selesai
| Modul | Versi/Tanggal | Keterangan |
|-------|--------------|------------|
| ✅ DB Schema migration 001 | 2026-06-17 | Initial schema: assets, snapshots, software, users, licenses |
| ✅ DB migration 002 | 2026-06-17 | disk_model kolom |
| ✅ DB migration 003 | 2026-06-22 | Drop machine_id UNIQUE constraint |
| ✅ DB migration 004 | 2026-06-22 | View `v_asset_hardware_latest` |
| ✅ FastAPI `/api/v1/agent/report` | 2026-06-17 | Ingress laporan agent, upsert assets + snapshots |
| ✅ FastAPI `/api/v1/assets` read API | 2026-06-17 | List, detail, filter |
| ✅ FastAPI `/health` | 2026-06-17 | Health check endpoint |
| ✅ FastAPI UI `/ui/` | 2026-06-22 | Read-only UI untuk CMDB data |
| ✅ Agent v0.1.6 | 2026-06-17 | Peripherals USB/BT/audio, scheduled task 3 jam |
| ✅ Agent v0.1.8 | 2026-06-22 | gpus[], disk model, auto-update, fix nginx $env: |
| ✅ Agent v0.1.9 | 2026-06-22 | physical_disks[], RAM DDR, GPU detail, form_factor |
| ✅ Agent v0.1.10 | 2026-06-22 | cpu{} detail (cores, clock, cache, arch), deploy PS1 |
| ✅ Web install | 2026-06-17 | `agent.gisportal.id/install/` + auto-update via manifest |
| ✅ Nginx reverse proxy | 2026-06-17 | agent.gisportal.id + collector.gisportal.id |
| ✅ Asset identity merge | 2026-06-17 | ADR-001: serial → smbios_uuid → MAC → machine_id |
| ✅ Split asset hardware campur | 2026-06-22 | ADR-002: pilot DESKTOP-RRJ9G01 laptop+PC |
| ✅ GPO scripts | 2026-06-22 | `agent/gpo/` — siap deploy, belum rollout ke semua |
| ✅ ADR-003 draft | 2026-06-22 | Desain dokumen QNAP + migration 005 draft SQL |
---
## Modul Sedang Dikerjakan
Tidak ada yang sedang in-progress saat ini.
---
## Modul Belum Dibuat (Backlog)
| Fase | Modul | Keterangan |
|------|-------|------------|
| Fase 1 | **Laravel asset index** | Halaman list asset read-only — BLOCKER |
| Fase 1 | **Auth IT user** | Login lokal untuk portal Laravel (M9) |
| Fase 2 | Zabbix webhook → Laravel | Alert Zabbix di portal |
| Fase 2 | Zabbix host status cache | Status hijau/merah dari Zabbix API |
| Fase 3 | Proxmox sync job (FastAPI) | VM otomatis masuk CMDB |
| Fase 3 | License module (Laravel) | Entri manual lisensi software |
| Fase 3 | User ↔ device assignment UI | Assign user ke asset |
| Fase 3 | Migration 005 + QNAP bucket | `asset_invoices`, `tickets`, `document_attachments` |
| Fase 3 | Laravel dokumen upload | Invoice, service report, complain |
| Fase 4 | GPO rollout ke semua laptop | Web install sudah siap |
| Fase 4 | Trend & compliance report | Analisis dari asset_snapshots |
| Fase 4 | Ollama assistant (opsional) | Query infra dengan natural language |
---
## Technical Debt
| Item | File | Prioritas | Keterangan |
|------|------|-----------|------------|
| ADR di `.cursor/adr/` tapi folder kosong | `.cursor/adr/` | Medium | ADR sebenarnya belum dibuat sebagai file — hanya disebut di progress.md |
| Collector port 8000 expose internal | Nginx | Low | Port 8000 di-allow firewalld untuk subnet 10.x, 192.168.x |
| `portal/` belum scaffold | monorepo | High | Laravel portal belum ada sama sekali |
| Uji agent di `BKK-MINIPC` | - | Low | Laptop kedua belum diuji |
---
## Agent Backlog (Fitur Belum Diimplementasi)
| Target Versi | Fitur |
|-------------|-------|
| v0.1.11+ | Printer (`Win32_Printer` / PnP PrintQueue) |
| v0.1.12+ | Klasifikasi HID keyboard vs mouse (saat ini generik) |
| Fase 3 | `is_factory` / baseline hardware vs upgrade aftermarket |
---
## Known Issues
Lihat [`issues.md`](issues.md) untuk detail lengkap.

80
GeoNetAgent/docs/roadmap.md

@ -0,0 +1,80 @@
# Roadmap
> **Terakhir Diperbarui:** 2026-06-29
---
## Status Saat Ini
```
✅ Fase 0 — Blueprint & Scaffold — Selesai Jun 2026
🔄 Fase 1 — Foundation (8/9) — Aktif, blocker: Laravel asset index
⏳ Fase 2 — Zabbix Integrasi — Pending
⏳ Fase 3 — CMDB Lengkap + Dokumen — Pending
⏳ Fase 4 — Scale & Intelligence — Pending
```
---
## Fase 1 — Foundation 🔄 (Aktif)
**DoD:** Agent di 1 laptop test POST → data di DB → Laravel tampilkan halaman asset list.
Yang sudah selesai:
- ✅ DB Schema migrated (geonetagent_dev)
- ✅ FastAPI `/agent/report` + health + read API + UI `/ui/`
- ✅ PowerShell agent v0.1.10 + POST ke collector (E2E)
- ✅ Nginx reverse proxy + web install `agent.gisportal.id/install/`
- ✅ Asset identity merge (ADR-001) + split hardware (ADR-002)
**Yang tersisa (blocker):**
- ❌ **Laravel asset index page** (read-only) — M4
- ❌ **Auth IT user lokal** — M9
---
## Fase 2 — Integrasi Monitoring
**DoD:** Alert Zabbix muncul di portal; status host dari Zabbix API ditampilkan.
- [ ] Zabbix webhook → Laravel (`POST /webhooks/zabbix`)
- [ ] Zabbix host status cache di dashboard (via Zabbix API)
---
## Fase 3 — CMDB Lengkap + Dokumen
**DoD:** VM Proxmox otomatis di CMDB; license dan dokumen asset bisa di-assign.
- [ ] Proxmox sync job (FastAPI APScheduler)
- [ ] License module (Laravel CRUD)
- [ ] User ↔ device assignment UI
- [ ] Migration 005 + bucket `geonetagent-documents` di QNAP
- [ ] Laravel upload/download dokumen (invoice, service report, complain)
---
## Fase 4 — Scale & Intelligence
- [ ] GPO rollout ke semua endpoint (web install sudah siap)
- [ ] Trend disk usage dari `asset_snapshots`
- [ ] Compliance report PDF
- [ ] Ollama assistant (opsional — query infra dengan natural language)
---
## Agent Backlog
| Target | Fitur |
|--------|-------|
| v0.1.11+ | Printer (`Win32_Printer`) |
| v0.1.12+ | Klasifikasi HID keyboard vs mouse |
| Fase 3 | `is_factory` / baseline vs upgrade hardware |
---
## Lihat Juga
- `docs/project-status.md` — status modul detail
- `docs/todo.md` — next actions berprioritas
- `.cursor/progress.md` — checklist DoD per fase

65
GeoNetAgent/docs/security.md

@ -0,0 +1,65 @@
# Security
> **Terakhir Diperbarui:** 2026-06-29
---
## Credential Management
| Secret | Lokasi | TIDAK boleh di |
|--------|--------|----------------|
| DB password | `/root/geonetagent_dev.credentials` di server 10.100.1.25 | Repo, log, chat |
| Agent bearer token | `.env` di server collector (10.100.1.24) | Repo |
| Zabbix API token | `.env` Laravel (Fase 2) | Repo |
| QNAP OSS credentials | `infra/qnap/README.md` (referensi lokasi) | Repo |
| SSH private keys | `~/.ssh/` di laptop developer | Repo, cloud |
---
## Auth Model
### Agent → Collector (FastAPI)
- HTTPS + `Authorization: Bearer <agent_token>`
- Token statis per site (`config.json` di endpoint)
- Opsi: validasi token ke DB jika `AGENT_TOKEN_CHECK_DB=true`
- Produksi: tambah mTLS (rencana)
### User → Collector UI
- Saat ini: **tidak ada auth** — UI read-only, hanya internal LAN
- Fase 1: Laravel auth (session / Sanctum)
### Zabbix Webhook (Fase 2)
- Shared secret di header
- IP allowlist Zabbix server
---
## Aturan AI Agent
1. **Jangan commit secret** ke repository
2. **Jangan tampilkan nilai secret** di output percakapan
3. Jangan hardcode credentials di kode
4. Credentials update di server → via SSH langsung, bukan melalui kode yang di-commit
5. SSH command ke server **wajib minta persetujuan user** sebelum dijalankan
---
## Nginx Security Notes
- `agent.gisportal.id`: `ssi off`**wajib** agar file `.ps1` tidak terpotong
- Jangan taruh `$env:` literal di file `.ps1` yang di-serve Nginx
- HTTPS via Let's Encrypt (certbot) di 10.100.1.24
- Internal UI (`/ui/`) hanya diakses via HTTPS domain, tidak direct IP:port (kecuali internal LAN)
---
## Data Sensitivity
| Data | Sensitivitas | Catatan |
|------|-------------|---------|
| Hostname, domain username | Medium | PII — diaudit akses |
| Hardware spec (CPU, RAM, GPU) | Low | Data inventaris |
| Software list | Medium | Bisa digunakan untuk targeting |
| IP/MAC address | Medium | Network fingerprinting |
| Serial number | Medium | Asset tracking |
| Credentials DB | **Critical** | Tidak boleh ada di kode |

90
GeoNetAgent/docs/server.md

@ -0,0 +1,90 @@
# Server Information
> **Terakhir Diperbarui:** 2026-06-29
---
## Daftar Server
| Role | IP | OS | SSH Command |
|------|----|----|-------------|
| **Reverse Proxy + Collector** | 10.100.1.24 | CentOS Linux 8 | `ssh -m hmac-sha1 root@10.100.1.24 -p 22` |
| **PostgreSQL** | 10.100.1.25 | Ubuntu 18.04.6 LTS | `ssh -m hmac-sha1 root@10.100.1.25 -p 22` |
| **AD / LDAP** | 10.100.1.40 | Windows Server 2012 R2 | `ssh -m hmac-sha1 Administrator@10.100.1.40 -p 22` |
| **Main Webserver** | 10.100.1.41 | Windows Server 2012 R2 | `ssh -m hmac-sha1 Administrator@10.100.1.41 -p 22` |
| **Zabbix** | 10.100.1.42 | Ubuntu 22.04.1 LTS | `ssh -m hmac-sha1 root@10.100.1.42 -p 22` |
| **Ollama** | 10.100.1.26 | Ubuntu 22.04.1 LTS | `ssh -i "$env:USERPROFILE\.ssh\id_rsa_geonet_laptop" -o IdentitiesOnly=yes -m hmac-sha1 root@10.100.1.26 -p 22` |
| **NAS QNAP TS-932X** | 10.100.1.10 | QTS 5.2.9 (ARM64) | `ssh -i "$env:USERPROFILE\.ssh\id_ed25519" -o IdentitiesOnly=yes admin@10.100.1.10 -p 22` |
| **MikroTik CCR2004** | 10.100.1.1 / 192.168.0.1 | RouterOS | `ssh -i "$env:USERPROFILE\.ssh\id_rsa_mikrotik" -o IdentitiesOnly=yes -m hmac-sha1 -p 255 admin@10.100.1.1` |
> Ollama & QNAP: Wajib **PowerShell** (bukan CMD) agar `$env:USERPROFILE` terbaca.
> SSH key sudah dikonfigurasi. Semua SSH command butuh **approval user** sebelum dijalankan.
---
## Server 10.100.1.24 — Reverse Proxy + Collector
### Services Running
| Service | Container | Port | URL |
|---------|-----------|------|-----|
| FastAPI Collector | `geonet-collector` | 18000 (internal: 8000) | `collector.gisportal.id` |
| Nginx | host | 80, 443 | reverse proxy |
### Nginx Config Files
| File | Domain |
|------|--------|
| `/etc/nginx/conf.d/production/collector.conf` | `collector.gisportal.id`, `agent.gisportal.id` |
### Perintah Umum
```bash
# Cek container
docker ps
# Log collector
docker logs geonet-collector --tail 50 -f
# Reload nginx
nginx -t && nginx -s reload
```
---
## Server 10.100.1.25 — PostgreSQL
```bash
# Akses PostgreSQL
ssh -m hmac-sha1 root@10.100.1.25 -p 22
# Credentials: /root/geonetagent_dev.credentials
psql -U geonet_collector -d geonetagent_dev
# Migration manual
psql -U geonet_collector -d geonetagent_dev -f infra/db/migration.sql
```
---
## Domain Map
| Domain | Backend |
|--------|---------|
| `agent.gisportal.id` | 10.100.1.24 — collector :18000 (POST agent) + `/install/` (static) |
| `collector.gisportal.id` | 10.100.1.24 — collector :18000 (read UI + API) |
---
## Transfer File ke Server
```powershell
# Upload file (Windows PowerShell)
scp -o "MACs=hmac-sha1" "D:\path\to\file" "root@10.100.1.24:/path/on/server"
# Upload archive (source code)
tar -czf "$env:TEMP\collector.tar.gz" --exclude=".venv" --exclude="__pycache__" -C "D:\Project\app on git\GeoNetAgent\collector" .
scp -o "MACs=hmac-sha1" "$env:TEMP\collector.tar.gz" "root@10.100.1.24:/opt/stacks/geonetagent-collector/collector.tar.gz"
```
> **Jangan** gunakan heredoc `<< 'EOF'` via SSH dari PowerShell — `$` variabel terpotong.

80
GeoNetAgent/docs/todo.md

@ -0,0 +1,80 @@
# TODO List
> **Terakhir Diperbarui:** 2026-06-29
---
## 🔴 High Priority (Fase 1 Blocker)
### Scaffold Laravel Portal — Asset Index Page
- **Deskripsi:** Buat Laravel project di `portal/`, halaman list asset read-only dari DB `geonetagent`
- **File:** `portal/` (belum ada), `infra/db/` (credentials)
- **Estimasi:** 2-3 hari
- **Dependency:** DB sudah running, FastAPI data sudah ada
### Auth IT User — Login Lokal Laravel
- **Deskripsi:** M9: Login lokal (bukan LDAP dulu) untuk IT admin mengakses portal
- **File:** `portal/` Laravel (M9)
- **Estimasi:** 1 hari
- **Dependency:** Laravel scaffold selesai
---
## 🟡 Medium Priority
### Buat ADR files di docs/adr/
- **Deskripsi:** ADR-001, ADR-002, ADR-003 belum ada sebagai file — hanya disebut di progress.md dan context-index. Perlu dibuat sebagai file `.md` di `docs/adr/`
- **File:** `docs/adr/ADR-001-*.md`, `docs/adr/ADR-002-*.md`, `docs/adr/ADR-003-*.md`
- **Estimasi:** 1 jam
- **Dependency:** -
### Isi .cursor/rules/ files
- **Deskripsi:** Folder `.cursor/rules/` ada tapi kosong (hanya `project-context.mdc` yang disebut tapi tidak ada)
- **File:** `.cursor/rules/project-context.mdc`, `coding.mdc`, `agent-release-deploy.mdc`
- **Estimasi:** 1 jam
- **Dependency:** -
### Apply migration 005 di dev DB
- **Deskripsi:** `infra/db/005_asset_documents.sql` sudah dibuat, belum diapply. Butuh setup QNAP bucket dulu.
- **File:** `infra/db/005_asset_documents.sql`
- **Estimasi:** 2 jam
- **Dependency:** QNAP bucket `geonetagent-documents` dibuat
### Buat QNAP bucket geonetagent-documents
- **Deskripsi:** Bucket untuk dokumen procurement/service/complain (ADR-003)
- **File:** `infra/qnap/README.md` (referensi)
- **Estimasi:** 30 menit
- **Dependency:** ADR-003
### Uji agent di BKK-MINIPC
- **Deskripsi:** Verifikasi agent berjalan di PC kedua (bukan laptop Dell)
- **Estimasi:** 30 menit
- **Dependency:** Akses fisik ke BKK-MINIPC
---
## 🟢 Low Priority
### Agent v0.1.11 — Printer collection
- **Deskripsi:** Tambah `Get-PrinterInfo.ps1` menggunakan `Win32_Printer`
- **File:** `agent/lib/Get-PrinterInfo.ps1` (baru)
- **Estimasi:** 2-3 jam
- **Dependency:** -
### Agent v0.1.12 — Klasifikasi HID
- **Deskripsi:** Bedakan keyboard vs mouse vs dongle di `Get-PeripheralsInfo.ps1`
- **File:** `agent/lib/Get-PeripheralsInfo.ps1`
- **Estimasi:** 2-4 jam
- **Dependency:** -
### Tambah pytest tests untuk collector
- **Deskripsi:** Coverage untuk `/api/v1/agent/report` endpoint (test payload validation, upsert logic)
- **File:** `collector/tests/`
- **Estimasi:** 1-2 hari
- **Dependency:** -
### OpenAPI / Swagger doc finalize
- **Deskripsi:** Pastikan semua endpoint FastAPI punya docstring dan description yang lengkap untuk auto-generated docs di `/docs`
- **File:** `collector/app/api/*.py`
- **Estimasi:** 2 jam
- **Dependency:** -

136
GeoNetAgent/docs/troubleshooting.md

@ -0,0 +1,136 @@
# Troubleshooting
> **Terakhir Diperbarui:** 2026-06-29
---
## Agent gagal kirim laporan — "Cannot connect"
**Kemungkinan:**
1. URL di `config.json` salah
2. Token tidak cocok dengan `AGENT_BEARER_TOKEN` di collector
```powershell
# Cek config
Get-Content "$env:ProgramData\GeoNetAgent\config.json"
# Test manual (dari endpoint)
& "$env:ProgramData\GeoNetAgent\UserAgent.ps1"
# Cek koneksi ke server
Test-NetConnection agent.gisportal.id -Port 443
```
---
## File `.ps1` di Nginx terpotong — `$env:` hilang
**Penyebab:** Nginx menginterpretasikan `$env:` sebagai variabel Nginx SSI dan memotong respons.
**Fix:** Pastikan nginx config untuk `agent.gisportal.id` memiliki `ssi off`:
```nginx
location /install/ {
ssi off;
alias /var/www/agent-install/;
}
```
**Cara deteksi:** Download file `.ps1` dari URL, bandingkan dengan source lokal. Jika ada teks yang hilang setelah `$env:`, ini masalahnya.
---
## Heredoc via SSH PowerShell — Variable $ hilang
**Penyebab:** PowerShell menginterpretasikan `$` sebelum dikirim ke SSH.
**Solusi:** Upload file via SCP:
```powershell
scp -o "MACs=hmac-sha1" "D:\path\to\file.sh" "root@10.100.1.24:/path/on/server/file.sh"
```
---
## SCP error "unknown option -- m"
Windows SCP tidak support flag `-m`. Gunakan `-o "MACs=hmac-sha1"`:
```powershell
scp -o "MACs=hmac-sha1" source destination
```
---
## Agent auto-update tidak berjalan
**Cek:**
```powershell
# Cek versi lokal
Get-Content "$env:ProgramData\GeoNetAgent\VERSION"
# Cek manifest server (harus accessible)
Invoke-WebRequest "https://agent.gisportal.id/install/manifest.json" -UseBasicParsing | Select-Object -ExpandProperty Content
```
**Auto-update hanya berjalan saat `UserAgent.ps1` dieksekusi** (setiap 3 jam via Scheduled Task). Jika task tidak running:
```powershell
Get-ScheduledTask -TaskName "GeoNetAgent-Report" | Select State
Start-ScheduledTask -TaskName "GeoNetAgent-Report"
```
---
## Collector container tidak mau start
```bash
# Cek exit code
docker ps -a --filter name=geonet-collector
# Cek log
docker logs geonet-collector --tail 100
# Cek DATABASE_URL valid
docker exec geonet-collector python -c "from app.config import settings; print(settings.database_url)"
# Test koneksi DB dari container
docker exec geonet-collector psql "$DATABASE_URL" -c "SELECT 1"
```
---
## Asset duplikat muncul setelah split hardware
**Penyebab:** Laptop dengan dual hardware (misalnya laptop Dell + PC Gigabyte dalam satu unit) — sesuai ADR-002.
**Cara split:**
```bash
docker exec geonet-collector python /app/scripts/split_asset_by_hardware.py <uuid>
```
Lihat `ADR-002` untuk detail keputusan.
---
## View `v_asset_hardware_latest` tidak ada di DB
Migration 004 mungkin belum diapply di environment ini.
```bash
ssh -m hmac-sha1 root@10.100.1.25 -p 22
psql -U geonet_collector -d geonetagent_dev -c "\dv"
# Jika tidak ada v_asset_hardware_latest:
psql -U geonet_collector -d geonetagent_dev -f /tmp/004_view_hardware_latest.sql
```
---
## Perintah Darurat
```bash
# Restart collector
docker restart geonet-collector
# Nginx reload
nginx -t && nginx -s reload
# Cek semua container
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
```

1
GeoNetAgent/infra/collector/Dockerfile

@ -9,6 +9,7 @@ RUN apt-get update -qq \
COPY pyproject.toml README.md ./ COPY pyproject.toml README.md ./
COPY app ./app COPY app ./app
COPY static ./static COPY static ./static
COPY scripts ./scripts
RUN pip install --no-cache-dir -e . RUN pip install --no-cache-dir -e .

82
GeoNetAgent/infra/collector/publish_agent_install.ps1

@ -0,0 +1,82 @@
# Publish agent install package ke proxy.lan (nginx /install/)
# Jalankan dari root repo (PowerShell):
# .\infra\collector\publish_agent_install.ps1
# Opsional: -Remote reverse-proxy
param(
[string]$Remote = "reverse-proxy",
[string]$InstallRoot = "/var/www/geonetagent/install"
)
$ErrorActionPreference = "Stop"
$RepoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$Agent = Join-Path $RepoRoot "agent"
$ManifestPath = Join-Path $Agent "install\manifest.json"
if (-not (Test-Path $ManifestPath)) {
throw "Manifest tidak ditemukan: $ManifestPath"
}
$manifest = Get-Content $ManifestPath -Raw | ConvertFrom-Json
$version = (Get-Content (Join-Path $Agent "VERSION") -Raw).Trim()
Write-Host "==> GeoNetAgent install package v$version (manifest $($manifest.version))"
ssh $Remote "mkdir -p $InstallRoot/lib"
Write-Host "==> Upload agent scripts ($($manifest.files.Count) file)"
foreach ($rel in $manifest.files) {
$local = Join-Path $Agent ($rel -replace '/', '\')
if (-not (Test-Path $local)) {
throw "File hilang di repo: $rel"
}
$destDir = Split-Path $rel -Parent
if ($destDir) {
ssh $Remote "mkdir -p `"$InstallRoot/$($destDir -replace '\\','/')`""
}
scp -q $local "${Remote}:${InstallRoot}/$($rel -replace '\\','/')"
Write-Host " -> $rel"
}
$extras = @(
@{ Local = "Install-GeoNetAgent.ps1"; Remote = "Install-GeoNetAgent.ps1" },
@{ Local = "install\bootstrap.ps1"; Remote = "bootstrap.ps1" },
@{ Local = "install\manifest.json"; Remote = "manifest.json" },
@{ Local = "install\GeoNetAgent-Install.ps1"; Remote = "GeoNetAgent-Install.ps1" },
@{ Local = "install\index.html"; Remote = "index.html" },
@{ Local = "lib\Agent-InstallCore.ps1"; Remote = "lib/Agent-InstallCore.ps1" }
)
Write-Host "==> Upload installer + manifest"
foreach ($item in $extras) {
$local = Join-Path $Agent $item.Local
scp -q $local "${Remote}:${InstallRoot}/$($item.Remote)"
}
$configShare = Join-Path $Agent "config.share.json"
if (Test-Path $configShare) {
Write-Host "==> Upload config.json"
scp -q $configShare "${Remote}:${InstallRoot}/config.json"
} else {
Write-Host "WARN: config.share.json tidak ada"
}
ssh $Remote "chown -R nginx:nginx /var/www/geonetagent 2>/dev/null || chown -R root:root /var/www/geonetagent; chmod -R a+rX /var/www/geonetagent"
Write-Host "==> Verifikasi manifest publik"
$remoteManifest = ssh $Remote "cat $InstallRoot/manifest.json"
$remoteVersion = ($remoteManifest | ConvertFrom-Json).version
Write-Host "Remote manifest version: $remoteVersion"
if ($remoteVersion -ne $manifest.version) {
throw "Version mismatch: remote=$remoteVersion local=$($manifest.version)"
}
try {
$urlManifest = Invoke-RestMethod -Uri "https://agent.gisportal.id/install/manifest.json" -TimeoutSec 15
Write-Host "HTTPS manifest version: $($urlManifest.version)"
if ($urlManifest.version -ne $manifest.version) {
Write-Warning "HTTPS manifest belum sinkron — cek nginx cache/proxy"
}
} catch {
Write-Warning "Tidak bisa verifikasi HTTPS: $_"
}
Write-Host "OK: https://agent.gisportal.id/install/"

19
GeoNetAgent/infra/collector/publish_agent_install.sh

@ -44,6 +44,25 @@ fi
ssh "$REMOTE" "chown -R nginx:nginx /var/www/geonetagent 2>/dev/null || chown -R root:root /var/www/geonetagent; chmod -R a+rX /var/www/geonetagent" ssh "$REMOTE" "chown -R nginx:nginx /var/www/geonetagent 2>/dev/null || chown -R root:root /var/www/geonetagent; chmod -R a+rX /var/www/geonetagent"
echo "==> Verifikasi" echo "==> Verifikasi"
for path in GeoNetAgent-Install.ps1 Install-GeoNetAgent.ps1 lib/Agent-InstallCore.ps1 manifest.json; do
local_size=$(wc -c < "$AGENT/install/$path" 2>/dev/null || wc -c < "$AGENT/$path" 2>/dev/null || wc -c < "$AGENT/lib/Agent-InstallCore.ps1")
case "$path" in
GeoNetAgent-Install.ps1) local_size=$(wc -c < "$AGENT/install/GeoNetAgent-Install.ps1") ;;
Install-GeoNetAgent.ps1) local_size=$(wc -c < "$AGENT/Install-GeoNetAgent.ps1") ;;
lib/Agent-InstallCore.ps1) local_size=$(wc -c < "$AGENT/lib/Agent-InstallCore.ps1") ;;
manifest.json) local_size=$(wc -c < "$AGENT/install/manifest.json") ;;
esac
remote_size=$(ssh "$REMOTE" "wc -c < $INSTALL_ROOT/$path" 2>/dev/null || echo 0)
echo " $path: local=$local_size remote=$remote_size"
if [[ "$local_size" != "$remote_size" ]]; then
echo "WARN: size mismatch for $path"
fi
url_size=$(curl -sfL "https://agent.gisportal.id/install/$path" 2>/dev/null | wc -c || echo 0)
if [[ "$url_size" != "$local_size" && "$url_size" != "0" ]]; then
echo "WARN: HTTPS body size $url_size != local $local_size for $path (cek nginx \$env: truncation)"
fi
done
curl -sfI "https://agent.gisportal.id/install/manifest.json" | head -3 || \ curl -sfI "https://agent.gisportal.id/install/manifest.json" | head -3 || \
ssh "$REMOTE" "curl -sfI -H 'Host: agent.gisportal.id' http://127.0.0.1/install/manifest.json | head -3" ssh "$REMOTE" "curl -sfI -H 'Host: agent.gisportal.id' http://127.0.0.1/install/manifest.json | head -3"

49
GeoNetAgent/infra/collector/push_collector.ps1

@ -0,0 +1,49 @@
# Upload collector + rebuild Docker di proxy.lan
# Jalankan dari root repo (PowerShell):
# .\infra\collector\push_collector.ps1
param(
[string]$Remote = "reverse-proxy",
[string]$StackDir = "/opt/stacks/geonetagent-collector"
)
$ErrorActionPreference = "Stop"
$RepoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$Collector = Join-Path $RepoRoot "collector"
$Dockerfile = Join-Path $RepoRoot "infra\collector\Dockerfile"
$TarPath = Join-Path $env:TEMP "geonet-collector.tgz"
$Stage = Join-Path $env:TEMP "geonet-collector-stage"
if (Test-Path $Stage) { Remove-Item $Stage -Recurse -Force }
New-Item -ItemType Directory -Path $Stage | Out-Null
Copy-Item -Path (Join-Path $Collector "*") -Destination $Stage -Recurse -Force
Copy-Item $Dockerfile (Join-Path $Stage "Dockerfile") -Force
if (Test-Path $TarPath) { Remove-Item $TarPath -Force }
Write-Host "==> Pack collector"
tar -czf $TarPath -C $Stage .
Write-Host "==> Upload ke $Remote"
ssh $Remote "mkdir -p $StackDir"
scp -q $TarPath "${Remote}:${StackDir}/collector.tgz"
Write-Host "==> Rebuild & restart container"
$remoteScript = @"
set -euo pipefail
cd '$StackDir'
if [ ! -f .env ]; then
echo 'ERROR: .env tidak ada di $StackDir'
exit 1
fi
rm -rf collector-src
mkdir -p collector-src
tar -xzf collector.tgz -C collector-src
docker compose build --no-cache
docker compose up -d
sleep 3
curl -sf http://127.0.0.1:18000/api/v1/health
echo
"@
ssh $Remote $remoteScript
Remove-Item $Stage -Recurse -Force -ErrorAction SilentlyContinue
Write-Host "OK: https://collector.gisportal.id/ui/"

47
GeoNetAgent/infra/collector/push_collector.sh

@ -0,0 +1,47 @@
#!/bin/bash
# Upload collector + rebuild Docker di proxy.lan (10.100.1.24)
# Jalankan dari repo root:
# bash infra/collector/push_collector.sh
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
REMOTE="${REMOTE:-root@10.100.1.24}"
STACK_DIR="/opt/stacks/geonetagent-collector"
TMP_PACK="$(mktemp -d)"
cleanup() { rm -rf "$TMP_PACK"; }
trap cleanup EXIT
if [[ ! -f "$STACK_DIR/.env" ]] 2>/dev/null; then
:
fi
echo "==> Pack collector"
cp -a "$REPO_ROOT/collector/." "$TMP_PACK/"
cp "$REPO_ROOT/infra/collector/Dockerfile" "$TMP_PACK/Dockerfile"
tar -czf /tmp/geonet-collector.tgz -C "$TMP_PACK" .
echo "==> Upload ke $REMOTE"
ssh "$REMOTE" "mkdir -p $STACK_DIR/collector-src"
scp -q /tmp/geonet-collector.tgz "$REMOTE:$STACK_DIR/collector.tgz"
echo "==> Rebuild & restart container"
ssh "$REMOTE" bash << 'REMOTE'
set -euo pipefail
STACK_DIR="/opt/stacks/geonetagent-collector"
cd "$STACK_DIR"
if [[ ! -f .env ]]; then
echo "ERROR: $STACK_DIR/.env tidak ada. Deploy collector pertama kali via deploy_reverse_proxy.sh"
exit 1
fi
rm -rf collector-src
mkdir -p collector-src
tar -xzf collector.tgz -C collector-src
docker compose build --no-cache
docker compose up -d
sleep 3
curl -sf http://127.0.0.1:18000/api/v1/health
echo
REMOTE
echo "OK: https://collector.gisportal.id/ui/"

18
GeoNetAgent/infra/db/README.md

@ -42,3 +42,21 @@ SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public';
``` ```
Expected: 13 core tables (portal_users via Laravel later). Expected: 13 core tables (portal_users via Laravel later).
## Migrations (incremental)
| File | Keterangan |
|------|------------|
| `001_initial_schema.sql` | Schema awal |
| `002_disk_model.sql` | Kolom `asset_disks.model` |
| `003_drop_machine_id_unique.sql` | `machine_id` bukan UNIQUE (disk clone) |
| `004_v_asset_hardware_latest.sql` | View hardware ringkas per asset (snapshot terbaru) |
| `005_asset_documents.sql` | `asset_invoices`, `tickets`, `document_attachments` (ADR 003 — Fase 3) |
Jalankan di dev:
```bash
ssh main-db
sudo -u postgres psql -d geonetagent_dev -f /path/to/003_drop_machine_id_unique.sql
sudo -u postgres psql -d geonetagent_dev -f /path/to/004_v_asset_hardware_latest.sql
```

6
GeoNetAgent/infra/db/migrations/003_drop_machine_id_unique.sql

@ -0,0 +1,6 @@
-- machine_id = Windows MachineGuid (OS install), not globally unique hardware id.
-- Disk clone can produce two physical machines with the same machine_id.
ALTER TABLE assets DROP CONSTRAINT IF EXISTS assets_machine_id_key;
CREATE INDEX IF NOT EXISTS idx_assets_machine_id ON assets (machine_id)
WHERE machine_id IS NOT NULL;

108
GeoNetAgent/infra/db/migrations/004_v_asset_hardware_latest.sql

@ -0,0 +1,108 @@
-- GeoNetAgent — view hardware ringkas dari snapshot terbaru per asset
-- Database: geonetagent_dev / geonetagent
-- Jalankan: sudo -u postgres psql -d geonetagent_dev -f 004_v_asset_hardware_latest.sql
\set ON_ERROR_STOP on
CREATE OR REPLACE VIEW v_asset_hardware_latest AS
WITH latest_snapshot AS (
SELECT DISTINCT ON (s.asset_id)
s.id AS snapshot_id,
s.asset_id,
s.agent_version,
s.collected_at,
s.os_name,
s.os_version,
s.cpu_model,
s.ram_gb,
s.gpu_model,
s.payload
FROM asset_snapshots s
ORDER BY s.asset_id, s.collected_at DESC, s.created_at DESC
),
display_stats AS (
SELECT
ls.asset_id,
COUNT(*) FILTER (
WHERE p.elem->>'connection' = 'display'
)::integer AS display_count,
COUNT(*) FILTER (
WHERE p.elem->>'connection' = 'display'
AND COALESCE(p.elem->>'is_external', '') = 'true'
)::integer AS display_external_count,
COUNT(*) FILTER (
WHERE p.elem->>'connection' = 'display'
AND COALESCE(p.elem->>'is_external', '') = 'false'
)::integer AS display_internal_count,
COALESCE(
jsonb_agg(p.elem ORDER BY p.ordinality)
FILTER (WHERE p.elem->>'connection' = 'display'),
'[]'::jsonb
) AS displays
FROM latest_snapshot ls
LEFT JOIN LATERAL jsonb_array_elements(
COALESCE(ls.payload->'peripherals', '[]'::jsonb)
) WITH ORDINALITY AS p(elem, ordinality) ON TRUE
GROUP BY ls.asset_id
)
SELECT
a.id AS asset_id,
a.hostname,
a.display_name,
a.asset_type,
a.status,
a.manufacturer,
a.model,
a.serial_number,
a.smbios_uuid,
a.machine_id,
a.primary_physical_mac,
a.last_seen_at,
ls.snapshot_id,
ls.agent_version,
ls.collected_at AS snapshot_collected_at,
ls.os_name,
ls.os_version,
COALESCE(ls.payload->'system'->>'form_factor', a.metadata->>'form_factor') AS form_factor,
COALESCE(ls.payload->'system'->>'chassis_label', a.metadata->>'chassis_label') AS chassis_label,
ls.payload->'system'->>'pc_system_label' AS pc_system_label,
COALESCE(ls.payload->'cpu'->>'model', ls.cpu_model) AS cpu_model,
NULLIF(ls.payload->'cpu'->>'manufacturer', '')::varchar AS cpu_manufacturer,
NULLIF(ls.payload->'cpu'->>'cores', '')::integer AS cpu_cores,
NULLIF(ls.payload->'cpu'->>'logical_processors', '')::integer AS cpu_logical_processors,
NULLIF(ls.payload->'cpu'->>'max_clock_mhz', '')::integer AS cpu_max_clock_mhz,
NULLIF(ls.payload->'cpu'->>'current_clock_mhz', '')::integer AS cpu_current_clock_mhz,
NULLIF(ls.payload->'cpu'->>'l2_cache_kb', '')::integer AS cpu_l2_cache_kb,
NULLIF(ls.payload->'cpu'->>'l3_cache_kb', '')::integer AS cpu_l3_cache_kb,
NULLIF(ls.payload->'cpu'->>'architecture', '')::varchar AS cpu_architecture,
ls.ram_gb,
NULLIF(ls.payload->'memory_summary'->>'ddr_generation', '')::varchar AS ram_ddr_generation,
NULLIF(ls.payload->'memory_summary'->>'layout', '')::varchar AS ram_layout,
NULLIF(ls.payload->'memory_summary'->>'configuration', '')::varchar AS ram_configuration,
NULLIF(ls.payload->'memory_summary'->>'module_count', '')::integer AS ram_module_count,
NULLIF(ls.payload->'memory_summary'->>'slot_count', '')::integer AS ram_slot_count,
NULLIF(ls.payload->'memory_summary'->>'empty_slots', '')::integer AS ram_empty_slots,
COALESCE(ls.payload->'memory_modules', '[]'::jsonb) AS memory_modules,
ls.gpu_model AS gpu_primary,
jsonb_array_length(COALESCE(ls.payload->'gpus', '[]'::jsonb))::integer AS gpu_count,
COALESCE(ls.payload->'gpus', '[]'::jsonb) AS gpus,
COALESCE(ds.display_count, 0) AS display_count,
COALESCE(ds.display_external_count, 0) AS display_external_count,
COALESCE(ds.display_internal_count, 0) AS display_internal_count,
COALESCE(ds.displays, '[]'::jsonb) AS displays
FROM assets a
LEFT JOIN latest_snapshot ls ON ls.asset_id = a.id
LEFT JOIN display_stats ds ON ds.asset_id = a.id
WHERE a.deleted_at IS NULL;
COMMENT ON VIEW v_asset_hardware_latest IS
'Satu baris per asset: CPU/GPU/RAM/display dari asset_snapshots terbaru (payload JSONB).';
GRANT SELECT ON v_asset_hardware_latest TO geonet_collector, geonet_portal;

111
GeoNetAgent/infra/db/migrations/005_asset_documents.sql

@ -0,0 +1,111 @@
-- GeoNetAgent — asset invoices, tickets, document attachments (QNAP Object Storage metadata)
-- ADR: .cursor/adr/003-asset-documents-qnap.md
-- Fase 3 — jalankan setelah portal Laravel scaffold
-- Database: geonetagent_dev / geonetagent
\set ON_ERROR_STOP on
-- 1. asset_invoices (procurement)
CREATE TABLE IF NOT EXISTS asset_invoices (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
asset_id uuid NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
invoice_number varchar(100) NOT NULL,
invoice_date date,
vendor varchar(200),
purchase_price numeric(14, 2),
currency varchar(3) NOT NULL DEFAULT 'IDR',
po_number varchar(100),
warranty_until date,
serial_at_purchase varchar(100),
notes text,
created_by varchar(100),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz,
CONSTRAINT uq_asset_invoices_number UNIQUE (invoice_number)
);
CREATE INDEX IF NOT EXISTS idx_asset_invoices_asset ON asset_invoices (asset_id);
CREATE INDEX IF NOT EXISTS idx_asset_invoices_date ON asset_invoices (invoice_date DESC);
CREATE TRIGGER trg_asset_invoices_updated_at
BEFORE UPDATE ON asset_invoices
FOR EACH ROW EXECUTE PROCEDURE set_updated_at();
-- 2. tickets (service + complaint)
CREATE TABLE IF NOT EXISTS tickets (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
ticket_number varchar(30) NOT NULL,
ticket_type varchar(20) NOT NULL,
status varchar(20) NOT NULL DEFAULT 'open',
priority varchar(10) NOT NULL DEFAULT 'medium',
asset_id uuid REFERENCES assets(id) ON DELETE SET NULL,
reporter_user_id uuid REFERENCES users(id) ON DELETE SET NULL,
assigned_to_user_id uuid REFERENCES users(id) ON DELETE SET NULL,
subject varchar(255) NOT NULL,
description text,
resolution_notes text,
opened_at timestamptz NOT NULL DEFAULT now(),
resolved_at timestamptz,
closed_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz,
CONSTRAINT uq_tickets_number UNIQUE (ticket_number),
CONSTRAINT chk_tickets_type CHECK (ticket_type IN ('service', 'complaint')),
CONSTRAINT chk_tickets_status CHECK (status IN (
'open', 'in_progress', 'waiting_user', 'resolved', 'closed', 'cancelled'
)),
CONSTRAINT chk_tickets_priority CHECK (priority IN ('low', 'medium', 'high', 'urgent'))
);
CREATE INDEX IF NOT EXISTS idx_tickets_asset ON tickets (asset_id);
CREATE INDEX IF NOT EXISTS idx_tickets_type_status ON tickets (ticket_type, status);
CREATE INDEX IF NOT EXISTS idx_tickets_opened ON tickets (opened_at DESC);
CREATE TRIGGER trg_tickets_updated_at
BEFORE UPDATE ON tickets
FOR EACH ROW EXECUTE PROCEDURE set_updated_at();
-- 3. document_attachments (shared — metadata file di QNAP S3)
CREATE TABLE IF NOT EXISTS document_attachments (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
domain varchar(20) NOT NULL,
kind varchar(40) NOT NULL DEFAULT 'other',
asset_id uuid REFERENCES assets(id) ON DELETE CASCADE,
invoice_id uuid REFERENCES asset_invoices(id) ON DELETE CASCADE,
ticket_id uuid REFERENCES tickets(id) ON DELETE CASCADE,
storage_provider varchar(20) NOT NULL DEFAULT 'qnap_s3',
bucket varchar(100) NOT NULL,
object_key varchar(500) NOT NULL,
original_filename varchar(255) NOT NULL,
content_type varchar(100),
file_size_bytes bigint,
checksum_sha256 varchar(64),
uploaded_by varchar(100),
uploaded_at timestamptz NOT NULL DEFAULT now(),
metadata jsonb NOT NULL DEFAULT '{}',
deleted_at timestamptz,
CONSTRAINT chk_document_attachments_domain CHECK (domain IN (
'procurement', 'service', 'complaint'
)),
CONSTRAINT chk_document_attachments_parent CHECK (
asset_id IS NOT NULL OR invoice_id IS NOT NULL OR ticket_id IS NOT NULL
),
CONSTRAINT uq_document_attachments_object UNIQUE (bucket, object_key)
);
CREATE INDEX IF NOT EXISTS idx_document_attachments_asset ON document_attachments (asset_id)
WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_document_attachments_invoice ON document_attachments (invoice_id)
WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_document_attachments_ticket ON document_attachments (ticket_id)
WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_document_attachments_domain ON document_attachments (domain, kind);
COMMENT ON TABLE asset_invoices IS 'Procurement — invoice/PO metadata; file di QNAP via document_attachments';
COMMENT ON TABLE tickets IS 'Service & complaint tickets; lampiran di document_attachments';
COMMENT ON TABLE document_attachments IS 'Metadata file di QNAP Object Storage (ADR 003)';
GRANT SELECT, INSERT, UPDATE, DELETE ON asset_invoices, tickets, document_attachments TO geonet_collector;
GRANT SELECT, INSERT, UPDATE ON asset_invoices, tickets, document_attachments TO geonet_portal;

2
GeoNetAgent/infra/nginx/geonetagent-collector-public.conf

@ -32,6 +32,7 @@ server {
} }
location ^~ /install/ { location ^~ /install/ {
# Jangan pakai $env:... di file .ps1 yang di-serve — nginx dapat memotong respons.
ssi off; ssi off;
sendfile off; sendfile off;
gzip off; gzip off;
@ -41,7 +42,6 @@ server {
types { types {
text/html html; text/html html;
application/json json; application/json json;
text/plain ps1;
} }
} }

51
GeoNetAgent/infra/qnap/README.md

@ -0,0 +1,51 @@
# QNAP Object Storage — GeoNetAgent Documents
> NAS: TS-932X · `10.100.1.10` · SSH: `infra/host.md`
> ADR: `.cursor/adr/003-asset-documents-qnap.md`
## Ringkasan
| Item | Nilai |
|------|--------|
| Bucket | `geonetagent-documents` |
| API | S3-compatible (QNAP Object Storage) |
| Akses | LAN internal; kredensial khusus portal |
| Isi | PDF invoice, foto unit, dokumen service, lampiran complain |
## Setup (sekali, di QNAP)
1. **Control Panel** → Object Storage → aktifkan / pastikan service jalan
2. Buat bucket `geonetagent-documents`
3. Buat user/API key khusus `geonetagent-portal` — hanya bucket ini (read/write)
4. Catat endpoint, access key, secret → portal `.env`:
```env
S3_ENDPOINT=http://10.100.1.10:PORT
S3_BUCKET=geonetagent-documents
S3_ACCESS_KEY_ID=...
S3_SECRET_ACCESS_KEY=...
S3_REGION=us-east-1
```
(Ganti `PORT` sesuai konfigurasi QNAP Object Storage.)
## Object key layout
```
{domain}/{yyyy}/{mm}/{attachment_uuid}/{filename}
procurement/2026/06/{uuid}/INV-2024-00123.pdf
service/2026/06/{uuid}/ba-perbaikan.pdf
complaint/2026/06/{uuid}/screenshot.png
```
`attachment_uuid` = `document_attachments.id`
## Database
Migration: `infra/db/migrations/005_asset_documents.sql` (jalankan saat portal Fase 3).
## Yang tidak disimpan di sini
- Hardware snapshot agent → PostgreSQL `asset_snapshots.payload`
- Alert Zabbix → PostgreSQL `alerts` (+ Zabbix master)

62
GeoNetAgent/perintah.md

@ -9,10 +9,13 @@
| Progress & fase | `.cursor/progress.md` | | Progress & fase | `.cursor/progress.md` |
| SSH server | `.cursor/server.md` | | SSH server | `.cursor/server.md` |
| Collector API | `collector/docs/API.md` | | Collector API | `collector/docs/API.md` |
| QNAP Object Storage (dokumen) | `infra/qnap/README.md` · ADR `003-asset-documents-qnap.md` |
| Install agent user | https://agent.gisportal.id/install/ · `agent/README.md` | | Install agent user | https://agent.gisportal.id/install/ · `agent/README.md` |
| Nginx reverse proxy | `infra/nginx/README.md` | | Nginx reverse proxy | `infra/nginx/README.md` |
| Publish paket agent | `bash infra/collector/publish_agent_install.sh` | | Publish paket agent | `.\infra\collector\publish_agent_install.ps1` (Windows) · `bash infra/collector/publish_agent_install.sh` |
| Deploy collector | `.\infra\collector\push_collector.ps1` (Windows) · `bash infra/collector/push_collector.sh` |
| Deploy nginx collector | `infra/collector/deploy_nginx.sh` | | Deploy nginx collector | `infra/collector/deploy_nginx.sh` |
| ADR identitas / split / dokumen | `.cursor/adr/001-agent-asset-identity.md`, `002-asset-split-by-hardware.md`, `003-asset-documents-qnap.md` |
### URL produksi (domain `gisportal.id`) ### URL produksi (domain `gisportal.id`)
@ -30,7 +33,42 @@ Invoke-WebRequest -Uri 'https://agent.gisportal.id/install/GeoNetAgent-Install.p
& "$env:TEMP\GeoNetAgent-Install.ps1" -SkipInitialRun & "$env:TEMP\GeoNetAgent-Install.ps1" -SkipInitialRun
``` ```
Path install: `%ProgramData%\GeoNetAgent` · Task: `GeoNetAgent-Report`, `GeoNetAgent-Report-Startup` Alternatif unduh: `curl.exe -fsSL --tlsv1.2 "https://agent.gisportal.id/install/GeoNetAgent-Install.ps1" -o "$env:TEMP\GeoNetAgent-Install.ps1"`
> **Urutan:** unduh dulu (baris 1), baru jalankan (baris 2). Jangan kebalik.
Path install: `%ProgramData%\GeoNetAgent` · Task: `GeoNetAgent-Report`, `GeoNetAgent-Report-Startup` (interval **3 jam**)
Kirim laporan manual / cek auto-update:
```powershell
Get-Content "$env:ProgramData\GeoNetAgent\VERSION"
& "$env:ProgramData\GeoNetAgent\UserAgent.ps1"
```
### Admin — deploy & DB
| Aksi | Perintah |
|------|----------|
| Publish agent | `.\infra\collector\publish_agent_install.ps1` (PowerShell) atau `bash infra/collector/publish_agent_install.sh` |
| Deploy collector | `.\infra\collector\push_collector.ps1` (PowerShell) atau `bash infra/collector/push_collector.sh` |
| Deploy nginx | `infra/collector/deploy_nginx.sh` |
| Migration DB | `002`–`004` applied dev · `005_asset_documents.sql` (Fase 3, belum apply) |
| View hardware | `v_asset_hardware_latest` (migration 004) |
| Split asset (hardware campur) | `docker exec geonet-collector python /app/scripts/split_asset_by_hardware.py <uuid>` |
### Agent versi saat ini
**v0.1.10-phase0** — `cpu{}` detail, `physical_disks[]`, RAM/GPU detail, `system.form_factor`, auto-update via manifest.
**Deploy wajib setiap bump versi agent** (urutan):
```powershell
.\infra\collector\publish_agent_install.ps1 # upload ke agent.gisportal.id/install/
.\infra\collector\push_collector.ps1 # rebuild UI/API jika ada perubahan collector
```
Tanpa publish, installer/auto-update user tetap dapat versi lama dari server.
--- ---
@ -77,13 +115,21 @@ Path install: `%ProgramData%\GeoNetAgent` · Task: `GeoNetAgent-Report`, `GeoNet
- Dokumentasi konsisten dengan implementasi. - Dokumentasi konsisten dengan implementasi.
- Rules AI tetap menjadi acuan utama pengembangan. - Rules AI tetap menjadi acuan utama pengembangan.
- Project tetap dapat dipahami AI Agent meskipun riwayat chat tidak tersedia. - Project tetap dapat dipahami AI Agent meskipun riwayat chat tidak tersedia.
- **Bump versi agent**: `publish_agent_install.ps1` + `push_collector.ps1` (jika collector/UI berubah) sudah dijalankan atau user diberi tahu; **Riwayat milestone** di `progress.md` ditambah (rule: `.cursor/rules/agent-release-deploy.mdc`).
- Jika salah satu poin belum terpenuhi, jangan nyatakan pekerjaan selesai. - Jika salah satu poin belum terpenuhi, jangan nyatakan pekerjaan selesai.
``` ```
### Sync terakhir: 2026-06-17 ### Sync terakhir: 2026-06-22
- Domain proyek: **`*.gisportal.id`** (bukan `geonet.co.id`) - **ADR 003** — dokumen procurement/service/complain di QNAP S3; `asset_invoices`, `tickets`, `document_attachments` (migration 005 draft, belum apply)
- Reverse proxy `10.100.1.24`: agent POST publik, collector HTTPS, HTTP `:8000` LAN, web install `/install/` - View **`v_asset_hardware_latest`** — CPU/GPU/RAM/display per asset (migration 004, applied dev)
- `Install-GeoNetAgent.ps1` + publish script; pilot install laptop dev (`DESKTOP-RRJ9G01`) - Agent **v0.1.10-phase0**: `cpu{}` detail, section CPU di UI, `Get-CpuInfo.ps1` (21 file manifest)
- Nginx `ssi off` di server `agent.gisportal.id` (fix truncate HTML/ps1) - **Deploy Windows**: `.\infra\collector\publish_agent_install.ps1` + `.\infra\collector\push_collector.ps1` (WSL tidak wajib)
- **Auto-update**: `UserAgent.ps1` bandingkan `VERSION` lokal vs `manifest.json` server sebelum kirim laporan
- **Periferal bawaan vs eksternal**: `peripherals[].is_external` (monitor/USB/BT); printer & upgrade hardware belum — lihat backlog di `progress.md`
- Agent **v0.1.9**: `physical_disks[]`, RAM (DDR label, part no), GPU detail, `form_factor` Laptop/PC
- Agent **v0.1.8**: GPU `gpus[]`, disk model, merge rule disk-clone, fix nginx truncate `$env:` di file served
- Collector: UI badge Laptop/PC, split asset by hardware (ADR 002), `machine_id` non-UNIQUE (migration 003)
- Pilot `DESKTOP-RRJ9G01`: split laptop Dell vs PC Gigabyte; domain `*.gisportal.id`
- Nginx `agent.gisportal.id`: `ssi off`, hindari `$env:` di file `.ps1` yang di-serve

404
GeoNetAgent/samples/agent-reports/DESKTOP-RRJ9G01_20260622_132706.json

@ -0,0 +1,404 @@
{
"schema_version": "1.0",
"agent_version": "0.1.10-phase0",
"collected_at": "2026-06-22T13:27:06+07:00",
"hostname": "DESKTOP-RRJ9G01",
"machine_id": "3709baa5-5265-4e4d-9a81-fb8c8927bb3b",
"smbios_uuid": "03D502E0-045E-0550-8E06-BA0700080009",
"serial_number": null,
"asset_type": "endpoint",
"logged_in_user": "DESKTOP-RRJ9G01\\admin",
"system": {
"manufacturer": "Gigabyte Technology Co., Ltd.",
"model": "H81M-DS2",
"bios_version": "F2",
"os_name": "Microsoft Windows 11 Pro",
"os_version": "10.0",
"os_build": "10.0.26200 (26200)",
"cpu_model": "Intel(R) Core(TM) i5-4460 CPU @ 3.20GHz",
"ram_gb": 15.9,
"gpu_model": "NVIDIA GeForce GT 730",
"last_boot_at": "2026-06-22T09:42:14+07:00",
"form_factor": "desktop",
"chassis_label": "Desktop",
"chassis_types": [
3
],
"pc_system_type": 1,
"pc_system_label": "Desktop"
},
"cpu": {
"model": "Intel(R) Core(TM) i5-4460 CPU @ 3.20GHz",
"manufacturer": "GenuineIntel",
"cores": 4,
"logical_processors": 4,
"max_clock_mhz": 3201,
"current_clock_mhz": 3201,
"socket": "SOCKET 0",
"socket_count": 1,
"l2_cache_kb": 1024,
"l3_cache_kb": 6144,
"architecture": "x64"
},
"gpus": [
{
"name": "NVIDIA GeForce GT 730",
"adapter_compatibility": "NVIDIA",
"video_processor": "GeForce GT 730",
"driver_version": "23.21.13.8813",
"driver_date": "2017-10-27T07:00:00+07:00",
"dedicated_memory_mb": 2048,
"pnp_device_id": "PCI\\VEN_10DE\u0026DEV_0F02\u0026SUBSYS_00000000\u0026REV_A1\\4\u00261286464\u00260\u00260008",
"status": "OK",
"current_resolution": "1920x1080",
"video_mode_description": "1920 x 1080 x 4294967296 colors"
},
{
"name": "Intel(R) HD Graphics 4600",
"adapter_compatibility": "Intel Corporation",
"video_processor": "Intel(R) HD Graphics Family",
"driver_version": "20.19.15.4531",
"driver_date": "2016-09-29T07:00:00+07:00",
"dedicated_memory_mb": 1024,
"pnp_device_id": "PCI\\VEN_8086\u0026DEV_0412\u0026SUBSYS_D0001458\u0026REV_06\\3\u002611583659\u00260\u002610",
"status": "OK",
"current_resolution": "1366x768",
"video_mode_description": "1366 x 768 x 4294967296 colors"
}
],
"disks": [
{
"drive_letter": "C:",
"total_gb": 188.7,
"free_gb": 4.5,
"pct_used": 97.6,
"file_system": "NTFS",
"model": "ADATA SU650"
},
{
"drive_letter": "D:",
"total_gb": 256.5,
"free_gb": 68.4,
"pct_used": 73.3,
"file_system": "NTFS",
"model": "ADATA SU650"
}
],
"physical_disks": [
{
"disk_number": 0,
"device_id": "0",
"model": "ADATA SU650",
"friendly_name": "ADATA SU650",
"media_type": "SSD",
"operational_status": "OK",
"health_status": "Healthy",
"total_gb": 447.1,
"partition_style": "MBR",
"bus_type": "SATA",
"serial_number": "2N29292H8BN9"
}
],
"memory_modules": [
{
"slot": "ChannelA-DIMM0",
"bank_label": "BANK 0",
"capacity_gb": 8,
"speed_mhz": 1600,
"configured_speed_mhz": 1600,
"manufacturer": "0000",
"part_number": "",
"serial_number": "00000000",
"form_factor": "tsop",
"memory_type": 24,
"memory_type_label": "DDR3",
"data_width": 64,
"total_width": 64,
"rank": null,
"configured_voltage_mv": null
},
{
"slot": "ChannelB-DIMM0",
"bank_label": "BANK 2",
"capacity_gb": 8,
"speed_mhz": 1600,
"configured_speed_mhz": 1600,
"manufacturer": "0000",
"part_number": "",
"serial_number": "00000000",
"form_factor": "tsop",
"memory_type": 24,
"memory_type_label": "DDR3",
"data_width": 64,
"total_width": 64,
"rank": null,
"configured_voltage_mv": null
}
],
"memory_summary": {
"total_gb": 16,
"module_count": 2,
"slot_count": 2,
"empty_slots": 0,
"layout": "2x8GB",
"configuration": "2x8GB tsop DDR3",
"upgradeable": false,
"ddr_generation": "DDR3"
},
"network": [
{
"adapter_name": "Realtek(R) PCI(e) Ethernet Controller",
"mac_address": "E0:D5:5E:50:8E:BA",
"ip_address": "192.168.0.21",
"adapter_kind": "physical",
"status": "Up",
"is_primary": true,
"is_built_in": true
},
{
"adapter_name": "Fortinet SSL VPN Virtual Ethernet Adapter",
"mac_address": "00:09:0F:AA:00:01",
"ip_address": null,
"adapter_kind": "virtual",
"status": "Not Present",
"is_primary": false,
"is_built_in": null
},
{
"adapter_name": "Fortinet Virtual Ethernet Adapter (NDIS 6.30)",
"mac_address": "00:09:0F:FE:00:01",
"ip_address": null,
"adapter_kind": "virtual",
"status": "Disconnected",
"is_primary": false,
"is_built_in": null
},
{
"adapter_name": "Hyper-V Virtual Ethernet Adapter",
"mac_address": "00:15:5D:F1:DB:09",
"ip_address": "172.24.128.1",
"adapter_kind": "virtual",
"status": "Up",
"is_primary": false,
"is_built_in": null
},
{
"adapter_name": "OpenVPN Data Channel Offload",
"mac_address": null,
"ip_address": null,
"adapter_kind": "virtual",
"status": "Disconnected",
"is_primary": false,
"is_built_in": null
},
{
"adapter_name": "PANGP Virtual Ethernet Adapter Secure #2",
"mac_address": "02:50:41:00:00:01",
"ip_address": null,
"adapter_kind": "virtual",
"status": "Disabled",
"is_primary": false,
"is_built_in": null
},
{
"adapter_name": "TAP-Windows Adapter V9 for OpenVPN Connect",
"mac_address": "00:FF:E0:E1:4D:4D",
"ip_address": null,
"adapter_kind": "virtual",
"status": "Disconnected",
"is_primary": false,
"is_built_in": null
}
],
"asset_identity": {
"primary_physical_mac": "E0:D5:5E:50:8E:BA",
"primary_physical_adapter": "Realtek(R) PCI(e) Ethernet Controller",
"primary_mac_built_in": true,
"primary_mac_source": "primary_route_built_in_physical",
"merge_key": "mac:E0:D5:5E:50:8E:BA",
"recommended_match_order": [
"serial_number",
"smbios_uuid",
"primary_physical_mac",
"machine_id",
"hostname"
]
},
"peripherals": [
{
"name": "E1942",
"device_class": "Monitor",
"manufacturer": "GSM",
"connection": "display",
"device_role": "display",
"vid": null,
"pid": null,
"pnp_device_id": "DISPLAY\\GSM4C09\\5\u0026316DE597\u00260\u0026UID4865",
"device_serial": "211INNG5R639",
"edid_product": "GSM4C09",
"resolution": "1920x1080",
"resolution_width": 1920,
"resolution_height": 1080,
"refresh_hz": 60,
"status": "OK",
"is_external": true
},
{
"name": "Q27G4",
"device_class": "Monitor",
"manufacturer": "AOC",
"connection": "display",
"device_role": "display",
"vid": null,
"pid": null,
"pnp_device_id": "DISPLAY\\AOCB403\\5\u0026316DE597\u00260\u0026UID4353",
"device_serial": "1O0Q7HA002010",
"edid_product": "AOCB403",
"resolution": "1366x768",
"resolution_width": 1366,
"resolution_height": 768,
"refresh_hz": 60,
"status": "OK",
"is_external": true
},
{
"name": "USB Input Device",
"device_class": "HIDClass",
"manufacturer": "(Standard system devices)",
"connection": "usb",
"device_role": "peripheral",
"vid": "3151",
"pid": "3022",
"pnp_device_id": "USB\\VID_3151\u0026PID_3022\u0026MI_01\\8\u0026104DB858\u00260\u00260001",
"device_serial": null,
"edid_product": null,
"resolution": null,
"resolution_width": null,
"resolution_height": null,
"refresh_hz": null,
"status": "OK",
"is_external": true
},
{
"name": "USB Input Device",
"device_class": "HIDClass",
"manufacturer": "(Standard system devices)",
"connection": "usb",
"device_role": "peripheral",
"vid": "3151",
"pid": "3022",
"pnp_device_id": "USB\\VID_3151\u0026PID_3022\u0026MI_00\\8\u0026104DB858\u00260\u00260000",
"device_serial": null,
"edid_product": null,
"resolution": null,
"resolution_width": null,
"resolution_height": null,
"refresh_hz": null,
"status": "OK",
"is_external": true
},
{
"name": "HID-compliant consumer control device",
"device_class": "HIDClass",
"manufacturer": "Microsoft",
"connection": "usb_hid",
"device_role": "input",
"vid": "3151",
"pid": "3022",
"pnp_device_id": "HID\\VID_3151\u0026PID_3022\u0026MI_01\u0026COL03\\9\u00262CF1CDFF\u00260\u00260002",
"device_serial": null,
"edid_product": null,
"resolution": null,
"resolution_width": null,
"resolution_height": null,
"refresh_hz": null,
"status": "OK",
"is_external": true
},
{
"name": "HID-compliant system controller",
"device_class": "HIDClass",
"manufacturer": "(Standard system devices)",
"connection": "usb_hid",
"device_role": "input",
"vid": "3151",
"pid": "3022",
"pnp_device_id": "HID\\VID_3151\u0026PID_3022\u0026MI_01\u0026COL02\\9\u00262CF1CDFF\u00260\u00260001",
"device_serial": null,
"edid_product": null,
"resolution": null,
"resolution_width": null,
"resolution_height": null,
"refresh_hz": null,
"status": "OK",
"is_external": true
},
{
"name": "HID-compliant vendor-defined device",
"device_class": "HIDClass",
"manufacturer": "(Standard system devices)",
"connection": "usb_hid",
"device_role": "input",
"vid": "3151",
"pid": "3022",
"pnp_device_id": "HID\\VID_3151\u0026PID_3022\u0026MI_01\u0026COL04\\9\u00262CF1CDFF\u00260\u00260003",
"device_serial": null,
"edid_product": null,
"resolution": null,
"resolution_width": null,
"resolution_height": null,
"refresh_hz": null,
"status": "OK",
"is_external": true
},
{
"name": "HID Keyboard Device",
"device_class": "Keyboard",
"manufacturer": "(Standard keyboards)",
"connection": "usb_hid",
"device_role": "input",
"vid": "3151",
"pid": "3022",
"pnp_device_id": "HID\\VID_3151\u0026PID_3022\u0026MI_00\\9\u002638016031\u00260\u00260000",
"device_serial": null,
"edid_product": null,
"resolution": null,
"resolution_width": null,
"resolution_height": null,
"refresh_hz": null,
"status": "OK",
"is_external": true
},
{
"name": "HID-compliant mouse",
"device_class": "Mouse",
"manufacturer": "Microsoft",
"connection": "usb_hid",
"device_role": "input",
"vid": "3151",
"pid": "3022",
"pnp_device_id": "HID\\VID_3151\u0026PID_3022\u0026MI_01\u0026COL01\\9\u00262CF1CDFF\u00260\u00260000",
"device_serial": null,
"edid_product": null,
"resolution": null,
"resolution_width": null,
"resolution_height": null,
"refresh_hz": null,
"status": "OK",
"is_external": true
}
],
"health": {
"bsod_count_30d": 10,
"pending_reboot": false,
"last_patch_installed": "2026-02-27"
},
"software": [
],
"metadata": {
"collect_mode": "phase0-local",
"software_included": false,
"software_count": 0
}
}

1650
GeoNetAgent/samples/dxdiag/DxDiag.txt

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save