From 559443174927f0d50a8cfd5884e85eb47c47408c Mon Sep 17 00:00:00 2001 From: root Date: Sun, 9 Aug 2026 13:17:44 +0200 Subject: [PATCH] callback container integrated fully --- .env.example | 5 +- README.md | 13 +++- SECURITY.md | 6 ++ VERSION | 2 +- app/i18n.py | 13 +++- app/main.py | 87 +++++++++++++++++-------- app/templates/settings_software.html | 32 +++++++-- app/version.py | 2 +- docker-compose.yml | 10 +-- docs/INSTALLATION.md | 77 +++++++++++++++++----- docs/version-history/README.md | 4 +- docs/version-history/UPDATE-0.5.5.65.md | 10 +++ 12 files changed, 196 insertions(+), 65 deletions(-) create mode 100644 docs/version-history/UPDATE-0.5.5.65.md diff --git a/.env.example b/.env.example index e378df8..a598bf4 100644 --- a/.env.example +++ b/.env.example @@ -10,5 +10,8 @@ LOCAL_ADMIN_PASSWORD=CHANGE_ME_MIN_12_CHARS BACKUP_INTERVAL_HOURS=8 BACKUP_RETENTION_DAYS=3 -# Optional host port for the callback-only Docker Compose profile. +# Dedicated callback-only listener. It starts with the normal Compose stack. +# Use 127.0.0.1 when a reverse proxy runs on the same host; use a specific LAN/DMZ +# address or 0.0.0.0 only when the listener must be reachable directly. +CALLBACK_BIND_IP=127.0.0.1 CALLBACK_PORT=8090 diff --git a/README.md b/README.md index 57db8a0..27060e3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# AssetManager 0.5.5.64 +# AssetManager 0.5.5.65 AssetManager is a self-hosted web application for managing IT equipment and other organizational assets. It provides asset inventory, software inventory, remote job execution, reporting, privacy/retention documentation, and optional integration with MeshCentral. @@ -81,7 +81,7 @@ data/ └── scripts/ deployed scripts ``` -These directories and `.env` may contain credentials, personal data, or internal operational information and must not be committed to a public repository. +Persistent runtime data and local secrets are intentionally kept outside the application image. See [SECURITY.md](SECURITY.md) and [docs/INSTALLATION.md](docs/INSTALLATION.md) for deployment guidance. ## Authentication @@ -95,6 +95,13 @@ MeshCentral integration is optional. AssetManager uses MeshCtrl for supported de MeshCentral and MeshCtrl are independent third-party projects and are not part of this repository. They are installed as dependencies during the Docker build. + +## Dedicated callback listener + +Docker deployments start a separate callback-only service together with the main application. The listener exposes only the software-job callback endpoint and its health check; the AssetManager web interface is not available on that port. `CALLBACK_BIND_IP` and `CALLBACK_PORT` control the host-side listener, while the public FQDN used by clients is configured in **Software and Jobs → Settings**. + +The settings page provides separate health checks for the internal callback service and the configured/public callback address. See [`docs/INSTALLATION.md`](docs/INSTALLATION.md) for reverse-proxy and DMZ examples. + ## Privacy and retention The application includes a **Security & Privacy** section for documenting processing activities, data categories, retention periods, recipients, international transfers, technical and organizational measures, and deletion checks. Reports can be exported as text, HTML, or Excel. @@ -115,7 +122,7 @@ Historical release notes are available in [`docs/version-history/`](docs/version ## Security -Do not commit credentials, tokens, private keys, personal data, runtime databases, logs, backups, or internal deployment details. Security reporting guidance is available in [SECURITY.md](SECURITY.md). +Keep deployment secrets and persistent runtime data outside source control. Security reporting and deployment guidance is available in [SECURITY.md](SECURITY.md). ## Third-party components diff --git a/SECURITY.md b/SECURITY.md index bb75e3c..f816efb 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -19,3 +19,9 @@ A report should include: - relevant log excerpts with all secrets removed Only test systems for which you have explicit authorization. + +## Deployment secrets and runtime data + +Keep `.env` and persistent runtime directories such as `data/config`, `data/postgres`, `data/uploads`, `data/logs`, and `data/backups` outside source control. They are deployment data rather than application source and can contain credentials or operational information. + +For Internet-facing callbacks, publish only the dedicated callback listener through the firewall or reverse proxy. The normal AssetManager web port should remain on the intended administrative network. diff --git a/VERSION b/VERSION index 4d1b92d..f8c741a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.5.64 +0.5.5.65 diff --git a/app/i18n.py b/app/i18n.py index 9929fe5..ffbc862 100644 --- a/app/i18n.py +++ b/app/i18n.py @@ -52,8 +52,8 @@ BASE_TRANSLATIONS = { "fields.data_type_user_reference": ("User reference", "Benutzer-Verweis"), "fields.data_type_status": ("Status", "Status"), "software.settings.dedicated_listener": ("Dedicated callback listener", "Separater Callback-Zugang"), -"software.settings.dedicated_listener_optional": ("Optional – callback API only", "Optional – nur Callback-API"), -"software.settings.dedicated_listener_help": ("For Internet/DMZ scenarios, the callback API can run as a separate Docker Compose service on host port {port}. This listener exposes only the callback endpoint and health check, never the AssetManager web interface. Configure the public FQDN above as the callback base URL.", "Für Internet-/DMZ-Szenarien kann die Callback-API als eigener Docker-Compose-Dienst auf Host-Port {port} laufen. Dieser Zugang stellt ausschließlich den Callback-Endpunkt und den Healthcheck bereit, niemals die AssetManager-Weboberfläche. Trage oben den öffentlichen FQDN als Callback-Basisadresse ein."), +"software.settings.dedicated_listener_active": ("Starts automatically with AssetManager", "Startet automatisch mit AssetManager"), +"software.settings.dedicated_listener_help": ("The callback-only service listens on {bind_ip}:{port}. It starts with the normal Docker Compose stack and exposes only the callback endpoint and health check, never the AssetManager web interface. Configure the public FQDN above as the callback base URL.", "Der reine Callback-Dienst lauscht auf {bind_ip}:{port}. Er startet zusammen mit dem normalen Docker-Compose-Stack und stellt ausschließlich den Callback-Endpunkt und den Healthcheck bereit, niemals die AssetManager-Weboberfläche. Trage oben den öffentlichen FQDN als Callback-Basisadresse ein."), "profile.title": ("User profile", "Benutzerprofil"), "profile.username": ("Username", "Benutzername"), "profile.display_name": ("Display name", "Anzeigename"), "profile.email": ("Email", "E-Mail"), "profile.source": ("Source", "Quelle"), "profile.last_login": ("Last sign-in", "Letzte Anmeldung"), "profile.change_password": ("Change password", "Passwort ändern"), @@ -833,6 +833,15 @@ BASE_TRANSLATIONS.update({ "software.settings.test_title": ("Callback test and live debug log", "Callback-Test und Live-Debugprotokoll"), "software.settings.test_help": ("The AssetManager calls the configured health endpoint and records DNS resolution, connection, HTTP status and response.", "Der AssetManager ruft den konfigurierten Health-Endpunkt auf und protokolliert DNS-Auflösung, Verbindung, HTTP-Status und Antwort."), "software.settings.run_test": ("Test callback", "Callback testen"), + "software.settings.run_internal_test": ("Test internal health check", "Internen Health-Check testen"), + "software.settings.run_external_test": ("Test public health check", "Öffentlichen Health-Check testen"), + "software.settings.internal_health_url": ("Internal callback health URL", "Interne Callback-Health-URL"), + "software.settings.test_result": ("Health-check result", "Health-Check-Ergebnis"), + "software.settings.test_reachable": ("Reachable", "Erreichbar"), + "software.settings.test_unreachable": ("Not reachable", "Nicht erreichbar"), + "software.settings.test_kind_internal": ("Internal callback service", "Interner Callback-Dienst"), + "software.settings.test_kind_external": ("Configured/public callback address", "Konfigurierte/öffentliche Callback-Adresse"), + "software.settings.response_time": ("Response time", "Antwortzeit"), "software.settings.log_auto_refresh": ("The log is refreshed automatically every three seconds.", "Das Protokoll wird automatisch alle drei Sekunden aktualisiert."), "software.settings.clear_log": ("Clear log", "Protokoll leeren"), "software.settings.no_log": ("No diagnostic entries yet.", "Noch keine Diagnoseeinträge vorhanden."), diff --git a/app/main.py b/app/main.py index fe78523..d53c08a 100644 --- a/app/main.py +++ b/app/main.py @@ -7778,15 +7778,24 @@ def settings_software_page(request: Request): _require_admin(request) settings = _software_settings() callback_base = str(settings.get("callback_base_url", "") or "").strip().rstrip("/") + callback_internal_base = str(os.getenv("CALLBACK_INTERNAL_BASE_URL", "http://callback:8001") or "http://callback:8001").strip().rstrip("/") return templates.TemplateResponse( "settings_software.html", { "request": request, "software_settings": settings, "callback_health_url": f"{callback_base}/api/software-callback/health" if callback_base else "", + "callback_internal_health_url": f"{callback_internal_base}/api/software-callback/health", + "callback_test_result": request.query_params.get("callback_test_result", ""), + "callback_test_kind": request.query_params.get("callback_test_kind", ""), + "callback_test_status": request.query_params.get("callback_test_status", ""), + "callback_test_ms": request.query_params.get("callback_test_ms", ""), + "callback_test_url": request.query_params.get("callback_test_url", ""), + "callback_test_message": request.query_params.get("callback_test_message", ""), "debug_log": _software_debug_tail(), "debug_log_path": str(SOFTWARE_CALLBACK_DEBUG_LOG), "callback_dedicated_port": str(os.getenv("CALLBACK_PORT", "8090") or "8090"), + "callback_bind_ip": str(os.getenv("CALLBACK_BIND_IP", "127.0.0.1") or "127.0.0.1"), }, ) @@ -7852,25 +7861,16 @@ def settings_software_save( ) -@app.post("/settings/software/test-callback") -def settings_software_test_callback(request: Request): - _require_admin(request) - settings = _software_settings() - callback_base = str(settings.get("callback_base_url", "") or "").strip().rstrip("/") - if not callback_base: - callback_base = str(request.base_url).rstrip("/") - health_url = f"{callback_base}/api/software-callback/health" - timeout = max(2, min(int(settings.get("callback_test_timeout_seconds", 10) or 10), 60)) - verify_tls = bool(settings.get("callback_test_verify_tls", True)) +def _run_callback_health_test(health_url: str, timeout: int, verify_tls: bool, test_kind: str) -> dict[str, str]: parsed = urllib.parse.urlsplit(health_url) - _software_debug_log("=" * 72, force=True) - _software_debug_log("CALLBACK TEST started", force=True) - _software_debug_log(f"Base URL: {callback_base}", force=True) + _software_debug_log(f"CALLBACK HEALTH TEST started | kind={test_kind}", force=True) _software_debug_log(f"Health URL: {health_url}", force=True) - _software_debug_log(f"Scheme: {parsed.scheme} | Host: {parsed.hostname} | Port: {parsed.port or ('443' if parsed.scheme == 'https' else '80')}", force=True) _software_debug_log(f"Timeout: {timeout}s | Verify TLS: {verify_tls}", force=True) + if parsed.scheme not in {"http", "https"} or not parsed.hostname: + return {"ok": "0", "status": "-", "ms": "-", "url": health_url, "message": "Invalid health URL."} + try: dns_started = time.monotonic() addresses = sorted({item[4][0] for item in socket.getaddrinfo(parsed.hostname, parsed.port or (443 if parsed.scheme == "https" else 80), type=socket.SOCK_STREAM)}) @@ -7886,37 +7886,68 @@ def settings_software_test_callback(request: Request): try: req = urllib.request.Request( health_url, - headers={"User-Agent": f"AssetManager/{application_version()} CallbackTest"}, + headers={"User-Agent": f"AssetManager/{application_version()} CallbackHealthTest"}, method="GET", ) with urllib.request.urlopen(req, timeout=timeout, context=context) as response: body = response.read(65536).decode("utf-8", errors="replace") elapsed = (time.monotonic() - request_started) * 1000 + status = str(response.status) _software_debug_log(f"HTTP STATUS: {response.status} {response.reason} ({elapsed:.1f} ms)", force=True) - _software_debug_log(f"Content-Type: {response.headers.get('Content-Type', '-')}", force=True) _software_debug_log(f"Response body: {body}", force=True) payload = json.loads(body) if response.status == 200 and payload.get("ok") is True: _software_debug_log("RESULT: Callback health endpoint is reachable.", force=True) - return RedirectResponse( - "/settings/software?toast_success=" + quote(_translate_request(request, "software.settings.test_success", "Callback endpoint is reachable.")), - status_code=303, - ) - raise ValueError("Health response did not contain ok=true") + return {"ok": "1", "status": status, "ms": f"{elapsed:.1f}", "url": health_url, "message": "OK"} + return {"ok": "0", "status": status, "ms": f"{elapsed:.1f}", "url": health_url, "message": "Health response did not contain ok=true."} except urllib.error.HTTPError as exc: + elapsed = (time.monotonic() - request_started) * 1000 body = exc.read(65536).decode("utf-8", errors="replace") _software_debug_log(f"HTTP ERROR: {exc.code} {exc.reason}", force=True) _software_debug_log(f"Response body: {body}", force=True) - message = f"HTTP {exc.code}: {exc.reason}" + return {"ok": "0", "status": str(exc.code), "ms": f"{elapsed:.1f}", "url": health_url, "message": f"HTTP {exc.code}: {exc.reason}"} except Exception as exc: + elapsed = (time.monotonic() - request_started) * 1000 _software_debug_log(f"REQUEST ERROR: {type(exc).__name__}: {exc}", force=True) - message = f"{type(exc).__name__}: {exc}" + return {"ok": "0", "status": "-", "ms": f"{elapsed:.1f}", "url": health_url, "message": f"{type(exc).__name__}: {exc}"} - _software_debug_log("RESULT: Callback health endpoint is not reachable.", force=True) - return RedirectResponse( - "/settings/software?toast_error=" + quote(_translate_request(request, "software.settings.test_failed", "Callback test failed: {error}", error=message)), - status_code=303, - ) + +def _callback_test_redirect(request: Request, result: dict[str, str], test_kind: str) -> RedirectResponse: + params = urllib.parse.urlencode({ + "callback_test_result": result.get("ok", "0"), + "callback_test_kind": test_kind, + "callback_test_status": result.get("status", "-"), + "callback_test_ms": result.get("ms", "-"), + "callback_test_url": result.get("url", ""), + "callback_test_message": result.get("message", ""), + }) + return RedirectResponse(f"/settings/software?{params}", status_code=303) + + +@app.post("/settings/software/test-callback") +@app.post("/settings/software/test-callback-external") +def settings_software_test_callback_external(request: Request): + _require_admin(request) + settings = _software_settings() + callback_base = str(settings.get("callback_base_url", "") or "").strip().rstrip("/") + if not callback_base: + callback_base = str(request.base_url).rstrip("/") + health_url = f"{callback_base}/api/software-callback/health" + timeout = max(2, min(int(settings.get("callback_test_timeout_seconds", 10) or 10), 60)) + verify_tls = bool(settings.get("callback_test_verify_tls", True)) + result = _run_callback_health_test(health_url, timeout, verify_tls, "external") + return _callback_test_redirect(request, result, "external") + + +@app.post("/settings/software/test-callback-internal") +def settings_software_test_callback_internal(request: Request): + _require_admin(request) + settings = _software_settings() + internal_base = str(os.getenv("CALLBACK_INTERNAL_BASE_URL", "http://callback:8001") or "http://callback:8001").strip().rstrip("/") + health_url = f"{internal_base}/api/software-callback/health" + timeout = max(2, min(int(settings.get("callback_test_timeout_seconds", 10) or 10), 60)) + result = _run_callback_health_test(health_url, timeout, True, "internal") + return _callback_test_redirect(request, result, "internal") @app.get("/settings/software/debug-log") diff --git a/app/templates/settings_software.html b/app/templates/settings_software.html index 5a3d1c4..86afd34 100644 --- a/app/templates/settings_software.html +++ b/app/templates/settings_software.html @@ -133,10 +133,10 @@
{{ t('software.settings.dedicated_listener') }}
- {{ t('software.settings.dedicated_listener_optional') }} -

