seperate port and container for external callback

This commit is contained in:
root
2026-08-09 10:24:53 +02:00
parent 3ff0794cfc
commit 639c2b68c8
13 changed files with 155 additions and 16 deletions
+40
View File
@@ -14,6 +14,46 @@ BASE_TRANSLATIONS = {
"common.import": ("Import", "Importieren"), "common.back": ("Back", "Zurück"), "common.yes": ("Yes", "Ja"), "common.no": ("No", "Nein"),
"common.active": ("Active", "Aktiv"), "common.actions": ("Actions", "Aktionen"), "common.name": ("Name", "Name"),
"common.description": ("Description", "Beschreibung"), "common.language": ("Language", "Sprache"), "common.all": ("All", "Alle"),
"fields.title": ("Fields", "Felder"),
"fields.add": ("Add field", "Feld hinzufügen"),
"fields.edit": ("Edit field", "Feld bearbeiten"),
"fields.catalog_help": ("Central labels and data types apply immediately to all categories, forms, tables and exports.", "Zentrale Bezeichnungen und Datentypen gelten sofort für alle Kategorien, Formulare, Tabellen und Exporte."),
"fields.field": ("Field", "Feld"),
"fields.label": ("Label", "Bezeichnung"),
"fields.data_type": ("Data type", "Datentyp"),
"fields.read_only": ("Read only", "Nur lesen"),
"fields.system": ("System", "System"),
"fields.stored_values": ("Stored values", "Gespeicherte Werte"),
"fields.delete_confirm": ("Delete this field and all values?", "Dieses Feld und alle gespeicherten Werte löschen?"),
"fields.confirm_values": ("Confirm values", "Werte bestätigen"),
"fields.technical_name": ("Technical field name", "Technischer Feldname"),
"fields.technical_name_help": ("Letters, numbers and underscores only.", "Nur Buchstaben, Zahlen und Unterstriche."),
"fields.automatic": ("automatic", "automatisch"),
"fields.default_field": ("Default field", "Standardfeld"),
"fields.searchable": ("Searchable", "Durchsuchbar"),
"fields.filterable": ("Filterable", "Filterbar"),
"fields.chart_enabled": ("Available for charts", "Für Diagramme verfügbar"),
"fields.excel_import": ("Excel import", "Excel-Import"),
"fields.excel_export": ("Excel export", "Excel-Export"),
"fields.bulk_edit": ("Bulk edit", "Sammelbearbeitung"),
"fields.data_type_short_text": ("Short text", "Kurztext"),
"fields.data_type_long_text": ("Long text", "Langtext"),
"fields.data_type_integer": ("Integer", "Ganzzahl"),
"fields.data_type_decimal": ("Decimal number", "Dezimalzahl"),
"fields.data_type_boolean": ("Boolean", "Ja/Nein"),
"fields.data_type_date": ("Date", "Datum"),
"fields.data_type_datetime": ("Date and time", "Datum und Uhrzeit"),
"fields.data_type_url": ("URL", "URL"),
"fields.data_type_email": ("E-mail address", "E-Mail-Adresse"),
"fields.data_type_ip": ("IP address", "IP-Adresse"),
"fields.data_type_json": ("JSON", "JSON"),
"fields.data_type_asset_reference": ("Asset reference", "Asset-Verweis"),
"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."),
"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"),
+12
View File
@@ -212,6 +212,15 @@ if not ldap_logger.handlers:
ldap_logger.addHandler(ldap_console_handler)
app = FastAPI(title=os.getenv("APP_TITLE", "AssetManager"))
# Dedicated callback-only ASGI application. It deliberately exposes only the
# software callback endpoint and its health endpoint so deployments may publish
# callbacks through a separate port/FQDN without exposing the AssetManager UI.
callback_app = FastAPI(
title=f"{os.getenv('APP_TITLE', 'AssetManager')} Callback",
docs_url=None,
redoc_url=None,
openapi_url=None,
)
_session_cfg = load_config().get("authentication", {})
_session_env = _session_cfg.get("session_secret_env", "SESSION_SECRET")
_session_secret = os.getenv(_session_env) or os.getenv("SESSION_SECRET") or "assetmanager-change-this-session-secret"
@@ -4870,6 +4879,7 @@ def category_delete(category_id: int, request: Request, db: Session = Depends(ge
@app.get("/api/software-callback/health")
@callback_app.get("/api/software-callback/health")
def software_callback_health():
return {
"ok": True,
@@ -6397,6 +6407,7 @@ def _process_software_job_callback(
@app.post("/api/software-jobs/{job_id}/callback")
@callback_app.post("/api/software-jobs/{job_id}/callback")
async def software_job_callback(job_id: int, request: Request, token: str = ""):
client_host = request.client.host if request.client else "-"
content_type = request.headers.get("content-type", "-")
@@ -7775,6 +7786,7 @@ def settings_software_page(request: Request):
"callback_health_url": f"{callback_base}/api/software-callback/health" if callback_base else "",
"debug_log": _software_debug_tail(),
"debug_log_path": str(SOFTWARE_CALLBACK_DEBUG_LOG),
"callback_dedicated_port": str(os.getenv("CALLBACK_PORT", "8090") or "8090"),
},
)
+8 -8
View File
@@ -1,11 +1,11 @@
{% extends 'base.html' %}{% block content %}<h1>{% if definition %}Edit field{% else %}Add field{% endif %}</h1>
{% extends 'base.html' %}{% block content %}<h1>{% if definition %}{{ t('fields.edit') }}{% else %}{{ t('fields.add') }}{% endif %}</h1>
<form method="post" class="settings-form">
{% if not definition %}<label>Technical field name<input name="field_name" pattern="[a-zA-Z0-9_]+" required><small>Letters, numbers and underscores only.</small></label>{% else %}<label>Technical field name<input value="{{definition.field_name}}" readonly></label>{% endif %}
<label>Label<input name="label" value="{{definition.label if definition else ''}}" required></label><label>Description<textarea name="description">{{definition.description or '' if definition else ''}}</textarea></label>
<label>Data type<select name="data_type">{% for key,label in data_types.items() %}<option value="{{key}}" {% if definition and definition.data_type==key %}selected{% endif %}>{{label}}</option>{% endfor %}</select></label><label>{{ t('fields.list_width', 'Breite in Listen (Pixel)') }}
<input type="number" name="list_width" min="40" max="800" step="10" value="{{ definition.list_width if definition and definition.list_width else '' }}" placeholder="automatisch">
<small>{{ t('fields.list_width_hint', 'Leer = automatische Breite. Sinnvoll sind etwa 60 für kurze Werte und 180300 für längere Texte.') }}</small>
{% if not definition %}<label>{{ t('fields.technical_name') }}<input name="field_name" pattern="[a-zA-Z0-9_]+" required><small>{{ t('fields.technical_name_help') }}</small></label>{% else %}<label>{{ t('fields.technical_name') }}<input value="{{definition.field_name}}" readonly></label>{% endif %}
<label>{{ t('fields.label') }}<input name="label" value="{{definition.label if definition else ''}}" required></label><label>{{ t('common.description') }}<textarea name="description">{{definition.description or '' if definition else ''}}</textarea></label>
<label>{{ t('fields.data_type') }}<select name="data_type">{% for key,label in data_types.items() %}<option value="{{key}}" {% if definition and definition.data_type==key %}selected{% endif %}>{{ t('fields.data_type_' ~ key, label) }}</option>{% endfor %}</select></label><label>{{ t('fields.list_width') }}
<input type="number" name="list_width" min="40" max="800" step="10" value="{{ definition.list_width if definition and definition.list_width else '' }}" placeholder="{{ t('fields.automatic') }}">
<small>{{ t('fields.list_width_hint') }}</small>
</label>
<div class="checkbox-row"><label class="checkbox-label"><input class="checkbox" type="checkbox" name="readonly" {% if definition and definition.readonly %}checked{% endif %}{% if definition and definition.field_name in ['mesh_node_id','mesh_mtype'] %} disabled{% endif %}> Read only</label>{% if definition and not definition.is_system %}<label class="checkbox-label"><input class="checkbox" type="checkbox" name="is_active" {% if definition.is_active %}checked{% endif %}> Active</label>{% endif %}<label class="checkbox-label"><input class="checkbox" type="checkbox" name="is_default" {% if definition and definition.is_default %}checked{% endif %}> Standardfeld</label></div>
<div class="checkbox-row">{% for key,label in [('searchable','Searchable'),('filterable','Filterable'),('chart_enabled','Available for charts'),('excel_import','Excel import'),('excel_export','Excel export'),('bulk_edit','Bulk edit')] %}<label class="checkbox-label"><input class="checkbox" type="checkbox" name="{{key}}" {% if not definition or definition|attr(key) %}checked{% endif %}> {{label}}</label>{% endfor %}</div>
<div class="checkbox-row"><label class="checkbox-label"><input class="checkbox" type="checkbox" name="readonly" {% if definition and definition.readonly %}checked{% endif %}{% if definition and definition.field_name in ['mesh_node_id','mesh_mtype','mesh_icon'] %} disabled{% endif %}> {{ t('fields.read_only') }}</label>{% if definition and not definition.is_system %}<label class="checkbox-label"><input class="checkbox" type="checkbox" name="is_active" {% if definition.is_active %}checked{% endif %}> {{ t('common.active') }}</label>{% endif %}<label class="checkbox-label"><input class="checkbox" type="checkbox" name="is_default" {% if definition and definition.is_default %}checked{% endif %}> {{ t('fields.default_field') }}</label></div>
<div class="checkbox-row">{% for key,label_key in [('searchable','fields.searchable'),('filterable','fields.filterable'),('chart_enabled','fields.chart_enabled'),('excel_import','fields.excel_import'),('excel_export','fields.excel_export'),('bulk_edit','fields.bulk_edit')] %}<label class="checkbox-label"><input class="checkbox" type="checkbox" name="{{key}}" {% if not definition or definition|attr(key) %}checked{% endif %}> {{ t(label_key) }}</label>{% endfor %}</div>
<button class="button button-save">💾 {{t('common.save')}}</button></form>{% endblock %}
+4 -4
View File
@@ -1,6 +1,6 @@
{% extends 'base.html' %}{% block content %}
<div class="toolbar"><h1>Fields</h1><div class="toolbar-actions"><a class="button" href="/settings/fields/new"> Add field</a><a class="button button-secondary" href="/settings">{{ t('common.back') }}</a></div></div>
<p class="muted">Central labels and data types apply immediately to all categories, forms, tables and exports.</p>
<div class="sticky-table management-table-scroll"><table class="data-table"><thead><tr><th>Field</th><th>Label</th><th>Description</th><th>Data type</th><th>Read only</th><th>System</th><th>Stored values</th><th>{{ t('common.actions') }}</th></tr></thead><tbody>
{% for d in definitions %}<tr><td><code>{{d.field_name}}</code></td><td>{{d.label}}</td><td>{{d.description or ''}}</td><td>{{data_types.get(d.data_type,d.data_type)}}</td><td>{{t('common.yes') if d.readonly else t('common.no')}}</td><td>{{t('common.yes') if d.is_system else t('common.no')}}</td><td>{{value_counts.get(d.id,0)}}</td><td><a class="button button-secondary" href="/settings/fields/{{d.id}}/edit">{{t('common.edit')}}</a>{% if not d.is_system %}<form class="inline-form" method="post" action="/settings/fields/{{d.id}}/delete" onsubmit="return confirm('Delete this field and all values?');"><label class="checkbox-label"><input class="checkbox" type="checkbox" name="confirm_values">Confirm values</label><button class="button button-danger">{{t('common.delete')}}</button></form>{% endif %}</td></tr>{% endfor %}
<div class="toolbar"><h1>{{ t('fields.title') }}</h1><div class="toolbar-actions"><a class="button" href="/settings/fields/new"> {{ t('fields.add') }}</a><a class="button button-secondary" href="/settings">{{ t('common.back') }}</a></div></div>
<p class="muted">{{ t('fields.catalog_help') }}</p>
<div class="sticky-table management-table-scroll"><table class="data-table"><thead><tr><th>{{ t('fields.field') }}</th><th>{{ t('fields.label') }}</th><th>{{ t('common.description') }}</th><th>{{ t('fields.data_type') }}</th><th>{{ t('fields.read_only') }}</th><th>{{ t('fields.system') }}</th><th>{{ t('fields.stored_values') }}</th><th>{{ t('common.actions') }}</th></tr></thead><tbody>
{% for d in definitions %}<tr><td><code>{{d.field_name}}</code></td><td>{{d.label}}</td><td>{{d.description or ''}}</td><td>{{ t('fields.data_type_' ~ d.data_type, data_types.get(d.data_type,d.data_type)) }}</td><td>{{t('common.yes') if d.readonly else t('common.no')}}</td><td>{{t('common.yes') if d.is_system else t('common.no')}}</td><td>{{value_counts.get(d.id,0)}}</td><td><a class="button button-secondary" href="/settings/fields/{{d.id}}/edit">{{t('common.edit')}}</a>{% if not d.is_system %}<form class="inline-form" method="post" action="/settings/fields/{{d.id}}/delete" onsubmit="return confirm({{ t('fields.delete_confirm')|tojson }});"><label class="checkbox-label"><input class="checkbox" type="checkbox" name="confirm_values">{{ t('fields.confirm_values') }}</label><button class="button button-danger">{{t('common.delete')}}</button></form>{% endif %}</td></tr>{% endfor %}
</tbody></table></div>{% endblock %}
+10
View File
@@ -129,6 +129,16 @@
<span>{{ t('software.settings.health_url') }}</span>
<code id="callback-health-preview">{{ callback_health_url or t('software.settings.automatic_url') }}</code>
</div>
<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>
<code>CALLBACK_PORT={{ callback_dedicated_port }}</code>
<code>docker compose --profile callback up -d callback</code>
</div>
</div>
</div>
</section>
+1 -1
View File
@@ -1,2 +1,2 @@
APP_VERSION = "0.5.5.63"
APP_VERSION = "0.5.5.64"
__version__ = APP_VERSION