Files
pxe-server/webapp/templates/base.html
cproudlock 0b86255711 Webapp redesign phase 2: Overview and imaging floor
Imaging floor (/imaging, /imaging/tiles):
- One line per PC: status chip, serial with machine number, hostname
  and IP, stage text with a segmented stage track, one action plus a
  '...' menu. Everything else (description, error, timestamps, Intune
  ID + QR, Set category / ARTS links, LAPS QR, log tail) is in the row's
  expandable body.
- Order: failed, needs action (stage 7), in progress, done; log-only
  bays fold into one collapsed group. imaging_status.floor_state() and
  floor_order() compute this.
- Status filter buttons with live counts next to the text filter. SSE
  refresh, expanded-row memory and LAPS handling are unchanged.
- Stage text is never truncated; BIOS updates and erasing are bold.

Overview (/):
- Services popover, technician to-do line, the six most urgent floor
  rows (?limit=6 partial, refreshed every 15 s), and image cards with
  lint warnings, map errors and missing zips as separate chips. New,
  clone, delete and unattend stay on the cards' menus.

Copy buttons are now handled once in static/app.js, with a fallback for
plain-http pages where navigator.clipboard is unavailable.
2026-10-14 13:44:07 -04:00

177 lines
8.9 KiB
HTML

<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{% block title %}PXE Server Manager{% endblock %}</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link href="{{ url_for('static', filename='bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='bootstrap-icons.min.css') }}" rel="stylesheet">
<!-- pxe-theme.css loads AFTER bootstrap so its token+component rules win -->
<link href="{{ url_for('static', filename='pxe-theme.css') }}" rel="stylesheet">
<!-- pxe-ui.css: redesign tokens and components; legacy tokens alias onto it -->
<link href="{{ url_for('static', filename='pxe-ui.css') }}" rel="stylesheet">
<script>
// Set the theme before first paint to avoid an unstyled flash.
(function () {
var KEY = 'pxe-theme';
var stored = null;
try { stored = localStorage.getItem(KEY); } catch (e) {}
var theme = (stored === 'light' || stored === 'dark')
? stored
: (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.setAttribute('data-bs-theme', theme);
})();
</script>
{% block extra_head %}{% endblock %}
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
{% set ep = request.endpoint or '' %}
{% macro navlink(label, href, on, count=None) -%}
<a class="ui-navlink" href="{{ href }}"{% if on %} aria-current="page"{% endif %}>{{ label }}{% if count %}<span class="count">{{ count }}<span class="vh"> open</span></span>{% endif %}</a>
{%- endmacro %}
<div class="ui-shell">
<nav class="ui-side" id="uiSide" aria-label="Main">
<div class="ui-brand">
<div>
<img class="logo" src="{{ url_for('static', filename='ge-aerospace-logo.svg') }}" alt="GE Aerospace">
<div class="app">PXE Manager</div>
<div class="host">{{ request.host.split(':')[0] }}</div>
</div>
<button type="button" class="ui-menu-btn" id="uiMenuBtn" aria-label="Open menu" aria-expanded="false" aria-controls="uiGroups">
<i class="bi bi-list" aria-hidden="true" style="font-size: 20px"></i>
</button>
</div>
<div class="ui-groups" id="uiGroups">
<div class="ui-group">
<div class="ui-navhead">Operate</div>
{{ navlink('Overview', url_for('dashboard'), ep == 'dashboard') }}
{{ navlink('Imaging floor', url_for('imaging_dashboard'), ep in ('imaging_dashboard', 'imaging_session_detail')) }}
{{ navlink('Technician to-do', url_for('blancco_todo'), ep in ('blancco_todo', 'blancco_servicenow'), blancco_todo.total) }}
{{ navlink('Blancco reports', url_for('blancco_reports'), ep in ('blancco_reports', 'blancco_view_report')) }}
{{ navlink('Hardware coverage', url_for('hardware_page'), ep == 'hardware_page') }}
</div>
<div class="ui-group">
<div class="ui-navhead">Images</div>
{% for it in all_image_types if it.startswith('gea-') %}
{# One entry per image; active on any of its tabs (see _image_header.html). #}
{% set _on_image = image_type is defined and image_type == it
and ((active_tab is defined and active_tab) or image_tab_for_endpoint(request.endpoint)) %}
{{ navlink(all_friendly_names[it] | replace('GE Aerospace ', ''), image_tab_url(it, 'overview'), _on_image) }}
{% endfor %}
{{ navlink('All images and import', url_for('images_import'), ep == 'images_import') }}
</div>
<div class="ui-group">
<div class="ui-navhead">Updates</div>
{{ navlink('Dell drivers and BIOS', url_for('dell_updates.page'), ep.startswith('dell_updates.')) }}
{{ navlink('iPXE boot files', url_for('ipxe.page'), ep.startswith('ipxe.')) }}
{{ navlink('Blancco releases', url_for('blancco_update.page'), ep.startswith('blancco_update.')) }}
{# Plain href: the page still renders if views.bprt_sync is not registered yet. #}
{{ navlink('BPRT sync', '/updates/bprt/', ep.startswith('bprt_sync.')) }}
</div>
<div class="ui-group">
<div class="ui-navhead">Tools</div>
{{ navlink('Boot menu', url_for('startnet_editor'), ep in ('startnet_editor', 'shopfloor_menu')) }}
{{ navlink('Enrollment packages', url_for('enrollment'), ep == 'enrollment') }}
{{ navlink('Clonezilla backups', url_for('clonezilla_backups'), ep == 'clonezilla_backups') }}
{{ navlink('Audit log', url_for('audit_log'), ep == 'audit_log') }}
</div>
</div>
<button type="button" id="themeToggle" class="ui-theme" aria-pressed="false">
<i class="bi bi-moon-stars" id="themeToggleIcon" aria-hidden="true"></i>
<span id="themeToggleLabel">Dark theme</span>
</button>
</nav>
<main class="ui-main main-content" id="main">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Dismiss"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% if blancco_todo.total and ep not in ('blancco_todo', 'blancco_servicenow', 'imaging_tiles', 'dashboard') %}
{% set parts = [] %}
{% if blancco_todo.servicenow %}{% set _ = parts.append(blancco_todo.servicenow ~ ' to tick in ServiceNow') %}{% endif %}
{% if blancco_todo.failed %}{% set _ = parts.append(blancco_todo.failed ~ ' drive' ~ ('s' if blancco_todo.failed != 1) ~ ' to destroy') %}{% endif %}
{% if blancco_todo.evidence %}{% set _ = parts.append(blancco_todo.evidence ~ ' evidence pack' ~ ('s' if blancco_todo.evidence != 1) ~ ' to upload') %}{% endif %}
{% if blancco_todo.not_in_cloud %}{% set _ = parts.append(blancco_todo.not_in_cloud ~ ' not in the Blancco cloud') %}{% endif %}
{% if blancco_todo.hardware %}{% set _ = parts.append(blancco_todo.hardware ~ ' hardware gap' ~ ('s' if blancco_todo.hardware != 1)) %}{% endif %}
<a class="ui-todo" href="{{ url_for('blancco_todo') }}">
<span class="chip c-neutral">To-do</span>
<span class="trunc" style="flex: 1 1 auto"><strong>{{ blancco_todo.total }} open:</strong> <span class="muted">{{ parts | join(', ') }}</span></span>
<span style="font-weight: 600; color: var(--accent); white-space: nowrap">Open to-do</span>
</a>
{% endif %}
{% block content %}{% endblock %}
</main>
</div>
<script>
// Theme toggle: flips data-theme (and data-bs-theme) and persists to localStorage.
(function () {
var KEY = 'pxe-theme';
var btn = document.getElementById('themeToggle');
var icon = document.getElementById('themeToggleIcon');
var label = document.getElementById('themeToggleLabel');
function paintToggle(theme) {
// Label/icon advertise the theme you would switch TO.
if (theme === 'dark') {
if (icon) icon.className = 'bi bi-sun';
if (label) label.textContent = 'Light theme';
} else {
if (icon) icon.className = 'bi bi-moon-stars';
if (label) label.textContent = 'Dark theme';
}
if (btn) btn.setAttribute('aria-pressed', theme === 'dark' ? 'true' : 'false');
}
function apply(theme) {
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.setAttribute('data-bs-theme', theme);
try { localStorage.setItem(KEY, theme); } catch (e) {}
paintToggle(theme);
}
paintToggle(document.documentElement.getAttribute('data-theme') || 'light');
if (btn) {
btn.addEventListener('click', function () {
var current = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
apply(current === 'dark' ? 'light' : 'dark');
});
}
})();
</script>
<script>
// Narrow screens: the sidebar collapses to a top bar with a menu button.
(function () {
var side = document.getElementById('uiSide');
var btn = document.getElementById('uiMenuBtn');
if (!side || !btn) return;
btn.addEventListener('click', function () {
var open = side.classList.toggle('open');
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
btn.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
});
})();
</script>
<script src="{{ url_for('static', filename='bootstrap.bundle.min.js') }}"></script>
<script src="{{ url_for('static', filename='app.js') }}"></script>
<script src="{{ url_for('static', filename='qrcode.min.js') }}"></script>
<script src="{{ url_for('static', filename='qr-render.js') }}"></script>
{% block extra_scripts %}{% endblock %}
</body>
</html>