diff --git a/README.md b/README.md index 39484b6..7718242 100755 --- a/README.md +++ b/README.md @@ -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. diff --git a/VERSION b/VERSION index 5322014..2b7d5a6 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.5.91 +0.5.5.92 diff --git a/app/main.py b/app/main.py index cbcfebd..c18c2ab 100755 --- a/app/main.py +++ b/app/main.py @@ -23,6 +23,8 @@ import ssl import time import urllib.error import urllib.request +import importlib.metadata +import platform from typing import Any from concurrent.futures import ThreadPoolExecutor 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_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 ''}", + f" Callback API URL : {(callback_base + '/api/software-jobs//callback') if callback_base else ''}", + f" Callback health URL : {(callback_base + '/api/software-callback/health') if callback_base else ''}", + 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")) # Dedicated callback-only ASGI application. It deliberately exposes only the # 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}) +@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") def start_backup_scheduler(): global BACKUP_THREAD diff --git a/app/software_control.py b/app/software_control.py index 9d2e6eb..bd3f58a 100755 --- a/app/software_control.py +++ b/app/software_control.py @@ -21,6 +21,7 @@ from .software_packages import build_deployment_user_script, load_package_manife _DISPATCH_SLOT_LOCK = threading.Lock() _NEXT_DISPATCH_MONOTONIC = 0.0 +_SOFTWARE_INVENTORY_MESHCTRL_LOCK = threading.Lock() def _wait_for_dispatch_slot() -> tuple[float, float]: @@ -744,134 +745,173 @@ def execute_job(job_id: int, token: str, callback_base: str) -> None: f'Execution timeout: {execution_timeout_seconds or timeout} seconds', ] - if upload_required: - suffix=_script_extension(interpreter) - temp_dir=tempfile.mkdtemp(prefix=f'assetmanager-job-{job.id}-') - temp_path=str(Path(temp_dir)/('run'+suffix)) - Path(temp_path).write_text(payload,encoding='utf-8',newline='\n') - diagnostics += [f'Remote directory: {remote_dir}',f'Remote file: {remote_file}',f'Local staging bytes: {os.path.getsize(temp_path)}'] + 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: + suffix=_script_extension(interpreter) + temp_dir=tempfile.mkdtemp(prefix=f'assetmanager-job-{job.id}-') + temp_path=str(Path(temp_dir)/('run'+suffix)) + Path(temp_path).write_text(payload,encoding='utf-8',newline='\n') + diagnostics += [f'Remote directory: {remote_dir}',f'Remote file: {remote_file}',f'Local staging bytes: {os.path.getsize(temp_path)}'] - if remote_cleanup_enabled: - stale_cleanup=_cleanup_stale_remote_job_directories( - cfg,asset,password,job.platform,remote_dir,remote_job_retention_hours,min(timeout,120) - ) - diagnostics += [ - '--- Stale remote job cleanup stdout ---',stale_cleanup.stdout or '', - '--- Stale remote job cleanup stderr ---',stale_cleanup.stderr or '', - f'Stale remote job cleanup return code: {stale_cleanup.returncode}', - ] + if remote_cleanup_enabled: + stale_cleanup=_cleanup_stale_remote_job_directories( + cfg,asset,password,job.platform,remote_dir,remote_job_retention_hours,min(timeout,120) + ) + diagnostics += [ + '--- Stale remote job cleanup stdout ---',stale_cleanup.stdout or '', + '--- Stale remote job cleanup stderr ---',stale_cleanup.stderr or '', + f'Stale remote job cleanup return code: {stale_cleanup.returncode}', + ] - prepared=_prepare_remote_directory(cfg,asset,password,job.platform,remote_dir,timeout,remote_file) - diagnostics += ['--- Prepare directory stdout ---',prepared.stdout or '','--- Prepare directory stderr ---',prepared.stderr or '',f'Prepare return code: {prepared.returncode}'] - if prepared.returncode!=0: raise RuntimeError(f'Remote Jobverzeichnis konnte nicht erstellt werden: {prepared.stderr or prepared.stdout}') + prepared=_prepare_remote_directory(cfg,asset,password,job.platform,remote_dir,timeout,remote_file) + diagnostics += ['--- Prepare directory stdout ---',prepared.stdout or '','--- Prepare directory stderr ---',prepared.stderr or '',f'Prepare return code: {prepared.returncode}'] + if prepared.returncode!=0: raise RuntimeError(f'Remote Jobverzeichnis konnte nicht erstellt werden: {prepared.stderr or prepared.stdout}') - upload_results=[] - uploaded=_upload_script(cfg,asset,password,temp_path,remote_dir,timeout) - upload_results.append(uploaded) - diagnostics += ['--- Upload attempt 1 stdout ---',uploaded.stdout or '','--- Upload attempt 1 stderr ---',uploaded.stderr or '',f'Upload attempt 1 return code: {uploaded.returncode}'] - upload_text=(uploaded.stdout or '')+'\n'+(uploaded.stderr or '') - upload_ok=uploaded.returncode==0 and 'Upload done' in upload_text and 'Upload error' not in upload_text - if not upload_ok: - # MeshCtrl can return code 0 together with "Upload error". - # Remove only the stale target and retry once. Do not touch ACLs of already uploaded files. - repair=_prepare_remote_directory(cfg,asset,password,job.platform,remote_dir,min(timeout,120),remote_file,reset_acl=False) - diagnostics += ['--- Upload repair stdout ---',repair.stdout or '','--- Upload repair stderr ---',repair.stderr or '',f'Upload repair return code: {repair.returncode}'] + upload_results=[] uploaded=_upload_script(cfg,asset,password,temp_path,remote_dir,timeout) upload_results.append(uploaded) - diagnostics += ['--- Upload attempt 2 stdout ---',uploaded.stdout or '','--- Upload attempt 2 stderr ---',uploaded.stderr or '',f'Upload attempt 2 return code: {uploaded.returncode}'] + diagnostics += ['--- Upload attempt 1 stdout ---',uploaded.stdout or '','--- Upload attempt 1 stderr ---',uploaded.stderr or '',f'Upload attempt 1 return code: {uploaded.returncode}'] upload_text=(uploaded.stdout or '')+'\n'+(uploaded.stderr or '') upload_ok=uploaded.returncode==0 and 'Upload done' in upload_text and 'Upload error' not in upload_text - if not upload_ok: - combined='\n\n'.join(((item.stdout or '')+'\n'+(item.stderr or '')).strip() for item in upload_results if ((item.stdout or '')+(item.stderr or '')).strip()) - raise RuntimeError(f'MeshCentral-Dateiupload fehlgeschlagen nach 2 Versuchen: {combined.strip()}') + if not upload_ok: + # MeshCtrl can return code 0 together with "Upload error". + # Remove only the stale target and retry once. Do not touch ACLs of already uploaded files. + repair=_prepare_remote_directory(cfg,asset,password,job.platform,remote_dir,min(timeout,120),remote_file,reset_acl=False) + diagnostics += ['--- Upload repair stdout ---',repair.stdout or '','--- Upload repair stderr ---',repair.stderr or '',f'Upload repair return code: {repair.returncode}'] + uploaded=_upload_script(cfg,asset,password,temp_path,remote_dir,timeout) + upload_results.append(uploaded) + diagnostics += ['--- Upload attempt 2 stdout ---',uploaded.stdout or '','--- Upload attempt 2 stderr ---',uploaded.stderr or '',f'Upload attempt 2 return code: {uploaded.returncode}'] + upload_text=(uploaded.stdout or '')+'\n'+(uploaded.stderr or '') + upload_ok=uploaded.returncode==0 and 'Upload done' in upload_text and 'Upload error' not in upload_text + if not upload_ok: + combined='\n\n'.join(((item.stdout or '')+'\n'+(item.stderr or '')).strip() for item in upload_results if ((item.stdout or '')+(item.stderr or '')).strip()) + raise RuntimeError(f'MeshCentral-Dateiupload fehlgeschlagen nach 2 Versuchen: {combined.strip()}') - if job.job_type == 'software_deployment': - payload_files = package_payload_files(job.package_id, job.action) - diagnostics += [f'Package payload files: {len(payload_files)}'] - for package_file in payload_files: - remote_package_file = remote_dir + '\\' + package_file.name - package_uploads = [] - package_uploaded = _upload_script(cfg, asset, password, str(package_file), remote_dir, timeout) - package_uploads.append(package_uploaded) - diagnostics += [ - f'--- Package upload {package_file.name} attempt 1 stdout ---', package_uploaded.stdout or '', - f'--- Package upload {package_file.name} attempt 1 stderr ---', package_uploaded.stderr or '', - f'Package upload {package_file.name} attempt 1 return code: {package_uploaded.returncode}', - ] - package_text = (package_uploaded.stdout or '') + '\n' + (package_uploaded.stderr or '') - package_ok = package_uploaded.returncode == 0 and 'Upload done' in package_text and 'Upload error' not in package_text - if not package_ok: - package_repair = _prepare_remote_directory( - cfg, - asset, - password, - job.platform, - remote_dir, - min(timeout, 120), - remote_package_file, - reset_acl=False, - ) - diagnostics += [ - f'--- Package upload {package_file.name} repair stdout ---', package_repair.stdout or '', - f'--- Package upload {package_file.name} repair stderr ---', package_repair.stderr or '', - f'Package upload {package_file.name} repair return code: {package_repair.returncode}', - ] + if job.job_type == 'software_deployment': + payload_files = package_payload_files(job.package_id, job.action) + diagnostics += [f'Package payload files: {len(payload_files)}'] + for package_file in payload_files: + remote_package_file = remote_dir + '\\' + package_file.name + package_uploads = [] package_uploaded = _upload_script(cfg, asset, password, str(package_file), remote_dir, timeout) package_uploads.append(package_uploaded) diagnostics += [ - f'--- Package upload {package_file.name} attempt 2 stdout ---', package_uploaded.stdout or '', - f'--- Package upload {package_file.name} attempt 2 stderr ---', package_uploaded.stderr or '', - f'Package upload {package_file.name} attempt 2 return code: {package_uploaded.returncode}', + f'--- Package upload {package_file.name} attempt 1 stdout ---', package_uploaded.stdout or '', + f'--- Package upload {package_file.name} attempt 1 stderr ---', package_uploaded.stderr or '', + f'Package upload {package_file.name} attempt 1 return code: {package_uploaded.returncode}', ] package_text = (package_uploaded.stdout or '') + '\n' + (package_uploaded.stderr or '') package_ok = package_uploaded.returncode == 0 and 'Upload done' in package_text and 'Upload error' not in package_text - if not package_ok: - combined = '\n\n'.join( - ((item.stdout or '') + '\n' + (item.stderr or '')).strip() - for item in package_uploads - if ((item.stdout or '') + (item.stderr or '')).strip() - ) - raise RuntimeError( - f'MeshCentral-Paketdateiupload fehlgeschlagen fuer {package_file.name}: {combined.strip()}' - ) + if not package_ok: + package_repair = _prepare_remote_directory( + cfg, + asset, + password, + job.platform, + remote_dir, + min(timeout, 120), + remote_package_file, + reset_acl=False, + ) + diagnostics += [ + f'--- Package upload {package_file.name} repair stdout ---', package_repair.stdout or '', + f'--- Package upload {package_file.name} repair stderr ---', package_repair.stderr or '', + f'Package upload {package_file.name} repair return code: {package_repair.returncode}', + ] + package_uploaded = _upload_script(cfg, asset, password, str(package_file), remote_dir, timeout) + package_uploads.append(package_uploaded) + diagnostics += [ + f'--- Package upload {package_file.name} attempt 2 stdout ---', package_uploaded.stdout or '', + f'--- Package upload {package_file.name} attempt 2 stderr ---', package_uploaded.stderr or '', + f'Package upload {package_file.name} attempt 2 return code: {package_uploaded.returncode}', + ] + package_text = (package_uploaded.stdout or '') + '\n' + (package_uploaded.stderr or '') + package_ok = package_uploaded.returncode == 0 and 'Upload done' in package_text and 'Upload error' not in package_text + if not package_ok: + combined = '\n\n'.join( + ((item.stdout or '') + '\n' + (item.stderr or '')).strip() + for item in package_uploads + if ((item.stdout or '') + (item.stderr or '')).strip() + ) + raise RuntimeError( + f'MeshCentral-Paketdateiupload fehlgeschlagen fuer {package_file.name}: {combined.strip()}' + ) - preflight=_remote_script_preflight(cfg,asset,password,job.platform,remote_file,min(timeout,120)) - diagnostics += [ - '--- Remote script preflight stdout ---', preflight.stdout or '', - '--- Remote script preflight stderr ---', preflight.stderr or '', - f'Remote script preflight return code: {preflight.returncode}', - ] - 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)): - raise RuntimeError('Remote Jobskript ist nach dem Upload nicht lesbar. Siehe Remote script preflight im Dispatcherlog.') + 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 += [ + f'--- Remote script preflight attempt {preflight_attempt}/{preflight_attempts} stdout ---', + preflight.stdout or '', + 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_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) - diagnostics += [f'Remote execution shell: {launch_shell}',f'Remote execution command: {launch_command}',f'MeshCtrl PowerShell mode: False'] - started=datetime.utcnow() - result=_launch_uploaded_script(cfg,asset,password,job.platform,interpreter,remote_file,timeout) - finished=datetime.utcnow() - diagnostics += [f'Execution started UTC: {started.isoformat()}Z',f'Execution finished UTC: {finished.isoformat()}Z','--- Execution stdout ---',result.stdout or '','--- Execution stderr ---',result.stderr or '',f'Execution return code: {result.returncode}'] - else: - started=datetime.utcnow() - action=['RunCommand','--id',asset.mesh_node_id,'--run',payload,'--reply'] - if job.platform=='windows' and interpreter=='powershell': action += ['--powershell'] - result=_run_meshctrl(cfg,asset,password,action,timeout) - finished=datetime.utcnow() - diagnostics += ['--- Direct execution stdout ---',result.stdout or '','--- Direct execution stderr ---',result.stderr or '',f'Execution return code: {result.returncode}'] + if not preflight_ok: + raise RuntimeError('Remote Jobskript ist nach dem Upload nicht lesbar. Siehe Remote script preflight im Dispatcherlog.') - now=datetime.utcnow() - # MeshCtrl itself returns code 0 even for some remote PowerShell parser - # errors. Only delete the remote files after a completed successful - # callback, never merely because the MeshCtrl process exited cleanly. - db.expire_all() - callback_state=db.get(SoftwareJob,job.id) - cleanup_result=None - callback_success=bool(callback_state and callback_state.callback_completed_at is not None and callback_state.status=='success') - diagnostics += [f'Callback completed before cleanup: {bool(callback_state and callback_state.callback_completed_at)}',f'Callback status before cleanup: {callback_state.status if callback_state else "unknown"}'] - if upload_required and callback_success: - cleanup_result=_cleanup_remote_directory(cfg,asset,password,job.platform,remote_dir,min(timeout,120)) - diagnostics += ['--- Cleanup stdout ---',cleanup_result.stdout or '','--- Cleanup stderr ---',cleanup_result.stderr or '',f'Cleanup return code: {cleanup_result.returncode}'] - elif upload_required: - diagnostics += ['Remote cleanup skipped: no successful completed callback; files retained for diagnosis.'] + diagnostics += [f'Remote execution shell: {launch_shell}',f'Remote execution command: {launch_command}',f'MeshCtrl PowerShell mode: False'] + started=datetime.utcnow() + result=_launch_uploaded_script(cfg,asset,password,job.platform,interpreter,remote_file,timeout) + finished=datetime.utcnow() + diagnostics += [f'Execution started UTC: {started.isoformat()}Z',f'Execution finished UTC: {finished.isoformat()}Z','--- Execution stdout ---',result.stdout or '','--- Execution stderr ---',result.stderr or '',f'Execution return code: {result.returncode}'] + else: + started=datetime.utcnow() + action=['RunCommand','--id',asset.mesh_node_id,'--run',payload,'--reply'] + if job.platform=='windows' and interpreter=='powershell': action += ['--powershell'] + result=_run_meshctrl(cfg,asset,password,action,timeout) + finished=datetime.utcnow() + diagnostics += ['--- Direct execution stdout ---',result.stdout or '','--- Direct execution stderr ---',result.stderr or '',f'Execution return code: {result.returncode}'] + + now=datetime.utcnow() + # MeshCtrl itself returns code 0 even for some remote PowerShell parser + # errors. Only delete the remote files after a completed successful + # callback, never merely because the MeshCtrl process exited cleanly. + db.expire_all() + callback_state=db.get(SoftwareJob,job.id) + cleanup_result=None + callback_success=bool(callback_state and callback_state.callback_completed_at is not None and callback_state.status=='success') + diagnostics += [f'Callback completed before cleanup: {bool(callback_state and callback_state.callback_completed_at)}',f'Callback status before cleanup: {callback_state.status if callback_state else "unknown"}'] + if upload_required and callback_success: + cleanup_result=_cleanup_remote_directory(cfg,asset,password,job.platform,remote_dir,min(timeout,120)) + diagnostics += ['--- Cleanup stdout ---',cleanup_result.stdout or '','--- Cleanup stderr ---',cleanup_result.stderr or '',f'Cleanup return code: {cleanup_result.returncode}'] + elif upload_required: + 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.commit() diff --git a/app/templates/asset_detail.html b/app/templates/asset_detail.html index 72d1177..b412ebe 100755 --- a/app/templates/asset_detail.html +++ b/app/templates/asset_detail.html @@ -312,6 +312,6 @@ {% if is_admin %}

