From bd78835f4a583ad1bd60f7ae11e83d5d6a4f34e3 Mon Sep 17 00:00:00 2001 From: Europium 63 Date: Wed, 28 Oct 2026 00:03:19 -0700 Subject: [PATCH] Inspector 0.9.0: edit every container option and the Uptime Kuma / Portainer integrations in the dashboard --- CHANGELOG.md | 35 ++ README.md | 10 +- src/inspector/__init__.py | 2 +- .../assets/guide/container-advanced.png | Bin 0 -> 49957 bytes src/inspector/assets/guide/guide.html | 44 +- .../assets/guide/settings-portainer.png | Bin 0 -> 41961 bytes src/inspector/dashboard.py | 594 +++++++++++++++++- src/inspector/main.py | 9 + src/inspector/uptime_kuma.py | 18 +- 9 files changed, 681 insertions(+), 31 deletions(-) create mode 100755 src/inspector/assets/guide/container-advanced.png create mode 100755 src/inspector/assets/guide/settings-portainer.png diff --git a/CHANGELOG.md b/CHANGELOG.md index cd990a9..1ad3566 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,41 @@ Version numbers follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html --- +## [0.9.0] — 2026-10-28 + +### Added + +- **Every container option is now in the dashboard.** The container page has three + new groups: + - **Reachability**: HTTP check (URL, timeout, expected status) and Uptime Kuma + monitor. + - **Dependencies**: `depends_on` (pick from your containers) and `vpn_routed`. + - **Recovery**: `recovery_action` with plain-language descriptions, plus the + Portainer environment and stack name for stack redeploys. +- **Integrations settings.** New **Uptime Kuma** and **Portainer** pages under + Settings. Portainer environments can be added, edited, renamed (containers + that use them are updated) and deleted (refused while a container uses one). + The API token can be an environment variable name (recommended) or a masked + value saved in `config.yml`. +- **Test connection** buttons for Uptime Kuma (lists monitors) and each Portainer + environment (lists stacks on the endpoint), using the values in the form even + before saving. +- The All containers grid shows each container's extras (HTTP check, Uptime Kuma, + dependency, recovery action). +- Guide: new sections on reachability checks and apps behind a VPN container. + +### Changed + +- Uptime Kuma and Portainer changes take effect on the next check; Inspector + rebuilds those clients when their settings change instead of needing a restart. +- Saving a container form unchanged now leaves `config.yml` byte-identical. Blank + optional fields are removed instead of written as empty values (`color: ''`, + `maintenance_window: false`), and an unchanged `restart_on` list keeps its + order and the comments after it. An explicit `recovery_action: restart_container` + is kept. + +--- + ## [0.8.0] — 2026-10-27 ### Added diff --git a/README.md b/README.md index 04a17d9..b30faf4 100755 --- a/README.md +++ b/README.md @@ -69,18 +69,20 @@ download it and open it in a browser to read it here). ## Advanced options -Most people never need these. They live in `config.yml` (in your -`INSPECTOR_DATA/config` folder) rather than the dashboard; see -[`config.example.yml`](./config.example.yml) for every option with comments. +Most people never need these. They are all on each container's page in the +dashboard (and in `config.yml`, see +[`config.example.yml`](./config.example.yml) for every option with comments). - **HTTP checks**: mark a container `UNREACHABLE` when a URL stops answering, even if Docker says it's healthy (useful for apps behind a VPN container). - **Uptime Kuma**: use a Kuma status page as an extra reachability signal. + **Test connection** lists the monitors Inspector can see. - **Dependencies** (`depends_on`): don't restart an app while the container it relies on (e.g. a VPN) is down. - **Portainer stack redeploy**: for containers that share a VPN container's network, recover by redeploying the whole stack instead of restarting one - container. + container. Set up the Portainer connection under **Settings → Portainer**; + **Test connection** lists your stacks. ## Security diff --git a/src/inspector/__init__.py b/src/inspector/__init__.py index 777f190..3e2f46a 100755 --- a/src/inspector/__init__.py +++ b/src/inspector/__init__.py @@ -1 +1 @@ -__version__ = "0.8.0" +__version__ = "0.9.0" diff --git a/src/inspector/assets/guide/container-advanced.png b/src/inspector/assets/guide/container-advanced.png new file mode 100755 index 0000000000000000000000000000000000000000..6ebb68148eba7ea8dd30b0ee5519f8c4f7a56f38 GIT binary patch literal 49957 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

w%C)$-%^o#;_HOsp?*G6 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 MsG(3SXa3>;0H4sHaR2}S literal 0 HcmV?d00001 diff --git a/src/inspector/assets/guide/guide.html b/src/inspector/assets/guide/guide.html index 89db1bb..e46ee8e 100755 --- a/src/inspector/assets/guide/guide.html +++ b/src/inspector/assets/guide/guide.html @@ -197,11 +197,51 @@ does not touch the container itself.

StatusWhen it happens UNHEALTHYThe container's own health check is failing. NOT RUNNINGStopped, exited, or crashed. - UNREACHABLERunning, but an HTTP check failed (advanced, set in config.yml). + UNREACHABLERunning, but its HTTP check or Uptime Kuma monitor says it's down (see below). ERRORInspector couldn't check it (Docker API error). STARTINGStill in its health-check start period. Usually leave this off. HEALTHY / NO HEALTH CHECKNormal states; almost never a reason to restart. + +

Reachability: catching apps that look fine but aren't

+

Docker only knows whether a container is running and whether its own health check passes. An +app can pass both and still be unusable, for example when it routes through a VPN container +whose tunnel dropped. Under Reachability on the container page you can add:

+
    +
  • HTTP check: a URL Inspector requests every check (for example + http://192.168.1.10:9696/ping). Set expected_status to the code the app + returns when it's healthy; most return 200, some return 401 or 302.
  • +
  • Uptime Kuma monitor: if you already run Uptime Kuma, name one of its monitors. Set up + the connection first in Settings → Uptime Kuma; Test connection lists the + monitor names it can see.
  • +
+

A failed check reports UNREACHABLE. Tick UNREACHABLE in the restart triggers if you want +Inspector to act on it.

+
Reachability, Dependencies and Recovery groups +
Bottom of a container's page: an app behind a VPN container with an HTTP check and stack-redeploy recovery.
+ +

Apps behind a VPN container

+

If an app shares a VPN container's network (network_mode: container:vpn or similar), set these on the app's page:

+
    +
  1. Dependencies → depends_on: pick the VPN container, and turn on vpn_routed. + Inspector checks the VPN first and waits for it to recover before touching the app.
  2. +
  3. Recovery → recovery_action: choose Restart, then redeploy stack if that fails + and enter the stack name exactly as Portainer shows it. When the VPN container gets + recreated, a plain restart of the app fails; redeploying the stack reconnects it.
  4. +
+

Stack redeploys need the Portainer connection in Settings → Portainer:

+
    +
  1. In Portainer, open My account → Access tokens and create a token.
  2. +
  3. Easiest and safest: add it to the Inspector stack as an environment variable (for example + PORTAINER_API_TOKEN) and redeploy Inspector. Or choose Saved in config.yml + and paste it in.
  4. +
  5. In Settings → Portainer, add an environment with Portainer's URL and endpoint ID + (usually 1 or 2; it's the number in the URL when you open the environment in Portainer). + Click Test connection: it should list your stacks.
  6. +
  7. Turn Portainer on and save.
  8. +
+
Portainer settings +
Settings → Portainer with one environment; the token comes from an environment variable.
@@ -225,6 +265,8 @@ maintenance. Use Prev day / Next day to look back.

Health summaryA periodic digest (e.g. daily) of what happened, sent through your notification channels. MaintenanceA daily quiet window (e.g. 02:00–05:00) so nightly backups or updates don't trigger restarts. Turn on maintenance_window on each container that should follow it. Notification muteTurns the per-container Mute notifications buttons on or off, and sets how long a mute lasts. + Uptime KumaConnection to your Uptime Kuma status page, used by container reachability checks. + PortainerConnection to Portainer, used to redeploy stacks. See Apps behind a VPN container.

At the bottom of every settings page is a strip showing where the config file lives and whether Inspector can write to it.

