Shopfloor PC type system, webapp enhancements, slim Blancco GRUB
- Shopfloor PC type menu (CMM, WaxAndTrace, Keyence, Genspect, Display, Standard) - Baseline scripts: OpenText CSF, Start Menu shortcuts, network/WinRM, power/display - Standard type: eDNC + MarkZebra with 64-bit path mirroring - CMM type: Hexagon CLM Tools, PC-DMIS 2016/2019 R2 - Display sub-type: Lobby vs Dashboard - Webapp: enrollment management, image config editor, UI refresh - Upload-Image.ps1: robocopy MCL cache to PXE server - Download-Drivers.ps1: Dell driver download pipeline - Slim Blancco GRUB EFI (10MB -> 660KB) for old hardware compat - Shopfloor display imaging guide docs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
499
webapp/app.py
499
webapp/app.py
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Flask web application for managing a GE Aerospace PXE server."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
@@ -51,14 +52,15 @@ def audit(action, detail=""):
|
||||
SAMBA_SHARE = os.environ.get("SAMBA_SHARE", "/srv/samba/winpeapps")
|
||||
CLONEZILLA_SHARE = os.environ.get("CLONEZILLA_SHARE", "/srv/samba/clonezilla")
|
||||
BLANCCO_REPORTS = os.environ.get("BLANCCO_REPORTS", "/srv/samba/blancco-reports")
|
||||
ENROLLMENT_SHARE = os.environ.get("ENROLLMENT_SHARE", "/srv/samba/enrollment")
|
||||
UPLOAD_DIR = os.environ.get("UPLOAD_DIR", "/home/pxe/image-upload")
|
||||
SHARED_DIR = os.path.join(SAMBA_SHARE, "_shared")
|
||||
# Subdirs inside Deploy/ shared across ALL image types
|
||||
SHARED_DEPLOY_GLOBAL = ["Out-of-box Drivers"]
|
||||
# Subdirs inside Deploy/ shared within the same image family (by prefix)
|
||||
SHARED_DEPLOY_SCOPED = {
|
||||
"gea-": ["Operating Systems"],
|
||||
"ge-": ["Operating Systems"],
|
||||
"gea-": ["Operating Systems", "Packages"],
|
||||
"ge-": ["Operating Systems", "Packages"],
|
||||
}
|
||||
# Sibling dirs at image root shared within the same image family
|
||||
SHARED_ROOT_DIRS = {
|
||||
@@ -149,6 +151,164 @@ def unattend_path(image_type):
|
||||
return os.path.join(deploy_path(image_type), "FlatUnattendW10.xml")
|
||||
|
||||
|
||||
def control_path(image_type):
|
||||
"""Return the Deploy/Control directory for an image type."""
|
||||
return os.path.join(deploy_path(image_type), "Control")
|
||||
|
||||
|
||||
def tools_path(image_type):
|
||||
"""Return the Tools directory for an image type."""
|
||||
return os.path.join(SAMBA_SHARE, image_type, "Tools")
|
||||
|
||||
|
||||
def _load_json(filepath):
|
||||
"""Parse a JSON file and return its contents, or [] on failure."""
|
||||
try:
|
||||
with open(filepath, "r", encoding="utf-8-sig") as fh:
|
||||
return json.load(fh)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return []
|
||||
|
||||
|
||||
def _save_json(filepath, data):
|
||||
"""Write data as pretty-printed JSON."""
|
||||
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||
with open(filepath, "w", encoding="utf-8") as fh:
|
||||
json.dump(data, fh, indent=2, ensure_ascii=False)
|
||||
fh.write("\n")
|
||||
|
||||
|
||||
def _resolve_destination(dest_dir, image_type):
|
||||
"""Convert a Windows *destinationdir* path to a Linux filesystem path.
|
||||
|
||||
Replaces the ``*destinationdir*`` placeholder and backslashes, then
|
||||
prepends ``SAMBA_SHARE/image_type/`` and resolves symlinks.
|
||||
"""
|
||||
if not dest_dir:
|
||||
return ""
|
||||
# Replace the placeholder (case-insensitive)
|
||||
path = dest_dir
|
||||
lower = path.lower()
|
||||
idx = lower.find("*destinationdir*")
|
||||
if idx != -1:
|
||||
path = path[idx + len("*destinationdir*"):]
|
||||
# Backslash → forward slash, strip leading slash
|
||||
path = path.replace("\\", "/").lstrip("/")
|
||||
full = os.path.join(SAMBA_SHARE, image_type, path)
|
||||
# Resolve symlinks so shared dirs are found
|
||||
try:
|
||||
full = os.path.realpath(full)
|
||||
except OSError:
|
||||
pass
|
||||
return full
|
||||
|
||||
|
||||
def _load_image_config(image_type):
|
||||
"""Load all JSON configs for an image and check on-disk presence."""
|
||||
ctrl = control_path(image_type)
|
||||
tools = tools_path(image_type)
|
||||
|
||||
# --- Drivers (merge HardwareDriver.json + hw_drivers.json) ---
|
||||
hw_driver_file = os.path.join(ctrl, "HardwareDriver.json")
|
||||
hw_drivers_extra = os.path.join(ctrl, "hw_drivers.json")
|
||||
drivers_raw = _load_json(hw_driver_file)
|
||||
extra_raw = _load_json(hw_drivers_extra)
|
||||
|
||||
# Merge: dedup by FileName (case-insensitive)
|
||||
seen_files = set()
|
||||
drivers = []
|
||||
for d in drivers_raw + extra_raw:
|
||||
fname = (d.get("FileName") or d.get("fileName") or "").lower()
|
||||
if fname and fname in seen_files:
|
||||
continue
|
||||
if fname:
|
||||
seen_files.add(fname)
|
||||
drivers.append(d)
|
||||
|
||||
# Check disk presence for each driver
|
||||
for d in drivers:
|
||||
fname = d.get("FileName") or d.get("fileName") or ""
|
||||
dest = d.get("DestinationDir") or d.get("destinationDir") or ""
|
||||
resolved = _resolve_destination(dest, image_type)
|
||||
if resolved and fname:
|
||||
d["_on_disk"] = os.path.isfile(os.path.join(resolved, fname))
|
||||
else:
|
||||
d["_on_disk"] = False
|
||||
|
||||
# --- Operating Systems ---
|
||||
os_file = os.path.join(ctrl, "OperatingSystem.json")
|
||||
operating_systems = _load_json(os_file)
|
||||
for entry in operating_systems:
|
||||
osv = entry.get("operatingSystemVersion", {})
|
||||
wim = osv.get("wim", {})
|
||||
dest = wim.get("DestinationDir") or wim.get("destinationDir") or ""
|
||||
resolved = _resolve_destination(dest, image_type)
|
||||
if resolved:
|
||||
entry["_on_disk"] = os.path.isfile(os.path.join(resolved, "install.wim"))
|
||||
else:
|
||||
entry["_on_disk"] = False
|
||||
|
||||
# --- Packages ---
|
||||
pkg_file = os.path.join(ctrl, "packages.json")
|
||||
packages = _load_json(pkg_file)
|
||||
for p in packages:
|
||||
fname = p.get("fileName") or p.get("FileName") or ""
|
||||
dest = p.get("destinationDir") or p.get("DestinationDir") or ""
|
||||
resolved = _resolve_destination(dest, image_type)
|
||||
if resolved and fname:
|
||||
p["_on_disk"] = os.path.isfile(os.path.join(resolved, fname))
|
||||
else:
|
||||
p["_on_disk"] = False
|
||||
|
||||
# --- Hardware Models (user_selections.json) ---
|
||||
us_file = os.path.join(tools, "user_selections.json")
|
||||
us_raw = _load_json(us_file)
|
||||
us_data = us_raw[0] if us_raw and isinstance(us_raw, list) else {}
|
||||
hardware_models = us_data.get("HardwareModelSelection", [])
|
||||
os_selection = str(us_data.get("OperatingSystemSelection", ""))
|
||||
|
||||
# Build a lookup: family → driver entry (for disk-presence on models)
|
||||
family_lookup = {}
|
||||
for d in drivers:
|
||||
family = d.get("family", "")
|
||||
if family:
|
||||
family_lookup[family] = d
|
||||
|
||||
for hm in hardware_models:
|
||||
family_id = hm.get("Id", "")
|
||||
matched = family_lookup.get(family_id)
|
||||
hm["_on_disk"] = matched["_on_disk"] if matched else False
|
||||
|
||||
# --- Orphan drivers: zip files on disk not referenced in any JSON ---
|
||||
orphan_drivers = []
|
||||
oob_dir = os.path.join(deploy_path(image_type), "Out-of-box Drivers")
|
||||
# Resolve symlinks
|
||||
try:
|
||||
oob_dir = os.path.realpath(oob_dir)
|
||||
except OSError:
|
||||
pass
|
||||
registered_files = set()
|
||||
for d in drivers:
|
||||
fname = d.get("FileName") or d.get("fileName") or ""
|
||||
if fname:
|
||||
registered_files.add(fname.lower())
|
||||
if os.path.isdir(oob_dir):
|
||||
for dirpath, _dirnames, filenames in os.walk(oob_dir):
|
||||
for fn in filenames:
|
||||
if fn.lower().endswith(".zip") and fn.lower() not in registered_files:
|
||||
rel = os.path.relpath(os.path.join(dirpath, fn), oob_dir)
|
||||
orphan_drivers.append({"fileName": fn, "relPath": rel})
|
||||
|
||||
return {
|
||||
"hardware_models": hardware_models,
|
||||
"drivers": drivers,
|
||||
"operating_systems": operating_systems,
|
||||
"packages": packages,
|
||||
"orphan_drivers": orphan_drivers,
|
||||
"os_selection": os_selection,
|
||||
}
|
||||
|
||||
|
||||
def image_status(image_type):
|
||||
"""Return a dict describing the state of an image type."""
|
||||
dp = deploy_path(image_type)
|
||||
@@ -255,10 +415,11 @@ def _import_deploy(src_deploy, dst_deploy, target="", move=False):
|
||||
_replace_with_symlink(dst_item, shared_dest)
|
||||
continue
|
||||
|
||||
# Normal transfer
|
||||
if os.path.exists(dst_item):
|
||||
shutil.rmtree(dst_item)
|
||||
_transfer_tree(src_item, dst_item)
|
||||
# Normal transfer — merge to preserve existing custom content
|
||||
if os.path.isdir(dst_item):
|
||||
_merge_tree(src_item, dst_item, move=move)
|
||||
else:
|
||||
_transfer_tree(src_item, dst_item)
|
||||
|
||||
|
||||
def _replace_with_symlink(link_path, target_path):
|
||||
@@ -355,6 +516,21 @@ def parse_unattend(xml_path):
|
||||
"SkipMachineOOBE": "true",
|
||||
},
|
||||
"firstlogon_commands": [],
|
||||
"user_accounts": [],
|
||||
"autologon": {
|
||||
"enabled": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"plain_text": "true",
|
||||
"logon_count": "",
|
||||
},
|
||||
"intl": {
|
||||
"input_locale": "",
|
||||
"system_locale": "",
|
||||
"ui_language": "",
|
||||
"user_locale": "",
|
||||
},
|
||||
"oobe_timezone": "",
|
||||
"raw_xml": "",
|
||||
}
|
||||
|
||||
@@ -418,6 +594,19 @@ def parse_unattend(xml_path):
|
||||
namespaces=ns,
|
||||
):
|
||||
comp_name = comp.get("name", "")
|
||||
|
||||
# International-Core component
|
||||
if "International-Core" in comp_name:
|
||||
for tag, key in [
|
||||
("InputLocale", "input_locale"),
|
||||
("SystemLocale", "system_locale"),
|
||||
("UILanguage", "ui_language"),
|
||||
("UserLocale", "user_locale"),
|
||||
]:
|
||||
el = comp.find(qn(tag))
|
||||
if el is not None and el.text:
|
||||
data["intl"][key] = el.text.strip()
|
||||
|
||||
if "OOBE" in comp_name or "Shell-Setup" in comp_name:
|
||||
oobe_el = comp.find(qn("OOBE"))
|
||||
if oobe_el is not None:
|
||||
@@ -426,6 +615,57 @@ def parse_unattend(xml_path):
|
||||
if local in data["oobe"] and child.text:
|
||||
data["oobe"][local] = child.text.strip()
|
||||
|
||||
# UserAccounts / LocalAccounts
|
||||
ua = comp.find(qn("UserAccounts"))
|
||||
if ua is not None:
|
||||
la_container = ua.find(qn("LocalAccounts"))
|
||||
if la_container is not None:
|
||||
for acct in la_container.findall(qn("LocalAccount")):
|
||||
name_el = acct.find(qn("Name"))
|
||||
group_el = acct.find(qn("Group"))
|
||||
display_el = acct.find(qn("DisplayName"))
|
||||
pw_el = acct.find(qn("Password"))
|
||||
pw_val = ""
|
||||
pw_plain = "true"
|
||||
if pw_el is not None:
|
||||
v = pw_el.find(qn("Value"))
|
||||
p = pw_el.find(qn("PlainText"))
|
||||
if v is not None and v.text:
|
||||
pw_val = v.text.strip()
|
||||
if p is not None and p.text:
|
||||
pw_plain = p.text.strip()
|
||||
data["user_accounts"].append({
|
||||
"name": name_el.text.strip() if name_el is not None and name_el.text else "",
|
||||
"password": pw_val,
|
||||
"plain_text": pw_plain,
|
||||
"group": group_el.text.strip() if group_el is not None and group_el.text else "Administrators",
|
||||
"display_name": display_el.text.strip() if display_el is not None and display_el.text else "",
|
||||
})
|
||||
|
||||
# AutoLogon
|
||||
al = comp.find(qn("AutoLogon"))
|
||||
if al is not None:
|
||||
enabled_el = al.find(qn("Enabled"))
|
||||
user_el = al.find(qn("Username"))
|
||||
count_el = al.find(qn("LogonCount"))
|
||||
pw_el = al.find(qn("Password"))
|
||||
pw_val = ""
|
||||
pw_plain = "true"
|
||||
if pw_el is not None:
|
||||
v = pw_el.find(qn("Value"))
|
||||
p = pw_el.find(qn("PlainText"))
|
||||
if v is not None and v.text:
|
||||
pw_val = v.text.strip()
|
||||
if p is not None and p.text:
|
||||
pw_plain = p.text.strip()
|
||||
data["autologon"] = {
|
||||
"enabled": enabled_el.text.strip() if enabled_el is not None and enabled_el.text else "",
|
||||
"username": user_el.text.strip() if user_el is not None and user_el.text else "",
|
||||
"password": pw_val,
|
||||
"plain_text": pw_plain,
|
||||
"logon_count": count_el.text.strip() if count_el is not None and count_el.text else "",
|
||||
}
|
||||
|
||||
# FirstLogonCommands
|
||||
flc = comp.find(qn("FirstLogonCommands"))
|
||||
if flc is not None:
|
||||
@@ -439,6 +679,11 @@ def parse_unattend(xml_path):
|
||||
"description": desc_el.text.strip() if desc_el is not None and desc_el.text else "",
|
||||
})
|
||||
|
||||
# TimeZone (oobeSystem pass)
|
||||
tz_el = comp.find(qn("TimeZone"))
|
||||
if tz_el is not None and tz_el.text:
|
||||
data["oobe_timezone"] = tz_el.text.strip()
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -515,6 +760,28 @@ def build_unattend_xml(form_data):
|
||||
|
||||
# --- oobeSystem ---
|
||||
oobe_settings = _settings_pass(root, "oobeSystem")
|
||||
|
||||
# International-Core component (before Shell-Setup)
|
||||
intl = form_data.get("intl", {})
|
||||
if any(v.strip() for v in intl.values() if v):
|
||||
intl_comp = etree.SubElement(oobe_settings, qn("component"))
|
||||
intl_comp.set("name", "Microsoft-Windows-International-Core")
|
||||
intl_comp.set("processorArchitecture", "amd64")
|
||||
intl_comp.set("publicKeyToken", "31bf3856ad364e35")
|
||||
intl_comp.set("language", "neutral")
|
||||
intl_comp.set("versionScope", "nonSxS")
|
||||
for tag, key in [
|
||||
("InputLocale", "input_locale"),
|
||||
("SystemLocale", "system_locale"),
|
||||
("UILanguage", "ui_language"),
|
||||
("UserLocale", "user_locale"),
|
||||
]:
|
||||
val = intl.get(key, "").strip()
|
||||
if val:
|
||||
el = etree.SubElement(intl_comp, qn(tag))
|
||||
el.text = val
|
||||
|
||||
# Shell-Setup component
|
||||
oobe_comp = etree.SubElement(oobe_settings, qn("component"))
|
||||
oobe_comp.set("name", "Microsoft-Windows-Shell-Setup")
|
||||
oobe_comp.set("processorArchitecture", "amd64")
|
||||
@@ -540,6 +807,46 @@ def build_unattend_xml(form_data):
|
||||
child = etree.SubElement(oobe_el, qn(key))
|
||||
child.text = str(val)
|
||||
|
||||
# UserAccounts / LocalAccounts
|
||||
accounts = form_data.get("user_accounts", [])
|
||||
if accounts:
|
||||
ua = etree.SubElement(oobe_comp, qn("UserAccounts"))
|
||||
la_container = etree.SubElement(ua, qn("LocalAccounts"))
|
||||
for acct in accounts:
|
||||
if not acct.get("name", "").strip():
|
||||
continue
|
||||
la = etree.SubElement(la_container, qn("LocalAccount"))
|
||||
la.set(qwcm("action"), "add")
|
||||
pw = etree.SubElement(la, qn("Password"))
|
||||
pw_val = etree.SubElement(pw, qn("Value"))
|
||||
pw_val.text = acct.get("password", "")
|
||||
pw_plain = etree.SubElement(pw, qn("PlainText"))
|
||||
pw_plain.text = acct.get("plain_text", "true")
|
||||
name_el = etree.SubElement(la, qn("Name"))
|
||||
name_el.text = acct["name"].strip()
|
||||
group_el = etree.SubElement(la, qn("Group"))
|
||||
group_el.text = acct.get("group", "Administrators").strip()
|
||||
display_el = etree.SubElement(la, qn("DisplayName"))
|
||||
display_el.text = acct.get("display_name", acct["name"]).strip()
|
||||
|
||||
# AutoLogon
|
||||
autologon = form_data.get("autologon", {})
|
||||
if autologon.get("username", "").strip():
|
||||
al = etree.SubElement(oobe_comp, qn("AutoLogon"))
|
||||
al_pw = etree.SubElement(al, qn("Password"))
|
||||
al_pw_val = etree.SubElement(al_pw, qn("Value"))
|
||||
al_pw_val.text = autologon.get("password", "")
|
||||
al_pw_plain = etree.SubElement(al_pw, qn("PlainText"))
|
||||
al_pw_plain.text = autologon.get("plain_text", "true")
|
||||
al_enabled = etree.SubElement(al, qn("Enabled"))
|
||||
al_enabled.text = autologon.get("enabled", "true")
|
||||
al_user = etree.SubElement(al, qn("Username"))
|
||||
al_user.text = autologon["username"].strip()
|
||||
logon_count = autologon.get("logon_count", "").strip()
|
||||
if logon_count:
|
||||
al_count = etree.SubElement(al, qn("LogonCount"))
|
||||
al_count.text = logon_count
|
||||
|
||||
# FirstLogonCommands
|
||||
fl_cmds = form_data.get("firstlogon_commands", [])
|
||||
if fl_cmds:
|
||||
@@ -556,6 +863,12 @@ def build_unattend_xml(form_data):
|
||||
desc_el = etree.SubElement(sc, qn("Description"))
|
||||
desc_el.text = cmd.get("description", "").strip()
|
||||
|
||||
# TimeZone (oobeSystem pass)
|
||||
oobe_tz = form_data.get("oobe_timezone", "").strip()
|
||||
if oobe_tz:
|
||||
tz_el = etree.SubElement(oobe_comp, qn("TimeZone"))
|
||||
tz_el.text = oobe_tz
|
||||
|
||||
xml_bytes = etree.tostring(
|
||||
root,
|
||||
pretty_print=True,
|
||||
@@ -616,6 +929,40 @@ def _extract_form_data(form):
|
||||
"description": fl_descs[i] if i < len(fl_descs) else "",
|
||||
})
|
||||
|
||||
# User accounts
|
||||
accounts = []
|
||||
i = 0
|
||||
while form.get(f"account_name_{i}"):
|
||||
accounts.append({
|
||||
"name": form.get(f"account_name_{i}", ""),
|
||||
"password": form.get(f"account_password_{i}", ""),
|
||||
"plain_text": form.get(f"account_plaintext_{i}", "true"),
|
||||
"group": form.get(f"account_group_{i}", "Administrators"),
|
||||
"display_name": form.get(f"account_display_{i}", ""),
|
||||
})
|
||||
i += 1
|
||||
data["user_accounts"] = accounts
|
||||
|
||||
# AutoLogon
|
||||
data["autologon"] = {
|
||||
"enabled": form.get("autologon_enabled", ""),
|
||||
"username": form.get("autologon_username", ""),
|
||||
"password": form.get("autologon_password", ""),
|
||||
"plain_text": form.get("autologon_plaintext", "true"),
|
||||
"logon_count": form.get("autologon_logoncount", ""),
|
||||
}
|
||||
|
||||
# International settings
|
||||
data["intl"] = {
|
||||
"input_locale": form.get("intl_input_locale", ""),
|
||||
"system_locale": form.get("intl_system_locale", ""),
|
||||
"ui_language": form.get("intl_ui_language", ""),
|
||||
"user_locale": form.get("intl_user_locale", ""),
|
||||
}
|
||||
|
||||
# OOBE TimeZone
|
||||
data["oobe_timezone"] = form.get("oobe_timezone", "")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -799,6 +1146,74 @@ def unattend_editor(image_type):
|
||||
)
|
||||
|
||||
|
||||
@app.route("/images/<image_type>/config")
|
||||
def image_config(image_type):
|
||||
if image_type not in IMAGE_TYPES:
|
||||
flash("Unknown image type.", "danger")
|
||||
return redirect(url_for("dashboard"))
|
||||
|
||||
config = _load_image_config(image_type)
|
||||
return render_template(
|
||||
"image_config.html",
|
||||
image_type=image_type,
|
||||
friendly_name=FRIENDLY_NAMES.get(image_type, image_type),
|
||||
config=config,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/images/<image_type>/config/save", methods=["POST"])
|
||||
def image_config_save(image_type):
|
||||
if image_type not in IMAGE_TYPES:
|
||||
flash("Unknown image type.", "danger")
|
||||
return redirect(url_for("dashboard"))
|
||||
|
||||
section = request.form.get("section", "")
|
||||
payload = request.form.get("payload", "[]")
|
||||
try:
|
||||
data = json.loads(payload)
|
||||
except json.JSONDecodeError:
|
||||
flash("Invalid JSON payload.", "danger")
|
||||
return redirect(url_for("image_config", image_type=image_type))
|
||||
|
||||
ctrl = control_path(image_type)
|
||||
tools = tools_path(image_type)
|
||||
|
||||
try:
|
||||
if section == "hardware_models":
|
||||
us_file = os.path.join(tools, "user_selections.json")
|
||||
us_raw = _load_json(us_file)
|
||||
us_data = us_raw[0] if us_raw and isinstance(us_raw, list) else {}
|
||||
us_data["HardwareModelSelection"] = data
|
||||
_save_json(us_file, [us_data])
|
||||
audit("CONFIG_SAVE", f"{image_type}/hardware_models")
|
||||
|
||||
elif section == "drivers":
|
||||
# Strip internal fields before saving
|
||||
clean = [{k: v for k, v in d.items() if not k.startswith("_")} for d in data]
|
||||
_save_json(os.path.join(ctrl, "HardwareDriver.json"), clean)
|
||||
audit("CONFIG_SAVE", f"{image_type}/drivers")
|
||||
|
||||
elif section == "operating_systems":
|
||||
clean = [{k: v for k, v in d.items() if not k.startswith("_")} for d in data]
|
||||
_save_json(os.path.join(ctrl, "OperatingSystem.json"), clean)
|
||||
audit("CONFIG_SAVE", f"{image_type}/operating_systems")
|
||||
|
||||
elif section == "packages":
|
||||
clean = [{k: v for k, v in d.items() if not k.startswith("_")} for d in data]
|
||||
_save_json(os.path.join(ctrl, "packages.json"), clean)
|
||||
audit("CONFIG_SAVE", f"{image_type}/packages")
|
||||
|
||||
else:
|
||||
flash(f"Unknown section: {section}", "danger")
|
||||
return redirect(url_for("image_config", image_type=image_type))
|
||||
|
||||
flash(f"Saved {section.replace('_', ' ')} successfully.", "success")
|
||||
except Exception as exc:
|
||||
flash(f"Failed to save {section}: {exc}", "danger")
|
||||
|
||||
return redirect(url_for("image_config", image_type=image_type))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Routes — Clonezilla Backups
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -922,6 +1337,78 @@ def blancco_delete_report(filename):
|
||||
return redirect(url_for("blancco_reports"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Routes — Enrollment Packages
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@app.route("/enrollment")
|
||||
def enrollment():
|
||||
packages = []
|
||||
if os.path.isdir(ENROLLMENT_SHARE):
|
||||
for f in sorted(os.listdir(ENROLLMENT_SHARE)):
|
||||
fpath = os.path.join(ENROLLMENT_SHARE, f)
|
||||
if os.path.isfile(fpath) and f.lower().endswith(".ppkg"):
|
||||
stat = os.stat(fpath)
|
||||
packages.append({
|
||||
"filename": f,
|
||||
"size": stat.st_size,
|
||||
"modified": stat.st_mtime,
|
||||
})
|
||||
return render_template(
|
||||
"enrollment.html",
|
||||
packages=packages,
|
||||
image_types=IMAGE_TYPES,
|
||||
friendly_names=FRIENDLY_NAMES,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/enrollment/upload", methods=["POST"])
|
||||
def enrollment_upload():
|
||||
if "ppkg_file" not in request.files:
|
||||
flash("No file selected.", "danger")
|
||||
return redirect(url_for("enrollment"))
|
||||
|
||||
f = request.files["ppkg_file"]
|
||||
if not f.filename:
|
||||
flash("No file selected.", "danger")
|
||||
return redirect(url_for("enrollment"))
|
||||
|
||||
filename = secure_filename(f.filename)
|
||||
if not filename.lower().endswith(".ppkg"):
|
||||
flash("Only .ppkg files are accepted.", "danger")
|
||||
return redirect(url_for("enrollment"))
|
||||
|
||||
os.makedirs(ENROLLMENT_SHARE, exist_ok=True)
|
||||
dest = os.path.join(ENROLLMENT_SHARE, filename)
|
||||
f.save(dest)
|
||||
audit("ENROLLMENT_UPLOAD", filename)
|
||||
flash(f"Uploaded {filename} successfully.", "success")
|
||||
return redirect(url_for("enrollment"))
|
||||
|
||||
|
||||
@app.route("/enrollment/download/<filename>")
|
||||
def enrollment_download(filename):
|
||||
filename = secure_filename(filename)
|
||||
fpath = os.path.join(ENROLLMENT_SHARE, filename)
|
||||
if not os.path.isfile(fpath):
|
||||
flash(f"Package not found: {filename}", "danger")
|
||||
return redirect(url_for("enrollment"))
|
||||
return send_file(fpath, as_attachment=True)
|
||||
|
||||
|
||||
@app.route("/enrollment/delete/<filename>", methods=["POST"])
|
||||
def enrollment_delete(filename):
|
||||
filename = secure_filename(filename)
|
||||
fpath = os.path.join(ENROLLMENT_SHARE, filename)
|
||||
if os.path.isfile(fpath):
|
||||
os.remove(fpath)
|
||||
audit("ENROLLMENT_DELETE", filename)
|
||||
flash(f"Deleted {filename}.", "success")
|
||||
else:
|
||||
flash(f"Package not found: {filename}", "danger")
|
||||
return redirect(url_for("enrollment"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Routes — startnet.cmd Editor (WIM)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -150,6 +150,71 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Add User Account
|
||||
// -----------------------------------------------------------------------
|
||||
var addUserAccountBtn = document.getElementById('addUserAccount');
|
||||
if (addUserAccountBtn) {
|
||||
addUserAccountBtn.addEventListener('click', function () {
|
||||
var tbody = document.querySelector('#userAccountsTable tbody');
|
||||
var idx = tbody.querySelectorAll('tr').length;
|
||||
var tr = document.createElement('tr');
|
||||
tr.innerHTML =
|
||||
'<td class="order-num">' + (idx + 1) + '</td>' +
|
||||
'<td><input type="text" class="form-control form-control-sm" name="account_name_' + idx + '" value="" placeholder="Username"></td>' +
|
||||
'<td><input type="text" class="form-control form-control-sm" name="account_password_' + idx + '" value="" placeholder="Password"></td>' +
|
||||
'<td><input type="text" class="form-control form-control-sm" name="account_group_' + idx + '" value="Administrators"></td>' +
|
||||
'<td><input type="text" class="form-control form-control-sm" name="account_display_' + idx + '" value="" placeholder="Display Name"></td>' +
|
||||
'<td>' +
|
||||
'<input type="hidden" name="account_plaintext_' + idx + '" value="true">' +
|
||||
'<button type="button" class="btn btn-outline-danger btn-row-action remove-account-row">Remove</button>' +
|
||||
'</td>';
|
||||
tbody.appendChild(tr);
|
||||
var emptyEl = document.getElementById('userAccountsEmpty');
|
||||
if (emptyEl) emptyEl.style.display = 'none';
|
||||
tr.querySelector('input[name^="account_name_"]').focus();
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Remove User Account row + renumber indices (delegated)
|
||||
// -----------------------------------------------------------------------
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('.remove-account-row');
|
||||
if (!btn) return;
|
||||
var row = btn.closest('tr');
|
||||
var tbody = row.parentElement;
|
||||
row.remove();
|
||||
// Renumber order and field name indices
|
||||
var rows = tbody.querySelectorAll('tr');
|
||||
rows.forEach(function (r, i) {
|
||||
var orderCell = r.querySelector('.order-num');
|
||||
if (orderCell) orderCell.textContent = i + 1;
|
||||
// Rename inputs to match new index
|
||||
r.querySelectorAll('input').forEach(function (inp) {
|
||||
var name = inp.getAttribute('name');
|
||||
if (name) {
|
||||
inp.setAttribute('name', name.replace(/_\d+$/, '_' + i));
|
||||
}
|
||||
});
|
||||
});
|
||||
var emptyEl = document.getElementById('userAccountsEmpty');
|
||||
if (emptyEl) emptyEl.style.display = rows.length > 0 ? 'none' : '';
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// AutoLogon Enabled toggle — keep hidden input in sync
|
||||
// -----------------------------------------------------------------------
|
||||
var autologonToggle = document.getElementById('autologonEnabledToggle');
|
||||
if (autologonToggle) {
|
||||
autologonToggle.addEventListener('change', function () {
|
||||
var hidden = document.getElementById('autologon_enabled_val');
|
||||
if (hidden) {
|
||||
hidden.value = this.checked ? 'true' : 'false';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// OOBE toggle switches — keep hidden input in sync
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -298,5 +363,107 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
toggleEmpty('driverPathsTable', 'driverPathsEmpty');
|
||||
toggleEmpty('specCmdTable', 'specCmdEmpty');
|
||||
toggleEmpty('flCmdTable', 'flCmdEmpty');
|
||||
toggleEmpty('userAccountsTable', 'userAccountsEmpty');
|
||||
|
||||
// =======================================================================
|
||||
// Image Configuration Editor handlers
|
||||
// =======================================================================
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Generic: collect table rows into JSON and submit form
|
||||
// -----------------------------------------------------------------------
|
||||
function collectAndSubmit(tableId, hiddenId, formId, extractor) {
|
||||
var tbody = document.querySelector('#' + tableId + ' tbody');
|
||||
if (!tbody) return;
|
||||
var rows = tbody.querySelectorAll('tr');
|
||||
var data = [];
|
||||
rows.forEach(function (row) {
|
||||
var item = extractor(row);
|
||||
if (item) data.push(item);
|
||||
});
|
||||
document.getElementById(hiddenId).value = JSON.stringify(data);
|
||||
document.getElementById(formId).submit();
|
||||
}
|
||||
|
||||
// Helper: extract JSON from data-json attr, stripping internal _fields
|
||||
function extractDataJson(row) {
|
||||
var raw = row.getAttribute('data-json');
|
||||
if (!raw) return null;
|
||||
try {
|
||||
var obj = JSON.parse(raw);
|
||||
Object.keys(obj).forEach(function (k) {
|
||||
if (k.charAt(0) === '_') delete obj[k];
|
||||
});
|
||||
return obj;
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Save: Hardware Models
|
||||
// -----------------------------------------------------------------------
|
||||
var saveHwModelsBtn = document.getElementById('saveHwModels');
|
||||
if (saveHwModelsBtn) {
|
||||
saveHwModelsBtn.addEventListener('click', function () {
|
||||
collectAndSubmit('hwModelsTable', 'hwModelsData', 'hwModelsForm', function (row) {
|
||||
var m = row.querySelector('[data-field="Model"]');
|
||||
var f = row.querySelector('[data-field="Id"]');
|
||||
if (!m || !f) return null;
|
||||
return { Model: m.value, Id: f.value };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Save: Drivers
|
||||
// -----------------------------------------------------------------------
|
||||
var saveDriversBtn = document.getElementById('saveDrivers');
|
||||
if (saveDriversBtn) {
|
||||
saveDriversBtn.addEventListener('click', function () {
|
||||
collectAndSubmit('driversTable', 'driversData', 'driversForm', extractDataJson);
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Save: Operating Systems
|
||||
// -----------------------------------------------------------------------
|
||||
var saveOsBtn = document.getElementById('saveOs');
|
||||
if (saveOsBtn) {
|
||||
saveOsBtn.addEventListener('click', function () {
|
||||
collectAndSubmit('osTable', 'osData', 'osForm', extractDataJson);
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Save: Packages
|
||||
// -----------------------------------------------------------------------
|
||||
var savePackagesBtn = document.getElementById('savePackages');
|
||||
if (savePackagesBtn) {
|
||||
savePackagesBtn.addEventListener('click', function () {
|
||||
collectAndSubmit('packagesTable', 'packagesData', 'packagesForm', extractDataJson);
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Add Hardware Model row
|
||||
// -----------------------------------------------------------------------
|
||||
var addHwModelBtn = document.getElementById('addHwModel');
|
||||
if (addHwModelBtn) {
|
||||
addHwModelBtn.addEventListener('click', function () {
|
||||
var tbody = document.querySelector('#hwModelsTable tbody');
|
||||
var idx = tbody.querySelectorAll('tr').length + 1;
|
||||
var tr = document.createElement('tr');
|
||||
tr.innerHTML =
|
||||
'<td class="order-num">' + idx + '</td>' +
|
||||
'<td><input type="text" class="form-control form-control-sm" data-field="Model" value="" placeholder="Model name"></td>' +
|
||||
'<td><input type="text" class="form-control form-control-sm" data-field="Id" value="" placeholder="Driver Family ID"></td>' +
|
||||
'<td><span class="badge bg-secondary badge-disk">New</span></td>' +
|
||||
'<td><button type="button" class="btn btn-outline-danger btn-row-action remove-row"><i class="bi bi-trash"></i></button></td>';
|
||||
tbody.appendChild(tr);
|
||||
var empty = document.getElementById('hwModelsEmpty');
|
||||
if (empty) empty.style.display = 'none';
|
||||
tr.querySelector('input').focus();
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="mb-0"><i class="bi bi-journal-text me-2"></i>Audit Log</h2>
|
||||
<h2 class="mb-0">Audit Log</h2>
|
||||
<span class="badge bg-secondary fs-6">{{ entries|length }} entries</span>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
<i class="bi bi-clock-history me-2"></i> Activity History
|
||||
Activity History
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if entries %}
|
||||
@@ -52,7 +52,6 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="bi bi-journal-text" style="font-size: 3rem;"></i>
|
||||
<p class="mt-2">No audit log entries yet.</p>
|
||||
<p class="small">Actions like image imports, unattend edits, and backup operations will be logged here.</p>
|
||||
</div>
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="mb-0"><i class="bi bi-archive me-2"></i>Clonezilla Backups</h2>
|
||||
<h2 class="mb-0">Clonezilla Backups</h2>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadModal">
|
||||
<i class="bi bi-upload me-1"></i> Upload Backup
|
||||
Upload Backup
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
<i class="bi bi-hdd-stack me-2"></i> Machine Backups
|
||||
Machine Backups
|
||||
<span class="badge bg-secondary ms-2">{{ backups|length }}</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@@ -36,12 +36,12 @@
|
||||
<td class="text-end text-nowrap">
|
||||
<a href="{{ url_for('clonezilla_download', filename=b.filename) }}"
|
||||
class="btn btn-sm btn-outline-primary" title="Download">
|
||||
<i class="bi bi-download"></i>
|
||||
Download
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||
data-filename="{{ b.filename }}" data-machine="{{ b.machine }}" title="Delete">
|
||||
<i class="bi bi-trash"></i>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -50,7 +50,6 @@
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="bi bi-archive" style="font-size: 3rem;"></i>
|
||||
<p class="mt-2">No backups found. Upload a Clonezilla backup .zip to get started.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -59,7 +58,7 @@
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title"><i class="bi bi-info-circle me-1"></i> Backup Naming Convention</h6>
|
||||
<h6 class="card-title">Backup Naming Convention</h6>
|
||||
<p class="card-text mb-0">
|
||||
Name backup files with the machine number (e.g., <code>6501.zip</code>).
|
||||
The Samba share <code>\\pxe-server\clonezilla</code> is also available on the network for direct Clonezilla save/restore operations.
|
||||
@@ -74,7 +73,7 @@
|
||||
<form action="{{ url_for('clonezilla_upload') }}" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-upload me-2"></i>Upload Backup</h5>
|
||||
<h5 class="modal-title">Upload Backup</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -89,7 +88,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-upload me-1"></i> Upload</button>
|
||||
<button type="submit" class="btn btn-primary">Upload</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -103,7 +102,7 @@
|
||||
<form id="deleteForm" method="post">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-exclamation-triangle me-2 text-danger"></i>Confirm Delete</h5>
|
||||
<h5 class="modal-title">Confirm Delete</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -112,7 +111,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-danger"><i class="bi bi-trash me-1"></i> Delete</button>
|
||||
<button type="submit" class="btn btn-danger">Delete</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<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">
|
||||
<style>
|
||||
:root {
|
||||
--sidebar-width: 280px;
|
||||
@@ -36,10 +35,6 @@
|
||||
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;
|
||||
@@ -59,10 +54,6 @@
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
.sidebar .brand .bi {
|
||||
font-size: 1.3rem;
|
||||
color: #0d6efd;
|
||||
}
|
||||
.main-content {
|
||||
margin-left: var(--sidebar-width);
|
||||
padding: 2rem;
|
||||
@@ -121,13 +112,13 @@
|
||||
<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
|
||||
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
|
||||
Image Import
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -138,25 +129,31 @@
|
||||
<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
|
||||
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-archive"></i> 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') }}">
|
||||
<i class="bi bi-shield-check"></i> 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') }}">
|
||||
<i class="bi bi-journal-text"></i> Audit Log
|
||||
Audit Log
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -168,7 +165,13 @@
|
||||
<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] }}
|
||||
{{ 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 %}
|
||||
@@ -181,7 +184,13 @@
|
||||
<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] }}
|
||||
{{ 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 %}
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
<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
|
||||
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
|
||||
PXE Services
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0">
|
||||
@@ -27,7 +27,6 @@
|
||||
{% for svc in services %}
|
||||
<tr>
|
||||
<td>
|
||||
<i class="bi bi-server me-1 text-muted"></i>
|
||||
<strong>{{ svc.name }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
@@ -45,7 +44,7 @@
|
||||
<!-- Images -->
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
<i class="bi bi-disc me-2"></i> Deployment Images
|
||||
Deployment Images
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0">
|
||||
@@ -67,23 +66,27 @@
|
||||
</td>
|
||||
<td>
|
||||
{% if img.has_content %}
|
||||
<span class="badge bg-success"><i class="bi bi-check-circle"></i> Present</span>
|
||||
<span class="badge bg-success">Present</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary"><i class="bi bi-x-circle"></i> Empty</span>
|
||||
<span class="badge bg-secondary">Empty</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if img.has_unattend %}
|
||||
<span class="badge bg-success"><i class="bi bi-check-circle"></i> Exists</span>
|
||||
<span class="badge bg-success">Exists</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning text-dark"><i class="bi bi-exclamation-triangle"></i> Missing</span>
|
||||
<span class="badge bg-warning text-dark">Missing</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><code class="small">{{ img.deploy_path }}</code></td>
|
||||
<td class="text-end">
|
||||
<a href="{{ url_for('image_config', image_type=img.image_type) }}"
|
||||
class="btn btn-sm btn-outline-secondary me-1">
|
||||
Config
|
||||
</a>
|
||||
<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
|
||||
Edit Unattend
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
135
webapp/templates/enrollment.html
Normal file
135
webapp/templates/enrollment.html
Normal file
@@ -0,0 +1,135 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Enrollment Packages - PXE Server Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="mb-0">Enrollment Packages</h2>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadModal">
|
||||
Upload Package
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
GCCH Provisioning Packages
|
||||
<span class="badge bg-secondary ms-2">{{ packages|length }}</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if packages %}
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Filename</th>
|
||||
<th>Size</th>
|
||||
<th>Last Modified</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in packages %}
|
||||
<tr>
|
||||
<td><code>{{ p.filename }}</code></td>
|
||||
<td>{{ "%.1f"|format(p.size / 1048576) }} MB</td>
|
||||
<td>{{ p.modified | timestamp_fmt }}</td>
|
||||
<td class="text-end text-nowrap">
|
||||
<a href="{{ url_for('enrollment_download', filename=p.filename) }}"
|
||||
class="btn btn-sm btn-outline-primary" title="Download">
|
||||
Download
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||
data-filename="{{ p.filename }}" title="Delete">
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-5">
|
||||
<p class="mt-2">No enrollment packages found. Upload a <code>.ppkg</code> file to get started.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">About Enrollment Packages</h6>
|
||||
<p class="card-text">
|
||||
GCCH enrollment <code>.ppkg</code> provisioning packages are copied to
|
||||
<code>C:\Enrollment\</code> on the target machine after imaging.
|
||||
At OOBE, connect to a network with internet, press <strong>Windows key 5 times</strong>,
|
||||
then browse to <code>C:\Enrollment\</code> and select the package. No USB stick needed.
|
||||
</p>
|
||||
<p class="card-text mb-0">
|
||||
<strong>Naming convention:</strong> Use <code>with-office.ppkg</code> and
|
||||
<code>without-office.ppkg</code> to match the WinPE enrollment menu options.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Modal -->
|
||||
<div class="modal fade" id="uploadModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form action="{{ url_for('enrollment_upload') }}" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Upload Enrollment Package</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="ppkgFile" class="form-label">Provisioning Package (.ppkg)</label>
|
||||
<input type="file" class="form-control" id="ppkgFile" name="ppkg_file"
|
||||
accept=".ppkg" required>
|
||||
<div class="form-text">
|
||||
Use <code>with-office.ppkg</code> or <code>without-office.ppkg</code> to match the WinPE boot menu.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Upload</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form id="deleteForm" method="post">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Confirm Delete</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete <strong id="deleteFilename"></strong>?</p>
|
||||
<p class="text-muted mb-0">This action cannot be undone.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-danger">Delete</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
document.getElementById('deleteModal').addEventListener('show.bs.modal', function (event) {
|
||||
var btn = event.relatedTarget;
|
||||
var filename = btn.getAttribute('data-filename');
|
||||
document.getElementById('deleteFilename').textContent = filename;
|
||||
document.getElementById('deleteForm').action = '/enrollment/delete/' + encodeURIComponent(filename);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
331
webapp/templates/image_config.html
Normal file
331
webapp/templates/image_config.html
Normal file
@@ -0,0 +1,331 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ friendly_name }} - Configuration{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
.section-card { margin-bottom: 1.5rem; }
|
||||
.section-card .card-header { padding: 0.6rem 1rem; font-size: 0.95rem; }
|
||||
.badge-disk { font-size: 0.75rem; }
|
||||
.orphan-section { background-color: #fff8e1; }
|
||||
.config-table td, .config-table th { vertical-align: middle; }
|
||||
.config-table .form-control-sm { min-width: 120px; }
|
||||
.text-truncate-cell { max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
</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">
|
||||
Image Configuration
|
||||
— OS Selection: <strong>{{ config.os_selection or 'Not set' }}</strong>
|
||||
</small>
|
||||
</div>
|
||||
<a href="{{ url_for('unattend_editor', image_type=image_type) }}" class="btn btn-outline-secondary btn-sm">
|
||||
Edit Unattend
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# ==================== SECTION 1: Hardware Models ==================== #}
|
||||
<div class="card section-card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>Hardware Models
|
||||
<span class="badge bg-secondary ms-1">{{ config.hardware_models|length }}</span>
|
||||
</span>
|
||||
<div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addHwModel">
|
||||
Add
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-success ms-1" id="saveHwModels">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<form method="POST" action="{{ url_for('image_config_save', image_type=image_type) }}" id="hwModelsForm">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="section" value="hardware_models">
|
||||
<input type="hidden" name="payload" id="hwModelsData" value="[]">
|
||||
</form>
|
||||
<table class="table table-sm table-hover mb-0 config-table" id="hwModelsTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:40px">#</th>
|
||||
<th>Model</th>
|
||||
<th>Driver Family ID</th>
|
||||
<th style="width:90px">On Disk</th>
|
||||
<th style="width:60px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for hm in config.hardware_models %}
|
||||
<tr>
|
||||
<td class="order-num">{{ loop.index }}</td>
|
||||
<td><input type="text" class="form-control form-control-sm" data-field="Model" value="{{ hm.Model }}"></td>
|
||||
<td><input type="text" class="form-control form-control-sm" data-field="Id" value="{{ hm.Id }}"></td>
|
||||
<td>
|
||||
{% if hm._on_disk %}
|
||||
<span class="badge bg-success badge-disk">Yes</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger badge-disk">No</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if not config.hardware_models %}
|
||||
<div class="text-center text-muted py-3 empty-message" id="hwModelsEmpty">
|
||||
No hardware models configured.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ==================== SECTION 2: Driver Packs ==================== #}
|
||||
<div class="card section-card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>Driver Packs
|
||||
<span class="badge bg-secondary ms-1">{{ config.drivers|length }}</span>
|
||||
</span>
|
||||
<div>
|
||||
<button type="button" class="btn btn-sm btn-success" id="saveDrivers">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<form method="POST" action="{{ url_for('image_config_save', image_type=image_type) }}" id="driversForm">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="section" value="drivers">
|
||||
<input type="hidden" name="payload" id="driversData" value="[]">
|
||||
</form>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0 config-table" id="driversTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:40px">#</th>
|
||||
<th>Family</th>
|
||||
<th>Models</th>
|
||||
<th>File Name</th>
|
||||
<th style="width:70px">OS IDs</th>
|
||||
<th style="width:90px">On Disk</th>
|
||||
<th style="width:60px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for drv in config.drivers %}
|
||||
<tr data-json='{{ drv | tojson }}'>
|
||||
<td class="order-num">{{ loop.index }}</td>
|
||||
<td class="text-truncate-cell" title="{{ drv.family }}">{{ drv.family }}</td>
|
||||
<td class="text-truncate-cell" title="{{ drv.models }}">{{ drv.models }}</td>
|
||||
<td class="text-truncate-cell" title="{{ drv.FileName or drv.get('fileName','') }}">
|
||||
<small>{{ drv.FileName or drv.get('fileName','') }}</small>
|
||||
</td>
|
||||
<td><small>{{ drv.osId }}</small></td>
|
||||
<td>
|
||||
{% if drv._on_disk %}
|
||||
<span class="badge bg-success badge-disk">Yes</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger badge-disk">No</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if not config.drivers %}
|
||||
<div class="text-center text-muted py-3 empty-message" id="driversEmpty">
|
||||
No driver packs configured.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Orphan drivers sub-section #}
|
||||
{% if config.orphan_drivers %}
|
||||
<div class="card-footer orphan-section p-0">
|
||||
<div class="px-3 py-2">
|
||||
<a class="text-decoration-none" data-bs-toggle="collapse" href="#orphanDrivers" role="button">
|
||||
<strong>Unregistered Drivers ({{ config.orphan_drivers|length }})</strong>
|
||||
<small class="text-muted ms-1">zip files on disk not in any JSON</small>
|
||||
</a>
|
||||
</div>
|
||||
<div class="collapse" id="orphanDrivers">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>File Name</th>
|
||||
<th>Relative Path</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for orph in config.orphan_drivers %}
|
||||
<tr>
|
||||
<td><small>{{ orph.fileName }}</small></td>
|
||||
<td><small class="text-muted">{{ orph.relPath }}</small></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# ==================== SECTION 3: Operating Systems ==================== #}
|
||||
<div class="card section-card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>Operating Systems
|
||||
<span class="badge bg-secondary ms-1">{{ config.operating_systems|length }}</span>
|
||||
</span>
|
||||
<div>
|
||||
<button type="button" class="btn btn-sm btn-success" id="saveOs">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<form method="POST" action="{{ url_for('image_config_save', image_type=image_type) }}" id="osForm">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="section" value="operating_systems">
|
||||
<input type="hidden" name="payload" id="osData" value="[]">
|
||||
</form>
|
||||
<table class="table table-sm table-hover mb-0 config-table" id="osTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:40px">#</th>
|
||||
<th>Product Name</th>
|
||||
<th>Version</th>
|
||||
<th>Build</th>
|
||||
<th style="width:60px">ID</th>
|
||||
<th style="width:70px">Active</th>
|
||||
<th style="width:90px">WIM On Disk</th>
|
||||
<th style="width:60px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in config.operating_systems %}
|
||||
{% set osv = entry.operatingSystemVersion %}
|
||||
<tr data-json='{{ entry | tojson }}'>
|
||||
<td class="order-num">{{ loop.index }}</td>
|
||||
<td>{{ osv.productName }}</td>
|
||||
<td>{{ osv.versionNumber }}</td>
|
||||
<td>{{ osv.buildNumber }}</td>
|
||||
<td>{{ osv.id }}</td>
|
||||
<td>
|
||||
{% if osv.isActive %}
|
||||
<span class="badge bg-success badge-disk">Active</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary badge-disk">Inactive</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if entry._on_disk %}
|
||||
<span class="badge bg-success badge-disk">Yes</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger badge-disk">No</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if not config.operating_systems %}
|
||||
<div class="text-center text-muted py-3 empty-message" id="osEmpty">
|
||||
No operating systems configured.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ==================== SECTION 4: Packages ==================== #}
|
||||
<div class="card section-card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>Packages
|
||||
<span class="badge bg-secondary ms-1">{{ config.packages|length }}</span>
|
||||
</span>
|
||||
<div>
|
||||
<button type="button" class="btn btn-sm btn-success" id="savePackages">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<form method="POST" action="{{ url_for('image_config_save', image_type=image_type) }}" id="packagesForm">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="section" value="packages">
|
||||
<input type="hidden" name="payload" id="packagesData" value="[]">
|
||||
</form>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0 config-table" id="packagesTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:40px">#</th>
|
||||
<th>Name</th>
|
||||
<th>Comment</th>
|
||||
<th>File</th>
|
||||
<th style="width:70px">OS IDs</th>
|
||||
<th style="width:80px">Enabled</th>
|
||||
<th style="width:90px">On Disk</th>
|
||||
<th style="width:60px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for pkg in config.packages %}
|
||||
<tr data-json='{{ pkg | tojson }}'>
|
||||
<td class="order-num">{{ loop.index }}</td>
|
||||
<td class="text-truncate-cell" title="{{ pkg.name }}"><small>{{ pkg.name }}</small></td>
|
||||
<td class="text-truncate-cell" title="{{ pkg.comment }}"><small>{{ pkg.comment }}</small></td>
|
||||
<td class="text-truncate-cell" title="{{ pkg.fileName or pkg.get('FileName','') }}">
|
||||
<small>{{ pkg.fileName or pkg.get('FileName','') }}</small>
|
||||
</td>
|
||||
<td><small>{{ pkg.osId }}</small></td>
|
||||
<td>
|
||||
{% if pkg.enabled %}
|
||||
<span class="badge bg-success badge-disk">Yes</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary badge-disk">No</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if pkg._on_disk %}
|
||||
<span class="badge bg-success badge-disk">Yes</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger badge-disk">No</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if not config.packages %}
|
||||
<div class="text-center text-muted py-3 empty-message" id="packagesEmpty">
|
||||
No packages configured.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -10,7 +10,7 @@
|
||||
<!-- Network Upload Import -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-cloud-upload me-2"></i> Import from Network Upload
|
||||
Import from Network Upload
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if upload_sources %}
|
||||
@@ -44,7 +44,6 @@
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info d-flex align-items-start" role="alert">
|
||||
<i class="bi bi-info-circle-fill me-2 mt-1"></i>
|
||||
<div>
|
||||
<strong>Shared Drivers:</strong> Out-of-box Drivers are automatically pooled
|
||||
into a shared directory and symlinked for each image type to save disk space.
|
||||
@@ -52,7 +51,6 @@
|
||||
</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
|
||||
@@ -61,19 +59,18 @@
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" id="uploadImportBtn">
|
||||
<i class="bi bi-download me-1"></i> Start Import
|
||||
Start Import
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="text-center py-4">
|
||||
<i class="bi bi-cloud-slash display-4 text-muted"></i>
|
||||
<h5 class="mt-3 text-muted">No Upload Content Found</h5>
|
||||
<p class="text-muted mb-0">
|
||||
Map <code>\\10.9.100.1\image-upload</code> on your Windows PC and copy
|
||||
the Deploy directory contents there.
|
||||
</p>
|
||||
<button class="btn btn-outline-secondary btn-sm mt-3" onclick="location.reload()">
|
||||
<i class="bi bi-arrow-clockwise"></i> Refresh
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -83,7 +80,7 @@
|
||||
<!-- USB Import -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-usb-drive me-2"></i> Import from USB Drive
|
||||
Import from USB Drive
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if usb_mounts %}
|
||||
@@ -116,7 +113,6 @@
|
||||
</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
|
||||
@@ -125,19 +121,18 @@
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" id="importBtn">
|
||||
<i class="bi bi-download me-1"></i> Start Import
|
||||
Start Import
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="text-center py-4">
|
||||
<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
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -148,7 +143,7 @@
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-info-circle me-2"></i> Current Image Status
|
||||
Current Image Status
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm mb-0">
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="mb-0"><i class="bi bi-file-earmark-check me-2"></i>Blancco Erasure Reports</h2>
|
||||
<h2 class="mb-0">Blancco Erasure Reports</h2>
|
||||
<span class="badge bg-secondary fs-6">{{ reports|length }} report{{ 's' if reports|length != 1 }}</span>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
<i class="bi bi-shield-check me-2"></i> Drive Erasure Certificates
|
||||
Drive Erasure Certificates
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if reports %}
|
||||
@@ -39,12 +39,12 @@
|
||||
<td class="text-end text-nowrap">
|
||||
<a href="{{ url_for('blancco_download_report', filename=r.filename) }}"
|
||||
class="btn btn-sm btn-outline-primary" title="Download">
|
||||
<i class="bi bi-download"></i>
|
||||
Download
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||
data-filename="{{ r.filename }}" title="Delete">
|
||||
<i class="bi bi-trash"></i>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -53,7 +53,6 @@
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="bi bi-shield-check" style="font-size: 3rem;"></i>
|
||||
<p class="mt-2">No erasure reports yet.</p>
|
||||
<p class="small">Reports will appear here after Blancco Drive Eraser completes a wipe.</p>
|
||||
</div>
|
||||
@@ -63,7 +62,7 @@
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title"><i class="bi bi-info-circle me-1"></i> Report Storage</h6>
|
||||
<h6 class="card-title">Report Storage</h6>
|
||||
<p class="card-text mb-1">
|
||||
Blancco Drive Eraser saves erasure certificates to the network share
|
||||
<code>\\10.9.100.1\blancco-reports</code>.
|
||||
@@ -81,7 +80,7 @@
|
||||
<form id="deleteForm" method="post">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-exclamation-triangle me-2 text-danger"></i>Confirm Delete</h5>
|
||||
<h5 class="modal-title">Confirm Delete</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -90,7 +89,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-danger"><i class="bi bi-trash me-1"></i> Delete</button>
|
||||
<button type="submit" class="btn btn-danger">Delete</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -34,12 +34,11 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="mb-0"><i class="bi bi-terminal me-2"></i>startnet.cmd Editor</h2>
|
||||
<h2 class="mb-0">startnet.cmd Editor</h2>
|
||||
</div>
|
||||
|
||||
{% if not wim_exists %}
|
||||
<div class="alert alert-warning">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
<strong>boot.wim not found</strong> at <code>{{ wim_path }}</code>.
|
||||
Run the PXE server setup playbook and import WinPE boot files first.
|
||||
</div>
|
||||
@@ -49,7 +48,7 @@
|
||||
<div class="col-lg-9">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-file-earmark-code me-2"></i>Windows\System32\startnet.cmd</span>
|
||||
<span>Windows\System32\startnet.cmd</span>
|
||||
<span class="badge bg-secondary">boot.wim</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@@ -64,14 +63,14 @@
|
||||
Editing the startnet.cmd inside <code>{{ wim_path }}</code>
|
||||
</small>
|
||||
<button type="submit" form="startnetForm" class="btn btn-primary">
|
||||
<i class="bi bi-floppy me-1"></i> Save to boot.wim
|
||||
Save to boot.wim
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title"><i class="bi bi-info-circle me-1"></i> Common startnet.cmd Commands</h6>
|
||||
<h6 class="card-title">Common startnet.cmd Commands</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<code class="d-block mb-1">wpeinit</code>
|
||||
@@ -93,7 +92,7 @@
|
||||
<div class="col-lg-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-info-square me-2"></i>WIM Info
|
||||
WIM Info
|
||||
</div>
|
||||
<div class="card-body wim-info">
|
||||
<dl class="mb-0">
|
||||
|
||||
@@ -43,13 +43,12 @@
|
||||
<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/FlatUnattendW10.xml</code>
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-success" id="saveFormBtn">
|
||||
<i class="bi bi-floppy me-1"></i> Save
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,13 +58,13 @@
|
||||
<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
|
||||
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
|
||||
Raw XML
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -80,9 +79,9 @@
|
||||
<!-- 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>
|
||||
<span>Driver Paths</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addDriverPath">
|
||||
<i class="bi bi-plus-lg"></i> Add
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@@ -104,7 +103,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
|
||||
<i class="bi bi-trash"></i>
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -122,7 +121,7 @@
|
||||
<!-- 2. Machine Settings -->
|
||||
<div class="card section-card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-pc-display me-1"></i> Machine Settings
|
||||
Machine Settings
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
@@ -154,9 +153,9 @@
|
||||
<!-- 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>
|
||||
<span>Specialize Commands (RunSynchronous)</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addSpecCmd">
|
||||
<i class="bi bi-plus-lg"></i> Add
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@@ -173,7 +172,7 @@
|
||||
<tbody>
|
||||
{% for cmd in data.specialize_commands %}
|
||||
<tr draggable="true">
|
||||
<td class="drag-handle"><i class="bi bi-grip-vertical"></i></td>
|
||||
<td class="drag-handle">::</td>
|
||||
<td class="order-num">{{ loop.index }}</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
@@ -185,13 +184,13 @@
|
||||
</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>
|
||||
Up
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-row-action move-down" title="Move down">
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
Down
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
|
||||
<i class="bi bi-trash"></i>
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -209,7 +208,7 @@
|
||||
<!-- 4. OOBE Settings -->
|
||||
<div class="card section-card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-shield-check me-1"></i> OOBE Settings
|
||||
OOBE Settings
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
@@ -260,12 +259,160 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 5. First Logon Commands -->
|
||||
<!-- 5. User Accounts -->
|
||||
<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>
|
||||
<span>User Accounts (Local)</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addUserAccount">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm mb-0" id="userAccountsTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:40px">#</th>
|
||||
<th>Name</th>
|
||||
<th>Password</th>
|
||||
<th>Group</th>
|
||||
<th>Display Name</th>
|
||||
<th style="width:60px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for acct in data.user_accounts %}
|
||||
<tr>
|
||||
<td class="order-num">{{ loop.index }}</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
name="account_name_{{ loop.index0 }}" value="{{ acct.name }}">
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
name="account_password_{{ loop.index0 }}" value="{{ acct.password }}">
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
name="account_group_{{ loop.index0 }}" value="{{ acct.group }}">
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
name="account_display_{{ loop.index0 }}" value="{{ acct.display_name }}">
|
||||
</td>
|
||||
<td>
|
||||
<input type="hidden" name="account_plaintext_{{ loop.index0 }}" value="{{ acct.plain_text }}">
|
||||
<button type="button" class="btn btn-outline-danger btn-row-action remove-account-row">
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if not data.user_accounts %}
|
||||
<div class="text-center text-muted py-3 empty-message" id="userAccountsEmpty">
|
||||
No local accounts configured. Click <strong>Add</strong> to add one.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 6. AutoLogon -->
|
||||
<div class="card section-card">
|
||||
<div class="card-header">
|
||||
AutoLogon
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<div class="form-check form-switch mt-4">
|
||||
<input class="form-check-input" type="checkbox" id="autologonEnabledToggle"
|
||||
{% if data.autologon.enabled|lower == 'true' %}checked{% endif %}>
|
||||
<label class="form-check-label fw-semibold" for="autologonEnabledToggle">Enabled</label>
|
||||
<input type="hidden" name="autologon_enabled" id="autologon_enabled_val"
|
||||
value="{{ data.autologon.enabled }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-semibold">Username</label>
|
||||
<input type="text" class="form-control" name="autologon_username"
|
||||
value="{{ data.autologon.username }}" placeholder="e.g. SupportUser">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-semibold">Password</label>
|
||||
<input type="text" class="form-control" name="autologon_password"
|
||||
value="{{ data.autologon.password }}">
|
||||
<input type="hidden" name="autologon_plaintext" value="{{ data.autologon.plain_text }}">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-semibold">Logon Count</label>
|
||||
<input type="text" class="form-control" name="autologon_logoncount"
|
||||
value="{{ data.autologon.logon_count }}" placeholder="e.g. 2 or 999">
|
||||
<div class="form-text">Number of auto-logon attempts.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 7. International Settings -->
|
||||
<div class="card section-card">
|
||||
<div class="card-header" data-bs-toggle="collapse" data-bs-target="#intlCollapse"
|
||||
role="button" style="cursor:pointer">
|
||||
International Settings
|
||||
<small class="text-muted ms-2">(click to expand)</small>
|
||||
</div>
|
||||
<div class="collapse {% if data.intl.input_locale or data.intl.system_locale or data.intl.ui_language or data.intl.user_locale %}show{% endif %}"
|
||||
id="intlCollapse">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-semibold">Input Locale</label>
|
||||
<input type="text" class="form-control" name="intl_input_locale"
|
||||
value="{{ data.intl.input_locale }}" placeholder="e.g. en-US">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-semibold">System Locale</label>
|
||||
<input type="text" class="form-control" name="intl_system_locale"
|
||||
value="{{ data.intl.system_locale }}" placeholder="e.g. en-US">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-semibold">UI Language</label>
|
||||
<input type="text" class="form-control" name="intl_ui_language"
|
||||
value="{{ data.intl.ui_language }}" placeholder="e.g. en-US">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-semibold">User Locale</label>
|
||||
<input type="text" class="form-control" name="intl_user_locale"
|
||||
value="{{ data.intl.user_locale }}" placeholder="e.g. en-US">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 8. OOBE Time Zone -->
|
||||
<div class="card section-card">
|
||||
<div class="card-header">
|
||||
OOBE Time Zone
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Time Zone (oobeSystem pass)</label>
|
||||
<input type="text" class="form-control" name="oobe_timezone"
|
||||
value="{{ data.oobe_timezone }}" placeholder="e.g. Eastern Standard Time">
|
||||
<div class="form-text">Separate from the specialize-pass time zone in Machine Settings above.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 9. First Logon Commands -->
|
||||
<div class="card section-card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>First Logon Commands</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addFlCmd">
|
||||
<i class="bi bi-plus-lg"></i> Add
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@@ -282,7 +429,7 @@
|
||||
<tbody>
|
||||
{% for cmd in data.firstlogon_commands %}
|
||||
<tr draggable="true">
|
||||
<td class="drag-handle"><i class="bi bi-grip-vertical"></i></td>
|
||||
<td class="drag-handle">::</td>
|
||||
<td class="order-num">{{ loop.index }}</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
@@ -294,13 +441,13 @@
|
||||
</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>
|
||||
Up
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-row-action move-down" title="Move down">
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
Down
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
|
||||
<i class="bi bi-trash"></i>
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -321,9 +468,9 @@
|
||||
<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>
|
||||
<span>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
|
||||
Save Raw XML
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
Reference in New Issue
Block a user