Webapp redesign phase 2: Overview and imaging floor
Imaging floor (/imaging, /imaging/tiles): - One line per PC: status chip, serial with machine number, hostname and IP, stage text with a segmented stage track, one action plus a '...' menu. Everything else (description, error, timestamps, Intune ID + QR, Set category / ARTS links, LAPS QR, log tail) is in the row's expandable body. - Order: failed, needs action (stage 7), in progress, done; log-only bays fold into one collapsed group. imaging_status.floor_state() and floor_order() compute this. - Status filter buttons with live counts next to the text filter. SSE refresh, expanded-row memory and LAPS handling are unchanged. - Stage text is never truncated; BIOS updates and erasing are bold. Overview (/): - Services popover, technician to-do line, the six most urgent floor rows (?limit=6 partial, refreshed every 15 s), and image cards with lint warnings, map errors and missing zips as separate chips. New, clone, delete and unattend stay on the cards' menus. Copy buttons are now handled once in static/app.js, with a fallback for plain-http pages where navigator.clipboard is unavailable.
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
{# Per-bay tiles. Rendered standalone by /imaging/tiles for SSE refresh, and
|
||||
included by imaging.html for first paint. Receives `sessions`. #}
|
||||
{# Imaging floor rows. Rendered standalone by /imaging/tiles for the SSE
|
||||
refresh, and included by imaging.html and the Overview for first paint.
|
||||
Receives `sessions` (already in floor order, each with floor_state),
|
||||
and optionally `compact` / `more` from the Overview.
|
||||
One line per PC; everything else is in the row's expandable body. #}
|
||||
{% import '_ui.html' as ui %}
|
||||
{% set stage_labels = {
|
||||
1: ('Booting from PXE', 'WinPE loaded - applying Windows image to disk.'),
|
||||
2: ('Configuring Windows', 'First boot. Running shopfloor setup baseline scripts.'),
|
||||
@@ -35,29 +39,26 @@
|
||||
'stage_2_smb_winpe': ('WinPE running (no push)','Client has opened SMB shares - WinPE is up but startnet has not pushed status.'),
|
||||
} %}
|
||||
|
||||
{% for s in sessions %}
|
||||
{% set state_chip = {
|
||||
'failed': ('Failed', 'fail'),
|
||||
'action': ('Needs action', 'act'),
|
||||
'running': ('In progress', 'run'),
|
||||
'done': ('Done', 'ok'),
|
||||
'logs': ('Seen in logs', 'off'),
|
||||
} %}
|
||||
{% set track_kind = {'failed': 'fail', 'action': 'act', 'running': 'run', 'done': 'ok'} %}
|
||||
|
||||
{% macro floor_row(s) %}
|
||||
{% set is_inferred = (s.source == 'inferred') %}
|
||||
{% set is_blancco = (s.source == 'blancco' or s.pctype == 'blancco') %}
|
||||
{% set stage_idx = s.stage_index | int(0) %}
|
||||
{% set stage_total = s.stage_total | int(0) %}
|
||||
{% set pct = 100 if s.status == 'succeeded' else ((stage_idx / stage_total * 100) | round(0, 'floor')) if stage_total > 0 else 0 %}
|
||||
{# Blancco erasing: fold the drive % ("... [75%]") into the bar between
|
||||
the start of stage 4 and the start of stage 5. #}
|
||||
{% set state = s.floor_state or 'running' %}
|
||||
{% set drive_pct = None %}
|
||||
{% if is_blancco and stage_idx == 4 and s.current_stage and '%]' in s.current_stage %}
|
||||
{% set drive_pct = s.current_stage.split('[')[-1].split('%')[0] | int(0) %}
|
||||
{% if stage_total > 0 and s.status != 'succeeded' %}
|
||||
{% set pct = (((stage_idx - 1) + drive_pct / 100) / stage_total * 100) | round(0, 'floor') %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% set pct = pct | int %}
|
||||
{% set is_failed = s.status == 'failed' %}
|
||||
{% set is_done = s.status == 'succeeded' %}
|
||||
{% if is_inferred %}
|
||||
{% set border = 'warning' %}
|
||||
{% else %}
|
||||
{% set border = 'danger' if is_failed else ('success' if is_done else 'primary') %}
|
||||
{% endif %}
|
||||
{% set warn = False %}
|
||||
{% if is_inferred %}
|
||||
{% set friendly = inferred_stage_labels.get(s.current_stage, ('Inferred from logs', s.current_stage or '')) %}
|
||||
{% elif is_blancco %}
|
||||
@@ -66,11 +67,13 @@
|
||||
{% else %}
|
||||
{% set friendly = blancco_stage_labels.get(stage_idx, ('Stage ' ~ stage_idx, '')) %}
|
||||
{% endif %}
|
||||
{% if stage_idx == 4 and s.status != 'failed' %}{% set warn = True %}{% endif %}
|
||||
{% else %}
|
||||
{% set friendly = stage_labels.get(stage_idx, ('Stage ' ~ stage_idx, '')) %}
|
||||
{% if stage_idx == 1 and s.current_stage and 'bios' in s.current_stage|lower %}
|
||||
{% set friendly = ('Updating BIOS firmware',
|
||||
{% set friendly = ('Updating BIOS: do not power off',
|
||||
'WinPE detected a firmware update for this model. Do NOT power off until the next reboot. Imaging continues afterward.') %}
|
||||
{% set warn = True %}
|
||||
{% endif %}
|
||||
{% if stage_idx == 7 and s.current_stage %}
|
||||
{% set _cs = s.current_stage|lower %}
|
||||
@@ -86,144 +89,117 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% set label = friendly[0] %}
|
||||
{% if drive_pct is not none and s.status != 'failed' %}{% set label = 'Erasing: ' ~ drive_pct ~ '%' %}{% endif %}
|
||||
{% set card_key = s.serial or ('mac-' ~ (s.mac or 'unknown') ~ '-' ~ (s.ip or '')) %}
|
||||
<details class="card mb-2 imaging-card{% if is_inferred %} imaging-inferred{% endif %}"
|
||||
style="border-left:4px solid var(--{{ border }});"
|
||||
data-serial="{{ s.serial or '' }}"
|
||||
data-key="{{ card_key }}"
|
||||
data-mac="{{ s.mac or '' }}"
|
||||
data-ip="{{ s.ip or '' }}"
|
||||
data-source="{{ s.source or 'client' }}"
|
||||
{% set ident = [] %}
|
||||
{% if s.machinenumber %}{% set _ = ident.append('Machine ' ~ s.machinenumber) %}{% endif %}
|
||||
{% if s.hostname_target and s.serial %}{% set _ = ident.append(s.hostname_target) %}{% endif %}
|
||||
{% if s.ip %}{% set _ = ident.append(s.ip) %}{% endif %}
|
||||
{% set title = s.serial or s.hostname_target or s.mac or s.ip or '(unknown bay)' %}
|
||||
{% set kind = 'Blancco wipe' if is_blancco else (s.pctype or '') %}
|
||||
<div class="ui-row floor-row imaging-card" data-state="{{ state }}"
|
||||
data-serial="{{ s.serial or '' }}" data-key="{{ card_key }}" data-mac="{{ s.mac or '' }}"
|
||||
data-ip="{{ s.ip or '' }}" data-source="{{ s.source or 'client' }}"
|
||||
data-filter="{{ (s.serial or '')|lower }} {{ (s.hostname_target or '')|lower }} {{ (s.pctype or '')|lower }} {{ (s.machinenumber or '')|lower }} {{ (s.intune_device_id or '')|lower }} {{ friendly[0]|lower }} stage-{{ stage_idx }} {{ (s.status or 'in_progress')|lower }} {{ (s.mac or '')|lower }} {{ (s.ip or '') }} {{ s.source or 'client' }}">
|
||||
<summary class="card-body py-2" style="cursor:pointer; list-style:none;">
|
||||
<div class="d-flex flex-wrap gap-3 align-items-center">
|
||||
{% if s.intune_device_id %}
|
||||
<div data-qr="{{ s.intune_device_id }}" data-qr-size="96" data-qr-ec="M"
|
||||
style="line-height:0; flex-shrink:0;"
|
||||
title="Intune Device ID: {{ s.intune_device_id }}"></div>
|
||||
{% else %}
|
||||
<div class="d-flex align-items-center justify-content-center eyebrow"
|
||||
style="width:96px; height:96px; border-radius:0.25rem; flex-shrink:0; text-align:center; padding:0.25rem; background:var(--bg); border:1px solid var(--border);">
|
||||
{% if is_inferred %}log-only{% elif is_blancco %}WIPE{% if drive_pct is not none %}<br>{{ drive_pct }}%{% endif %}{% else %}no DeviceId{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex-grow-1" style="min-width:0;">
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<strong class="fs-6">
|
||||
{% if s.serial %}{{ s.serial }}
|
||||
{% elif s.hostname_target %}{{ s.hostname_target }}
|
||||
{% elif s.mac %}{{ s.mac }}
|
||||
{% else %}{{ s.ip or '(unknown bay)' }}{% endif %}
|
||||
</strong>
|
||||
{% if s.hostname_target and s.serial %}<code class="mono" style="color:var(--text-light);">{{ s.hostname_target }}</code>{% endif %}
|
||||
{% if s.ip %}<code class="mono" style="color:var(--text-light);">{{ s.ip }}</code>{% endif %}
|
||||
{% if s.pctype %}<span class="badge badge-info">{{ s.pctype }}</span>{% endif %}
|
||||
{% if s.machinenumber %}<span class="badge badge-secondary">#{{ s.machinenumber }}</span>{% endif %}
|
||||
{% if is_inferred %}<span class="badge badge-warning">log-inferred</span>{% endif %}
|
||||
<span class="badge badge-{{ border }} ms-auto">{{ s.status or 'in_progress' }}</span>
|
||||
<details class="imaging-toggle">
|
||||
<summary class="frow">
|
||||
<span class="g-st">{{ ui.chip(state_chip[state][0], state_chip[state][1]) }}</span>
|
||||
<span class="g-pc"><span class="mono serial">{{ title }}</span><span class="sub">{{ ident | join(' | ') or ('MAC ' ~ s.mac if s.mac else '') }}</span></span>
|
||||
<span class="g-stage">
|
||||
<span class="stageline"><span class="txt{{ ' warnline' if warn }}">{{ label }}</span>{% if kind %}<span class="kind">{{ kind }}</span>{% endif %}{% if not is_inferred %}<span class="mono step">{{ stage_idx }}/{{ stage_total or '?' }}</span>{% endif %}<span class="chev" aria-hidden="true">▾</span></span>
|
||||
{% if not is_inferred and stage_total %}{{ ui.track(stage_total, stage_idx, track_kind.get(state, 'run'),
|
||||
label=('Done, all ' ~ stage_total ~ ' stages' if state == 'done' else 'Stage ' ~ stage_idx ~ ' of ' ~ stage_total)) }}{% endif %}
|
||||
</span>
|
||||
</summary>
|
||||
<div class="fbody">
|
||||
<div class="fbody-main">
|
||||
{% if friendly[1] %}<p class="muted" style="margin: 0">{{ friendly[1] }}</p>{% endif %}
|
||||
{% if s.error %}<div class="errbox"><strong>Error:</strong> {{ s.error }}</div>{% endif %}
|
||||
<div class="sub fmeta">
|
||||
<span>Started <span class="mono">{{ s.started_at | local_dt(true) }}</span></span>
|
||||
<span>Last update <span class="mono">{{ s.last_updated | local_dt(true) }}</span></span>
|
||||
<span>MAC <span class="mono">{{ s.mac or '-' }}</span></span>
|
||||
{% if s.current_stage %}<span>Raw stage <span class="mono">{{ s.current_stage }}</span></span>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<strong>{{ friendly[0] }}</strong>
|
||||
{% if is_blancco and stage_idx == 4 and s.current_stage %}<span class="mono" style="color:var(--text-light); font-size:0.8rem;">{{ s.current_stage | replace('Erasing: ', '') }}</span>{% endif %}
|
||||
{% if not is_inferred %}
|
||||
<span class="badge badge-secondary">{{ stage_idx }}/{{ stage_total or '?' }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not is_inferred %}<span class="eyebrow">{{ pct }}%</span>{% endif %}
|
||||
</div>
|
||||
{% if not is_inferred %}
|
||||
<div class="progress mt-1" style="height:0.7rem; background:var(--bg); border-radius:0.25rem;">
|
||||
<div class="progress-bar bg-{{ border }} {% if not is_done and not is_failed %}progress-bar-striped progress-bar-animated{% endif %}"
|
||||
role="progressbar" style="width: {{ pct }}%; background-color:var(--{{ border }});"
|
||||
aria-valuenow="{{ pct }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
{% if s.intune_device_id %}
|
||||
<div class="fline"><span class="sub">Intune device</span> <span class="mono">{{ s.intune_device_id }}</span>
|
||||
<button type="button" class="ui-btn sm ghost copy-btn" data-copy-text="{{ s.intune_device_id }}">Copy</button>
|
||||
<a class="ui-btn sm" target="_blank" rel="noopener"
|
||||
href="https://portal.azure.us/?feature.msaljs=false#view/Microsoft_Intune_Devices/DeviceSettingsMenuBlade/~/properties/aadDeviceId/{{ s.intune_device_id }}">Set category</a>
|
||||
<a class="ui-btn sm" target="_blank" rel="noopener" href="https://arts.dw.geaerospace.net/requests/type">ARTS request</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
<div class="card-body pt-0 pb-3" style="border-top:1px solid var(--border);">
|
||||
{% if friendly[1] %}<div class="mt-2" style="color:var(--text-light); font-size:0.85rem;">{{ friendly[1] }}</div>{% endif %}
|
||||
|
||||
{% if s.intune_device_id %}
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mt-3">
|
||||
<span class="eyebrow">Intune</span>
|
||||
<code class="mono">{{ s.intune_device_id }}</code>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary btn-row-action copy-btn"
|
||||
data-copy-text="{{ s.intune_device_id }}">copy</button>
|
||||
<a class="btn btn-sm btn-outline-primary btn-row-action"
|
||||
target="_blank" rel="noopener"
|
||||
href="https://portal.azure.us/?feature.msaljs=false#view/Microsoft_Intune_Devices/DeviceSettingsMenuBlade/~/properties/aadDeviceId/{{ s.intune_device_id }}">set category</a>
|
||||
<a class="btn btn-sm btn-outline-warning btn-row-action"
|
||||
target="_blank" rel="noopener"
|
||||
href="https://arts.dw.geaerospace.net/requests/type">ARTS request</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-2 d-flex flex-wrap gap-3" style="color:var(--text-light); font-size:0.75rem;">
|
||||
<span>started <code class="mono">{{ s.started_at | local_dt(true) }}</code></span>
|
||||
<span>last <code class="mono">{{ s.last_updated | local_dt(true) }}</code></span>
|
||||
<span>MAC <code class="mono">{{ s.mac or '-' }}</code></span>
|
||||
{% if s.current_stage %}<span class="mono">{{ s.current_stage }}</span>{% endif %}
|
||||
</div>
|
||||
|
||||
{% if s.error %}
|
||||
<div class="alert alert-danger py-2 mb-2 mt-3" style="font-size:0.85rem;">
|
||||
<strong>Error:</strong> {{ s.error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_blancco %}
|
||||
<div class="mt-3">
|
||||
<a href="{{ url_for('blancco_reports') }}" class="btn btn-sm btn-outline-primary btn-row-action">Erasure reports</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not is_inferred and not is_blancco %}
|
||||
<div class="mt-3 laps-card" data-serial="{{ s.serial }}">
|
||||
<div class="eyebrow mb-2">LAPS password QR (paste -> scan on bay - persists until cleared)</div>
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
{# Masked: the tile is on a shared screen; Show reveals it while typing. #}
|
||||
<input type="password"
|
||||
class="form-control form-control-sm laps-input mono"
|
||||
style="max-width: 22rem;"
|
||||
placeholder="paste LAPS password from Intune portal here"
|
||||
autocomplete="off"
|
||||
value="{{ s.laps_password or '' }}">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary laps-show-btn" aria-pressed="false">Show</button>
|
||||
<button type="button" class="btn btn-sm btn-primary laps-make-btn">{% if s.laps_password %}Update QR{% else %}Make QR{% endif %}</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary laps-clear-btn" {% if not s.laps_password %}style="display:none;"{% endif %}>Clear</button>
|
||||
</div>
|
||||
<div class="laps-qr-container mt-2"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if s.log_tail %}
|
||||
<details class="mt-3 imaging-log" data-key="{{ card_key }}">
|
||||
<summary class="eyebrow" style="cursor:pointer;">{% if is_inferred %}Evidence{% else %}Log tail{% endif %} ({{ s.log_tail | length }} line{{ 's' if s.log_tail | length != 1 }})</summary>
|
||||
<pre class="mono p-2 mt-2 mb-0 imaging-log-pre" style="max-height: 12rem; overflow-y: auto; background:var(--bg); border:1px solid var(--border); border-radius:0.25rem; font-size:0.78rem;">{% for line in s.log_tail %}{{ line }}
|
||||
{% if compact and not is_inferred and not is_blancco %}
|
||||
<div class="sub">LAPS QR and the live log are on the <a href="{{ url_for('imaging_dashboard') }}">imaging floor</a>.</div>
|
||||
{% elif not is_inferred and not is_blancco %}
|
||||
<div class="laps-card" data-serial="{{ s.serial }}">
|
||||
<div class="sub" style="margin-bottom: 6px">LAPS password QR: paste, then scan on the bay. Kept until cleared.</div>
|
||||
<div class="fline">
|
||||
{# Masked: the floor is on a shared screen; Show reveals it while typing. #}
|
||||
<input type="password" class="field laps-input mono" style="max-width: 22rem; flex: 1 1 200px"
|
||||
placeholder="Paste the LAPS password from Intune" autocomplete="off" aria-label="LAPS password for {{ title }}"
|
||||
value="{{ s.laps_password or '' }}">
|
||||
<button type="button" class="ui-btn laps-show-btn" aria-pressed="false">Show</button>
|
||||
<button type="button" class="ui-btn pri laps-make-btn">{% if s.laps_password %}Update QR{% else %}Make QR{% endif %}</button>
|
||||
<button type="button" class="ui-btn laps-clear-btn" {% if not s.laps_password %}style="display:none;"{% endif %}>Clear</button>
|
||||
</div>
|
||||
<div class="laps-qr-container" style="margin-top: 8px"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if s.log_tail %}
|
||||
<details class="imaging-log" data-key="{{ card_key }}">
|
||||
<summary class="disc"><span class="chev" aria-hidden="true">▾</span>{% if is_inferred %}Evidence{% else %}Log tail{% endif %} ({{ s.log_tail | length }} line{{ 's' if s.log_tail | length != 1 }})</summary>
|
||||
<pre class="mono imaging-log-pre">{% for line in s.log_tail %}{{ line }}
|
||||
{% endfor %}</pre>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
{% if not is_inferred and s.serial %}
|
||||
<div class="mt-3 d-flex justify-content-between align-items-center">
|
||||
<a href="{{ url_for('imaging_session_detail', serial=s.serial) }}"
|
||||
class="btn btn-sm btn-outline-primary">
|
||||
Details / full log
|
||||
</a>
|
||||
<form method="post" action="{{ url_for('imaging_delete_session', serial=s.serial) }}" style="display: inline;">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="return confirm('Clear session {{ s.serial }}?');">
|
||||
Clear
|
||||
</button>
|
||||
</form>
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if s.intune_device_id %}
|
||||
<div class="fbody-qr" data-qr="{{ s.intune_device_id }}" data-qr-size="112" data-qr-ec="M"
|
||||
title="Intune device ID {{ s.intune_device_id }}" role="img" aria-label="QR code of the Intune device ID"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
<div class="act g-act">
|
||||
{% if state == 'action' and s.intune_device_id %}
|
||||
<a class="ui-btn pri" target="_blank" rel="noopener"
|
||||
href="https://portal.azure.us/?feature.msaljs=false#view/Microsoft_Intune_Devices/DeviceSettingsMenuBlade/~/properties/aadDeviceId/{{ s.intune_device_id }}">Set category</a>
|
||||
{% elif is_blancco and state in ('done', 'failed') %}
|
||||
<a class="ui-btn" href="{{ url_for('blancco_reports') }}">Report</a>
|
||||
{% elif not is_inferred and s.serial %}
|
||||
<a class="ui-btn" href="{{ url_for('imaging_session_detail', serial=s.serial) }}">{{ 'Full log' if state == 'failed' else 'Details' }}</a>
|
||||
{% endif %}
|
||||
{% if not is_inferred and s.serial %}
|
||||
{% call ui.more(title) %}
|
||||
<a href="{{ url_for('imaging_session_detail', serial=s.serial) }}">Details and full log</a>
|
||||
{% if s.intune_device_id %}<button type="button" class="copy-btn" data-copy-text="{{ s.intune_device_id }}">Copy Intune device ID</button>
|
||||
<a target="_blank" rel="noopener" href="https://arts.dw.geaerospace.net/requests/type">ARTS request</a>{% endif %}
|
||||
{% if is_blancco %}<a href="{{ url_for('blancco_reports') }}">Erasure reports</a>{% endif %}
|
||||
<div class="sep"></div>
|
||||
<form method="post" action="{{ url_for('imaging_delete_session', serial=s.serial) }}"
|
||||
onsubmit="return confirm('Clear the session for {{ s.serial }}? Its status and log files are deleted.');">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="del">Clear session</button>
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% set real = sessions | rejectattr('floor_state', 'equalto', 'logs') | list %}
|
||||
{% set logs = sessions | selectattr('floor_state', 'equalto', 'logs') | list %}
|
||||
{% for s in real %}{{ floor_row(s) }}{% endfor %}
|
||||
{% if not compact and logs %}
|
||||
<details class="floor-logs">
|
||||
<summary class="disc"><span class="chev" aria-hidden="true">▾</span><span><strong>{{ logs | length }}</strong> seen in DHCP/TFTP logs only, no status pushed yet</span></summary>
|
||||
{% for s in logs %}{{ floor_row(s) }}{% endfor %}
|
||||
</details>
|
||||
{% endif %}
|
||||
{% if compact and more %}
|
||||
<div class="hidden-bar"><span>{{ more }} more on the floor</span><a class="ui-btn" href="{{ url_for('imaging_dashboard') }}">Open imaging floor</a></div>
|
||||
{% endif %}
|
||||
{% if not sessions %}
|
||||
<div class="floor-empty muted">No PCs on the imaging network right now. A PC appears here once it touches DHCP, TFTP or boot.wim.</div>
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user