- One line per entry: position, model tokens, pack, OS, size and status chips; family, friendly name, osId, folder, zip state, Dell hint and the verbatim lint findings are in the row body. - Status chips compare the staged map with the saved one: New, Edited, Moved from #n, Removing, the worst current finding, and 'Clears on save' / 'Clears n' when the staged changes resolve this entry's findings. The toolbar and a sticky save bar show lint now and after. - Removing an entry is staged with Undo remove instead of deleting the row; Undo edit puts an edited entry back. - Reordering moved into the row menu (Move up / down / to position). The Move dialog shows the order after the move and re-lints the staged map with the move applied, listing any new errors before staging it. - An edit that changes the tokens but keeps the old friendly name is flagged in the row and in the review step. - Filters: Win11/Win10 with counts, 'Findings or unsaved', 'Zip problems' (missing, trap and case); a status key explains the chips. - _image_header.html: new header and underline tabs for every image tab; the shared review modal and diff use the design tokens.
1054 lines
60 KiB
HTML
1054 lines
60 KiB
HTML
{% extends "base.html" %}
|
|
{% import '_ui.html' as ui %}
|
|
{% block title %}{{ friendly_name }} - Drivers{% endblock %}
|
|
|
|
{# Drivers tab: the whole HardwareDriver.json staged in the browser, saved in
|
|
one go through a review step (views/images_drivers.py). The list is drawn
|
|
by the script at the bottom from #drvData; the server re-lints the staged
|
|
map after every edit (/drivers/check). One line per entry; the rest of the
|
|
entry, its findings and what the staged changes clear are in the row body. #}
|
|
|
|
{% block extra_head %}
|
|
<style>
|
|
.drv-rule > summary { display: flex; align-items: center; gap: 10px; padding: 0 16px; min-height: 44px; font-size: 14px; }
|
|
.drv-rule p { margin: 0; padding: 0 16px 14px 42px; font-size: 14px; line-height: 1.6; color: var(--text-2); max-width: 900px; }
|
|
.drv-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 16px 20px 8px; }
|
|
.drv-seg { display: flex; border: 1px solid var(--control); border-radius: 8px; overflow: hidden; flex: none; }
|
|
.drv-seg button { height: 34px; padding: 0 12px; border: 0; background: var(--surface); font: inherit; font-size: 13px; color: var(--text); cursor: pointer; }
|
|
.drv-seg button + button { border-left: 1px solid var(--control); }
|
|
.drv-seg button[aria-pressed="true"] { background: var(--text); color: var(--surface); font-weight: 600; }
|
|
.drv-lintline { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 0 20px 12px; font-size: 13px; }
|
|
|
|
.dgrid { display: grid; grid-template-columns: 34px minmax(0, 1.1fr) minmax(0, 1.2fr) 62px 74px 210px; align-items: center;
|
|
gap: 0 14px; padding: 11px 130px 11px 20px; font-size: 14px; min-height: 24px; }
|
|
.dhead { font-size: 12px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); padding-top: 6px; padding-bottom: 6px; }
|
|
.drow .num { color: var(--muted); font-size: 13px; }
|
|
.drow .size { color: var(--muted); font-size: 13px; text-align: right; white-space: nowrap; }
|
|
.drow .tok { font-family: var(--font-mono); font-size: 13px; }
|
|
.drow .flags { display: flex; align-items: center; gap: 6px; min-width: 0; flex-wrap: nowrap; }
|
|
.drow.st-edited, .drow.st-new { background: var(--edit-soft); }
|
|
.drow.st-moved .num { color: var(--accent); font-weight: 600; }
|
|
.drow.st-removed { background: var(--remove-soft); }
|
|
.drow.st-removed .strike { text-decoration: line-through; color: var(--muted); }
|
|
.dbody { padding: 2px 24px 16px 68px; display: grid; grid-template-columns: minmax(300px, .9fr) minmax(0, 1.6fr); gap: 12px 32px;
|
|
font-size: 13px; align-items: start; }
|
|
.dbody dl { margin: 0; display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 14px; align-content: start; align-items: baseline; }
|
|
.dbody dt { color: var(--muted); font-weight: 400; } .dbody dd { margin: 0; }
|
|
.dbody h4 { margin: 0 0 6px; font-size: 12px; font-weight: 600; color: var(--muted); letter-spacing: .03em; text-transform: uppercase; }
|
|
.dbody .find { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; line-height: 1.5; }
|
|
.dbody .find.cleared li { color: var(--muted); }
|
|
.dbody .find .chip { font-size: 11px; padding: 1px 6px; }
|
|
.tokchip { display: inline-block; font-family: var(--font-mono); font-size: 12px; border: 1px solid var(--line-2); border-radius: 4px; padding: 1px 6px; margin: 0 4px 4px 0; }
|
|
.tokchip.bad { border-color: var(--fail); color: var(--fail-text); }
|
|
.drv-empty { padding: 28px 24px; text-align: center; color: var(--muted); border-top: 1px solid var(--line); }
|
|
.narrow-acts { display: none; }
|
|
@container (max-width: 940px) {
|
|
.dgrid { grid-template-columns: 30px minmax(0, 1fr) 62px 200px; }
|
|
.dgrid .packcell, .dgrid .size { display: none; }
|
|
.dbody { grid-template-columns: 1fr; padding-left: 24px; }
|
|
}
|
|
@container (max-width: 640px) {
|
|
.dgrid { grid-template-columns: 26px minmax(0, 1fr) auto; padding: 11px 16px; gap: 4px 10px; }
|
|
.dgrid .oscell { grid-column: 2; grid-row: 2; }
|
|
.dgrid .flags { grid-column: 3; grid-row: 1; }
|
|
.dhead { display: none; }
|
|
.drow > .act { display: none; }
|
|
.narrow-acts { display: flex; grid-column: 1 / -1; gap: 8px; justify-content: flex-end; }
|
|
.dbody { padding: 2px 16px 14px; }
|
|
}
|
|
|
|
.savebar { position: sticky; bottom: 12px; z-index: 50; display: none; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px;
|
|
padding: 12px 12px 12px 20px; margin-top: 18px; border-radius: 12px; background: var(--savebar-bg); border: 1px solid var(--savebar-edge);
|
|
color: var(--savebar-text); box-shadow: 0 8px 24px rgba(0,0,0,.25); }
|
|
.dirty .savebar { display: flex; }
|
|
.savebar .ui-btn { background: transparent; color: var(--savebar-text); border-color: #8E9AAB; }
|
|
.savebar .ui-btn.pri { background: var(--savebar-pri); border-color: var(--savebar-pri); color: var(--on-savebar-pri); }
|
|
|
|
.orphan-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
.orphan-table th { text-align: left; font-weight: 500; font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; padding: 6px 12px; }
|
|
.orphan-table td { padding: 10px 12px; border-top: 1px solid var(--line); vertical-align: top; }
|
|
.orphan-table .sub { overflow-wrap: anywhere; }
|
|
|
|
#drvEditModal .tok-preview { min-height: 1.6rem; margin-top: 0.4rem; }
|
|
#drvEditModal .zip-check { font-size: 12.5px; margin-top: 0.35rem; }
|
|
#drvEditModal textarea { min-height: 5.5rem; font-family: var(--font-mono); font-size: 13px; }
|
|
.mv-order { list-style: none; margin: 0; padding: 0; border: 1px solid var(--line-2); border-radius: 10px; overflow: hidden; }
|
|
.mv-order li { display: grid; grid-template-columns: 44px minmax(0, 1fr) 62px; gap: 12px; align-items: center; padding: 8px 14px; border-top: 1px solid var(--line); font-size: 13px; }
|
|
.mv-order li:first-child { border-top: 0; }
|
|
.mv-order li.moved { background: var(--edit-soft); box-shadow: inset 4px 0 0 var(--accent); }
|
|
.mv-lbl { font-size: 12px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); margin: 14px 0 8px; }
|
|
.rv-summary { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 0.75rem; }
|
|
.rv-moves { list-style: none; padding: 0; margin: 0 0 1rem; font-size: 13px; }
|
|
.rv-moves li { padding: 0.2rem 0; }
|
|
.rv-lint { font-size: 13px; margin-bottom: 1rem; }
|
|
.rv-lint ul { margin: 0.35rem 0 0; padding-left: 1.1rem; }
|
|
details.rv-full summary { cursor: pointer; font-size: 13px; font-weight: 600; margin-bottom: 0.5rem; }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
{% set header_actions %}
|
|
{% if last_save %}
|
|
<form method="POST" action="{{ url_for('images_drivers.undo', image_type=image_type) }}" class="d-inline"
|
|
onsubmit="return confirm('Put HardwareDriver.json back as it was before the save of {{ last_save.ts | local_dt }}?');">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="ui-btn" title="Last save: {{ last_save.ts | local_dt }}">Undo last save</button>
|
|
</form>
|
|
{% else %}
|
|
<span class="sub">Undo last save: nothing saved from this page yet</span>
|
|
{% endif %}
|
|
{% endset %}
|
|
{% set header_subtitle = 'Deploy/Control/HardwareDriver.json' %}
|
|
{% include "_image_header.html" %}
|
|
|
|
<div class="ui-stack" id="drvPage">
|
|
<details class="panel drv-rule" style="border-radius: 10px">
|
|
<summary><i class="bi bi-info-circle" aria-hidden="true" style="color: var(--accent)"></i>
|
|
<span class="trunc" style="flex: 1 1 auto"><strong>First match wins, and there is no OS filter.</strong> <span class="muted">Win11 entries must sit above Win10 entries for the same model.</span></span>
|
|
<span class="chev" aria-hidden="true">▾</span></summary>
|
|
<p>At imaging PESetup reads this map and takes the first entry, from the top, that has a model token contained in
|
|
the PC's WMI model (case-insensitive). Latitude, OptiPlex and Precision PCs only look at entries whose family
|
|
contains that word. A PC that matches nothing still images, with no drivers. Edits, moves and removals are staged
|
|
until Review and save. The <a href="{{ image_tab_url(image_type, 'os') }}">Hardware Models</a> list
|
|
(user_selections.json) is the Media Creator Lite build selection; PESetup does not read it.</p>
|
|
</details>
|
|
|
|
{% if load_error %}
|
|
<div class="notice bad"><i class="bi bi-x-octagon" aria-hidden="true"></i><span>{{ load_error }}. The list is empty and saving is off until the file is fixed.</span></div>
|
|
{% endif %}
|
|
{% if hw_drivers_extra %}
|
|
<div class="notice info"><i class="bi bi-info-circle" aria-hidden="true"></i><span>This image also has <span class="mono">Deploy/Control/hw_drivers.json</span>. PESetup never reads it; only the map below counts.</span></div>
|
|
{% endif %}
|
|
|
|
<section class="panel ui-list" id="drvCard" aria-labelledby="drv-h">
|
|
<h2 id="drv-h" class="vh">Driver map</h2>
|
|
<div class="drv-tools">
|
|
<div role="search" style="flex: 1 1 200px; min-width: 150px; display: flex">
|
|
<label for="drvSearch" class="vh">Filter entries by model, family, file or folder</label>
|
|
<input type="search" class="field" id="drvSearch" style="flex: 1; min-width: 0" placeholder="Filter by model, family, file or folder">
|
|
</div>
|
|
<div class="drv-seg" role="group" aria-label="Windows version" id="drvOsSeg">
|
|
<button type="button" data-os="" aria-pressed="true">All</button>
|
|
<button type="button" data-os="Win11" aria-pressed="false">Win11 <span data-n="Win11"></span></button>
|
|
<button type="button" data-os="Win10" aria-pressed="false">Win10 <span data-n="Win10"></span></button>
|
|
</div>
|
|
<div role="group" aria-label="Show only" style="display: flex; gap: 6px; flex-wrap: wrap">
|
|
<button type="button" class="fbtn" id="drvFind" aria-pressed="false">Findings or unsaved <span id="drvFindN"></span></button>
|
|
<button type="button" class="fbtn" id="drvMissing" aria-pressed="false">Zip problems <span id="drvMissingN"></span></button>
|
|
</div>
|
|
<details class="more lft">
|
|
<summary class="ui-btn pri">Add</summary>
|
|
<div class="menu">
|
|
<button type="button" id="drvAdd" {% if load_error %}disabled{% endif %}>New entry</button>
|
|
<button type="button" data-bs-toggle="modal" data-bs-target="#driverUploadModal">Upload zip</button>
|
|
<a href="#orphanCard">Unregistered zips ({{ orphans | selectattr('used_by', 'equalto', []) | list | length }})</a>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
<div class="drv-lintline">
|
|
<span id="drvLint"></span>
|
|
<span class="muted" id="drvShowing"></span>
|
|
<details class="more" style="margin-left: auto">
|
|
<summary class="disc" style="min-height: 30px">Status key <span class="chev" aria-hidden="true">▾</span></summary>
|
|
<div class="menu" style="width: min(380px, calc(100vw - 48px)); padding: 12px; gap: 8px; font-size: 13px; white-space: normal">
|
|
<div>{{ ui.chip('CRITICAL', 'fail') }} Map unusable. A save that adds one is refused.</div>
|
|
<div>{{ ui.chip('ERROR', 'err') }} Imaging goes wrong: zip missing, zip name case WinPE cannot find, Win10 entry wins</div>
|
|
<div>{{ ui.chip('WARN', 'warn') }} Shadowed or duplicate tokens, zip name case differs but WinPE still finds it</div>
|
|
<div>{{ ui.chip('Clears on save', 'ok') }} Your unsaved changes resolve this entry's findings</div>
|
|
<div>{{ ui.chip('Edited', 'edit') }} {{ ui.chip('New', 'edit') }} {{ ui.chip('Removing', 'remove') }} Unsaved change</div>
|
|
<div class="sub">A duplicate-token warning shows on both entries it names.</div>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
<div class="dgrid dhead" aria-hidden="true"><span>#</span><span>Model tokens</span><span class="packcell">Driver pack</span><span class="oscell">OS</span><span class="size">Size</span><span>Status</span></div>
|
|
<div id="drvList"></div>
|
|
<div class="hidden-bar" id="drvFilterBar" hidden><span id="drvFilterText"></span><button type="button" class="ui-btn" id="drvClearFilter">Show all</button></div>
|
|
</section>
|
|
|
|
{# ==================== Unregistered zips ==================== #}
|
|
{# Zips no image uses come first and can be deleted. Zips another image's
|
|
map uses (the driver tree is shared) are folded away: they can be added
|
|
to this map but never deleted from here. #}
|
|
{% set free = orphans | selectattr('used_by', 'equalto', []) | list %}
|
|
{% set shared = orphans | rejectattr('used_by', 'equalto', []) | list %}
|
|
{% macro orphan_row(o, hidden=False) %}
|
|
<tr class="{{ 'orphan-shared' if o.used_by }}" {% if hidden %}style="display:none"{% endif %}>
|
|
<td>
|
|
{% if o.used_by %}
|
|
<input type="checkbox" disabled title="Used by {{ o.used_by | join(', ') }}: cannot be deleted from here" aria-label="In use">
|
|
{% else %}
|
|
<input type="checkbox" class="orphan-select" name="rel" value="{{ o.rel }}" aria-label="Select {{ o.name }}">
|
|
{% endif %}
|
|
</td>
|
|
<td><div class="mono" style="overflow-wrap:anywhere;">{{ o.name }}</div><div class="sub mono">{{ o.folder or '(top folder)' }}</div></td>
|
|
<td style="white-space: nowrap">{{ (o.size or 0) | filesizeformat }}<div class="sub">{{ o.mtime | local_dt if o.mtime else '' }}</div></td>
|
|
<td>{% if o.used_by %}{% for u in o.used_by %}{{ ui.chip(u, 'neutral') }} {% endfor %}{% else %}<span class="sub">No image</span>{% endif %}</td>
|
|
<td style="text-align: right; white-space: nowrap">
|
|
<button type="button" class="ui-btn orphan-add" {% if load_error %}disabled{% endif %}
|
|
data-name="{{ o.name }}" data-dest="{{ o.dest }}" data-size="{{ o.size or 0 }}">Add to map</button>
|
|
</td>
|
|
</tr>
|
|
{% endmacro %}
|
|
<details class="panel" id="orphanCard" style="border-radius: 14px" {% if request.args.get('orphans') %}open{% endif %}>
|
|
<summary class="disc" style="padding: 0 20px; min-height: 48px; color: var(--text); font-size: 15px">
|
|
<span class="chev" aria-hidden="true">▾</span> Unregistered zips
|
|
{{ ui.chip(free | length ~ ' unused', 'warn' if free else 'neutral') }}
|
|
<span class="sub" style="font-weight: 400">Zips under Out-of-box Drivers that this map does not use</span>
|
|
</summary>
|
|
{% if orphans %}
|
|
<form method="POST" action="{{ url_for('images_drivers.orphans_remove', image_type=image_type) }}"
|
|
onsubmit="return confirm('Delete the selected zip files from the share? This cannot be undone.');">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<div style="overflow-x: auto">
|
|
<table class="orphan-table">
|
|
<thead><tr>
|
|
<th style="width:30px"><input type="checkbox" id="orphanSelectAll" aria-label="Select all" {% if not free %}disabled{% endif %}></th>
|
|
<th>File</th><th>Size and date</th><th>Used by</th><th></th>
|
|
</tr></thead>
|
|
<tbody>
|
|
{% for o in free %}{{ orphan_row(o) }}{% endfor %}
|
|
{% if not free %}<tr><td colspan="5" class="sub">No zip here is unused: every one is in this map or in another image's map.</td></tr>{% endif %}
|
|
{% if shared %}
|
|
<tr class="orphan-toggle-row"><td colspan="5">
|
|
<button type="button" class="ui-btn ghost sm" id="orphanSharedToggle" aria-expanded="false">
|
|
Show {{ shared | length }} zip{{ 's' if shared | length != 1 }} that other images (or an undo or rollback) use</button>
|
|
</td></tr>
|
|
{% for o in shared %}{{ orphan_row(o, hidden=True) }}{% endfor %}
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div style="display: flex; flex-wrap: wrap; gap: 8px; justify-content: space-between; align-items: center; padding: 10px 20px 14px">
|
|
<span class="sub">Only zips no image uses can be deleted. Add to map stages a new entry; it is written with the next save.</span>
|
|
<button type="submit" class="ui-btn danger" {% if not free %}disabled title="No zip here is unused"{% endif %}>Delete selected...</button>
|
|
</div>
|
|
</form>
|
|
{% else %}
|
|
<div class="sub" style="padding: 0 20px 16px">None: every zip under Out-of-box Drivers is in this map.</div>
|
|
{% endif %}
|
|
</details>
|
|
|
|
<div class="savebar dark" role="region" aria-label="Unsaved changes">
|
|
<p role="status" style="margin: 0; font-size: 14px" id="drvSaveText"></p>
|
|
<div style="display: flex; gap: 8px">
|
|
<button type="button" class="ui-btn" id="drvDiscard">Discard</button>
|
|
<button type="button" class="ui-btn pri" id="drvReview">Review and save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# ==================== Edit entry modal ==================== #}
|
|
<div class="modal fade" id="drvEditModal" tabindex="-1" aria-labelledby="drvEditTitle">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title" id="drvEditTitle">Edit entry</h3>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="form-field">
|
|
<label class="form-label" for="drvEdTokens">Model tokens (modelswminame), one per line</label>
|
|
<textarea class="form-control" id="drvEdTokens" rows="3" spellcheck="false"></textarea>
|
|
<div class="tok-preview" id="drvEdTokPreview"></div>
|
|
<div class="form-text">The entry matches when any token appears, as written, inside the PC's WMI model (case is ignored):
|
|
<span class="mono">OptiPlex 7020</span> matches the model <span class="mono">OptiPlex 7020</span> but not
|
|
<span class="mono">OptiPlex Micro 7020</span>. Saved comma-separated with no spaces, because PESetup does not trim.</div>
|
|
</div>
|
|
<div class="row g-3">
|
|
<div class="col-md-6 form-field mb-0">
|
|
<label class="form-label" for="drvEdFamily">Family</label>
|
|
<input type="text" class="form-control" id="drvEdFamily" list="drvFamilies">
|
|
<div class="form-text">For Latitude, OptiPlex and Precision PCs this must contain that word, or the entry is skipped.</div>
|
|
</div>
|
|
<div class="col-md-6 form-field mb-0">
|
|
<label class="form-label" for="drvEdFriendly">Friendly name (modelsfriendlyname)</label>
|
|
<input type="text" class="form-control" id="drvEdFriendly">
|
|
<div class="form-text">Shown in logs, lint messages and on this page.</div>
|
|
</div>
|
|
<div class="col-md-6 form-field mb-0">
|
|
<label class="form-label" for="drvEdOsId">osId</label>
|
|
<input type="text" class="form-control mono" id="drvEdOsId" placeholder="20,21">
|
|
<div class="form-text">18 = Windows 10; 20 = Win11 LTSC 24H2; 21 = Win11 Pro 24H2. Informational: not part of the match.</div>
|
|
</div>
|
|
<div class="col-md-6 form-field mb-0">
|
|
<label class="form-label" for="drvEdPos">Position</label>
|
|
<input type="number" class="form-control" id="drvEdPos" min="1">
|
|
<div class="form-text" id="drvEdPosHelp">1 is the top. The first match wins.</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-field mt-3">
|
|
<label class="form-label" for="drvEdFile">Zip file name (fileName)</label>
|
|
<input type="text" class="form-control mono" id="drvEdFile" spellcheck="false">
|
|
</div>
|
|
<div class="form-field">
|
|
<label class="form-label" for="drvEdDir">Folder (destinationDir)</label>
|
|
<input type="text" class="form-control mono" id="drvEdDir" list="drvFolders" spellcheck="false">
|
|
<div class="zip-check" id="drvEdZip"></div>
|
|
</div>
|
|
<p class="small text-muted mb-0" id="drvEdNote">
|
|
url, hash and size describe the pack download (Updates, Dell drivers sets them) and are not changed here.
|
|
FileName, DestinationDir, models and aOsIds are kept in step with the fields above.
|
|
</p>
|
|
<div class="alert alert-danger d-none mt-3 mb-0" id="drvEdError"></div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="ui-btn" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="ui-btn pri" id="drvEdApply">Apply</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<datalist id="drvFamilies"></datalist>
|
|
<datalist id="drvFolders"></datalist>
|
|
|
|
{# ==================== Move dialog ==================== #}
|
|
<div class="modal fade" id="drvMoveModal" tabindex="-1" aria-labelledby="drvMoveTitle" aria-describedby="drvMoveImpact">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header" style="flex-direction: column; align-items: flex-start; gap: 2px">
|
|
<h3 class="modal-title" id="drvMoveTitle">Move entry</h3>
|
|
<div class="sub" id="drvMoveSub"></div>
|
|
</div>
|
|
<div class="modal-body">
|
|
<label class="radio" style="display: flex; align-items: center; gap: 10px">Move to position
|
|
<input class="field mono" type="number" id="drvMovePos" min="1" style="width: 90px"></label>
|
|
<div class="mv-lbl">Order after the move</div>
|
|
<ol class="mv-order" id="drvMoveOrder"></ol>
|
|
<div class="sub" id="drvMoveShift" style="margin-top: 6px"></div>
|
|
<div id="drvMoveImpact">
|
|
<div class="mv-lbl">Lint after the move, against your staged map</div>
|
|
<div id="drvMoveLint" class="sub">Checking...</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="ui-btn pri" data-bs-dismiss="modal" id="drvMoveCancel">Cancel</button>
|
|
<button type="button" class="ui-btn" id="drvMoveApply">Stage move</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Driver upload modal -->
|
|
<div class="modal fade" id="driverUploadModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<form method="POST" action="{{ url_for('images_drivers_upload', image_type=image_type) }}" enctype="multipart/form-data">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">Upload driver zip</h3>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="form-field">
|
|
<label class="form-label" for="driverFile">Driver .zip file</label>
|
|
<input type="file" class="form-control" name="driver_file" id="driverFile" accept=".zip" required>
|
|
<div class="form-text">Lands in the top of <span class="mono">Out-of-box Drivers/</span> and shows under
|
|
Unregistered zips. Use Add to map there to give it an entry (reviewed with the next save).</div>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="overwrite" value="1" id="driverOverwriteCheck">
|
|
<label class="form-check-label" for="driverOverwriteCheck">Overwrite if same filename exists</label>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="ui-btn" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="ui-btn pri">Upload</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="application/json" id="drvData">{{ page_json }}</script>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
// Drivers tab. state.items holds the staged map in order, removed entries
|
|
// included (flag removed; they are left out of every request). Each item is
|
|
// {orig: position in the file or null, moved, removed, status, open,
|
|
// data: the entry, row: server facts for the staged map, saved: row as saved}.
|
|
(function () {
|
|
'use strict';
|
|
var DATA = JSON.parse(document.getElementById('drvData').textContent);
|
|
var URLS = DATA.urls;
|
|
// Key order of a new entry: the shape most entries in the GE maps have.
|
|
var NEW_KEYS = ['manufacturer', 'product', 'family', 'modelswminame', 'modelsfriendlyname', 'fileName',
|
|
'destinationDir', 'url', 'hash', 'size', 'modifiedDate', 'osId', 'imagedisk', 'models', 'FileName',
|
|
'DestinationDir', 'aOsIds'];
|
|
|
|
var state = {
|
|
items: DATA.entries.map(function (e, i) {
|
|
return { orig: i, moved: false, removed: false, status: null, open: false, data: e, row: DATA.rows[i], saved: DATA.rows[i] };
|
|
}),
|
|
counts: DATA.counts,
|
|
dirty: false
|
|
};
|
|
var filt = { q: '', os: '', missing: false, find: false };
|
|
|
|
var list = document.getElementById('drvList');
|
|
var page = document.getElementById('drvPage');
|
|
|
|
// ---- small helpers ----
|
|
function h(tag, attrs, kids) {
|
|
var e = document.createElement(tag);
|
|
Object.keys(attrs || {}).forEach(function (k) {
|
|
if (k === 'text') e.textContent = attrs[k];
|
|
else if (k === 'cls') e.className = attrs[k];
|
|
else e.setAttribute(k, attrs[k]);
|
|
});
|
|
(kids || []).forEach(function (c) {
|
|
if (c === null || c === undefined || c === false) return;
|
|
e.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
|
|
});
|
|
return e;
|
|
}
|
|
function chip(text, kind) { return h('span', { cls: 'chip c-' + kind, text: text }); }
|
|
function fileOf(d) { return String(d.fileName || d.FileName || ''); }
|
|
function dirOf(d) { return String(d.destinationDir || d.DestinationDir || ''); }
|
|
function tokensOf(d) { return d.modelswminame === undefined || d.modelswminame === null ? [] : String(d.modelswminame).split(','); }
|
|
function shortDir(dir) { return dir.replace(/^\*destinationdir\*\\?/i, '').replace(/^Deploy\\Out-of-box Drivers\\?/i, ''); }
|
|
function fmtSize(n) {
|
|
if (n === null || n === undefined) return '';
|
|
if (n >= 1e9) return (n / 1e9).toFixed(2) + ' GB';
|
|
if (n >= 1e6) return (n / 1e6).toFixed(1) + ' MB';
|
|
return Math.round(n / 1e3) + ' KB';
|
|
}
|
|
function fmtDate(ts) {
|
|
if (!ts) return '';
|
|
var d = new Date(ts * 1000);
|
|
function p(n) { return (n < 10 ? '0' : '') + n; }
|
|
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes());
|
|
}
|
|
// Same rule as services/image_pages.driver_os.
|
|
function osOf(d) {
|
|
var name = fileOf(d).toLowerCase(), dir = dirOf(d).toLowerCase();
|
|
if (name.indexOf('win10') === 0) return 'Win10';
|
|
if (name.indexOf('win11') === 0) return 'Win11';
|
|
if (dir.indexOf('\\dell_10\\') >= 0 || dir.indexOf('/dell_10/') >= 0) return 'Win10';
|
|
if (dir.indexOf('\\dell_11\\') >= 0 || dir.indexOf('/dell_11/') >= 0) return 'Win11';
|
|
var ids = String(d.osId || '').split(',').map(function (s) { return s.trim(); }).filter(Boolean);
|
|
if (!ids.length) return 'Other';
|
|
return ids.every(function (i) { return i === '18'; }) ? 'Win10' : 'Win11';
|
|
}
|
|
function zipBad(row) { return row && ['missing', 'trap', 'none', 'case'].indexOf(row.zip.status) >= 0; }
|
|
function live() { return state.items.filter(function (it) { return !it.removed; }); }
|
|
function livePos(it) { return live().indexOf(it) + 1; }
|
|
function label(it) { return it.data.modelsfriendlyname || it.data.modelswminame || fileOf(it.data); }
|
|
function worst(findings) {
|
|
var rank = { CRITICAL: 3, ERROR: 2, WARN: 1, INFO: 0 }, w = null;
|
|
(findings || []).forEach(function (f) { if (!w || rank[f.level] > rank[w]) w = f.level; });
|
|
return w;
|
|
}
|
|
function plural(n, s) { return n + ' ' + s + (n === 1 ? '' : 's'); }
|
|
function unsaved(it) { return it.removed || it.orig === null || it.status === 'edited' || (it.moved && livePos(it) !== it.orig + 1); }
|
|
|
|
// ---- filters ----
|
|
function filtering() { return !!(filt.q || filt.os || filt.missing || filt.find); }
|
|
function visible(it) {
|
|
var d = it.data, r = it.row;
|
|
if (filt.os && osOf(d) !== filt.os) return false;
|
|
if (filt.missing && !zipBad(r)) return false;
|
|
if (filt.find && !((r && r.findings.length) || (it.saved && it.saved.findings.length) || unsaved(it))) return false;
|
|
if (filt.q) {
|
|
var hay = [d.modelswminame, d.modelsfriendlyname, d.family, fileOf(d), dirOf(d), d.osId, d.product].join(' ').toLowerCase();
|
|
var words = filt.q.toLowerCase().split(/\s+/).filter(Boolean);
|
|
for (var i = 0; i < words.length; i++) if (hay.indexOf(words[i]) < 0) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ---- row ----
|
|
function statusChips(it) {
|
|
var r = it.row, out = [];
|
|
if (it.removed) return [chip('Removing', 'remove')];
|
|
if (it.orig === null) out.push(chip('New', 'edit'));
|
|
else if (it.status === 'edited') out.push(chip('Edited', 'edit'));
|
|
else if (it.moved && livePos(it) !== it.orig + 1) out.push(chip('Moved from #' + (it.orig + 1), 'neutral'));
|
|
var now = r ? r.findings.length : 0;
|
|
var before = it.saved ? it.saved.findings.length : 0;
|
|
var w = r ? worst(r.findings) : null;
|
|
if (w === 'CRITICAL' || w === 'ERROR') out.push(chip(plural(now, w === 'CRITICAL' ? 'critical' : 'error').replace('criticals', 'critical'), w === 'CRITICAL' ? 'fail' : 'err'));
|
|
else if (w === 'WARN') out.push(chip(plural(now, 'warning'), 'warn'));
|
|
if (state.dirty && before > now) out.push(chip(now ? 'Clears ' + (before - now) : 'Clears on save', 'ok'));
|
|
if (r && r.dell && !out.length) out.push(chip('Dell ' + r.dell, 'neutral'));
|
|
if (!r && !it.removed) out.push(h('span', { cls: 'sub', text: 'checking...' }));
|
|
return out;
|
|
}
|
|
|
|
function findList(findings, cleared) {
|
|
var ul = h('ul', { cls: 'find' + (cleared ? ' cleared' : '') });
|
|
findings.forEach(function (x) {
|
|
var k = x.level === 'WARN' ? 'warn' : x.level === 'INFO' ? 'neutral' : (x.level === 'CRITICAL' ? 'fail' : 'err');
|
|
ul.appendChild(h('li', {}, [chip(x.level, k), ' ', h('span', { cls: 'mono', style: 'font-size:12px', text: x.check }), ' ', x.message]));
|
|
});
|
|
return ul;
|
|
}
|
|
|
|
function rowMenu(it, f) {
|
|
var pos = livePos(it), n = live().length;
|
|
var menu = h('div', { cls: 'menu' }, [
|
|
h('button', { type: 'button', 'data-act': 'up', title: f ? 'Clear the search and filters to reorder' : '' }, ['Move up']),
|
|
h('button', { type: 'button', 'data-act': 'down', title: f ? 'Clear the search and filters to reorder' : '' }, ['Move down']),
|
|
h('button', { type: 'button', 'data-act': 'moveto' }, ['Move to position...']),
|
|
fileOf(it.data) ? h('button', { type: 'button', cls: 'copy-btn', 'data-copy-text': fileOf(it.data) }, ['Copy file name']) : null,
|
|
h('div', { cls: 'sep' }),
|
|
h('button', { type: 'button', cls: 'del', 'data-act': 'remove' }, ['Remove'])
|
|
]);
|
|
if (f || pos <= 1) menu.children[0].disabled = true;
|
|
if (f || pos >= n) menu.children[1].disabled = true;
|
|
if (f) menu.insertBefore(h('div', { cls: 'sub', style: 'padding: 4px 12px; white-space: normal', text: 'Up and down need the full list: clear the filter.' }), menu.children[0]);
|
|
return h('details', { cls: 'more' }, [h('summary', { cls: 'ui-btn ghost', 'aria-label': 'More actions for entry ' + pos }, ['\u22EF']), menu]);
|
|
}
|
|
|
|
function rowEl(it, idx, f) {
|
|
var d = it.data, r = it.row, pos = it.removed ? null : livePos(it);
|
|
var cls = 'ui-row drow' + (it.removed ? ' st-removed' : it.orig === null ? ' st-new' : it.status === 'edited' ? ' st-edited' : (unsaved(it) ? ' st-moved' : ''));
|
|
var toks = tokensOf(d);
|
|
var tokText = toks.join(', ');
|
|
var name = fileOf(d);
|
|
var os = osOf(d);
|
|
var strike = it.removed ? ' strike' : '';
|
|
var summary = h('summary', { cls: 'dgrid' }, [
|
|
h('span', { cls: 'mono num', text: pos ? String(pos) : '-' }),
|
|
h('span', { cls: 'tip', 'data-tip': tokText + (d.family ? ' | ' + d.family : '') }, [h('span', { cls: 'trunc tok' + strike, text: tokText || '(no modelswminame)' })]),
|
|
h('span', { cls: 'tip packcell', 'data-tip': name }, [h('span', { cls: 'trunc mono' + strike, style: 'font-size:13px', text: name || '(no fileName)' })]),
|
|
h('span', { cls: 'oscell' }, [chip(os, os === 'Win11' ? 'w11' : (os === 'Win10' ? 'w10' : 'warn'))]),
|
|
h('span', { cls: 'mono size', text: r && r.zip.size ? fmtSize(r.zip.size) : '' }),
|
|
h('span', { cls: 'flags' }, statusChips(it).concat([h('span', { cls: 'chev', 'aria-hidden': 'true', text: '\u25BE' })]))
|
|
]);
|
|
|
|
// body: only what the row does not already show
|
|
var dl = h('dl');
|
|
function dt(k, v) { dl.appendChild(h('dt', { text: k })); dl.appendChild(h('dd', {}, [v])); }
|
|
if (toks.length > 1 || toks.some(function (t) { return t === '' || t !== t.trim(); })) {
|
|
dt('Model tokens', h('span', {}, toks.map(function (t) {
|
|
var bad = t === '' || t !== t.trim();
|
|
return h('span', { cls: 'tokchip' + (bad ? ' bad' : ''), title: bad ? 'Empty or untrimmed token' : '', text: t === '' ? '(empty)' : t });
|
|
})));
|
|
}
|
|
dt('Friendly name', d.modelsfriendlyname || '-');
|
|
dt('Family', d.family === undefined || d.family === null ? '(none)' : String(d.family));
|
|
dt('OS', os + (d.osId !== undefined ? ' (osId ' + d.osId + ')' : ''));
|
|
var fold = h('span', { cls: 'mono' });
|
|
shortDir(dirOf(d)).split('\\').forEach(function (part, i) { if (i) { fold.appendChild(document.createTextNode('\\')); fold.appendChild(h('wbr')); } fold.appendChild(document.createTextNode(part)); });
|
|
dt('Folder', dirOf(d) ? fold : '(no destinationDir)');
|
|
if (r && r.zip.status !== 'ok') {
|
|
var zs = r.zip.status === 'case' ? 'On the share as ' + r.zip.actual + ' (other letter case; WinPE finds it)'
|
|
: r.zip.status === 'trap' ? 'Only at ' + r.zip.actual + ': WinPE will not find it'
|
|
: r.zip.status === 'none' ? 'No file name' : 'Not on the share: ' + r.zip.rel;
|
|
dt('Zip', h('span', { style: r.zip.status === 'case' ? '' : 'color: var(--fail-text)', text: zs }));
|
|
} else if (r && r.zip.mtime) {
|
|
dt('Zip', 'On the share, ' + fmtDate(r.zip.mtime));
|
|
}
|
|
if (r && r.dell) dt('Dell', 'Newer pack ' + r.dell + ' in the last Updates scan');
|
|
|
|
var right = h('div');
|
|
if (it.status === 'edited' && it.orig !== null) {
|
|
var was = DATA.entries[it.orig];
|
|
if (String(was.modelswminame) !== String(d.modelswminame) && d.modelsfriendlyname === was.modelsfriendlyname && d.modelsfriendlyname) {
|
|
right.appendChild(h('div', { cls: 'notice', style: 'margin-bottom:10px' }, [h('span', { 'aria-hidden': 'true', style: 'font-weight:700', text: '!' }),
|
|
h('span', {}, ['Unsaved edit: tokens were ', h('span', { cls: 'mono', text: String(was.modelswminame) }),
|
|
'. The friendly name still says ', h('span', { cls: 'mono', text: d.modelsfriendlyname }), ', and logs and lint messages use it. ',
|
|
h('button', { type: 'button', cls: 'ui-btn sm', 'data-act': 'edit', text: 'Edit friendly name' })])]));
|
|
}
|
|
}
|
|
var cur = r ? r.findings : [];
|
|
var savedF = it.saved ? it.saved.findings : [];
|
|
if (cur.length) { right.appendChild(h('h4', { text: state.dirty ? 'Findings after your changes' : 'Findings' })); right.appendChild(findList(cur)); }
|
|
if (state.dirty && savedF.length > cur.length) {
|
|
right.appendChild(h('h4', { style: cur.length ? 'margin-top:10px' : '', text: 'In the saved map, cleared by your changes' }));
|
|
right.appendChild(findList(savedF, true));
|
|
}
|
|
if (!cur.length && !(state.dirty && savedF.length > cur.length)) right.appendChild(h('p', { cls: 'muted', style: 'margin:0', text: r ? 'No findings.' : 'Checking...' }));
|
|
|
|
var primary = it.removed ? h('button', { type: 'button', cls: 'ui-btn', 'data-act': 'unremove', text: 'Undo remove' })
|
|
: (it.status === 'edited' || it.orig === null) && it.orig !== null ? h('button', { type: 'button', cls: 'ui-btn', 'data-act': 'revert', text: 'Undo edit' })
|
|
: h('button', { type: 'button', cls: 'ui-btn', 'data-act': 'edit', text: 'Edit', 'aria-label': 'Edit entry ' + (pos || '') });
|
|
var acts = it.removed ? [primary] : [primary, rowMenu(it, f)];
|
|
var body = h('div', { cls: 'dbody' }, [dl, right, h('div', { cls: 'narrow-acts' }, acts.map(function (a) { return a.cloneNode(true); }))]);
|
|
var det = h('details', {}, [summary, body]);
|
|
if (it.open) det.open = true;
|
|
det.addEventListener('toggle', function () { it.open = det.open; });
|
|
var row = h('div', { cls: cls, 'data-idx': String(idx) }, [det, h('div', { cls: 'act' }, acts)]);
|
|
return row;
|
|
}
|
|
|
|
// ---- list ----
|
|
function render() {
|
|
list.innerHTML = '';
|
|
var all = state.items, f = filtering(), shown = 0, n = live().length;
|
|
all.forEach(function (it, idx) {
|
|
if (!visible(it)) return;
|
|
shown++;
|
|
list.appendChild(rowEl(it, idx, f));
|
|
});
|
|
if (!shown) list.appendChild(h('div', { cls: 'drv-empty', text: n ? 'No entry matches the search and filters.' : 'The map is empty: no PC gets drivers.' }));
|
|
var bar = document.getElementById('drvFilterBar');
|
|
bar.hidden = !f;
|
|
document.getElementById('drvFilterText').textContent = (all.length - shown) + ' entries hidden by the filter. Reordering works on the full list.';
|
|
document.getElementById('drvShowing').textContent = f ? 'Showing ' + shown + ' of ' + n : n + ' entries';
|
|
var miss = live().filter(function (it) { return zipBad(it.row); }).length;
|
|
var withF = all.filter(function (it) { return (it.row && it.row.findings.length) || (it.saved && it.saved.findings.length) || unsaved(it); }).length;
|
|
document.getElementById('drvMissingN').textContent = miss;
|
|
document.getElementById('drvFindN').textContent = withF;
|
|
['Win11', 'Win10'].forEach(function (o) {
|
|
document.querySelector('#drvOsSeg [data-n="' + o + '"]').textContent = live().filter(function (it) { return osOf(it.data) === o; }).length;
|
|
});
|
|
renderLint();
|
|
renderSavebar();
|
|
}
|
|
|
|
function countsText(c) {
|
|
var parts = [];
|
|
if (c.CRITICAL) parts.push(c.CRITICAL + ' critical');
|
|
if (c.ERROR) parts.push(plural(c.ERROR, 'error'));
|
|
if (c.WARN) parts.push(plural(c.WARN, 'warning'));
|
|
return parts.length ? parts.join(', ') : 'no findings';
|
|
}
|
|
function renderLint() {
|
|
var box = document.getElementById('drvLint');
|
|
box.innerHTML = '';
|
|
var c = DATA.counts || {}, s = state.counts || {};
|
|
var kind = function (x) { return (x.CRITICAL || x.ERROR) ? 'err' : (x.WARN ? 'warn' : 'ok'); };
|
|
box.appendChild(chip(countsText(c) + (state.dirty ? ' now' : ''), kind(c)));
|
|
if (state.dirty) { box.appendChild(document.createTextNode(' ')); box.appendChild(chip(countsText(s) + ' after save', kind(s))); }
|
|
}
|
|
function nChanges() { return state.items.filter(unsaved).length; }
|
|
function renderSavebar() {
|
|
page.classList.toggle('dirty', state.dirty && nChanges() > 0);
|
|
var t = document.getElementById('drvSaveText');
|
|
t.innerHTML = '';
|
|
t.appendChild(h('strong', { style: 'font-weight:600', text: plural(nChanges(), 'unsaved change') + '. ' }));
|
|
t.appendChild(h('span', { style: 'color: var(--savebar-sub)', text: 'Lint: ' + countsText(DATA.counts || {}) + ' now, ' + countsText(state.counts || {}) + ' after save.' }));
|
|
}
|
|
|
|
// ---- staging ----
|
|
var checkTimer = null, checkSeq = 0;
|
|
function payloadOf(items) {
|
|
return items.filter(function (it) { return !it.removed; }).map(function (it) {
|
|
var o = {};
|
|
Object.keys(it.data).forEach(function (k) { o[k] = it.data[k]; });
|
|
if (it.orig !== null) o._orig = it.orig;
|
|
if (it.moved) o._moved = true;
|
|
return o;
|
|
});
|
|
}
|
|
function payload() { return payloadOf(state.items); }
|
|
function changed() {
|
|
// Rows from a check still in flight describe the old order: drop them.
|
|
checkSeq++;
|
|
state.dirty = true;
|
|
render();
|
|
clearTimeout(checkTimer);
|
|
checkTimer = setTimeout(recheck, 250);
|
|
}
|
|
function recheck() {
|
|
var seq = ++checkSeq;
|
|
PxeImage.postJSON(URLS.check, { entries: payload() }).then(function (r) {
|
|
if (seq !== checkSeq || !r.ok) return;
|
|
var lv = live();
|
|
r.data.rows.forEach(function (row, i) { if (lv[i]) lv[i].row = row; });
|
|
state.counts = r.data.counts;
|
|
render();
|
|
});
|
|
}
|
|
// Put item it at live position pos (1-based) among the other live items.
|
|
function placeAt(it, pos) {
|
|
var from = state.items.indexOf(it);
|
|
state.items.splice(from, 1);
|
|
var others = state.items.filter(function (x) { return !x.removed; });
|
|
var before = others[pos - 1];
|
|
var at = before ? state.items.indexOf(before) : state.items.length;
|
|
state.items.splice(at, 0, it);
|
|
it.moved = true;
|
|
}
|
|
|
|
// ---- row events ----
|
|
function handle(btn) {
|
|
var row = btn.closest('.drow');
|
|
if (!row) return;
|
|
var it = state.items[parseInt(row.getAttribute('data-idx'), 10)];
|
|
var act = btn.getAttribute('data-act');
|
|
var m = btn.closest('details.more'); if (m) m.open = false;
|
|
if (act === 'up') { placeAt(it, livePos(it) - 1); changed(); }
|
|
else if (act === 'down') { placeAt(it, livePos(it) + 1); changed(); }
|
|
else if (act === 'moveto') openMove(it);
|
|
else if (act === 'edit') openEditor(state.items.indexOf(it));
|
|
else if (act === 'remove') {
|
|
if (it.orig === null) state.items.splice(state.items.indexOf(it), 1);
|
|
else { it.removed = true; it.open = false; }
|
|
changed();
|
|
}
|
|
else if (act === 'unremove') { it.removed = false; changed(); }
|
|
else if (act === 'revert') { it.data = DATA.entries[it.orig]; it.status = null; changed(); }
|
|
}
|
|
document.getElementById('drvCard').addEventListener('click', function (e) {
|
|
var btn = e.target.closest('button[data-act]');
|
|
if (btn) { e.preventDefault(); handle(btn); }
|
|
});
|
|
|
|
// ---- filter events ----
|
|
var search = document.getElementById('drvSearch');
|
|
search.addEventListener('input', function () { filt.q = search.value.trim(); render(); });
|
|
function setOs(os) {
|
|
document.querySelectorAll('#drvOsSeg button').forEach(function (x) { x.setAttribute('aria-pressed', x.getAttribute('data-os') === os ? 'true' : 'false'); });
|
|
filt.os = os; render();
|
|
}
|
|
document.querySelectorAll('#drvOsSeg button').forEach(function (b) { b.addEventListener('click', function () { setOs(b.getAttribute('data-os')); }); });
|
|
function toggle(id, key, on) {
|
|
var b = document.getElementById(id);
|
|
filt[key] = on === undefined ? !filt[key] : on;
|
|
b.setAttribute('aria-pressed', filt[key] ? 'true' : 'false');
|
|
render();
|
|
}
|
|
document.getElementById('drvFind').addEventListener('click', function () { toggle('drvFind', 'find'); });
|
|
document.getElementById('drvMissing').addEventListener('click', function () { toggle('drvMissing', 'missing'); });
|
|
document.getElementById('drvClearFilter').addEventListener('click', function () {
|
|
search.value = ''; filt.q = ''; filt.missing = false; filt.find = false;
|
|
document.getElementById('drvFind').setAttribute('aria-pressed', 'false');
|
|
document.getElementById('drvMissing').setAttribute('aria-pressed', 'false');
|
|
setOs('');
|
|
});
|
|
// Deep links from the Overview: ?q=, ?os=Win10, ?missing=1, ?findings=1
|
|
(function () {
|
|
var p = new URLSearchParams(window.location.search);
|
|
if (p.get('q')) { search.value = p.get('q'); filt.q = search.value.trim(); }
|
|
var osq = p.get('os');
|
|
if (osq === 'Win10' || osq === 'Win11') { filt.os = osq; document.querySelectorAll('#drvOsSeg button').forEach(function (x) { x.setAttribute('aria-pressed', x.getAttribute('data-os') === osq ? 'true' : 'false'); }); }
|
|
if (p.get('missing') === '1') { filt.missing = true; document.getElementById('drvMissing').setAttribute('aria-pressed', 'true'); }
|
|
if (p.get('findings') === '1') { filt.find = true; document.getElementById('drvFind').setAttribute('aria-pressed', 'true'); }
|
|
})();
|
|
|
|
// ---- move dialog ----
|
|
var mv = { modal: document.getElementById('drvMoveModal'), pos: document.getElementById('drvMovePos'), it: null, seq: 0 };
|
|
function moveRows(it, pos) {
|
|
var others = live().filter(function (x) { return x !== it; });
|
|
var order = others.slice(0, pos - 1).concat([it]).concat(others.slice(pos - 1));
|
|
return order;
|
|
}
|
|
function renderMove() {
|
|
var it = mv.it, n = live().length;
|
|
var pos = Math.max(1, Math.min(n, parseInt(mv.pos.value, 10) || 1));
|
|
var order = moveRows(it, pos), from = livePos(it);
|
|
var ol = document.getElementById('drvMoveOrder');
|
|
ol.innerHTML = '';
|
|
for (var i = Math.max(0, pos - 2); i < Math.min(order.length, pos + 2); i++) {
|
|
var x = order[i], os = osOf(x.data);
|
|
var note = x === it ? ' moved from #' + from : (livePos(x) !== i + 1 ? ' was #' + livePos(x) : '');
|
|
ol.appendChild(h('li', { cls: x === it ? 'moved' : '' }, [h('span', { cls: 'mono', text: '#' + (i + 1) }),
|
|
h('span', {}, [h(x === it ? 'strong' : 'span', { text: tokensOf(x.data).join(', ') }), h('span', { cls: 'muted', text: note })]),
|
|
chip(os, os === 'Win11' ? 'w11' : 'w10')]));
|
|
}
|
|
document.getElementById('drvMoveShift').textContent = pos === from ? 'Same position: nothing moves.'
|
|
: pos < from ? 'Entries ' + pos + ' to ' + (from - 1) + ' move down one place.' : 'Entries ' + (from + 1) + ' to ' + pos + ' move up one place.';
|
|
var box = document.getElementById('drvMoveLint');
|
|
box.textContent = 'Checking...';
|
|
var seq = ++mv.seq;
|
|
var items = order.map(function (x) { return x; });
|
|
var body = items.map(function (x) {
|
|
var o = {}; Object.keys(x.data).forEach(function (k) { o[k] = x.data[k]; });
|
|
if (x.orig !== null) o._orig = x.orig; return o;
|
|
});
|
|
PxeImage.postJSON(URLS.check, { entries: body }).then(function (r) {
|
|
if (seq !== mv.seq || !r.ok) return;
|
|
var now = state.counts || {}, after = r.data.counts;
|
|
box.innerHTML = '';
|
|
var newErr = (after.CRITICAL + after.ERROR) - ((now.CRITICAL || 0) + (now.ERROR || 0));
|
|
var line = h('div', { style: 'display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px' }, [
|
|
newErr > 0 ? chip(plural(newErr, 'new error'), 'err') : chip('No new errors', 'ok'),
|
|
chip(plural(after.WARN, 'warning') + ' (' + (now.WARN || 0) + ' now)', after.WARN ? 'warn' : 'ok')]);
|
|
box.appendChild(line);
|
|
var errs = [];
|
|
r.data.rows.forEach(function (row) { row.findings.forEach(function (f) { if ((f.level === 'ERROR' || f.level === 'CRITICAL') && f.check !== 'missing-zip' && errs.indexOf(f.message) < 0) errs.push(f.message); }); });
|
|
if (newErr > 0) {
|
|
errs.slice(0, 4).forEach(function (m) { box.appendChild(h('div', { cls: 'errbox', style: 'margin-bottom:6px' }, [chip('ERROR', 'err'), ' ', m])); });
|
|
box.appendChild(h('p', { cls: 'sub', style: 'margin:8px 0 0', text: 'Errors do not block saving (only a save that adds a CRITICAL finding is refused), so this move can be staged.' }));
|
|
}
|
|
var apply = document.getElementById('drvMoveApply');
|
|
apply.textContent = newErr > 0 ? 'Stage move anyway' : 'Stage move';
|
|
apply.className = 'ui-btn' + (newErr > 0 ? ' danger' : '');
|
|
});
|
|
}
|
|
function openMove(it) {
|
|
mv.it = it;
|
|
var pos = livePos(it), os = osOf(it.data);
|
|
document.getElementById('drvMoveTitle').textContent = 'Move entry #' + pos;
|
|
document.getElementById('drvMoveSub').textContent = tokensOf(it.data).join(', ') + ' | ' + os + ' | ' + fileOf(it.data);
|
|
mv.pos.max = live().length;
|
|
mv.pos.value = pos;
|
|
renderMove();
|
|
bootstrap.Modal.getOrCreateInstance(mv.modal).show();
|
|
}
|
|
var mvTimer = null;
|
|
mv.pos.addEventListener('input', function () { clearTimeout(mvTimer); mvTimer = setTimeout(renderMove, 200); });
|
|
document.getElementById('drvMoveApply').addEventListener('click', function () {
|
|
var n = live().length, pos = Math.max(1, Math.min(n, parseInt(mv.pos.value, 10) || 1));
|
|
bootstrap.Modal.getOrCreateInstance(mv.modal).hide();
|
|
if (pos !== livePos(mv.it)) { placeAt(mv.it, pos); changed(); }
|
|
});
|
|
|
|
// ---- edit modal ----
|
|
var ed = {
|
|
modal: document.getElementById('drvEditModal'),
|
|
title: document.getElementById('drvEditTitle'),
|
|
tokens: document.getElementById('drvEdTokens'),
|
|
preview: document.getElementById('drvEdTokPreview'),
|
|
family: document.getElementById('drvEdFamily'),
|
|
friendly: document.getElementById('drvEdFriendly'),
|
|
osId: document.getElementById('drvEdOsId'),
|
|
pos: document.getElementById('drvEdPos'),
|
|
posHelp: document.getElementById('drvEdPosHelp'),
|
|
file: document.getElementById('drvEdFile'),
|
|
dir: document.getElementById('drvEdDir'),
|
|
zip: document.getElementById('drvEdZip'),
|
|
err: document.getElementById('drvEdError'),
|
|
apply: document.getElementById('drvEdApply'),
|
|
it: null, fresh: null
|
|
};
|
|
DATA.families.forEach(function (f) { document.getElementById('drvFamilies').appendChild(h('option', { value: f })); });
|
|
DATA.folders.forEach(function (f) { document.getElementById('drvFolders').appendChild(h('option', { value: f })); });
|
|
|
|
function edTokens() { return ed.tokens.value.split(/\r?\n|,/).map(function (t) { return t.trim(); }).filter(Boolean); }
|
|
function edPreview() {
|
|
var t = edTokens();
|
|
ed.preview.innerHTML = '';
|
|
if (!t.length) ed.preview.appendChild(h('span', { cls: 'text-muted small', text: 'No tokens: an empty modelswminame matches EVERY model.' }));
|
|
t.forEach(function (x) { ed.preview.appendChild(h('span', { cls: 'tokchip', text: x })); });
|
|
}
|
|
var zipTimer = null;
|
|
function edZip() {
|
|
clearTimeout(zipTimer);
|
|
zipTimer = setTimeout(function () {
|
|
var probe = { fileName: ed.file.value.trim(), destinationDir: ed.dir.value.trim(), modelswminame: 'x', family: 'x' };
|
|
ed.zip.textContent = 'Looking for the zip...';
|
|
ed.zip.className = 'zip-check text-muted';
|
|
PxeImage.postJSON(URLS.check, { entries: [probe] }).then(function (r) {
|
|
if (!r.ok) return;
|
|
var z = r.data.rows[0].zip;
|
|
if (z.status === 'ok') { ed.zip.className = 'zip-check text-success'; ed.zip.textContent = 'On the share: ' + fmtSize(z.size) + ', ' + fmtDate(z.mtime); }
|
|
else if (z.status === 'case') { ed.zip.className = 'zip-check text-warning'; ed.zip.textContent = 'On the share with other letter case (' + z.actual + '); WinPE finds it.'; }
|
|
else if (z.status === 'trap') { ed.zip.className = 'zip-check text-danger'; ed.zip.textContent = 'Only at ' + z.actual + ': WinPE will not find it at this path.'; }
|
|
else if (z.status === 'none') { ed.zip.className = 'zip-check text-danger'; ed.zip.textContent = 'No file name.'; }
|
|
else { ed.zip.className = 'zip-check text-danger'; ed.zip.textContent = 'Not on the share: ' + z.rel; }
|
|
});
|
|
}, 250);
|
|
}
|
|
ed.tokens.addEventListener('input', edPreview);
|
|
ed.file.addEventListener('input', edZip);
|
|
ed.dir.addEventListener('input', edZip);
|
|
|
|
// Where a new entry goes by default: a Win11 entry above the first Win10
|
|
// one (first match wins), anything else at the end.
|
|
function defaultPos(d) {
|
|
var lv = live();
|
|
if (osOf(d) === 'Win11') {
|
|
for (var i = 0; i < lv.length; i++) if (osOf(lv[i].data) === 'Win10') return i + 1;
|
|
}
|
|
return lv.length + 1;
|
|
}
|
|
|
|
function openEditor(idx, fresh) {
|
|
ed.it = fresh ? null : state.items[idx]; ed.fresh = fresh || null;
|
|
var d = fresh || ed.it.data;
|
|
var cur = fresh ? null : livePos(ed.it);
|
|
ed.title.textContent = fresh ? 'New entry' : ('Edit entry #' + cur);
|
|
ed.tokens.value = tokensOf(d).join('\n');
|
|
ed.family.value = d.family || '';
|
|
ed.friendly.value = d.modelsfriendlyname || '';
|
|
ed.osId.value = d.osId === undefined ? '' : String(d.osId);
|
|
ed.file.value = fileOf(d);
|
|
ed.dir.value = dirOf(d);
|
|
var max = live().length + (fresh ? 1 : 0);
|
|
ed.pos.max = max;
|
|
ed.pos.value = fresh ? defaultPos(d) : cur;
|
|
ed.posHelp.textContent = '1 is the top, ' + max + ' the bottom. The first match wins' + (fresh ? '; a Windows 11 entry defaults to just above the first Windows 10 entry.' : '.');
|
|
ed.err.classList.add('d-none');
|
|
edPreview(); edZip();
|
|
bootstrap.Modal.getOrCreateInstance(ed.modal).show();
|
|
}
|
|
|
|
function setKey(d, key, value) { d[key] = value; }
|
|
ed.apply.addEventListener('click', function () {
|
|
var toks = edTokens();
|
|
var fail = function (m) { ed.err.textContent = m; ed.err.classList.remove('d-none'); };
|
|
if (!toks.length) return fail('Give at least one model token. An empty modelswminame matches every PC.');
|
|
if (!ed.family.value.trim()) return fail('Give a family.');
|
|
if (!ed.file.value.trim()) return fail('Give the zip file name.');
|
|
if (!ed.dir.value.trim()) return fail('Give the folder (destinationDir).');
|
|
var max = parseInt(ed.pos.max, 10);
|
|
var pos = parseInt(ed.pos.value, 10);
|
|
if (!(pos >= 1 && pos <= max)) return fail('Position must be between 1 and ' + max + '.');
|
|
|
|
var src = ed.fresh || ed.it.data;
|
|
var d = {};
|
|
Object.keys(src).forEach(function (k) { d[k] = src[k]; });
|
|
var osIds = ed.osId.value.split(',').map(function (s) { return s.trim(); }).filter(Boolean);
|
|
setKey(d, 'family', ed.family.value.trim());
|
|
setKey(d, 'modelswminame', toks.join(','));
|
|
if ('modelsfriendlyname' in d || ed.friendly.value.trim()) setKey(d, 'modelsfriendlyname', ed.friendly.value.trim());
|
|
if ('osId' in d || osIds.length) setKey(d, 'osId', osIds.join(','));
|
|
// Mirrored keys: both spellings are in the GE maps; keep them equal.
|
|
if ('fileName' in d || !('FileName' in d)) setKey(d, 'fileName', ed.file.value.trim());
|
|
if ('FileName' in d) setKey(d, 'FileName', ed.file.value.trim());
|
|
if ('destinationDir' in d || !('DestinationDir' in d)) setKey(d, 'destinationDir', ed.dir.value.trim());
|
|
if ('DestinationDir' in d) setKey(d, 'DestinationDir', ed.dir.value.trim());
|
|
if ('models' in d) setKey(d, 'models', d.modelswminame);
|
|
if ('aOsIds' in d) setKey(d, 'aOsIds', osIds);
|
|
|
|
if (ed.fresh) {
|
|
// product holds the Dell line in the GE maps (Latitude, OptiPlex, Precision).
|
|
var line = /precision/i.test(toks[0]) ? 'Precision' : /optiplex/i.test(toks[0]) ? 'OptiPlex'
|
|
: /latitude/i.test(toks[0]) ? 'Latitude' : '';
|
|
if (line && d.product === 'Dell') setKey(d, 'product', line);
|
|
var nw = { orig: null, moved: false, removed: false, status: 'new', open: true, data: d, row: null, saved: null };
|
|
state.items.push(nw);
|
|
placeAt(nw, pos);
|
|
} else {
|
|
var it = ed.it;
|
|
if (JSON.stringify(d) !== JSON.stringify(it.data)) {
|
|
it.data = d;
|
|
it.status = (it.orig !== null && JSON.stringify(d) === JSON.stringify(DATA.entries[it.orig])) ? null : 'edited';
|
|
}
|
|
if (pos !== livePos(it)) placeAt(it, pos);
|
|
}
|
|
bootstrap.Modal.getOrCreateInstance(ed.modal).hide();
|
|
changed();
|
|
});
|
|
|
|
function newEntry(fileName, dest, size) {
|
|
var d = {};
|
|
var os = /\\dell_10\\/i.test(dest + '\\') ? '18' : '20,21';
|
|
var defaults = { manufacturer: 'Dell', product: 'Dell', family: '', modelswminame: '', modelsfriendlyname: '',
|
|
fileName: fileName || '', destinationDir: dest || '', url: '', hash: '', size: size || 0,
|
|
modifiedDate: '0001-01-01T00:00:00', osId: os, imagedisk: 0, models: '', FileName: fileName || '',
|
|
DestinationDir: dest || '', aOsIds: os.split(',') };
|
|
NEW_KEYS.forEach(function (k) { d[k] = defaults[k]; });
|
|
return d;
|
|
}
|
|
document.getElementById('drvAdd').addEventListener('click', function () {
|
|
var m = this.closest('details.more'); if (m) m.open = false;
|
|
openEditor(null, newEntry('', '', 0));
|
|
});
|
|
document.querySelectorAll('.orphan-add').forEach(function (b) {
|
|
b.addEventListener('click', function () {
|
|
openEditor(null, newEntry(b.getAttribute('data-name'), b.getAttribute('data-dest'), parseInt(b.getAttribute('data-size'), 10) || 0));
|
|
});
|
|
});
|
|
var shTog = document.getElementById('orphanSharedToggle');
|
|
if (shTog) shTog.addEventListener('click', function () {
|
|
var open = shTog.getAttribute('aria-expanded') !== 'true';
|
|
shTog.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
document.querySelectorAll('.orphan-shared').forEach(function (tr) { tr.style.display = open ? '' : 'none'; });
|
|
});
|
|
var selAll = document.getElementById('orphanSelectAll');
|
|
if (selAll) selAll.addEventListener('change', function () {
|
|
document.querySelectorAll('.orphan-select').forEach(function (cb) { cb.checked = selAll.checked; });
|
|
});
|
|
|
|
// ---- review and save (shared modal from _image_header.html) ----
|
|
function review() {
|
|
var modalEl = document.getElementById('imageReviewModal');
|
|
var body = document.getElementById('imageReviewBody');
|
|
var err = document.getElementById('imageReviewError');
|
|
var ok = document.getElementById('imageReviewConfirm');
|
|
document.getElementById('imageReviewTitle').textContent = 'Review changes to HardwareDriver.json';
|
|
body.innerHTML = '<div class="text-muted py-3">Building the preview...</div>';
|
|
err.classList.add('d-none');
|
|
ok.disabled = true;
|
|
bootstrap.Modal.getOrCreateInstance(modalEl).show();
|
|
function fail(m) { err.textContent = m; err.classList.remove('d-none'); }
|
|
var req = { entries: payload(), base: DATA.base };
|
|
|
|
PxeImage.postJSON(URLS.preview, req).then(function (r) {
|
|
if (!r.ok) { body.innerHTML = ''; fail(r.data.error || 'Preview failed.'); return; }
|
|
var p = r.data;
|
|
body.innerHTML = '';
|
|
var s = p.summary;
|
|
var sum = h('div', { cls: 'rv-summary' });
|
|
[['edited', 'edit'], ['moved', 'neutral'], ['added', 'edit'], ['removed', 'remove']].forEach(function (k) {
|
|
if (s[k[0]]) sum.appendChild(chip(s[k[0]] + ' ' + k[0], k[1]));
|
|
});
|
|
var fileChanged = p.files[0] && p.files[0].changed;
|
|
if (!fileChanged) {
|
|
body.appendChild(h('div', { cls: 'notice info', text: 'No changes: the file already holds exactly this.' }));
|
|
return;
|
|
}
|
|
body.appendChild(sum);
|
|
|
|
var lb = p.lint.before, la = p.lint.after;
|
|
var lint = h('div', { cls: 'rv-lint' }, [chip(countsText(lb) + ' now', (lb.CRITICAL || lb.ERROR) ? 'err' : lb.WARN ? 'warn' : 'ok'), ' ',
|
|
chip(countsText(la) + ' after save', (la.CRITICAL || la.ERROR) ? 'err' : la.WARN ? 'warn' : 'ok')]);
|
|
if (la.CRITICAL + la.ERROR > lb.CRITICAL + lb.ERROR) {
|
|
var ul = h('ul');
|
|
p.lint.after_errors.slice(0, 8).forEach(function (m) { ul.appendChild(h('li', { text: m })); });
|
|
lint.appendChild(ul);
|
|
}
|
|
body.appendChild(lint);
|
|
if (p.blocked.length) {
|
|
body.appendChild(h('div', { cls: 'notice bad', style: 'margin-bottom:12px' }, [h('strong', { text: 'Cannot save: ' }),
|
|
h('span', { text: 'this adds a CRITICAL finding, which breaks imaging for many or all PCs. ' + p.blocked[0] })]));
|
|
}
|
|
// Friendly names that no longer describe the tokens.
|
|
live().forEach(function (it) {
|
|
if (it.status !== 'edited' || it.orig === null) return;
|
|
var was = DATA.entries[it.orig];
|
|
if (String(was.modelswminame) !== String(it.data.modelswminame) && it.data.modelsfriendlyname && it.data.modelsfriendlyname === was.modelsfriendlyname) {
|
|
body.appendChild(h('div', { cls: 'notice', style: 'margin-bottom:12px' }, [h('span', { text: '#' + livePos(it) + ': the friendly name still says ' + it.data.modelsfriendlyname + '. You can save anyway, or go back and change it.' })]));
|
|
}
|
|
});
|
|
|
|
var moves = p.changes.filter(function (c) { return c.kind === 'moved'; });
|
|
if (moves.length) {
|
|
body.appendChild(h('div', { cls: 'fw-semibold small mb-1', text: 'Moved (content unchanged)' }));
|
|
var ml = h('ul', { cls: 'rv-moves' });
|
|
moves.forEach(function (c) {
|
|
ml.appendChild(h('li', {}, [h('span', { cls: 'mono', text: '#' + c.old_pos + ' to #' + c.new_pos }),
|
|
' ' + c.label.replace(/^#\d+ /, ''), h('span', { cls: 'text-muted mono', text: c.file ? ' ' + c.file : '' })]));
|
|
});
|
|
body.appendChild(ml);
|
|
}
|
|
var diffs = p.changes.filter(function (c) { return c.kind !== 'moved'; }).map(function (c) {
|
|
var what = c.kind === 'added' ? 'new at #' + c.new_pos
|
|
: c.kind === 'removed' ? 'removed from #' + c.old_pos
|
|
: c.kind === 'edited+moved' ? 'edited, moved #' + c.old_pos + ' to #' + c.new_pos
|
|
: 'edited (' + c.fields.join(', ') + ')';
|
|
return { name: c.label.replace(/^#\d+ /, '') + ': ' + what, changed: true, existed: true, diff: c.diff, reformat: false };
|
|
});
|
|
if (diffs.length) {
|
|
var dbox = h('div');
|
|
body.appendChild(dbox);
|
|
PxeImage.renderDiff(dbox, diffs);
|
|
}
|
|
var full = h('details', { cls: 'rv-full' }, [h('summary', { text: 'Whole-file diff of HardwareDriver.json' })]);
|
|
var fbox = h('div');
|
|
full.appendChild(fbox);
|
|
body.appendChild(full);
|
|
PxeImage.renderDiff(fbox, p.files);
|
|
|
|
var expect = {};
|
|
p.files.forEach(function (f) { expect[f.name] = f.sha_before; });
|
|
ok.disabled = p.blocked.length > 0;
|
|
ok.onclick = function () {
|
|
ok.disabled = true;
|
|
PxeImage.postJSON(URLS.save, { entries: req.entries, base: req.base, expect: expect }).then(function (sres) {
|
|
if (!sres.ok) { fail(sres.data.error || 'Save failed.'); ok.disabled = !!sres.data.stale; return; }
|
|
state.dirty = false;
|
|
window.location.reload();
|
|
}).catch(function (e) { fail('Save failed: ' + e); ok.disabled = false; });
|
|
};
|
|
}).catch(function (e) { body.innerHTML = ''; fail('Preview failed: ' + e); });
|
|
}
|
|
document.getElementById('drvReview').addEventListener('click', review);
|
|
document.getElementById('drvDiscard').addEventListener('click', function () {
|
|
if (confirm('Discard ' + plural(nChanges(), 'unsaved change') + '?')) { state.dirty = false; window.location.reload(); }
|
|
});
|
|
window.addEventListener('beforeunload', function (e) {
|
|
if (state.dirty && nChanges()) { e.preventDefault(); e.returnValue = ''; }
|
|
});
|
|
|
|
render();
|
|
})();
|
|
</script>
|
|
{% endblock %}
|