callback container integrated fully
This commit is contained in:
+11
-2
@@ -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."),
|
||||
|
||||
+59
-28
@@ -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")
|
||||
|
||||
@@ -133,10 +133,10 @@
|
||||
<div class="software-callback-preview software-settings-wide">
|
||||
<span>{{ t('software.settings.dedicated_listener') }}</span>
|
||||
<div>
|
||||
<strong>{{ t('software.settings.dedicated_listener_optional') }}</strong>
|
||||
<p class="muted">{{ t('software.settings.dedicated_listener_help', port=callback_dedicated_port) }}</p>
|
||||
<strong>{{ t('software.settings.dedicated_listener_active') }}</strong>
|
||||
<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>docker compose --profile callback up -d callback</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -238,9 +238,29 @@
|
||||
<h2>{{ t('software.settings.test_title') }}</h2>
|
||||
<p class="muted">{{ t('software.settings.test_help') }}</p>
|
||||
</div>
|
||||
<form method="post" action="/settings/software/test-callback" class="inline-form">
|
||||
<button type="submit" class="button">{{ t('software.settings.run_test') }}</button>
|
||||
</form>
|
||||
<div class="inline-form">
|
||||
<form method="post" action="/settings/software/test-callback-internal" class="inline-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 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
|
||||
|
||||
Reference in New Issue
Block a user