Browse Source

feat(GeoNetAgent): agent v0.1.8, GPO rollout, disk model, install core

Refactor install/update into Agent-InstallCore, add GPO startup scripts,
GPU and disk model collection, DB migration 002, and collector UI updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
master
Budi Setiawan 4 weeks ago
parent
commit
527fd6223d
  1. 15
      GeoNetAgent/.cursor/api-contracts.md
  2. 5
      GeoNetAgent/.cursor/db-schema.md
  3. 119
      GeoNetAgent/.cursor/progress.md
  4. 20
      GeoNetAgent/agent/Collect-GeoNetReport.ps1
  5. 175
      GeoNetAgent/agent/Install-GeoNetAgent.ps1
  6. 6
      GeoNetAgent/agent/README.md
  7. 9
      GeoNetAgent/agent/Send-GeoNetReport.ps1
  8. 31
      GeoNetAgent/agent/UserAgent.ps1
  9. 2
      GeoNetAgent/agent/VERSION
  10. 6
      GeoNetAgent/agent/gpo/GeoNetAgent-GPO-Startup.cmd
  11. 185
      GeoNetAgent/agent/gpo/GeoNetAgent-GPO-Startup.ps1
  12. 146
      GeoNetAgent/agent/gpo/README.md
  13. 21
      GeoNetAgent/agent/install/GeoNetAgent-Install.ps1
  14. 21
      GeoNetAgent/agent/install/bootstrap.ps1
  15. 5
      GeoNetAgent/agent/install/index.html
  16. 9
      GeoNetAgent/agent/install/manifest.json
  17. 209
      GeoNetAgent/agent/lib/Agent-InstallCore.ps1
  18. 27
      GeoNetAgent/agent/lib/Get-DiskInfo.ps1
  19. 39
      GeoNetAgent/agent/lib/Get-GpuInfo.ps1
  20. 13
      GeoNetAgent/agent/lib/Get-SystemInfo.ps1
  21. 74
      GeoNetAgent/agent/lib/Update-GeoNetAgent.ps1
  22. 1
      GeoNetAgent/collector/app/db/models.py
  23. 7
      GeoNetAgent/collector/app/schemas/agent_report.py
  24. 7
      GeoNetAgent/collector/app/schemas/assets.py
  25. 32
      GeoNetAgent/collector/app/services/asset_read_service.py
  26. 3
      GeoNetAgent/collector/app/services/asset_service.py
  27. 17
      GeoNetAgent/collector/static/app.js
  28. 2
      GeoNetAgent/infra/collector/publish_agent_install.sh
  29. 3
      GeoNetAgent/infra/db/migrations/002_disk_model.sql
  30. 320
      GeoNetAgent/samples/agent-reports/DESKTOP-RRJ9G01_20260618_112523.json

15
GeoNetAgent/.cursor/api-contracts.md

