Files
Assetmanager/app/templates/asset_detail.html
T

303 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% macro inventory_label(label) -%}
{%- set text = label|string -%}
{{- t(text, text) if text.startswith('inventory.') else text -}}
{%- endmacro %}
{% macro inventory_icon(label) -%}
{%- set key = (label|string)|lower -%}
{%- if 'system_overview' in key or 'window' in key or key in ['computer', 'system', 'hardware'] -%}🖥️
{%- elif 'processor' in key or 'cpu' in key -%}🧠
{%- elif 'memory' in key or 'ram' in key -%}🧩
{%- elif 'storage' in key or 'drive' in key or 'disk' in key or 'volume' in key or 'partition' in key -%}💾
{%- elif 'graphic' in key or 'gpu' in key or 'video' in key or 'display' in key -%}🎨
{%- elif 'monitor' in key or 'screen' in key -%}🖥️
{%- elif 'network' in key or 'ethernet' in key or 'wifi' in key or 'adapter' in key or 'wlan' in key -%}🌐
{%- elif 'battery' in key -%}🔋
{%- elif 'bios' in key or 'uefi' in key or 'firmware' in key -%}⚙️
{%- elif 'board' in key or 'motherboard' in key or 'mainboard' in key -%}🔌
{%- elif 'audio' in key or 'sound' in key -%}🔊
{%- elif 'usb' in key -%}🔗
{%- elif 'printer' in key -%}🖨️
{%- elif 'software' in key or 'application' in key -%}📦
{%- elif 'os_' in key or 'operating' in key -%}🪟
{%- elif 'antivirus' in key or 'security' in key or 'tpm' in key -%}🛡️
{%- elif 'mesh' in key or 'agent' in key or 'connection' in key -%}🔗
{%- else -%}📁
{%- endif -%}
{%- endmacro %}
{% macro inventory_node(label, value, level=0, path='root') %}
{% set node_path = path ~ '/' ~ (label|string)|replace(' ', '_')|replace('/', '_') %}
{% if value is mapping %}
<details class="inventory-node" data-inventory-tree-path="{{ node_path|e }}">
<summary><span class="inventory-section-icon" aria-hidden="true">{{ inventory_icon(label) }}</span><span class="inventory-branch">{{ inventory_label(label) }}</span><span class="inventory-count">{{ value|length }}</span></summary>
<div class="inventory-children">{% for key, child in value.items() %}{{ inventory_node(key, child, level + 1, node_path) }}{% endfor %}</div>
</details>
{% elif value is sequence and value is not string %}
<details class="inventory-node" data-inventory-tree-path="{{ node_path|e }}">
<summary><span class="inventory-section-icon" aria-hidden="true">{{ inventory_icon(label) }}</span><span class="inventory-branch">{{ inventory_label(label) }}</span><span class="inventory-count">{{ value|length }}</span></summary>
<div class="inventory-children">
{% for child in value %}
{% if child is mapping %}{% set child_label = child.get('inventory.property.name') or child.get('Name') or child.get('name') or child.get('Caption') or child.get('caption') or child.get('DeviceID') or child.get('Tag') or (t('inventory.entry', 'Entry') ~ ' ' ~ loop.index) %}{% else %}{% set child_label = t('inventory.entry', 'Entry') ~ ' ' ~ loop.index %}{% endif %}
{{ inventory_node(child_label, child, level + 1, node_path) }}
{% endfor %}
</div>
</details>
{% else %}
<div class="inventory-property"><span>{{ inventory_label(label) }}</span><strong>{% if value == 'inventory.value.unknown' %}{{ t('inventory.value.unknown', 'Unknown') }}{% elif value is sameas true %}{{ t('common.yes') }}{% elif value is sameas false %}{{ t('common.no') }}{% else %}{{ value if value is not none and value != '' else '' }}{% endif %}</strong></div>
{% endif %}
{% endmacro %}
{% block content %}
<div class="toolbar">
<h1>{{ asset.name }}</h1>
<div class="toolbar-actions">
{% if asset.mesh_node_id %}
<span
class="presence-badge presence-{{ asset.mesh_online_state or 'unknown' }}"
data-presence-asset="{{ asset.id }}"
data-label-online="{{ t('presence.online') }}"
data-label-offline="{{ t('presence.offline') }}"
data-label-unknown="{{ t('presence.unknown') }}"
data-last-seen-label="{{ t('presence.last_seen') }}"
title="{{ t('presence.' ~ (asset.mesh_online_state or 'unknown')) }} · {{ t('presence.last_seen') }}: {% if asset.mesh_last_seen %}{{ asset.mesh_last_seen }}{% else %}{% endif %}"
><span class="presence-dot" aria-hidden="true"></span><span data-presence-label>{{ t('presence.' ~ (asset.mesh_online_state or 'unknown')) }}</span></span>
{% endif %}
{% if is_admin %}
{% if issue_status_name and asset.status == issue_status_name %}<form class="inline-form" method="post" action="/assets/{{asset.id}}/return"><button class="button button-return">↩ {{ t('assets.return') }}</button></form>
{% else %}<button type="button" class="button button-issue" data-issue-action="/assets/{{asset.id}}/issue" data-return-to="detail">↗ {{ t('assets.issue') }}</button>{% endif %}
<a class="button button-duplicate" href="/assets/{{asset.id}}/duplicate">⧉ {{ t('assets.duplicate') }}</a>
<form class="inline-form" method="post" action="/assets/{{ asset.id }}/delete" onsubmit="return confirm({{ t('assets.delete_confirm', name=asset.name)|tojson }});"><button type="submit" class="button button-danger">🗑 {{ t('common.delete') }}</button></form>
<a class="button" href="/assets/{{ asset.id }}/edit">✎ {{ t('common.edit') }}</a>
{% endif %}
<a class="button button-secondary" href="/assets/{{ asset.id }}/history">🕘 {{ t('assets.history') }} ({{ history_count }})</a>
</div>
</div>
<div class="detail">
<div>{% if asset.image_path %}<img class="asset-image" src="{{ asset.image_path }}">{% elif asset.category.image_path %}<img class="asset-image" src="{{ asset.category.image_path }}">{% endif %}</div>
<dl>{% for f in active_fields %}{% if f.field_name not in ['room_number', 'parent_asset_id'] %}{% set d=f.definition %}{% set v=custom_values.get(f.field_name) if d and not d.is_system else asset|attr(f.field_name) %}{% if v %}<dt>{{ t(d.translation_key, d.label) if d else f.label }}</dt><dd>{{ v }}</dd>{% endif %}{% endif %}{% endfor %}{% if asset.room_number %}<dt>{{ t('assets.room') }}</dt><dd>{{ asset.room_number }}</dd>{% endif %}
{% if parent_asset %}<dt>{{ t('assets.parent') }}</dt><dd><a href="/assets/{{ parent_asset.id }}">{{ parent_asset.name }}</a></dd>{% endif %}
{% if child_assets %}<dt>{{ t('assets.child_assets') }}</dt><dd>{% for child in child_assets %}<a href="/assets/{{ child.id }}">{{ child.name }}</a>{% if not loop.last %}, {% endif %}{% endfor %}</dd>{% endif %}
</dl>
</div>
{% if is_admin %}
<section class="panel">
<div class="inventory-header">
<div>
<h2>{{ t('mesh.link.section_title', 'MeshCentral link') }}</h2>
<p class="muted">{{ t('mesh.link.section_help', 'The node ID can be changed manually without deleting the asset. Other asset fields are updated only by the next MeshCentral synchronization.') }}</p>
</div>
<div class="toolbar-actions">
<a class="button button-secondary" href="/assets/{{ asset.id }}/mesh-link">
✎ {% if asset.mesh_node_id %}{{ t('mesh.link.change', 'Change link') }}{% else %}{{ t('mesh.link.initialize', 'Initialize link') }}{% endif %}
</a>
{% if asset.mesh_node_id %}
<form class="inline-form" method="post" action="/assets/{{ asset.id }}/mesh-unlink" onsubmit="return confirm({{ t('mesh.link.unlink_confirm', 'Remove the MeshCentral link from this asset? The asset itself and its stored data remain unchanged.')|tojson }});">
<button type="submit" class="button button-danger">⛓ {{ t('mesh.link.unlink', 'Remove link') }}</button>
</form>
{% endif %}
</div>
</div>
<div class="inventory-meta">
<span><strong>{{ t('field.mesh_node_id', 'MeshCentral node ID') }}:</strong> <code>{{ asset.mesh_node_id or '' }}</code></span>
{% if asset.mesh_sync_status %}<span><strong>{{ t('assets.status', 'Status') }}:</strong> <span class="sync-status status-{{ asset.mesh_sync_status }}">{{ t('mesh.sync_status.' ~ asset.mesh_sync_status, asset.mesh_sync_status) }}</span></span>{% endif %}
</div>
</section>
{% endif %}
{% if is_admin and asset.mesh_node_id %}
<section class="panel">
<div class="inventory-header">
<div>
<h2>{{ t('jobs.run_definition', 'Job ausführen') }}</h2>
<p class="muted">{{ t('jobs.run_definition_help', 'Es werden nur aktive Jobdefinitionen angezeigt, die zur erkannten Plattform dieses Assets passen.') }}</p>
</div>
{% if job_definitions %}
<form method="post" action="/assets/{{ asset.id }}/jobs/run" class="inline-form" onsubmit="return confirm({{ t('jobs.single.confirm', 'Den ausgewählten Job jetzt auf diesem Asset starten?')|tojson }});">
<select name="definition_id" required aria-label="{{ t('jobs.select_definition', 'Jobdefinition auswählen')|e }}">
<option value="">{{ t('jobs.select_definition', 'Jobdefinition auswählen') }}</option>
{% for definition in job_definitions %}<option value="{{ definition.id }}">{{ definition.name }}</option>{% endfor %}
</select>
<button type="submit" class="button">▶ {{ t('jobs.start', 'Job starten') }}</button>
</form>
{% else %}
<span class="muted">{{ t('jobs.no_compatible_definitions', 'Für diese Plattform ist keine aktive Jobdefinition verfügbar.') }}</span>
{% endif %}
</div>
</section>
{% endif %}
{% if is_admin %}
<section class="panel">
<h2>{{ t('jobparams.asset_title') }}</h2>
<p class="muted">{{ t('jobparams.asset_help') }}</p>
{% if job_definitions %}
<div class="job-definition-asset-list">
{% for definition in job_definitions %}
<details class="job-parameter-asset">
<summary>
<span><strong>{{ definition.name }}</strong>{% if definition.description %}<small>{{ definition.description }}</small>{% endif %}</span>
<span class="muted">{{ t('jobparams.parameter_count', count=definition.parameters|length) }}</span>
</summary>
{% if definition.parameters %}
<form method="post" action="/assets/{{ asset.id }}/job-parameters/{{ definition.id }}" class="form-grid">
{% for parameter in definition.parameters %}
{% set override = job_parameter_overrides.get(parameter.id) %}
<div class="form-wide job-asset-param">
<label class="checkbox-label"><input type="checkbox" name="override_{{ parameter.id }}" {% if override is not none %}checked{% endif %}> {{ t('jobparams.override') }}</label>
<label>{{ parameter.label }} <code>{{ parameter.parameter_key }}</code>
<input name="value_{{ parameter.id }}" value="{{ override if override is not none else parameter.default_value or '' }}">
{% if parameter.description %}<small>{{ parameter.description }}</small>{% endif %}
<small>{{ t('jobparams.default_value') }}: {{ parameter.default_value or '' }} · {{ t('jobparams.source') }}: {{ t('jobparams.source.asset') if override is not none else t('jobparams.source.default') }}</small>
</label>
</div>
{% endfor %}
<div class="form-actions form-wide"><button class="button button-save" type="submit">{{ t('common.save') }}</button></div>
</form>
{% else %}
<p class="muted job-parameter-empty">{{ t('jobparams.none_defined') }}</p>
{% endif %}
</details>
{% endfor %}
</div>
{% else %}
<p class="muted">{{ t('jobs.no_compatible_definitions') }}</p>
{% endif %}
</section>
{% endif %}
{% if asset.mesh_node_id %}
<section class="panel inventory-panel">
<div class="inventory-header">
<div>
<h2>{{ t('inventory.title') }}</h2>
<p class="muted">{{ t('inventory.subtitle') }}</p>
</div>
<a class="button button-secondary" href="/assets/{{ asset.id }}/mesh-inventory.json">{{ t('inventory.download_json') }}</a>
</div>
<div class="inventory-meta">
<span><strong>{{ t('mesh.node_id') }}:</strong> <code>{{ asset.mesh_node_id }}</code></span>
<span><strong>{{ t('inventory.snapshot') }}:</strong> {% if asset.mesh_inventory_updated_at or asset.mesh_last_sync %}<time class="local-time" datetime="{{ utc_iso(asset.mesh_inventory_updated_at or asset.mesh_last_sync) }}" data-utc="{{ utc_iso(asset.mesh_inventory_updated_at or asset.mesh_last_sync) }}">{{ asset.mesh_inventory_updated_at or asset.mesh_last_sync }}</time>{% else %}{{ '' }}{% endif %}</span>
<span><strong>{{ t('assets.status') }}:</strong> <span class="sync-status status-{{ asset.mesh_sync_status }}">{% if asset.mesh_sync_status %}{{ t('mesh.sync_status.' ~ asset.mesh_sync_status, asset.mesh_sync_status) }}{% else %}{{ t('common.unknown') }}{% endif %}</span></span>
<span data-presence-meta="{{ asset.id }}">
<strong>{{ t('presence.title') }}:</strong>
<span data-presence-meta-state>{{ t('presence.' ~ (asset.mesh_online_state or 'unknown')) }}</span>
· {{ t('presence.updated') }}:
<span data-presence-updated>{% if asset.mesh_presence_updated_at %}<time class="local-time" datetime="{{ utc_iso(asset.mesh_presence_updated_at) }}" data-utc="{{ utc_iso(asset.mesh_presence_updated_at) }}">{{ asset.mesh_presence_updated_at }}</time>{% else %}{{ '' }}{% endif %}</span>
· {{ t('presence.last_seen') }}:
<span data-presence-last-seen>{% if asset.mesh_last_seen %}<time class="local-time" datetime="{{ utc_iso(asset.mesh_last_seen) }}" data-utc="{{ utc_iso(asset.mesh_last_seen) }}">{{ asset.mesh_last_seen }}</time>{% else %}{{ '' }}{% endif %}</span>
· {{ t('presence.last_online') }}:
<span data-presence-last-online>{% if asset.mesh_last_online_at %}<time class="local-time" datetime="{{ utc_iso(asset.mesh_last_online_at) }}" data-utc="{{ utc_iso(asset.mesh_last_online_at) }}">{{ asset.mesh_last_online_at }}</time>{% else %}{{ '' }}{% endif %}</span>
</span>
</div>
<div class="inventory-tabs" role="tablist">
<button type="button" class="inventory-tab active" data-inventory-tab="hardware">{{ t('inventory.tab.hardware') }}</button>
<button type="button" class="inventory-tab" data-inventory-tab="software">{{ t('inventory.tab.software') }}{% if installed_software %} ({{ installed_software|length }}){% endif %}</button>
<button type="button" class="inventory-tab" data-inventory-tab="raw">{{ t('inventory.tab.raw_json') }}</button>
<button type="button" class="inventory-tab" data-inventory-tab="sync">{{ t('inventory.tab.sync') }}</button>
</div>
<div class="inventory-tab-content active" data-inventory-panel="hardware">
{% if hardware_inventory %}
<div class="inventory-tree">{% for key, value in hardware_inventory.items() %}{{ inventory_node(key, value) }}{% endfor %}</div>
{% else %}<div class="empty-state">{{ t('inventory.no_hardware') }}</div>{% endif %}
</div>
<div class="inventory-tab-content" data-inventory-panel="software">
<div class="asset-software-toolbar">
<div>
<h3>{{ t('software.installed') }}</h3>
{% if installed_software %}
<p class="muted">{{ t('software.inventory_count', '{count} software entries stored.', count=installed_software|length) }}</p>
{% else %}
<p class="muted">{{ t('software.inventory_not_initialized') }}</p>
{% endif %}
</div>
{% if is_admin %}
<form method="post" action="/assets/{{ asset.id }}/software-inventory" class="inline-form">
<input type="hidden" name="return_to" value="asset-detail">
<button type="submit" class="button">
{% if installed_software %}{{ t('software.update_inventory') }}{% else %}{{ t('software.initialize_inventory') }}{% endif %}
</button>
</form>
{% endif %}
</div>
{% if latest_software_job %}
<div class="software-inventory-meta">
<span><strong>{{ t('software.last_job') }}:</strong> <a href="/software/jobs/{{ latest_software_job.id }}">#{{ latest_software_job.id }}</a></span>
<span><strong>{{ t('software.status') }}:</strong> <span class="job-status job-status-{{ latest_software_job.status }}">{{ t('software.status.' ~ latest_software_job.status, latest_software_job.status) }}</span></span>
<span><strong>{{ t('software.last_inventory') }}:</strong> {% if latest_software_job.finished_at or latest_software_job.created_at %}<time class="local-time" datetime="{{ utc_iso(latest_software_job.finished_at or latest_software_job.created_at) }}" data-utc="{{ utc_iso(latest_software_job.finished_at or latest_software_job.created_at) }}">{{ latest_software_job.finished_at or latest_software_job.created_at }}</time>{% else %}{{ '' }}{% endif %}</span>
</div>
{% endif %}
{% if installed_software %}
<div data-software-selection>
{% if is_admin %}<form method="post" action="/software/inventory/delete" data-software-delete-form data-confirm-template="{{ t('software.delete.confirm')|e }}" data-inventory-only-warning="{{ t('software.delete.inventory_only')|e }}" class="software-delete-toolbar">
<input type="hidden" name="redirect_to" value="/assets/{{ asset.id }}?inventory_tab=software">
<span>{{ t('software.selected_count') }}: <strong data-software-selection-count>0</strong></span>
<button type="submit" class="button button-danger" data-software-delete-button disabled>🗑 {{ t('software.delete.selected') }}</button>
</form>{% endif %}
<div class="table-scroll management-table-scroll">
<table class="data-table" id="asset-installed-software-table" data-storage-key="asset-installed-software-{{ asset.id }}">
<thead><tr>
{% if is_admin %}<th data-no-sort="1"><input type="checkbox" data-software-select-all aria-label="{{ t('software.select_all_visible')|e }}"></th>{% endif %}
<th>{{ t('common.name') }}</th>
<th>{{ t('about.version') }}</th>
<th>{{ t('software.publisher') }}</th>
<th>{{ t('software.architecture') }}</th>
<th>{{ t('software.platform') }}</th>
<th>{{ t('software.install_date') }}</th>
<th>{{ t('software.last_seen') }}</th>
</tr></thead>
<tbody>
{% for entry in installed_software %}
<tr>
{% if is_admin %}<td><input type="checkbox" value="{{ entry.id }}" data-software-entry-checkbox aria-label="{{ t('software.select_entry', name=entry.name)|e }}"></td>{% endif %}
<td>{{ entry.name }}</td>
<td>{{ entry.version or '' }}</td>
<td>{{ entry.publisher or '' }}</td>
<td>{{ entry.architecture or '' }}</td>
<td>{{ t('software.platform.' ~ (entry.platform or 'unknown'), entry.platform or 'unknown') }}</td>
<td data-sort-value="{{ entry.install_date or '' }}">{{ entry.install_date or '' }}</td>
<td data-sort-value="{{ entry.last_seen_at.isoformat() if entry.last_seen_at else '' }}">{% if entry.last_seen_at %}<time class="local-time" datetime="{{ utc_iso(entry.last_seen_at) }}" data-utc="{{ utc_iso(entry.last_seen_at) }}">{{ entry.last_seen_at }}</time>{% else %}{{ '' }}{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<div class="empty-state software-empty-inventory">
<strong>{{ t('software.inventory_not_initialized') }}</strong>
<span>{{ t('software.inventory_initialize_help') }}</span>
</div>
{% endif %}
</div>
<div class="inventory-tab-content" data-inventory-panel="raw">
{% if raw_inventory %}<pre class="json-viewer">{{ raw_inventory|tojson(indent=2) }}</pre>{% else %}<div class="empty-state">{{ t('inventory.no_raw_data') }}</div>{% endif %}
</div>
<div class="inventory-tab-content" data-inventory-panel="sync">
<dl class="config-list"><dt>{{ t('mesh.last_sync') }}</dt><dd>{{ asset.mesh_last_sync or '' }}</dd><dt>{{ t('mesh.message') }}</dt><dd>{{ asset.mesh_sync_message or '' }}</dd></dl>
{% if asset.mesh_conflicts %}<h3>{{ t('mesh.field_conflicts') }}</h3><table class="data-table"><thead><tr><th>{{ t('common.field') }}</th><th>{{ t('mesh.local_value') }}</th><th>MeshCentral</th><th>{{ t('mesh.rule') }}</th></tr></thead><tbody>{% for field, conflict in asset.mesh_conflicts.items() %}<tr><td>{{ field }}</td><td>{{ conflict.local }}</td><td>{{ conflict.meshcentral }}</td><td>{{ conflict.rule }}</td></tr>{% endfor %}</tbody></table>{% endif %}
</div>
</section>
{% endif %}
<script>
(() => {
const params = new URLSearchParams(window.location.search);
if (params.get('inventory_tab') !== 'software') return;
const button = document.querySelector('[data-inventory-tab="software"]');
button?.click();
})();
</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 %}
<script src="/static/js/issue-dialog.js"></script>
<script src="/static/js/asset-inventory.js"></script>
{% endblock %}