From 0b86255711c407d9315aaeeae5f11e3999754051 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Wed, 14 Oct 2026 13:44:07 -0400 Subject: [PATCH] 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. --- webapp/app.py | 29 ++- webapp/services/imaging_status.py | 36 ++++ webapp/static/app.js | 27 +++ webapp/static/pxe-ui.css | 40 ++++ webapp/templates/_imaging_tiles.html | 276 ++++++++++++--------------- webapp/templates/base.html | 2 +- webapp/templates/dashboard.html | 232 +++++++++------------- webapp/templates/imaging.html | 207 ++++++++------------ 8 files changed, 427 insertions(+), 422 deletions(-) diff --git a/webapp/app.py b/webapp/app.py index 79a5a2d..f05cd9e 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -103,13 +103,30 @@ app.register_blueprint(images_unattend_views.bp) # Unattend review/save/undo @app.route("/") def dashboard(): image_list = [images.image_status(it) for it in config.IMAGE_TYPES] + for img in image_list: + # Card details: OS, driver entries and lint, from the image pages. + try: + info = image_pages.header_info(img["image_type"]) + img["os_label"] = info["os_label"] + img["driver_count"] = info["driver_count"] + except Exception: + img["os_label"], img["driver_count"] = "", 0 + img["lint"] = image_pages._lint_counts(img["image_type"]) services = [system.service_status(s) for s in ("dnsmasq", "apache2", "smbd")] + sessions, _hidden = imaging_status.hide_stale(imaging_status.list_sessions()) + floor = imaging_status.floor_order(sessions) + real = [x for x in floor if x["floor_state"] != "logs"] + counts = {k: sum(1 for x in real if x["floor_state"] == k) for k in ("failed", "action", "running", "done")} return render_template( "dashboard.html", images=image_list, services=services, image_types=config.IMAGE_TYPES, friendly_names=config.FRIENDLY_NAMES, + sessions=real[:6], + more=max(0, len(real) - 6), + floor_counts=counts, + compact=True, ) @@ -845,7 +862,7 @@ def imaging_dashboard(): hidden = 0 if not show_all: sessions, hidden = imaging_status.hide_stale(sessions) - return render_template("imaging.html", sessions=sessions, hidden=hidden, + return render_template("imaging.html", sessions=imaging_status.floor_order(sessions), hidden=hidden, show_all=show_all, window_hours=imaging_status.ACTIVE_WINDOW_HOURS) @@ -857,7 +874,15 @@ def imaging_tiles_partial(): sessions = imaging_status.list_sessions() if request.args.get("all") != "1": sessions, _hidden = imaging_status.hide_stale(sessions) - return render_template("_imaging_tiles.html", sessions=sessions) + sessions = imaging_status.floor_order(sessions) + # ?limit=N (the Overview): the first N PCs, log-only bays left out. + limit = request.args.get("limit", type=int) + more = 0 + if limit: + real = [x for x in sessions if x["floor_state"] != "logs"] + more = max(0, len(real) - limit) + sessions = real[:limit] + return render_template("_imaging_tiles.html", sessions=sessions, more=more, compact=bool(limit)) def _sessions_hash() -> str: diff --git a/webapp/services/imaging_status.py b/webapp/services/imaging_status.py index 201f17f..a7814c8 100644 --- a/webapp/services/imaging_status.py +++ b/webapp/services/imaging_status.py @@ -243,6 +243,42 @@ def hide_stale(sessions: list[dict], hours: float = ACTIVE_WINDOW_HOURS) -> tupl return kept, hidden +# Imaging floor order: what needs a person first, then live work, then done. +FLOOR_RANK = {"failed": 0, "action": 1, "running": 2, "done": 3, "logs": 4} + + +def floor_state(s: dict) -> str: + """One of failed / action / running / done / logs for the floor list. + + "action" is stage 7 of a Windows imaging run (registered in Intune and + waiting for someone to assign the device category or request lockdown). + """ + if s.get("source") == "inferred": + return "logs" + status = s.get("status") or "in_progress" + if status == "failed": + return "failed" + if status == "succeeded": + return "done" + blancco = s.get("source") == "blancco" or s.get("pctype") == "blancco" + try: + idx = int(s.get("stage_index") or 0) + except (TypeError, ValueError): + idx = 0 + if not blancco and idx == 7: + return "action" + return "running" + + +def floor_order(sessions: list[dict]) -> list[dict]: + """Sessions sorted for the floor, each tagged with "floor_state".""" + for s in sessions: + s["floor_state"] = floor_state(s) + # Stable sorts: newest first within each state. + out = sorted(sessions, key=lambda s: s.get("last_updated") or "", reverse=True) + return sorted(out, key=lambda s: FLOOR_RANK[s["floor_state"]]) + + def get_session(serial: str) -> Optional[dict]: path = _path_for(serial) if not os.path.isfile(path): diff --git a/webapp/static/app.js b/webapp/static/app.js index 9db19a5..3d2c2b1 100644 --- a/webapp/static/app.js +++ b/webapp/static/app.js @@ -467,3 +467,30 @@ document.addEventListener('DOMContentLoaded', function () { }); + +// Copy buttons: any .copy-btn[data-copy-text], on every page. The LAN is +// plain http, so navigator.clipboard is often unavailable; fall back to a +// hidden textarea + execCommand. The button briefly reads "Copied". +document.addEventListener('click', function (e) { + var btn = e.target.closest && e.target.closest('.copy-btn'); + if (!btn) return; + var text = btn.getAttribute('data-copy-text') || ''; + if (!text) return; + var done = function (ok) { + var orig = btn.getAttribute('data-orig-label') || btn.textContent; + btn.setAttribute('data-orig-label', orig); + btn.textContent = ok ? 'Copied' : 'Copy failed'; + setTimeout(function () { btn.textContent = orig; }, 1400); + }; + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(text).then(function () { done(true); }, function () { done(false); }); + return; + } + var ta = document.createElement('textarea'); + ta.value = text; ta.setAttribute('readonly', ''); ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (err) { ok = false; } + document.body.removeChild(ta); + done(ok); +}); diff --git a/webapp/static/pxe-ui.css b/webapp/static/pxe-ui.css index a793156..8ec9679 100644 --- a/webapp/static/pxe-ui.css +++ b/webapp/static/pxe-ui.css @@ -237,3 +237,43 @@ details.more .menu form { margin: 0; } /* Flash messages restyled to the token palette (Bootstrap markup kept). */ .ui-main > .alert { border-radius: 10px; border: 0; box-shadow: 0 1px 2px var(--shadow); } + +/* -------------------------------------------------------------------------- + Imaging floor rows (_imaging_tiles.html), used by /imaging and Overview. + -------------------------------------------------------------------------- */ +.floor { container-type: inline-size; } +.frow, .fhead { + display: grid; grid-template-columns: 118px minmax(160px, 270px) minmax(0, 1fr); + grid-template-areas: "st pc stage"; align-items: center; gap: 0 20px; padding: 14px 230px 14px 24px; +} +.fhead { padding-top: 6px; padding-bottom: 6px; font-size: 12px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); } +.g-st { grid-area: st; } .g-pc { grid-area: pc; min-width: 0; display: flex; flex-direction: column; } .g-stage { grid-area: stage; min-width: 0; } +.floor-row .g-act { position: absolute; right: 20px; top: 16px; width: 190px; justify-content: flex-end; } +.floor-row .serial { font-size: 15px; } +.stageline { display: flex; align-items: center; gap: 12px; font-size: 13px; min-height: 24px; } +.stageline .txt { flex: 1 1 auto; min-width: 0; white-space: normal; } +.stageline .warnline { font-weight: 600; } +.stageline .kind { flex: none; font-size: 12px; color: var(--muted); max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.stageline .step { flex: none; color: var(--muted); font-size: 12px; } +.fbody { display: flex; gap: 24px; align-items: flex-start; padding: 0 24px 16px 162px; font-size: 13px; } +.fbody-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 10px; } +.fbody-qr { flex: none; line-height: 0; background: #FFFFFF; padding: 6px; border-radius: 6px; } +.fmeta { display: flex; flex-wrap: wrap; gap: 4px 16px; } +.fline { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; } +.imaging-log-pre { max-height: 14rem; overflow-y: auto; margin: 6px 0 0; padding: 8px 10px; background: var(--surface-2); border: 1px solid var(--line); border-radius: 6px; font-size: 12px; white-space: pre-wrap; } +.floor-logs { border-top: 1px solid var(--line); } +.floor-logs > summary { padding: 0 24px; min-height: 44px; color: var(--text-2); font-weight: 400; } +.floor-empty { padding: 28px 24px; text-align: center; border-top: 1px solid var(--line); } +@container (max-width: 820px) { + .fhead { display: none; } + .frow { grid-template-columns: auto minmax(0, 1fr); grid-template-areas: "st pc" "stage stage"; gap: 10px 14px; padding: 14px 18px; } + .floor-row .g-act { position: static; width: auto; display: flex; justify-content: flex-end; padding: 0 18px 12px; } + .fbody { padding: 0 18px 14px; flex-direction: column; } +} + +/* Overview image cards */ +.img-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(250px, 100%), 1fr)); gap: 16px; } +.img-card { padding: 16px 18px; display: flex; flex-direction: column; gap: 8px; min-width: 0; } +.img-top { display: flex; align-items: center; gap: 8px; } +.img-name { font-weight: 600; font-size: 16px; color: var(--text); text-decoration: none; margin-right: auto; } +.img-name:hover { color: var(--accent); } diff --git a/webapp/templates/_imaging_tiles.html b/webapp/templates/_imaging_tiles.html index a2d0f87..fbe0ea9 100644 --- a/webapp/templates/_imaging_tiles.html +++ b/webapp/templates/_imaging_tiles.html @@ -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 '')) %} -
- -
- {% if s.intune_device_id %} -
- {% else %} -
- {% if is_inferred %}log-only{% elif is_blancco %}WIPE{% if drive_pct is not none %}
{{ drive_pct }}%{% endif %}{% else %}no DeviceId{% endif %} -
- {% endif %} - -
-
- - {% if s.serial %}{{ s.serial }} - {% elif s.hostname_target %}{{ s.hostname_target }} - {% elif s.mac %}{{ s.mac }} - {% else %}{{ s.ip or '(unknown bay)' }}{% endif %} - - {% if s.hostname_target and s.serial %}{{ s.hostname_target }}{% endif %} - {% if s.ip %}{{ s.ip }}{% endif %} - {% if s.pctype %}{{ s.pctype }}{% endif %} - {% if s.machinenumber %}#{{ s.machinenumber }}{% endif %} - {% if is_inferred %}log-inferred{% endif %} - {{ s.status or 'in_progress' }} +
+ + {{ ui.chip(state_chip[state][0], state_chip[state][1]) }} + {{ title }}{{ ident | join(' | ') or ('MAC ' ~ s.mac if s.mac else '') }} + + {{ label }}{% if kind %}{{ kind }}{% endif %}{% if not is_inferred %}{{ stage_idx }}/{{ stage_total or '?' }}{% endif %} + {% 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 %} + + +
+
+ {% if friendly[1] %}