{{ t('assets.issue_asset') }}

{% endif %} - - + + {% endblock %} diff --git a/app/templates/assets.html b/app/templates/assets.html index 1ce023f..a887baf 100755 --- a/app/templates/assets.html +++ b/app/templates/assets.html @@ -625,7 +625,7 @@ })(); {% endif %} -{% if is_admin %}

{{ t('assets.issue_asset') }}

{% endif %} +{% if is_admin %}

{{ t('assets.issue_asset') }}

{% endif %} + diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index e3bb50d..295ec70 100755 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -8,5 +8,5 @@
{% for f in fields if f.field_name != 'name' %}{% endfor %} {% for a in assets %}{% for f in fields if f.field_name != 'name' %}{% endfor %}{% else %}{% endfor %}
{{ t('assets.name') }}{{ t('assets.category') }}{{ t('field.' ~ f.field_name, f.label) }}{{ t('common.actions') }}
{{a.name}}{{a.category.name}}{{a|attr(f.field_name) or ''}}{% if is_admin %}{% if issue_status_name and a.status == issue_status_name %}
{% else %}{% endif %}⧉
{% else %}{{ t('common.read_only') }}{% endif %}
{{ t('assets.none') }}
-{% if is_admin %}

{{ t('assets.issue_asset') }}

