Log Startdialog added

This commit is contained in:
2026-09-29 11:40:50 +02:00
parent 1a8f654de9
commit c3f1ef43db
13 changed files with 274 additions and 123 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# AssetManager 0.5.5.91 # AssetManager 0.5.5.92
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.
+1 -1
View File
@@ -1 +1 @@
0.5.5.91 0.5.5.92
+85
View File
@@ -23,6 +23,8 @@ import ssl
import time import time
import urllib.error import urllib.error
import urllib.request import urllib.request
import importlib.metadata
import platform
from typing import Any from typing import Any
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from urllib.parse import quote from urllib.parse import quote
@@ -214,6 +216,79 @@ if not ldap_logger.handlers:
ldap_console_handler.setFormatter(logging.Formatter("LDAP | %(levelname)s | %(message)s")) ldap_console_handler.setFormatter(logging.Formatter("LDAP | %(levelname)s | %(message)s"))
ldap_logger.addHandler(ldap_console_handler) ldap_logger.addHandler(ldap_console_handler)
def _runtime_package_version(distribution_name: str) -> str:
try:
return importlib.metadata.version(distribution_name)
except importlib.metadata.PackageNotFoundError:
return "not installed"
except Exception:
return "unknown"
def _database_server_version() -> str:
try:
with engine.connect() as connection:
version = connection.exec_driver_sql("SELECT version()").scalar()
return str(version or "unknown").split(",", 1)[0]
except Exception as exc:
return f"unavailable ({exc.__class__.__name__})"
def _configured_callback_base_url() -> str:
try:
software = load_config().get("software", {}) or {}
configured = str(software.get("callback_base_url", "") or "").strip().rstrip("/")
if configured:
return configured
general = load_config().get("general", {}) or {}
return str(general.get("callback_base_url", "") or "").strip().rstrip("/")
except Exception:
return ""
def _startup_banner(role: str) -> None:
role_name = "Application" if role == "app" else "Callback"
callback_base = _configured_callback_base_url()
callback_bind = str(os.getenv("CALLBACK_BIND_IP", "127.0.0.1") or "127.0.0.1")
callback_port = str(os.getenv("CALLBACK_PORT", "8090") or "8090")
internal_callback = str(os.getenv("CALLBACK_INTERNAL_BASE_URL", "http://callback:8001") or "http://callback:8001").rstrip("/")
lines = [
"=" * 80,
f" AssetManager - {role_name} Startup",
"=" * 80,
f" AssetManager version : {application_version()}",
f" Container role : {role}",
f" Container version : {application_version()}",
f" Image reference : {os.getenv('ASSETMANAGER_IMAGE_REF', 'not provided')}",
f" Build version : {APP_VERSION}",
f" Build date : {os.getenv('ASSETMANAGER_BUILD_DATE', 'not provided')}",
f" Git commit : {os.getenv('ASSETMANAGER_GIT_COMMIT', 'not provided')}",
f" Python : {platform.python_version()}",
f" FastAPI : {_runtime_package_version('fastapi')}",
f" Uvicorn : {_runtime_package_version('uvicorn')}",
f" SQLAlchemy : {_runtime_package_version('sqlalchemy')}",
f" psycopg : {_runtime_package_version('psycopg')}",
f" Database : {_database_server_version()}",
f" Data directory : {DATA_ROOT}",
f" Startup time UTC : {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')}",
]
if role == "callback":
lines.extend([
"-" * 80,
f" Callback base URL : {callback_base or '<automatic / not fixed>'}",
f" Callback API URL : {(callback_base + '/api/software-jobs/<job_id>/callback') if callback_base else '<resolved by application per job>'}",
f" Callback health URL : {(callback_base + '/api/software-callback/health') if callback_base else '<resolved from callback base URL>'}",
f" Internal callback : {internal_callback}",
f" Published listener : {callback_bind}:{callback_port} -> container:8001",
])
lines.extend([
"-" * 80,
f" AssetManager {role} ready",
"=" * 80,
])
print("\n".join(lines), flush=True)
app = FastAPI(title=os.getenv("APP_TITLE", "AssetManager")) app = FastAPI(title=os.getenv("APP_TITLE", "AssetManager"))
# Dedicated callback-only ASGI application. It deliberately exposes only the # Dedicated callback-only ASGI application. It deliberately exposes only the
# software callback endpoint and its health endpoint so deployments may publish # software callback endpoint and its health endpoint so deployments may publish
@@ -956,6 +1031,16 @@ async def job_definition_script_generator(request: Request):
return JSONResponse({"script": script, "interpreter": interpreter, "action": action}) return JSONResponse({"script": script, "interpreter": interpreter, "action": action})
@app.on_event("startup")
def log_application_startup():
_startup_banner("app")
@callback_app.on_event("startup")
def log_callback_startup():
_startup_banner("callback")
@app.on_event("startup") @app.on_event("startup")
def start_backup_scheduler(): def start_backup_scheduler():
global BACKUP_THREAD global BACKUP_THREAD
+45 -5
View File
@@ -21,6 +21,7 @@ from .software_packages import build_deployment_user_script, load_package_manife
_DISPATCH_SLOT_LOCK = threading.Lock() _DISPATCH_SLOT_LOCK = threading.Lock()
_NEXT_DISPATCH_MONOTONIC = 0.0 _NEXT_DISPATCH_MONOTONIC = 0.0
_SOFTWARE_INVENTORY_MESHCTRL_LOCK = threading.Lock()
def _wait_for_dispatch_slot() -> tuple[float, float]: def _wait_for_dispatch_slot() -> tuple[float, float]:
@@ -744,6 +745,16 @@ def execute_job(job_id: int, token: str, callback_base: str) -> None:
f'Execution timeout: {execution_timeout_seconds or timeout} seconds', f'Execution timeout: {execution_timeout_seconds or timeout} seconds',
] ]
inventory_meshctrl_lock_acquired = False
if job.job_type == 'software_inventory':
lock_started = time.monotonic()
_SOFTWARE_INVENTORY_MESHCTRL_LOCK.acquire()
inventory_meshctrl_lock_acquired = True
diagnostics += [
'MeshCtrl isolation: software inventory serialized',
f'MeshCtrl isolation wait: {time.monotonic() - lock_started:.3f} seconds',
]
try:
if upload_required: if upload_required:
suffix=_script_extension(interpreter) suffix=_script_extension(interpreter)
temp_dir=tempfile.mkdtemp(prefix=f'assetmanager-job-{job.id}-') temp_dir=tempfile.mkdtemp(prefix=f'assetmanager-job-{job.id}-')
@@ -835,14 +846,39 @@ def execute_job(job_id: int, token: str, callback_base: str) -> None:
f'MeshCentral-Paketdateiupload fehlgeschlagen fuer {package_file.name}: {combined.strip()}' f'MeshCentral-Paketdateiupload fehlgeschlagen fuer {package_file.name}: {combined.strip()}'
) )
preflight=_remote_script_preflight(cfg,asset,password,job.platform,remote_file,min(timeout,120)) preflight_ok = False
preflight_attempts = 3
for preflight_attempt in range(1, preflight_attempts + 1):
preflight = _remote_script_preflight(
cfg,
asset,
password,
job.platform,
remote_file,
min(timeout, 120),
)
diagnostics += [ diagnostics += [
'--- Remote script preflight stdout ---', preflight.stdout or '', f'--- Remote script preflight attempt {preflight_attempt}/{preflight_attempts} stdout ---',
'--- Remote script preflight stderr ---', preflight.stderr or '', preflight.stdout or '',
f'Remote script preflight return code: {preflight.returncode}', f'--- Remote script preflight attempt {preflight_attempt}/{preflight_attempts} stderr ---',
preflight.stderr or '',
f'Remote script preflight attempt {preflight_attempt}/{preflight_attempts} return code: {preflight.returncode}',
] ]
preflight_text = (preflight.stdout or '') + '\n' + (preflight.stderr or '') preflight_text = (preflight.stdout or '') + '\n' + (preflight.stderr or '')
if preflight.returncode!=0 or (job.platform=='windows' and ('File exists: True' not in preflight_text or 'Readable: True' not in preflight_text)): preflight_ok = preflight.returncode == 0 and (
job.platform != 'windows'
or ('File exists: True' in preflight_text and 'Readable: True' in preflight_text)
)
if preflight_ok:
break
if preflight_attempt < preflight_attempts:
diagnostics += [
f'Remote script preflight attempt {preflight_attempt}/{preflight_attempts}: not readable',
'Retrying remote script preflight in 1 second...',
]
time.sleep(1)
if not preflight_ok:
raise RuntimeError('Remote Jobskript ist nach dem Upload nicht lesbar. Siehe Remote script preflight im Dispatcherlog.') raise RuntimeError('Remote Jobskript ist nach dem Upload nicht lesbar. Siehe Remote script preflight im Dispatcherlog.')
diagnostics += [f'Remote execution shell: {launch_shell}',f'Remote execution command: {launch_command}',f'MeshCtrl PowerShell mode: False'] diagnostics += [f'Remote execution shell: {launch_shell}',f'Remote execution command: {launch_command}',f'MeshCtrl PowerShell mode: False']
@@ -873,6 +909,10 @@ def execute_job(job_id: int, token: str, callback_base: str) -> None:
elif upload_required: elif upload_required:
diagnostics += ['Remote cleanup skipped: no successful completed callback; files retained for diagnosis.'] diagnostics += ['Remote cleanup skipped: no successful completed callback; files retained for diagnosis.']
finally:
if inventory_meshctrl_lock_acquired:
_SOFTWARE_INVENTORY_MESHCTRL_LOCK.release()
db.query(SoftwareJob).filter(SoftwareJob.id==job.id).update({SoftwareJob.meshctrl_stdout:'\n'.join(diagnostics)[-30000:],SoftwareJob.meshctrl_stderr:(result.stderr or '')[-20000:],SoftwareJob.sent_at:now},synchronize_session=False) db.query(SoftwareJob).filter(SoftwareJob.id==job.id).update({SoftwareJob.meshctrl_stdout:'\n'.join(diagnostics)[-30000:],SoftwareJob.meshctrl_stderr:(result.stderr or '')[-20000:],SoftwareJob.sent_at:now},synchronize_session=False)
db.commit() db.commit()
terminal_states={'success','failed','partial','timeout','cancelled'} terminal_states={'success','failed','partial','timeout','cancelled'}
+2 -2
View File
@@ -312,6 +312,6 @@
</script> </script>
{% if is_admin %}<dialog id="issue-dialog"><form method="post" id="issue-form"><input type="hidden" name="return_to" id="issue-return-to"><h2>{{ t('assets.issue_asset') }}</h2><label>{{ t('common.name') }}<input name="assigned_to" required></label><label>{{ t('common.date') }}<input type="date" name="assigned_on" value="{{ today }}" required></label><div class="form-actions"><button type="button" class="button button-secondary" onclick="this.closest('dialog').close()">{{ t('common.cancel') }}</button><button type="submit" class="button button-issue">↗ {{ t('assets.issue') }}</button></div></form></dialog>{% endif %} {% if is_admin %}<dialog id="issue-dialog"><form method="post" id="issue-form"><input type="hidden" name="return_to" id="issue-return-to"><h2>{{ t('assets.issue_asset') }}</h2><label>{{ t('common.name') }}<input name="assigned_to" required></label><label>{{ t('common.date') }}<input type="date" name="assigned_on" value="{{ today }}" required></label><div class="form-actions"><button type="button" class="button button-secondary" onclick="this.closest('dialog').close()">{{ t('common.cancel') }}</button><button type="submit" class="button button-issue">↗ {{ t('assets.issue') }}</button></div></form></dialog>{% endif %}
<script src="/static/js/issue-dialog.js"></script> <script src="/static/js/issue-dialog.js?v={{ application_version() }}"></script>
<script src="/static/js/asset-inventory.js"></script> <script src="/static/js/asset-inventory.js?v={{ application_version() }}"></script>
{% endblock %} {% endblock %}
+1 -1
View File
@@ -625,7 +625,7 @@
})(); })();
</script> </script>
{% endif %} {% endif %}
{% if is_admin %}<dialog id="issue-dialog"><form method="post" id="issue-form"><input type="hidden" name="return_to" id="issue-return-to"><h2>{{ t('assets.issue_asset') }}</h2><label>{{ t('common.name') }}<input name="assigned_to" required></label><label>{{ t('common.date') }}<input type="date" name="assigned_on" value="{{ now().strftime('%Y-%m-%d') if now is defined else '' }}" required></label><div class="form-actions"><button type="button" onclick="this.closest('dialog').close()">{{ t('common.cancel') }}</button><button type="submit">↗ {{ t('assets.issue') }}</button></div></form></dialog>{% endif %}<script src="/static/js/issue-dialog.js"></script> {% if is_admin %}<dialog id="issue-dialog"><form method="post" id="issue-form"><input type="hidden" name="return_to" id="issue-return-to"><h2>{{ t('assets.issue_asset') }}</h2><label>{{ t('common.name') }}<input name="assigned_to" required></label><label>{{ t('common.date') }}<input type="date" name="assigned_on" value="{{ now().strftime('%Y-%m-%d') if now is defined else '' }}" required></label><div class="form-actions"><button type="button" onclick="this.closest('dialog').close()">{{ t('common.cancel') }}</button><button type="submit">↗ {{ t('assets.issue') }}</button></div></form></dialog>{% endif %}<script src="/static/js/issue-dialog.js?v={{ application_version() }}"></script>
<script> <script>
(() => { (() => {
+2 -2
View File
@@ -105,7 +105,7 @@
{% endif %} {% endif %}
</aside> </aside>
<main class="app-content">{% block content %}{% endblock %}</main> <main class="app-content">{% block content %}{% endblock %}</main>
</div><div id="toast-container" class="toast-container" aria-live="polite"></div><script src="/static/js/local-time.js"></script><script src="/static/js/browser-state.js?v={{ app_version }}"></script><script src="/static/js/table-tools.js?v={{ app_version }}"></script><script src="/static/js/list-tools.js?v={{ app_version }}"></script><script src="/static/js/asset-columns.js?v={{ app_version }}"></script><script src="/static/js/asset-duplicates.js?v={{ app_version }}"></script><script src="/static/js/toasts.js"></script><script src="/static/js/image-editor.js?v={{ app_version }}"></script><script> </div><div id="toast-container" class="toast-container" aria-live="polite"></div><script src="/static/js/local-time.js?v={{ application_version() }}"></script><script src="/static/js/browser-state.js?v={{ application_version() }}"></script><script src="/static/js/table-tools.js?v={{ application_version() }}"></script><script src="/static/js/list-tools.js?v={{ application_version() }}"></script><script src="/static/js/asset-columns.js?v={{ application_version() }}"></script><script src="/static/js/asset-duplicates.js?v={{ application_version() }}"></script><script src="/static/js/toasts.js?v={{ application_version() }}"></script><script src="/static/js/image-editor.js?v={{ application_version() }}"></script><script>
(() => { (() => {
document.addEventListener('keydown', event => { document.addEventListener('keydown', event => {
if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 's') return; if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 's') return;
@@ -136,5 +136,5 @@
document.documentElement.classList.toggle('sidebar-collapsed-preset', collapsed); document.documentElement.classList.toggle('sidebar-collapsed-preset', collapsed);
}); });
})(); })();
</script><script src="/static/js/asset-page-boot.js"></script><script src="/static/js/presence.js"></script><script src="/static/js/file-picker.js?v={{ app_version }}"></script><script src="/static/js/software-inventory-selection.js?v={{ app_version }}"></script> </script><script src="/static/js/asset-page-boot.js?v={{ application_version() }}"></script><script src="/static/js/presence.js?v={{ application_version() }}"></script><script src="/static/js/file-picker.js?v={{ application_version() }}"></script><script src="/static/js/software-inventory-selection.js?v={{ application_version() }}"></script>
</body></html> </body></html>
+1 -1
View File
@@ -8,5 +8,5 @@
<div class="table-scroll sticky-table"><table class="data-table" id="dashboard-asset-table"><thead><tr><th>{{ t('assets.name') }}</th><th>{{ t('assets.category') }}</th>{% for f in fields if f.field_name != 'name' %}<th>{{ t('field.' ~ f.field_name, f.label) }}</th>{% endfor %}<th data-no-sort="1" data-no-filter="1">{{ t('common.actions') }}</th></tr></thead><tbody> <div class="table-scroll sticky-table"><table class="data-table" id="dashboard-asset-table"><thead><tr><th>{{ t('assets.name') }}</th><th>{{ t('assets.category') }}</th>{% for f in fields if f.field_name != 'name' %}<th>{{ t('field.' ~ f.field_name, f.label) }}</th>{% endfor %}<th data-no-sort="1" data-no-filter="1">{{ t('common.actions') }}</th></tr></thead><tbody>
{% for a in assets %}<tr><td><a href="/assets/{{a.id}}">{{a.name}}</a></td><td>{{a.category.name}}</td>{% for f in fields if f.field_name != 'name' %}<td class="multiline">{{a|attr(f.field_name) or ''}}</td>{% endfor %}<td class="row-actions">{% if is_admin %}{% if issue_status_name and a.status == issue_status_name %}<form class="inline-form" method="post" action="/assets/{{a.id}}/return"><input type="hidden" name="return_to" value="dashboard"><button class="icon-button button-return" title="{{ t('assets.return') }}">↩</button></form>{% else %}<button type="button" class="icon-button button-issue" data-issue-action="/assets/{{a.id}}/issue" data-return-to="dashboard" title="{{ t('assets.issue') }}">↗</button>{% endif %}<a class="icon-button button-duplicate" href="/assets/{{a.id}}/duplicate" title="{{ t('assets.duplicate') }}">⧉</a><form class="inline-form" method="post" action="/assets/{{a.id}}/delete" onsubmit="return confirm({{ t('assets.delete_confirm', name=a.name)|tojson }});"><button class="icon-button button-danger" title="{{ t('common.delete') }}">🗑</button></form>{% else %}<span class="muted">{{ t('common.read_only') }}</span>{% endif %}</td></tr>{% else %}<tr><td colspan="40" class="empty-state">{{ t('assets.none') }}</td></tr>{% endfor %} {% for a in assets %}<tr><td><a href="/assets/{{a.id}}">{{a.name}}</a></td><td>{{a.category.name}}</td>{% for f in fields if f.field_name != 'name' %}<td class="multiline">{{a|attr(f.field_name) or ''}}</td>{% endfor %}<td class="row-actions">{% if is_admin %}{% if issue_status_name and a.status == issue_status_name %}<form class="inline-form" method="post" action="/assets/{{a.id}}/return"><input type="hidden" name="return_to" value="dashboard"><button class="icon-button button-return" title="{{ t('assets.return') }}">↩</button></form>{% else %}<button type="button" class="icon-button button-issue" data-issue-action="/assets/{{a.id}}/issue" data-return-to="dashboard" title="{{ t('assets.issue') }}">↗</button>{% endif %}<a class="icon-button button-duplicate" href="/assets/{{a.id}}/duplicate" title="{{ t('assets.duplicate') }}">⧉</a><form class="inline-form" method="post" action="/assets/{{a.id}}/delete" onsubmit="return confirm({{ t('assets.delete_confirm', name=a.name)|tojson }});"><button class="icon-button button-danger" title="{{ t('common.delete') }}">🗑</button></form>{% else %}<span class="muted">{{ t('common.read_only') }}</span>{% endif %}</td></tr>{% else %}<tr><td colspan="40" class="empty-state">{{ t('assets.none') }}</td></tr>{% endfor %}
</tbody></table></div> </tbody></table></div>
{% if is_admin %}<dialog id="issue-dialog"><form method="post" id="issue-form"><input type="hidden" name="return_to" id="issue-return-to"><h2>{{ t('assets.issue_asset') }}</h2><label>{{ t('common.name') }}<input name="assigned_to" required></label><label>{{ t('common.date') }}<input type="date" name="assigned_on" required></label><div class="form-actions"><button type="button" onclick="this.closest('dialog').close()">{{ t('common.cancel') }}</button><button type="submit">↗ {{ t('assets.issue') }}</button></div></form></dialog>{% endif %}<script src="/static/js/issue-dialog.js"></script> {% if is_admin %}<dialog id="issue-dialog"><form method="post" id="issue-form"><input type="hidden" name="return_to" id="issue-return-to"><h2>{{ t('assets.issue_asset') }}</h2><label>{{ t('common.name') }}<input name="assigned_to" required></label><label>{{ t('common.date') }}<input type="date" name="assigned_on" required></label><div class="form-actions"><button type="button" onclick="this.closest('dialog').close()">{{ t('common.cancel') }}</button><button type="submit">↗ {{ t('assets.issue') }}</button></div></form></dialog>{% endif %}<script src="/static/js/issue-dialog.js?v={{ application_version() }}"></script>
{% endblock %} {% endblock %}
+2 -2
View File
@@ -133,6 +133,6 @@
</table> </table>
</div> </div>
</section> </section>
<script src="/static/js/software-job-selection.js?v={{ app_version }}"></script> <script src="/static/js/software-job-selection.js?v={{ application_version() }}"></script>
<script src="/static/js/software-job-status.js?v={{ app_version }}"></script> <script src="/static/js/software-job-status.js?v={{ application_version() }}"></script>
{% endblock %} {% endblock %}
+1 -1
View File
@@ -1,2 +1,2 @@
APP_VERSION = "0.5.5.91" APP_VERSION = "0.5.5.92"
__version__ = APP_VERSION __version__ = APP_VERSION
+10
View File
@@ -27,6 +27,10 @@ services:
environment: environment:
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-assetmanager}:${POSTGRES_PASSWORD:-change-me}@db:5432/${POSTGRES_DB:-assetmanager} DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-assetmanager}:${POSTGRES_PASSWORD:-change-me}@db:5432/${POSTGRES_DB:-assetmanager}
APP_TITLE: AssetManager APP_TITLE: AssetManager
ASSETMANAGER_CONTAINER_ROLE: app
ASSETMANAGER_IMAGE_REF: ${ASSETMANAGER_IMAGE:-local-build}
ASSETMANAGER_BUILD_DATE: ${ASSETMANAGER_BUILD_DATE:-}
ASSETMANAGER_GIT_COMMIT: ${ASSETMANAGER_GIT_COMMIT:-}
ASSETMANAGER_DATA_ROOT: /assetmanager-data ASSETMANAGER_DATA_ROOT: /assetmanager-data
APP_CONFIG: /assetmanager-data/config/config.json APP_CONFIG: /assetmanager-data/config/config.json
APPINFO_PATH: /assetmanager-data/config/APPINFO.json APPINFO_PATH: /assetmanager-data/config/APPINFO.json
@@ -74,6 +78,12 @@ services:
environment: environment:
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-assetmanager}:${POSTGRES_PASSWORD:-change-me}@db:5432/${POSTGRES_DB:-assetmanager} DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-assetmanager}:${POSTGRES_PASSWORD:-change-me}@db:5432/${POSTGRES_DB:-assetmanager}
APP_TITLE: AssetManager APP_TITLE: AssetManager
ASSETMANAGER_CONTAINER_ROLE: callback
ASSETMANAGER_IMAGE_REF: ${ASSETMANAGER_IMAGE:-local-build}
ASSETMANAGER_BUILD_DATE: ${ASSETMANAGER_BUILD_DATE:-}
ASSETMANAGER_GIT_COMMIT: ${ASSETMANAGER_GIT_COMMIT:-}
CALLBACK_BIND_IP: ${CALLBACK_BIND_IP:-127.0.0.1}
CALLBACK_PORT: ${CALLBACK_PORT:-8090}
ASSETMANAGER_DATA_ROOT: /assetmanager-data ASSETMANAGER_DATA_ROOT: /assetmanager-data
APP_CONFIG: /assetmanager-data/config/config.json APP_CONFIG: /assetmanager-data/config/config.json
APPINFO_PATH: /assetmanager-data/config/APPINFO.json APPINFO_PATH: /assetmanager-data/config/APPINFO.json
+2 -1
View File
@@ -1,3 +1,4 @@
- [0.5.5.92](UPDATE-0.5.5.92.md) - startup diagnostics with runtime, database and callback endpoint information.
- [0.5.5.91](UPDATE-0.5.5.91.md) - smoother table scrolling and consolidated current development changes. - [0.5.5.91](UPDATE-0.5.5.91.md) - smoother table scrolling and consolidated current development changes.
- [0.5.5.90](UPDATE-0.5.5.90.md) - modular analyzer profiles, community repository foundation and portable package import/export. - [0.5.5.90](UPDATE-0.5.5.90.md) - modular analyzer profiles, community repository foundation and portable package import/export.
- [0.5.5.89](UPDATE-0.5.5.89.md) - recursively analyze supported SFX payloads and package selected embedded installers. - [0.5.5.89](UPDATE-0.5.5.89.md) - recursively analyze supported SFX payloads and package selected embedded installers.
@@ -27,7 +28,7 @@
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.91**. The current release is **0.5.5.92**.
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.
+15
View File
@@ -0,0 +1,15 @@
# Update 0.5.5.92
Version 0.5.5.92 adds structured startup diagnostics for the AssetManager application and callback containers.
## Changes
- Added a clearly delimited startup block to Docker/container logs for the application service.
- Added a dedicated startup block to the callback-only service.
- Startup diagnostics report the running AssetManager/build version, Python and core runtime package versions.
- The actual connected PostgreSQL server version is queried and logged at startup.
- Callback startup diagnostics show the configured public callback base/API/health URLs and the internal/published listener information.
- Missing build metadata is reported as `not provided`; no credentials, tokens or database passwords are logged.
- Version updated to 0.5.5.92.
Version 0.5.5.91 remains the stabilization baseline for table scrolling, JSON query handling, static cache busting, remote-script preflight retries and serialized software-inventory MeshCtrl execution.