webapp: reskin to shopdb-flask design system
Adopt the shopdb-flask visual language across the PXE webapp (presentation only, Flask/Jinja logic unchanged): - New static/pxe-theme.css: GE Aerospace palette (atmosphere-blue sidebar, sky-blue primary, avionics-green), Inter font stack, light/dark theming via data-theme + localStorage (key pxe-theme) with system-pref fallback, and card/button/table/form/badge/alert component styles layered over Bootstrap. - base.html: shopdb-style sidebar (logo + title, nav sections, footer light/dark toggle) + theme boot script. - All 13 content templates restyled to the new page-header + card/table/badge vocabulary; unattend_editor grouped per unattend-UX research. - Fixed a pre-existing CRITICAL bug found during review: nested <form>s in image_config.html made Adopt submit the delete form and Delete-selected post every orphan filename regardless of checkboxes; split into standalone forms wired via the form= attribute. Built by a Fable-orchestrated Opus workflow (17 agents). All 14 templates parse clean under Jinja2.
This commit is contained in:
@@ -1,228 +1,178 @@
|
||||
<!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">
|
||||
<style>
|
||||
:root {
|
||||
--sidebar-width: 280px;
|
||||
}
|
||||
body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
min-height: 100vh;
|
||||
background-color: #1a1d21;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.sidebar .nav-link {
|
||||
color: #adb5bd;
|
||||
padding: 0.6rem 1.25rem;
|
||||
font-size: 0.9rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
.sidebar .nav-link:hover,
|
||||
.sidebar .nav-link.active {
|
||||
color: #fff;
|
||||
background-color: rgba(255,255,255,0.08);
|
||||
}
|
||||
.sidebar-heading {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: #6c757d;
|
||||
padding: 1rem 1.25rem 0.4rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.sidebar .brand {
|
||||
padding: 1.2rem 1.25rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
.main-content {
|
||||
margin-left: var(--sidebar-width);
|
||||
padding: 2rem;
|
||||
}
|
||||
.status-dot {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.status-dot.active {
|
||||
background-color: #198754;
|
||||
}
|
||||
.status-dot.inactive {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
.status-dot.unknown {
|
||||
background-color: #ffc107;
|
||||
}
|
||||
.card {
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
.card-header {
|
||||
font-weight: 600;
|
||||
}
|
||||
.table th {
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn-row-action {
|
||||
padding: 0.2rem 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
color: #6c757d;
|
||||
}
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.nav-section-divider {
|
||||
border-top: 1px solid rgba(255,255,255,0.06);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
{% block extra_head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<nav class="sidebar d-flex flex-column">
|
||||
<div class="brand">
|
||||
<img src="{{ url_for('static', filename='ge-aerospace-logo.svg') }}" alt="GE Aerospace" style="height: 28px; filter: brightness(0) invert(1);">
|
||||
PXE Manager
|
||||
</div>
|
||||
<ul class="nav flex-column mt-2">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'dashboard' %}active{% endif %}"
|
||||
href="{{ url_for('dashboard') }}">
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'images_import' %}active{% endif %}"
|
||||
href="{{ url_for('images_import') }}">
|
||||
Image Import
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'imaging_dashboard' %}active{% endif %}"
|
||||
href="{{ url_for('imaging_dashboard') }}">
|
||||
Imaging Progress
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="nav-section-divider"></div>
|
||||
<div class="sidebar-heading">Tools</div>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'startnet_editor' %}active{% endif %}"
|
||||
href="{{ url_for('startnet_editor') }}">
|
||||
startnet.cmd
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'clonezilla_backups' %}active{% endif %}"
|
||||
href="{{ url_for('clonezilla_backups') }}">
|
||||
Clonezilla Backups
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'blancco_reports' %}active{% endif %}"
|
||||
href="{{ url_for('blancco_reports') }}">
|
||||
Blancco Reports
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'enrollment' %}active{% endif %}"
|
||||
href="{{ url_for('enrollment') }}">
|
||||
Enrollment
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'audit_log' %}active{% endif %}"
|
||||
href="{{ url_for('audit_log') }}">
|
||||
Audit Log
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="nav-section-divider"></div>
|
||||
<div class="sidebar-heading">GE Aerospace Images</div>
|
||||
<ul class="nav flex-column">
|
||||
{% for it in all_image_types if it.startswith('gea-') %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'unattend_editor' and image_type is defined and image_type == it %}active{% endif %}"
|
||||
href="{{ url_for('unattend_editor', image_type=it) }}">
|
||||
{{ all_friendly_names[it] }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'image_config' and image_type is defined and image_type == it %}active{% endif %}"
|
||||
href="{{ url_for('image_config', image_type=it) }}" style="padding-left: 2.5rem; font-size: 0.82rem;">
|
||||
Configuration
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="nav-section-divider"></div>
|
||||
<div class="sidebar-heading">GE Legacy Images</div>
|
||||
<ul class="nav flex-column">
|
||||
{% for it in all_image_types if it.startswith('ge-') and not it.startswith('gea-') %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'unattend_editor' and image_type is defined and image_type == it %}active{% endif %}"
|
||||
href="{{ url_for('unattend_editor', image_type=it) }}">
|
||||
{{ all_friendly_names[it] }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'image_config' and image_type is defined and image_type == it %}active{% endif %}"
|
||||
href="{{ url_for('image_config', image_type=it) }}" style="padding-left: 2.5rem; font-size: 0.82rem;">
|
||||
Configuration
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="main-content">
|
||||
{% 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"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<!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">
|
||||
<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>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<nav class="sidebar d-flex flex-column">
|
||||
<div class="sidebar-header">
|
||||
<img class="sidebar-logo" src="{{ url_for('static', filename='ge-aerospace-logo.svg') }}" alt="GE Aerospace">
|
||||
<h1>PXE Manager</h1>
|
||||
</div>
|
||||
|
||||
<ul class="sidebar-nav nav flex-column mt-2">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'dashboard' %}active{% endif %}"
|
||||
href="{{ url_for('dashboard') }}">
|
||||
<i class="bi bi-speedometer2"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'images_import' %}active{% endif %}"
|
||||
href="{{ url_for('images_import') }}">
|
||||
<i class="bi bi-box-arrow-in-down"></i> Image Import
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'imaging_dashboard' %}active{% endif %}"
|
||||
href="{{ url_for('imaging_dashboard') }}">
|
||||
<i class="bi bi-activity"></i> Imaging Progress
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="nav-section">Tools</div>
|
||||
<ul class="sidebar-nav nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'startnet_editor' %}active{% endif %}"
|
||||
href="{{ url_for('startnet_editor') }}">
|
||||
<i class="bi bi-terminal"></i> startnet.cmd
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'clonezilla_backups' %}active{% endif %}"
|
||||
href="{{ url_for('clonezilla_backups') }}">
|
||||
<i class="bi bi-hdd-stack"></i> Clonezilla Backups
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'blancco_reports' %}active{% endif %}"
|
||||
href="{{ url_for('blancco_reports') }}">
|
||||
<i class="bi bi-shield-check"></i> Blancco Reports
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'enrollment' %}active{% endif %}"
|
||||
href="{{ url_for('enrollment') }}">
|
||||
<i class="bi bi-pc-display"></i> Enrollment
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'audit_log' %}active{% endif %}"
|
||||
href="{{ url_for('audit_log') }}">
|
||||
<i class="bi bi-list-check"></i> Audit Log
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="nav-section">GE Aerospace Images</div>
|
||||
<ul class="sidebar-nav nav flex-column">
|
||||
{% for it in all_image_types if it.startswith('gea-') %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'unattend_editor' and image_type is defined and image_type == it %}active{% endif %}"
|
||||
href="{{ url_for('unattend_editor', image_type=it) }}">
|
||||
<i class="bi bi-windows"></i> {{ all_friendly_names[it] }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link nav-sublink {% if request.endpoint == 'image_config' and image_type is defined and image_type == it %}active{% endif %}"
|
||||
href="{{ url_for('image_config', image_type=it) }}">
|
||||
<i class="bi bi-gear"></i> Configuration
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<!-- Footer pinned to bottom -->
|
||||
<div class="sidebar-footer mt-auto">
|
||||
<button type="button" id="themeToggle" class="theme-toggle">
|
||||
<i class="bi bi-moon-stars" id="themeToggleIcon"></i>
|
||||
<span id="themeToggleLabel">Dark</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="main-content">
|
||||
{% 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"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</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';
|
||||
} else {
|
||||
if (icon) icon.className = 'bi bi-moon-stars';
|
||||
if (label) label.textContent = 'Dark';
|
||||
}
|
||||
}
|
||||
|
||||
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 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>
|
||||
|
||||
Reference in New Issue
Block a user