251 lines
7.4 KiB
HTML
251 lines
7.4 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Power Options</title>
|
|
<link rel="icon" type="image/x-icon" href="/static/images/favicon.ico" />
|
|
<link rel="stylesheet" href="/static/css/styles.css" />
|
|
<link rel="stylesheet" href="/static/fontawesome/css/all.min.css" />
|
|
<style>
|
|
:root{
|
|
--brand: #1e40af; /* blue-800 */
|
|
--brand-hover: #15327f;
|
|
--warn: #f59e0b; /* amber-500 */
|
|
--warn-hover:#d97706;
|
|
--danger: #ef4444; /* red-500 */
|
|
--danger-hover:#dc2626;
|
|
--border: #d1d5db; /* gray-300 */
|
|
--text: #111827; /* gray-900 */
|
|
--muted: #4b5563; /* gray-600 */
|
|
--bg: #ffffff;
|
|
--bg-alt: #f9fafb; /* gray-50 */
|
|
--focus: #2563eb; /* blue-600 */
|
|
}
|
|
@media (prefers-color-scheme: dark){
|
|
:root{
|
|
--brand: #60a5fa;
|
|
--brand-hover:#3b82f6;
|
|
--warn:#fbbf24;
|
|
--warn-hover:#f59e0b;
|
|
--danger:#f87171;
|
|
--danger-hover:#ef4444;
|
|
--border:#374151;
|
|
--text:#e5e7eb;
|
|
--muted:#9ca3af;
|
|
--bg:#0b0f14;
|
|
--bg-alt:#111827;
|
|
--focus:#60a5fa;
|
|
}
|
|
}
|
|
|
|
*{ box-sizing: border-box; }
|
|
html, body{ height: 100%; }
|
|
body{
|
|
margin: 0;
|
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
|
color: var(--text);
|
|
background: var(--bg);
|
|
line-height: 1.55;
|
|
}
|
|
|
|
#navbar{ position: sticky; top: 0; z-index: 20; }
|
|
|
|
.content-wrapper{
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
padding: 16px;
|
|
}
|
|
@media (min-width: 980px){
|
|
.content-wrapper{ padding: 24px; }
|
|
}
|
|
|
|
.card{
|
|
padding: 20px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
background: var(--bg-alt);
|
|
}
|
|
|
|
.center-content{
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
text-align: center;
|
|
gap: 14px;
|
|
}
|
|
|
|
h1{ margin: 0; font-size: clamp(1.5rem, 2vw, 2rem); }
|
|
.muted{ color: var(--muted); }
|
|
|
|
.header-img{
|
|
margin: 10px 0 6px;
|
|
width: auto;
|
|
height: 72px;
|
|
}
|
|
|
|
.actions{
|
|
display: flex;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
button{
|
|
appearance: none;
|
|
border: 1px solid transparent;
|
|
border-radius: 10px;
|
|
padding: 10px 16px;
|
|
font-size: 1rem;
|
|
cursor: pointer;
|
|
color: #fff;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
button:focus-visible{
|
|
outline: 3px solid var(--focus);
|
|
outline-offset: 2px;
|
|
}
|
|
.btn-warn{
|
|
background: var(--warn);
|
|
border-color: var(--warn-hover);
|
|
}
|
|
.btn-warn:hover{ background: var(--warn-hover); }
|
|
.btn-danger{
|
|
background: var(--danger);
|
|
border-color: var(--danger-hover);
|
|
}
|
|
.btn-danger:hover{ background: var(--danger-hover); }
|
|
|
|
.status{
|
|
min-height: 1.2em;
|
|
margin-top: 6px;
|
|
font-size: 0.98rem;
|
|
}
|
|
.status[aria-live]{ /* ensure space even when empty */ display: block; }
|
|
|
|
.help{
|
|
margin-top: 4px;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.spinner{
|
|
display: inline-block;
|
|
width: 1em; height: 1em;
|
|
border: 2px solid currentColor;
|
|
border-right-color: transparent;
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
vertical-align: -2px;
|
|
margin-right: 6px;
|
|
}
|
|
@keyframes spin{ to { transform: rotate(360deg); } }
|
|
|
|
.sr-only{
|
|
position: absolute;
|
|
width: 1px; height: 1px;
|
|
padding: 0; margin: -1px;
|
|
overflow: hidden; clip: rect(0,0,1,1);
|
|
white-space: nowrap; border: 0;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="navbar" role="navigation" aria-label="Primary"></div>
|
|
<script>
|
|
fetch('/static/html/nav.html')
|
|
.then(r => r.ok ? r.text() : Promise.reject(r.status))
|
|
.then(html => { document.getElementById('navbar').innerHTML = html; })
|
|
.catch(() => {
|
|
document.getElementById('navbar').innerHTML =
|
|
'<div class="card" role="alert">Navigation failed to load.</div>';
|
|
});
|
|
</script>
|
|
|
|
<main class="content-wrapper">
|
|
<section class="card center-content" aria-labelledby="power-heading">
|
|
<h1 id="power-heading">Power Options</h1>
|
|
<img src="/static/images/switch-icon.png" alt="Power switch icon" class="header-img" />
|
|
<p class="muted help">
|
|
These actions affect the entire device. Save your work before proceeding.
|
|
</p>
|
|
|
|
<div class="actions" role="group" aria-label="Power controls">
|
|
<button type="button" id="reboot-button" class="btn-warn">
|
|
<i class="fa fa-rotate-right" aria-hidden="true"></i>
|
|
Reboot
|
|
</button>
|
|
<button type="button" id="shutdown-button" class="btn-danger">
|
|
<i class="fa fa-power-off" aria-hidden="true"></i>
|
|
Shut Down
|
|
</button>
|
|
</div>
|
|
|
|
<div id="status" class="status" role="status" aria-live="polite"></div>
|
|
</section>
|
|
</main>
|
|
|
|
<script>
|
|
const rebootBtn = document.getElementById('reboot-button');
|
|
const shutdownBtn = document.getElementById('shutdown-button');
|
|
const statusEl = document.getElementById('status');
|
|
|
|
function setBusy(btn, busy){
|
|
btn.disabled = busy;
|
|
if (busy) btn.setAttribute('aria-busy', 'true'); else btn.removeAttribute('aria-busy');
|
|
}
|
|
|
|
function setStatus(message, isError = false){
|
|
statusEl.textContent = '';
|
|
if (!message) return;
|
|
statusEl.innerHTML = (isError ? '' : '<span class="spinner"></span>') + message;
|
|
statusEl.style.color = isError ? 'var(--danger)' : 'inherit';
|
|
}
|
|
|
|
async function postAction(url, startMsg, successMsg){
|
|
setStatus(startMsg, false);
|
|
try{
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
if (!res.ok) throw new Error('Network error');
|
|
let data = null;
|
|
try { data = await res.json(); } catch {}
|
|
const ok = (data && (data.reply === true || data.ok === true)) || res.ok;
|
|
if (!ok) throw new Error((data && data.message) || 'Server error');
|
|
// Success: message without spinner
|
|
statusEl.innerHTML = successMsg;
|
|
statusEl.style.color = 'inherit';
|
|
} catch (e){
|
|
console.error(e);
|
|
setStatus('Operation failed. Please try again.', true);
|
|
alert('The request did not complete successfully.');
|
|
}
|
|
}
|
|
|
|
rebootBtn?.addEventListener('click', async () => {
|
|
const confirmed = confirm('Reboot now? All services will restart.');
|
|
if (!confirmed) return;
|
|
setBusy(rebootBtn, true);
|
|
setBusy(shutdownBtn, true);
|
|
await postAction('/reboot', 'Rebooting device… This may take up to a minute.', 'Reboot initiated.');
|
|
// Keep buttons disabled briefly to prevent repeats
|
|
setTimeout(() => { setBusy(rebootBtn, false); setBusy(shutdownBtn, false); }, 4000);
|
|
});
|
|
|
|
shutdownBtn?.addEventListener('click', async () => {
|
|
const confirmed = confirm('Shut down now? The device will power off.');
|
|
if (!confirmed) return;
|
|
setBusy(rebootBtn, true);
|
|
setBusy(shutdownBtn, true);
|
|
await postAction('/shutdown', 'Shutting down… The device will power off shortly.', 'Shutdown initiated.');
|
|
// Keep disabled; device may go offline
|
|
setTimeout(() => { setBusy(rebootBtn, false); setBusy(shutdownBtn, false); }, 4000);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|