Boot menu: "Clonezilla: back up this PC" and "restore this PC" ask for the machine number and boot Clonezilla (64-bit with Debian shim, or 32-bit i686 by cpuid) with ocs_repository=smb3://clonezilla:<pw>@server/clonezilla/, ocs_prerun fetching clonezilla-scripts/pxe-cz.sh and ocs_live_run running it. The script validates the machine number, saves <machine>-<YYYY-MM-DD-HHMM> (server time), writes pxe-meta.txt, runs ocs-chkimg, and restores only that machine's images with -k1 -r after a typed YES. The old entry is "Clonezilla: manual". Playbook: clonezilla Samba user for the [clonezilla] share only, password from clonezilla_smb_password (validated, filled into the menu placeholder), scripts installed to the web root. Webapp: Clonezilla backups page lists image folders and zips grouped by machine number with serial/model/version/check, serial mismatch flag, delete, move to another machine, streamed tar download, disk space bar and an explicit keep-last-N retention with preview. Tests and a VM test plan added.
360 lines
22 KiB
HTML
360 lines
22 KiB
HTML
{% extends "base.html" %}
|
|
{% import '_ui.html' as ui %}
|
|
{% block title %}Clonezilla backups - PXE Manager{% endblock %}
|
|
|
|
{# Clonezilla backups, grouped by machine number (newest activity first):
|
|
image directories saved by the boot menu's "Clonezilla: back up this PC"
|
|
(or by hand) and the older machine-number .zip files. One line per
|
|
image; its details (disks, Clonezilla version, check) are in the
|
|
expandable body; Download is the row's action, Move and Delete are in
|
|
the menu and open confirm modals. #}
|
|
|
|
{% macro gb(n) -%}
|
|
{%- if n >= 1e12 %}{{ "%.2f"|format(n / 1e12) }} TB{% elif n >= 1e9 %}{{ "%.1f"|format(n / 1e9) }} GB{% else %}{{ "%.0f"|format(n / 1e6) }} MB{% endif -%}
|
|
{%- endmacro %}
|
|
|
|
{% macro status_chip(i) -%}
|
|
{%- if i.status == 'ok' %}{{ ui.chip('Checked', 'ok', 'Clonezilla image check passed' ~ (' ' ~ i.check_date if i.check_date else '')) }}
|
|
{%- elif i.status == 'check-failed' %}{{ ui.chip('Check failed', 'fail', 'The image check after saving failed: do not rely on this image') }}
|
|
{%- elif i.status == 'failed' %}{{ ui.chip('Save failed', 'fail', 'Clonezilla reported an error while saving: partial image') }}
|
|
{%- elif i.status == 'unchecked' %}{{ ui.chip('Not checked', 'warn', 'Saved, but the image check did not run') }}
|
|
{%- elif i.status == 'in-progress' %}{{ ui.chip('In progress?', 'run', 'No pxe-meta.txt yet and changed in the last 6 hours: probably still being written') }}
|
|
{%- else %}{{ ui.chip('Manual', 'neutral', 'Saved without the PXE menu script: no check result recorded') }}{% endif -%}
|
|
{%- endmacro %}
|
|
|
|
{% block extra_head %}
|
|
<style>
|
|
.igrid { display: grid; grid-template-columns: 186px minmax(0, 1fr) 140px 84px 118px 14px; align-items: center;
|
|
gap: 0 14px; padding: 11px 184px 11px 24px; font-size: 14px; min-height: 30px; }
|
|
.ihead { font-size: 12px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); padding-top: 6px; padding-bottom: 6px; min-height: 0; }
|
|
.i-when { font-size: 13px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.i-serial.odd { color: var(--warn-text); font-weight: 600; }
|
|
.i-size { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
.mach { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; padding: 14px 24px 10px; border-top: 1px solid var(--line-2); background: var(--surface-2); cursor: pointer; }
|
|
details.mgroup > summary { list-style: none; }
|
|
details.mgroup > summary::-webkit-details-marker { display: none; }
|
|
.mach h3 { margin: 0; font-size: 15px; font-weight: 600; }
|
|
.mach .sp { margin-left: auto; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
.ibody { padding: 2px 24px 16px 208px; font-size: 13px; display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 16px; }
|
|
.ibody dt { color: var(--muted); font-weight: 400; }
|
|
.ibody dd { margin: 0; overflow-wrap: anywhere; }
|
|
.bl-empty { padding: 28px 24px; text-align: center; color: var(--muted); border-top: 1px solid var(--line); }
|
|
.bnote { padding: 18px 24px; font-size: 13px; color: var(--text-2); display: flex; flex-direction: column; gap: 6px; }
|
|
.bnote p { margin: 0; }
|
|
.space { padding: 16px 24px; display: flex; flex-direction: column; gap: 8px; font-size: 13px; }
|
|
.space .row1 { display: flex; flex-wrap: wrap; gap: 4px 16px; align-items: baseline; }
|
|
.sbar { height: 10px; border-radius: 5px; background: var(--surface-3); box-shadow: inset 0 0 0 1px var(--line-2); overflow: hidden; }
|
|
.sbar > i { display: block; height: 100%; background: var(--ok); }
|
|
.sbar.warn > i { background: var(--act-track); }
|
|
.plan { padding: 16px 24px; display: flex; flex-direction: column; gap: 10px; font-size: 13px; }
|
|
.plan ul { margin: 0; padding-left: 18px; max-height: 18rem; overflow-y: auto; }
|
|
.plan .btns { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
.setform { width: min(340px, calc(100vw - 32px)); padding: 14px; gap: 10px; }
|
|
.setform label { display: flex; flex-direction: column; gap: 4px; }
|
|
@container (max-width: 1000px) {
|
|
.igrid { grid-template-columns: 186px minmax(0, 1fr) 118px 14px; }
|
|
.igrid .i-serial, .igrid .i-size { display: none; }
|
|
.ibody { padding-left: 24px; }
|
|
}
|
|
@container (max-width: 640px) {
|
|
.igrid { grid-template-columns: minmax(0, 1fr) auto 14px; padding: 11px 16px; gap: 2px 10px; }
|
|
.igrid .i-when { grid-column: 1 / -1; }
|
|
.igrid .i-size { display: none; }
|
|
.ihead { display: none; }
|
|
.ui-row > .act { position: static; display: flex; justify-content: flex-end; padding: 0 16px 10px; }
|
|
.ibody { padding: 2px 16px 14px; grid-template-columns: minmax(0, 1fr); }
|
|
.ibody dt { margin-top: 4px; }
|
|
.mach, .space, .plan { padding-left: 16px; padding-right: 16px; }
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
{% set nmach = groups | rejectattr('machine', 'equalto', '') | list | length %}
|
|
{% call ui.head('Clonezilla backups', 'Tools', image_count ~ ' image' ~ ('s' if image_count != 1) ~ ' and ' ~ (backups | length) ~ ' zip' ~ ('s' if backups | length != 1) ~ ' for ' ~ nmach ~ ' machine' ~ ('s' if nmach != 1) ~ ' on the share') %}
|
|
<details class="more">
|
|
<summary class="ui-btn">Settings</summary>
|
|
<form class="menu setform" method="post" action="{{ url_for('clonezilla_settings') }}">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<label for="keepLast" class="sub">Keep last N images per machine (0 = off)
|
|
<input id="keepLast" type="number" name="keep_last" min="0" max="100" value="{{ cz_settings.keep_last }}" class="field" required></label>
|
|
<label for="warnPct" class="sub">Warn when the share is this full (%)
|
|
<input id="warnPct" type="number" name="warn_percent" min="50" max="99" value="{{ cz_settings.warn_percent }}" class="field" required></label>
|
|
<div class="sub" style="white-space: normal">Nothing is deleted automatically: Apply retention shows what would go and asks first.</div>
|
|
<button type="submit" class="ui-btn pri">Save settings</button>
|
|
</form>
|
|
</details>
|
|
{% if cz_settings.keep_last %}
|
|
<a class="ui-btn" href="{{ url_for('clonezilla_retention_preview') }}">Apply retention...</a>
|
|
{% endif %}
|
|
<button type="button" class="ui-btn pri" data-bs-toggle="modal" data-bs-target="#uploadModal">Upload backup</button>
|
|
{% endcall %}
|
|
|
|
<div class="ui-stack">
|
|
|
|
{% if plan is not none %}
|
|
<section class="panel plan" aria-labelledby="plan-h">
|
|
<h2 id="plan-h" class="sec" style="font-size: 15px; margin: 0">Apply retention: keep the last {{ plan_keep }} image{{ 's' if plan_keep != 1 }} per machine</h2>
|
|
{% if plan %}
|
|
<div class="notice bad">These {{ plan | length }} image{{ 's' if plan | length != 1 }} ({{ gb(plan | sum(attribute='size')) }}) will be DELETED. This cannot be undone.</div>
|
|
<p class="sub" style="margin: 0">Kept per machine: the {{ plan_keep }} newest images that saved and passed (or skipped) the check. Newer failed or in-progress images are kept too. Images with no machine number are never touched.</p>
|
|
<ul>
|
|
{% for p in plan %}<li><span class="mono">{{ p.name }}</span> <span class="sub">machine {{ p.machine }} | {{ p.when | timestamp_fmt }} | {{ gb(p.size) }}</span></li>{% endfor %}
|
|
</ul>
|
|
<form method="post" action="{{ url_for('clonezilla_retention_apply') }}" onsubmit="return confirm('Delete {{ plan | length }} image(s)? This cannot be undone.');">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
{% for p in plan %}<input type="hidden" name="name" value="{{ p.name }}">{% endfor %}
|
|
<div class="btns">
|
|
<a class="ui-btn pri" href="{{ url_for('clonezilla_backups') }}">Cancel</a>
|
|
<button type="submit" class="ui-btn danger">Delete {{ plan | length }} image{{ 's' if plan | length != 1 }}</button>
|
|
</div>
|
|
</form>
|
|
{% else %}
|
|
<div class="notice info">Nothing to delete: no machine has more than {{ plan_keep }} good image{{ 's' if plan_keep != 1 }}.</div>
|
|
<div class="btns"><a class="ui-btn" href="{{ url_for('clonezilla_backups') }}">Back</a></div>
|
|
{% endif %}
|
|
</section>
|
|
{% endif %}
|
|
|
|
{% if space %}
|
|
<section class="panel space" aria-labelledby="sp-h">
|
|
<h2 id="sp-h" class="vh">Share disk space</h2>
|
|
<div class="row1">
|
|
<strong>Share disk space</strong>
|
|
<span>{{ gb(space.used) }} used of {{ gb(space.total) }} ({{ "%.0f"|format(space.percent) }}%)</span>
|
|
<span class="sub">{{ gb(space.free) }} free</span>
|
|
{% if space.warn %}{{ ui.chip('Over ' ~ space.warn_percent ~ '%', 'warn') }}{% endif %}
|
|
</div>
|
|
<div class="sbar{{ ' warn' if space.warn }}" role="meter" aria-label="Share disk space used" aria-valuemin="0" aria-valuemax="100" aria-valuenow="{{ '%.0f'|format(space.percent) }}"><i style="width: {{ '%.1f'|format(space.percent) }}%"></i></div>
|
|
{% if space.warn %}<div class="notice">The share is {{ "%.0f"|format(space.percent) }}% full (warning at {{ space.warn_percent }}%). Delete old images, or set Keep last N in Settings and Apply retention. A backup that runs out of space fails part-way.</div>{% endif %}
|
|
</section>
|
|
{% endif %}
|
|
|
|
{% if mismatches %}
|
|
<div class="notice" role="status">{{ mismatches }} machine{{ 's have' if mismatches != 1 else ' has' }} images with different serial numbers (marked Serial mismatch below): a typo in the machine number, or the PC was replaced. Move the wrong images to the right machine number.</div>
|
|
{% endif %}
|
|
|
|
<section class="panel ui-list" aria-labelledby="bl-h">
|
|
<h2 id="bl-h" class="vh">Backups by machine</h2>
|
|
{% if groups %}
|
|
<div class="igrid ihead" aria-hidden="true"><span>Saved</span><span>Model</span><span class="i-serial">Serial</span><span class="i-size">Size</span><span>Check</span><span></span></div>
|
|
{% for g in groups %}
|
|
{% set label = g.machine or 'No machine number' %}
|
|
<details class="mgroup" {% if loop.index <= 6 or g.mismatch %}open{% endif %} data-machine="{{ g.machine }}">
|
|
<summary class="mach">
|
|
<h3 class="{{ 'mono' if g.machine }}">{{ label }}</h3>
|
|
<span class="sub">{{ g.images | length }} image{{ 's' if g.images | length != 1 }}{% if g.zips %}, {{ g.zips | length }} zip{{ 's' if g.zips | length != 1 }}{% endif %} | {{ gb(g.size) }}</span>
|
|
<span class="sp">
|
|
{% if g.mismatch %}{{ ui.chip('Serial mismatch', 'warn', 'Serials: ' ~ (g.serials | join(', '))) }}<span class="sub mono">{{ g.serials | join(', ') }}</span>{% endif %}
|
|
<span class="chev" aria-hidden="true">▾</span>
|
|
</span>
|
|
</summary>
|
|
|
|
{% for i in g.images %}
|
|
<div class="ui-row irow" data-name="{{ i.name }}">
|
|
<details>
|
|
<summary class="igrid">
|
|
<span class="i-when mono">{{ i.when | timestamp_fmt }}</span>
|
|
<span class="tip" data-tip="{{ i.name }}"><span class="trunc">{{ i.model or i.name }}</span></span>
|
|
<span class="i-serial mono tip{{ ' odd' if i.serial_differs }}" data-tip="{{ (i.serial ~ (' (differs from the newest image of this machine)' if i.serial_differs else '')) if i.serial else 'no serial recorded' }}"><span class="trunc">{{ i.serial or '-' }}</span></span>
|
|
<span class="i-size">{{ gb(i.size) }}</span>
|
|
<span>{{ status_chip(i) }}</span>
|
|
<span class="chev" aria-hidden="true">▾</span>
|
|
</summary>
|
|
<dl class="ibody">
|
|
<dt>Image</dt><dd class="mono">{{ i.name }}</dd>
|
|
<dt>Machine</dt><dd><span class="mono">{{ i.machine or '-' }}</span> <span class="sub">({{ {'meta': 'from pxe-meta.txt', 'name': 'from the folder name'}.get(i.machine_source, 'unknown: move it to a machine number') }})</span></dd>
|
|
<dt>PC</dt><dd>{{ i.manufacturer }} {{ i.model or '-' }} | serial <span class="mono">{{ i.serial or '-' }}</span>{% if i.serial_differs %} {{ ui.chip('Different serial', 'warn', 'Not the serial of the newest image of this machine') }}{% endif %}</dd>
|
|
<dt>Size</dt><dd>{{ gb(i.size) }}</dd>
|
|
<dt>Disk</dt><dd><span class="mono">{{ i.disks | join(' ') or '-' }}</span>{% if i.disk_model %} | {{ i.disk_model }}{% endif %}{% if i.disk_size %} | {{ gb(i.disk_size) }}{% endif %}</dd>
|
|
<dt>Partitions</dt><dd class="mono">{{ i.parts | join(' ') or '-' }}</dd>
|
|
<dt>Clonezilla</dt><dd>{{ i.clonezilla or '-' }}{% if i.bits %} | {{ i.bits }}-bit{% endif %}{% if i.firmware %} | {{ i.firmware | upper }}{% endif %}</dd>
|
|
<dt>Saved</dt><dd>{{ i.date_text or (i.when | timestamp_fmt) }}{% if i.save == 'failed' %} | <strong>save failed</strong>{% endif %}</dd>
|
|
<dt>Check</dt><dd>{{ {'ok': 'passed', 'failed': 'FAILED', 'not-run': 'not run'}.get(i.check, 'no result recorded') }}{% if i.check_date %} ({{ i.check_date }}){% endif %}</dd>
|
|
</dl>
|
|
</details>
|
|
<div class="act">
|
|
<a class="ui-btn" href="{{ url_for('clonezilla_image_tar', name=i.name) }}" title="Download the image folder as a .tar (streamed)">Download .tar</a>
|
|
{% call ui.more(i.name) %}
|
|
<button type="button" class="copy-btn" data-copy-text="{{ i.name }}">Copy image name</button>
|
|
<button type="button" data-bs-toggle="modal" data-bs-target="#moveModal" data-name="{{ i.name }}" data-machine="{{ i.machine }}">Move to another machine...</button>
|
|
<div class="sep"></div>
|
|
<button type="button" class="del" data-bs-toggle="modal" data-bs-target="#imageDeleteModal"
|
|
data-name="{{ i.name }}" data-machine="{{ label }}">Delete image...</button>
|
|
{% endcall %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
|
|
{% for b in g.zips %}
|
|
<div class="ui-row zrow">
|
|
<div class="igrid">
|
|
<span class="i-when mono">{{ b.modified | timestamp_fmt }}</span>
|
|
<span class="tip" data-tip="{{ b.filename }}"><span class="trunc mono">{{ b.filename }}</span></span>
|
|
<span class="i-serial sub">-</span>
|
|
<span class="i-size">{{ gb(b.size) }}</span>
|
|
<span>{{ ui.chip('Zip', 'neutral', 'Uploaded .zip backup') }}</span>
|
|
<span></span>
|
|
</div>
|
|
<div class="act">
|
|
<a class="ui-btn" href="{{ url_for('clonezilla_download', filename=b.filename) }}">Download</a>
|
|
{% call ui.more(b.machine) %}
|
|
<button type="button" class="copy-btn" data-copy-text="{{ b.filename }}">Copy file name</button>
|
|
<div class="sep"></div>
|
|
<button type="button" class="del" data-bs-toggle="modal" data-bs-target="#deleteModal"
|
|
data-filename="{{ b.filename }}" data-machine="{{ b.machine }}">Delete backup...</button>
|
|
{% endcall %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</details>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="bl-empty">No backups yet. Boot a PC from the network and pick <strong>Clonezilla: back up this PC</strong>, or upload a .zip.</div>
|
|
{% endif %}
|
|
</section>
|
|
|
|
<section class="panel bnote" aria-labelledby="bn-h">
|
|
<h2 id="bn-h" class="sec" style="font-size: 15px">How backups are named</h2>
|
|
<p>The boot menu's <strong>Clonezilla: back up this PC</strong> asks for the machine number and saves <span class="mono"><machine>-<YYYY-MM-DD-HHMM></span> (for example <span class="mono">6501-2026-10-15-1403</span>) to <span class="mono">\\pxe-server\clonezilla</span>, then checks it. <strong>Clonezilla: restore this PC</strong> lists only that machine's images, newest first.</p>
|
|
<p>Images saved by hand (manual mode) are grouped by the start of their folder name; move them to a machine number if they show under No machine number. Uploaded zips are named with the machine number, for example <span class="mono">6501.zip</span>.</p>
|
|
</section>
|
|
</div>
|
|
|
|
<!-- Upload Modal -->
|
|
<div class="modal fade" id="uploadModal" tabindex="-1" aria-labelledby="uploadTitle">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<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">
|
|
<h3 class="modal-title" id="uploadTitle">Upload backup</h3>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="form-field mb-0">
|
|
<label for="backupFile" class="form-label">Backup file (.zip)</label>
|
|
<input type="file" class="form-control" id="backupFile" name="backup_file"
|
|
accept=".zip" required>
|
|
<div class="form-text">
|
|
Name the file with the machine number (e.g. <span class="mono">6501.zip</span>).
|
|
A file with the same name as an existing backup is refused unless you tick the box below.
|
|
</div>
|
|
</div>
|
|
<div class="form-check mt-2">
|
|
<input class="form-check-input" type="checkbox" name="overwrite" value="1" id="backupOverwrite">
|
|
<label class="form-check-label" for="backupOverwrite">Replace the existing backup with the same name</label>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="ui-btn" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="ui-btn pri">Upload</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete zip Confirmation Modal -->
|
|
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteTitle">
|
|
<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">
|
|
<h3 class="modal-title" id="deleteTitle">Delete backup</h3>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Are you sure you want to delete the backup for machine <strong id="deleteMachine"></strong>?</p>
|
|
<p class="muted mb-0">This action cannot be undone.</p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="ui-btn pri" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="ui-btn danger">Delete</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete image Confirmation Modal -->
|
|
<div class="modal fade" id="imageDeleteModal" tabindex="-1" aria-labelledby="imageDeleteTitle">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<form id="imageDeleteForm" method="post">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title" id="imageDeleteTitle">Delete image</h3>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Delete the image <strong class="mono" id="imageDeleteName"></strong> of machine <strong id="imageDeleteMachine"></strong>?</p>
|
|
<p class="muted mb-0">The whole image folder is removed from the share. This cannot be undone.</p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="ui-btn pri" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="ui-btn danger">Delete image</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Move image Modal -->
|
|
<div class="modal fade" id="moveModal" tabindex="-1" aria-labelledby="moveTitle">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<form id="moveForm" method="post">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title" id="moveTitle">Move image to another machine</h3>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="mb-2">Image <strong class="mono" id="moveName"></strong></p>
|
|
<div class="form-field mb-0">
|
|
<label for="moveMachine" class="form-label">New machine number</label>
|
|
<input type="text" class="form-control mono" id="moveMachine" name="machine" required maxlength="32"
|
|
pattern="[A-Za-z0-9][A-Za-z0-9\-]{0,31}" autocomplete="off" spellcheck="false">
|
|
<div class="form-text">Letters, digits and dash. The folder is renamed to <span class="mono"><machine>-<same date></span> and its pxe-meta.txt updated, so restore by machine number finds it.</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="ui-btn" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="ui-btn pri">Move</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');
|
|
var machine = btn.getAttribute('data-machine');
|
|
document.getElementById('deleteMachine').textContent = machine;
|
|
document.getElementById('deleteForm').action = "{{ url_for('clonezilla_delete', filename='__F__') }}".replace('__F__', encodeURIComponent(filename));
|
|
});
|
|
document.getElementById('imageDeleteModal').addEventListener('show.bs.modal', function (event) {
|
|
var btn = event.relatedTarget;
|
|
var name = btn.getAttribute('data-name');
|
|
document.getElementById('imageDeleteName').textContent = name;
|
|
document.getElementById('imageDeleteMachine').textContent = btn.getAttribute('data-machine');
|
|
document.getElementById('imageDeleteForm').action = "{{ url_for('clonezilla_image_delete', name='__N__') }}".replace('__N__', encodeURIComponent(name));
|
|
});
|
|
document.getElementById('moveModal').addEventListener('show.bs.modal', function (event) {
|
|
var btn = event.relatedTarget;
|
|
var name = btn.getAttribute('data-name');
|
|
document.getElementById('moveName').textContent = name;
|
|
var inp = document.getElementById('moveMachine');
|
|
inp.value = btn.getAttribute('data-machine') || '';
|
|
document.getElementById('moveForm').action = "{{ url_for('clonezilla_image_move', name='__N__') }}".replace('__N__', encodeURIComponent(name));
|
|
setTimeout(function () { inp.focus(); inp.select(); }, 200);
|
|
});
|
|
</script>
|
|
{% endblock %}
|