diff --git a/deploy/deploy-frontend.sh b/deploy/deploy-frontend.sh index bac73f3c..1b68449f 100644 --- a/deploy/deploy-frontend.sh +++ b/deploy/deploy-frontend.sh @@ -23,10 +23,12 @@ echo "▶ Uploading to server..." scp /tmp/nexus-frontend.tar.gz nexus:/tmp/nexus-frontend.tar.gz echo "✓ Upload complete" -# 4. Extract on server (overwrites old index.html + assets) +# 4. Extract on server + prune orphaned legacy chunks echo "▶ Deploying on server..." ssh nexus "cd /www/wwwroot/api.synaglobal.vip/web/app && tar xzf /tmp/nexus-frontend.tar.gz && rm /tmp/nexus-frontend.tar.gz" -echo "✓ Extracted" +echo "▶ Pruning orphaned assets..." +ssh nexus "python3 /www/wwwroot/api.synaglobal.vip/deploy/prune_frontend_assets.py" +echo "✓ Extracted and pruned" # 5. Restart backend echo "▶ Restarting backend..." diff --git a/deploy/prune_frontend_assets.py b/deploy/prune_frontend_assets.py new file mode 100644 index 00000000..fa3c728c --- /dev/null +++ b/deploy/prune_frontend_assets.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Remove orphaned files under web/app/assets/ not referenced by current Vite build.""" +from __future__ import annotations + +import re +import sys +from pathlib import Path + +IMPORT_RE = re.compile( + r"""(?:from\s*["']|import\s*\(\s*["']|import\s*["'])\./([^"']+\.(?:js|css))""" +) +URL_RE = re.compile(r"""url\(["']?([^"')]+\.(?:woff2?|ttf|eot))["']?\)""") + + +def collect(app_dir: Path) -> set[Path]: + assets = app_dir / "assets" + index = app_dir / "index.html" + if not index.is_file(): + raise SystemExit(f"missing {index}") + keep: set[Path] = set() + queue: list[Path] = [] + + for m in re.finditer(r"/app/assets/([^\s\"']+)", index.read_text(encoding="utf-8")): + p = assets / m.group(1) + if p.is_file(): + queue.append(p) + + while queue: + path = queue.pop() + if path in keep or not path.is_file(): + continue + keep.add(path) + text = path.read_text(encoding="utf-8", errors="ignore") + for rel in IMPORT_RE.findall(text): + nxt = assets / rel + if nxt.is_file(): + queue.append(nxt) + if path.suffix == ".css": + for rel in URL_RE.findall(text): + rel = rel.lstrip("./") + nxt = assets / rel + if nxt.is_file(): + keep.add(nxt) + return keep + + +def main() -> int: + app_dir = Path(sys.argv[1] if len(sys.argv) > 1 else "/www/wwwroot/api.synaglobal.vip/web/app") + assets = app_dir / "assets" + if not assets.is_dir(): + raise SystemExit(f"not a directory: {assets}") + + keep = collect(app_dir) + all_files = {p for p in assets.rglob("*") if p.is_file()} + orphan = sorted(all_files - keep) + for p in orphan: + p.unlink() + print(f"kept={len(keep)} removed={len(orphan)} total_before={len(all_files)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/deploy/verify_login.py b/deploy/verify_login.py new file mode 100644 index 00000000..7ac05b15 --- /dev/null +++ b/deploy/verify_login.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Post-deploy smoke: login + optional batch endpoint (run on server).""" +import json +import os +import sys +import urllib.error +import urllib.request + +BASE = os.environ.get("NEXUS_TEST_BASE", "http://127.0.0.1:8600") +USER = os.environ.get("NEXUS_TEST_ADMIN_USER", "admin") +PASSWORD = os.environ.get("NEXUS_TEST_ADMIN_PASSWORD", "") + + +def post(path: str, body: dict, headers: dict | None = None) -> dict: + h = {"Content-Type": "application/json", **(headers or {})} + req = urllib.request.Request( + f"{BASE}{path}", + data=json.dumps(body).encode(), + headers=h, + method="POST", + ) + with urllib.request.urlopen(req, timeout=30) as resp: + return json.load(resp) + + +def main() -> int: + if not PASSWORD: + print("SKIP: set NEXUS_TEST_ADMIN_PASSWORD", file=sys.stderr) + return 2 + login = post("/api/auth/login", {"username": USER, "password": PASSWORD}) + token = login.get("access_token") + exp = login.get("expires_in") + print(f"login ok expires_in={exp}") + if exp != 3600: + print(f"WARN: expected expires_in=3600, got {exp}") + if not token: + print("FAIL: no access_token") + return 1 + # Minimal authenticated call + req = urllib.request.Request( + f"{BASE}/api/servers/stats", + headers={"Authorization": f"Bearer {token}"}, + ) + with urllib.request.urlopen(req, timeout=30) as resp: + stats = json.load(resp) + print(f"servers/stats ok keys={list(stats.keys())[:5]}") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except urllib.error.HTTPError as e: + print(f"HTTP {e.code}: {e.read().decode()[:500]}", file=sys.stderr) + raise SystemExit(1)