diff --git a/.env.example b/.env.example index 7ede38d..8d28312 100644 --- a/.env.example +++ b/.env.example @@ -113,15 +113,6 @@ NETX_UME_NOTIFICATION_TOPIC=ALARM # NETX_OCLAW_FORWARD_MAX_RETRIES=3 # NETX_UME_RAW_JSON_MAX_BYTES=65536 # NETX_NE_COLLECTION_KEEP_DAYS=14 -# Optional: serve built UI from API (same origin). Default web/dist; empty disables. -# NETX_UI_DIST_DIR=web/dist -# --- Windows packaging only (Linux ignores; always use NETX_DATABASE_URL) --- -# NETX_DB_MODE=external -# bundled | external; unset = external (compatible with existing deploys) -# NETX_BUNDLED_PG_PORT=15432 -# NETX_BUNDLED_PG_DATA_DIR= -# NETX_UPDATE_URL= -# NETX_UPDATE_CHANNEL=stable # Heavier fleets: raise CLI/DB together; also ensure Postgres max_connections and bastion session limits. # One-click start (recommended): .\scripts\start_netx.ps1 -Background -WithWeb # -> API + netx_api.worker + N× netx_api.biz_state_worker + optional Vite diff --git a/.github/workflows/release-windows.yml b/.github/workflows/release-windows.yml deleted file mode 100644 index 12e2fa9..0000000 --- a/.github/workflows/release-windows.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Release Windows - -on: - push: - tags: - - "v*" - -permissions: - contents: write - -jobs: - build: - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: "20" - - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Build release zip - shell: pwsh - run: | - powershell -ExecutionPolicy Bypass -File ./packaging/build_release.ps1 -CreateVenv - - - name: Install Inno Setup - shell: pwsh - run: | - choco install innosetup -y --no-progress - echo "C:\Program Files (x86)\Inno Setup 6" >> $env:GITHUB_PATH - - - name: Compile installer - shell: pwsh - run: | - $ver = (Get-Content pyproject.toml | Select-String '^\s*version\s*=').Line -replace '.*"(.+)"','$1' - ISCC.exe "/DMyAppVersion=$ver" packaging/installer/netx.iss - - - name: Upload to GitHub Release - uses: softprops/action-gh-release@v2 - with: - files: | - packaging/release/NetX-*-win64.zip - packaging/release/NetX-Setup-*.exe - generate_release_notes: true diff --git a/.gitignore b/.gitignore index 92197b4..80fd701 100644 --- a/.gitignore +++ b/.gitignore @@ -16,8 +16,3 @@ data/ne_collections/ data/webcrt/ data/auth/ data/runtime/ -# Windows packaging artifacts (binaries / release trees) -packaging/cache/ -packaging/release/ -packaging/postgres/pgsql/ -*.exe diff --git a/PROD_MIN_CHECKLIST.md b/PROD_MIN_CHECKLIST.md index ff8e580..f0d6f64 100644 --- a/PROD_MIN_CHECKLIST.md +++ b/PROD_MIN_CHECKLIST.md @@ -33,4 +33,3 @@ - Validate after restart: - `GET /v1/integrations/status` returns all major components as `up`. - Keep parser config and importer changes versioned and reviewed before release. -- **Windows packaged installs:** program under `%ProgramFiles%\NetX`, data under `%ProgramData%\NetX` (see [packaging/README.md](packaging/README.md)). Prefer `update_netx.ps1` or Setup upgrade so `pgdata` / `.env` are preserved. Linux continues to manage Postgres separately. diff --git a/README.md b/README.md index 2ce7550..2e27e94 100644 --- a/README.md +++ b/README.md @@ -156,12 +156,6 @@ powershell -ExecutionPolicy Bypass -File .\scripts\stop_netx.ps1 -Force Primary web UI (Vite): `http://127.0.0.1:5173/` API base: `http://127.0.0.1:8890/` -After `npm run build` in `web/`, the API can also serve the UI at `http://127.0.0.1:8890/` (same origin). See `NETX_UI_DIST_DIR` in `.env.example`. - -### Windows installer / portable package (optional) - -Fool-proof Windows delivery (bundled or external Postgres, program/data split, manual update) lives under [`packaging/`](packaging/README.md). **Linux installs are unchanged** — keep using your own Postgres and `scripts/start_netx.sh`. - ### 6) MCP(Cursor / oclaw / Claude) 先启动 netx API(§5),再在 **MCP 宿主同机** 安装轻量客户端并配置。 diff --git a/netx_api/auth_middleware.py b/netx_api/auth_middleware.py index 3375420..d6c9bd3 100644 --- a/netx_api/auth_middleware.py +++ b/netx_api/auth_middleware.py @@ -35,18 +35,6 @@ _PUBLIC_PREFIXES = ( "/assets", ) -# Authenticated API / infra only. Everything else (SPA routes, favicon, …) is public. -def _is_api_auth_path(path: str) -> bool: - p = str(path or "") - if p.startswith("/v1/"): - return True - if p in ("/metrics", "/metrics/json", "/v1/metrics/json", "/openapi.json"): - return True - if p.startswith("/docs") or p.startswith("/redoc"): - return True - return False - - # While must_change_password is true, only these authenticated endpoints are allowed. _PASSWORD_CHANGE_ALLOW = frozenset( { @@ -72,12 +60,7 @@ def _is_public(path: str) -> bool: or p.startswith("/redoc") ): return True - if any(p.startswith(pref) for pref in _PUBLIC_PREFIXES): - return True - # Bundled UI routes (/login, /topology, …) must load without a token. - if not _is_api_auth_path(p): - return True - return False + return any(p.startswith(pref) for pref in _PUBLIC_PREFIXES) def _client_ip(request: Request) -> str: diff --git a/netx_api/config.py b/netx_api/config.py index 1126fc5..0fb1a12 100644 --- a/netx_api/config.py +++ b/netx_api/config.py @@ -23,8 +23,6 @@ class Settings(BaseSettings): source_type: str = "gateway_export_excel" parser_config: str = "netx_api/config/parsers/zte_alarm_monitor_v1.yaml" frontend_url: str = "http://127.0.0.1:5173" - # Built Vite UI (web/dist). Empty / missing → API-only root JSON (dev with Vite). - ui_dist_dir: str = "web/dist" oclaw_analyze_url: str = "http://127.0.0.1:8787/admin/api/ops-ai/analyze-sync" oclaw_analyze_token: str = "" oclaw_health_url: str = "http://127.0.0.1:8787/admin/api/ops-ai/health" diff --git a/netx_api/main.py b/netx_api/main.py index e56f91d..98eb06f 100644 --- a/netx_api/main.py +++ b/netx_api/main.py @@ -95,21 +95,16 @@ def health() -> dict[str, str]: return {"status": "ok"} -from .ui_static import mount_ui_if_present # noqa: E402 - -_UI_MOUNTED = mount_ui_if_present(app) -if not _UI_MOUNTED: - - @app.get("/") - def root() -> dict: - return { - "ok": True, - "mode": "api_only", - "message": "netx UI is served by Vite frontend only", - "frontend_url": settings.frontend_url, - "api_health": "/health", - "api_status": "/v1/integrations/status", - } +@app.get("/") +def root() -> dict: + return { + "ok": True, + "mode": "api_only", + "message": "netx UI is served by Vite frontend only", + "frontend_url": settings.frontend_url, + "api_health": "/health", + "api_status": "/v1/integrations/status", + } if __name__ == "__main__": diff --git a/netx_api/ui_static.py b/netx_api/ui_static.py deleted file mode 100644 index e606adf..0000000 --- a/netx_api/ui_static.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Optional SPA hosting from a Vite build (web/dist).""" - -from __future__ import annotations - -import logging -from pathlib import Path - -from fastapi import FastAPI -from fastapi.responses import FileResponse -from fastapi.staticfiles import StaticFiles -from starlette.requests import Request -from starlette.responses import Response - -from .config import settings - -_log = logging.getLogger("netx.ui") - -# Paths that must never fall through to index.html. -_API_PREFIXES = ( - "/v1/", - "/docs", - "/redoc", - "/openapi.json", - "/metrics", - "/health", -) - - -def resolve_ui_dist() -> Path | None: - raw = str(getattr(settings, "ui_dist_dir", "") or "").strip() - if not raw: - return None - p = Path(raw) - if not p.is_absolute(): - p = Path(__file__).resolve().parents[1] / p - index = p / "index.html" - if index.is_file(): - return p - return None - - -def _is_api_or_infra(path: str) -> bool: - p = path or "/" - if p == "/health" or p.startswith("/health/"): - return True - if p in ("/metrics", "/metrics/json", "/openapi.json"): - return True - return any(p == pref.rstrip("/") or p.startswith(pref) for pref in _API_PREFIXES) - - -def mount_ui_if_present(app: FastAPI) -> bool: - """Mount /assets + SPA fallback when ui_dist_dir has index.html. Returns True if mounted.""" - dist = resolve_ui_dist() - if dist is None: - return False - - assets = dist / "assets" - if assets.is_dir(): - app.mount("/assets", StaticFiles(directory=str(assets)), name="ui_assets") - - index_path = dist / "index.html" - - @app.get("/") - async def spa_root() -> FileResponse: - return FileResponse(index_path) - - @app.api_route("/{full_path:path}", methods=["GET", "HEAD"], include_in_schema=False) - async def spa_fallback(full_path: str, request: Request) -> Response: - # Let API / infra 404s stay as API 404s (this route is last). - path = "/" + (full_path or "").lstrip("/") - if _is_api_or_infra(path): - return Response(status_code=404, content=b'{"detail":"Not Found"}', media_type="application/json") - # Prefer real files under dist (favicon, robots, etc.) - candidate = dist / full_path - try: - candidate.resolve().relative_to(dist.resolve()) - except ValueError: - return FileResponse(index_path) - if candidate.is_file(): - return FileResponse(candidate) - return FileResponse(index_path) - - _log.info("UI mounted from %s", dist) - return True diff --git a/packaging/README.md b/packaging/README.md deleted file mode 100644 index 4f22d6f..0000000 --- a/packaging/README.md +++ /dev/null @@ -1,97 +0,0 @@ -# NetX Windows packaging - -**Linux is unchanged:** keep using your own PostgreSQL and `NETX_DATABASE_URL` with `scripts/start_netx.sh`. Nothing under this folder is required on Linux. - -This directory builds a Windows deliverable with: - -- Optional **bundled** portable PostgreSQL **or** **external** existing Postgres -- API-hosted UI (`web/dist`) — no separate Vite process for end users -- Program / data split so upgrades do not wipe the database -- Manual update script + auto-update **manifest contract** (fetch not implemented yet) - -## What users get - -| Artifact | How | -|----------|-----| -| `NetX-x.y.z-win64.zip` | `build_release.ps1` | -| `NetX-Setup-x.y.z.exe` | Compile `installer/netx.iss` with [Inno Setup](https://jrsoftware.org/isinfo.php) after staging | - -## Build (developer machine) - -Prerequisites: Python 3.11+, Node 20+, PowerShell, network (to download PG binaries once). - -```powershell -cd netx -# Optional: download portable Postgres into packaging\postgres\pgsql -powershell -ExecutionPolicy Bypass -File .\packaging\download_postgres.ps1 - -# Build web + stage + zip (-CreateVenv ships a ready .venv; large) -powershell -ExecutionPolicy Bypass -File .\packaging\build_release.ps1 -CreateVenv -``` - -Output: - -- `packaging/release/netx-win64/` — stage tree -- `packaging/release/NetX--win64.zip` - -Inno Setup: - -```text -ISCC.exe packaging\installer\netx.iss -``` - -Override version in the `.iss` or edit `#define MyAppVersion`. - -## End-user install - -### Setup.exe - -1. Run `NetX-Setup-x.y.z.exe` (admin). -2. Files → `%ProgramFiles%\NetX\` (program root). -3. Data → `%ProgramData%\NetX\` (`.env`, `pgdata`, spool, secrets). -4. Optional post-install task runs `setup_first_run.ps1` (choose bundled vs external DB). -5. Start menu: **Start NetX** / **Stop NetX** / **Open NetX UI**. - -### Zip (portable) - -1. Unpack anywhere. -2. Marker `.portable` → data root is sibling `NetXData\`. -3. Target needs **Python 3.11+** on PATH unless the zip was built with `-CreateVenv`. -4. Run: - -```powershell -.\packaging\setup_first_run.ps1 -.\packaging\start_netx_app.ps1 -``` - -## Database modes - -| Mode | Behavior | -|------|----------| -| `bundled` | Start portable PG on `127.0.0.1:15432`, data in data-root `pgdata\` | -| `external` | Do not start PG; use `NETX_DATABASE_URL` (same as today / Linux) | -| unset | Treated as **external** | - -Existing Windows deploys that only set `NETX_DATABASE_URL` keep working: never set `NETX_DB_MODE=bundled` unless you want the portable engine. - -## Manual update - -```powershell -.\packaging\update_netx.ps1 -PackagePath .\NetX-0.3.0-win64.zip -``` - -Stops services, replaces program folders (`netx_api`, `web`, `packaging`, `postgres`, …), **keeps** the data root, restarts. Schema migrations still run via Alembic on API start. - -## Auto-update (reserved) - -See `manifest.example.json`. Future: set `NETX_UPDATE_URL` + `NETX_UPDATE_CHANNEL`; a checker will download the zip and call `update_netx.ps1`. Not implemented in this phase. - -## Layout reminder - -``` -Program root (replaceable) Data root (never wiped by update) - netx_api\ web\dist\ .env - postgres\pgsql\ pgdata\ (bundled only) - packaging\ scripts\ data\auth\ data\runtime\ - version.json backups\ -``` diff --git a/packaging/_common.ps1 b/packaging/_common.ps1 deleted file mode 100644 index 4a1ef66..0000000 --- a/packaging/_common.ps1 +++ /dev/null @@ -1,137 +0,0 @@ -# Shared path helpers for Windows packaging scripts. -# Dot-source: . "$PSScriptRoot\_common.ps1" - -$ErrorActionPreference = "Stop" - -function Get-NetxRepoRoot { - return (Resolve-Path (Join-Path $PSScriptRoot "..")).Path -} - -function Get-NetxProgramRoot { - param([string]$Override = "") - if ($Override) { return (Resolve-Path $Override).Path } - if ($env:NETX_PROGRAM_ROOT) { return $env:NETX_PROGRAM_ROOT } - # Packaging scripts live in \packaging when installed; in-repo they live under netx\packaging. - $parent = Split-Path -Parent $PSScriptRoot - return $parent -} - -function Get-NetxDataRoot { - param( - [string]$ProgramRoot, - [string]$Override = "" - ) - if ($Override) { return $Override } - if ($env:NETX_DATA_ROOT) { return $env:NETX_DATA_ROOT } - $programData = [Environment]::GetFolderPath("CommonApplicationData") - $defaultPd = Join-Path $programData "NetX" - # Portable layout: prefer sibling NetXData next to program root when marker exists or Program Files not writable. - $portable = Join-Path (Split-Path -Parent $ProgramRoot) "NetXData" - if (Test-Path (Join-Path $ProgramRoot ".portable")) { - return $portable - } - if (Test-Path $defaultPd) { - return $defaultPd - } - # Dev / first run from repo: use repo-local data packaging area under ProgramData if possible. - try { - if (-not (Test-Path $defaultPd)) { - New-Item -ItemType Directory -Path $defaultPd -Force -ErrorAction Stop | Out-Null - } - return $defaultPd - } catch { - return $portable - } -} - -function Get-NetxVersion { - param([string]$ProgramRoot) - $vj = Join-Path $ProgramRoot "version.json" - if (Test-Path $vj) { - try { - $j = Get-Content -Raw -Path $vj | ConvertFrom-Json - if ($j.version) { return [string]$j.version } - } catch {} - } - $toml = Join-Path $ProgramRoot "pyproject.toml" - if (-not (Test-Path $toml)) { - $toml = Join-Path (Get-NetxRepoRoot) "pyproject.toml" - } - if (Test-Path $toml) { - $m = Select-String -Path $toml -Pattern '^\s*version\s*=\s*"([^"]+)"' | Select-Object -First 1 - if ($m) { return $m.Matches[0].Groups[1].Value } - } - return "0.0.0" -} - -function Read-DotEnv { - param([string]$Path) - $map = @{} - if (-not (Test-Path $Path)) { return $map } - Get-Content -Path $Path -Encoding utf8 | ForEach-Object { - $line = $_.Trim() - if (-not $line -or $line.StartsWith("#")) { return } - $i = $line.IndexOf("=") - if ($i -lt 1) { return } - $k = $line.Substring(0, $i).Trim() - $v = $line.Substring($i + 1).Trim() - if (($v.StartsWith('"') -and $v.EndsWith('"')) -or ($v.StartsWith("'") -and $v.EndsWith("'"))) { - $v = $v.Substring(1, $v.Length - 2) - } - $map[$k] = $v - } - return $map -} - -function Write-DotEnvValue { - param( - [string]$Path, - [hashtable]$Values - ) - $lines = @() - $seen = @{} - if (Test-Path $Path) { - Get-Content -Path $Path -Encoding utf8 | ForEach-Object { - $line = $_ - $t = $line.Trim() - if ($t -and -not $t.StartsWith("#") -and $t.Contains("=")) { - $k = $t.Substring(0, $t.IndexOf("=")).Trim() - if ($Values.ContainsKey($k)) { - $lines += "$k=$($Values[$k])" - $seen[$k] = $true - return - } - } - $lines += $line - } - } - foreach ($k in $Values.Keys) { - if (-not $seen.ContainsKey($k)) { - $lines += "$k=$($Values[$k])" - } - } - $dir = Split-Path -Parent $Path - if (-not (Test-Path $dir)) { - New-Item -ItemType Directory -Path $dir -Force | Out-Null - } - Set-Content -Path $Path -Value ($lines -join "`r`n") -Encoding utf8 -} - -function Get-DbMode { - param([hashtable]$EnvMap) - $m = "" - if ($EnvMap.ContainsKey("NETX_DB_MODE")) { $m = [string]$EnvMap["NETX_DB_MODE"] } - if (-not $m -and $env:NETX_DB_MODE) { $m = $env:NETX_DB_MODE } - $m = $m.Trim().ToLowerInvariant() - if ($m -eq "bundled") { return "bundled" } - return "external" -} - -function Import-NetxEnvFile { - param([string]$Path) - $map = Read-DotEnv -Path $Path - foreach ($k in $map.Keys) { - Set-Item -Path "Env:$k" -Value $map[$k] - } - return $map -} diff --git a/packaging/build_release.ps1 b/packaging/build_release.ps1 deleted file mode 100644 index a347f03..0000000 --- a/packaging/build_release.ps1 +++ /dev/null @@ -1,124 +0,0 @@ -param( - [string]$Version = "", - [string]$OutDir = "", - [switch]$SkipWebBuild = $false, - [switch]$SkipPostgresDownload = $false, - [switch]$SkipZip = $false, - [switch]$CreateVenv = $false -) - -$ErrorActionPreference = "Stop" -. "$PSScriptRoot\_common.ps1" - -$repo = Get-NetxRepoRoot -if (-not $Version) { - $Version = Get-NetxVersion -ProgramRoot $repo -} -$stage = if ($OutDir) { $OutDir } else { Join-Path $PSScriptRoot "release\netx-win64" } - -Write-Host "==> Building NetX Windows release $Version" -Write-Host " Repo: $repo" -Write-Host " Stage: $stage" - -if (Test-Path $stage) { - Remove-Item -Recurse -Force $stage -} -New-Item -ItemType Directory -Path $stage -Force | Out-Null - -$webRoot = Join-Path $repo "web" -$dist = Join-Path $webRoot "dist" -if (-not $SkipWebBuild) { - if (-not (Get-Command npm.cmd -ErrorAction SilentlyContinue)) { - throw "npm_not_found" - } - Write-Host "==> npm build" - if (-not (Test-Path (Join-Path $webRoot "node_modules"))) { - & npm.cmd install --prefix $webRoot - if ($LASTEXITCODE -ne 0) { throw "npm_install_failed" } - } - # Release packaging skips tsc (dev CI still uses `npm run build`). - & npm.cmd run build:release --prefix $webRoot - if ($LASTEXITCODE -ne 0) { throw "web_build_failed" } -} -if (-not (Test-Path (Join-Path $dist "index.html"))) { - throw "web_dist_missing: build web/ or drop -SkipWebBuild" -} - -$pgsql = Join-Path $PSScriptRoot "postgres\pgsql" -if (-not $SkipPostgresDownload) { - if (-not (Test-Path (Join-Path $pgsql "bin\pg_ctl.exe"))) { - & powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "download_postgres.ps1") - } -} - -# Flat layout = same as repo so scripts\start_netx.ps1 works unchanged. -foreach ($d in @("netx_api", "alembic", "scripts")) { - Copy-Item -Path (Join-Path $repo $d) -Destination (Join-Path $stage $d) -Recurse -Force -} -foreach ($f in @("pyproject.toml", "requirements.txt", "alembic.ini", "README.md", "LICENSE", "CONTRIBUTING.md", "SECURITY.md")) { - $src = Join-Path $repo $f - if (Test-Path $src) { - Copy-Item -Path $src -Destination (Join-Path $stage $f) -Force - } -} - -$webOut = Join-Path $stage "web\dist" -New-Item -ItemType Directory -Path (Split-Path $webOut -Parent) -Force | Out-Null -Copy-Item -Path $dist -Destination $webOut -Recurse -Force - -$packOut = Join-Path $stage "packaging" -New-Item -ItemType Directory -Path $packOut -Force | Out-Null -Copy-Item -Path (Join-Path $PSScriptRoot "_common.ps1") -Destination $packOut -Force -foreach ($name in @( - "download_postgres.ps1", "setup_first_run.ps1", "start_netx_app.ps1", - "stop_netx_app.ps1", "update_netx.ps1", "build_release.ps1", - "README.md", "manifest.example.json" - )) { - $src = Join-Path $PSScriptRoot $name - if (Test-Path $src) { Copy-Item $src (Join-Path $packOut $name) -Force } -} -Copy-Item -Path (Join-Path $PSScriptRoot "config") -Destination (Join-Path $packOut "config") -Recurse -Force -Copy-Item -Path (Join-Path $PSScriptRoot "installer") -Destination (Join-Path $packOut "installer") -Recurse -Force -New-Item -ItemType Directory -Path (Join-Path $packOut "postgres") -Force | Out-Null -Copy-Item -Path (Join-Path $PSScriptRoot "postgres\README.md") -Destination (Join-Path $packOut "postgres\README.md") -Force - -if (Test-Path (Join-Path $pgsql "bin\pg_ctl.exe")) { - Write-Host "==> Copying bundled PostgreSQL" - New-Item -ItemType Directory -Path (Join-Path $stage "postgres") -Force | Out-Null - Copy-Item -Path $pgsql -Destination (Join-Path $stage "postgres\pgsql") -Recurse -Force -} - -$builtAt = (Get-Date).ToUniversalTime().ToString("o") -@{ - version = $Version - channel = "stable" - min_data_layout = 1 - built_at = $builtAt - platform = "win64" -} | ConvertTo-Json | Set-Content -Path (Join-Path $stage "version.json") -Encoding utf8 - -Set-Content -Path (Join-Path $stage ".portable") -Value "1" -Encoding ascii - -if ($CreateVenv) { - Write-Host "==> Creating .venv in stage (requires Python 3.11+ on PATH)" - $py = (Get-Command python -ErrorAction SilentlyContinue) - if (-not $py) { throw "python_not_found_for_venv" } - & $py.Source -m venv (Join-Path $stage ".venv") - & (Join-Path $stage ".venv\Scripts\python.exe") -m pip install --upgrade pip - & (Join-Path $stage ".venv\Scripts\python.exe") -m pip install -r (Join-Path $stage "requirements.txt") -} - -Write-Host "==> Stage ready: $stage" -ForegroundColor Green - -if (-not $SkipZip) { - $zip = Join-Path (Split-Path -Parent $stage) "NetX-$Version-win64.zip" - if (Test-Path $zip) { Remove-Item -Force $zip } - Compress-Archive -Path $stage -DestinationPath $zip -Force - Write-Host "==> Zip: $zip" -ForegroundColor Green -} - -Write-Host "" -Write-Host "Ship options:" -Write-Host " Zip: user unpacks, runs packaging\setup_first_run.ps1 then start_netx_app.ps1" -Write-Host " Exe: compile packaging\installer\netx.iss with Inno Setup (needs stage above)" -Write-Host "Python: install 3.11+ on target, or rebuild with -CreateVenv and ship .venv" diff --git a/packaging/config/database.example.env b/packaging/config/database.example.env deleted file mode 100644 index a9d072a..0000000 --- a/packaging/config/database.example.env +++ /dev/null @@ -1,9 +0,0 @@ -# NETX_DB_MODE=bundled -# NETX_DATABASE_URL=postgresql+psycopg://netx:CHANGE_ME@127.0.0.1:15432/netx -# NETX_BUNDLED_PG_PORT=15432 -# NETX_BUNDLED_PG_DATA_DIR= -# NETX_HOST=127.0.0.1 -# NETX_PORT=8890 -# NETX_UI_DIST_DIR=web/dist -# NETX_UPDATE_URL= -# NETX_UPDATE_CHANNEL=stable diff --git a/packaging/download_postgres.ps1 b/packaging/download_postgres.ps1 deleted file mode 100644 index 38c7b41..0000000 --- a/packaging/download_postgres.ps1 +++ /dev/null @@ -1,57 +0,0 @@ -param( - [string]$Version = "16.4-1", - [string]$OutDir = "" -) - -$ErrorActionPreference = "Stop" -. "$PSScriptRoot\_common.ps1" - -# EnterpriseDB binary zip (Windows x86-64). Override with -Version if the URL 404s. -$ver = $Version -$url = "https://get.enterprisedb.com/postgresql/postgresql-$ver-windows-x64-binaries.zip" - -$cache = if ($OutDir) { $OutDir } else { Join-Path $PSScriptRoot "cache" } -$zipPath = Join-Path $cache "postgresql-$ver-windows-x64-binaries.zip" -$extractRoot = Join-Path $cache "pgsql-$ver" -$destPgsql = Join-Path $PSScriptRoot "postgres\pgsql" - -if (-not (Test-Path $cache)) { - New-Item -ItemType Directory -Path $cache -Force | Out-Null -} - -Write-Host "==> PostgreSQL binaries URL: $url" -if (-not (Test-Path $zipPath)) { - Write-Host "==> Downloading (large; may take several minutes)..." - Invoke-WebRequest -Uri $url -OutFile $zipPath -UseBasicParsing -} else { - Write-Host "==> Using cached zip: $zipPath" -} - -if (Test-Path $extractRoot) { - Remove-Item -Recurse -Force $extractRoot -} -New-Item -ItemType Directory -Path $extractRoot -Force | Out-Null -Write-Host "==> Extracting..." -Expand-Archive -Path $zipPath -DestinationPath $extractRoot -Force - -# Zip usually contains a top-level "pgsql" folder. -$found = Get-ChildItem -Path $extractRoot -Recurse -Filter "pg_ctl.exe" -ErrorAction SilentlyContinue | - Select-Object -First 1 -if (-not $found) { - throw "pg_ctl.exe not found after extract; check zip layout" -} -$pgsqlSrc = Split-Path -Parent (Split-Path -Parent $found.FullName) -Write-Host "==> Found pgsql tree: $pgsqlSrc" - -if (Test-Path $destPgsql) { - Remove-Item -Recurse -Force $destPgsql -} -New-Item -ItemType Directory -Path (Split-Path -Parent $destPgsql) -Force | Out-Null -Copy-Item -Path $pgsqlSrc -Destination $destPgsql -Recurse -Force - -$pgCtl = Join-Path $destPgsql "bin\pg_ctl.exe" -if (-not (Test-Path $pgCtl)) { - throw "install_failed: missing $pgCtl" -} -Write-Host "==> Bundled PostgreSQL ready: $destPgsql" -ForegroundColor Green -Write-Host " Tip: data directory is NOT here; setup_first_run / start scripts use the NetX data root." diff --git a/packaging/installer/netx.iss b/packaging/installer/netx.iss deleted file mode 100644 index 55090ea..0000000 --- a/packaging/installer/netx.iss +++ /dev/null @@ -1,69 +0,0 @@ -; NetX Windows installer (Inno Setup 6+) -; Compile after: packaging\build_release.ps1 -; ISCC.exe packaging\installer\netx.iss - -#define MyAppName "NetX" -#ifndef MyAppVersion - #define MyAppVersion "0.3.0" -#endif -#define MyAppPublisher "NetX" -#define MyAppURL "https://github.com/hansjone/netx" -#define MyAppExeName "packaging\start_netx_app.ps1" - -; Stage directory produced by build_release.ps1 (relative to this .iss) -#define StageDir "..\release\netx-win64" - -[Setup] -AppId={{A7E3C2D1-9F40-4B8E-9C1A-NETXWIN64001} -AppName={#MyAppName} -AppVersion={#MyAppVersion} -AppPublisher={#MyAppPublisher} -AppPublisherURL={#MyAppURL} -DefaultDirName={autopf}\NetX -DefaultGroupName=NetX -DisableProgramGroupPage=yes -LicenseFile={#StageDir}\LICENSE -OutputDir=..\release -OutputBaseFilename=NetX-Setup-{#MyAppVersion} -Compression=lzma2 -SolidCompression=yes -WizardStyle=modern -ArchitecturesAllowed=x64compatible -ArchitecturesInstallIn64BitMode=x64compatible -PrivilegesRequired=admin -; Data lives under {commonappdata}\NetX — not overwritten by upgrades -CloseApplications=yes - -[Languages] -Name: "english"; MessagesFile: "compiler:Default.isl" - -[Tasks] -Name: "desktopicon"; Description: "Create a desktop shortcut"; GroupDescription: "Additional icons:"; Flags: unchecked -Name: "firstrun"; Description: "Run first-time setup (database mode) after install"; GroupDescription: "Setup:"; Flags: checkedonce - -[Files] -; Entire release stage → {app}. Exclude .portable so installed builds use ProgramData. -Source: "{#StageDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Excludes: ".portable" - -[Dirs] -Name: "{commonappdata}\NetX" -Name: "{commonappdata}\NetX\data" -Name: "{commonappdata}\NetX\data\auth" -Name: "{commonappdata}\NetX\data\runtime" -Name: "{commonappdata}\NetX\pgdata" -Name: "{commonappdata}\NetX\backups" - -[Icons] -Name: "{group}\Start NetX"; Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\start_netx_app.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}" -Name: "{group}\Stop NetX"; Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\stop_netx_app.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}" -Name: "{group}\Open NetX UI"; Filename: "http://127.0.0.1:8890/" -Name: "{group}\First-time setup"; Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\setup_first_run.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}" -Name: "{autodesktop}\Start NetX"; Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\start_netx_app.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"; Tasks: desktopicon - -[Run] -Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\setup_first_run.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"; Flags: postinstall skipifsilent; Tasks: firstrun -Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\packaging\start_netx_app.ps1"" -ProgramRoot ""{app}"" -DataRoot ""{commonappdata}\NetX"""; WorkingDir: "{app}"; Description: "Start NetX now"; Flags: postinstall nowait skipifsilent unchecked - -[UninstallDelete] -; Do NOT delete {commonappdata}\NetX — preserves DB and secrets across reinstall -Type: filesandordirs; Name: "{app}" diff --git a/packaging/manifest.example.json b/packaging/manifest.example.json deleted file mode 100644 index 9f51c14..0000000 --- a/packaging/manifest.example.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "channel": "stable", - "latest": "0.3.0", - "min_compatible": "0.3.0", - "notes_url": "", - "windows": { - "url": "https://example.com/netx/NetX-0.2.0-win64.zip", - "sha256": "REPLACE_WITH_SHA256", - "size": 0 - } -} diff --git a/packaging/postgres/README.md b/packaging/postgres/README.md deleted file mode 100644 index fcda877..0000000 --- a/packaging/postgres/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Windows packaging — portable PostgreSQL binaries - -Place an extracted EnterpriseDB Windows x64 PostgreSQL tree here as `pgsql/` so that: - -``` -packaging/postgres/pgsql/bin/pg_ctl.exe -packaging/postgres/pgsql/bin/initdb.exe -packaging/postgres/pgsql/bin/psql.exe -``` - -Do not commit the binaries. Use: - -```powershell -powershell -ExecutionPolicy Bypass -File .\packaging\download_postgres.ps1 -``` - -Bundled mode uses a separate data directory under the NetX **data root** (never under `pgsql/`), so upgrading the binary tree does not wipe the database. diff --git a/packaging/publish_release.ps1 b/packaging/publish_release.ps1 deleted file mode 100644 index ab97531..0000000 --- a/packaging/publish_release.ps1 +++ /dev/null @@ -1,102 +0,0 @@ -param( - [string]$Version = "", - [switch]$SkipBuild = $false, - [switch]$SkipInstaller = $false, - [switch]$SkipGitHub = $false, - [switch]$Draft = $false -) - -$ErrorActionPreference = "Stop" -. "$PSScriptRoot\_common.ps1" - -$repo = Get-NetxRepoRoot -if (-not $Version) { - $Version = Get-NetxVersion -ProgramRoot $repo -} -$tag = "v$Version" -$releaseDir = Join-Path $PSScriptRoot "release" -$zip = Join-Path $releaseDir "NetX-$Version-win64.zip" -$setup = Join-Path $releaseDir "NetX-Setup-$Version.exe" - -Write-Host "==> NetX publish $tag" - -if (-not $SkipBuild) { - & powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "build_release.ps1") ` - -Version $Version -CreateVenv -} - -if (-not (Test-Path $zip)) { - throw "missing_zip: $zip" -} - -if (-not $SkipInstaller) { - $isccCmd = Get-Command ISCC.exe -ErrorAction SilentlyContinue - $isccCandidates = @( - $(if ($isccCmd) { $isccCmd.Source }), - "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe", - "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe", - "$env:ProgramFiles\Inno Setup 6\ISCC.exe" - ) | Where-Object { $_ -and (Test-Path $_) } - $iscc = $isccCandidates | Select-Object -First 1 - if (-not $iscc) { - Write-Host "[WARN] Inno Setup (ISCC) not found. Install: winget install JRSoftware.InnoSetup" -ForegroundColor Yellow - Write-Host " Skipping Setup.exe; zip-only release." - } else { - Write-Host "==> Compiling installer: $iscc" - & $iscc "/DMyAppVersion=$Version" (Join-Path $PSScriptRoot "installer\netx.iss") - if (-not (Test-Path $setup)) { - throw "installer_build_failed: expected $setup" - } - Write-Host "==> Setup.exe: $setup" -ForegroundColor Green - } -} - -if ($SkipGitHub) { - Write-Host "==> Skip GitHub release (-SkipGitHub)" - exit 0 -} - -if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { - throw "gh_cli_not_found" -} - -Set-Location $repo -$existing = gh release view $tag 2>$null -if ($LASTEXITCODE -eq 0) { - Write-Host "==> Release $tag exists; uploading assets" - gh release upload $tag $zip --clobber - if (Test-Path $setup) { - gh release upload $tag $setup --clobber - } -} else { - $notes = @" -## NetX $Version — first Windows installable release - -### Downloads -- **NetX-Setup-$Version.exe** — recommended installer (Program Files + ProgramData) -- **NetX-$Version-win64.zip** — portable directory package - -### Requirements -- Windows 10/11 x64 -- No separate Python or PostgreSQL install required (bundled in package) - -### Quick start (installer) -1. Run ``NetX-Setup-$Version.exe`` as administrator. -2. Complete first-time setup (choose **bundled** or **external** PostgreSQL). -3. Start menu → **Start NetX** → browser opens ``http://127.0.0.1:8890/`` -4. Default login: ``admin`` / ``admin123`` (change after first login) - -### Linux -Unchanged — use your own PostgreSQL and ``scripts/start_netx.sh``. - -See [packaging/README.md](https://github.com/hansjone/netx/blob/main/packaging/README.md) for details. -"@ - $args = @("release", "create", $tag, "--title", "NetX $Version", "--notes", $notes) - if ($Draft) { $args += "--draft" } - $args += $zip - if (Test-Path $setup) { $args += $setup } - & gh @args -} - -Write-Host "==> Published $tag" -ForegroundColor Green -gh release view $tag --web 2>$null diff --git a/packaging/setup_first_run.ps1 b/packaging/setup_first_run.ps1 deleted file mode 100644 index fa8e0f5..0000000 --- a/packaging/setup_first_run.ps1 +++ /dev/null @@ -1,185 +0,0 @@ -param( - [ValidateSet("bundled", "external", "")] - [string]$DbMode = "", - [string]$ProgramRoot = "", - [string]$DataRoot = "", - [string]$ExternalDatabaseUrl = "", - [string]$BundledPassword = "", - [int]$BundledPort = 15432, - [switch]$NonInteractive = $false -) - -$ErrorActionPreference = "Stop" -. "$PSScriptRoot\_common.ps1" - -$prog = Get-NetxProgramRoot -Override $ProgramRoot -$data = Get-NetxDataRoot -ProgramRoot $prog -Override $DataRoot -$envPath = Join-Path $data ".env" - -Write-Host "==> Program root: $prog" -Write-Host "==> Data root: $data" -Write-Host "==> Env file: $envPath" - -if (-not (Test-Path $data)) { - New-Item -ItemType Directory -Path $data -Force | Out-Null -} -foreach ($sub in @("data", "data\auth", "data\runtime", "backups", "pgdata")) { - $p = Join-Path $data $sub - if (-not (Test-Path $p)) { - New-Item -ItemType Directory -Path $p -Force | Out-Null - } -} - -$existing = Read-DotEnv -Path $envPath -if (-not $DbMode) { - if ($NonInteractive) { - if ($existing.ContainsKey("NETX_DB_MODE")) { - $DbMode = $existing["NETX_DB_MODE"] - } elseif ($existing.ContainsKey("NETX_DATABASE_URL")) { - $DbMode = "external" - } else { - $DbMode = "bundled" - } - } else { - Write-Host "" - Write-Host "Choose database mode:" - Write-Host " 1) bundled — use NetX portable PostgreSQL (default for new installs)" - Write-Host " 2) external — connect to an existing PostgreSQL (Linux / already deployed)" - $choice = Read-Host "Enter 1 or 2" - if ($choice -eq "2") { $DbMode = "external" } else { $DbMode = "bundled" } - } -} -$DbMode = $DbMode.Trim().ToLowerInvariant() -if ($DbMode -ne "bundled" -and $DbMode -ne "external") { - throw "invalid_db_mode: $DbMode" -} - -$values = @{} -$values["NETX_DB_MODE"] = $DbMode -$values["NETX_HOST"] = "127.0.0.1" -$values["NETX_PORT"] = "8890" -$values["NETX_UI_DIST_DIR"] = "web/dist" - -# Point runtime data dirs into the data root (absolute). -$values["NETX_AUTH_MCP_TOKEN_FILE"] = ((Join-Path $data "data\auth\mcp_token") -replace '\\', '/') -$values["NETX_SCHEDULER_HEARTBEAT_PATH"] = ((Join-Path $data "data\runtime\scheduler_heartbeat.json") -replace '\\', '/') - -if ($DbMode -eq "bundled") { - $pgsql = Join-Path $prog "postgres\pgsql" - if (-not (Test-Path (Join-Path $pgsql "bin\initdb.exe"))) { - # In-repo layout - $alt = Join-Path $PSScriptRoot "postgres\pgsql" - if (Test-Path (Join-Path $alt "bin\initdb.exe")) { - $pgsql = $alt - } else { - throw "bundled_postgres_missing: run packaging\download_postgres.ps1 first (expected $pgsql)" - } - } - if (-not $BundledPassword) { - if ($NonInteractive) { - $BundledPassword = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 24 | ForEach-Object { [char]$_ }) - } else { - $sec = Read-Host -Prompt "Password for bundled role 'netx' (empty = auto-generate)" -AsSecureString - $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec) - try { - $BundledPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) - } finally { - [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) - } - if (-not $BundledPassword) { - $BundledPassword = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 24 | ForEach-Object { [char]$_ }) - Write-Host "Generated password (saved in .env only)." - } - } - } - $pgData = Join-Path $data "pgdata" - $values["NETX_BUNDLED_PG_PORT"] = "$BundledPort" - $values["NETX_BUNDLED_PG_DATA_DIR"] = ($pgData -replace '\\', '/') - $pwFile = Join-Path $data "pg_netx.pw" - Set-Content -Path $pwFile -Value $BundledPassword -Encoding ascii -NoNewline - $encPw = [uri]::EscapeDataString($BundledPassword) - $values["NETX_DATABASE_URL"] = "postgresql+psycopg://netx:${encPw}@127.0.0.1:${BundledPort}/netx" - - # Initialize cluster if needed - $initdb = Join-Path $pgsql "bin\initdb.exe" - $pgCtl = Join-Path $pgsql "bin\pg_ctl.exe" - $psql = Join-Path $pgsql "bin\psql.exe" - $createdb = Join-Path $pgsql "bin\createdb.exe" - $createuser = Join-Path $pgsql "bin\createuser.exe" - - if (-not (Test-Path (Join-Path $pgData "PG_VERSION"))) { - Write-Host "==> initdb $pgData" - $pwSuper = Join-Path $data "pg_super.pw" - Set-Content -Path $pwSuper -Value $BundledPassword -Encoding ascii -NoNewline - & $initdb -D $pgData -U postgres -A password --pwfile=$pwSuper -E UTF8 --locale=C - if ($LASTEXITCODE -ne 0) { throw "initdb_failed" } - } - - # Ensure listen / port in postgresql.conf - $conf = Join-Path $pgData "postgresql.conf" - $hba = Join-Path $pgData "pg_hba.conf" - if (Test-Path $conf) { - $confText = Get-Content -Raw -Path $conf - if ($confText -notmatch "(?m)^\s*port\s*=") { - Add-Content -Path $conf -Value "`nport = $BundledPort`nlisten_addresses = '127.0.0.1'`n" - } else { - $confText = $confText -replace '(?m)^\s*port\s*=\s*\d+', "port = $BundledPort" - if ($confText -notmatch "(?m)^\s*listen_addresses\s*=") { - $confText += "`nlisten_addresses = '127.0.0.1'`n" - } - Set-Content -Path $conf -Value $confText -Encoding utf8 - } - } - if (Test-Path $hba) { - $hbaText = Get-Content -Raw -Path $hba - if ($hbaText -notmatch "127\.0\.0\.1/32") { - Add-Content -Path $hba -Value "`nhost all all 127.0.0.1/32 scram-sha-256`n" - } - } - - Write-Host "==> Starting bundled PostgreSQL for bootstrap" - & $pgCtl -D $pgData -l (Join-Path $data "pgdata\pg.log") start - Start-Sleep -Seconds 2 - - $env:PGPASSWORD = $BundledPassword - try { - $role = & $psql -h 127.0.0.1 -p $BundledPort -U postgres -d postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname='netx'" - if ($role -notmatch "1") { - & $psql -h 127.0.0.1 -p $BundledPort -U postgres -d postgres -v ON_ERROR_STOP=1 ` - -c "CREATE ROLE netx LOGIN PASSWORD '$BundledPassword';" - } else { - & $psql -h 127.0.0.1 -p $BundledPort -U postgres -d postgres -v ON_ERROR_STOP=1 ` - -c "ALTER ROLE netx WITH LOGIN PASSWORD '$BundledPassword';" - } - $db = & $psql -h 127.0.0.1 -p $BundledPort -U postgres -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname='netx'" - if ($db -notmatch "1") { - & $psql -h 127.0.0.1 -p $BundledPort -U postgres -d postgres -v ON_ERROR_STOP=1 ` - -c "CREATE DATABASE netx OWNER netx;" - } - & $psql -h 127.0.0.1 -p $BundledPort -U postgres -d postgres -v ON_ERROR_STOP=1 ` - -c "GRANT ALL PRIVILEGES ON DATABASE netx TO netx;" - } finally { - Remove-Item Env:PGPASSWORD -ErrorAction SilentlyContinue - } - Write-Host "==> Bundled Postgres ready on 127.0.0.1:$BundledPort" -ForegroundColor Green -} else { - if (-not $ExternalDatabaseUrl) { - if ($existing.ContainsKey("NETX_DATABASE_URL") -and $existing["NETX_DATABASE_URL"]) { - $ExternalDatabaseUrl = $existing["NETX_DATABASE_URL"] - } elseif ($NonInteractive) { - throw "external_url_required" - } else { - $ExternalDatabaseUrl = Read-Host "NETX_DATABASE_URL (postgresql+psycopg://user:pass@host:5432/netx)" - } - } - if (-not $ExternalDatabaseUrl) { - throw "external_url_required" - } - $values["NETX_DATABASE_URL"] = $ExternalDatabaseUrl - Write-Host "==> External Postgres configured (ensure role/db exist; see scripts\init_pg.ps1)" -ForegroundColor Green -} - -Write-DotEnvValue -Path $envPath -Values $values -Write-Host "" -Write-Host "Wrote $envPath" -ForegroundColor Green -Write-Host "Next: .\packaging\start_netx_app.ps1" diff --git a/packaging/start_netx_app.ps1 b/packaging/start_netx_app.ps1 deleted file mode 100644 index 3fe2313..0000000 --- a/packaging/start_netx_app.ps1 +++ /dev/null @@ -1,112 +0,0 @@ -param( - [string]$ProgramRoot = "", - [string]$DataRoot = "", - [switch]$SkipBrowser = $false, - [switch]$InlineSchedulers = $false -) - -$ErrorActionPreference = "Stop" -. "$PSScriptRoot\_common.ps1" - -$prog = Get-NetxProgramRoot -Override $ProgramRoot -$data = Get-NetxDataRoot -ProgramRoot $prog -Override $DataRoot -$envPath = Join-Path $data ".env" - -if (-not (Test-Path $envPath)) { - Write-Host "[ERR] Missing $envPath — run setup_first_run.ps1 first." -ForegroundColor Red - exit 1 -} - -if (-not (Test-Path (Join-Path $prog "netx_api"))) { - throw "netx_api not found under program root: $prog" -} - -$map = Import-NetxEnvFile -Path $envPath -Set-Location $prog -$env:PYTHONPATH = $prog - -# Keep runtime artifacts in the data root (not under Program Files). -$env:NETX_AUTH_MCP_TOKEN_FILE = Join-Path $data "data\auth\mcp_token" -$env:NETX_SCHEDULER_HEARTBEAT_PATH = Join-Path $data "data\runtime\scheduler_heartbeat.json" -$env:NETX_AUTH_SECRET_FILE = Join-Path $data "data\auth\jwt_secret" - -$dist = Join-Path $prog "web\dist" -if (Test-Path (Join-Path $dist "index.html")) { - $env:NETX_UI_DIST_DIR = $dist -} - -$mode = Get-DbMode -EnvMap $map -Write-Host "==> Program: $prog" -Write-Host "==> Data: $data" -Write-Host "==> DB mode: $mode" - -function Get-PgsqlBin { - foreach ($b in @( - (Join-Path $prog "postgres\pgsql\bin"), - (Join-Path $PSScriptRoot "postgres\pgsql\bin") - )) { - if (Test-Path (Join-Path $b "pg_ctl.exe")) { return $b } - } - return $null -} - -if ($mode -eq "bundled") { - $pgBin = Get-PgsqlBin - if (-not $pgBin) { throw "bundled_postgres_missing" } - $pgData = if ($map["NETX_BUNDLED_PG_DATA_DIR"]) { - $map["NETX_BUNDLED_PG_DATA_DIR"] - } else { - Join-Path $data "pgdata" - } - $pgCtl = Join-Path $pgBin "pg_ctl.exe" - $status = & $pgCtl -D $pgData status 2>&1 | Out-String - if ($status -notmatch "server is running") { - Write-Host "==> Starting bundled PostgreSQL" - if (-not (Test-Path $pgData)) { - New-Item -ItemType Directory -Path $pgData -Force | Out-Null - } - $log = Join-Path $pgData "pg.log" - & $pgCtl -D $pgData -l $log start - if ($LASTEXITCODE -ne 0) { throw "pg_start_failed" } - Start-Sleep -Seconds 2 - } else { - Write-Host "==> Bundled PostgreSQL already running" - } -} - -# Ensure venv exists for start_netx.ps1 -$venvPy = Join-Path $prog ".venv\Scripts\python.exe" -if (-not (Test-Path $venvPy)) { - Write-Host "==> Creating .venv (one-time)" - $pyCmd = Get-Command python -ErrorAction SilentlyContinue - if (-not $pyCmd) { throw "python_not_found: install Python 3.11+ and re-run" } - & $pyCmd.Source -m venv (Join-Path $prog ".venv") - & $venvPy -m pip install --upgrade pip - & $venvPy -m pip install -r (Join-Path $prog "requirements.txt") - if ($LASTEXITCODE -ne 0) { throw "pip_install_failed" } -} - -$hostBind = if ($env:NETX_HOST) { $env:NETX_HOST } else { "127.0.0.1" } -$port = if ($env:NETX_PORT) { [int]$env:NETX_PORT } else { 8890 } - -$startScript = Join-Path $prog "scripts\start_netx.ps1" -if (-not (Test-Path $startScript)) { - throw "start_netx.ps1 not found at $startScript" -} - -$psArgs = @( - "-ExecutionPolicy", "Bypass", "-File", $startScript, - "-SkipInstall", "-Background", - "-BindHost", $hostBind, "-Port", "$port" -) -if ($InlineSchedulers) { $psArgs += "-InlineSchedulers" } - -Write-Host "==> Starting NetX (API + workers; UI via API on :$port)" -& powershell @psArgs -if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - -$url = "http://${hostBind}:${port}/" -Write-Host "==> Open: $url" -ForegroundColor Green -if (-not $SkipBrowser) { - try { Start-Process $url } catch {} -} diff --git a/packaging/stop_netx_app.ps1 b/packaging/stop_netx_app.ps1 deleted file mode 100644 index 174c631..0000000 --- a/packaging/stop_netx_app.ps1 +++ /dev/null @@ -1,46 +0,0 @@ -param( - [string]$ProgramRoot = "", - [string]$DataRoot = "", - [switch]$KeepPostgres = $false -) - -$ErrorActionPreference = "Continue" -. "$PSScriptRoot\_common.ps1" - -$prog = Get-NetxProgramRoot -Override $ProgramRoot -$data = Get-NetxDataRoot -ProgramRoot $prog -Override $DataRoot -$envPath = Join-Path $data ".env" -$map = @{} -if (Test-Path $envPath) { - $map = Read-DotEnv -Path $envPath -} - -$stopScript = Join-Path $prog "scripts\stop_netx.ps1" -if (-not (Test-Path $stopScript)) { - $stopScript = Join-Path (Get-NetxRepoRoot) "scripts\stop_netx.ps1" -} -if (Test-Path $stopScript) { - Write-Host "==> Stopping NetX processes" - & powershell -ExecutionPolicy Bypass -File $stopScript -Force -} else { - Write-Host "[WARN] stop_netx.ps1 not found" -} - -$mode = Get-DbMode -EnvMap $map -if ($mode -eq "bundled" -and -not $KeepPostgres) { - $pgBinCandidates = @( - (Join-Path $prog "postgres\pgsql\bin\pg_ctl.exe"), - (Join-Path $PSScriptRoot "postgres\pgsql\bin\pg_ctl.exe") - ) - $pgCtl = $null - foreach ($c in $pgBinCandidates) { - if (Test-Path $c) { $pgCtl = $c; break } - } - $pgData = if ($map["NETX_BUNDLED_PG_DATA_DIR"]) { $map["NETX_BUNDLED_PG_DATA_DIR"] } else { Join-Path $data "pgdata" } - if ($pgCtl -and (Test-Path $pgData)) { - Write-Host "==> Stopping bundled PostgreSQL" - & $pgCtl -D $pgData stop -m fast - } -} - -Write-Host "==> Stop done" diff --git a/packaging/update_netx.ps1 b/packaging/update_netx.ps1 deleted file mode 100644 index 1ddd0ec..0000000 --- a/packaging/update_netx.ps1 +++ /dev/null @@ -1,104 +0,0 @@ -param( - [Parameter(Mandatory = $true)] - [string]$PackagePath, - [string]$ProgramRoot = "", - [string]$DataRoot = "", - [switch]$SkipBackup = $false, - [switch]$NoStart = $false -) - -$ErrorActionPreference = "Stop" -. "$PSScriptRoot\_common.ps1" - -$prog = Get-NetxProgramRoot -Override $ProgramRoot -$data = Get-NetxDataRoot -ProgramRoot $prog -Override $DataRoot - -if (-not (Test-Path $PackagePath)) { - throw "package_not_found: $PackagePath" -} - -$work = Join-Path $env:TEMP ("netx-update-" + [guid]::NewGuid().ToString("n")) -New-Item -ItemType Directory -Path $work -Force | Out-Null - -try { - Write-Host "==> Extracting update package" - if ($PackagePath.ToLowerInvariant().EndsWith(".zip")) { - Expand-Archive -Path $PackagePath -DestinationPath $work -Force - } else { - throw "unsupported_package: use a .zip built by build_release.ps1" - } - - $src = $work - # If zip has a single top-level folder, use it. - $kids = @(Get-ChildItem -Path $work -Directory) - if ($kids.Count -eq 1 -and (Test-Path (Join-Path $kids[0].FullName "version.json"))) { - $src = $kids[0].FullName - } elseif (-not (Test-Path (Join-Path $work "version.json"))) { - $nested = Get-ChildItem -Path $work -Recurse -Filter "version.json" | Select-Object -First 1 - if ($nested) { $src = Split-Path -Parent $nested.FullName } - } - - $newVerFile = Join-Path $src "version.json" - if (-not (Test-Path $newVerFile)) { - throw "version.json missing in package" - } - $newVer = (Get-Content -Raw $newVerFile | ConvertFrom-Json).version - $oldVer = Get-NetxVersion -ProgramRoot $prog - Write-Host "==> Updating $oldVer -> $newVer" - - if (-not $SkipBackup) { - $stamp = Get-Date -Format "yyyyMMdd_HHmmss" - $bak = Join-Path $data "backups\pre-update-$stamp" - New-Item -ItemType Directory -Path $bak -Force | Out-Null - $envFile = Join-Path $data ".env" - if (Test-Path $envFile) { - Copy-Item $envFile (Join-Path $bak ".env") - } - Write-Host "==> Backup marker: $bak (data/pgdata left in place; optional pg_dump not run)" - } - - Write-Host "==> Stopping services" - & powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "stop_netx_app.ps1") ` - -ProgramRoot $prog -DataRoot $data - - # Replace program layers only — never wipe data root. - $replaceDirs = @("netx_api", "alembic", "web", "packaging", "scripts", "postgres", "packages") - foreach ($name in $replaceDirs) { - $from = Join-Path $src $name - $to = Join-Path $prog $name - if (-not (Test-Path $from)) { continue } - Write-Host "==> Replacing $name" - if (Test-Path $to) { - Remove-Item -Recurse -Force $to - } - Copy-Item -Path $from -Destination $to -Recurse -Force - } - foreach ($f in @("requirements.txt", "pyproject.toml", "alembic.ini", "version.json", "README.md", "LICENSE")) { - $from = Join-Path $src $f - if (Test-Path $from) { - Copy-Item -Path $from -Destination (Join-Path $prog $f) -Force - } - } - # Optional runtime / .venv shipped in package - if (Test-Path (Join-Path $src "runtime")) { - $rt = Join-Path $prog "runtime" - if (Test-Path $rt) { Remove-Item -Recurse -Force $rt } - Copy-Item -Path (Join-Path $src "runtime") -Destination $rt -Recurse -Force - } - if (Test-Path (Join-Path $src ".venv")) { - Write-Host "==> Replacing .venv from package" - $venvTo = Join-Path $prog ".venv" - if (Test-Path $venvTo) { Remove-Item -Recurse -Force $venvTo } - Copy-Item -Path (Join-Path $src ".venv") -Destination $venvTo -Recurse -Force - } - - Write-Host "==> Update files applied" -ForegroundColor Green - if (-not $NoStart) { - & powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "start_netx_app.ps1") ` - -ProgramRoot $prog -DataRoot $data -SkipBrowser - } -} finally { - if (Test-Path $work) { - Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue - } -} diff --git a/pyproject.toml b/pyproject.toml index a2836f0..fde81d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "netx-ops" -version = "0.3.0" +version = "0.2.0" description = "netx operations tool: alarm-centric workflows with REST API and stdio MCP" readme = "README.md" requires-python = ">=3.11" diff --git a/tests/test_ui_static.py b/tests/test_ui_static.py deleted file mode 100644 index 2e1c9f4..0000000 --- a/tests/test_ui_static.py +++ /dev/null @@ -1,75 +0,0 @@ -"""Tests for optional SPA static hosting.""" - -from __future__ import annotations - -from pathlib import Path - -from fastapi.testclient import TestClient - - -def test_api_only_root_when_no_dist(monkeypatch, tmp_path: Path): - monkeypatch.setenv("NETX_UI_DIST_DIR", str(tmp_path / "missing")) - # Re-import settings + app with patched env is heavy; call helpers directly. - from netx_api.ui_static import resolve_ui_dist - from netx_api import config as cfg - - monkeypatch.setattr(cfg.settings, "ui_dist_dir", str(tmp_path / "missing")) - assert resolve_ui_dist() is None - - -def test_resolve_ui_dist_when_index_present(monkeypatch, tmp_path: Path): - dist = tmp_path / "dist" - assets = dist / "assets" - assets.mkdir(parents=True) - (dist / "index.html").write_text("netx", encoding="utf-8") - (assets / "app.js").write_text("console.log(1)", encoding="utf-8") - - from netx_api import config as cfg - from netx_api.ui_static import resolve_ui_dist - - monkeypatch.setattr(cfg.settings, "ui_dist_dir", str(dist)) - assert resolve_ui_dist() == dist.resolve() - - -def test_mount_ui_serves_index(monkeypatch, tmp_path: Path): - dist = tmp_path / "dist" - assets = dist / "assets" - assets.mkdir(parents=True) - (dist / "index.html").write_text("ui", encoding="utf-8") - (assets / "x.js").write_text("ok", encoding="utf-8") - - from fastapi import FastAPI - - from netx_api import config as cfg - from netx_api.ui_static import mount_ui_if_present - - monkeypatch.setattr(cfg.settings, "ui_dist_dir", str(dist)) - app = FastAPI() - - @app.get("/health") - def health(): - return {"status": "ok"} - - assert mount_ui_if_present(app) is True - client = TestClient(app) - r = client.get("/") - assert r.status_code == 200 - assert "ui" in r.text - r2 = client.get("/assets/x.js") - assert r2.status_code == 200 - assert r2.text == "ok" - r3 = client.get("/login") - assert r3.status_code == 200 - assert "ui" in r3.text - assert client.get("/health").json()["status"] == "ok" - - -def test_spa_paths_are_public(): - from netx_api.auth_middleware import _is_public - - assert _is_public("/login") is True - assert _is_public("/topology") is True - assert _is_public("/assets/app.js") is True - assert _is_public("/v1/managed-ne") is False - assert _is_public("/health") is True - assert _is_public("/v1/auth/login") is True diff --git a/web/package.json b/web/package.json index 9580af5..3d529ca 100644 --- a/web/package.json +++ b/web/package.json @@ -9,7 +9,6 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "build:release": "vite build", "lint": "eslint .", "preview": "vite preview" },