Add web management UI, offline packages, WinPE consolidation, and docs
- webapp/: Flask web management app with:
- Dashboard showing image types and service status
- USB import page for WinPE deployment content
- Unattend.xml visual editor (driver paths, specialize commands,
OOBE settings, first logon commands, raw XML view)
- API endpoints for services and image management
- SETUP.md: Complete setup documentation for streamlined process
- build-usb.sh: Now copies webapp and optional WinPE images to USB
- playbook: Added webapp deployment (systemd service, Apache reverse
proxy), offline package verification, WinPE auto-import from USB
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
185
webapp/templates/base.html
Normal file
185
webapp/templates/base.html
Normal file
@@ -0,0 +1,185 @@
|
||||
<!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">
|
||||
<title>{% block title %}PXE Server Manager{% endblock %}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YcnS49cn91B2HOwP4cMpe1bBMnos9GBsYl7a"
|
||||
crossorigin="anonymous">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.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 .nav-link .bi {
|
||||
margin-right: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.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;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.sidebar .brand .bi {
|
||||
font-size: 1.3rem;
|
||||
color: #0d6efd;
|
||||
}
|
||||
.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">
|
||||
<i class="bi bi-hdd-network"></i>
|
||||
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') }}">
|
||||
<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-download"></i> Image Import
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="nav-section-divider"></div>
|
||||
<div class="sidebar-heading">PBR Images</div>
|
||||
<ul class="nav flex-column">
|
||||
{% for it in ['geastandardpbr', 'geaengineerpbr', 'geashopfloorpbr'] %}
|
||||
<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-file-earmark-code"></i> {{ all_friendly_names[it] }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="nav-section-divider"></div>
|
||||
<div class="sidebar-heading">Legacy Images</div>
|
||||
<ul class="nav flex-column">
|
||||
{% for it in ['gestandardlegacy', 'geengineerlegacy', 'geshopfloorlegacy'] %}
|
||||
<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-file-earmark-code"></i> {{ all_friendly_names[it] }}
|
||||
</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="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="{{ url_for('static', filename='app.js') }}"></script>
|
||||
{% block extra_scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
95
webapp/templates/dashboard.html
Normal file
95
webapp/templates/dashboard.html
Normal file
@@ -0,0 +1,95 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard - PXE Server Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="mb-0">Dashboard</h2>
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="location.reload()">
|
||||
<i class="bi bi-arrow-clockwise"></i> Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Services -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
<i class="bi bi-gear-wide-connected me-2"></i> PXE Services
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Service</th>
|
||||
<th>Status</th>
|
||||
<th>State</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for svc in services %}
|
||||
<tr>
|
||||
<td>
|
||||
<i class="bi bi-server me-1 text-muted"></i>
|
||||
<strong>{{ svc.name }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-dot {{ 'active' if svc.active else 'inactive' }}"></span>
|
||||
{{ "Running" if svc.active else "Stopped" }}
|
||||
</td>
|
||||
<td><code>{{ svc.state }}</code></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Images -->
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
<i class="bi bi-disc me-2"></i> Deployment Images
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Image</th>
|
||||
<th>Deploy Content</th>
|
||||
<th>unattend.xml</th>
|
||||
<th>Path</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for img in images %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ img.friendly_name }}</strong><br>
|
||||
<small class="text-muted">{{ img.image_type }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{% if img.has_content %}
|
||||
<span class="badge bg-success"><i class="bi bi-check-circle"></i> Present</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary"><i class="bi bi-x-circle"></i> Empty</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if img.has_unattend %}
|
||||
<span class="badge bg-success"><i class="bi bi-check-circle"></i> Exists</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning text-dark"><i class="bi bi-exclamation-triangle"></i> Missing</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><code class="small">{{ img.deploy_path }}</code></td>
|
||||
<td class="text-end">
|
||||
<a href="{{ url_for('unattend_editor', image_type=img.image_type) }}"
|
||||
class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil-square"></i> Edit Unattend
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
119
webapp/templates/import.html
Normal file
119
webapp/templates/import.html
Normal file
@@ -0,0 +1,119 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Image Import - PXE Server Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-4">Image Import</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-usb-drive me-2"></i> Import from USB Drive
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if usb_mounts %}
|
||||
<form method="POST" id="importForm">
|
||||
<div class="mb-3">
|
||||
<label for="source" class="form-label fw-semibold">Source (USB Mount Point)</label>
|
||||
<select class="form-select" name="source" id="source" required>
|
||||
<option value="">-- Select a mounted USB drive --</option>
|
||||
{% for mount in usb_mounts %}
|
||||
<option value="{{ mount }}">{{ mount }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="form-text">
|
||||
Select the mounted USB drive containing the WinPE deployment content.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="target" class="form-label fw-semibold">Target Image Type</label>
|
||||
<select class="form-select" name="target" id="target" required>
|
||||
<option value="">-- Select target image --</option>
|
||||
{% for it in image_types %}
|
||||
<option value="{{ it }}">{{ friendly_names[it] }} ({{ it }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="form-text">
|
||||
Content will be copied into the Deploy directory for this image type.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning d-flex align-items-start" role="alert">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2 mt-1"></i>
|
||||
<div>
|
||||
<strong>Warning:</strong> Existing files in the target Deploy directory with the
|
||||
same names will be overwritten. This operation may take several minutes for large
|
||||
images.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" id="importBtn">
|
||||
<i class="bi bi-download me-1"></i> Start Import
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-usb-plug display-4 text-muted"></i>
|
||||
<h5 class="mt-3 text-muted">No USB Drives Detected</h5>
|
||||
<p class="text-muted mb-0">
|
||||
No mounted USB drives were found under <code>/mnt/</code> or <code>/media/</code>.<br>
|
||||
Mount a USB drive and refresh this page.
|
||||
</p>
|
||||
<button class="btn btn-outline-secondary btn-sm mt-3" onclick="location.reload()">
|
||||
<i class="bi bi-arrow-clockwise"></i> Refresh
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-info-circle me-2"></i> Current Image Status
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Image</th>
|
||||
<th>Content</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for img in images %}
|
||||
<tr>
|
||||
<td class="small">{{ img.friendly_name }}</td>
|
||||
<td>
|
||||
{% if img.has_content %}
|
||||
<span class="badge bg-success">Present</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Empty</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var form = document.getElementById('importForm');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function() {
|
||||
var btn = document.getElementById('importBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Importing...';
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
346
webapp/templates/unattend_editor.html
Normal file
346
webapp/templates/unattend_editor.html
Normal file
@@ -0,0 +1,346 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ friendly_name }} - Unattend Editor{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
.editor-tabs .nav-link {
|
||||
font-weight: 500;
|
||||
}
|
||||
.command-table tbody tr {
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
.command-table tbody tr.dragging {
|
||||
opacity: 0.5;
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
.raw-xml-editor {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.85rem;
|
||||
min-height: 500px;
|
||||
tab-size: 2;
|
||||
white-space: pre;
|
||||
resize: vertical;
|
||||
}
|
||||
.section-card {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.section-card .card-header {
|
||||
padding: 0.6rem 1rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.order-num {
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: #6c757d;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2 class="mb-1">{{ friendly_name }}</h2>
|
||||
<small class="text-muted">
|
||||
<i class="bi bi-file-earmark-code me-1"></i>
|
||||
<code>{{ image_type }}/Deploy/Control/unattend.xml</code>
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-success" id="saveFormBtn">
|
||||
<i class="bi bi-floppy me-1"></i> Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<ul class="nav nav-tabs editor-tabs mb-3" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" id="form-tab" data-bs-toggle="tab"
|
||||
href="#formView" role="tab">
|
||||
<i class="bi bi-ui-checks-grid me-1"></i> Form Editor
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="raw-tab" data-bs-toggle="tab"
|
||||
href="#rawView" role="tab">
|
||||
<i class="bi bi-code-slash me-1"></i> Raw XML
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<form method="POST" id="unattendForm">
|
||||
<div class="tab-content">
|
||||
|
||||
<!-- ==================== FORM VIEW ==================== -->
|
||||
<div class="tab-pane fade show active" id="formView" role="tabpanel">
|
||||
|
||||
<!-- 1. Driver Paths -->
|
||||
<div class="card section-card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-motherboard me-1"></i> Driver Paths</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addDriverPath">
|
||||
<i class="bi bi-plus-lg"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm mb-0" id="driverPathsTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:40px">#</th>
|
||||
<th>Path</th>
|
||||
<th style="width:60px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for dp in data.driver_paths %}
|
||||
<tr>
|
||||
<td class="order-num">{{ loop.index }}</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
name="driver_path[]" value="{{ dp }}">
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if not data.driver_paths %}
|
||||
<div class="text-center text-muted py-3 empty-message" id="driverPathsEmpty">
|
||||
No driver paths configured. Click <strong>Add</strong> to add one.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Machine Settings -->
|
||||
<div class="card section-card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-pc-display me-1"></i> Machine Settings
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Computer Name</label>
|
||||
<input type="text" class="form-control" name="computer_name"
|
||||
value="{{ data.computer_name }}" placeholder="* (auto-generate)">
|
||||
<div class="form-text">Use * to let Windows auto-generate a name.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Time Zone</label>
|
||||
<input type="text" class="form-control" name="time_zone"
|
||||
value="{{ data.time_zone }}" placeholder="Eastern Standard Time">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Registered Organization</label>
|
||||
<input type="text" class="form-control" name="registered_organization"
|
||||
value="{{ data.registered_organization }}" placeholder="GE Aerospace">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Registered Owner</label>
|
||||
<input type="text" class="form-control" name="registered_owner"
|
||||
value="{{ data.registered_owner }}" placeholder="GE Aerospace">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. Specialize Commands (RunSynchronous) -->
|
||||
<div class="card section-card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-terminal me-1"></i> Specialize Commands (RunSynchronous)</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addSpecCmd">
|
||||
<i class="bi bi-plus-lg"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm mb-0 command-table" id="specCmdTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:30px"></th>
|
||||
<th style="width:50px">Order</th>
|
||||
<th>Path / Command</th>
|
||||
<th>Description</th>
|
||||
<th style="width:90px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for cmd in data.specialize_commands %}
|
||||
<tr draggable="true">
|
||||
<td class="drag-handle"><i class="bi bi-grip-vertical"></i></td>
|
||||
<td class="order-num">{{ loop.index }}</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
name="spec_cmd_path[]" value="{{ cmd.path }}">
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
name="spec_cmd_desc[]" value="{{ cmd.description }}">
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
<button type="button" class="btn btn-outline-secondary btn-row-action move-up" title="Move up">
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-row-action move-down" title="Move down">
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if not data.specialize_commands %}
|
||||
<div class="text-center text-muted py-3 empty-message" id="specCmdEmpty">
|
||||
No specialize commands configured. Click <strong>Add</strong> to add one.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. OOBE Settings -->
|
||||
<div class="card section-card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-shield-check me-1"></i> OOBE Settings
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
{% set bool_oobe_fields = [
|
||||
("HideEULAPage", "Hide EULA Page"),
|
||||
("HideOEMRegistrationScreen", "Hide OEM Registration Screen"),
|
||||
("HideOnlineAccountScreens", "Hide Online Account Screens"),
|
||||
("HideWirelessSetupInOOBE", "Hide Wireless Setup in OOBE"),
|
||||
("HideLocalAccountScreen", "Hide Local Account Screen"),
|
||||
("SkipUserOOBE", "Skip User OOBE"),
|
||||
("SkipMachineOOBE", "Skip Machine OOBE"),
|
||||
] %}
|
||||
{% for key, label in bool_oobe_fields %}
|
||||
<div class="col-md-4">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input oobe-toggle" type="checkbox"
|
||||
id="oobe_{{ key }}"
|
||||
data-field="oobe_{{ key }}"
|
||||
{% if data.oobe[key]|lower == 'true' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="oobe_{{ key }}">{{ label }}</label>
|
||||
<input type="hidden" name="oobe_{{ key }}" id="oobe_{{ key }}_val"
|
||||
value="{{ data.oobe[key] }}">
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Network Location</label>
|
||||
<select class="form-select form-select-sm" name="oobe_NetworkLocation">
|
||||
{% for opt in ["Home", "Work", "Other"] %}
|
||||
<option value="{{ opt }}" {% if data.oobe.NetworkLocation == opt %}selected{% endif %}>
|
||||
{{ opt }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">ProtectYourPC</label>
|
||||
<select class="form-select form-select-sm" name="oobe_ProtectYourPC">
|
||||
{% for opt in ["1", "2", "3"] %}
|
||||
<option value="{{ opt }}" {% if data.oobe.ProtectYourPC == opt %}selected{% endif %}>
|
||||
{{ opt }}{% if opt == "1" %} (Recommended){% elif opt == "3" %} (Skip){% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="form-text">1 = Recommended, 2 = Install only updates, 3 = Skip</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 5. First Logon Commands -->
|
||||
<div class="card section-card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-play-circle me-1"></i> First Logon Commands</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addFlCmd">
|
||||
<i class="bi bi-plus-lg"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm mb-0 command-table" id="flCmdTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:30px"></th>
|
||||
<th style="width:50px">Order</th>
|
||||
<th>Command Line</th>
|
||||
<th>Description</th>
|
||||
<th style="width:90px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for cmd in data.firstlogon_commands %}
|
||||
<tr draggable="true">
|
||||
<td class="drag-handle"><i class="bi bi-grip-vertical"></i></td>
|
||||
<td class="order-num">{{ loop.index }}</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
name="fl_cmd_commandline[]" value="{{ cmd.commandline }}">
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
name="fl_cmd_desc[]" value="{{ cmd.description }}">
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
<button type="button" class="btn btn-outline-secondary btn-row-action move-up" title="Move up">
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-row-action move-down" title="Move down">
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if not data.firstlogon_commands %}
|
||||
<div class="text-center text-muted py-3 empty-message" id="flCmdEmpty">
|
||||
No first logon commands configured. Click <strong>Add</strong> to add one.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- end formView -->
|
||||
|
||||
<!-- ==================== RAW XML VIEW ==================== -->
|
||||
<div class="tab-pane fade" id="rawView" role="tabpanel">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-code-slash me-1"></i> Raw XML</span>
|
||||
<button type="button" class="btn btn-sm btn-success" id="saveRawBtn">
|
||||
<i class="bi bi-floppy me-1"></i> Save Raw XML
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<textarea class="form-control raw-xml-editor" name="raw_xml"
|
||||
id="rawXmlEditor">{{ data.raw_xml }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- end tab-content -->
|
||||
|
||||
<input type="hidden" name="save_mode" id="saveMode" value="form">
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
// Pass the image_type and API URL to JavaScript
|
||||
window.PXE_IMAGE_TYPE = "{{ image_type }}";
|
||||
window.PXE_API_URL = "{{ url_for('api_save_unattend', image_type=image_type) }}";
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user