diff --git a/main.py b/main.py index 6ecdc91..4103e2c 100644 --- a/main.py +++ b/main.py @@ -35,6 +35,11 @@ MATOMO_SITE = os.environ.get("MATOMO_SITE", "2") IP_LABELS_LOCAL = DATA / "ip_labels.json" IP_LABELS_REMOTE = os.environ.get("IP_LABELS_REMOTE", "") # fx root@192.168.10.201:/opt/npmplus/custom_nginx/ip_labels.json SETTINGS_PATH = DATA / "settings.json" +RP_BASE_URL = os.environ.get("RP_BASE_URL", "https://rp.weircon.dk").rstrip("/") +RP_API_KEY = os.environ.get("RP_API_KEY", "") +ASSETS_VERSION_URL = os.environ.get("ASSETS_VERSION_URL", "https://assets.weircon.dk/VERSION") +GEOIP_DB = os.environ.get("GEOIP_DB", str(DATA / "dbip-country.mmdb")) +_geoip_reader = None app = FastAPI(title="Weircon Portal") app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY, https_only=True, same_site="lax", max_age=60 * 60 * 12) @@ -60,6 +65,8 @@ ADMIN_MODULES = [ "desc": "Hurtigt overblik fra Matomo."}, {"key": "apps", "title": "App-beskrivelser & grupper", "icon": "🧩", "desc": "Rediger beskrivelse og gruppe på dine Authentik-apps."}, + {"key": "ops", "title": "Drift / egress", "icon": "🛰️", + "desc": "Static-assets-version + random-proxy egress, rollover & inflight."}, ] @@ -187,6 +194,49 @@ async def patch_apps(rows): return n +def geoip_country(ip): + global _geoip_reader + try: + if _geoip_reader is None: + import maxminddb + _geoip_reader = maxminddb.open_database(GEOIP_DB) + rec = _geoip_reader.get(ip) or {} + c = rec.get("country") or {} + iso = c.get("iso_code") or "" + name = (c.get("names") or {}).get("en") or "" + flag = "".join(chr(0x1F1E6 + ord(ch) - 65) for ch in iso) if len(iso) == 2 and iso.isalpha() else "" + return {"iso": iso, "name": name, "flag": flag} + except Exception: + return {"iso": "", "name": "", "flag": ""} + + +async def assets_version(): + try: + async with httpx.AsyncClient(timeout=6) as c: + r = await c.get(ASSETS_VERSION_URL) + return r.text.strip()[:40] + except Exception: + return None + + +async def rp_status(): + if not RP_API_KEY: + return {"error": "ingen RP_API_KEY konfigureret"} + try: + async with httpx.AsyncClient(timeout=6) as c: + r = await c.get(RP_BASE_URL + "/status", headers={"X-WEIRCON-RANDOM-IP": RP_API_KEY}) + d = r.json() + for p in d.get("proxies", []): + p["geo"] = geoip_country(p.get("egress_ip") or "") + rot = d.get("rotation") or {} + if rot.get("next_egress_ip"): + rot["geo"] = geoip_country(rot["next_egress_ip"]) + d["inflight_total"] = sum(int(p.get("inflight") or 0) for p in d.get("proxies", [])) + return d + except Exception as exc: + return {"error": str(exc)[:200]} + + async def matomo_summary(): if not MATOMO_TOKEN: return None @@ -332,6 +382,18 @@ async def admin_cookies_save(request: Request, cookie_banner: str = Form(""), co return RedirectResponse("/admin/cookies?saved=Gemt", status_code=303) +@app.get("/admin/ops", response_class=HTMLResponse) +async def admin_ops(request: Request): + user = current_user(request) + if not is_admin(user): + raise HTTPException(403, "Kun for administratorer") + version = await assets_version() + rp = await rp_status() + return templates.TemplateResponse(request, "admin_ops.html", { + "request": request, "user": user, "is_admin": True, "version": version, "rp": rp, + }) + + @app.get("/admin/stats", response_class=HTMLResponse) async def admin_stats(request: Request): user = current_user(request) diff --git a/requirements.txt b/requirements.txt index 0e6bcd7..565b2a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ authlib httpx itsdangerous python-multipart +maxminddb diff --git a/templates/admin_ops.html b/templates/admin_ops.html new file mode 100644 index 0000000..8e3e0a6 --- /dev/null +++ b/templates/admin_ops.html @@ -0,0 +1,65 @@ +{% extends "base.html" %} +{% block title %}Admin — drift{% endblock %} +{% block body %} +
Live driftsoverblik. Genindlæser automatisk hvert 20. sekund.
+ +