callback container integrated fully
This commit is contained in:
+4
-1
@@ -10,5 +10,8 @@ LOCAL_ADMIN_PASSWORD=CHANGE_ME_MIN_12_CHARS
|
|||||||
BACKUP_INTERVAL_HOURS=8
|
BACKUP_INTERVAL_HOURS=8
|
||||||
BACKUP_RETENTION_DAYS=3
|
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
|
CALLBACK_PORT=8090
|
||||||
|
|||||||
@@ -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.
|
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
|
└── 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
|
## 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.
|
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
|
## 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.
|
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
|
## 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
|
## Third-party components
|
||||||
|
|
||||||
|
|||||||
@@ -19,3 +19,9 @@ A report should include:
|
|||||||
- relevant log excerpts with all secrets removed
|
- relevant log excerpts with all secrets removed
|
||||||
|
|
||||||
Only test systems for which you have explicit authorization.
|
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.
|
||||||
|
|||||||
+11
-2
@@ -52,8 +52,8 @@ BASE_TRANSLATIONS = {
|
|||||||
"fields.data_type_user_reference": ("User reference", "Benutzer-Verweis"),
|
"fields.data_type_user_reference": ("User reference", "Benutzer-Verweis"),
|
||||||
"fields.data_type_status": ("Status", "Status"),
|
"fields.data_type_status": ("Status", "Status"),
|
||||||
"software.settings.dedicated_listener": ("Dedicated callback listener", "Separater Callback-Zugang"),
|
"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_active": ("Starts automatically with AssetManager", "Startet automatisch mit AssetManager"),
|
||||||
"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_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.title": ("User profile", "Benutzerprofil"), "profile.username": ("Username", "Benutzername"),
|
||||||
"profile.display_name": ("Display name", "Anzeigename"), "profile.email": ("Email", "E-Mail"), "profile.source": ("Source", "Quelle"),
|
"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"),
|
"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_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.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_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.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.clear_log": ("Clear log", "Protokoll leeren"),
|
||||||
"software.settings.no_log": ("No diagnostic entries yet.", "Noch keine Diagnoseeinträge vorhanden."),
|
"software.settings.no_log": ("No diagnostic entries yet.", "Noch keine Diagnoseeinträge vorhanden."),
|
||||||
|
|||||||
+59
-28
@@ -7778,15 +7778,24 @@ def settings_software_page(request: Request):
|
|||||||
_require_admin(request)
|
_require_admin(request)
|
||||||
settings = _software_settings()
|
settings = _software_settings()
|
||||||
callback_base = str(settings.get("callback_base_url", "") or "").strip().rstrip("/")
|
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(
|
return templates.TemplateResponse(
|
||||||
"settings_software.html",
|
"settings_software.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
"request": request,
|
||||||
"software_settings": settings,
|
"software_settings": settings,
|
||||||
"callback_health_url": f"{callback_base}/api/software-callback/health" if callback_base else "",
|
"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": _software_debug_tail(),
|
||||||
"debug_log_path": str(SOFTWARE_CALLBACK_DEBUG_LOG),
|
"debug_log_path": str(SOFTWARE_CALLBACK_DEBUG_LOG),
|
||||||
"callback_dedicated_port": str(os.getenv("CALLBACK_PORT", "8090") or "8090"),
|
"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 _run_callback_health_test(health_url: str, timeout: int, verify_tls: bool, test_kind: str) -> dict[str, str]:
|
||||||
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))
|
|
||||||
parsed = urllib.parse.urlsplit(health_url)
|
parsed = urllib.parse.urlsplit(health_url)
|
||||||
|
|
||||||
_software_debug_log("=" * 72, force=True)
|
_software_debug_log("=" * 72, force=True)
|
||||||
_software_debug_log("CALLBACK TEST started", force=True)
|
_software_debug_log(f"CALLBACK HEALTH TEST started | kind={test_kind}", force=True)
|
||||||
_software_debug_log(f"Base URL: {callback_base}", force=True)
|
|
||||||
_software_debug_log(f"Health URL: {health_url}", 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)
|
_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:
|
try:
|
||||||
dns_started = time.monotonic()
|
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)})
|
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:
|
try:
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
health_url,
|
health_url,
|
||||||
headers={"User-Agent": f"AssetManager/{application_version()} CallbackTest"},
|
headers={"User-Agent": f"AssetManager/{application_version()} CallbackHealthTest"},
|
||||||
method="GET",
|
method="GET",
|
||||||
)
|
)
|
||||||
with urllib.request.urlopen(req, timeout=timeout, context=context) as response:
|
with urllib.request.urlopen(req, timeout=timeout, context=context) as response:
|
||||||
body = response.read(65536).decode("utf-8", errors="replace")
|
body = response.read(65536).decode("utf-8", errors="replace")
|
||||||
elapsed = (time.monotonic() - request_started) * 1000
|
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"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)
|
_software_debug_log(f"Response body: {body}", force=True)
|
||||||
payload = json.loads(body)
|
payload = json.loads(body)
|
||||||
if response.status == 200 and payload.get("ok") is True:
|
if response.status == 200 and payload.get("ok") is True:
|
||||||
_software_debug_log("RESULT: Callback health endpoint is reachable.", force=True)
|
_software_debug_log("RESULT: Callback health endpoint is reachable.", force=True)
|
||||||
return RedirectResponse(
|
return {"ok": "1", "status": status, "ms": f"{elapsed:.1f}", "url": health_url, "message": "OK"}
|
||||||
"/settings/software?toast_success=" + quote(_translate_request(request, "software.settings.test_success", "Callback endpoint is reachable.")),
|
return {"ok": "0", "status": status, "ms": f"{elapsed:.1f}", "url": health_url, "message": "Health response did not contain ok=true."}
|
||||||
status_code=303,
|
|
||||||
)
|
|
||||||
raise ValueError("Health response did not contain ok=true")
|
|
||||||
except urllib.error.HTTPError as exc:
|
except urllib.error.HTTPError as exc:
|
||||||
|
elapsed = (time.monotonic() - request_started) * 1000
|
||||||
body = exc.read(65536).decode("utf-8", errors="replace")
|
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"HTTP ERROR: {exc.code} {exc.reason}", force=True)
|
||||||
_software_debug_log(f"Response body: {body}", 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:
|
except Exception as exc:
|
||||||
|
elapsed = (time.monotonic() - request_started) * 1000
|
||||||
_software_debug_log(f"REQUEST ERROR: {type(exc).__name__}: {exc}", force=True)
|
_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(
|
def _callback_test_redirect(request: Request, result: dict[str, str], test_kind: str) -> RedirectResponse:
|
||||||
"/settings/software?toast_error=" + quote(_translate_request(request, "software.settings.test_failed", "Callback test failed: {error}", error=message)),
|
params = urllib.parse.urlencode({
|
||||||
status_code=303,
|
"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")
|
@app.get("/settings/software/debug-log")
|
||||||
|
|||||||
@@ -133,10 +133,10 @@
|
|||||||
<div class="software-callback-preview software-settings-wide">
|
<div class="software-callback-preview software-settings-wide">
|
||||||
<span>{{ t('software.settings.dedicated_listener') }}</span>
|
<span>{{ t('software.settings.dedicated_listener') }}</span>
|
||||||
<div>
|
<div>
|
||||||
<strong>{{ t('software.settings.dedicated_listener_optional') }}</strong>
|
<strong>{{ t('software.settings.dedicated_listener_active') }}</strong>
|
||||||
<p class="muted">{{ t('software.settings.dedicated_listener_help', port=callback_dedicated_port) }}</p>
|
<p class="muted">{{ t('software.settings.dedicated_listener_help', port=callback_dedicated_port, bind_ip=callback_bind_ip) }}</p>
|
||||||
|
<code>CALLBACK_BIND_IP={{ callback_bind_ip }}</code>
|
||||||
<code>CALLBACK_PORT={{ callback_dedicated_port }}</code>
|
<code>CALLBACK_PORT={{ callback_dedicated_port }}</code>
|
||||||
<code>docker compose --profile callback up -d callback</code>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -238,9 +238,29 @@
|
|||||||
<h2>{{ t('software.settings.test_title') }}</h2>
|
<h2>{{ t('software.settings.test_title') }}</h2>
|
||||||
<p class="muted">{{ t('software.settings.test_help') }}</p>
|
<p class="muted">{{ t('software.settings.test_help') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="/settings/software/test-callback" class="inline-form">
|
<div class="inline-form">
|
||||||
<button type="submit" class="button">{{ t('software.settings.run_test') }}</button>
|
<form method="post" action="/settings/software/test-callback-internal" class="inline-form">
|
||||||
</form>
|
<button type="submit" class="button button-secondary">{{ t('software.settings.run_internal_test') }}</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/settings/software/test-callback-external" class="inline-form">
|
||||||
|
<button type="submit" class="button">{{ t('software.settings.run_external_test') }}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="software-callback-preview software-settings-wide" style="margin-top:.75rem">
|
||||||
|
<span>{{ t('software.settings.internal_health_url') }}</span>
|
||||||
|
<code>{{ callback_internal_health_url }}</code>
|
||||||
|
</div>
|
||||||
|
{% if callback_test_result %}
|
||||||
|
<div class="software-callback-preview software-settings-wide" style="margin-top:.75rem">
|
||||||
|
<span>{{ t('software.settings.test_result') }}</span>
|
||||||
|
<div>
|
||||||
|
<strong>{% if callback_test_result == '1' %}✓ {{ t('software.settings.test_reachable') }}{% else %}✗ {{ t('software.settings.test_unreachable') }}{% endif %}</strong>
|
||||||
|
<p class="muted">{{ t('software.settings.test_kind_' ~ callback_test_kind) }}</p>
|
||||||
|
<code>{{ callback_test_url }}</code>
|
||||||
|
<div class="muted">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 %}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="software-debug-toolbar">
|
<div class="software-debug-toolbar">
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
APP_VERSION = "0.5.5.64"
|
APP_VERSION = "0.5.5.65"
|
||||||
__version__ = APP_VERSION
|
__version__ = APP_VERSION
|
||||||
|
|||||||
+5
-5
@@ -40,6 +40,7 @@ services:
|
|||||||
LOCAL_ADMIN_USERNAME: ${LOCAL_ADMIN_USERNAME:-}
|
LOCAL_ADMIN_USERNAME: ${LOCAL_ADMIN_USERNAME:-}
|
||||||
LOCAL_ADMIN_PASSWORD: ${LOCAL_ADMIN_PASSWORD:-}
|
LOCAL_ADMIN_PASSWORD: ${LOCAL_ADMIN_PASSWORD:-}
|
||||||
CALLBACK_PORT: ${CALLBACK_PORT:-8090}
|
CALLBACK_PORT: ${CALLBACK_PORT:-8090}
|
||||||
|
CALLBACK_BIND_IP: ${CALLBACK_BIND_IP:-127.0.0.1}
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "${APP_PORT:-8088}:8000"
|
- "${APP_PORT:-8088}:8000"
|
||||||
@@ -50,12 +51,11 @@ services:
|
|||||||
- ./data/backups:/data/backups
|
- ./data/backups:/data/backups
|
||||||
- ./data/scripts:/scripts
|
- ./data/scripts:/scripts
|
||||||
|
|
||||||
# Optional callback-only listener for Internet/DMZ scenarios.
|
# Callback-only listener for Internet/DMZ scenarios.
|
||||||
# Start with: docker compose --profile callback up -d
|
# Starts together with AssetManager and exposes only the callback POST endpoint
|
||||||
# Only the callback POST endpoint and callback health endpoint are exposed.
|
# and callback health endpoint, never the AssetManager web interface.
|
||||||
callback:
|
callback:
|
||||||
build: .
|
build: .
|
||||||
profiles: ["callback"]
|
|
||||||
container_name: assetmanager-callback
|
container_name: assetmanager-callback
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -71,7 +71,7 @@ services:
|
|||||||
SESSION_SECRET: ${SESSION_SECRET:-}
|
SESSION_SECRET: ${SESSION_SECRET:-}
|
||||||
command: ["uvicorn", "app.main:callback_app", "--host", "0.0.0.0", "--port", "8001"]
|
command: ["uvicorn", "app.main:callback_app", "--host", "0.0.0.0", "--port", "8001"]
|
||||||
ports:
|
ports:
|
||||||
- "${CALLBACK_PORT:-8090}:8001"
|
- "${CALLBACK_BIND_IP:-127.0.0.1}:${CALLBACK_PORT:-8090}:8001"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/config:/app/config
|
- ./data/config:/app/config
|
||||||
- ./data/logs:/app/data/logs
|
- ./data/logs:/app/data/logs
|
||||||
|
|||||||
+60
-17
@@ -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:
|
Create a `.env` file. Use strong, unique values for all secrets:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
ASSETMANAGER_VERSION=0.5.5.47
|
ASSETMANAGER_VERSION=0.5.5.65
|
||||||
APP_PORT=8088
|
APP_PORT=8088
|
||||||
POSTGRES_DB=assetmanager
|
POSTGRES_DB=assetmanager
|
||||||
POSTGRES_USER=assetmanager
|
POSTGRES_USER=assetmanager
|
||||||
@@ -27,6 +27,8 @@ MESHCENTRAL_PASSWORD=
|
|||||||
LDAP_BIND_PASSWORD=
|
LDAP_BIND_PASSWORD=
|
||||||
BACKUP_INTERVAL_HOURS=8
|
BACKUP_INTERVAL_HOURS=8
|
||||||
BACKUP_RETENTION_DAYS=3
|
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.
|
Keep `.env` private. Do not commit it to a public repository.
|
||||||
@@ -56,7 +58,7 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
|
|
||||||
app:
|
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
|
container_name: assetmanager-app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -77,6 +79,8 @@ services:
|
|||||||
SESSION_SECRET: ${SESSION_SECRET:-}
|
SESSION_SECRET: ${SESSION_SECRET:-}
|
||||||
LOCAL_ADMIN_USERNAME: ${LOCAL_ADMIN_USERNAME:-}
|
LOCAL_ADMIN_USERNAME: ${LOCAL_ADMIN_USERNAME:-}
|
||||||
LOCAL_ADMIN_PASSWORD: ${LOCAL_ADMIN_PASSWORD:-}
|
LOCAL_ADMIN_PASSWORD: ${LOCAL_ADMIN_PASSWORD:-}
|
||||||
|
CALLBACK_BIND_IP: ${CALLBACK_BIND_IP:-127.0.0.1}
|
||||||
|
CALLBACK_PORT: ${CALLBACK_PORT:-8090}
|
||||||
ports:
|
ports:
|
||||||
- "${APP_PORT:-8088}:8000"
|
- "${APP_PORT:-8088}:8000"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -85,6 +89,34 @@ services:
|
|||||||
- ./data/logs:/app/data/logs
|
- ./data/logs:/app/data/logs
|
||||||
- ./data/backups:/data/backups
|
- ./data/backups:/data/backups
|
||||||
- ./data/scripts:/scripts
|
- ./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:
|
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:
|
Set the desired fixed image version in `.env`, for example:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
ASSETMANAGER_VERSION=0.5.5.47
|
ASSETMANAGER_VERSION=0.5.5.65
|
||||||
```
|
```
|
||||||
|
|
||||||
Then update:
|
Then update:
|
||||||
@@ -280,39 +312,50 @@ docker compose pull
|
|||||||
docker compose up -d
|
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.
|
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`
|
- `GET /api/software-callback/health`
|
||||||
- `POST /api/software-jobs/{job_id}/callback`
|
- `POST /api/software-jobs/{job_id}/callback`
|
||||||
|
|
||||||
It does not expose the AssetManager UI, login, assets, settings, static files, or administration routes.
|
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
|
```env
|
||||||
|
CALLBACK_BIND_IP=127.0.0.1
|
||||||
CALLBACK_PORT=8090
|
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
|
```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.
|
||||||
|
|||||||
@@ -5,12 +5,14 @@
|
|||||||
|
|
||||||
Release notes are stored outside the project root to keep the repository overview compact.
|
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.
|
Older notes are concise English summaries migrated from the original release documents. Git history remains authoritative for exact implementation details.
|
||||||
|
|
||||||
## Releases
|
## Releases
|
||||||
|
|
||||||
|
- [0.5.5.65](UPDATE-0.5.5.65.md)
|
||||||
|
|
||||||
- [0.5.5.47](UPDATE-0.5.5.47.md)
|
- [0.5.5.47](UPDATE-0.5.5.47.md)
|
||||||
- [0.5.5.46](UPDATE-0.5.5.46.md)
|
- [0.5.5.46](UPDATE-0.5.5.46.md)
|
||||||
- [0.5.5.45](UPDATE-0.5.5.45.md)
|
- [0.5.5.45](UPDATE-0.5.5.45.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.
|
||||||
Reference in New Issue
Block a user