callback container integrated fully

This commit is contained in:
root
2026-08-09 13:17:44 +02:00
parent 639c2b68c8
commit 5594431749
12 changed files with 196 additions and 65 deletions
+4 -1
View File
@@ -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
+10 -3
View File
@@ -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
+6
View File
@@ -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.
+1 -1
View File
@@ -1 +1 @@
0.5.5.64
0.5.5.65
+11 -2
View File
@@ -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
View File
@@ -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")
+25 -5
View File
@@ -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>
<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
View File
@@ -1,2 +1,2 @@
APP_VERSION = "0.5.5.64"
APP_VERSION = "0.5.5.65"
__version__ = APP_VERSION
+5 -5
View File
@@ -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
+60 -17
View File
@@ -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.
+3 -1
View File
@@ -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)
+10
View File
@@ -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.