@ -51,16 +51,27 @@ Content-Type: application/json
"os_build": "10.0.26200 (26200)", "os_build": "10.0.26200 (26200)",
"cpu_model": "Intel(R) Core(TM) i7-4710HQ CPU @ 2.50GHz", "cpu_model": "Intel(R) Core(TM) i7-4710HQ CPU @ 2.50GHz",
"ram_gb": 15.9, "ram_gb": 15.9,
"gpu_model": "Intel(R) HD Graphics 4600", "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"
}, },
"gpus": [
{
"name": "NVIDIA GeForce GTX 850M",
"dedicated_memory_mb": 4054
},
{
"name": "Intel(R) HD Graphics 4600",
"dedicated_memory_mb": 128
}
],
"disks": [ "disks": [
{ {
"drive_letter": "C:", "drive_letter": "C:",
"total_gb": 188.7, "total_gb": 188.7,
"free_gb": 3.1, "free_gb": 3.1,
"pct_used": 98.3, "pct_used": 98.3,
"file_system": "NTFS" "file_system": "NTFS",
"model": "ADATA SU650"
} }
], ],
"memory_modules": [ "memory_modules": [

5
GeoNetAgent/.cursor/db-schema.md

@ -187,9 +187,14 @@ Normalized dari snapshot — memudahkan query "disk > 85%".
| free_gb | numeric(10,2) | NO | | | free_gb | numeric(10,2) | NO | |
| pct_used | numeric(5,2) | NO | generated or computed | | pct_used | numeric(5,2) | NO | generated or computed |
| file_system | varchar(20) | YES | NTFS | | file_system | varchar(20) | YES | NTFS |
| model | varchar(100) | YES | physical drive model (WMI), e.g. ADATA SU650 |
**Index:** `idx_disks_snapshot` **Index:** `idx_disks_snapshot`
**Migration:** `002_disk_model.sql` menambah kolom `model`.
**Catatan GPU:** array `gpus[]` disimpan di `asset_snapshots.payload` (belum dinormalisasi ke tabel terpisah).
--- ---
### `asset_network_adapters` ### `asset_network_adapters`

119
GeoNetAgent/.cursor/progress.md

@ -1,120 +1,239 @@
# 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-17 (web install agent · domain gisportal.id · reverse proxy) **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 POC | `v0.1.6-phase0` — collect + POST + web install | | Agent POC | `v0.1.6-phase0` — collect + POST + web install |
| Collector | `https://collector.gisportal.id` UI · `agent.gisportal.id` POST · `:8000` HTTP LAN | | 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 | 7 / 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] FastAPI `/api/v1/agent/report` + `/health` + read API assets - [x] FastAPI `/api/v1/agent/report` + `/health` + read API assets
- [x] PowerShell agent v0.1.6 + 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`
- [ ] Laravel asset index (read-only) - [ ] Laravel asset index (read-only)
- [ ] Auth IT user lokal (M9) - [ ] Auth IT user lokal (M9)
--- ---
## Fase 2 — Integrasi monitoring ⏳ ## Fase 2 — Integrasi monitoring ⏳
**DoD:** Alert Zabbix di portal; status host dari Zabbix API. **DoD:** Alert Zabbix di portal; status host dari Zabbix API.
- [ ] Zabbix webhook → Laravel - [ ] 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 manual 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
--- ---
## 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`
--- ---
## 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` |

20
GeoNetAgent/agent/Collect-GeoNetReport.ps1

@ -24,7 +24,7 @@ param(
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$AgentVersion = '0.1.6-phase0' $AgentVersion = '0.1.8-phase0'
$SchemaVersion = '1.0' $SchemaVersion = '1.0'
$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path $scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
@ -33,6 +33,7 @@ $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-GpuInfo.ps1')
. (Join-Path $libPath 'Get-DiskInfo.ps1') . (Join-Path $libPath 'Get-DiskInfo.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')
@ -99,7 +100,22 @@ $payload = [ordered]@{
gpu_model = (Convert-GeoNetOptionalString -Value $sys.GpuModel) gpu_model = (Convert-GeoNetOptionalString -Value $sys.GpuModel)
last_boot_at = $sys.LastBootAt last_boot_at = $sys.LastBootAt
} }
disks = $disks 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 { memory_modules = @($memory.Modules | ForEach-Object {
[ordered]@{ [ordered]@{
slot = $_.slot slot = $_.slot

175
GeoNetAgent/agent/Install-GeoNetAgent.ps1

@ -8,177 +8,18 @@ param(
[string]$SourcePath, [string]$SourcePath,
[switch]$SkipScheduledTask, [switch]$SkipScheduledTask,
[switch]$SkipInitialRun, [switch]$SkipInitialRun,
[int]$ScheduleHours = 6 [int]$ScheduleHours = 3
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$core = Join-Path $PSScriptRoot 'lib\Agent-InstallCore.ps1'
function Test-GeoNetAdmin { if (-not (Test-Path $core)) {
$id = [Security.Principal.WindowsIdentity]::GetCurrent() $core = Join-Path $InstallRoot 'lib\Agent-InstallCore.ps1'
$p = New-Object Security.Principal.WindowsPrincipal($id)
return $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Get-GeoNetInstallBaseUrl {
param([string]$Url)
return $Url.TrimEnd('/')
}
function Get-GeoNetRemoteJson {
param([string]$Url)
return (Invoke-WebRequest -Uri $Url -UseBasicParsing).Content | ConvertFrom-Json
}
function Get-GeoNetRemoteFile {
param([string]$Url, [string]$Destination)
$parent = Split-Path -Parent $Destination
if (-not (Test-Path $parent)) {
New-Item -ItemType Directory -Path $parent -Force | Out-Null
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$lastErr = $null
for ($try = 1; $try -le 3; $try++) {
try {
Invoke-WebRequest -Uri $Url -OutFile $Destination -UseBasicParsing -TimeoutSec 90
if ((Get-Item $Destination).Length -gt 0) { return }
}
catch {
$lastErr = $_
Start-Sleep -Seconds 2
}
}
if (Get-Command curl.exe -ErrorAction SilentlyContinue) {
& curl.exe -fsSL --tlsv1.2 $Url -o $Destination
if ($LASTEXITCODE -eq 0 -and (Test-Path $Destination)) { return }
}
throw $lastErr
}
function Copy-GeoNetAgentPayload {
param(
[string]$SourceRoot,
[string]$DestinationRoot,
[object]$Manifest
)
foreach ($rel in $Manifest.files) {
$src = Join-Path $SourceRoot ($rel -replace '/', [IO.Path]::DirectorySeparatorChar)
$dst = Join-Path $DestinationRoot ($rel -replace '/', [IO.Path]::DirectorySeparatorChar)
if (-not (Test-Path $src)) {
throw "File sumber tidak ada: $src"
}
$dir = Split-Path -Parent $dst
if (-not (Test-Path $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
}
Copy-Item -Path $src -Destination $dst -Force
}
}
function Install-GeoNetAgentPayload {
param(
[string]$Base,
[string]$DestinationRoot,
[object]$Manifest
)
foreach ($rel in $Manifest.files) {
$url = "$Base/$($rel -replace '\\', '/')"
$dst = Join-Path $DestinationRoot ($rel -replace '/', [IO.Path]::DirectorySeparatorChar)
Write-Host " -> $rel"
Get-GeoNetRemoteFile -Url $url -Destination $dst
}
} }
if (-not (Test-Path $core)) {
function Set-GeoNetAgentConfig { throw "Agent-InstallCore.ps1 tidak ditemukan (cari di $PSScriptRoot\lib dan $InstallRoot\lib)."
param(
[string]$InstallRoot,
[string]$Base,
[string]$Token,
[bool]$UseRemoteConfig
)
$configPath = Join-Path $InstallRoot 'config.json'
if ($Token) {
$cfg = [ordered]@{
collector_url = 'https://agent.gisportal.id/api/v1/agent/report'
agent_token = $Token
include_software = $true
max_software = 500
timeout_seconds = 120
}
if (Test-Path $configPath) {
try {
$existing = Get-Content $configPath -Raw | ConvertFrom-Json
if ($existing.collector_url) { $cfg.collector_url = $existing.collector_url }
if ($null -ne $existing.include_software) { $cfg.include_software = [bool]$existing.include_software }
if ($existing.max_software) { $cfg.max_software = [int]$existing.max_software }
if ($existing.timeout_seconds) { $cfg.timeout_seconds = [int]$existing.timeout_seconds }
}
catch { }
}
$cfg | ConvertTo-Json | Set-Content -Path $configPath -Encoding UTF8
return
}
if ((Test-Path $configPath) -and -not $UseRemoteConfig) {
Write-Host 'config.json lokal dipertahankan.' -ForegroundColor DarkGray
return
}
if ($UseRemoteConfig) {
Get-GeoNetRemoteFile -Url "$Base/config.json" -Destination $configPath
return
}
throw 'config.json tidak ada. Sediakan -AgentToken atau config.json di URL install.'
}
function Invoke-GeoNetSchtasks {
param(
[Parameter(Mandatory)][string[]]$ArgumentList,
[switch]$IgnoreError
)
$prev = $ErrorActionPreference
$ErrorActionPreference = 'SilentlyContinue'
$output = & schtasks.exe @ArgumentList 2>&1
$code = $LASTEXITCODE
$ErrorActionPreference = $prev
if ($code -ne 0 -and -not $IgnoreError) {
$msg = ($output | Out-String).Trim()
if ($msg) { throw $msg }
throw "schtasks gagal (exit $code): schtasks.exe $($ArgumentList -join ' ')"
}
return $code
}
function Register-GeoNetAgentTask {
param(
[string]$InstallRoot,
[int]$Hours
)
$userScript = Join-Path $InstallRoot 'UserAgent.ps1'
if (-not (Test-Path $userScript)) {
throw "UserAgent.ps1 tidak ditemukan di $InstallRoot"
}
$taskArgs = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$userScript`""
$taskCmd = "powershell.exe $taskArgs"
$taskNames = @('GeoNetAgent-Report', 'GeoNetAgent-Report-Startup')
foreach ($name in $taskNames) {
Unregister-ScheduledTask -TaskName $name -Confirm:$false -ErrorAction SilentlyContinue
Invoke-GeoNetSchtasks -ArgumentList @('/Delete', '/TN', $name, '/F') -IgnoreError | Out-Null
}
Invoke-GeoNetSchtasks -ArgumentList @(
'/Create', '/TN', 'GeoNetAgent-Report-Startup', '/TR', $taskCmd,
'/SC', 'ONSTART', '/RU', 'SYSTEM', '/RL', 'HIGHEST', '/F'
) | Out-Null
Invoke-GeoNetSchtasks -ArgumentList @(
'/Create', '/TN', 'GeoNetAgent-Report', '/TR', $taskCmd,
'/SC', 'HOURLY', '/MO', "$Hours", '/RU', 'SYSTEM', '/RL', 'HIGHEST', '/F'
) | Out-Null
} }
. $core
$base = Get-GeoNetInstallBaseUrl -Url $BaseUrl $base = Get-GeoNetInstallBaseUrl -Url $BaseUrl
Write-Host "GeoNetAgent install/update" -ForegroundColor Cyan Write-Host "GeoNetAgent install/update" -ForegroundColor Cyan
@ -216,7 +57,7 @@ else {
Set-GeoNetAgentConfig -InstallRoot $InstallRoot -Base $base -Token $AgentToken -UseRemoteConfig:$remoteConfig Set-GeoNetAgentConfig -InstallRoot $InstallRoot -Base $base -Token $AgentToken -UseRemoteConfig:$remoteConfig
} }
$manifest.version | Set-Content -Path (Join-Path $InstallRoot 'VERSION') -Encoding ASCII -NoNewline Set-GeoNetLocalAgentVersion -InstallRoot $InstallRoot -Version $manifest.version
if (-not $SkipScheduledTask) { if (-not $SkipScheduledTask) {
if (-not (Test-GeoNetAdmin)) { if (-not (Test-GeoNetAdmin)) {

6
GeoNetAgent/agent/README.md

@ -30,6 +30,12 @@ Atau hanya pasang/update + task (tanpa laporan):
Perintah yang sama untuk **update** agent. `config.json` lokal di `%ProgramData%\GeoNetAgent` dipertahankan. Perintah yang sama untuk **update** agent. `config.json` lokal di `%ProgramData%\GeoNetAgent` dipertahankan.
Setelah terpasang, **auto-update**: Scheduled Task (SYSTEM) membandingkan `manifest.json` server vs file `VERSION` lokal sebelum setiap laporan. Jika versi server lebih baru, file agent diunduh ulang tanpa interaksi user.
## Rollout massal (GPO)
Skrip startup siap pakai: **`agent/gpo/`** — lihat [gpo/README.md](./gpo/README.md).
## IT — publish paket ke server ## IT — publish paket ke server
```bash ```bash

9
GeoNetAgent/agent/Send-GeoNetReport.ps1

@ -22,6 +22,7 @@ $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-GpuInfo.ps1')
. (Join-Path $libPath 'Get-DiskInfo.ps1') . (Join-Path $libPath 'Get-DiskInfo.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')
@ -54,7 +55,7 @@ if (-not $CollectorUrl -or -not $AgentToken) {
} }
# Build payload (same as Collect-GeoNetReport) # Build payload (same as Collect-GeoNetReport)
$AgentVersion = '0.1.6-phase0' $AgentVersion = '0.1.8-phase0'
$SchemaVersion = '1.0' $SchemaVersion = '1.0'
$sys = Get-GeoNetSystemInfo $sys = Get-GeoNetSystemInfo
@ -102,6 +103,12 @@ $payload = [ordered]@{
gpu_model = (Convert-GeoNetOptionalString -Value $sys.GpuModel) gpu_model = (Convert-GeoNetOptionalString -Value $sys.GpuModel)
last_boot_at = $sys.LastBootAt last_boot_at = $sys.LastBootAt
} }
gpus = @($sys.Gpus | ForEach-Object {
[ordered]@{
name = $_.name
dedicated_memory_mb = $_.dedicated_memory_mb
}
})
disks = $disks disks = $disks
memory_modules = @($memory.Modules) memory_modules = @($memory.Modules)
memory_summary = $memory.Summary memory_summary = $memory.Summary

31
GeoNetAgent/agent/UserAgent.ps1

@ -1,41 +1,50 @@
<# <#
.SYNOPSIS .SYNOPSIS
Entry point agent GeoNetAgent untuk user - sekali run / double-click. Entry point agent GeoNetAgent auto-update (opsional) + kirim laporan.
.DESCRIPTION .DESCRIPTION
Jalankan dari share IT, mis.: Dipanggil Scheduled Task (SYSTEM) atau manual.
\\10.100.1.10\software installer\GeonetAgent\agent\UserAgent.ps1 Sebelum kirim laporan: cek manifest.json vs VERSION lokal; update silent jika beda.
config.json lokal dipertahankan saat auto-update.
Membaca config global config.json di folder yang sama (collector URL + token).
Tidak perlu config per laptop - hostname dan identitas diambil otomatis dari WMI.
.EXAMPLE .EXAMPLE
powershell -NoProfile -ExecutionPolicy Bypass -File "\\10.100.1.10\software installer\GeonetAgent\agent\UserAgent.ps1" powershell -NoProfile -ExecutionPolicy Bypass -File "$env:ProgramData\GeoNetAgent\UserAgent.ps1"
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(
[switch]$IncludeSoftware [switch]$IncludeSoftware,
[switch]$SkipAutoUpdate
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$agentRoot = Split-Path -Parent $MyInvocation.MyCommand.Path $agentRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
$sendScript = Join-Path $agentRoot 'Send-GeoNetReport.ps1'
$configPath = Join-Path $agentRoot 'config.json' $configPath = Join-Path $agentRoot 'config.json'
$updateScript = Join-Path $agentRoot 'lib\Update-GeoNetAgent.ps1'
$sendScript = Join-Path $agentRoot 'Send-GeoNetReport.ps1'
if (-not (Test-Path $configPath)) { if (-not (Test-Path $configPath)) {
$shareTemplate = Join-Path $agentRoot 'config.share.json' $shareTemplate = Join-Path $agentRoot 'config.share.json'
if (Test-Path $shareTemplate) { if (Test-Path $shareTemplate) {
Write-Warning 'config.json tidak ada - memakai config.share.json. Di share QNAP rename ke config.json.' Write-Warning 'config.json tidak ada - memakai config.share.json.'
$configPath = $shareTemplate $configPath = $shareTemplate
} }
} }
if (-not $SkipAutoUpdate -and (Test-Path $updateScript)) {
. $updateScript
$null = Update-GeoNetAgentIfNeeded `
-InstallRoot $agentRoot `
-ConfigPath $configPath `
-ScheduleHours 3 `
-Quiet
}
if (-not (Test-Path $sendScript)) { if (-not (Test-Path $sendScript)) {
throw "Send-GeoNetReport.ps1 tidak ditemukan di: $agentRoot" throw "Send-GeoNetReport.ps1 tidak ditemukan di: $agentRoot"
} }
if (-not (Test-Path $configPath)) { if (-not (Test-Path $configPath)) {
throw "config.json tidak ditemukan di: $agentRoot. Copy config.share.json ke config.json di share IT." throw "config.json tidak ditemukan di: $agentRoot"
} }
$params = @{ $params = @{

2
GeoNetAgent/agent/VERSION

@ -1 +1 @@
0.1.6-phase0 0.1.8-phase0

6
GeoNetAgent/agent/gpo/GeoNetAgent-GPO-Startup.cmd

@ -0,0 +1,6 @@
@echo off
REM GeoNetAgent — GPO Computer Startup Script (wrapper)
REM Deploy folder ini ke SYSVOL / NETLOGON, lalu tautkan .cmd ini di GPO Startup.
powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "%~dp0GeoNetAgent-GPO-Startup.ps1"
exit /b 0

185
GeoNetAgent/agent/gpo/GeoNetAgent-GPO-Startup.ps1

@ -0,0 +1,185 @@
#Requires -Version 5.1
<#
.SYNOPSIS
GPO Startup Script pasang atau update GeoNetAgent (silent, SYSTEM).
.DESCRIPTION
Dipanggil dari Group Policy (Computer Configuration Startup Scripts).
- Belum terpasang install dari https://agent.gisportal.id/install/
- Sudah terpasang cek manifest vs VERSION, update jika perlu
- Memastikan Scheduled Task ada (startup + tiap 3 jam)
- Tidak kirim laporan langsung (-SkipInitialRun); task SYSTEM yang kirim
.PARAMETER BaseUrl
URL paket install. Default: https://agent.gisportal.id/install
.PARAMETER ScheduleHours
Interval laporan terjadwal. Default: 3
.PARAMETER MaxNetworkWaitSeconds
Tunggu jaringan siap sebelum unduh (boot awal). Default: 120
.NOTES
Log: %ProgramData%\GeoNetAgent\logs\gpo-startup.log
Exit selalu 0 agar GPO tidak mengulang agresif; cek log jika perlu.
#>
[CmdletBinding()]
param(
[string]$BaseUrl = 'https://agent.gisportal.id/install',
[int]$ScheduleHours = 3,
[int]$MaxNetworkWaitSeconds = 120
)
$ErrorActionPreference = 'Continue'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$InstallRoot = Join-Path $env:ProgramData 'GeoNetAgent'
$LogDir = Join-Path $InstallRoot 'logs'
$LogFile = Join-Path $LogDir 'gpo-startup.log'
function Write-GeoNetGpoLog {
param([string]$Message)
$line = '{0} {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message
try {
if (-not (Test-Path $LogDir)) {
New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
}
Add-Content -Path $LogFile -Value $line -Encoding UTF8
}
catch { }
}
function Wait-GeoNetNetwork {
param(
[string]$Url,
[int]$MaxSeconds
)
$base = $Url.TrimEnd('/')
$probe = "$base/manifest.json"
$deadline = (Get-Date).AddSeconds($MaxSeconds)
while ((Get-Date) -lt $deadline) {
try {
$null = Invoke-WebRequest -Uri $probe -Method Head -UseBasicParsing -TimeoutSec 8
return $true
}
catch {
Start-Sleep -Seconds 5
}
}
return $false
}
function Save-GeoNetRemoteFile {
param([string]$Url, [string]$Destination)
$parent = Split-Path -Parent $Destination
if (-not (Test-Path $parent)) {
New-Item -ItemType Directory -Path $parent -Force | Out-Null
}
try {
Invoke-WebRequest -Uri $Url -OutFile $Destination -UseBasicParsing -TimeoutSec 90
if ((Get-Item $Destination).Length -gt 0) { return }
}
catch {
if (Get-Command curl.exe -ErrorAction SilentlyContinue) {
& curl.exe -fsSL --tlsv1.2 $Url -o $Destination
if ($LASTEXITCODE -eq 0 -and (Test-Path $Destination)) { return }
}
throw
}
}
function Invoke-GeoNetGpoWebInstall {
param([string]$Url)
$base = $Url.TrimEnd('/')
$stage = Join-Path $env:TEMP 'GeoNetAgent-gpo-install'
$installer = Join-Path $stage 'Install-GeoNetAgent.ps1'
$coreLib = Join-Path $stage 'lib\Agent-InstallCore.ps1'
Write-GeoNetGpoLog "Unduh installer dari $base"
Save-GeoNetRemoteFile -Url "$base/Install-GeoNetAgent.ps1" -Destination $installer
Save-GeoNetRemoteFile -Url "$base/lib/Agent-InstallCore.ps1" -Destination $coreLib
Write-GeoNetGpoLog 'Jalankan Install-GeoNetAgent.ps1 -SkipInitialRun'
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $installer `
-BaseUrl $base -InstallRoot $InstallRoot -SkipInitialRun -ScheduleHours $ScheduleHours
if ($LASTEXITCODE -ne 0) {
throw "Install-GeoNetAgent exit code $LASTEXITCODE"
}
}
function Test-GeoNetScheduledTasks {
$names = @('GeoNetAgent-Report', 'GeoNetAgent-Report-Startup')
foreach ($name in $names) {
$null = & schtasks.exe /Query /TN $name /FO LIST 2>$null
if ($LASTEXITCODE -ne 0) { return $false }
}
return $true
}
function Repair-GeoNetScheduledTasks {
param([string]$Root, [int]$Hours)
$core = Join-Path $Root 'lib\Agent-InstallCore.ps1'
if (-not (Test-Path $core)) {
throw "Agent-InstallCore.ps1 tidak ada di $Root"
}
. $core
Register-GeoNetAgentTask -InstallRoot $Root -Hours $Hours
Write-GeoNetGpoLog "Scheduled Task diperbaiki (interval ${Hours} jam)"
}
Write-GeoNetGpoLog '=== GPO startup begin ==='
try {
if (-not (Wait-GeoNetNetwork -Url $BaseUrl -MaxSeconds $MaxNetworkWaitSeconds)) {
throw "Jaringan/install URL tidak reachable dalam ${MaxNetworkWaitSeconds}s"
}
$userAgent = Join-Path $InstallRoot 'UserAgent.ps1'
$updateScript = Join-Path $InstallRoot 'lib\Update-GeoNetAgent.ps1'
if (-not (Test-Path $userAgent)) {
Write-GeoNetGpoLog 'Agent belum terpasang — install baru'
Invoke-GeoNetGpoWebInstall -Url $BaseUrl
}
elseif (-not (Test-Path $updateScript)) {
Write-GeoNetGpoLog 'Agent lama (tanpa auto-update) — reinstall'
Invoke-GeoNetGpoWebInstall -Url $BaseUrl
}
else {
Write-GeoNetGpoLog 'Agent ada — cek update'
. $updateScript
$result = Update-GeoNetAgentIfNeeded `
-InstallRoot $InstallRoot `
-BaseUrl $BaseUrl `
-ConfigPath (Join-Path $InstallRoot 'config.json') `
-ScheduleHours $ScheduleHours `
-Quiet
if ($result.Updated) {
Write-GeoNetGpoLog "Updated: $($result.From) -> $($result.To)"
}
else {
Write-GeoNetGpoLog "Versi OK: $($result.From)"
if ($result.Error) {
Write-GeoNetGpoLog "Update check warning: $($result.Error)"
}
}
}
if (-not (Test-GeoNetScheduledTasks)) {
Write-GeoNetGpoLog 'Scheduled Task hilang — perbaiki'
Repair-GeoNetScheduledTasks -Root $InstallRoot -Hours $ScheduleHours
}
$version = $null
$versionPath = Join-Path $InstallRoot 'VERSION'
if (Test-Path $versionPath) {
$version = (Get-Content $versionPath -Raw).Trim()
}
Write-GeoNetGpoLog "Selesai OK. VERSION=$version"
}
catch {
Write-GeoNetGpoLog "ERROR: $($_.Exception.Message)"
}
Write-GeoNetGpoLog '=== GPO startup end ==='
exit 0

146
GeoNetAgent/agent/gpo/README.md

@ -0,0 +1,146 @@
# GeoNetAgent — Rollout massal via GPO
Skrip startup untuk **Computer Configuration** Active Directory. Jalan sebagai **SYSTEM** saat boot — tidak perlu user Admin login.
## Isi folder
| File | Fungsi |
|------|--------|
| `GeoNetAgent-GPO-Startup.cmd` | Entry point untuk GPO (tambahkan file ini di policy) |
| `GeoNetAgent-GPO-Startup.ps1` | Logika install / update / perbaiki task |
| `README.md` | Panduan ini |
## Prasyarat server
1. Agent v0.1.8+ sudah di-publish: `bash infra/collector/publish_agent_install.sh`
2. `https://agent.gisportal.id/install/manifest.json` bisa diakses dari jaringan user
3. Migration DB `002_disk_model.sql` sudah dijalankan (jika pakai fitur model disk)
## Langkah 1 — Salin ke SYSVOL
Salin seluruh folder `agent/gpo/` ke share domain, contoh:
```
\\gisportal.id\SYSVOL\gisportal.id\scripts\GeoNetAgent\
```
Atau (klassik):
```
\\<DC>\NETLOGON\GeoNetAgent\
```
Pastikan **Computer Account** domain bisa **Read** folder tersebut.
## Langkah 2 — Buat / edit GPO
1. Buka **Group Policy Management** di DC (`AD 2012 R2` OK)
2. Buat GPO baru, mis. `GeoNetAgent — Deploy Endpoint`
3. Edit GPO → **Computer Configuration****Policies****Windows Settings** → **Scripts (Startup/Shutdown)**
4. Tab **Startup****Add** → pilih:
```
\\gisportal.id\SYSVOL\gisportal.id\scripts\GeoNetAgent\GeoNetAgent-GPO-Startup.cmd
```
5. **OK** → tutup editor GPO
## Langkah 3 — Link ke OU
- Link GPO ke OU yang berisi **laptop/PC Windows** user
- **Jangan** link ke OU server jika server tidak perlu agent
- Urutan: taruh GPO di level OU yang tepat; hindari konflik dengan GPO lain yang memblokir script
## Langkah 4 — Uji (1–2 mesin dulu)
Pada laptop uji (sudah join domain):
```cmd
gpupdate /force
shutdown /r /t 0
```
Setelah boot, cek:
```powershell
Get-Content "$env:ProgramData\GeoNetAgent\VERSION"
Get-Content "$env:ProgramData\GeoNetAgent\logs\gpo-startup.log" -Tail 20
schtasks /Query /TN GeoNetAgent-Report /V /FO LIST
Test-Path "$env:ProgramData\GeoNetAgent\UserAgent.ps1"
```
Harapan:
- `VERSION` = versi terbaru di server (mis. `0.1.8-phase0`)
- Log berisi `Selesai OK`
- Task `GeoNetAgent-Report` — HOURLY, interval **3** jam, run as **SYSTEM**
## Perilaku skrip
| Kondisi | Aksi |
|---------|------|
| Agent belum ada | Install penuh dari web (silent, `-SkipInitialRun`) |
| Agent lama tanpa modul auto-update | Reinstall |
| Agent ada | Cek `manifest.json` vs `VERSION`, update jika beda |
| Scheduled Task hilang | Daftar ulang (startup + 3 jam) |
| Jaringan belum siap saat boot | Tunggu hingga 120 detik |
| Error | Catat di log, **exit 0** (boot tidak terganggu) |
Laporan inventaris dikirim oleh Scheduled Task (`UserAgent.ps1`), bukan langsung dari skrip GPO.
## Log
```
%ProgramData%\GeoNetAgent\logs\gpo-startup.log
```
Contoh:
```
2026-06-18 08:15:02 === GPO startup begin ===
2026-06-18 08:15:08 Agent belum terpasang — install baru
2026-06-18 08:15:45 Selesai OK. VERSION=0.1.8-phase0
2026-06-18 08:15:45 === GPO startup end ===
```
## Firewall / proxy
Laptop harus bisa **HTTPS outbound** ke:
- `https://agent.gisportal.id` (install + POST laporan)
Split-DNS MikroTik sudah mengarahkan domain ini ke `10.100.1.24` di LAN.
## Opsi parameter (advanced)
Edit baris di `.cmd` jika perlu:
```batch
powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "%~dp0GeoNetAgent-GPO-Startup.ps1" -ScheduleHours 3 -MaxNetworkWaitSeconds 180
```
## Rollback / uninstall
1. Unlink GPO dari OU
2. Hapus task:
```cmd
schtasks /Delete /TN GeoNetAgent-Report /F
schtasks /Delete /TN GeoNetAgent-Report-Startup /F
```
3. Hapus folder: `%ProgramData%\GeoNetAgent`
## Troubleshooting
| Gejala | Cek |
|--------|-----|
| Log tidak ada | GPO belum jalan / path script salah / OU salah |
| `Jaringan tidak reachable` | DNS, firewall, proxy; boot terlalu cepat — naikkan `MaxNetworkWaitSeconds` |
| VERSION kosong | Install gagal — buka log, coba web install manual Admin |
| Task tidak ada | Jalankan manual: `Repair` via install Admin sekali |
| GPO tidak apply | `gpresult /H report.html` pada laptop |
## Alternatif tanpa GPO
Web install manual (2 baris PS Admin) — lihat `agent/install/index.html` atau `agent/README.md`.

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

@ -6,16 +6,29 @@ param(
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$base = 'https://agent.gisportal.id/install' $base = 'https://agent.gisportal.id/install'
$installer = Join-Path $env:TEMP 'Install-GeoNetAgent.ps1'
Write-Host "GeoNetAgent install - $base" -ForegroundColor Cyan function Save-GeoNetInstallFile {
param([string]$Url, [string]$Destination)
$parent = Split-Path -Parent $Destination
if (-not (Test-Path $parent)) {
New-Item -ItemType Directory -Path $parent -Force | Out-Null
}
try { try {
Invoke-WebRequest -Uri "$base/Install-GeoNetAgent.ps1" -OutFile $installer -UseBasicParsing -TimeoutSec 90 Invoke-WebRequest -Uri $Url -OutFile $Destination -UseBasicParsing -TimeoutSec 90
} }
catch { catch {
& curl.exe -fsSL --tlsv1.2 "$base/Install-GeoNetAgent.ps1" -o $installer & curl.exe -fsSL --tlsv1.2 $Url -o $Destination
if ($LASTEXITCODE -ne 0) { throw $_ } if ($LASTEXITCODE -ne 0) { throw $_ }
} }
}
$stage = Join-Path $env:TEMP 'GeoNetAgent-install'
$installer = Join-Path $stage 'Install-GeoNetAgent.ps1'
$coreLib = Join-Path $stage 'lib\Agent-InstallCore.ps1'
Write-Host "GeoNetAgent install - $base" -ForegroundColor Cyan
Save-GeoNetInstallFile -Url "$base/Install-GeoNetAgent.ps1" -Destination $installer
Save-GeoNetInstallFile -Url "$base/lib/Agent-InstallCore.ps1" -Destination $coreLib
$params = @{ BaseUrl = $base } $params = @{ BaseUrl = $base }
if ($SkipInitialRun) { $params['SkipInitialRun'] = $true } if ($SkipInitialRun) { $params['SkipInitialRun'] = $true }

21
GeoNetAgent/agent/install/bootstrap.ps1

@ -9,16 +9,29 @@ param(
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$base = $BaseUrl.TrimEnd('/') $base = $BaseUrl.TrimEnd('/')
$installer = Join-Path $env:TEMP 'Install-GeoNetAgent.ps1'
Write-Host "GeoNetAgent bootstrap - $base" -ForegroundColor Cyan function Save-GeoNetInstallFile {
param([string]$Url, [string]$Destination)
$parent = Split-Path -Parent $Destination
if (-not (Test-Path $parent)) {
New-Item -ItemType Directory -Path $parent -Force | Out-Null
}
try { try {
Invoke-WebRequest -Uri "$base/Install-GeoNetAgent.ps1" -OutFile $installer -UseBasicParsing -TimeoutSec 90 Invoke-WebRequest -Uri $Url -OutFile $Destination -UseBasicParsing -TimeoutSec 90
} }
catch { catch {
& curl.exe -fsSL --tlsv1.2 "$base/Install-GeoNetAgent.ps1" -o $installer & curl.exe -fsSL --tlsv1.2 $Url -o $Destination
if ($LASTEXITCODE -ne 0) { throw $_ } if ($LASTEXITCODE -ne 0) { throw $_ }
} }
}
$stage = Join-Path $env:TEMP 'GeoNetAgent-install'
$installer = Join-Path $stage 'Install-GeoNetAgent.ps1'
$coreLib = Join-Path $stage 'lib\Agent-InstallCore.ps1'
Write-Host "GeoNetAgent bootstrap - $base" -ForegroundColor Cyan
Save-GeoNetInstallFile -Url "$base/Install-GeoNetAgent.ps1" -Destination $installer
Save-GeoNetInstallFile -Url "$base/lib/Agent-InstallCore.ps1" -Destination $coreLib
$params = @{ BaseUrl = $base } $params = @{ BaseUrl = $base }
if ($AgentToken) { $params['AgentToken'] = $AgentToken } if ($AgentToken) { $params['AgentToken'] = $AgentToken }

5
GeoNetAgent/agent/install/index.html

@ -36,6 +36,9 @@
<p><span class="label">Baris 2a</span> Install + kirim laporan pertama:</p> <p><span class="label">Baris 2a</span> Install + kirim laporan pertama:</p>
<pre>&amp; "$env:TEMP\GeoNetAgent-Install.ps1"</pre> <pre>&amp; "$env:TEMP\GeoNetAgent-Install.ps1"</pre>
<p><span class="label">Baris 2a</span> Install + kirim laporan pertama (Optional jika error):</p>
<pre>powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$env:TEMP\GeoNetAgent-Install.ps1"</pre>
<p><span class="label">Baris 2b</span> Atau hanya pasang/update file + task (tanpa laporan sekarang):</p> <p><span class="label">Baris 2b</span> Atau hanya pasang/update file + task (tanpa laporan sekarang):</p>
<pre>&amp; "$env:TEMP\GeoNetAgent-Install.ps1" -SkipInitialRun</pre> <pre>&amp; "$env:TEMP\GeoNetAgent-Install.ps1" -SkipInitialRun</pre>
@ -56,7 +59,7 @@ schtasks /Query /TN GeoNetAgent-Report-Startup
<ul> <ul>
<li>File agent di <code>%ProgramData%\GeoNetAgent</code></li> <li>File agent di <code>%ProgramData%\GeoNetAgent</code></li>
<li><code>GeoNetAgent-Report-Startup</code> — jalan saat boot (SYSTEM)</li> <li><code>GeoNetAgent-Report-Startup</code> — jalan saat boot (SYSTEM)</li>
<li><code>GeoNetAgent-Report</code> — tiap 6 jam (SYSTEM)</li> <li><code>GeoNetAgent-Report</code> — tiap 3 jam (SYSTEM); cek update otomatis sebelum kirim laporan</li>
</ul> </ul>
<div class="warn"> <div class="warn">

9
GeoNetAgent/agent/install/manifest.json

@ -1,13 +1,15 @@
{ {
"version": "0.1.6-phase0", "version": "0.1.8-phase0",
"files": [ "files": [
"UserAgent.ps1",
"Send-GeoNetReport.ps1", "Send-GeoNetReport.ps1",
"Collect-GeoNetReport.ps1", "Collect-GeoNetReport.ps1",
"lib/Agent-InstallCore.ps1",
"lib/Update-GeoNetAgent.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-DiskInfo.ps1", "lib/Get-DiskInfo.ps1",
"lib/Get-GpuInfo.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",
@ -15,6 +17,7 @@
"lib/Get-PeripheralsInfo.ps1", "lib/Get-PeripheralsInfo.ps1",
"lib/Get-SoftwareInfo.ps1", "lib/Get-SoftwareInfo.ps1",
"lib/Get-SystemInfo.ps1", "lib/Get-SystemInfo.ps1",
"lib/Send-CollectorReport.ps1" "lib/Send-CollectorReport.ps1",
"UserAgent.ps1"
] ]
} }

209
GeoNetAgent/agent/lib/Agent-InstallCore.ps1

@ -0,0 +1,209 @@
function Get-GeoNetInstallBaseUrl {
param([string]$Url)
return $Url.TrimEnd('/')
}
function Get-GeoNetRemoteJson {
param([string]$Url)
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
return (Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 60).Content | ConvertFrom-Json
}
function Get-GeoNetRemoteFile {
param([string]$Url, [string]$Destination)
$parent = Split-Path -Parent $Destination
if (-not (Test-Path $parent)) {
New-Item -ItemType Directory -Path $parent -Force | Out-Null
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$lastErr = $null
for ($try = 1; $try -le 3; $try++) {
try {
Invoke-WebRequest -Uri $Url -OutFile $Destination -UseBasicParsing -TimeoutSec 90
if ((Get-Item $Destination).Length -gt 0) { return }
}
catch {
$lastErr = $_
Start-Sleep -Seconds 2
}
}
if (Get-Command curl.exe -ErrorAction SilentlyContinue) {
& curl.exe -fsSL --tlsv1.2 $Url -o $Destination
if ($LASTEXITCODE -eq 0 -and (Test-Path $Destination)) { return }
}
throw $lastErr
}
function Copy-GeoNetAgentPayload {
param(
[string]$SourceRoot,
[string]$DestinationRoot,
[object]$Manifest
)
foreach ($rel in @($Manifest.files)) {
$src = Join-Path $SourceRoot ($rel -replace '/', [IO.Path]::DirectorySeparatorChar)
$dst = Join-Path $DestinationRoot ($rel -replace '/', [IO.Path]::DirectorySeparatorChar)
if (-not (Test-Path $src)) {
throw "File sumber tidak ada: $src"
}
$dir = Split-Path -Parent $dst
if (-not (Test-Path $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
}
Copy-Item -Path $src -Destination $dst -Force
}
}
function Install-GeoNetAgentPayload {
param(
[string]$Base,
[string]$DestinationRoot,
[object]$Manifest,
[switch]$Quiet
)
foreach ($rel in @(Get-GeoNetManifestFileOrder -Files $Manifest.files)) {
$url = "$Base/$($rel -replace '\\', '/')"
$dst = Join-Path $DestinationRoot ($rel -replace '/', [IO.Path]::DirectorySeparatorChar)
if (-not $Quiet) { Write-Host " -> $rel" }
Get-GeoNetRemoteFile -Url $url -Destination $dst
}
}
function Get-GeoNetManifestFileOrder {
param([object[]]$Files)
@($Files | Sort-Object {
if ($_ -eq 'UserAgent.ps1') { 'zzz' } else { $_ }
})
}
function Get-GeoNetLocalAgentVersion {
param([string]$InstallRoot)
$versionPath = Join-Path $InstallRoot 'VERSION'
if (-not (Test-Path $versionPath)) { return $null }
$text = (Get-Content $versionPath -Raw -ErrorAction SilentlyContinue)
if ([string]::IsNullOrWhiteSpace($text)) { return $null }
return $text.Trim()
}
function Set-GeoNetLocalAgentVersion {
param(
[string]$InstallRoot,
[string]$Version
)
$Version.Trim() | Set-Content -Path (Join-Path $InstallRoot 'VERSION') -Encoding ASCII -NoNewline
}
function Invoke-GeoNetSchtasks {
param(
[Parameter(Mandatory)][string[]]$ArgumentList,
[switch]$IgnoreError
)
$prev = $ErrorActionPreference
$ErrorActionPreference = 'SilentlyContinue'
$output = & schtasks.exe @ArgumentList 2>&1
$code = $LASTEXITCODE
$ErrorActionPreference = $prev
if ($code -ne 0 -and -not $IgnoreError) {
$msg = ($output | Out-String).Trim()
if ($msg) { throw $msg }
throw "schtasks gagal (exit $code): schtasks.exe $($ArgumentList -join ' ')"
}
return $code
}
function Register-GeoNetAgentTask {
param(
[string]$InstallRoot,
[int]$Hours = 3
)
$userScript = Join-Path $InstallRoot 'UserAgent.ps1'
if (-not (Test-Path $userScript)) {
throw "UserAgent.ps1 tidak ditemukan di $InstallRoot"
}
$taskArgs = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$userScript`""
$taskCmd = "powershell.exe $taskArgs"
$taskNames = @('GeoNetAgent-Report', 'GeoNetAgent-Report-Startup')
foreach ($name in $taskNames) {
Unregister-ScheduledTask -TaskName $name -Confirm:$false -ErrorAction SilentlyContinue
Invoke-GeoNetSchtasks -ArgumentList @('/Delete', '/TN', $name, '/F') -IgnoreError | Out-Null
}
Invoke-GeoNetSchtasks -ArgumentList @(
'/Create', '/TN', 'GeoNetAgent-Report-Startup', '/TR', $taskCmd,
'/SC', 'ONSTART', '/RU', 'SYSTEM', '/RL', 'HIGHEST', '/F'
) | Out-Null
Invoke-GeoNetSchtasks -ArgumentList @(
'/Create', '/TN', 'GeoNetAgent-Report', '/TR', $taskCmd,
'/SC', 'HOURLY', '/MO', "$Hours", '/RU', 'SYSTEM', '/RL', 'HIGHEST', '/F'
) | Out-Null
}
function Set-GeoNetAgentConfig {
param(
[string]$InstallRoot,
[string]$Base,
[string]$Token,
[bool]$UseRemoteConfig
)
$configPath = Join-Path $InstallRoot 'config.json'
if ($Token) {
$cfg = [ordered]@{
collector_url = 'https://agent.gisportal.id/api/v1/agent/report'
agent_token = $Token
include_software = $true
max_software = 500
timeout_seconds = 120
}
if (Test-Path $configPath) {
try {
$existing = Get-Content $configPath -Raw | ConvertFrom-Json
if ($existing.collector_url) { $cfg.collector_url = $existing.collector_url }
if ($null -ne $existing.include_software) { $cfg.include_software = [bool]$existing.include_software }
if ($existing.max_software) { $cfg.max_software = [int]$existing.max_software }
if ($existing.timeout_seconds) { $cfg.timeout_seconds = [int]$existing.timeout_seconds }
}
catch { }
}
$cfg | ConvertTo-Json | Set-Content -Path $configPath -Encoding UTF8
return
}
if ((Test-Path $configPath) -and -not $UseRemoteConfig) {
return
}
if ($UseRemoteConfig) {
Get-GeoNetRemoteFile -Url "$Base/config.json" -Destination $configPath
return
}
throw 'config.json tidak ada. Sediakan -AgentToken atau config.json di URL install.'
}
function Test-GeoNetAdmin {
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
$p = New-Object Security.Principal.WindowsPrincipal($id)
return $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Get-GeoNetInstallBaseUrlFromConfig {
param([string]$ConfigPath)
$default = 'https://agent.gisportal.id/install'
if (-not $ConfigPath -or -not (Test-Path $ConfigPath)) { return $default }
try {
$cfg = Get-Content $ConfigPath -Raw | ConvertFrom-Json
if ($cfg.install_url) { return Get-GeoNetInstallBaseUrl -Url $cfg.install_url }
if ($cfg.collector_url -match '^(https?://[^/]+)') {
return Get-GeoNetInstallBaseUrl -Url "$($Matches[1])/install"
}
}
catch { }
return $default
}

27
GeoNetAgent/agent/lib/Get-DiskInfo.ps1

@ -1,3 +1,29 @@
$formatText = Join-Path $PSScriptRoot 'Format-Text.ps1'
if (Test-Path $formatText) { . $formatText }
function Get-GeoNetPhysicalDiskModel {
param([string]$DriveLetter)
if (-not $DriveLetter) { return $null }
$model = Invoke-GeoNetSafe {
$logical = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='$DriveLetter'" -ErrorAction Stop
if (-not $logical) { return $null }
$partitions = @(Get-CimAssociatedInstance -InputObject $logical -Association Win32_LogicalDiskToPartition)
foreach ($partition in $partitions) {
$drives = @(Get-CimAssociatedInstance -InputObject $partition -Association Win32_DiskDriveToDiskPartition)
foreach ($drive in $drives) {
$candidate = Convert-GeoNetOptionalString -Value $drive.Model
if ($candidate) { return $candidate }
}
}
return $null
}
return $model
}
function Get-GeoNetDiskInfo { function Get-GeoNetDiskInfo {
Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" |
ForEach-Object { ForEach-Object {
@ -13,6 +39,7 @@ function Get-GeoNetDiskInfo {
free_gb = $freeGb free_gb = $freeGb
pct_used = $usedPct pct_used = $usedPct
file_system = $_.FileSystem file_system = $_.FileSystem
model = Get-GeoNetPhysicalDiskModel -DriveLetter $_.DeviceID
} }
} }
} }

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

@ -0,0 +1,39 @@
$formatText = Join-Path $PSScriptRoot 'Format-Text.ps1'
if (Test-Path $formatText) { . $formatText }
function Convert-GeoNetAdapterRamMb {
param([AllowNull()]$AdapterRam)
if ($null -eq $AdapterRam -or $AdapterRam -le 0) { return $null }
try {
$mb = [math]::Round([decimal]$AdapterRam / 1MB, 0)
if ($mb -le 0 -or $mb -gt 131072) { return $null }
return [int]$mb
} catch {
return $null
}
}
function Get-GeoNetGpuInfo {
$controllers = Invoke-GeoNetSafe {
Get-CimInstance Win32_VideoController -ErrorAction Stop |
Where-Object {
$_.Name -and $_.Name -notmatch 'Microsoft Basic|Remote|Sage Display|Display Manager'
}
} -Default @()
$gpus = @($controllers | ForEach-Object {
$name = Convert-GeoNetOptionalString -Value $_.Name
if (-not $name) { return }
[pscustomobject]@{
name = $name
dedicated_memory_mb = Convert-GeoNetAdapterRamMb -AdapterRam $_.AdapterRAM
}
})
@($gpus | Sort-Object {
if ($null -ne $_.dedicated_memory_mb) { $_.dedicated_memory_mb } else { -1 }
} -Descending)
}

13
GeoNetAgent/agent/lib/Get-SystemInfo.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 }
$gpuInfo = Join-Path $PSScriptRoot 'Get-GpuInfo.ps1'
if (Test-Path $gpuInfo) { . $gpuInfo }
function Test-GeoNetValidUuid { function Test-GeoNetValidUuid {
param($Uuid) param($Uuid)
@ -53,11 +56,8 @@ function Get-GeoNetSystemInfo {
Convert-GeoNetOptionalString -Value $guid Convert-GeoNetOptionalString -Value $guid
} }
$gpu = Invoke-GeoNetSafe { $gpus = @(Invoke-GeoNetSafe { @(Get-GeoNetGpuInfo) } -Default @())
Get-CimInstance Win32_VideoController -ErrorAction Stop | $gpu = $gpus | Select-Object -First 1
Where-Object { $_.Name -and $_.Name -notmatch 'Microsoft Basic|Remote' } |
Select-Object -First 1
}
$ramGb = $null $ramGb = $null
if ($cs -and $cs.TotalPhysicalMemory) { if ($cs -and $cs.TotalPhysicalMemory) {
@ -97,7 +97,8 @@ function Get-GeoNetSystemInfo {
OsBuild = $osBuild OsBuild = $osBuild
CpuModel = Convert-GeoNetOptionalString -Value $cpu.Name CpuModel = Convert-GeoNetOptionalString -Value $cpu.Name
RamGb = $ramGb RamGb = $ramGb
GpuModel = Convert-GeoNetOptionalString -Value $gpu.Name GpuModel = Convert-GeoNetOptionalString -Value $gpu.name
Gpus = $gpus
LastBootAt = $lastBootAt LastBootAt = $lastBootAt
} }
} }

74
GeoNetAgent/agent/lib/Update-GeoNetAgent.ps1

@ -0,0 +1,74 @@
$installCore = Join-Path $PSScriptRoot 'Agent-InstallCore.ps1'
if (Test-Path $installCore) { . $installCore }
function Update-GeoNetAgentIfNeeded {
<#
.SYNOPSIS
Cek manifest.json di server vs VERSION lokal; unduh paket jika beda.
Gagal update tidak menghentikan pemanggil (mis. kirim laporan tetap jalan).
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$InstallRoot,
[string]$BaseUrl,
[string]$ConfigPath,
[int]$ScheduleHours = 3,
[switch]$Quiet
)
$result = [ordered]@{
Updated = $false
From = $null
To = $null
Error = $null
}
try {
if (-not $BaseUrl) {
$BaseUrl = Get-GeoNetInstallBaseUrlFromConfig -ConfigPath $ConfigPath
}
$base = Get-GeoNetInstallBaseUrl -Url $BaseUrl
$manifest = Get-GeoNetRemoteJson -Url "$base/manifest.json"
$remoteVersion = [string]$manifest.version
if ([string]::IsNullOrWhiteSpace($remoteVersion)) {
throw 'manifest.json server tidak memuat version.'
}
$remoteVersion = $remoteVersion.Trim()
$localVersion = Get-GeoNetLocalAgentVersion -InstallRoot $InstallRoot
$result.From = $localVersion
if ($localVersion -eq $remoteVersion) {
return [pscustomobject]$result
}
if (-not $Quiet) {
Write-Host "GeoNetAgent auto-update: $localVersion -> $remoteVersion" -ForegroundColor Cyan
}
Install-GeoNetAgentPayload -Base $base -DestinationRoot $InstallRoot -Manifest $manifest -Quiet:$Quiet
Set-GeoNetLocalAgentVersion -InstallRoot $InstallRoot -Version $remoteVersion
try {
Register-GeoNetAgentTask -InstallRoot $InstallRoot -Hours $ScheduleHours
}
catch {
if (-not $Quiet) {
Write-Warning "Auto-update: file terbaru, tetapi refresh Scheduled Task gagal: $_"
}
}
$result.Updated = $true
$result.To = $remoteVersion
}
catch {
$result.Error = $_.Exception.Message
if (-not $Quiet) {
Write-Warning "GeoNetAgent auto-update dilewati: $($result.Error)"
}
}
return [pscustomobject]$result
}

1
GeoNetAgent/collector/app/db/models.py

@ -59,6 +59,7 @@ class AssetDisk(Base):
free_gb: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False) free_gb: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False)
pct_used: Mapped[float] = mapped_column(Numeric(5, 2), nullable=False) pct_used: Mapped[float] = mapped_column(Numeric(5, 2), nullable=False)
file_system: Mapped[str | None] = mapped_column(String(20)) file_system: Mapped[str | None] = mapped_column(String(20))
model: Mapped[str | None] = mapped_column(String(100))
class AssetNetworkAdapter(Base): class AssetNetworkAdapter(Base):

7
GeoNetAgent/collector/app/schemas/agent_report.py

@ -4,6 +4,11 @@ from typing import Any, Literal
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
class GpuInfo(BaseModel):
name: str
dedicated_memory_mb: int | None = None
class SystemInfo(BaseModel): class SystemInfo(BaseModel):
manufacturer: str | None = None manufacturer: str | None = None
model: str | None = None model: str | None = None
@ -23,6 +28,7 @@ class DiskInfo(BaseModel):
free_gb: float free_gb: float
pct_used: float pct_used: float
file_system: str | None = None file_system: str | None = None
model: str | None = None
class MemoryModule(BaseModel): class MemoryModule(BaseModel):
@ -116,6 +122,7 @@ class AgentReportRequest(BaseModel):
asset_type: str = "endpoint" asset_type: str = "endpoint"
logged_in_user: str | None = None logged_in_user: str | None = None
system: SystemInfo system: SystemInfo
gpus: list[GpuInfo] = Field(default_factory=list)
disks: list[DiskInfo] = Field(default_factory=list) disks: list[DiskInfo] = Field(default_factory=list)
memory_modules: list[MemoryModule] = Field(default_factory=list) memory_modules: list[MemoryModule] = Field(default_factory=list)
memory_summary: MemorySummary | None = None memory_summary: MemorySummary | None = None

7
GeoNetAgent/collector/app/schemas/assets.py

@ -34,6 +34,12 @@ class DiskOut(BaseModel):
free_gb: float free_gb: float
pct_used: float pct_used: float
file_system: str | None = None file_system: str | None = None
model: str | None = None
class GpuOut(BaseModel):
name: str
dedicated_memory_mb: int | None = None
class NetworkAdapterOut(BaseModel): class NetworkAdapterOut(BaseModel):
@ -78,6 +84,7 @@ class SnapshotSummary(BaseModel):
software_count: int | None = None software_count: int | None = None
collected_at: datetime collected_at: datetime
created_at: datetime created_at: datetime
gpus: list[GpuOut] = Field(default_factory=list)
disks: list[DiskOut] = Field(default_factory=list) disks: list[DiskOut] = Field(default_factory=list)
network: list[NetworkAdapterOut] = Field(default_factory=list) network: list[NetworkAdapterOut] = Field(default_factory=list)
peripherals: list[PeripheralOut] = Field(default_factory=list) peripherals: list[PeripheralOut] = Field(default_factory=list)

32
GeoNetAgent/collector/app/services/asset_read_service.py

@ -10,6 +10,7 @@ from app.schemas.assets import (
AssetDetail, AssetDetail,
AssetListItem, AssetListItem,
DiskOut, DiskOut,
GpuOut,
NetworkAdapterOut, NetworkAdapterOut,
PaginatedAssets, PaginatedAssets,
PaginatedSnapshots, PaginatedSnapshots,
@ -183,6 +184,7 @@ def _load_snapshot_children(db: Session, snapshot: AssetSnapshot) -> tuple[list[
free_gb=float(d.free_gb), free_gb=float(d.free_gb),
pct_used=float(d.pct_used), pct_used=float(d.pct_used),
file_system=d.file_system, file_system=d.file_system,
model=d.model,
) )
for d in disks for d in disks
], ],
@ -238,9 +240,38 @@ def _peripherals_from_payload(payload: dict | None) -> list[PeripheralOut]:
return items return items
def _gpus_from_payload(payload: dict | None) -> list[GpuOut]:
if not payload:
return []
raw = payload.get("gpus")
if not isinstance(raw, list):
gpu_model = payload.get("system", {}).get("gpu_model") if isinstance(payload.get("system"), dict) else None
if gpu_model:
return [GpuOut(name=str(gpu_model))]
return []
items: list[GpuOut] = []
for entry in raw:
if not isinstance(entry, dict):
continue
name = entry.get("name")
if not name:
continue
dedicated = entry.get("dedicated_memory_mb")
items.append(
GpuOut(
name=str(name),
dedicated_memory_mb=int(dedicated) if dedicated is not None else None,
)
)
return items
def _snapshot_to_summary(db: Session, snapshot: AssetSnapshot) -> SnapshotSummary: def _snapshot_to_summary(db: Session, snapshot: AssetSnapshot) -> SnapshotSummary:
disks, network = _load_snapshot_children(db, snapshot) disks, network = _load_snapshot_children(db, snapshot)
peripherals = _peripherals_from_payload(snapshot.payload) peripherals = _peripherals_from_payload(snapshot.payload)
gpus = _gpus_from_payload(snapshot.payload)
return SnapshotSummary( return SnapshotSummary(
id=str(snapshot.id), id=str(snapshot.id),
agent_version=snapshot.agent_version, agent_version=snapshot.agent_version,
@ -255,6 +286,7 @@ def _snapshot_to_summary(db: Session, snapshot: AssetSnapshot) -> SnapshotSummar
software_count=snapshot.software_count, software_count=snapshot.software_count,
collected_at=snapshot.collected_at, collected_at=snapshot.collected_at,
created_at=snapshot.created_at, created_at=snapshot.created_at,
gpus=gpus,
disks=disks, disks=disks,
network=network, network=network,
peripherals=peripherals, peripherals=peripherals,

3
GeoNetAgent/collector/app/services/asset_service.py

@ -164,7 +164,7 @@ def ingest_agent_report(db: Session, report: AgentReportRequest) -> AgentReportR
os_version=report.system.os_version or report.system.os_build, os_version=report.system.os_version or report.system.os_build,
cpu_model=report.system.cpu_model, cpu_model=report.system.cpu_model,
ram_gb=report.system.ram_gb, ram_gb=report.system.ram_gb,
gpu_model=report.system.gpu_model, gpu_model=report.system.gpu_model or (report.gpus[0].name if report.gpus else None),
bios_version=report.system.bios_version, bios_version=report.system.bios_version,
last_boot_at=report.system.last_boot_at, last_boot_at=report.system.last_boot_at,
bsod_count_30d=report.health.bsod_count_30d, bsod_count_30d=report.health.bsod_count_30d,
@ -184,6 +184,7 @@ def ingest_agent_report(db: Session, report: AgentReportRequest) -> AgentReportR
free_gb=disk.free_gb, free_gb=disk.free_gb,
pct_used=disk.pct_used, pct_used=disk.pct_used,
file_system=disk.file_system, file_system=disk.file_system,
model=_sanitize_text(disk.model, 100),
) )
) )

17
GeoNetAgent/collector/static/app.js

@ -220,15 +220,28 @@ function renderDetail(asset, software) {
<dt>OS</dt><dd>${escapeHtml(snap.os_name || "")} ${escapeHtml(snap.os_version || "")}</dd> <dt>OS</dt><dd>${escapeHtml(snap.os_name || "")} ${escapeHtml(snap.os_version || "")}</dd>
<dt>CPU</dt><dd>${escapeHtml(snap.cpu_model || "")}</dd> <dt>CPU</dt><dd>${escapeHtml(snap.cpu_model || "")}</dd>
<dt>RAM</dt><dd>${escapeHtml(snap.ram_gb != null ? `${snap.ram_gb} GB` : "")}</dd> <dt>RAM</dt><dd>${escapeHtml(snap.ram_gb != null ? `${snap.ram_gb} GB` : "")}</dd>
<dt>GPU</dt><dd>${escapeHtml(snap.gpu_model || "")}</dd>
<dt>Agent</dt><dd>${escapeHtml(snap.agent_version)}</dd> <dt>Agent</dt><dd>${escapeHtml(snap.agent_version)}</dd>
</dl>`; </dl>`;
if (snap.gpus?.length) {
html += `<div class="section-title">GPU</div><table><thead><tr><th>Card</th><th>Dedicated memory</th></tr></thead><tbody>`;
html += snap.gpus
.map((g) => {
const mem =
g.dedicated_memory_mb != null ? `${g.dedicated_memory_mb} MB` : "—";
return `<tr><td>${escapeHtml(g.name)}</td><td>${escapeHtml(mem)}</td></tr>`;
})
.join("");
html += `</tbody></table>`;
}
if (snap.disks?.length) { if (snap.disks?.length) {
html += `<div class="section-title">Disk</div><table><thead><tr><th>Drive</th><th>Used</th><th>Free</th></tr></thead><tbody>`; html += `<div class="section-title">Disk</div><table><thead><tr><th>Drive</th><th>Model</th><th>Used</th><th>Free</th></tr></thead><tbody>`;
html += snap.disks html += snap.disks
.map((d) => { .map((d) => {
const warn = d.pct_used > 85 ? "style='color:var(--warn)'" : ""; const warn = d.pct_used > 85 ? "style='color:var(--warn)'" : "";
return `<tr><td>${escapeHtml(d.drive_letter)}</td><td ${warn}>${escapeHtml(d.pct_used)}%</td><td>${escapeHtml(d.free_gb)} GB</td></tr>`; return `<tr><td>${escapeHtml(d.drive_letter)}</td><td class="muted">${escapeHtml(d.model || "—")}</td><td ${warn}>${escapeHtml(d.pct_used)}%</td><td>${escapeHtml(d.free_gb)} GB</td></tr>`;
}) })
.join(""); .join("");
html += `</tbody></table>`; html += `</tbody></table>`;

2
GeoNetAgent/infra/collector/publish_agent_install.sh

@ -27,6 +27,8 @@ done
echo "==> Upload installer + manifest" echo "==> Upload installer + manifest"
scp -q "$AGENT/Install-GeoNetAgent.ps1" "$REMOTE:$INSTALL_ROOT/" scp -q "$AGENT/Install-GeoNetAgent.ps1" "$REMOTE:$INSTALL_ROOT/"
ssh "$REMOTE" "mkdir -p $INSTALL_ROOT/lib"
scp -q "$AGENT/lib/Agent-InstallCore.ps1" "$REMOTE:$INSTALL_ROOT/lib/"
scp -q "$AGENT/install/bootstrap.ps1" "$REMOTE:$INSTALL_ROOT/" scp -q "$AGENT/install/bootstrap.ps1" "$REMOTE:$INSTALL_ROOT/"
scp -q "$AGENT/install/manifest.json" "$REMOTE:$INSTALL_ROOT/" scp -q "$AGENT/install/manifest.json" "$REMOTE:$INSTALL_ROOT/"
scp -q "$AGENT/install/GeoNetAgent-Install.ps1" "$REMOTE:$INSTALL_ROOT/" scp -q "$AGENT/install/GeoNetAgent-Install.ps1" "$REMOTE:$INSTALL_ROOT/"

3
GeoNetAgent/infra/db/migrations/002_disk_model.sql

@ -0,0 +1,3 @@
-- Migration 002: physical disk model on asset_disks
ALTER TABLE asset_disks
ADD COLUMN IF NOT EXISTS model varchar(100);

320
GeoNetAgent/samples/agent-reports/DESKTOP-RRJ9G01_20260618_112523.json

@ -0,0 +1,320 @@
{
"schema_version": "1.0",
"agent_version": "0.1.7-phase0",
"collected_at": "2026-06-18T11:25:23+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-18T10:41:02+07:00"
},
"gpus": [
{
"name": "NVIDIA GeForce GT 730",
"dedicated_memory_mb": 2048
},
{
"name": "Intel(R) HD Graphics 4600",
"dedicated_memory_mb": 1024
}
],
"disks": [
{
"drive_letter": "C:",
"total_gb": 188.7,
"free_gb": 4.6,
"pct_used": 97.6,
"file_system": "NTFS",
"model": "ADATA SU650"
},
{
"drive_letter": "D:",
"total_gb": 256.5,
"free_gb": 68.5,
"pct_used": 73.3,
"file_system": "NTFS",
"model": "ADATA SU650"
}
],
"memory_modules": [
{
"slot": "ChannelA-DIMM0",
"bank_label": "BANK 0",
"capacity_gb": 8,
"speed_mhz": 1600,
"manufacturer": "0000",
"part_number": "",
"form_factor": "tsop",
"memory_type": 24
},
{
"slot": "ChannelB-DIMM0",
"bank_label": "BANK 2",
"capacity_gb": 8,
"speed_mhz": 1600,
"manufacturer": "0000",
"part_number": "",
"form_factor": "tsop",
"memory_type": 24
}
],
"memory_summary": {
"total_gb": 16,
"module_count": 2,
"slot_count": 2,
"empty_slots": 0,
"layout": "2x8GB",
"configuration": "2x8GB tsop",
"upgradeable": false
},
"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.27.176.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": "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": 6,
"pending_reboot": false,
"last_patch_installed": "2026-02-27"
},
"software": [
],
"metadata": {
"collect_mode": "phase0-local",
"software_included": false,
"software_count": 0
}
}
Loading…
Cancel
Save