{{ t('software.settings.dedicated_listener_help', port=callback_dedicated_port) }}

+ {{ t('software.settings.dedicated_listener_active') }} +

{{ t('software.settings.dedicated_listener_help', port=callback_dedicated_port, bind_ip=callback_bind_ip) }}

+ CALLBACK_BIND_IP={{ callback_bind_ip }} CALLBACK_PORT={{ callback_dedicated_port }} - docker compose --profile callback up -d callback
@@ -238,9 +238,29 @@

{{ t('software.settings.test_title') }}

{{ t('software.settings.test_help') }}

-
- -
+
+
+ +
+
+ +
+
+
+ {{ t('software.settings.internal_health_url') }} + {{ callback_internal_health_url }} +
+ {% if callback_test_result %} +
+ {{ t('software.settings.test_result') }} +
+ {% if callback_test_result == '1' %}✓ {{ t('software.settings.test_reachable') }}{% else %}✗ {{ t('software.settings.test_unreachable') }}{% endif %} +

{{ t('software.settings.test_kind_' ~ callback_test_kind) }}

+ {{ callback_test_url }} +
HTTP: {{ callback_test_status }} · {{ t('software.settings.response_time') }}: {{ callback_test_ms }} ms{% if callback_test_message and callback_test_message != 'OK' %} · {{ callback_test_message }}{% endif %}
+
+
+ {% endif %}
diff --git a/app/version.py b/app/version.py index 8b38c3a..34fca34 100644 --- a/app/version.py +++ b/app/version.py @@ -1,2 +1,2 @@ -APP_VERSION = "0.5.5.64" +APP_VERSION = "0.5.5.65" __version__ = APP_VERSION diff --git a/docker-compose.yml b/docker-compose.yml index 0dbccaf..54d7d8a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,6 +40,7 @@ services: LOCAL_ADMIN_USERNAME: ${LOCAL_ADMIN_USERNAME:-} LOCAL_ADMIN_PASSWORD: ${LOCAL_ADMIN_PASSWORD:-} CALLBACK_PORT: ${CALLBACK_PORT:-8090} + CALLBACK_BIND_IP: ${CALLBACK_BIND_IP:-127.0.0.1} ports: - "${APP_PORT:-8088}:8000" @@ -50,12 +51,11 @@ services: - ./data/backups:/data/backups - ./data/scripts:/scripts - # Optional callback-only listener for Internet/DMZ scenarios. - # Start with: docker compose --profile callback up -d - # Only the callback POST endpoint and callback health endpoint are exposed. + # Callback-only listener for Internet/DMZ scenarios. + # Starts together with AssetManager and exposes only the callback POST endpoint + # and callback health endpoint, never the AssetManager web interface. callback: build: . - profiles: ["callback"] container_name: assetmanager-callback restart: unless-stopped depends_on: @@ -71,7 +71,7 @@ services: SESSION_SECRET: ${SESSION_SECRET:-} command: ["uvicorn", "app.main:callback_app", "--host", "0.0.0.0", "--port", "8001"] ports: - - "${CALLBACK_PORT:-8090}:8001" + - "${CALLBACK_BIND_IP:-127.0.0.1}:${CALLBACK_PORT:-8090}:8001" volumes: - ./data/config:/app/config - ./data/logs:/app/data/logs diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 1fa3321..043c3c5 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -15,7 +15,7 @@ mkdir -p data/config data/uploads data/logs data/backups data/scripts data/postg Create a `.env` file. Use strong, unique values for all secrets: ```env -ASSETMANAGER_VERSION=0.5.5.47 +ASSETMANAGER_VERSION=0.5.5.65 APP_PORT=8088 POSTGRES_DB=assetmanager POSTGRES_USER=assetmanager @@ -27,6 +27,8 @@ MESHCENTRAL_PASSWORD= LDAP_BIND_PASSWORD= BACKUP_INTERVAL_HOURS=8 BACKUP_RETENTION_DAYS=3 +CALLBACK_BIND_IP=127.0.0.1 +CALLBACK_PORT=8090 ``` Keep `.env` private. Do not commit it to a public repository. @@ -56,7 +58,7 @@ services: retries: 10 app: - image: git.jusaro.de/roland/assetmanager:${ASSETMANAGER_VERSION:-0.5.5.47} + image: git.jusaro.de/roland/assetmanager:${ASSETMANAGER_VERSION:-0.5.5.65} container_name: assetmanager-app restart: unless-stopped depends_on: @@ -77,6 +79,8 @@ services: SESSION_SECRET: ${SESSION_SECRET:-} LOCAL_ADMIN_USERNAME: ${LOCAL_ADMIN_USERNAME:-} LOCAL_ADMIN_PASSWORD: ${LOCAL_ADMIN_PASSWORD:-} + CALLBACK_BIND_IP: ${CALLBACK_BIND_IP:-127.0.0.1} + CALLBACK_PORT: ${CALLBACK_PORT:-8090} ports: - "${APP_PORT:-8088}:8000" volumes: @@ -85,6 +89,34 @@ services: - ./data/logs:/app/data/logs - ./data/backups:/data/backups - ./data/scripts:/scripts + + callback: + image: git.jusaro.de/roland/assetmanager:${ASSETMANAGER_VERSION:-0.5.5.65} + container_name: assetmanager-callback + restart: unless-stopped + depends_on: + db: + condition: service_healthy + environment: + DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-assetmanager}:${POSTGRES_PASSWORD:-change-me}@db:5432/${POSTGRES_DB:-assetmanager} + APP_TITLE: AssetManager + APP_CONFIG: /app/config/config.json + APPINFO_PATH: /app/config/APPINFO.json + MESHCENTRAL_PASSWORD: ${MESHCENTRAL_PASSWORD:-} + LDAP_BIND_PASSWORD: ${LDAP_BIND_PASSWORD:-} + SESSION_SECRET: ${SESSION_SECRET:-} + command: ["uvicorn", "app.main:callback_app", "--host", "0.0.0.0", "--port", "8001"] + ports: + - "${CALLBACK_BIND_IP:-127.0.0.1}:${CALLBACK_PORT:-8090}:8001" + volumes: + - ./data/config:/app/config + - ./data/logs:/app/data/logs + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8001/api/software-callback/health', timeout=3).read()"] + interval: 30s + timeout: 5s + start_period: 10s + retries: 3 ``` If the registry is private, sign in once on the Docker host: @@ -186,7 +218,7 @@ Back up `.env` and the complete `data/` directory before significant updates. Set the desired fixed image version in `.env`, for example: ```env -ASSETMANAGER_VERSION=0.5.5.47 +ASSETMANAGER_VERSION=0.5.5.65 ``` Then update: @@ -280,39 +312,50 @@ docker compose pull docker compose up -d ``` -You can verify the active mount with: - -```bash -``` - Images copied directly into `./data/uploads/library` are available in the AssetManager image library after reloading the page. -## Optional dedicated callback port / DMZ listener +## Dedicated callback port / DMZ listener -AssetManager can expose software-job callbacks through a separate, callback-only Docker service. This is useful when remote devices must send callbacks through an Internet-facing FQDN while the normal AssetManager web interface remains internal. +AssetManager starts a separate callback-only Docker service together with the normal application. This is intended for remote devices that must return job results through an Internet-facing FQDN while the AssetManager web interface remains internal. -The optional service exposes only: +The callback service exposes only: - `GET /api/software-callback/health` - `POST /api/software-jobs/{job_id}/callback` It does not expose the AssetManager UI, login, assets, settings, static files, or administration routes. -Set the desired host port in `.env`: +Configure the host-side listener in `.env`: ```env +CALLBACK_BIND_IP=127.0.0.1 CALLBACK_PORT=8090 ``` -Start the additional service: +`127.0.0.1` is a good default when the reverse proxy runs on the same Docker host. If a reverse proxy or firewall on another system must reach the listener, bind it to the appropriate host interface or, when explicitly required, `0.0.0.0`. + +The normal stack is started with the usual command; no Compose profile is required: ```bash -docker compose --profile callback up -d callback +docker compose up -d ``` -The internal callback container listens on port `8001`; Docker maps the configured host port to it. A reverse proxy or firewall can therefore publish only this port/service. For example, a public `https://callback.example.org` can proxy to the Docker host on port `8090`. Configure that public URL under **Software and Jobs → Settings → Callback base URL**. +The internal callback container listens on port `8001`. Docker maps `CALLBACK_BIND_IP:CALLBACK_PORT` to that internal port. A public `https://callback.example.org` can therefore proxy only to the callback listener, while the normal AssetManager web interface remains on `APP_PORT` (default `8088`). -The standard AssetManager application continues to run on `APP_PORT` (default `8088`). The dedicated callback service is optional; installations that do not enable the `callback` profile behave as before. +Configure the public URL under **Software and Jobs → Settings → Callback base URL**: -For image-based installations, use the same AssetManager image for both the `app` and `callback` services instead of `build: .`. The callback service must always run the same version as the main application. +```text +https://callback.example.org +``` + +### Health checks from the GUI + +The callback settings page provides two separate tests: + +- **Internal health check** tests `http://callback:8001/api/software-callback/health` from the main AssetManager container. This verifies Docker networking and the callback-only service. +- **Public health check** tests the configured callback base URL through DNS/TLS/reverse proxy. This verifies the route that remote clients are expected to use. + +The GUI displays reachability, HTTP status, response time, and the tested URL. The detailed callback diagnostic log remains available on the same settings page. + +For image-based installations, use the same fixed AssetManager image version for both the `app` and `callback` services. diff --git a/docs/version-history/README.md b/docs/version-history/README.md index bcf3ff4..94de587 100644 --- a/docs/version-history/README.md +++ b/docs/version-history/README.md @@ -5,12 +5,14 @@ Release notes are stored outside the project root to keep the repository overview compact. -The current release is **0.5.5.47**. +The current release is **0.5.5.65**. Older notes are concise English summaries migrated from the original release documents. Git history remains authoritative for exact implementation details. ## Releases +- [0.5.5.65](UPDATE-0.5.5.65.md) + - [0.5.5.47](UPDATE-0.5.5.47.md) - [0.5.5.46](UPDATE-0.5.5.46.md) - [0.5.5.45](UPDATE-0.5.5.45.md) diff --git a/docs/version-history/UPDATE-0.5.5.65.md b/docs/version-history/UPDATE-0.5.5.65.md new file mode 100644 index 0000000..2fb6247 --- /dev/null +++ b/docs/version-history/UPDATE-0.5.5.65.md @@ -0,0 +1,10 @@ +# Update 0.5.5.65 + +## Callback service lifecycle, health tests, and documentation + +- The callback-only Docker service now starts with the normal Compose stack; the callback profile is no longer required. +- Added `CALLBACK_BIND_IP` so deployments can bind the callback listener to loopback, a selected host interface, or explicitly all interfaces. +- Added separate internal and public callback health checks in the GUI with tested URL, HTTP status, response time, and reachability result. +- The internal test validates Docker service networking; the public test validates the configured FQDN/reverse-proxy path. +- Updated installation, security, README, environment example, and callback deployment documentation. +- Reworded the repository warning about `.env` and runtime data as normal deployment/security guidance rather than a development note.