{% endif %} +{% if is_admin %}

{{ t('assets.issue_asset') }}

{% endif %} {% endblock %} diff --git a/app/templates/software.html b/app/templates/software.html index 1ada23d..c662209 100755 --- a/app/templates/software.html +++ b/app/templates/software.html @@ -133,6 +133,6 @@ - - + + {% endblock %} diff --git a/app/version.py b/app/version.py index 4484513..cebc007 100755 --- a/app/version.py +++ b/app/version.py @@ -1,2 +1,2 @@ -APP_VERSION = "0.5.5.91" +APP_VERSION = "0.5.5.92" __version__ = APP_VERSION diff --git a/docker-compose.yml b/docker-compose.yml index 3c1d29d..1b4c1b7 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,6 +27,10 @@ services: environment: DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-assetmanager}:${POSTGRES_PASSWORD:-change-me}@db:5432/${POSTGRES_DB:-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 APP_CONFIG: /assetmanager-data/config/config.json APPINFO_PATH: /assetmanager-data/config/APPINFO.json @@ -74,6 +78,12 @@ services: environment: DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-assetmanager}:${POSTGRES_PASSWORD:-change-me}@db:5432/${POSTGRES_DB:-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 APP_CONFIG: /assetmanager-data/config/config.json APPINFO_PATH: /assetmanager-data/config/APPINFO.json diff --git a/docs/version-history/README.md b/docs/version-history/README.md index 2298ff0..2e8ca94 100755 --- a/docs/version-history/README.md +++ b/docs/version-history/README.md @@ -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.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. @@ -27,7 +28,7 @@ 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. diff --git a/docs/version-history/UPDATE-0.5.5.92.md b/docs/version-history/UPDATE-0.5.5.92.md new file mode 100755 index 0000000..648fa31 --- /dev/null +++ b/docs/version-history/UPDATE-0.5.5.92.md @@ -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.