Files
pxe-server/webapp/templates/image_drivers.html
cproudlock 731d7b044d Webapp redesign phase 3: Drivers tab
- 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.
2026-10-14 13:51:04 -04:00

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">&#9662;</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">&#9662;</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">&#9662;</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 %}