Webapp redesign phase 6: the remaining pages
Restyled to the same row pattern (one line, details in an expandable body, one action plus a menu, tokens for light and dark): - hardware coverage, audit log (family chips, destructive actions in red, client-side filters, first 300 rows), Clonezilla backups, enrollment packages (active package marked and undeletable, MCL flags with a visible legend) - image Overview, OS and packages, Unattend (sticky save bar), Import - Dell, iPXE, Blancco and BPRT update pages; Dell history links to the rollback review instead of rolling back on one click - boot menu editor (tabs, rows with a menu, lint chips that jump to the line, tokens instead of hard-coded dark colours) and the imaging session detail page Fixes found on the way: - BPRT batch Activate never asked to confirm: a hidden input named 'confirm' shadowed window.confirm in the inline handler, so the form submitted without a prompt. It now calls window.confirm. - Backup upload refused when a backup with that name exists, unless 'Replace the existing backup' is ticked (it overwrote silently). - app.js: em dashes in comments replaced (plain ASCII).
This commit is contained in:
@@ -1,189 +1,200 @@
|
||||
{% extends "base.html" %}
|
||||
{% import '_ui.html' as ui %}
|
||||
{% block title %}{{ session.serial }} - Imaging Session{% endblock %}
|
||||
|
||||
{% set st = session.status or 'in_progress' %}
|
||||
{% set st_chip = {'succeeded': ('Succeeded', 'ok'), 'failed': ('Failed', 'fail')}.get(st, ('In progress', 'run')) %}
|
||||
{% set st_kind = {'succeeded': 'ok', 'failed': 'fail'}.get(st, 'run') %}
|
||||
{% set stage_idx = session.stage_index | int(0) %}
|
||||
{% set stage_total = session.stage_total | int(0) %}
|
||||
{% set history = session.stage_history or [] %}
|
||||
|
||||
{# Support summary: built here so the copy button carries it (static/app.js
|
||||
handles .copy-btn[data-copy-text]). Same fields as the metadata list. #}
|
||||
{% set summary_lines = [
|
||||
'Serial: ' ~ session.serial,
|
||||
'Status: ' ~ st,
|
||||
'Hostname: ' ~ (session.hostname_target or '-'),
|
||||
'IP: ' ~ (session.ip or '-'),
|
||||
'MAC: ' ~ (session.mac or '-'),
|
||||
'PC type: ' ~ (session.pctype or '-'),
|
||||
'Machine #: ' ~ (session.machinenumber or '-'),
|
||||
'Intune ID: ' ~ (session.intune_device_id or '-'),
|
||||
'Started: ' ~ (session.started_at | local_dt(true)),
|
||||
'Last update: ' ~ (session.last_updated | local_dt(true)),
|
||||
'Stage: ' ~ stage_idx ~ '/' ~ (session.stage_total or '?') ~ ((' - ' ~ session.current_stage) if session.current_stage else ''),
|
||||
] %}
|
||||
{% if session.previous_run_at %}{% set _ = summary_lines.append('Prior run: ' ~ (session.previous_run_at | local_dt(true))) %}{% endif %}
|
||||
{% if session.error %}{% set _ = summary_lines.append('Error: ' ~ session.error) %}{% endif %}
|
||||
{% if history %}
|
||||
{% set _ = summary_lines.append('') %}{% set _ = summary_lines.append('Stage timeline:') %}
|
||||
{% for h in history %}{% set _ = summary_lines.append(' - ' ~ (h.ts | local_dt(true)) ~ ' stage ' ~ h.stage_index ~ ' ' ~ h.status ~ ' ' ~ (h.current_stage or '')) %}{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
.id-hero { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(170px, 100%), 1fr)); gap: 12px; }
|
||||
.id-stat { background: var(--surface); border-radius: 12px; padding: 14px 16px; box-shadow: 0 1px 2px var(--shadow); min-width: 0; }
|
||||
.id-stat .k { font-size: 12px; color: var(--muted); }
|
||||
.id-stat .v { font-size: 18px; font-weight: 600; margin-top: 4px; overflow-wrap: anywhere; }
|
||||
.id-stat .v.sm { font-size: 13px; font-weight: 400; line-height: 1.45; }
|
||||
.id-cols { display: grid; grid-template-columns: minmax(0, 1.7fr) minmax(280px, 1fr); grid-template-areas: "log side"; gap: 16px; align-items: start; }
|
||||
.id-cols > .id-main { grid-area: log; display: flex; flex-direction: column; gap: 16px; min-width: 0; }
|
||||
.id-cols > .id-side { grid-area: side; display: flex; flex-direction: column; min-width: 0; }
|
||||
@media (max-width: 1100px) { .id-cols { grid-template-columns: minmax(0, 1fr); grid-template-areas: "side" "log"; } }
|
||||
.id-side section { padding: 16px 18px; }
|
||||
.id-side section + section { border-top: 1px solid var(--line); }
|
||||
.id-side h2, .id-log h2 { margin: 0; font-size: 15px; font-weight: 600; }
|
||||
.kv { margin: 0; display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px 16px; font-size: 13px; }
|
||||
.kv dt { color: var(--muted); font-weight: 400; } .kv dd { margin: 0; overflow-wrap: anywhere; }
|
||||
/* Stage history: vertical track, one entry per transition. */
|
||||
.sh-list { list-style: none; margin: 12px 0 0; padding: 0; }
|
||||
.sh-item { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 12px; padding: 0 0 14px 24px; font-size: 13px; }
|
||||
.sh-item::before { content: ""; position: absolute; left: 5px; top: 14px; bottom: -2px; width: 2px; background: var(--line-2); }
|
||||
.sh-item:last-child { padding-bottom: 0; }
|
||||
.sh-item:last-child::before { display: none; }
|
||||
.sh-dot { position: absolute; left: 0; top: 4px; width: 12px; height: 12px; border-radius: 6px; background: var(--track-done); box-shadow: 0 0 0 3px var(--surface); }
|
||||
.sh-dot.run { background: var(--run); } .sh-dot.fail { background: var(--fail); } .sh-dot.ok { background: var(--ok); }
|
||||
.sh-name { font-weight: 600; min-width: 0; overflow-wrap: anywhere; }
|
||||
.sh-when { color: var(--muted); font-size: 12px; white-space: nowrap; }
|
||||
.sh-meta { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; color: var(--muted); font-size: 12px; }
|
||||
/* Logs: contained scroll panels on a token background. */
|
||||
.id-log { padding: 16px 18px; display: flex; flex-direction: column; gap: 10px; min-width: 0; }
|
||||
.id-loghead { display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: center; }
|
||||
.id-loghead .grow { margin-right: auto; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
||||
.id-pre { margin: 0; padding: 10px 12px; background: var(--surface-2); color: var(--text); border: 1px solid var(--line); border-radius: 8px;
|
||||
font-family: var(--font-mono); font-size: 12px; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; overflow: auto; }
|
||||
#full-log { max-height: 36rem; }
|
||||
.id-tail { max-height: 12rem; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>{{ session.serial }}
|
||||
{% if session.hostname_target %}<span class="text-light" style="font-size:14px; font-weight:400;">{{ session.hostname_target }}</span>{% endif %}
|
||||
<header class="ui-head">
|
||||
<div style="min-width: 0">
|
||||
<a href="{{ url_for('imaging_dashboard') }}" class="eyebrow-t" style="text-decoration: none">Imaging</a>
|
||||
<h1 style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 14px">
|
||||
<span class="mono">{{ session.serial }}</span>
|
||||
{{ ui.chip(st_chip[0], st_chip[1]) }}
|
||||
</h1>
|
||||
<div style="margin-top:0.5rem; display:flex; gap:0.4rem; flex-wrap:wrap;">
|
||||
{% if session.pctype %}<span class="badge badge-info">{{ session.pctype }}</span>{% endif %}
|
||||
{% if session.machinenumber %}<span class="badge badge-secondary">#{{ session.machinenumber }}</span>{% endif %}
|
||||
<span class="badge badge-{{ 'success' if session.status == 'succeeded' else ('danger' if session.status == 'failed' else 'primary') }}">{{ session.status or 'in_progress' }}</span>
|
||||
<div class="sub" style="margin-top: 4px; font-size: 13px">
|
||||
Stage {{ stage_idx }} of {{ session.stage_total or '?' }}{% if session.current_stage %}: <span class="mono">{{ session.current_stage }}</span>{% endif %}
|
||||
<span class="muted"> | updated {{ session.last_updated | local_dt(true) }}</span>
|
||||
</div>
|
||||
{% if stage_total %}<div style="max-width: 360px">{{ ui.track(stage_total, stage_idx, st_kind) }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a href="{{ url_for('imaging_dashboard') }}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Back to dashboard
|
||||
</a>
|
||||
<button type="button" class="btn btn-primary btn-sm copy-summary-btn">
|
||||
<i class="bi bi-clipboard"></i> Copy support summary
|
||||
</button>
|
||||
<div class="actions">
|
||||
<a href="{{ url_for('imaging_dashboard') }}" class="ui-btn">Back to imaging</a>
|
||||
<button type="button" class="ui-btn pri copy-btn copy-summary-btn" data-copy-text="{{ summary_lines | join('\n') }}">Copy support summary</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="ui-stack">
|
||||
{% if session.error %}
|
||||
<div class="errbox" role="alert"><strong>Error.</strong> {{ session.error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="id-hero">
|
||||
<div class="id-stat"><div class="k">Machine number</div><div class="v mono">{{ session.machinenumber or '-' }}</div></div>
|
||||
<div class="id-stat"><div class="k">Hostname</div><div class="v mono">{{ session.hostname_target or '-' }}</div></div>
|
||||
<div class="id-stat"><div class="k">IP</div><div class="v mono">{{ session.ip or '-' }}</div>{% if session.mac %}<div class="sub mono">{{ session.mac }}</div>{% endif %}</div>
|
||||
<div class="id-stat"><div class="k">PC type</div><div class="v" style="font-size: 15px">{{ session.pctype or '-' }}</div></div>
|
||||
<div class="id-stat"><div class="k">Intune ID</div>
|
||||
{% if session.intune_device_id %}<div class="v sm mono">{{ session.intune_device_id }}</div>
|
||||
<button type="button" class="ui-btn sm ghost copy-btn" data-copy-text="{{ session.intune_device_id }}" style="margin-top: 4px">Copy ID</button>
|
||||
{% else %}<div class="v">-</div><div class="sub">Not enrolled yet</div>{% endif %}</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-5">
|
||||
|
||||
<div class="section-card">
|
||||
<div class="section-title">Session metadata</div>
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-4 text-light">Serial</dt><dd class="col-8"><span class="mono">{{ session.serial }}</span></dd>
|
||||
<dt class="col-4 text-light">Hostname</dt><dd class="col-8"><span class="mono">{{ session.hostname_target or '-' }}</span></dd>
|
||||
<dt class="col-4 text-light">MAC</dt><dd class="col-8"><span class="mono">{{ session.mac or '-' }}</span></dd>
|
||||
<dt class="col-4 text-light">PC type</dt><dd class="col-8">{{ session.pctype or '-' }}</dd>
|
||||
<dt class="col-4 text-light">Machine #</dt><dd class="col-8">{{ session.machinenumber or '-' }}</dd>
|
||||
<dt class="col-4 text-light">Intune ID</dt><dd class="col-8">
|
||||
{% if session.intune_device_id %}
|
||||
<span class="mono" style="word-break: break-all;">{{ session.intune_device_id }}</span>
|
||||
{% else %}-{% endif %}
|
||||
</dd>
|
||||
<dt class="col-4 text-light">Started</dt><dd class="col-8"><span class="mono">{{ session.started_at | local_dt(true) }}</span></dd>
|
||||
<dt class="col-4 text-light">Last update</dt><dd class="col-8"><span class="mono">{{ session.last_updated | local_dt(true) }}</span></dd>
|
||||
<dt class="col-4 text-light">Stage</dt><dd class="col-8">
|
||||
{{ session.stage_index or 0 }}/{{ session.stage_total or '?' }}
|
||||
{% if session.current_stage %} - <span class="mono">{{ session.current_stage }}</span>{% endif %}
|
||||
</dd>
|
||||
{% if session.previous_run_at %}
|
||||
<dt class="col-4 text-light">Prior run</dt><dd class="col-8"><span class="mono">{{ session.previous_run_at | local_dt(true) }}</span></dd>
|
||||
{% endif %}
|
||||
{% if session.error %}
|
||||
<dt class="col-4 text-danger">Error</dt>
|
||||
<dd class="col-8 text-danger">{{ session.error }}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="section-title">Stage timeline</div>
|
||||
{% if session.stage_history %}
|
||||
<div class="table-container">
|
||||
<table class="data-table mb-0">
|
||||
<thead>
|
||||
<tr><th>Time</th><th>Stage</th><th>Status</th><th>Detail</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for h in session.stage_history %}
|
||||
<tr>
|
||||
<td><span class="mono">{{ h.ts | local_dt(true) }}</span></td>
|
||||
<td><span class="badge badge-secondary">{{ h.stage_index }}</span></td>
|
||||
<td>
|
||||
<span class="badge badge-{{ 'success' if h.status == 'succeeded' else ('danger' if h.status == 'failed' else 'primary') }}">
|
||||
{{ h.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td><span class="mono">{{ h.current_stage or '' }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="id-cols">
|
||||
<div class="panel id-side">
|
||||
<section aria-labelledby="shHead">
|
||||
<h2 id="shHead">Stage history</h2>
|
||||
{% if history %}
|
||||
<ol class="sh-list">
|
||||
{% for h in history %}
|
||||
{% set hk = {'succeeded': 'ok', 'failed': 'fail'}.get(h.status, 'run' if loop.last else '') %}
|
||||
<li class="sh-item">
|
||||
<span class="sh-dot {{ hk }}" aria-hidden="true"></span>
|
||||
<span class="sh-name">{{ h.current_stage or ('Stage ' ~ h.stage_index) }}</span>
|
||||
<span class="sh-when mono">{{ h.ts | local_dt(true) }}</span>
|
||||
<span class="sh-meta">Stage {{ h.stage_index }}{% if stage_total %} of {{ stage_total }}{% endif %}
|
||||
{% if h.status == 'failed' %}{{ ui.chip('Failed', 'fail') }}{% elif h.status == 'succeeded' %}{{ ui.chip('Succeeded', 'ok') }}{% endif %}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% else %}
|
||||
<div class="status-indicator">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
<span>No stage transitions recorded yet. The client only logs a row when
|
||||
stage_index advances or status flips to succeeded/failed.</span>
|
||||
</div>
|
||||
<p class="sub" style="margin: 8px 0 0">No stage transitions recorded yet. The client only logs a row when
|
||||
stage_index advances or status flips to succeeded or failed.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</section>
|
||||
<section aria-labelledby="metaHead">
|
||||
<h2 id="metaHead" style="margin-bottom: 10px">Session</h2>
|
||||
<dl class="kv" id="sessionMeta">
|
||||
<dt>Serial</dt><dd class="mono">{{ session.serial }}</dd>
|
||||
<dt>MAC</dt><dd class="mono">{{ session.mac or '-' }}</dd>
|
||||
<dt>Started</dt><dd class="mono">{{ session.started_at | local_dt(true) }}</dd>
|
||||
<dt>Last update</dt><dd class="mono">{{ session.last_updated | local_dt(true) }}</dd>
|
||||
{% if session.previous_run_at %}<dt>Prior run</dt><dd class="mono">{{ session.previous_run_at | local_dt(true) }}</dd>{% endif %}
|
||||
{% if session.source %}<dt>Source</dt><dd>{{ session.source }}</dd>{% endif %}
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-7">
|
||||
|
||||
<div class="section-card">
|
||||
<div class="section-title" style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<span>Full log
|
||||
{% if full_log_truncated %}
|
||||
<span class="badge badge-warning" style="margin-left:0.5rem;">truncated (last 1 MB)</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm copy-log-btn"><i class="bi bi-clipboard"></i> Copy</button>
|
||||
<div class="id-main">
|
||||
<section class="panel id-log" aria-labelledby="logHead">
|
||||
<div class="id-loghead">
|
||||
<div class="grow"><h2 id="logHead">Full log</h2>
|
||||
{% if full_log_truncated %}{{ ui.chip('Truncated: last 1 MB', 'warn') }}{% endif %}</div>
|
||||
{% if full_log %}<button type="button" class="ui-btn sm copy-log-btn">Copy log</button>{% endif %}
|
||||
</div>
|
||||
{% if full_log %}
|
||||
<pre id="full-log" class="mono mb-0" style="max-height: 36rem; overflow: auto; white-space: pre-wrap; background: var(--bg); padding: 0.75rem; border-radius: 0.25rem;">{{ full_log }}</pre>
|
||||
<pre id="full-log" class="id-pre" tabindex="0" aria-label="Full log">{{ full_log }}</pre>
|
||||
{% else %}
|
||||
<div class="status-indicator">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
<span>No log content. The client has not pushed any <code>log_lines</code>
|
||||
entries yet, or the sidecar file was cleared.</span>
|
||||
</div>
|
||||
<div class="notice info">No log content. The client has not pushed any log_lines entries yet, or the sidecar file was cleared.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if session.log_tail %}
|
||||
<div class="section-card">
|
||||
<div class="section-title">Recent tail ({{ session.log_tail | length }} line{{ 's' if session.log_tail | length != 1 }})</div>
|
||||
<pre class="mono mb-0" style="max-height: 12rem; overflow: auto; background: var(--bg); padding: 0.75rem; border-radius: 0.25rem;">{% for line in session.log_tail %}{{ line }}
|
||||
<section class="panel id-log" aria-labelledby="tailHead">
|
||||
<div class="id-loghead"><h2 id="tailHead">Recent tail</h2><span class="sub">{{ session.log_tail | length }} line{{ 's' if session.log_tail | length != 1 }}</span></div>
|
||||
<pre class="id-pre id-tail" tabindex="0" aria-label="Recent log tail">{% for line in session.log_tail %}{{ line }}
|
||||
{% endfor %}</pre>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
function copyText(text) {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
return navigator.clipboard.writeText(text);
|
||||
}
|
||||
return new Promise(function(resolve, reject) {
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.position = 'fixed'; ta.style.left = '-9999px';
|
||||
document.body.appendChild(ta);
|
||||
ta.focus(); ta.select();
|
||||
try {
|
||||
var ok = document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
if (ok) resolve(); else reject(new Error('execCommand returned false'));
|
||||
} catch (err) {
|
||||
document.body.removeChild(ta);
|
||||
reject(err);
|
||||
}
|
||||
// Copy log: the log can be up to 1 MB, so it is read from the <pre> rather
|
||||
// than carried in a data-copy-text attribute. Same fallback as static/app.js
|
||||
// (the LAN is plain http, so navigator.clipboard is often unavailable).
|
||||
(function () {
|
||||
function copyText(text) {
|
||||
if (navigator.clipboard && window.isSecureContext) return navigator.clipboard.writeText(text);
|
||||
return new Promise(function (resolve, reject) {
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = text; ta.setAttribute('readonly', ''); ta.style.position = 'fixed'; ta.style.opacity = '0';
|
||||
document.body.appendChild(ta); ta.select();
|
||||
var ok = false;
|
||||
try { ok = document.execCommand('copy'); } catch (err) { ok = false; }
|
||||
document.body.removeChild(ta);
|
||||
if (ok) resolve(); else reject(new Error('copy failed'));
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
var logBtn = e.target.closest('.copy-log-btn');
|
||||
var summaryBtn = e.target.closest('.copy-summary-btn');
|
||||
if (logBtn) {
|
||||
var pre = document.getElementById('full-log');
|
||||
if (pre) {
|
||||
copyText(pre.textContent).then(function() {
|
||||
logBtn.innerHTML = '<i class="bi bi-check2"></i> copied!';
|
||||
setTimeout(function() { logBtn.innerHTML = '<i class="bi bi-clipboard"></i> Copy'; }, 1200);
|
||||
});
|
||||
}
|
||||
} else if (summaryBtn) {
|
||||
var lines = [];
|
||||
document.querySelectorAll('dl dt').forEach(function(dt) {
|
||||
var dd = dt.nextElementSibling;
|
||||
if (dd) {
|
||||
lines.push(dt.textContent.trim() + ': ' + dd.textContent.trim().replace(/\s+/g, ' '));
|
||||
}
|
||||
});
|
||||
var rows = document.querySelectorAll('table tbody tr');
|
||||
if (rows.length) {
|
||||
lines.push('');
|
||||
lines.push('Stage timeline:');
|
||||
rows.forEach(function(tr) {
|
||||
var cells = tr.querySelectorAll('td');
|
||||
if (cells.length === 4) {
|
||||
lines.push(' - ' + cells[0].textContent.trim() + ' stage ' + cells[1].textContent.trim()
|
||||
+ ' ' + cells[2].textContent.trim() + ' ' + cells[3].textContent.trim());
|
||||
}
|
||||
});
|
||||
}
|
||||
copyText(lines.join('\n')).then(function() {
|
||||
summaryBtn.innerHTML = '<i class="bi bi-check2"></i> copied!';
|
||||
setTimeout(function() { summaryBtn.innerHTML = '<i class="bi bi-clipboard"></i> Copy support summary'; }, 1200);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest && e.target.closest('.copy-log-btn');
|
||||
if (!btn) return;
|
||||
var pre = document.getElementById('full-log');
|
||||
if (!pre) return;
|
||||
var done = function (ok) {
|
||||
btn.textContent = ok ? 'Copied' : 'Copy failed';
|
||||
setTimeout(function () { btn.textContent = 'Copy log'; }, 1400);
|
||||
};
|
||||
copyText(pre.textContent).then(function () { done(true); }, function () { done(false); });
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user