diff --git a/src/inspector/assets/guide/settings-portainer.png b/src/inspector/assets/guide/settings-portainer.png new file mode 100755 index 0000000000000000000000000000000000000000..edfc67e5706b2fb885a5ade15c3f52aee4896db4 GIT binary patch literal 41961 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 z zmhE3;6BTsHzvY4#Om4HQ;!RnbJHL3!DHXpfOaEc*_zQyczq%U#z5D<6o9GUF$CVQ+ X)@~Kr)#&&K@VaVZajE2@>x2IRS5HrM literal 0 HcmV?d00001 diff --git a/src/inspector/dashboard.py b/src/inspector/dashboard.py index 6a8f665..acc0bde 100755 --- a/src/inspector/dashboard.py +++ b/src/inspector/dashboard.py @@ -21,6 +21,8 @@ from .config import ( AppConfig, ConfigRef, ContainerConfig, + HttpCheckConfig, + UptimeKumaMonitorConfig, build_config_from_raw, write_config_raw, ) @@ -595,12 +597,25 @@ a.signout:hover { color: var(--ink); border-color: var(--muted); } .settings-pane .field label { font-size: 0.76rem; color: #94a3b8; font-weight: 600; } .settings-pane .field.field-row label { font-size: 0.85rem; color: #cbd5e1; } .settings-pane .field input[type=text], .settings-pane .field input[type=number], -.settings-pane .field input[type=password] { +.settings-pane .field input[type=password], .settings-pane .field select { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 0.5rem 0.65rem; color: var(--ink); font-size: 0.88rem; font-family: inherit; outline: none; } .settings-pane .field input:focus { border-color: var(--blue); box-shadow: 0 0 0 2px rgba(96,165,250,0.15); } .settings-pane .field-hint { font-size: 0.72rem; color: var(--muted); } +.settings-pane .field-warn { font-size: 0.72rem; color: #fbbf24; } +.settings-pane .field-warn a { color: inherit; text-decoration: underline; } +.settings-pane .action-help { margin: -0.4rem 0 0.9rem; padding-left: 1rem; font-size: 0.72rem; color: var(--muted); } +.settings-pane .action-help b { color: #cbd5e1; font-weight: 600; } +/* Grouped blocks on the container form (Reachability / Dependencies / Recovery). */ +.settings-pane .form-group { + border: 1px solid var(--border); border-radius: 10px; padding: 0.9rem 1rem 0.2rem; + margin: 1.2rem 0; +} +.settings-pane .form-group > h4 { margin: 0 0 0.2rem; font-size: 0.88rem; color: var(--ink); } +.settings-pane .form-group > .group-sub { margin: 0 0 0.9rem; color: var(--muted); font-size: 0.75rem; } +.settings-pane .form-group .form-group { margin: 0.4rem 0 0.9rem; background: rgba(15,24,41,0.5); } +.settings-pane .portainer-only[hidden], .settings-pane .token-src[hidden] { display: none; } .settings-pane .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0.1rem 1rem; } @media (max-width: 640px) { .settings-pane .grid-2 { grid-template-columns: 1fr; } } .settings-pane .form-actions { @@ -1186,6 +1201,18 @@ class _SettingsError(ValueError): pass +# (value, label, description) for the recovery_action dropdown. "" = default. +_RECOVERY_ACTION_OPTIONS = [ + ("", "Restart container (default)", "docker restart. Right for most containers."), + ("start_container", "Start container", "docker start. Only starts a stopped container; never restarts a running one."), + ("portainer_stack_redeploy", "Redeploy Portainer stack", + "Redeploys the whole stack through Portainer. For containers whose network is tied to a VPN container."), + ("restart_then_redeploy", "Restart, then redeploy stack if that fails", + "Tries docker restart first; escalates to a stack redeploy if the restart errors or the container is still failing next check."), +] +_PORTAINER_ACTIONS = ("portainer_stack_redeploy", "restart_then_redeploy") + + def _form_str(form: dict, key: str, default: str = "") -> str: return form.get(key, [default])[0] @@ -1209,6 +1236,146 @@ def _apply_secret(form: dict, raw_section: dict, form_key: str, yaml_key: str) - raw_section[yaml_key] = submitted +def _form_float(form: dict, key: str, default: float = 0.0) -> float: + try: + return float(_form_str(form, key, str(default))) + except (TypeError, ValueError): + return default + + +def _set_or_drop(section: dict, key: str, value) -> None: + """Store value, or remove the key when it is blank/False so config.yml + doesn't fill up with empty defaults.""" + if value in ("", None, False): + section.pop(key, None) + else: + section[key] = value + + +def _apply_container_advanced(raw: dict, name: str, cfg: dict, form: dict) -> None: + """Merge the Reachability / Dependencies / Recovery fields into `cfg`. + + Only our own container form carries `advanced_present`; without it the + keys are left untouched so older clients can't wipe YAML-only settings. + """ + if not form.get("advanced_present"): + return + + hc_enabled = _form_bool(form, "http_check_enabled") + hc_url = _form_str(form, "http_check_url", "").strip() + if hc_enabled and not hc_url.startswith(("http://", "https://")): + raise _SettingsError("HTTP check needs a URL starting with http:// or https://") + if hc_enabled or hc_url: + hc = cfg.get("http_check") or {} + hc["enabled"] = hc_enabled + hc["url"] = hc_url + timeout = _form_float(form, "http_check_timeout_seconds", 5.0) + hc["timeout_seconds"] = int(timeout) if timeout.is_integer() else timeout + hc["expected_status"] = _form_int(form, "http_check_expected_status", 200) + cfg["http_check"] = hc + else: + cfg.pop("http_check", None) + + uk_enabled = _form_bool(form, "uptime_kuma_enabled") + uk_monitor = _form_str(form, "uptime_kuma_monitor_name", "").strip() + if uk_enabled and not uk_monitor: + raise _SettingsError("Uptime Kuma check needs a monitor name") + if uk_enabled or uk_monitor: + uk = cfg.get("uptime_kuma") or {} + uk["enabled"] = uk_enabled + uk["monitor_name"] = uk_monitor + cfg["uptime_kuma"] = uk + else: + cfg.pop("uptime_kuma", None) + + depends_on = _form_str(form, "depends_on", "").strip() + if depends_on == name: + raise _SettingsError("a container can't depend on itself") + _set_or_drop(cfg, "depends_on", depends_on) + _set_or_drop(cfg, "vpn_routed", _form_bool(form, "vpn_routed")) + + action = _form_str(form, "recovery_action", "").strip() + # "" (the dropdown's default) and an explicit restart_container behave the + # same; keep the explicit value if the YAML already has it. + if action or cfg.get("recovery_action") != "restart_container": + _set_or_drop(cfg, "recovery_action", action) + if action in _PORTAINER_ACTIONS: + env = _form_str(form, "portainer_environment", "").strip() + envs = ((raw.get("integrations") or {}).get("portainer") or {}).get("environments") or {} + if env and env not in envs: + raise _SettingsError(f"unknown Portainer environment {env!r}") + stack = _form_str(form, "portainer_stack_name", "").strip() + if not stack: + raise _SettingsError("Portainer recovery needs a stack name") + _set_or_drop(cfg, "portainer_environment", env) + cfg["portainer_stack_name"] = stack + else: + cfg.pop("portainer_environment", None) + cfg.pop("portainer_stack_name", None) + + +def _apply_portainer_env(raw: dict, form: dict) -> None: + """Add, edit, rename or delete one integrations.portainer.environments entry.""" + pt = raw.setdefault("integrations", {}).setdefault("portainer", {}) + envs = pt.get("environments") or {} + original = _form_str(form, "env_original", "").strip() + containers = raw.get("containers") or {} + + def _users(env_name: str) -> list[str]: + return sorted(n for n, c in containers.items() if (c or {}).get("portainer_environment") == env_name) + + if original and original not in envs: + raise _SettingsError(f"unknown Portainer environment {original!r}") + + if _form_bool(form, "delete"): + users = _users(original) + if users: + raise _SettingsError( + f"environment {original!r} is used by {', '.join(users)}; change those containers first" + ) + del envs[original] + if pt.get("default_environment") == original: + pt.pop("default_environment", None) + pt["environments"] = envs + return + + env_name = _form_str(form, "env_name", "").strip() + if not _CONTAINER_NAME_RE.match(env_name): + raise _SettingsError("environment name must start with a letter/digit and use only letters, digits, _ or -") + if env_name != original and env_name in envs: + raise _SettingsError(f"environment {env_name!r} already exists") + base_url = _form_str(form, "base_url", "").strip().rstrip("/") + if not base_url.startswith(("http://", "https://")): + raise _SettingsError("Portainer URL must start with http:// or https://") + + env = envs.get(original) or {} + env["base_url"] = base_url + env["endpoint_id"] = _form_int(form, "endpoint_id", 1) + env["verify_ssl"] = _form_bool(form, "verify_ssl") + if _form_str(form, "token_source", "env") == "inline": + _apply_secret(form, env, "api_token", "api_token") + if not env.get("api_token"): + raise _SettingsError("enter the Portainer API token, or choose an environment variable") + env.pop("api_token_env", None) + else: + token_env = _form_str(form, "api_token_env", "").strip() + if not token_env: + raise _SettingsError("enter the name of the environment variable holding the API token") + env["api_token_env"] = token_env + env.pop("api_token", None) + + if original and env_name != original: + # Rename: rebuild the mapping in order and repoint references. + envs = {(env_name if k == original else k): (env if k == original else v) for k, v in envs.items()} + for cname in _users(original): + containers[cname]["portainer_environment"] = env_name + if pt.get("default_environment") == original: + pt["default_environment"] = env_name + else: + envs[env_name] = env + pt["environments"] = envs + + def _apply_settings_update(config_ref: ConfigRef, section: str, form: dict) -> None: """Validate the form, merge into the raw config, write atomically, swap ref.""" raw = config_ref.get_raw() @@ -1271,14 +1438,39 @@ def _apply_settings_update(config_ref: ConfigRef, section: str, form: dict) -> N cfg["cooldown_seconds"] = _form_int(form, "cooldown_seconds", 300) cfg["grace_period_seconds"] = _form_int(form, "grace_period_seconds", 120) cfg["max_restarts_per_hour"] = _form_int(form, "max_restarts_per_hour", 3) - cfg["color"] = _form_str(form, "color", "") + _set_or_drop(cfg, "color", _form_str(form, "color", "").strip()) if form.get("restart_on_present"): # only our own form carries this field - cfg["maintenance_window"] = _form_bool(form, "maintenance_window") - restart_on = form.get("restart_on", []) - if restart_on or form.get("restart_on_present"): - cfg["restart_on"] = [s.upper() for s in restart_on] + _set_or_drop(cfg, "maintenance_window", _form_bool(form, "maintenance_window")) + restart_on = [s.upper() for s in form.get("restart_on", [])] + existing = cfg.get("restart_on") + # Keep the existing list (its order and trailing YAML comments) when + # the selection didn't change. + same = existing is not None and {str(s).upper() for s in existing} == set(restart_on) + if (restart_on or form.get("restart_on_present")) and not same: + cfg["restart_on"] = restart_on + _apply_container_advanced(raw, name, cfg, form) containers[name] = cfg raw["containers"] = containers + elif section == "uptime_kuma": + integ = raw.setdefault("integrations", {}) + uk = integ.setdefault("uptime_kuma", {}) + uk["enabled"] = _form_bool(form, "enabled") + uk["url"] = _form_str(form, "url", "").strip() + uk["status_page_slug"] = _form_str(form, "status_page_slug", "").strip() + if uk["enabled"] and not (uk["url"] and uk["status_page_slug"]): + raise _SettingsError("Uptime Kuma needs both a URL and a status page slug") + elif section == "portainer": + integ = raw.setdefault("integrations", {}) + pt = integ.setdefault("portainer", {}) + pt["enabled"] = _form_bool(form, "enabled") + default_env = _form_str(form, "default_environment", "").strip() + if default_env and default_env not in (pt.get("environments") or {}): + raise _SettingsError(f"unknown Portainer environment {default_env!r}") + _set_or_drop(pt, "default_environment", default_env) + pt["stack_redeploy_cooldown_seconds"] = _form_int(form, "stack_redeploy_cooldown_seconds", 300) + pt["post_redeploy_wait_seconds"] = _form_int(form, "post_redeploy_wait_seconds", 90) + elif section == "portainer_env": + _apply_portainer_env(raw, form) else: raise _SettingsError(f"unknown settings section {section!r}") @@ -1325,6 +1517,7 @@ def _apply_container_create(config_ref: ConfigRef, form: dict) -> str: restart_on = form.get("restart_on", []) if restart_on or form.get("restart_on_present"): cfg["restart_on"] = [s.upper() for s in restart_on] + _apply_container_advanced(raw, name, cfg, form) containers[name] = cfg raw["containers"] = containers @@ -1429,6 +1622,79 @@ def _run_test_notification(channel: str, form: dict, app: AppConfig) -> dict: return {"ok": False, "error": f"unknown channel {channel!r}"} +def _run_test_integration(kind: str, form: dict, app: AppConfig) -> dict: + """Check an Uptime Kuma or Portainer connection using form values. + + Returns {"ok": bool, "message"/"error": str}. Never echoes tokens. + """ + import requests + + from .config import UptimeKumaConfig + from .uptime_kuma import UptimeKumaClient + + if kind == "uptime_kuma": + url = _form_str(form, "url", "").strip().rstrip("/") + slug = _form_str(form, "status_page_slug", "").strip() + if not url or not slug: + return {"ok": False, "error": "URL and status page slug required"} + client = UptimeKumaClient(UptimeKumaConfig(enabled=True, url=url, status_page_slug=slug)) + try: + snapshot = client.fetch_or_raise() + except requests.HTTPError as exc: + return {"ok": False, "error": f"HTTP {exc.response.status_code} — check the URL and slug"} + except (ValueError, AttributeError): # requests' JSONDecodeError is a ValueError too + return {"ok": False, "error": "response was not Uptime Kuma status-page JSON"} + except requests.RequestException as exc: + return {"ok": False, "error": f"request failed: {exc}"} + names = sorted(snapshot.up_by_monitor) + if not names: + return {"ok": True, "message": "Connected, but the status page lists no monitors with heartbeats."} + shown = ", ".join(names[:15]) + (" …" if len(names) > 15 else "") + return {"ok": True, "message": f"Connected — {len(names)} monitor(s): {shown}"} + + if kind == "portainer": + base_url = _form_str(form, "base_url", "").strip().rstrip("/") + if not base_url: + return {"ok": False, "error": "URL required"} + if _form_str(form, "token_source", "env") == "inline": + token = _form_str(form, "api_token", "") + if token == _UNCHANGED_SECRET_PLACEHOLDER: + saved = app.integrations.portainer.environments.get(_form_str(form, "env_original", "")) + token = saved.api_token if saved else "" + if not token: + return {"ok": False, "error": "API token required"} + else: + var = _form_str(form, "api_token_env", "").strip() + token = os.environ.get(var, "") if var else "" + if not token: + return {"ok": False, "error": f"environment variable {var!r} is not set in the Inspector container"} + endpoint_id = _form_int(form, "endpoint_id", 1) + try: + resp = requests.get( + f"{base_url}/api/stacks", headers={"X-API-Key": token}, + timeout=10.0, verify=_form_bool(form, "verify_ssl"), + ) + except requests.RequestException as exc: + return {"ok": False, "error": f"request failed: {exc}"} + if resp.status_code in (401, 403): + return {"ok": False, "error": f"HTTP {resp.status_code} — the API token was rejected"} + if resp.status_code >= 400: + return {"ok": False, "error": f"HTTP {resp.status_code}: {resp.text[:150]}"} + try: + stacks = resp.json() or [] + except ValueError: + return {"ok": False, "error": "response was not Portainer API JSON — check the URL"} + if not isinstance(stacks, list): + return {"ok": False, "error": "response was not Portainer API JSON — check the URL"} + names = sorted(st.get("Name", "?") for st in stacks if st.get("EndpointId") == endpoint_id) + if not names: + return {"ok": True, "message": f"Connected, but no stacks found on endpoint {endpoint_id}. Check the endpoint ID."} + shown = ", ".join(names[:15]) + (" …" if len(names) > 15 else "") + return {"ok": True, "message": f"Connected — {len(names)} stack(s) on endpoint {endpoint_id}: {shown}"} + + return {"ok": False, "error": f"unknown integration {kind!r}"} + + _RESTART_LABEL = ( '' "requires restart" @@ -1489,6 +1755,26 @@ def _settings_secret_field(label: str, name: str, has_value: bool) -> str: ) +def _settings_select( + label: str, name: str, value: str, options: list[tuple[str, str]], + *, hint: str = "", onchange: str = "", +) -> str: + opts = "".join( + f'' + for v, text in options + ) + on = f' onchange="{onchange}"' if onchange else "" + hint_html = f'
{_escape(hint)}
' if hint else "" + return ( + f'
' + f'{hint_html}
' + ) + + +def _warn(text: str) -> str: + return f'
⚠ {text}
' + + # Each tuple: (slug, label, icon, subtitle_fn, group). # Groups show up as uppercase headers in the left rail. def _subtitle_inspector(app: AppConfig) -> str: @@ -1527,6 +1813,20 @@ def _subtitle_notification_mute(app: AppConfig) -> str: return f"{'on' if nm.enabled else 'off'} · {nm.default_hours}h default" +def _subtitle_uptime_kuma(app: AppConfig) -> str: + uk = app.integrations.uptime_kuma + if not uk.enabled: + return "off" + n = sum(1 for c in app.containers if c.uptime_kuma.enabled) + return f"on · {n} container(s)" + + +def _subtitle_portainer(app: AppConfig) -> str: + pt = app.integrations.portainer + n = len(pt.environments) + return f"{'on' if pt.enabled else 'off'} · {n} environment{'s' if n != 1 else ''}" + + _SETTINGS_SECTIONS = [ ("inspector", "Inspector loop", "⚙", _subtitle_inspector, "Core"), ("notifications", "Notifications", "🔔", _subtitle_notifications, "Core"), @@ -1535,6 +1835,8 @@ _SETTINGS_SECTIONS = [ ("health_summary", "Health summary", "📋", _subtitle_health_summary, "Data"), ("maintenance", "Maintenance", "🔧", _subtitle_maintenance, "Scheduling"), ("notification_mute", "Notification mute","🔇", _subtitle_notification_mute, "Scheduling"), + ("uptime_kuma", "Uptime Kuma", "📡", _subtitle_uptime_kuma, "Integrations"), + ("portainer", "Portainer", "🐳", _subtitle_portainer, "Integrations"), ] _SETTINGS_SECTION_SLUGS = {slug for slug, *_ in _SETTINGS_SECTIONS} @@ -1621,18 +1923,18 @@ def _settings_form_with_test(section: str, title: str, body: str, test_row: str, _TEST_NOTIFY_JS = """ " +) + + +def _portainer_env_form(env_name: str, env, csrf_token: str, *, suggest_name: str = "") -> str: + """Add/edit form for one Portainer environment. `env` is None for the add form.""" + inline = bool(env and env.api_token and not env.api_token_env) + source = "inline" if inline else "env" + body = ( + f'' + '
' + + _settings_field("name", "env_name", env_name or suggest_name, + hint="Your own label, referenced by containers") + + _settings_field("endpoint_id", "endpoint_id", str(env.endpoint_id if env else 1), kind="number", + hint="Portainer → Environments; the number in the URL (usually 1 or 2)") + + "
" + + _settings_field("base_url", "base_url", env.base_url if env else "", + hint="e.g. http://192.168.1.10:9000 or https://portainer.example.com") + + _settings_select( + "API token stored as", "token_source", source, + [("env", "Environment variable (recommended)"), ("inline", "Saved in config.yml")], + hint="Create a token in Portainer → My account → Access tokens.", + onchange="InspectorTokenSrc(this)", + ) + + f'
' + + _settings_field("environment variable name", "api_token_env", + (env.api_token_env if env else "") or "PORTAINER_API_TOKEN", + hint="Set this variable on the Inspector container (Portainer → stack → Environment variables), then redeploy Inspector.") + + "
" + + f'
' + + _settings_secret_field("api_token", "api_token", bool(env and env.api_token)) + + "
" + + _settings_field("verify_ssl", "verify_ssl", "on" if (env.verify_ssl if env else False) else "", kind="switch", + hint="Turn off for Portainer's default self-signed certificate or plain http") + + _test_row("portainer", "Lists this environment's stacks using the values above (even if unsaved).") + ) + delete = "" + if env is not None: + delete = ( + '" + ) + label = "environment" if env is not None else "new environment" + return ( + '
' + f'

{_escape(env_name) if env is not None else "Add environment"}

' + f'' + f"{body}" + f'
{delete}' + '
" + ) + + +def _portainer_section_body(app: AppConfig, csrf_token: str) -> str: + pt = app.integrations.portainer + env_options = [("", "Automatic (only if one environment exists)")] + [(n, n) for n in pt.environments] + body = ( + _settings_field("enabled", "enabled", "on" if pt.enabled else "", kind="switch") + + _settings_select("default_environment", "default_environment", pt.default_environment, env_options, + hint="Used by containers that don't pick an environment") + + '
' + + _settings_field("stack_redeploy_cooldown_seconds", "stack_redeploy_cooldown_seconds", + str(pt.stack_redeploy_cooldown_seconds), kind="number", + hint="Minimum seconds between redeploys of the same stack") + + _settings_field("post_redeploy_wait_seconds", "post_redeploy_wait_seconds", + str(pt.post_redeploy_wait_seconds), kind="number", + hint="Wait this long after a redeploy before checking again") + + "
" + ) + users = [c.name for c in app.containers if c.recovery_action in _PORTAINER_ACTIONS] + warn = "" + if pt.enabled and not pt.environments: + warn = _warn("Enabled, but no environment is configured yet. Add one below.") + elif not pt.enabled and users: + warn = _warn(f"Off, but {_escape(', '.join(users))} use stack redeploy. Their redeploys will fail until this is enabled.") + envs = "".join(_portainer_env_form(n, e, csrf_token) for n, e in pt.environments.items()) + used_by = ( + f'

Containers using stack redeploy: {_escape(", ".join(users))}

' + if users else '

No container uses stack redeploy yet. Set it per container under Recovery.

' + ) + add_form = _portainer_env_form("", None, csrf_token, suggest_name="" if pt.environments else "local") + return ( + '

Portainer

' + '

Lets Inspector redeploy a whole Portainer stack. Needed for containers that share ' + "another container's network (for example apps routed through a VPN container): when the VPN container is " + "recreated, a plain restart fails and only a redeploy reconnects them.

" + + '
' + warn + + _settings_form("portainer", "Portainer", body, csrf_token) + "
" + + '

Environments

' + used_by + + envs + add_form + "
" + + _TOKEN_SOURCE_JS + ) + + def _render_flash(flash: str) -> str: if not flash: return "" @@ -1855,12 +2285,27 @@ def _render_settings_page(app: AppConfig, config_ref: ConfigRef, csrf_token: str parts.append(_settings_body_for(section, app, csrf_token)) parts.append("
\n") parts.append(_render_diag_strip(config_ref)) - if section == "notifications": + if section in ("notifications", "uptime_kuma", "portainer"): parts.append(_TEST_NOTIFY_JS) parts.append(_PAGE_TAIL) return "".join(parts).encode("utf-8") +def _container_extras(c: ContainerConfig) -> str: + """One-line summary of the non-default checks / recovery for the grid tile.""" + bits = [] + if c.http_check.enabled: + bits.append("HTTP check") + if c.uptime_kuma.enabled: + bits.append("Uptime Kuma") + if c.depends_on: + bits.append(f"after {c.depends_on}" + (" (VPN)" if c.vpn_routed else "")) + label = next((lbl for v, lbl, _ in _RECOVERY_ACTION_OPTIONS if v == c.recovery_action and v), "") + if label: + bits.append(label.lower()) + return " · ".join(bits) + + def _render_containers_page(app: AppConfig, config_ref: ConfigRef, states: dict, flash: str) -> bytes: parts = [_page_head(refresh=False, active="containers")] parts.append(_render_flash(flash)) @@ -1883,6 +2328,8 @@ def _render_containers_page(app: AppConfig, config_ref: ConfigRef, states: dict, state = states.get(c.name) status = state.current_status if state and state.current_status else "—" restart_on = ", ".join(sorted(c.restart_on)) if c.restart_on else "observe only" + extras = _container_extras(c) + extras_html = f'Extras: {_escape(extras)}' if extras else "" parts.append( f'' @@ -1894,7 +2341,8 @@ def _render_containers_page(app: AppConfig, config_ref: ConfigRef, states: dict, f'Max errors: {c.max_error_count}' f'Cooldown: {c.cooldown_seconds}s' f'Restart on: {_escape(restart_on)}' - "\n" + + extras_html + + "\n" ) parts.append("\n") parts.append("\n") @@ -1957,9 +2405,106 @@ _RESTART_ON_JS = ( ) +_RECOVERY_JS = ( + "" +) + + +def _render_container_advanced(name: str, cfg, app: AppConfig) -> str: + """Reachability / Dependencies / Recovery groups for the container form.""" + hc = getattr(cfg, "http_check", None) or HttpCheckConfig() + ukm = getattr(cfg, "uptime_kuma", None) or UptimeKumaMonitorConfig() + uk_global = app.integrations.uptime_kuma + pt = app.integrations.portainer + action = getattr(cfg, "recovery_action", "") or "" + if action == "restart_container": # same as the default option + action = "" + uses_portainer = action in _PORTAINER_ACTIONS + timeout = hc.timeout_seconds + timeout_str = str(int(timeout)) if float(timeout).is_integer() else str(timeout) + + uk_warn = "" + if not (uk_global.enabled and uk_global.url and uk_global.status_page_slug): + uk_warn = _warn('The Uptime Kuma integration is off. Set it up in ' + 'Settings → Uptime Kuma or this check is ignored.') + reach = ( + '

Reachability

' + '

Docker can say a container is healthy when nobody can reach it, for example when ' + "its VPN tunnel dropped. A failed check reports UNREACHABLE; tick UNREACHABLE above to restart on it.

" + '

HTTP check

' + + _settings_field("enabled", "http_check_enabled", "on" if hc.enabled else "", kind="switch") + + _settings_field("url", "http_check_url", hc.url, + hint="Address Inspector requests, e.g. http://192.168.1.10:9696/ping. Use an IP or a name Inspector can resolve.") + + '
' + + _settings_field("timeout_seconds", "http_check_timeout_seconds", timeout_str, kind="number") + + _settings_field("expected_status", "http_check_expected_status", str(hc.expected_status), kind="number", + hint="Exact HTTP code a healthy service returns (often 200; some apps return 401 or 302)") + + "
" + '

Uptime Kuma monitor

' + + _settings_field("enabled", "uptime_kuma_enabled", "on" if ukm.enabled else "", kind="switch") + + _settings_field("monitor_name", "uptime_kuma_monitor_name", ukm.monitor_name, + hint="Exact monitor name as shown on your status page. If both checks are on, the HTTP check wins.") + + uk_warn + + "
" + ) + + others = [c.name for c in app.containers if c.name != name] + dep_value = getattr(cfg, "depends_on", "") or "" + dep_options = [("", "— none —")] + [(n, n) for n in others] + if dep_value and dep_value not in others: + dep_options.append((dep_value, f"{dep_value} (not configured)")) + deps = ( + '

Dependencies

' + '

Check this container after the one it relies on, and hold off recovering it ' + "while that one is still failing.

" + + _settings_select("depends_on", "depends_on", dep_value, dep_options, + hint="e.g. the VPN container, for apps that use network_mode: container:") + + _settings_field("vpn_routed", "vpn_routed", "on" if getattr(cfg, "vpn_routed", False) else "", kind="switch", + hint="Routes through the container above. Logs why recovery is deferred while it is down.") + + "
" + ) + + action_help = '
    ' + "".join( + f"
  • {_escape(label)} — {_escape(desc)}
  • " for _, label, desc in _RECOVERY_ACTION_OPTIONS + ) + "
" + env_value = getattr(cfg, "portainer_environment", "") or "" + default_label = f"Default ({pt.default_environment})" if pt.default_environment else "Default" + env_options = [("", default_label)] + [(n, n) for n in pt.environments] + if env_value and env_value not in pt.environments: + env_options.append((env_value, f"{env_value} (not configured)")) + pt_warn = "" + if not pt.environments: + pt_warn = _warn('Portainer isn\'t set up yet. Add an environment in ' + 'Settings → Portainer before relying on a redeploy.') + elif not pt.enabled: + pt_warn = _warn('The Portainer integration is off. Turn it on in ' + 'Settings → Portainer or redeploys will fail.') + hidden = "" if uses_portainer else " hidden" + recovery = ( + '

Recovery

' + '

What Inspector does once the restart conditions above are met.

' + + _settings_select("recovery_action", "recovery_action", action, + [(v, label) for v, label, _ in _RECOVERY_ACTION_OPTIONS], + onchange="InspectorRecovery(this)") + + action_help + + f'
' + + '
' + + _settings_select("portainer_environment", "portainer_environment", env_value, env_options) + + _settings_field("portainer_stack_name", "portainer_stack_name", + getattr(cfg, "portainer_stack_name", "") or "", + hint="Stack name exactly as shown in Portainer → Stacks") + + "
" + pt_warn + "
" + + "
" + + _RECOVERY_JS + ) + return '' + reach + deps + recovery + + def _render_container_form( name: str, cfg, csrf_token: str, action: str, submit_label: str, - *, show_name_field: bool = False, show_delete: bool = False, + *, app: AppConfig, show_name_field: bool = False, show_delete: bool = False, ) -> str: """Build the container edit form. `cfg` may be a ContainerConfig or a dummy with defaults.""" current_restart_on = getattr(cfg, "restart_on", []) or [] @@ -2021,6 +2566,7 @@ def _render_container_form( requires_restart=True) + _render_color_input("color", getattr(cfg, "color", "")) + restart_on_html + + _render_container_advanced(name, cfg, app) ) delete_btn = "" if show_delete: @@ -2056,6 +2602,7 @@ def _render_container_edit_page(app: AppConfig, config_ref: ConfigRef, name: str name, cfg, csrf_token, action=f"/containers/{_escape(name)}", submit_label="Save container", + app=app, show_delete=True, ) parts = [_page_head(refresh=False, active="containers")] @@ -2065,7 +2612,7 @@ def _render_container_edit_page(app: AppConfig, config_ref: ConfigRef, name: str parts.append('
') parts.append( f'

Container — {_escape(name)}

' - '

Restart rules Inspector applies when this container\'s health changes.

' + '

How Inspector checks this container and what it does when it fails.

' ) parts.append(form_html) parts.append("
\n") @@ -2091,6 +2638,7 @@ def _render_container_new_page(app: AppConfig, config_ref: ConfigRef, csrf_token "", _NewContainerDefaults(), csrf_token, action="/containers/new", submit_label="Create container", + app=app, show_name_field=True, ) parts = [_page_head(refresh=False, active="containers")] @@ -2101,9 +2649,7 @@ def _render_container_new_page(app: AppConfig, config_ref: ConfigRef, csrf_token parts.append( '

New container

' '

Add a container to config.yml. ' - "The new entry is picked up automatically on the next polling cycle; " - "advanced fields (Portainer stack, http_check, uptime_kuma, depends_on, " - "vpn_routed) remain YAML-only for now.

" + "The new entry is picked up automatically on the next polling cycle.

" ) parts.append(form_html) parts.append("\n") @@ -2443,6 +2989,8 @@ class _Handler(BaseHTTPRequestHandler): # else matches the section slug. if section in ("gotify", "pushover"): return_path = "/settings/notifications" + elif section == "portainer_env": + return_path = "/settings/portainer" else: return_path = f"/settings/{section}" try: @@ -2454,8 +3002,9 @@ class _Handler(BaseHTTPRequestHandler): self.send_header("Content-Length", "0") self.end_headers() return + ok_label = "Portainer environment" if section == "portainer_env" else section self.send_response(HTTPStatus.SEE_OTHER) - self.send_header("Location", f"{return_path}?flash={_url_quote('ok:' + section)}") + self.send_header("Location", f"{return_path}?flash={_url_quote('ok:' + ok_label)}") self.send_header("Content-Length", "0") self.end_headers() return @@ -2526,6 +3075,13 @@ class _Handler(BaseHTTPRequestHandler): self._send_bytes(HTTPStatus.OK, "application/json", body) return + # POST /api/test-integration/ — check an Uptime Kuma / Portainer connection + if len(parts) == 3 and parts[0] == "api" and parts[1] == "test-integration": + payload = _run_test_integration(parts[2], form, app) + body = json.dumps(payload).encode("utf-8") + self._send_bytes(HTTPStatus.OK, "application/json", body) + return + self._send_bytes(HTTPStatus.NOT_FOUND, "text/plain; charset=utf-8", b"not found") diff --git a/src/inspector/main.py b/src/inspector/main.py index 302686f..bcc0635 100755 --- a/src/inspector/main.py +++ b/src/inspector/main.py @@ -417,6 +417,7 @@ def run_loop(config_ref: ConfigRef) -> int: check_order = _topological_order(enabled_configs) portainer_client = PortainerClient(app.integrations.portainer) + integrations_cfg = app.integrations stack_cooldowns: dict[str, datetime] = {} dashboard = DashboardServer( @@ -456,6 +457,14 @@ def run_loop(config_ref: ConfigRef) -> int: enabled_configs = current_enabled check_order = _topological_order(enabled_configs) + # Integration clients hold their config; rebuild them when the + # settings page changes Uptime Kuma / Portainer. + if app.integrations != integrations_cfg: + integrations_cfg = app.integrations + uptime_kuma_client = UptimeKumaClient(integrations_cfg.uptime_kuma) + portainer_client = PortainerClient(integrations_cfg.portainer) + log.info("Integration settings changed; reloaded Uptime Kuma / Portainer clients") + uptime_kuma_snapshot = uptime_kuma_client.fetch() for name in check_order: cfg = enabled_configs[name] diff --git a/src/inspector/uptime_kuma.py b/src/inspector/uptime_kuma.py index fb82546..9772dd4 100755 --- a/src/inspector/uptime_kuma.py +++ b/src/inspector/uptime_kuma.py @@ -41,20 +41,26 @@ class UptimeKumaClient: and bool(self._config.status_page_slug) ) + def _heartbeat_url(self) -> str: + return f"{self._config.url}/api/status-page/heartbeat/{self._config.status_page_slug}" + def fetch(self) -> HeartbeatSnapshot: if not self.enabled: return _EMPTY - url = f"{self._config.url}/api/status-page/heartbeat/{self._config.status_page_slug}" try: - resp = requests.get(url, timeout=self._timeout) - resp.raise_for_status() - data = resp.json() - return self._parse(data) + return self.fetch_or_raise() except Exception as exc: - log.warning("Uptime Kuma heartbeat fetch failed (%s): %s", url, exc) + log.warning("Uptime Kuma heartbeat fetch failed (%s): %s", self._heartbeat_url(), exc) return _EMPTY + def fetch_or_raise(self) -> HeartbeatSnapshot: + """Like fetch(), but lets request/parse errors propagate (used by the + settings page's "Test connection" button).""" + resp = requests.get(self._heartbeat_url(), timeout=self._timeout) + resp.raise_for_status() + return self._parse(resp.json()) + def _parse(self, data: dict) -> HeartbeatSnapshot: # Expected shape: # {"publicGroupList": [{"monitorList": [{"id": 1, "name": "..."}]}],