diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2b23ceb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.git +**/.git +fe/node_modules +fe/.next +fe/out +be/bin +be/internal/web/dist +be/ai +backups +exports +license +dist +.env diff --git a/.env.example b/.env.example index ab76a68..11c9450 100644 --- a/.env.example +++ b/.env.example @@ -25,3 +25,7 @@ NETRA_INSPECTION_MODELS=qwen3-vl:2b-instruct,moondream:latest,ahmadwaqar/smolvlm # Integration API for third-party apps (/api/ext/docs): access token lifetime, 5 to 1440 minutes NETRA_EXT_TOKEN_MINUTES=60 + +# The license is the file license/netra.lic (license/README.md), or installed in Settings › License. +# It decides the modules (anomaly, crowd, substation, exports, Telegram, audit, integration, ...), +# the robot limit and the branding; without one only the core runs, with one robot. diff --git a/.env.local.example b/.env.local.example index a46c419..f411fb6 100644 --- a/.env.local.example +++ b/.env.local.example @@ -28,6 +28,8 @@ NETRA_SESSION_HOURS=72 NETRA_TELEGRAM_TOKEN= NETRA_TELEGRAM_CHAT_ID= +# License: the file license/netra.lic, or Settings › License in the dashboard (license/README.md). + # Final detection model served by the native Ollama (qwen2.5vl:7b needs 16 GB RAM, else qwen2.5vl:3b) NETRA_AI_MODEL=__AI_MODEL__ NETRA_AI_LANGUAGE=English diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..34b230e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,212 @@ +name: release + +# A tag vX.Y.Z builds what clients install: the netra binaries with the dashboard inside, the +# Ubuntu/Debian packages, the Windows installer, the macOS package and the Docker image, and +# publishes them as a GitHub release. Needs the secret NETRA_REPO_TOKEN (read access to +# netra-fleet-be and netra-fleet-fe) for the submodules. +on: + push: + tags: ["v*"] + workflow_dispatch: + inputs: + version: + description: "Version, e.g. v1.2.0-rc1 (artifacts only; no release is published)" + required: true + +permissions: + contents: write + packages: write + +env: + VERSION: ${{ github.event.inputs.version || github.ref_name }} + POSTGRES_WINDOWS: https://get.enterprisedb.com/postgresql/postgresql-16.4-1-windows-x64-binaries.zip + POSTGRES_MACOS: https://get.enterprisedb.com/postgresql/postgresql-16.4-1-osx-binaries.zip + +jobs: + web: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.NETRA_REPO_TOKEN }} + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: fe/package-lock.json + - run: npm ci --no-audit --no-fund && npm run build + working-directory: fe + - uses: actions/upload-artifact@v4 + with: + name: web + path: fe/out + + binaries: + needs: web + runs-on: ubuntu-latest + strategy: + matrix: + target: [linux/amd64, linux/arm64, windows/amd64, darwin/amd64, darwin/arm64] + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.NETRA_REPO_TOKEN }} + - uses: actions/setup-go@v5 + with: + go-version: "1.22" + cache-dependency-path: be/go.sum + - uses: actions/download-artifact@v4 + with: + name: web + path: be/internal/web/dist + - name: build + working-directory: be + run: | + GOOS=${TARGET%/*}; GOARCH=${TARGET#*/} + EXT=""; [ "$GOOS" = windows ] && EXT=.exe + PKG=github.com/DarmawanWeb/netra-fleet-be/internal/version + CGO_ENABLED=0 GOOS=$GOOS GOARCH=$GOARCH go build -tags webui -trimpath \ + -ldflags "-s -w -X $PKG.Version=$VERSION -X $PKG.Commit=$(git rev-parse --short HEAD)" \ + -o "../out/netra-$GOOS-$GOARCH$EXT" ./cmd/netra + env: + TARGET: ${{ matrix.target }} + - uses: actions/upload-artifact@v4 + with: + name: netra-${{ strategy.job-index }} + path: out/* + + deb: + needs: binaries + runs-on: ubuntu-latest + strategy: + matrix: + arch: [amd64, arm64] + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + pattern: netra-* + merge-multiple: true + path: bin + - run: packaging/linux/build-deb.sh "bin/netra-linux-${{ matrix.arch }}" "$VERSION" "${{ matrix.arch }}" dist + - uses: actions/upload-artifact@v4 + with: + name: deb-${{ matrix.arch }} + path: dist/*.deb + + windows: + needs: binaries + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + pattern: netra-* + merge-multiple: true + path: bin + - name: stage + shell: pwsh + run: packaging/windows/stage.ps1 -Netra bin/netra-windows-amd64.exe -Out stage -PostgresZip $env:POSTGRES_WINDOWS + - name: installer + shell: pwsh + run: | + choco install innosetup -y --no-progress + $v = $env:VERSION.TrimStart('v') + & "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" "/DVersion=$v" "/DStage=$PWD\stage" "/Odist" packaging\windows\netra.iss + - uses: actions/upload-artifact@v4 + with: + name: windows + path: dist/*.exe + + macos: + needs: binaries + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + pattern: netra-* + merge-multiple: true + path: bin + - name: package + run: | + lipo -create -output netra bin/netra-darwin-amd64 bin/netra-darwin-arm64 + curl -fsSL -o pgsql.zip "$POSTGRES_MACOS" && unzip -q pgsql.zip + rm -rf pgsql/pgAdmin* pgsql/stackbuilder* pgsql/include pgsql/doc + packaging/macos/build-pkg.sh netra "$VERSION" pgsql dist + - uses: actions/upload-artifact@v4 + with: + name: macos + path: dist/*.pkg + + image: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.NETRA_REPO_TOKEN }} + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name == 'push' }} + build-args: | + VERSION=${{ env.VERSION }} + COMMIT=${{ github.sha }} + tags: | + ghcr.io/${{ github.repository_owner }}/netra:${{ env.VERSION }} + ghcr.io/${{ github.repository_owner }}/netra:main + cache-from: type=gha,scope=netra + cache-to: type=gha,mode=max,scope=netra + + release: + if: github.event_name == 'push' + needs: [binaries, deb, windows, macos, image] + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + pattern: netra-* + merge-multiple: true + path: assets + - uses: actions/download-artifact@v4 + with: + pattern: deb-* + merge-multiple: true + path: assets + - uses: actions/download-artifact@v4 + with: + name: windows + path: assets + - uses: actions/download-artifact@v4 + with: + name: macos + path: assets + - name: checksums + working-directory: assets + run: | + sha256sum * > ../SHA256SUMS && mv ../SHA256SUMS . + cat SHA256SUMS + - uses: softprops/action-gh-release@v2 + with: + name: NETRA ${{ env.VERSION }} + prerelease: ${{ contains(env.VERSION, '-') }} + generate_release_notes: true + files: | + assets/netra_*.deb + assets/NETRA-Setup-*.exe + assets/NETRA-*-macos.pkg + assets/netra-linux-* + assets/netra-windows-* + assets/netra-darwin-* + assets/SHA256SUMS diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 14ea189..cddc29a 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -3,6 +3,7 @@ on: push: branches: [main] pull_request: + branches: [main] jobs: compose: runs-on: ubuntu-latest @@ -12,7 +13,7 @@ jobs: - run: docker compose --env-file .env.example config --quiet - run: docker compose -f docker-compose.yml -f docker-compose.local.yml --env-file .env.local.example config --quiet - run: docker compose -f docker-compose.yml -f docker-compose.local.yml -f docker-compose.tunnel.yml --env-file .env.local.example config --quiet - - run: shellcheck scripts/*.sh + - run: shellcheck scripts/*.sh packaging/linux/*.sh packaging/linux/debian/* packaging/macos/*.sh packaging/macos/scripts/* - run: docker run --rm -v "$PWD/Caddyfile:/etc/caddy/Caddyfile:ro" -e NETRA_DOMAIN=example.com caddy:2-alpine caddy validate --config /etc/caddy/Caddyfile - run: docker run --rm -v "$PWD/Caddyfile.local:/etc/caddy/Caddyfile:ro" caddy:2-alpine caddy validate --config /etc/caddy/Caddyfile - run: docker run --rm -v "$PWD/Caddyfile.tunnel:/etc/caddy/Caddyfile:ro" caddy:2-alpine caddy validate --config /etc/caddy/Caddyfile diff --git a/.gitignore b/.gitignore index a1c6bf5..e79e79b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ backups/ exports/ .awake.pid +license/*.lic diff --git a/Caddyfile b/Caddyfile index a588a75..6ef36a0 100644 --- a/Caddyfile +++ b/Caddyfile @@ -13,7 +13,7 @@ reverse_proxy grafana:3000 } handle { - reverse_proxy web:3000 + reverse_proxy api:8080 } header { Strict-Transport-Security "max-age=31536000" diff --git a/Caddyfile.local b/Caddyfile.local index 8b56240..c93609e 100644 --- a/Caddyfile.local +++ b/Caddyfile.local @@ -14,6 +14,6 @@ reverse_proxy grafana:3000 } handle { - reverse_proxy web:3000 + reverse_proxy api:8080 } } diff --git a/Caddyfile.tunnel b/Caddyfile.tunnel index 9f75032..4067664 100644 --- a/Caddyfile.tunnel +++ b/Caddyfile.tunnel @@ -26,6 +26,6 @@ reverse_proxy grafana:3000 } handle { - reverse_proxy web:3000 + reverse_proxy api:8080 } } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..87f139c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +# NETRA as one image: the dashboard export (fe/) built into the service (be/). Needs the +# submodules: git submodule update --init. +FROM node:20-alpine AS web +WORKDIR /fe +COPY fe/package.json fe/package-lock.json ./ +RUN npm ci --no-audit --no-fund --ignore-scripts +COPY fe/ ./ +RUN npm run postinstall && npm run build + +FROM golang:1.22-alpine AS build +WORKDIR /be +COPY be/go.mod be/go.sum ./ +RUN go mod download +COPY be/ ./ +COPY --from=web /fe/out ./internal/web/dist +ARG VERSION=dev +ARG COMMIT="" +RUN LDFLAGS="-s -w -X github.com/DarmawanWeb/netra-fleet-be/internal/version.Version=${VERSION} \ + -X github.com/DarmawanWeb/netra-fleet-be/internal/version.Commit=${COMMIT}" && \ + CGO_ENABLED=0 go build -tags webui -trimpath -ldflags="$LDFLAGS" -o /out/netra ./cmd/netra && \ + CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/netra-license ./cmd/netra-license && \ + mkdir -p /out/data + +FROM gcr.io/distroless/static-debian12:nonroot +COPY --from=build /out/netra /netra +# netra-license inspect checks a license file on site (issuing needs the vendor's key). +COPY --from=build /out/netra-license /netra-license +# The data volume must be writable by the nonroot user (uid 65532). +COPY --from=build --chown=65532:65532 /out/data /data +ENV NETRA_DATA_DIR=/data NETRA_ADDR=:8080 +VOLUME ["/data"] +EXPOSE 8080 +ENTRYPOINT ["/netra"] +CMD ["serve"] diff --git a/INSTALL.id.md b/INSTALL.id.md new file mode 100644 index 0000000..7f1b4b8 --- /dev/null +++ b/INSTALL.id.md @@ -0,0 +1,138 @@ +# Memasang NETRA + +NETRA adalah satu aplikasi: layanan pusat komando, dasbornya, dan basis datanya. Pasang di +komputer yang selalu menyala dan berada di jaringan yang sama dengan robot, lalu selesaikan +penyiapan lewat browser. (English: [INSTALL.md](INSTALL.md).) + +## Yang Anda terima dari EZRA Robotics + +| Item | | +|---|---| +| Installer | `NETRA-Setup--x64.exe` (Windows), `netra__amd64.deb` (Ubuntu/Debian), `NETRA--macos.pkg` (macOS), atau image Docker `ghcr.io/darmawanweb/netra` | +| Lisensi | `.lic`, diterbitkan untuk ID instalasi server Anda (lihat [Lisensi](#lisensi)) | +| Panduan | panduan ini dan panduan operator (PDF) | + +## Kebutuhan + +- Windows 10/11 64-bit atau Windows Server 2019+, Ubuntu 22.04/24.04 atau Debian 12, atau macOS 13+. +- 4 inti CPU, memori 8 GB, ruang disk kosong 50 GB (foto dan peta bertambah seiring pemakaian). +- Alamat tetap di jaringan robot (atur reservasi DHCP di router). +- Opsional, untuk verifikasi AI: [Ollama](https://ollama.com) dengan model vision; GPU atau + memori 16 GB untuk model 7B. + +## Pemasangan + +**Windows.** Jalankan `NETRA-Setup--x64.exe` sebagai administrator. Installer memasang +NETRA dan basis datanya, menjalankan layanan **NETRA** (ikut menyala bersama Windows), membuka +port 8080 di firewall, dan membuka browser di akhir. Konfigurasi dan data: +`C:\ProgramData\NETRA` (`netra.env`, `data\`, `logs\`). + +**Ubuntu atau Debian.** + +```bash +sudo apt install ./netra__amd64.deb +``` + +Paket ini memasang PostgreSQL bila belum ada, membuat basis data `netra`, dan menjalankan +layanan `netra`. Konfigurasi: `/etc/netra/netra.env`. Data dan cadangan: `/var/lib/netra`. +Log: `journalctl -u netra -f`. + +**macOS.** Buka `NETRA--macos.pkg` (pertama kali: klik kanan > Open). Konfigurasi dan +data: `/Library/Application Support/NETRA`. + +**Docker.** Untuk server yang sudah memakai Docker: `docker-compose.yml` repositori Netra dengan +image `ghcr.io/darmawanweb/netra` ([README](README.md#deploy-on-a-server)). + +## Penyiapan awal + +Buka `http://:8080` di browser pada jaringan yang sama. Instalasi baru membuka +penyiapan: + +1. **Instalasi**: ID instalasi server ini, misalnya `NETRA-7KQ3-M9XD-2WPA`. Salin ID ini: + lisensi Anda diterbitkan untuknya. +2. **Administrator**: akun pertama, yang mengelola pengguna, robot dan lisensi. +3. **Lisensi**: tempel lisensi atau pilih berkas `.lic`. Tanpa lisensi Anda tetap bisa + melanjutkan: fitur inti berjalan dengan satu robot sampai lisensi dipasang. + +## Lisensi + +Lisensi memuat nama organisasi Anda dan menentukan modul, jenis peta, jumlah robot, serta masa +berlakunya. Lisensi hanya berlaku di server yang ID instalasinya tercantum, tanpa perlu internet. + +- Belum punya lisensi: kirim ID instalasi ke EZRA Robotics, lalu pasang berkas yang Anda terima. +- Memasang atau mengganti lisensi: **Pengaturan › Lisensi** (administrator), kapan saja, tanpa + restart. Bisa juga menaruh berkasnya di lokasi lisensi pada `netra.env` lalu restart layanan. +- Pengaturan › Lisensi menampilkan status, modul, jumlah robot terhadap batasnya, dan masa + berlaku. Mintalah perpanjangan sebelum habis; setelah habis, modul masih berjalan 14 hari. +- Server baru (atau basis data baru) punya ID instalasi baru dan memerlukan lisensi baru. + +## Menghubungkan robot + +Di **Pengaturan › Robot**, tambahkan tiap robot. Salin tokennya, yang hanya ditampilkan sekali, +ke konfigurasi robot bersama alamat server ini (`CC_URL`), sesuai panduan robot. + +## Konfigurasi + +`netra.env` menyimpan pengaturan; restart layanan setelah mengubahnya. + +- Windows: services.msc > NETRA > Restart. +- Linux: `sudo systemctl restart netra`. +- macOS: `sudo launchctl kickstart -k system/id.ezrarobotics.netra`. + +| Pengaturan | | +|---|---| +| `NETRA_ADDR` | port (`:8080`); `:80` agar dasbor terbuka tanpa nomor port | +| `NETRA_PUBLIC_URL` | alamat yang dipakai robot dan ponsel, misalnya `http://192.168.1.10:8080` | +| `NETRA_TLS=self-signed` | HTTPS dengan sertifikat buatan NETRA (browser meminta persetujuan sekali); atau `NETRA_TLS_CERT` dan `NETRA_TLS_KEY` untuk sertifikat sendiri | +| `NETRA_OLLAMA_URL` | verifikasi AI dengan Ollama, misalnya `http://localhost:11434` (perlu modul di lisensi) | +| `NETRA_TELEGRAM_TOKEN`, `NETRA_TELEGRAM_CHAT_ID` | notifikasi Telegram (perlu modul di lisensi) | +| `NETRA_BACKUP_KEEP_DAYS`, `NETRA_BACKUP_AT` | cadangan harian basis data: jumlah hari disimpan, jam | + +## Cadangan + +NETRA mencadangkan basis datanya setiap hari (pukul 02.30 secara bawaan) ke folder `backups` +di folder datanya dan menyimpannya 14 hari. Halaman **Sistem** (administrator) menampilkan daftar +cadangan, membuat cadangan saat itu juga, mengunduhnya, dan menampilkan ruang disk. Foto dan peta +berupa berkas di folder data: salin folder itu ke disk lain secara berkala. + +Memulihkan, dengan layanan dihentikan: + +```bash +netra restore -yes /backups/netra-YYYYMMDD-HHMMSS.dump +``` + +## Pembaruan + +Pasang versi baru di atas versi lama (installer atau paket yang sama). Data, konfigurasi dan +lisensi tetap ada; basis data diperbarui saat layanan berjalan kembali. + +## Lupa kata sandi administrator + +Di server, sebagai administrator: + +- Windows: `"C:\Program Files\NETRA\netra.exe" reset-password admin` +- Linux: `sudo -u netra netra reset-password admin` +- macOS: `sudo -u _netra "/Library/NETRA/netra" -config "/Library/Application Support/NETRA/netra.env" reset-password admin` + +Perintah ini mencetak kata sandi baru dan mengeluarkan akun tersebut dari semua sesi. + +## Perintah + +`netra version`, `netra installation-id`, `netra backup`, `netra restore -yes `, +`netra license ` (memeriksa lisensi terhadap server ini), +`netra reset-password [kata-sandi]`. + +## Mengatasi masalah + +| Gejala | Periksa | +|---|---| +| Halaman tidak terbuka | layanan berjalan (services.msc, `systemctl status netra`); firewall mengizinkan port; alamatnya alamat server | +| Robot tetap offline | robot bisa menjangkau server: `curl http://:8080/healthz` dari robot; tokennya sama dengan yang ditampilkan saat robot ditambahkan | +| "Tidak termasuk lisensi" | modul itu tidak ada di lisensi: Pengaturan › Lisensi, lalu hubungi EZRA Robotics | +| Lisensi ditolak: "for another installation" | lisensi diterbitkan untuk ID server lain; kirim ID server ini | +| Log | Windows: `C:\ProgramData\NETRA\logs`; Linux: `journalctl -u netra`; macOS: `/Library/Application Support/NETRA/logs` | + +## Mencopot + +Windows: Settings > Apps > NETRA. Linux: `sudo apt remove netra` (`purge` juga menghapus +konfigurasi). Folder data dan basis data tetap ada sampai Anda menghapusnya. diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..913a59a --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,139 @@ +# Installing NETRA + +NETRA is one application: the command center's service and its dashboard, with its database. +Install it on a computer that stays on, on the same network as the robots. Then finish the setup +in a browser. (Indonesian: [INSTALL.id.md](INSTALL.id.md).) + +## What you receive from EZRA Robotics + +| Item | | +|---|---| +| Installer | `NETRA-Setup--x64.exe` (Windows), `netra__amd64.deb` (Ubuntu/Debian), `NETRA--macos.pkg` (macOS), or the Docker image `ghcr.io/darmawanweb/netra` | +| License | `.lic`, issued for your server's installation ID (see [License](#license)) | +| Guides | this guide and the operator guide (PDF) | + +## Requirements + +- 64-bit Windows 10/11 or Windows Server 2019+, Ubuntu 22.04/24.04 or Debian 12, or macOS 13+. +- 4 CPU cores, 8 GB of memory, 50 GB of free disk (photos and maps grow with use). +- A fixed address on the robots' network (set a DHCP reservation in the router). +- Optional, for AI verification: [Ollama](https://ollama.com) with a vision model; a GPU or + 16 GB of memory for the 7B models. + +## Install + +**Windows.** Run `NETRA-Setup--x64.exe` as an administrator. It installs NETRA and +its database, starts the **NETRA** service (it starts with Windows), opens port 8080 in the +firewall, and opens the browser at the end. Configuration and data: +`C:\ProgramData\NETRA` (`netra.env`, `data\`, `logs\`). + +**Ubuntu or Debian.** + +```bash +sudo apt install ./netra__amd64.deb +``` + +It installs PostgreSQL when missing, creates the `netra` database and starts the `netra` +service. Configuration: `/etc/netra/netra.env`. Data and backups: `/var/lib/netra`. +Logs: `journalctl -u netra -f`. + +**macOS.** Open `NETRA--macos.pkg` (the first time: right-click > Open). Configuration +and data: `/Library/Application Support/NETRA`. + +**Docker.** For servers already run with Docker: the Netra repository's `docker-compose.yml` +with the image `ghcr.io/darmawanweb/netra` ([README](README.md#deploy-on-a-server)). + +## First-time setup + +Open `http://:8080` in a browser on the same network. A new installation opens +the setup: + +1. **Installation**: the installation ID of this server, such as `NETRA-7KQ3-M9XD-2WPA`. Copy + it: your license is issued for it. +2. **Administrator**: the first account; it manages users, robots and the license. +3. **License**: paste the license or choose the `.lic` file. Without one you can continue: the + core runs with one robot until the license is installed. + +## License + +The license names your organisation and decides the modules, the kind of map, the number of +robots and the validity. It works only on the server whose installation ID it names, without +internet. + +- No license yet: send the installation ID to EZRA Robotics, then install the file you receive. +- Install or replace a license: **Settings › License** (administrators), at any time, without a + restart. Or put the file at the license path of `netra.env` and restart the service. +- Settings › License shows the status, the modules, the robots against the limit and the expiry. + Before it expires, ask for a renewal; after the expiry the modules keep working for 14 days. +- A new server (or a new database) has a new installation ID and needs a new license. + +## Connect the robots + +In **Settings › Robots**, add each robot. Copy its token, shown once, into the robot's +configuration with the address of this server (`CC_URL`), as the robot's guide describes. + +## Configuration + +`netra.env` holds the settings; restart the service after a change. + +- Windows: services.msc > NETRA > Restart. +- Linux: `sudo systemctl restart netra`. +- macOS: `sudo launchctl kickstart -k system/id.ezrarobotics.netra`. + +| Setting | | +|---|---| +| `NETRA_ADDR` | port (`:8080`); `:80` to open the dashboard without a port | +| `NETRA_PUBLIC_URL` | the address robots and phones use, e.g. `http://192.168.1.10:8080` | +| `NETRA_TLS=self-signed` | HTTPS with a certificate made by NETRA (browsers ask once to trust it); or `NETRA_TLS_CERT` and `NETRA_TLS_KEY` for your own | +| `NETRA_OLLAMA_URL` | AI verification on Ollama, e.g. `http://localhost:11434` (license module needed) | +| `NETRA_TELEGRAM_TOKEN`, `NETRA_TELEGRAM_CHAT_ID` | Telegram alerts (license module needed) | +| `NETRA_BACKUP_KEEP_DAYS`, `NETRA_BACKUP_AT` | daily database backups: days kept, time | + +## Backups + +NETRA backs up its database every day (02:30 by default) into the `backups` folder of its data +and keeps 14 days. The **System** page (administrators) lists the backups, makes one now and +downloads them, and shows the disk space. +Photos and maps are files in the data folder: copy that folder to another disk regularly. + +Restore, with the service stopped: + +```bash +netra restore -yes /backups/netra-YYYYMMDD-HHMMSS.dump +``` + +## Update + +Install the new version over the old one (the same installer or package). The data, the +configuration and the license stay; the database is upgraded at the next start. + +## Forgotten administrator password + +On the server, as an administrator: + +- Windows: `"C:\Program Files\NETRA\netra.exe" reset-password admin` +- Linux: `sudo -u netra netra reset-password admin` +- macOS: `sudo -u _netra "/Library/NETRA/netra" -config "/Library/Application Support/NETRA/netra.env" reset-password admin` + +It prints a new password and signs the account out everywhere. + +## Commands + +`netra version`, `netra installation-id`, `netra backup`, `netra restore -yes `, +`netra license ` (checks a license against this server), +`netra reset-password [password]`. + +## Troubleshooting + +| Symptom | Check | +|---|---| +| The page does not open | the service runs (services.msc, `systemctl status netra`); the firewall allows the port; the address is the server's | +| Robots stay offline | the robot reaches the server: `curl http://:8080/healthz` on the robot; the token is the one shown when the robot was added | +| "Not in your license" | the module is not in the license: Settings › License, then EZRA Robotics | +| License refused: "for another installation" | the license was issued for another server's ID; send this server's ID | +| Logs | Windows: `C:\ProgramData\NETRA\logs`; Linux: `journalctl -u netra`; macOS: `/Library/Application Support/NETRA/logs` | + +## Uninstall + +Windows: Settings > Apps > NETRA. Linux: `sudo apt remove netra` (`purge` also removes the +configuration). The data folder and the database stay until you delete them. diff --git a/LOCAL.md b/LOCAL.md index ed640f7..b0ae1e9 100644 --- a/LOCAL.md +++ b/LOCAL.md @@ -5,9 +5,10 @@ connect to it directly, with or without internet. The vision model runs in a nat macOS so it can use the Apple GPU. ``` - Robot(s) ── LAN ──> Mac :80 (Caddy) ─┬─ /api, robot WebSocket ─> api ─┬─> TimescaleDB - ├─ /grafana ─> grafana ├─> NATS - └─ /* ─> web (dashboard) └─> ai ─> Ollama (native, Metal GPU) + Robot(s) ── LAN ──> Mac :80 (Caddy) ─┬─ /grafana ─> grafana + └─ /* ─> netra (API + dashboard) ─┬─> TimescaleDB + ├─> NATS + └─> Ollama (native, Metal GPU) ``` ## Requirements @@ -31,6 +32,11 @@ scripts/local.sh awake # keep the Mac awake for schedules and Open and log in as `admin` with the password printed by `setup` (it is also `NETRA_ADMIN_PASSWORD` in `.env`). Grafana is at . +Install the license: copy it to `license/netra.lic` and `scripts/local.sh up`, or paste it in +**Settings › License**. Without one only the core runs, with one robot +([license/README.md](license/README.md)). `scripts/local.sh export` carries a license installed in +the dashboard (it is in the database); a license file is copied by hand. + ## Connect a robot 1. Put the robot and the Mac on the same network: the same Wi-Fi, the robot's own Wi-Fi, a phone @@ -133,7 +139,7 @@ Third-party apps use the integration API through the same hostname: docs at | `scripts/local.sh start` | start the images built before, without internet (demo day) | | `scripts/local.sh status` | containers and API health | | `scripts/local.sh lan` | the Mac's address on every network, for `CC_URL` and phones | -| `scripts/local.sh logs [api\|ai\|web\|db\|...]` | follow logs | +| `scripts/local.sh logs [api\|db\|caddy\|...]` | follow logs | | `scripts/local.sh doctor` | check Docker, Ollama, model, AI link, LAN reachability, firewall, sleep | | `scripts/local.sh export [file]` | database, photos and floor plans in one file | | `scripts/local.sh import ` | replace all data here with an export (asks first) | @@ -155,6 +161,7 @@ daily database dumps go to `backups/`, exports to `exports/`. | `NETRA_LAN_PORT` | port for the dashboard and robots (80); add it to `NETRA_PUBLIC_URL` when changed | | `NETRA_TUNNEL_PORT`, `NETRA_LAN_BIND` | tunnel only: local port for cloudflared (8088), LAN side (`0.0.0.0`) | | `NETRA_EXT_TOKEN_MINUTES` | integration API access token lifetime (60) | +| (file) `license/netra.lic` | the license; not in `.env` | After editing `.env`: `scripts/local.sh up`. @@ -162,7 +169,7 @@ After editing `.env`: `scripts/local.sh up`. | Symptom | Fix | |---|---| -| `doctor`: AI container cannot reach Ollama | `brew services stop ollama && OLLAMA_HOST=0.0.0.0 ollama serve` | +| `doctor`: containers cannot reach Ollama | `brew services stop ollama && OLLAMA_HOST=0.0.0.0 ollama serve` | | Robot stays offline | on the robot `curl http://
/healthz`; no answer: other network, venue Wi-Fi isolating devices, or the macOS firewall (allow Docker); `401` in `logs api`: wrong `CC_TOKEN` | | Port 80 already in use | stop the other web server (`sudo lsof -i :80`) or set `NETRA_LAN_PORT` | | `git pull` refuses: untracked `docker-compose.tunnel.yml` / `Caddyfile.tunnel` | delete the two hand-made files, the repository has them now | diff --git a/README.md b/README.md index 8fbea24..0df7d46 100644 --- a/README.md +++ b/README.md @@ -76,21 +76,23 @@ flowchart LR R <-- WebSocket + HTTPS --> C[Caddy
HTTPS] B[Browser
operators] --> C P[Third-party app] -- OAuth2 --> C - C --> API[netra-api
Go] - C --> WEB[netra-web
Next.js] - API --> DB[(TimescaleDB)] + C --> API[netra
Go service + dashboard] + API --> DB[(PostgreSQL / TimescaleDB)] API --> NATS[NATS JetStream] - API --> AI[netra-ai
FastAPI] --> OL[Ollama
vision-language model] + API --> OL[Ollama
vision-language model] API --> TG[Telegram] PR[Prometheus + Grafana] -.-> API ``` | Path | | |---|---| -| [`be/`](https://github.com/DarmawanWeb/netra-fleet-be) | `netra-api` (Go) and `netra-ai` (FastAPI, vision-language model) | -| [`fe/`](https://github.com/DarmawanWeb/netra-fleet-fe) | `netra-web` (Next.js dashboard) | +| [`be/`](https://github.com/DarmawanWeb/netra-fleet-be) | the `netra` service (Go); `netra-ai` (FastAPI), now optional | +| [`fe/`](https://github.com/DarmawanWeb/netra-fleet-fe) | the dashboard (Next.js static export, built into `netra`) | +| `Dockerfile` | the single image `netra`: the dashboard built into the service | +| `packaging/` | the installers: Linux `.deb` with systemd, Windows (Inno Setup, WinSW), macOS `.pkg` | +| `INSTALL.md`, `INSTALL.id.md` | the client's install guide | | `docker-compose.yml` | the whole stack | -| `Caddyfile` | HTTPS, `/api` to the API, `/grafana` to Grafana, the rest to the dashboard | +| `Caddyfile` | HTTPS, `/grafana` to Grafana, everything else to `netra` (API and dashboard) | | `monitoring/` | Prometheus scrape config, Grafana datasources (Prometheus, TimescaleDB) and dashboard | | `scripts/deploy.sh` | server: pull, update submodules, pull images, restart, pull the AI model | | `docker-compose.local.yml`, `Caddyfile.local`, `.env.local.example`, `scripts/local.sh` | running on a Mac ([LOCAL.md](LOCAL.md)) | @@ -100,6 +102,14 @@ flowchart LR The robots run [Pashupati](https://github.com/DarmawanRobotics/Pashupati); the handheld is [Pashupati Controller](https://github.com/DarmawanRobotics/PashupatiController). +## Install at a client + +One application with its database, installed like any other program: the Windows installer, the +Ubuntu/Debian package or the macOS package of a [release](docs/release.md). The browser then opens +the setup (installation ID, administrator, license). Client guide: **[INSTALL.md](INSTALL.md)** +([Indonesian](INSTALL.id.md)); what EZRA hands over and how a license is activated: +[docs/client-kit.md](docs/client-kit.md). + ## Run on a Mac Robots on the same network as the Mac. See **[LOCAL.md](LOCAL.md)**: @@ -113,12 +123,14 @@ change `CC_URL` ([Demo: the command center on a laptop](LOCAL.md#demo-the-comman ```bash git clone --recursive git@github.com:DarmawanWeb/Netra.git && cd Netra cp .env.example .env # domain, passwords, Telegram, AI model +cp ~/bekasi.lic license/netra.lic # the client's license docker compose up -d --build # or with CI images: docker compose pull && docker compose up -d docker compose exec ollama ollama pull qwen2.5vl:7b ``` -Then open `https://`, log in as `NETRA_ADMIN_USER`, add users (roles viewer, operator, -supervisor, admin), register robots and upload the floor plan. Grafana is at `/grafana`. +Put the license in `license/netra.lic` ([License](#license)). Then open `https://`, log in +as `NETRA_ADMIN_USER`, add users (roles viewer, operator, supervisor, admin), register robots and +upload the floor plan. Grafana is at `/grafana`.
Updates, backups @@ -133,6 +145,21 @@ Anomaly photos and floor plans are in the `api-data` volume.
+## License + +One NETRA for every client: the license of a deployment decides its **edition** (mall, +substation), its **tier** (basic, pro, enterprise), the **modules** (anomaly detection, crowd +analytics, substation inspection, report exports, Telegram alerts, audit log, server AI +verification, integration API, multi-site sync), the **robot limit** and the **branding** (name, +colours, logo). Put the license from EZRA Robotics in `license/netra.lic` and +`docker compose up -d api`, or install it in **Settings › License** +([license/README.md](license/README.md)). Without one, the core runs with one robot. + +Updating a deployment from before licensing: install its license (a mall: +`-edition mall -tier enterprise`, or what the client bought) together with the update, or the +anomaly, crowd and other modules disappear until it is installed. How licenses are made: +[netra-fleet-be licensing](https://github.com/DarmawanWeb/netra-fleet-be/blob/main/docs/licensing.md). + ## Integration API Other systems read the anomalies (with their original photos), robots, patrols, routes and floor @@ -148,7 +175,11 @@ interactive docs at `https:///api/ext/docs` (OpenAPI: `/api/ext/v1/opena |---|---| | Operators, security staff, supervisors | [Panduan Operator (PDF, Indonesian)](docs/NETRA-Panduan-Operator.pdf): safety, daily start with the Genisom L1W, dashboard, patrols, validation, schedules, routes, reports, troubleshooting | | Running NETRA on a Mac | [LOCAL.md](LOCAL.md) | -| Backend developers | [netra-fleet-be/docs](https://github.com/DarmawanWeb/netra-fleet-be/tree/main/docs): architecture, dashboard API, database, configuration, development, AI service | +| Backend developers | [netra-fleet-be/docs](https://github.com/DarmawanWeb/netra-fleet-be/tree/main/docs): architecture, dashboard API, database, licensing, configuration, development, AI service | +| Client administrators | [INSTALL.md](INSTALL.md) ([Indonesian](INSTALL.id.md)): install, first-time setup, license, robots, backups, updates, recovery | +| EZRA Robotics: delivery | [client kit](docs/client-kit.md): what a client gets, activation, renewal | +| EZRA Robotics: releases | [releasing](docs/release.md): the release workflow, installers, building by hand | +| Licenses (EZRA Robotics) | [licensing](https://github.com/DarmawanWeb/netra-fleet-be/blob/main/docs/licensing.md): editions, tiers, modules, signing key, issuing; [license/README.md](license/README.md) on a server | | Dashboard developers | [netra-fleet-fe/docs](https://github.com/DarmawanWeb/netra-fleet-fe/tree/main/docs): architecture, development | | Third-party integrators | [INTEGRATION.md](https://github.com/DarmawanWeb/netra-fleet-be/blob/main/INTEGRATION.md) and `/api/ext/docs` on the server | | Robot side | [Pashupati](https://github.com/DarmawanRobotics/Pashupati) ([robot API](https://github.com/DarmawanRobotics/Pashupati/blob/main/src/robot_fleet/API.md)) | diff --git a/be b/be index 4048e0e..3a5636f 160000 --- a/be +++ b/be @@ -1 +1 @@ -Subproject commit 4048e0ecac02df7844a269c5cea4612f719df322 +Subproject commit 3a5636ff60f5a32df31406e75618e9ebfd71e29a diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 983f722..658dab5 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -7,6 +7,8 @@ services: environment: NETRA_PUBLIC_URL: ${NETRA_PUBLIC_URL:-http://localhost} NETRA_ALLOWED_ORIGIN: "*" + NETRA_OLLAMA_URL: ${OLLAMA_URL:-http://host.docker.internal:11434} + NETRA_AI_TIMEOUT: "120" ai: pull_policy: build @@ -18,9 +20,6 @@ services: ollama: profiles: [server] # not started locally - web: - pull_policy: build - caddy: ports: !override ["${NETRA_LAN_PORT:-80}:80"] volumes: diff --git a/docker-compose.yml b/docker-compose.yml index efd7520..c1abf84 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,10 +26,13 @@ services: command: ["--jetstream", "--store_dir", "/data", "--http_port", "8222"] volumes: [nats-data:/data] + # NETRA: the API with the dashboard inside (one image built from be/ and fe/, see Dockerfile). api: <<: *restart - image: ${REGISTRY:-ghcr.io/darmawanweb}/netra-api:${TAG:-main} - build: ./be + image: ${REGISTRY:-ghcr.io/darmawanweb}/netra:${TAG:-main} + build: + context: . + args: { VERSION: "${TAG:-dev}" } env_file: .env environment: NETRA_DATABASE_URL: postgres://netra:${POSTGRES_PASSWORD}@db:5432/netra?sslmode=disable @@ -37,14 +40,22 @@ services: NETRA_PUBLIC_URL: https://${NETRA_DOMAIN} NETRA_ALLOWED_ORIGIN: https://${NETRA_DOMAIN} NETRA_NATS_URL: nats://nats:4222 - NETRA_FINAL_DETECTOR_URL: http://ai:8090/detect - volumes: [api-data:/data] + # Final detection on Ollama, built into NETRA (the separate ai service is optional) + NETRA_OLLAMA_URL: http://ollama:11434 + # The backup service below dumps the database + NETRA_BACKUP_KEEP_DAYS: "0" + # The license of this deployment (license/README.md); missing: the core only + NETRA_LICENSE_FILE: /license/netra.lic + volumes: [api-data:/data, ./license:/license:ro] depends_on: db: { condition: service_healthy } nats: { condition: service_started } + # The former final-detection service; NETRA calls Ollama itself now. To keep using it: + # COMPOSE_PROFILES=ai-service and NETRA_FINAL_DETECTOR_URL=http://ai:8090/detect in .env. ai: <<: *restart + profiles: [ai-service] image: ${REGISTRY:-ghcr.io/darmawanweb}/netra-ai:${TAG:-main} build: ./be/ai environment: @@ -63,11 +74,6 @@ services: # reservations: # devices: [{ driver: nvidia, count: all, capabilities: [gpu] }] - web: - <<: *restart - image: ${REGISTRY:-ghcr.io/darmawanweb}/netra-web:${TAG:-main} - build: ./fe - depends_on: [api] caddy: <<: *restart @@ -79,7 +85,7 @@ services: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy-data:/data - caddy-config:/config - depends_on: [api, web, grafana] + depends_on: [api, grafana] prometheus: <<: *restart diff --git a/docs/client-kit.md b/docs/client-kit.md new file mode 100644 index 0000000..232c7da --- /dev/null +++ b/docs/client-kit.md @@ -0,0 +1,51 @@ +# Delivering NETRA to a client + +What EZRA Robotics hands over for each deployment, and in which order. The client-facing guide +is [INSTALL.md](../INSTALL.md) ([Indonesian](../INSTALL.id.md)); licenses are explained in +[netra-fleet-be licensing](https://github.com/DarmawanWeb/netra-fleet-be/blob/main/docs/licensing.md) +and issued with the +[License Manager](https://github.com/DarmawanWeb/netra-fleet-be/blob/main/docs/license-manager.md) +(`make license-manager` in netra-fleet-be, on EZRA's own computer). + +## What the client gets + +| Item | From | Notes | +|---|---|---| +| Installer for their server | the GitHub release of the version | Windows `NETRA-Setup--x64.exe`, Ubuntu/Debian `netra__.deb`, macOS `NETRA--macos.pkg`, Docker image `ghcr.io/darmawanweb/netra:`; with `SHA256SUMS` | +| License file | the License Manager | bound to the server's installation ID; edition, tier, modules, map mode, robot limit, validity, branding | +| Install guide | `INSTALL.md` / `INSTALL.id.md` | install, first-time setup, license, robots, backups, updates, recovery | +| Operator guide | `docs/NETRA-Panduan-Operator.pdf` | daily use (mall edition today) | +| Robot settings | the client's Settings › Robots | each robot's token, shown once, and the server address | + +Never send the signing key, the License Manager, the `netra-license` tool or a license issued for +another client. + +## Steps + +1. **Agree** on the edition (mall, substation), the tier (basic, pro, enterprise), extra modules, + the map mode (floor plan, robot map, GPS), the number of robots, the validity, and the + branding (application name, colours, logo as SVG or PNG up to 256 KB). +2. **Prepare the server** with the client: a computer that stays on, a fixed address on the + robots' network, the requirements of INSTALL.md. +3. **Install** the version's installer. The browser opens the setup at `http://:8080`. +4. **Activate**: the setup shows the installation ID (`NETRA-XXXX-XXXX-XXXX`). The client sends + it; issue the license bound to it in the License Manager (the client's deployment record keeps + the ID) and send the `.lic` file. The client installs it in the setup or later in + Settings › License. No internet is needed on site. +5. **Configure**: users and roles, robots and their tokens, the map (floor plan, robot map or + GPS site map), patrol routes and schedules; the AI model on Ollama if `ai.final` is included. +6. **Hand over**: the operator guide, a walk through backups and the System page, the support + contact; note the installation ID, version and license expiry in the client's record. + +## Afterwards + +- **Renewal**: the License Manager lists licenses expiring in the next 30 days. Renew (same terms, + new dates) and send the file; the client installs it in Settings › License, no restart. +- **Upgrade or downgrade** of tier or modules: a new license with the change. Data of a module + that goes away stays in the database and comes back with the module. +- **New server or reinstallation on an empty database**: a new installation ID, so a new license. + Restoring a backup keeps the installation ID. +- **Updates**: the new version's installer over the old one; the data, configuration and license + stay. Read the release notes for anything to do on site. +- **Support**: ask for the System page (version, installation ID, disk, backups), the license + status and the logs (INSTALL.md lists where they are). diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 0000000..3b83c78 --- /dev/null +++ b/docs/release.md @@ -0,0 +1,58 @@ +# Releasing NETRA + +A release is a tag on this repository (`vX.Y.Z`, or `vX.Y.Z-rc1` for a pre-release) with the +`be` and `fe` submodules on the commits to ship. The [release workflow](../.github/workflows/release.yml) +builds everything a client installs and publishes a GitHub release. + +## Before tagging + +1. `be` and `fe` are on their `main` commits with green CI; move the submodules here: + `git submodule update --remote be fe && git commit -am "chore: update be and fe"`. +2. The trusted public key is in `be/internal/license/keys/` (no key: every installation runs the + core only). +3. The secret `NETRA_REPO_TOKEN` exists in this repository: a token that can read + `netra-fleet-be` and `netra-fleet-fe`, for the submodules. +4. Try the version on a test server: install the previous release, update with the new one, + check the setup, a license, a robot, a backup and a restore. + +## What the workflow builds + +| Job | Output | +|---|---| +| `web` | the dashboard export (`fe/out`) | +| `binaries` | `netra--` for linux/amd64, linux/arm64, windows/amd64, darwin/amd64, darwin/arm64, with the dashboard inside and the version | +| `deb` | `netra__amd64.deb`, `netra__arm64.deb` ([packaging/linux](../packaging/linux)) | +| `windows` | `NETRA-Setup--x64.exe`: Inno Setup, WinSW service, PostgreSQL binaries ([packaging/windows](../packaging/windows)) | +| `macos` | `NETRA--macos.pkg`: universal binary, PostgreSQL binaries, launchd service ([packaging/macos](../packaging/macos)); not signed | +| `image` | `ghcr.io//netra:` and `:main`, linux/amd64 and arm64 ([Dockerfile](../Dockerfile)) | +| `release` | the GitHub release with the files above and `SHA256SUMS` | + +Run it by hand (Actions > release > Run workflow, with a version) to build the files without +publishing a release. + +## Build by hand + +```bash +git submodule update --init +(cd fe && npm ci && npm run build) +rm -rf be/internal/web/dist && cp -r fe/out be/internal/web/dist +(cd be && go build -tags webui -ldflags "-X github.com/DarmawanWeb/netra-fleet-be/internal/version.Version=v1.2.0" -o ../netra ./cmd/netra) +packaging/linux/build-deb.sh netra v1.2.0 amd64 dist # needs dpkg-deb +docker build --build-arg VERSION=v1.2.0 -t netra . # the single image +``` + +## Installers in detail + +- **Linux**: `/usr/bin/netra`, the systemd unit `netra.service` (user `netra`, + `/var/lib/netra`), `/etc/netra/netra.env` (kept on upgrade). The postinst creates the `netra` + role and database in the local PostgreSQL; the service connects through the socket with peer + authentication, so no password is stored. +- **Windows**: `C:\Program Files\NETRA` (netra.exe, the WinSW wrapper `netra-service.exe`, + `pgsql\`), `C:\ProgramData\NETRA` (netra.env, data, logs). The service runs as Network Service; + NETRA runs its own PostgreSQL in `data\pgdata` on 127.0.0.1:5433. Upgrades stop the service + first and keep netra.env. +- **macOS**: `/Library/NETRA`, the LaunchDaemon `id.ezrarobotics.netra` running as the hidden + user `_netra`, `/Library/Application Support/NETRA`; its own PostgreSQL like Windows. Sign it + with a Developer ID (`productsign`) to avoid the Gatekeeper warning. +- **Docker**: the compose file of this repository; the database is the `db` service and the + `backup` service dumps it. diff --git a/fe b/fe index 147545f..5d832a0 160000 --- a/fe +++ b/fe @@ -1 +1 @@ -Subproject commit 147545f349b7bf7bcfda0a5f5d8217023bd6c9b3 +Subproject commit 5d832a013d4d61668e2847f84d02306fe07be829 diff --git a/license/README.md b/license/README.md new file mode 100644 index 0000000..bef93e9 --- /dev/null +++ b/license/README.md @@ -0,0 +1,14 @@ +# License + +Put the license of this deployment here as `netra.lic` (the one line `NETRA-LIC-1....` from EZRA +Robotics). The API reads it at start (`NETRA_LICENSE_FILE=/license/netra.lic`, mounted read-only), +so after replacing it run `docker compose up -d api`. An admin can also install a license in the +dashboard, Settings › License, without a restart. + +Without a valid license NETRA runs the core only (robots, live map, routes, patrols, schedules, +floor plans, users, patrol reports) with one robot. The license decides the edition (mall, +substation), the tier (basic, pro, enterprise), the modules, the robot limit and the branding; +Settings › License shows what it includes. + +License files are ignored by git (`*.lic`). How licenses work and how they are issued: +[netra-fleet-be licensing](https://github.com/DarmawanWeb/netra-fleet-be/blob/main/docs/licensing.md). diff --git a/packaging/linux/build-deb.sh b/packaging/linux/build-deb.sh new file mode 100755 index 0000000..3b4b3c9 --- /dev/null +++ b/packaging/linux/build-deb.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Build the NETRA Debian/Ubuntu package from a netra binary: +# packaging/linux/build-deb.sh [out dir] +# The package depends on the distribution's PostgreSQL; its postinst creates the database. +set -euo pipefail + +BIN=${1:?netra binary} +VERSION=${2:?version, e.g. 1.2.0} +ARCH=${3:?amd64 or arm64} +OUT=${4:-dist} +MAINTAINER=${MAINTAINER:-EZRA Robotics } +HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +VERSION=${VERSION#v} + +STAGE=$(mktemp -d) +trap 'rm -rf "$STAGE"' EXIT +chmod 0755 "$STAGE" +install -D -m 0755 "$BIN" "$STAGE/usr/bin/netra" +install -D -m 0644 "$HERE/netra.service" "$STAGE/lib/systemd/system/netra.service" +install -D -m 0640 "$HERE/netra.env" "$STAGE/etc/netra/netra.env" +install -D -m 0644 "$HERE/../../INSTALL.md" "$STAGE/usr/share/doc/netra/INSTALL.md" +install -d "$STAGE/DEBIAN" +install -m 0755 "$HERE/debian/postinst" "$HERE/debian/prerm" "$HERE/debian/postrm" "$STAGE/DEBIAN/" +echo /etc/netra/netra.env > "$STAGE/DEBIAN/conffiles" +SIZE=$(du -sk --exclude=DEBIAN "$STAGE" | cut -f1) +cat > "$STAGE/DEBIAN/control" <= 14) | postgresql-17 | postgresql-16 | postgresql-15 | postgresql-14, adduser, passwd +Recommends: postgresql-contrib +Installed-Size: $SIZE +Section: net +Priority: optional +Homepage: https://github.com/DarmawanWeb/Netra +Description: NETRA patrol command center + The command center for patrol and inspection robots: live fleet and map, patrols and + schedules, inspections, reports and the integration API, as one service with its dashboard. + Open the server in a browser after installing to finish the setup. +CONTROL +mkdir -p "$OUT" +dpkg-deb --root-owner-group --build "$STAGE" "$OUT/netra_${VERSION}_${ARCH}.deb" >/dev/null +echo "$OUT/netra_${VERSION}_${ARCH}.deb" diff --git a/packaging/linux/debian/postinst b/packaging/linux/debian/postinst new file mode 100755 index 0000000..5d7bbbf --- /dev/null +++ b/packaging/linux/debian/postinst @@ -0,0 +1,51 @@ +#!/bin/sh +# NETRA: a system user, its directories, a PostgreSQL role and database, the service. +set -e + +[ "$1" = "configure" ] || exit 0 + +ENV=/etc/netra/netra.env + +if ! getent passwd netra >/dev/null; then + adduser --system --group --home /var/lib/netra --no-create-home --gecos "NETRA" netra >/dev/null +fi +install -d -o netra -g netra -m 0750 /var/lib/netra +chown root:netra /etc/netra "$ENV" +chmod 0750 /etc/netra +chmod 0640 "$ENV" + +# The database: a role and a database in the local PostgreSQL, once. The service connects through +# the local socket as the system user netra (peer authentication), so no password is stored. +if ! grep -q '^NETRA_DATABASE_URL=.\+' "$ENV"; then + if command -v systemctl >/dev/null && [ -d /run/systemd/system ]; then + systemctl start postgresql >/dev/null 2>&1 || true + fi + for _ in $(seq 1 30); do + runuser -u postgres -- psql -qAtc 'SELECT 1' >/dev/null 2>&1 && break + sleep 1 + done + if ! runuser -u postgres -- psql -qAtc "SELECT 1 FROM pg_roles WHERE rolname = 'netra'" | grep -q 1; then + runuser -u postgres -- psql -qc "CREATE ROLE netra WITH LOGIN" >/dev/null + fi + if ! runuser -u postgres -- psql -qAtc "SELECT 1 FROM pg_database WHERE datname = 'netra'" | grep -q 1; then + runuser -u postgres -- createdb -O netra netra + fi + sed -i "s|^NETRA_DATABASE_URL=.*|NETRA_DATABASE_URL=postgres://netra@/netra?host=/var/run/postgresql\\&sslmode=disable|" "$ENV" +fi + +if command -v systemctl >/dev/null && [ -d /run/systemd/system ]; then + systemctl daemon-reload + systemctl enable netra >/dev/null 2>&1 + systemctl restart netra +fi + +PORT=$(sed -n 's/^NETRA_ADDR=.*:\([0-9]*\)$/\1/p' "$ENV") +cat </dev/null | cut -d' ' -f1 || hostname):${PORT:-8080} + in a browser to finish the setup (installation ID, administrator, license). + Configuration: $ENV Data and backups: /var/lib/netra + Service: systemctl status netra Logs: journalctl -u netra -f + +MSG +exit 0 diff --git a/packaging/linux/debian/postrm b/packaging/linux/debian/postrm new file mode 100755 index 0000000..dcf618d --- /dev/null +++ b/packaging/linux/debian/postrm @@ -0,0 +1,12 @@ +#!/bin/sh +# Removing NETRA keeps its data (/var/lib/netra) and its database; purging removes the +# configuration too. Delete the data by hand once it is no longer needed: +# rm -rf /var/lib/netra && sudo -u postgres dropdb netra && sudo -u postgres dropuser netra +set -e +if [ "$1" = "purge" ]; then + rm -rf /etc/netra +fi +if command -v systemctl >/dev/null && [ -d /run/systemd/system ]; then + systemctl daemon-reload >/dev/null 2>&1 || true +fi +exit 0 diff --git a/packaging/linux/debian/prerm b/packaging/linux/debian/prerm new file mode 100755 index 0000000..64b5927 --- /dev/null +++ b/packaging/linux/debian/prerm @@ -0,0 +1,7 @@ +#!/bin/sh +set -e +if [ "$1" = "remove" ] && command -v systemctl >/dev/null && [ -d /run/systemd/system ]; then + systemctl stop netra >/dev/null 2>&1 || true + systemctl disable netra >/dev/null 2>&1 || true +fi +exit 0 diff --git a/packaging/linux/netra.env b/packaging/linux/netra.env new file mode 100644 index 0000000..5aaa8a8 --- /dev/null +++ b/packaging/linux/netra.env @@ -0,0 +1,35 @@ +# NETRA configuration (systemd reads it; `systemctl restart netra` after a change). +# Every setting: https://github.com/DarmawanWeb/netra-fleet-be/blob/main/docs/configuration.md + +# Address and port of the dashboard and of the robots +NETRA_ADDR=:8080 +# Address robots and phones use to reach this server, e.g. http://192.168.1.10:8080 +NETRA_PUBLIC_URL= +NETRA_TZ=Asia/Jakarta +# Language of Telegram captions: en or id +NETRA_LANG=id + +NETRA_DATA_DIR=/var/lib/netra +# Written by the installer: the local PostgreSQL through its socket, as the system user netra +NETRA_DATABASE_URL= + +# The license file (or install it in the dashboard: Settings > License) +NETRA_LICENSE_FILE=/etc/netra/license.lic + +# HTTPS: self-signed (browsers ask to trust it once), or a certificate of your own +# NETRA_TLS=self-signed +# NETRA_TLS_CERT=/etc/netra/tls/cert.pem +# NETRA_TLS_KEY=/etc/netra/tls/key.pem + +# Final AI detection on Ollama (module ai.final), e.g. http://localhost:11434 +# NETRA_OLLAMA_URL= +# NETRA_AI_MODEL=qwen2.5vl:7b +# NETRA_AI_LANGUAGE=Indonesian + +# Daily database backups in /var/lib/netra/backups +NETRA_BACKUP_KEEP_DAYS=14 +NETRA_BACKUP_AT=02:30 + +# Telegram alerts (module alerts.telegram) +# NETRA_TELEGRAM_TOKEN= +# NETRA_TELEGRAM_CHAT_ID= diff --git a/packaging/linux/netra.service b/packaging/linux/netra.service new file mode 100644 index 0000000..c7f85ae --- /dev/null +++ b/packaging/linux/netra.service @@ -0,0 +1,27 @@ +[Unit] +Description=NETRA patrol command center +Documentation=https://github.com/DarmawanWeb/Netra/blob/main/INSTALL.md +After=network-online.target postgresql.service +Wants=network-online.target postgresql.service + +[Service] +Type=simple +User=netra +Group=netra +EnvironmentFile=/etc/netra/netra.env +ExecStart=/usr/bin/netra serve +WorkingDirectory=/var/lib/netra +Restart=on-failure +RestartSec=5 +TimeoutStopSec=30 +LimitNOFILE=65536 +# Ports below 1024 (NETRA_ADDR=:80 or :443) without running as root +AmbientCapabilities=CAP_NET_BIND_SERVICE +NoNewPrivileges=true +ProtectSystem=full +ProtectHome=true +PrivateTmp=true +ReadWritePaths=/var/lib/netra + +[Install] +WantedBy=multi-user.target diff --git a/packaging/macos/build-pkg.sh b/packaging/macos/build-pkg.sh new file mode 100755 index 0000000..4b617bf --- /dev/null +++ b/packaging/macos/build-pkg.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Build the NETRA installer for macOS (on a Mac): +# packaging/macos/build-pkg.sh [out dir] +# holds PostgreSQL's bin/, lib/ and share/ for macOS. The package is not signed: +# first open it with right-click > Open, or sign it with productsign and a Developer ID. +set -euo pipefail + +BIN=${1:?netra binary} +VERSION=${2:?version} +PGSQL=${3:?PostgreSQL binaries directory} +OUT=${4:-dist} +HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +VERSION=${VERSION#v} + +ROOT=$(mktemp -d) +trap 'rm -rf "$ROOT"' EXIT +install -d "$ROOT/Library/NETRA" "$ROOT/Library/LaunchDaemons" +install -m 0755 "$BIN" "$ROOT/Library/NETRA/netra" +cp -R "$PGSQL" "$ROOT/Library/NETRA/pgsql" +install -m 0644 "$HERE/../../INSTALL.md" "$ROOT/Library/NETRA/INSTALL.md" +install -m 0644 "$HERE/id.ezrarobotics.netra.plist" "$ROOT/Library/LaunchDaemons/" +mkdir -p "$OUT" +pkgbuild --root "$ROOT" --scripts "$HERE/scripts" --identifier id.ezrarobotics.netra --version "$VERSION" \ + --install-location / "$OUT/NETRA-$VERSION-macos.pkg" +echo "$OUT/NETRA-$VERSION-macos.pkg" diff --git a/packaging/macos/id.ezrarobotics.netra.plist b/packaging/macos/id.ezrarobotics.netra.plist new file mode 100644 index 0000000..f56a881 --- /dev/null +++ b/packaging/macos/id.ezrarobotics.netra.plist @@ -0,0 +1,32 @@ + + + + + Label + id.ezrarobotics.netra + ProgramArguments + + /Library/NETRA/netra + -config + /Library/Application Support/NETRA/netra.env + serve + + UserName + _netra + WorkingDirectory + /Library/Application Support/NETRA + RunAtLoad + + KeepAlive + + SuccessfulExit + + + ExitTimeOut + 45 + StandardOutPath + /Library/Application Support/NETRA/logs/netra.log + StandardErrorPath + /Library/Application Support/NETRA/logs/netra.log + + diff --git a/packaging/macos/scripts/postinstall b/packaging/macos/scripts/postinstall new file mode 100755 index 0000000..a18743a --- /dev/null +++ b/packaging/macos/scripts/postinstall @@ -0,0 +1,44 @@ +#!/bin/sh +# NETRA on macOS: a hidden system user, its folders, the configuration once, the service. +set -e +SUPPORT="/Library/Application Support/NETRA" + +if ! dscl . -read /Users/_netra >/dev/null 2>&1; then + ID=480 + while dscl . -list /Users UniqueID | awk '{print $2}' | grep -qx "$ID" || + dscl . -list /Groups PrimaryGroupID | awk '{print $2}' | grep -qx "$ID"; do + ID=$((ID + 1)) + done + dscl . -create /Groups/_netra PrimaryGroupID "$ID" + dscl . -create /Users/_netra UniqueID "$ID" + dscl . -create /Users/_netra PrimaryGroupID "$ID" + dscl . -create /Users/_netra UserShell /usr/bin/false + dscl . -create /Users/_netra NFSHomeDirectory /var/empty + dscl . -create /Users/_netra RealName "NETRA" + dscl . -create /Users/_netra IsHidden 1 +fi + +mkdir -p "$SUPPORT/data" "$SUPPORT/logs" +if [ ! -f "$SUPPORT/netra.env" ]; then + cat > "$SUPPORT/netra.env" </dev/null || hostname).local:8080 +NETRA_PUBLIC_URL= +NETRA_TZ=Asia/Jakarta +NETRA_LANG=id +NETRA_DATA_DIR=$SUPPORT/data +NETRA_PG_BIN=/Library/NETRA/pgsql/bin +NETRA_PG_PORT=5433 +NETRA_LICENSE_FILE=$SUPPORT/license.lic +NETRA_BACKUP_KEEP_DAYS=14 +# NETRA_TLS=self-signed +# NETRA_OLLAMA_URL=http://localhost:11434 +ENV +fi +chown -R _netra:_netra "$SUPPORT" +chmod 750 "$SUPPORT" + +launchctl bootstrap system /Library/LaunchDaemons/id.ezrarobotics.netra.plist +echo "NETRA is installed: open http://localhost:8080 to finish the setup." +exit 0 diff --git a/packaging/macos/scripts/preinstall b/packaging/macos/scripts/preinstall new file mode 100755 index 0000000..08d943f --- /dev/null +++ b/packaging/macos/scripts/preinstall @@ -0,0 +1,4 @@ +#!/bin/sh +# An upgrade replaces the program: stop the running service first. +launchctl bootout system/id.ezrarobotics.netra 2>/dev/null || true +exit 0 diff --git a/packaging/windows/netra-service.xml b/packaging/windows/netra-service.xml new file mode 100644 index 0000000..0ea1ecc --- /dev/null +++ b/packaging/windows/netra-service.xml @@ -0,0 +1,24 @@ + + + netra + NETRA + NETRA patrol command center + %BASE%\netra.exe + -config "%ProgramData%\NETRA\netra.env" serve + %ProgramData%\NETRA + Automatic + true + + 1 hour + 45 sec + %ProgramData%\NETRA\logs + + 10240 + 8 + + + + NT AUTHORITY + NetworkService + + diff --git a/packaging/windows/netra.iss b/packaging/windows/netra.iss new file mode 100644 index 0000000..6849ff5 --- /dev/null +++ b/packaging/windows/netra.iss @@ -0,0 +1,102 @@ +; NETRA installer for Windows (Inno Setup 6): the service with its own PostgreSQL. +; Built by the release workflow: iscc /DVersion=1.2.0 /DStage= packaging\windows\netra.iss +; holds netra.exe, netra-service.exe (WinSW 2), pgsql\ (PostgreSQL binaries), INSTALL.md. + +#ifndef Version + #define Version "0.0.0" +#endif +#ifndef Stage + #define Stage "stage" +#endif +#define Port "8080" + +[Setup] +AppId={{6E3C1A54-8E8B-4C2B-9F0E-3B9C7D8A4E21} +AppName=NETRA +AppVersion={#Version} +AppPublisher=EZRA Robotics +AppComments=Patrol command center +DefaultDirName={autopf}\NETRA +DefaultGroupName=NETRA +DisableProgramGroupPage=yes +OutputBaseFilename=NETRA-Setup-{#Version}-x64 +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible +PrivilegesRequired=admin +Compression=lzma2/max +SolidCompression=yes +WizardStyle=modern +UninstallDisplayIcon={app}\netra.exe +UninstallDisplayName=NETRA +CloseApplications=no + +[Files] +Source: "{#Stage}\netra.exe"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#Stage}\netra-service.exe"; DestDir: "{app}"; Flags: ignoreversion +Source: "netra-service.xml"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#Stage}\pgsql\*"; DestDir: "{app}\pgsql"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "{#Stage}\INSTALL.md"; DestDir: "{app}"; Flags: ignoreversion + +[Dirs] +Name: "{commonappdata}\NETRA"; Permissions: networkservice-modify +Name: "{commonappdata}\NETRA\data"; Permissions: networkservice-modify +Name: "{commonappdata}\NETRA\logs"; Permissions: networkservice-modify + +[Icons] +Name: "{group}\NETRA Dashboard"; Filename: "http://localhost:{#Port}" +Name: "{group}\NETRA data and configuration"; Filename: "{commonappdata}\NETRA" +Name: "{group}\Uninstall NETRA"; Filename: "{uninstallexe}" + +[Run] +Filename: "{app}\netra-service.exe"; Parameters: "install"; Flags: runhidden; StatusMsg: "Installing the NETRA service..." +Filename: "{sys}\netsh.exe"; Parameters: "advfirewall firewall add rule name=""NETRA"" dir=in action=allow protocol=TCP localport={#Port}"; Flags: runhidden; StatusMsg: "Opening port {#Port}..." +Filename: "{app}\netra-service.exe"; Parameters: "start"; Flags: runhidden; StatusMsg: "Starting NETRA..." +Filename: "http://localhost:{#Port}"; Description: "Open NETRA to finish the setup"; Flags: shellexec postinstall nowait skipifsilent + +[UninstallRun] +Filename: "{app}\netra-service.exe"; Parameters: "stop"; Flags: runhidden; RunOnceId: "StopService" +Filename: "{app}\netra-service.exe"; Parameters: "uninstall"; Flags: runhidden; RunOnceId: "RemoveService" +Filename: "{sys}\netsh.exe"; Parameters: "advfirewall firewall delete rule name=""NETRA"""; Flags: runhidden; RunOnceId: "RemoveFirewallRule" + +[Code] +const + CRLF = #13#10; + +{ An upgrade replaces netra.exe: stop the running service first. } +function PrepareToInstall(var NeedsRestart: Boolean): String; +var + Code: Integer; +begin + if FileExists(ExpandConstant('{app}\netra-service.exe')) then + Exec(ExpandConstant('{app}\netra-service.exe'), 'stop', '', SW_HIDE, ewWaitUntilTerminated, Code); + Result := ''; +end; + +{ The configuration is written once and kept across upgrades and uninstalls, with the data. } +procedure CurStepChanged(CurStep: TSetupStep); +var + Env: String; +begin + if CurStep = ssPostInstall then + begin + Env := ExpandConstant('{commonappdata}\NETRA\netra.env'); + if not FileExists(Env) then + SaveStringToFile(Env, + '# NETRA configuration. Restart the NETRA service after a change.' + CRLF + + '# Every setting: https://github.com/DarmawanWeb/netra-fleet-be/blob/main/docs/configuration.md' + CRLF + + 'NETRA_ADDR=:{#Port}' + CRLF + + '# Address robots and phones use to reach this computer, e.g. http://192.168.1.10:{#Port}' + CRLF + + 'NETRA_PUBLIC_URL=' + CRLF + + 'NETRA_TZ=Asia/Jakarta' + CRLF + + 'NETRA_LANG=id' + CRLF + + 'NETRA_DATA_DIR=' + ExpandConstant('{commonappdata}\NETRA\data') + CRLF + + '# The private PostgreSQL of this installation' + CRLF + + 'NETRA_PG_BIN=' + ExpandConstant('{app}\pgsql\bin') + CRLF + + 'NETRA_PG_PORT=5433' + CRLF + + '# The license file (or Settings > License in the dashboard)' + CRLF + + 'NETRA_LICENSE_FILE=' + ExpandConstant('{commonappdata}\NETRA\license.lic') + CRLF + + 'NETRA_BACKUP_KEEP_DAYS=14' + CRLF + + '# NETRA_TLS=self-signed' + CRLF + + '# NETRA_OLLAMA_URL=http://localhost:11434' + CRLF, False); + end; +end; diff --git a/packaging/windows/stage.ps1 b/packaging/windows/stage.ps1 new file mode 100644 index 0000000..a2bcf59 --- /dev/null +++ b/packaging/windows/stage.ps1 @@ -0,0 +1,22 @@ +# Stage the files of the Windows installer: netra.exe, WinSW, PostgreSQL binaries, INSTALL.md. +# pwsh packaging/windows/stage.ps1 -Netra netra.exe -Out stage +param( + [Parameter(Mandatory)] [string] $Netra, + [string] $Out = "stage", + [string] $PostgresZip = "https://get.enterprisedb.com/postgresql/postgresql-16.4-1-windows-x64-binaries.zip", + [string] $WinSW = "https://github.com/winsw/winsw/releases/download/v2.12.0/WinSW-x64.exe" +) +$ErrorActionPreference = "Stop" +New-Item -ItemType Directory -Force -Path $Out | Out-Null +Copy-Item $Netra (Join-Path $Out "netra.exe") +Invoke-WebRequest $WinSW -OutFile (Join-Path $Out "netra-service.exe") +$zip = Join-Path $env:RUNNER_TEMP "pgsql.zip" +Invoke-WebRequest $PostgresZip -OutFile $zip +Expand-Archive $zip -DestinationPath $Out -Force +# Only the server and its tools: no pgAdmin, StackBuilder, headers, documentation or symbols. +foreach ($d in "pgAdmin 4", "StackBuilder", "include", "doc", "symbols") { + $p = Join-Path $Out "pgsql\$d" + if (Test-Path $p) { Remove-Item -Recurse -Force $p } +} +Copy-Item (Join-Path $PSScriptRoot "..\..\INSTALL.md") (Join-Path $Out "INSTALL.md") +Get-ChildItem $Out | Format-Table Name, Length diff --git a/scripts/local.sh b/scripts/local.sh index fea7f7f..094e69a 100755 --- a/scripts/local.sh +++ b/scripts/local.sh @@ -253,6 +253,11 @@ cmd_status() { need_env docker compose ps --format 'table {{.Service}}\t{{.State}}\t{{.Status}}' if curl -fsS --max-time 3 "$BASE_URL/healthz" >/dev/null 2>&1; then ok "API healthy"; else bad "API not reachable on $BASE_URL"; fi + if [[ -s license/netra.lic ]]; then + ok "license file license/netra.lic (Settings › License shows what it includes)" + else + warn "no license/netra.lic: the core only, unless a license is installed in Settings › License" + fi } cmd_logs() { docker compose logs -f --tail=150 "$@"; } @@ -429,10 +434,11 @@ cmd_doctor() { else bad "model $model missing (ollama pull $model)" fi - if docker compose exec -T ai python -c "import urllib.request;urllib.request.urlopen('http://host.docker.internal:11434/api/tags',timeout=3)" >/dev/null 2>&1; then - ok "AI container reaches Ollama" + # NETRA calls Ollama from its container (the netra image has no shell; the db one has wget). + if docker compose exec -T db wget -q -O /dev/null -T 3 http://host.docker.internal:11434/api/tags >/dev/null 2>&1; then + ok "containers reach Ollama" else - bad "AI container cannot reach Ollama: brew services stop ollama && OLLAMA_HOST=0.0.0.0 ollama serve" + bad "containers cannot reach Ollama: brew services stop ollama && OLLAMA_HOST=0.0.0.0 ollama serve" fi title "Stack"