{{ friendly[1] }}

{% endif %} + {% if s.error %}
Error: {{ s.error }}
{% endif %} +
+ Started {{ s.started_at | local_dt(true) }} + Last update {{ s.last_updated | local_dt(true) }} + MAC {{ s.mac or '-' }} + {% if s.current_stage %}Raw stage {{ s.current_stage }}{% endif %}
- -
-
- {{ friendly[0] }} - {% if is_blancco and stage_idx == 4 and s.current_stage %}{{ s.current_stage | replace('Erasing: ', '') }}{% endif %} - {% if not is_inferred %} - {{ stage_idx }}/{{ stage_total or '?' }} - {% endif %} -
- {% if not is_inferred %}{{ pct }}%{% endif %} -
- {% if not is_inferred %} -
-
+ {% if s.intune_device_id %} +
Intune device {{ s.intune_device_id }} + + Set category + ARTS request
{% endif %} -
-
-
- -
- {% if friendly[1] %}
{{ friendly[1] }}
{% endif %} - - {% if s.intune_device_id %} -
- Intune - {{ s.intune_device_id }} - - set category - ARTS request -
- {% endif %} - -
- started {{ s.started_at | local_dt(true) }} - last {{ s.last_updated | local_dt(true) }} - MAC {{ s.mac or '-' }} - {% if s.current_stage %}{{ s.current_stage }}{% endif %} -
- - {% if s.error %} -
- Error: {{ s.error }} -
- {% endif %} - - {% if is_blancco %} - - {% endif %} - - {% if not is_inferred and not is_blancco %} -
-
LAPS password QR (paste -> scan on bay - persists until cleared)
-
- {# Masked: the tile is on a shared screen; Show reveals it while typing. #} - - - - -
-
-
- {% endif %} - - {% if s.log_tail %} -
- {% if is_inferred %}Evidence{% else %}Log tail{% endif %} ({{ s.log_tail | length }} line{{ 's' if s.log_tail | length != 1 }}) -
{% for line in s.log_tail %}{{ line }}
+          {% if compact and not is_inferred and not is_blancco %}
+          
LAPS QR and the live log are on the imaging floor.
+ {% elif not is_inferred and not is_blancco %} +
+
LAPS password QR: paste, then scan on the bay. Kept until cleared.
+
+ {# Masked: the floor is on a shared screen; Show reveals it while typing. #} + + + + +
+
+
+ {% endif %} + {% if s.log_tail %} +
+ {% if is_inferred %}Evidence{% else %}Log tail{% endif %} ({{ s.log_tail | length }} line{{ 's' if s.log_tail | length != 1 }}) +
{% for line in s.log_tail %}{{ line }}
 {% endfor %}
-
- {% endif %} - - {% if not is_inferred and s.serial %} -
- - Details / full log - -
- - -
+
+ {% endif %} +
+ {% if s.intune_device_id %} + + {% endif %} +
+
+ {% if state == 'action' and s.intune_device_id %} + Set category + {% elif is_blancco and state in ('done', 'failed') %} + Report + {% elif not is_inferred and s.serial %} + {{ 'Full log' if state == 'failed' else 'Details' }} + {% endif %} + {% if not is_inferred and s.serial %} + {% call ui.more(title) %} + Details and full log + {% if s.intune_device_id %} + ARTS request{% endif %} + {% if is_blancco %}Erasure reports{% endif %} +
+
+ + +
+ {% endcall %} {% endif %}
- -{% endfor %} + +{% 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 %} +
+ {{ logs | length }} seen in DHCP/TFTP logs only, no status pushed yet + {% for s in logs %}{{ floor_row(s) }}{% endfor %} +
+{% endif %} +{% if compact and more %} +
{{ more }} more on the floorOpen imaging floor
+{% endif %} +{% if not sessions %} +
No PCs on the imaging network right now. A PC appears here once it touches DHCP, TFTP or boot.wim.
+{% endif %} diff --git a/webapp/templates/base.html b/webapp/templates/base.html index ba17462..4951f58 100644 --- a/webapp/templates/base.html +++ b/webapp/templates/base.html @@ -98,7 +98,7 @@ {% endif %} {% endwith %} - {% if blancco_todo.total and ep not in ('blancco_todo', 'blancco_servicenow', 'imaging_tiles') %} + {% if blancco_todo.total and ep not in ('blancco_todo', 'blancco_servicenow', 'imaging_tiles', 'dashboard') %} {% set parts = [] %} {% if blancco_todo.servicenow %}{% set _ = parts.append(blancco_todo.servicenow ~ ' to tick in ServiceNow') %}{% endif %} {% if blancco_todo.failed %}{% set _ = parts.append(blancco_todo.failed ~ ' drive' ~ ('s' if blancco_todo.failed != 1) ~ ' to destroy') %}{% endif %} diff --git a/webapp/templates/dashboard.html b/webapp/templates/dashboard.html index ea14827..e68f578 100644 --- a/webapp/templates/dashboard.html +++ b/webapp/templates/dashboard.html @@ -1,151 +1,96 @@ {% extends "base.html" %} -{% block title %}Dashboard - PXE Server Manager{% endblock %} +{% import '_ui.html' as ui %} +{% block title %}Overview - PXE Manager{% endblock %} {% block content %} - +{% set services_up = services | selectattr('active') | list | length %} +{% call ui.head('Overview', 'West Jefferson') %} +
+ + + {{ 'Services OK' if services_up == services|length else (services|length - services_up) ~ ' service' ~ ('s' if services|length - services_up != 1) ~ ' down' }} + + + +
+{% endcall %} - -
- {% set services_up = services | selectattr('active') | list | length %} - {% set services_total = services | list | length %} -
-
Services running
-
{{ services_up }} / {{ services_total }}
-
- {% set images_total = images | list | length %} -
-
Image types
-
{{ images_total }}
-
- {% set images_ready = images | selectattr('has_content') | list | length %} -
-
Images with content
-
{{ images_ready }}
-
- {% set unattend_missing = images | rejectattr('has_unattend') | list | length %} -
-
Missing unattend.xml
-
{{ unattend_missing }}
-
-
+
+{% if blancco_todo.total %} +{% set parts = [] %} +{% if blancco_todo.servicenow %}{% set _ = parts.append(blancco_todo.servicenow ~ ' to tick in ServiceNow') %}{% endif %} +{% if blancco_todo.failed %}{% set _ = parts.append(blancco_todo.failed ~ ' drive' ~ ('s' if blancco_todo.failed != 1) ~ ' to destroy') %}{% endif %} +{% if blancco_todo.evidence %}{% set _ = parts.append(blancco_todo.evidence ~ ' evidence pack' ~ ('s' if blancco_todo.evidence != 1) ~ ' to upload') %}{% endif %} +{% if blancco_todo.not_in_cloud %}{% set _ = parts.append(blancco_todo.not_in_cloud ~ ' not in the Blancco cloud') %}{% endif %} +{% if blancco_todo.hardware %}{% set _ = parts.append(blancco_todo.hardware ~ ' hardware gap' ~ ('s' if blancco_todo.hardware != 1)) %}{% endif %} + + To-do + {{ blancco_todo.total }} open. {{ parts | join(', ') }} + Open to-do + +{% endif %} - -
-
- PXE Services -
-
-
- - - - - - - - - - {% for svc in services %} - - - - - - {% endfor %} - -
ServiceStatusState
- {{ svc.name }} - - - {{ "Running" if svc.active else "Stopped" }} - {{ svc.state }}
+
+
+

Imaging floor

+
+ {% for key, label in (('failed', 'failed'), ('action', 'need action'), ('running', 'in progress'), ('done', 'done')) %} + {{ floor_counts[key] }} {{ label }} + {% endfor %}
-
+ +
{% include "_imaging_tiles.html" %}
+ - -
-
- Deployment Images - -
-
-
- - - - - - - - - - - - {% for img in images %} - - - - - - - - {% endfor %} - -
ImageDeploy Contentunattend.xmlPathActions
- {{ img.friendly_name }}
- {{ img.image_type }} -
- {% if img.has_content %} - Present - {% else %} - Empty - {% endif %} - - {% if img.has_unattend %} - Exists - {% else %} - Missing - {% endif %} - {{ img.deploy_path }} - - Open - - - Unattend - - - -
+
+
+

Images

+
+ Import content +
+
+ {% for img in images %} + {% set warn = img.lint.by_level.get('WARN', 0) if img.lint.available else 0 %} + {% set errs = img.lint.map_errors if img.lint.available else 0 %} + {% set zips = ((img.lint.by_level.get('ERROR', 0) + img.lint.by_level.get('CRITICAL', 0)) - img.lint.map_errors) if img.lint.available else 0 %} +
+
+ {{ img.friendly_name | replace('GE Aerospace ', '') }} + {% if errs %}{{ ui.chip(errs ~ ' error' ~ ('s' if errs != 1), 'err') }}{% endif %} + {% if zips %}{{ ui.chip(zips ~ ' zip' ~ ('s' if zips != 1) ~ ' missing', 'err') }}{% endif %} + {% if errs or zips %} + {% elif warn %}{{ ui.chip(warn ~ ' warning' ~ ('s' if warn != 1), 'warn') }} + {% elif img.lint.available and img.driver_count %}{{ ui.chip('Lint clean', 'ok') }}{% endif %} + {% call ui.more(img.friendly_name) %} + Open + Drivers + Unattend + +
+ + {% endcall %} +
+
{{ img.os_label or 'No OS selected' }} | {{ img.driver_count }} driver entr{{ 'y' if img.driver_count == 1 else 'ies' }}
+ {% if not img.has_content or not img.has_unattend %} +
+ {% if not img.has_content %}{{ ui.chip('Deploy folder empty', 'off') }}{% endif %} + {% if not img.has_unattend %}{{ ui.chip('No unattend.xml', 'warn') }}{% endif %} +
+ {% endif %} +
{{ img.image_type }}
+
+ {% endfor %} +
+
@@ -259,6 +204,17 @@ {% block extra_scripts %}