303 lines
21 KiB
HTML
303 lines
21 KiB
HTML
{% 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 %}
|