Files
Goddess/ui/index.html
TutorialsGHG b011c7ad75 Initial commit
2026-04-12 21:57:21 +02:00

611 lines
29 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>NotifyPulse</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0d0d0f;
--card: #16161a;
--border: #2a2a32;
--accent: #4f8ef7;
--accent2: #7c5cfc;
--text: #e2e2e8;
--muted: #6b6b7b;
--green: #22c55e;
--red: #ef4444;
--yellow: #f59e0b;
--mono: 'Cascadia Code', 'Consolas', monospace;
}
body { background: var(--bg); color: var(--text); font-family: 'Segoe UI', system-ui, sans-serif; min-height: 100vh; }
header {
display: flex; align-items: center; justify-content: space-between;
padding: 16px 28px; border-bottom: 1px solid var(--border); background: var(--card);
}
header h1 { font-size: 1.25rem; font-weight: 700; letter-spacing: .03em; color: var(--accent); }
.header-right { display: flex; align-items: center; gap: 10px; }
.pwa-pill {
display: flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: 11px;
background: var(--bg); border: 1px solid var(--border); padding: 4px 10px; border-radius: 20px; color: var(--muted);
}
.pwa-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); transition: .3s; }
.pwa-dot.on { background: var(--green); box-shadow: 0 0 6px var(--green); }
.status-pill {
display: flex; align-items: center; gap: 8px; padding: 6px 14px; border-radius: 999px;
font-size: .8rem; font-weight: 600; border: 1px solid var(--border);
}
.dot { width: 8px; height: 8px; border-radius: 50%; }
.running .dot { background: var(--green); box-shadow: 0 0 6px var(--green); }
.paused .dot { background: var(--yellow); box-shadow: 0 0 6px var(--yellow); }
main { max-width: 1100px; margin: 0 auto; padding: 28px 20px; display: grid; gap: 20px; grid-template-columns: 1fr 1fr; }
@media(max-width: 700px) { main { grid-template-columns: 1fr; } }
.full-width { grid-column: 1 / -1; }
.card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 20px; }
.card h2 { font-size: .72rem; text-transform: uppercase; letter-spacing: .1em; color: var(--muted); margin-bottom: 14px; }
#countdown-ring { display: flex; align-items: center; justify-content: center; flex-direction: column; padding: 8px 0; }
.ring-wrap { position: relative; width: 130px; height: 130px; }
.ring-wrap svg { transform: rotate(-90deg); }
.ring-bg { fill: none; stroke: var(--border); stroke-width: 10; }
.ring-arc { fill: none; stroke: var(--accent); stroke-width: 10; stroke-linecap: round; transition: stroke-dashoffset .9s linear; }
.ring-label { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.ring-label .big { font-size: 1.5rem; font-weight: 700; line-height: 1; color: var(--accent); }
.ring-label .small { font-size: .65rem; color: var(--muted); margin-top: 3px; }
#next-label { margin-top: 10px; font-size: .8rem; color: var(--muted); text-align: center; }
.stat-row { display: grid; grid-template-columns: repeat(3,1fr); gap: 10px; margin-top: 14px; }
.stat { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; }
.stat-label { font-size: 10px; color: var(--muted); font-family: var(--mono); letter-spacing: .06em; margin-bottom: 4px; }
.stat-val { font-size: 1.2rem; font-weight: 700; color: var(--text); }
.stat-val.green { color: var(--green); }
.stat-val.red { color: var(--red); }
.stat-val.accent { color: var(--accent); }
.btn { flex: 1; padding: 9px 14px; border: none; border-radius: 8px; cursor: pointer; font-size: .83rem; font-weight: 600; transition: opacity .15s, transform .1s; display: inline-flex; align-items: center; gap: 5px; justify-content: center; }
.btn:hover { opacity: .85; transform: translateY(-1px); }
.btn:active { transform: scale(.97); }
.btn-primary { background: var(--accent); color: #fff; }
.btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text); }
.btn-danger { background: #2d1a1a; border: 1px solid var(--red); color: var(--red); }
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; }
.field { margin-bottom: 13px; }
.field:last-child { margin-bottom: 0; }
.field label { display: block; font-size: .72rem; color: var(--muted); font-family: var(--mono); margin-bottom: 5px; letter-spacing: .04em; }
.field input[type=text], .field input[type=number], .field textarea {
width: 100%; padding: 8px 11px; background: var(--bg); border: 1px solid var(--border);
border-radius: 7px; color: var(--text); font-size: .85rem; outline: none; transition: border-color .15s;
}
.field input:focus, .field textarea:focus { border-color: var(--accent); }
.field textarea { resize: vertical; min-height: 160px; font-family: var(--mono); font-size: .78rem; line-height: 1.55; }
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.save-msg { font-size: .72rem; color: var(--green); min-height: 14px; margin-top: 5px; font-family: var(--mono); }
.save-msg.err { color: var(--red); }
.toggle-row { display: flex; align-items: center; justify-content: space-between; padding: 9px 11px; background: var(--bg); border: 1px solid var(--border); border-radius: 7px; margin-bottom: 8px; }
.toggle-row:last-child { margin-bottom: 0; }
.toggle-label { font-size: .83rem; color: var(--text); }
.toggle { position: relative; width: 38px; height: 21px; flex-shrink: 0; }
.toggle input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; inset: 0; background: var(--border); border-radius: 11px; cursor: pointer; transition: .2s; }
.slider:before { content: ''; position: absolute; width: 15px; height: 15px; top: 3px; left: 3px; background: #fff; border-radius: 50%; transition: .2s; }
.toggle input:checked + .slider { background: var(--accent); }
.toggle input:checked + .slider:before { transform: translateX(17px); }
input[type=range] { -webkit-appearance: none; height: 4px; border-radius: 2px; background: var(--border); outline: none; width: 100%; margin-top: 6px; }
input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; background: var(--accent); cursor: pointer; }
.entry-list { list-style: none; max-height: 220px; overflow-y: auto; }
.entry-list li { display: flex; justify-content: space-between; align-items: center; padding: 7px 0; border-bottom: 1px solid var(--border); font-size: .82rem; gap: 10px; }
.entry-list li:last-child { border-bottom: none; }
.entry-tag { flex-shrink: 0; padding: 2px 7px; border-radius: 4px; font-size: .7rem; font-family: var(--mono); font-weight: 600; }
.tag-pct { background: rgba(79,142,247,.15); color: var(--accent); }
.tag-time { background: rgba(245,158,11,.15); color: var(--yellow); }
.tag-special{ background: rgba(124,92,252,.15); color: var(--accent2); }
.prog-big { font-size: 2.4rem; font-weight: 700; color: var(--accent); line-height: 1; }
.prog-sub { font-size: .75rem; color: var(--muted); margin-top: 4px; }
.prog-row { display: flex; gap: 14px; margin-top: 14px; flex-wrap: wrap; }
.prog-cell { flex: 1; min-width: 80px; background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; }
.prog-cell .label { font-size: .7rem; color: var(--muted); text-transform: uppercase; letter-spacing: .07em; }
.prog-cell .val { font-size: 1.15rem; font-weight: 700; color: var(--text); margin-top: 2px; }
.log-list { list-style: none; max-height: 240px; overflow-y: auto; }
.log-list li { display: grid; grid-template-columns: 52px 1fr; gap: 10px; padding: 6px 0; border-bottom: 1px solid var(--border); font-size: .78rem; }
.log-list li:last-child { border-bottom: none; }
.log-time { color: var(--muted); font-family: var(--mono); }
.log-msg { color: var(--text); }
.log-msg.error { color: var(--red); }
.log-msg.warn { color: var(--yellow); }
.card-heading { display:flex; align-items:center; justify-content:space-between; gap:12px; }
.display-mode-toggle { display:flex; gap:5px; align-items:center; }
.toggle-pill { border:none; border-radius:999px; padding:4px 10px; font-size:.7rem; font-weight:600; background: var(--bg); color: var(--muted); border:1px solid var(--border); cursor:pointer; transition:.2s; }
.toggle-pill.active { background: var(--accent); color:#fff; border-color: var(--accent); }
.pwa-url-box { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px; margin-bottom: 12px; }
.pwa-url-label { font-size: 10px; color: var(--muted); font-family: var(--mono); margin-bottom: 4px; }
.pwa-url-val { font-family: var(--mono); font-size: .82rem; color: var(--accent); word-break: break-all; }
.divider { border: none; border-top: 1px solid var(--border); margin: 16px 0; }
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
body.is-paused header, body.is-paused main { display: none; }
</style>
</head>
<body>
<header>
<h1 id="app-name">NotifyPulse</h1>
<div class="header-right">
<div class="pwa-pill">
<div class="pwa-dot" id="pwa-dot"></div>
<span id="pwa-count">0 PWA</span>
</div>
<div id="status-pill" class="status-pill running">
<div class="dot"></div>
<span id="status-text">Running</span>
</div>
</div>
</header>
<main>
<!-- Countdown -->
<div class="card">
<h2>Next Notification</h2>
<div id="countdown-ring">
<div class="ring-wrap">
<svg viewBox="0 0 130 130" width="130" height="130">
<circle class="ring-bg" cx="65" cy="65" r="55"/>
<circle class="ring-arc" cx="65" cy="65" r="55" id="arc"
stroke-dasharray="345.4" stroke-dashoffset="0"/>
</svg>
<div class="ring-label">
<span class="big" id="cd-time">--</span>
<span class="small" id="cd-label"></span>
</div>
</div>
<div id="next-label">Loading…</div>
</div>
<div class="stat-row">
<div class="stat">
<div class="stat-label">STATUS</div>
<div class="stat-val green" id="s-status">Running</div>
</div>
<div class="stat">
<div class="stat-label">INTERVAL</div>
<div class="stat-val accent" id="s-interval"></div>
</div>
<div class="stat">
<div class="stat-label">ENTRIES</div>
<div class="stat-val" id="s-entries"></div>
</div>
</div>
</div>
<!-- Controls -->
<div class="card">
<h2>Controls</h2>
<div class="btn-row">
<button class="btn btn-primary" onclick="togglePause()" id="pause-btn" style="flex:2">⏸ Pause / Resume</button>
<button class="btn btn-outline" onclick="fireNow()" style="flex:1">⚡ Fire Now</button>
</div>
<div style="margin-top:16px; border-top:1px solid var(--border); padding-top:14px">
<div style="font-size:.68rem;text-transform:uppercase;letter-spacing:.1em;color:var(--muted);margin-bottom:10px">🖥️ Desktop Tests</div>
<div class="btn-row">
<button class="btn btn-outline" onclick="testNotif()">🔔 Toast</button>
<button class="btn btn-outline" onclick="testWallpaper()">🖼️ Wallpaper</button>
<button class="btn btn-outline" onclick="testOverlay()">✨ Overlay</button>
</div>
</div>
<div style="margin-top:14px; border-top:1px solid var(--border); padding-top:14px">
<div style="font-size:.68rem;text-transform:uppercase;letter-spacing:.1em;color:var(--muted);margin-bottom:10px">📱 Mobile Tests</div>
<div class="btn-row">
<button class="btn btn-outline" onclick="testMobileLockscreen()">🔒 Lockscreen WP</button>
<button class="btn btn-outline" onclick="testMobileBackground()">🏠 Home WP</button>
</div>
<div class="btn-row" style="margin-top:8px">
<button class="btn btn-outline" onclick="testMobileWallpaper()">📱 Both WPs</button>
<button class="btn btn-outline" onclick="testMobileNotif()">📳 Push Ping</button>
</div>
</div>
<hr class="divider">
<h2 style="margin-bottom:12px">Mobile PWA</h2>
<div class="pwa-url-box">
<div class="pwa-url-label">PWA URL — open on your phone (same network)</div>
<div class="pwa-url-val" id="pwa-url">Loading…</div>
</div>
<div class="stat" style="margin-top:10px">
<div class="stat-label">CONNECTED CLIENTS</div>
<div class="stat-val" id="client-count">0</div>
</div>
</div>
<!-- Settings -->
<div class="card">
<h2>Settings</h2>
<div class="two-col">
<div class="field">
<label>APP NAME</label>
<input id="s-name" type="text" placeholder="NotifyPulse" oninput="markDirty()">
</div>
<div class="field">
<label>HOTKEY</label>
<input id="s-hotkey" type="text" placeholder="F13" oninput="markDirty()">
</div>
<div class="field">
<label>MIN INTERVAL (min)</label>
<input id="s-min" type="number" min="1" placeholder="10" oninput="markDirty()">
</div>
<div class="field">
<label>MAX INTERVAL (min)</label>
<input id="s-max" type="number" min="1" placeholder="30" oninput="markDirty()">
</div>
</div>
<div style="margin: 14px 0 12px; border-top: 1px solid var(--border); padding-top: 12px;">
<div class="toggle-row">
<span class="toggle-label">Startup toast</span>
<label class="toggle"><input type="checkbox" id="t-startup"><span class="slider"></span></label>
</div>
<div class="toggle-row">
<span class="toggle-label">Notification sound</span>
<label class="toggle"><input type="checkbox" id="t-sound"><span class="slider"></span></label>
</div>
<div class="toggle-row">
<span class="toggle-label">Auto-open browser on start</span>
<label class="toggle"><input type="checkbox" id="t-browser"><span class="slider"></span></label>
</div>
</div>
<button class="btn btn-primary" onclick="saveSettings()" style="width:100%">Save Settings</button>
<div class="save-msg" id="save-msg"></div>
</div>
<!-- Overlay + Advanced -->
<div class="card">
<h2>Overlay Settings</h2>
<div class="two-col">
<div class="field">
<label>DEFAULT DURATION (sec)</label>
<input id="s-ovr-dur" type="number" min="1" max="300" oninput="markDirty()">
</div>
<div class="field">
<label>OPACITY — <span id="opacity-val">0.4</span></label>
<input type="range" id="s-ovr-opacity" min="0.05" max="1" step="0.05"
oninput="document.getElementById('opacity-val').textContent=this.value; markDirty()">
</div>
</div>
<div class="toggle-row" style="margin-top:10px">
<span class="toggle-label">Stretch to fill (vs letterbox fit)</span>
<label class="toggle"><input type="checkbox" id="t-stretch" onchange="markDirty()"><span class="slider"></span></label>
</div>
<div class="btn-row" style="margin-top:12px">
<button class="btn btn-primary" onclick="saveSettings()">Save</button>
<button class="btn btn-outline" onclick="testOverlay()">Preview Overlay</button>
</div>
<div class="save-msg" id="save-msg2"></div>
<hr class="divider">
<h2 style="margin-bottom:12px">Advanced</h2>
<div class="field">
<label>PWA AUTH TOKEN (optional)</label>
<input type="text" id="s-token" placeholder="leave empty = no auth" oninput="markDirty()">
</div>
<div class="btn-row" style="margin-top:8px">
<button class="btn btn-danger" onclick="if(confirm('Reset all settings to defaults?'))resetSettings()">Reset to Defaults</button>
</div>
</div>
<!-- Active Entries -->
<div class="card">
<div class="card-heading">
<h2>Active Entries</h2>
<div class="display-mode-toggle" id="display-mode-toggle">
<button type="button" class="toggle-pill active" data-mode="percent">Percent</button>
<button type="button" class="toggle-pill" data-mode="chance">Chance</button>
</div>
</div>
<ul class="entry-list" id="entry-list"></ul>
</div>
<!-- Prognosis -->
<div class="card">
<h2>Prognosis — next hour</h2>
<div style="display:flex;align-items:flex-end;gap:14px;flex-wrap:wrap">
<div>
<div class="prog-big" id="prog-total"></div>
<div class="prog-sub">estimated notifications</div>
</div>
<div class="prog-sub" style="margin-bottom:5px;flex:1" id="prog-range"></div>
</div>
<div class="prog-row">
<div class="prog-cell"><div class="label">Random</div><div class="val" id="prog-random"></div></div>
<div class="prog-cell"><div class="label">Timed (this hour)</div><div class="val" id="prog-timed"></div></div>
</div>
</div>
<!-- Editor -->
<div class="card full-width">
<h2>Edit notifications.txt</h2>
<div class="pwa-url-box" style="margin-bottom:10px">
<div class="pwa-url-label">ACTIVE CONFIG FILE</div>
<div class="pwa-url-val" id="config-path">-</div>
</div>
<div class="btn-row" style="margin-bottom:10px">
<button class="btn btn-primary" style="flex:0" onclick="saveEditor()">💾 Save</button>
<button class="btn btn-outline" style="flex:0" onclick="loadEditor()">↺ Reload</button>
<span class="save-msg" id="editor-msg" style="margin-top:0;align-self:center"></span>
</div>
<div class="field" style="margin-bottom:0">
<textarea id="editor-textarea" spellcheck="false" placeholder="One notification per line."></textarea>
</div>
</div>
<!-- Log -->
<div class="card full-width">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
<h2 style="margin-bottom:0">Log</h2>
<button class="btn btn-outline" style="flex:0;padding:4px 10px;font-size:.7rem" onclick="clearLogUI()">Clear</button>
</div>
<ul class="log-list" id="log-list"></ul>
</div>
</main>
<script>
let state = {};
let _logCache = [];
let totalInterval = 0;
let settingsDirty = false;
const ENTRY_DISPLAY_MODE_KEY = 'entryDisplayMode';
let entryDisplayMode = localStorage.getItem(ENTRY_DISPLAY_MODE_KEY) || 'percent';
function markDirty() { settingsDirty = true; }
function setEntryDisplayMode(mode) {
if (mode !== 'percent' && mode !== 'chance') return;
if (entryDisplayMode === mode) return;
entryDisplayMode = mode;
localStorage.setItem(ENTRY_DISPLAY_MODE_KEY, mode);
updateDisplayModeToggleButtons();
updateDisplayModeToggleButtons();
renderEntries(state.entries || []);
}
function updateDisplayModeToggleButtons() {
const toggle = document.getElementById('display-mode-toggle');
if (!toggle) return;
toggle.querySelectorAll('[data-mode]').forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === entryDisplayMode);
});
}
function initDisplayModeToggle() {
const toggle = document.getElementById('display-mode-toggle');
if (!toggle) return;
toggle.addEventListener('click', (event) => {
const btn = event.target.closest('[data-mode]');
if (!btn) return;
setEntryDisplayMode(btn.dataset.mode);
});
updateDisplayModeToggleButtons();
}
async function fetchState() {
try {
const r = await fetch('/api/state');
state = await r.json();
_logCache = state.log || [];
updateUI();
} catch(e) {}
}
function updateUI() {
const paused = state.paused;
const appName = state.app_name || 'NotifyPulse';
document.getElementById('app-name').textContent = appName;
document.title = paused ? 'Paused' : appName;
document.body.classList.toggle('is-paused', paused);
const pill = document.getElementById('status-pill');
pill.className = 'status-pill ' + (paused ? 'paused' : 'running');
document.getElementById('status-text').textContent = paused ? 'Paused' : 'Running';
document.getElementById('s-status').textContent = paused ? 'Paused' : 'Running';
document.getElementById('s-status').className = 'stat-val ' + (paused ? 'red' : 'green');
document.getElementById('s-interval').textContent = (state.min_interval||'?') + '' + (state.max_interval||'?') + ' min';
document.getElementById('s-entries').textContent = (state.entries||[]).length;
const pwa = state.pwa_clients || 0;
document.getElementById('pwa-count').textContent = pwa + ' PWA';
document.getElementById('pwa-dot').classList.toggle('on', pwa > 0);
document.getElementById('client-count').textContent = pwa;
document.getElementById('config-path').textContent = state.config_path || '-';
const host = window.location.hostname;
const port = window.location.port || 5000;
document.getElementById('pwa-url').textContent = 'http://' + host + ':' + port + '/pwa';
// Countdown ring
const now = Date.now() / 1000;
const secsLeft = Math.max(0, (state.next_fire_at || 0) - now);
totalInterval = state.total_interval || ((state.max_interval || 30) * 60);
const pct = totalInterval > 0 ? secsLeft / totalInterval : 1;
document.getElementById('arc').style.strokeDashoffset = 345.4 * (1 - pct);
const m = Math.floor(secsLeft / 60);
const s = Math.floor(secsLeft % 60);
document.getElementById('cd-time').textContent = paused ? '--' : (m + 'm ' + String(s).padStart(2,'0') + 's');
document.getElementById('next-label').textContent = paused
? 'Paused — notifications suspended'
: 'Next in ' + m + 'm ' + String(s).padStart(2,'0') + 's';
document.getElementById('pause-btn').textContent = paused ? '▶ Resume' : '⏸ Pause';
if (!settingsDirty) {
const s2 = state.settings || {};
setVal('s-name', s2.name || state.app_name || '');
setVal('s-hotkey', s2.hotkey || 'F13');
setVal('s-min', state.min_interval || 10);
setVal('s-max', state.max_interval || 30);
setVal('s-ovr-dur', s2.overlay_duration || 6);
setVal('s-ovr-opacity', s2.overlay_opacity || 0.4);
document.getElementById('opacity-val').textContent = s2.overlay_opacity || 0.4;
setVal('s-token', s2.pwa_token || '');
setCheck('t-startup', s2.startup_toast !== false);
setCheck('t-sound', s2.notify_sound !== false);
setCheck('t-browser', s2.auto_open_browser !== false);
setCheck('t-stretch', !!s2.overlay_stretch);
}
renderEntries(state.entries || []);
updatePrognosis(state.min_interval, state.max_interval, state.entries || []);
renderLog();
}
function renderEntries(entries) {
const totalWeight = entries.filter(e => e.weight != null).reduce((s,e) => s + e.weight, 0);
document.getElementById('entry-list').innerHTML = entries.map(e => {
let tag = '';
if (e.trigger_time) {
tag = '<span class="entry-tag tag-time">⏰ ' + e.trigger_time + '</span>';
} else if (/wallpaper|overlay/i.test(e.text)) {
tag = '<span class="entry-tag tag-special">' + (/wallpaper/i.test(e.text) ? '🖼 wallpaper' : '✨ overlay') + '</span>';
} else if (e.weight != null) {
const weight = Number(e.weight);
if (totalWeight > 0 && weight > 0) {
if (entryDisplayMode === 'chance') {
const chance = totalWeight / weight;
tag = '<span class="entry-tag tag-pct">' + formatChanceLabel(chance) + '</span>';
} else {
const pct = weight / totalWeight * 100;
tag = '<span class="entry-tag tag-pct">' + pct.toFixed(1) + '%</span>';
}
}
}
return '<li><span>' + escHtml(e.text) + '</span>' + tag + '</li>';
}).join('');
}
function formatChanceLabel(value) {
if (!isFinite(value) || value <= 0) return '1 in ?';
if (value < 2) return '1 in 1';
if (value < 10) return '1 in ' + value.toFixed(1);
return '1 in ' + Math.round(value);
}
function updatePrognosis(minInt, maxInt, entries) {
const avg = ((minInt||10) + (maxInt||30)) / 2;
const rph = 60 / avg;
const now = new Date();
const nowMins = now.getHours() * 60 + now.getMinutes();
const endMins = nowMins + 60;
let timed = 0;
for (const e of entries) {
if (e.trigger_time) {
const [hh,mm] = e.trigger_time.split(':').map(Number);
const em = hh * 60 + mm;
if (endMins >= 1440) { if (em >= nowMins || em < endMins - 1440) timed++; }
else { if (em >= nowMins && em < endMins) timed++; }
}
}
document.getElementById('prog-total').textContent = (rph + timed).toFixed(1);
document.getElementById('prog-random').textContent = rph.toFixed(1);
document.getElementById('prog-timed').textContent = timed;
document.getElementById('prog-range').textContent =
Math.floor(60/(maxInt||30)) + '' + Math.ceil(60/(minInt||10)) + ' random + ' + timed + ' timed';
}
function renderLog() {
document.getElementById('log-list').innerHTML = (_logCache||[]).map(e =>
'<li><span class="log-time">' + e.time + '</span><span class="log-msg ' + (e.level||'info') + '">' + escHtml(e.msg) + '</span></li>'
).join('');
}
function clearLogUI() { _logCache = []; renderLog(); }
async function togglePause() {
const r = await fetch('/api/pause', {method:'POST'});
const d = await r.json();
document.body.classList.toggle('is-paused', d.paused);
document.getElementById('status-pill').className = 'status-pill ' + (d.paused ? 'paused' : 'running');
document.getElementById('status-text').textContent = d.paused ? 'Paused' : 'Running';
document.getElementById('pause-btn').textContent = d.paused ? '▶ Resume' : '⏸ Pause';
fetchState();
}
async function fireNow() { await fetch('/api/fire_now',{method:'POST'}); fetchState(); }
async function testNotif() { await fetch('/api/test_notification',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:'Test from Web UI!'})}); }
async function testWallpaper() { await fetch('/api/test_wallpaper',{method:'POST'}); }
async function testOverlay() { await fetch('/api/test_overlay',{method:'POST'}); }
async function testMobileWallpaper() { await fetch('/api/test_mobile_wallpaper',{method:'POST'}); }
async function testMobileLockscreen(){ await fetch('/api/test_mobile_wallpaper',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type:'lockscreen'})}); }
async function testMobileBackground(){ await fetch('/api/test_mobile_wallpaper',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type:'background'})}); }
async function testMobileNotif() { await fetch('/api/test_notification',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:'Mobile ping test!',mobile:true})}); }
async function loadEditor() {
try { const r = await fetch('/api/entries'); const d = await r.json(); document.getElementById('editor-textarea').value = d.content||''; } catch(e){}
}
async function saveEditor() {
const msg = document.getElementById('editor-msg');
try {
const r = await fetch('/api/entries',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({content:document.getElementById('editor-textarea').value})});
const d = await r.json();
msg.textContent = d.ok ? '✓ Saved' : '✗ Error'; msg.className='save-msg'+(d.ok?'':' err');
setTimeout(()=>{msg.textContent='';},3000);
if (d.ok) fetchState();
} catch(e){ msg.textContent='✗ Network error'; msg.className='save-msg err'; }
}
async function saveSettings() {
const payload = {
name: getVal('s-name'), hotkey: getVal('s-hotkey'),
min_interval: parseInt(getVal('s-min')), max_interval: parseInt(getVal('s-max')),
overlay_duration: parseInt(getVal('s-ovr-dur')), overlay_opacity: parseFloat(getVal('s-ovr-opacity')),
overlay_stretch: getCheck('t-stretch'), startup_toast: getCheck('t-startup'),
notify_sound: getCheck('t-sound'), auto_open_browser: getCheck('t-browser'),
pwa_token: getVal('s-token'),
};
try {
const r = await fetch('/api/settings',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
const d = await r.json();
['save-msg','save-msg2'].forEach(id=>{
const el=document.getElementById(id);
if(el){el.textContent=d.ok?'✓ Saved!':'✗ Error';el.className='save-msg'+(d.ok?'':' err');}
});
settingsDirty = false;
setTimeout(()=>['save-msg','save-msg2'].forEach(id=>{const el=document.getElementById(id);if(el)el.textContent='';}),2500);
fetchState();
} catch(e){}
}
async function resetSettings() {
await fetch('/api/settings',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({overlay_stretch:false,overlay_opacity:0.4,overlay_duration:6,startup_toast:true,notify_sound:true,auto_open_browser:true})});
settingsDirty=false; fetchState();
}
function setVal(id,v){const el=document.getElementById(id);if(el)el.value=v;}
function setCheck(id,v){const el=document.getElementById(id);if(el)el.checked=v;}
function getVal(id){return document.getElementById(id)?.value;}
function getCheck(id){return document.getElementById(id)?.checked;}
function escHtml(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
fetchState();
loadEditor();
initDisplayModeToggle();
setInterval(fetchState, 1000);
</script>
</body>
</html>