Tonnage-app-IMCO/static/license.html

337 lines
17 KiB
HTML
Raw Normal View History

2026-04-23 10:08:35 +00:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Force Monitor — License</title>
<style>
:root {
--bg1:#050816; --bg2:#0b1224; --panel:rgba(255,255,255,0.055); --border:rgba(255,255,255,0.10);
--text:#f4f4f5; --muted:#a1a1aa; --muted2:#71717a; --btnbg:rgba(255,255,255,0.05); --shadow:0 16px 36px rgba(0,0,0,0.18);
--ok:#34d399; --warn:#facc15; --bad:#f87171; --info:#93c5fd;
}
body[data-theme="light"] {
--bg1:#eef4ff; --bg2:#f8fafc; --panel:rgba(255,255,255,0.84); --border:rgba(15,23,42,0.10);
--text:#0f172a; --muted:#475569; --muted2:#64748b; --btnbg:rgba(255,255,255,0.88); --shadow:0 16px 36px rgba(15,23,42,0.08);
--ok:#059669; --warn:#b45309; --bad:#dc2626; --info:#1d4ed8;
}
* { box-sizing:border-box; }
body {
margin:0; min-height:100vh; color:var(--text); font-family:'Segoe UI',system-ui,-apple-system,sans-serif;
background:
radial-gradient(circle at 10% 10%, rgba(34,211,238,0.12), transparent 18%),
radial-gradient(circle at 90% 10%, rgba(168,85,247,0.14), transparent 18%),
linear-gradient(180deg, var(--bg1) 0%, var(--bg2) 100%);
}
body[data-theme="light"] {
background:
radial-gradient(circle at 10% 10%, rgba(14,165,233,0.10), transparent 20%),
radial-gradient(circle at 90% 10%, rgba(168,85,247,0.10), transparent 18%),
linear-gradient(180deg, var(--bg1) 0%, var(--bg2) 100%);
}
.wrap { width:min(94vw, 1560px); margin:0 auto; padding:24px; }
.glass { background:var(--panel); border:1px solid var(--border); border-radius:24px; backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px); box-shadow:var(--shadow); }
.nav, .row, .toolbar { display:flex; flex-wrap:wrap; gap:12px; align-items:center; }
.nav { margin-bottom:18px; }
.btn, .input, textarea {
min-height:42px; border-radius:14px; border:1px solid var(--border); background:var(--btnbg); color:var(--text);
padding:10px 14px; font:inherit;
}
.btn { cursor:pointer; font-weight:600; text-decoration:none; display:inline-flex; align-items:center; justify-content:center; }
.btn.primary { background:rgba(14,165,233,0.14); border-color:rgba(14,165,233,0.35); }
.btn.good { background:rgba(16,185,129,0.14); border-color:rgba(16,185,129,0.35); }
.btn.warn { background:rgba(245,158,11,0.14); border-color:rgba(245,158,11,0.35); }
.grid { display:grid; gap:16px; }
.cards { grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); margin-bottom:18px; }
.card { padding:18px 20px; }
.kicker { font-size:11px; letter-spacing:.22em; color:var(--muted2); text-transform:uppercase; }
.value { font-size:30px; font-weight:800; margin-top:8px; }
.sub { color:var(--muted); margin-top:8px; font-size:14px; }
.mono { font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
textarea { width:100%; min-height:210px; resize:vertical; }
.two { grid-template-columns:1.1fr .9fr; }
pre {
margin:0; padding:16px; border-radius:18px; border:1px solid var(--border); background:rgba(2,6,23,0.35);
color:var(--text); white-space:pre-wrap; word-break:break-word; min-height:210px; overflow:auto;
}
body[data-theme="light"] pre { background:rgba(248,250,252,0.96); }
.badge { display:inline-flex; align-items:center; gap:8px; border-radius:999px; padding:6px 12px; font-weight:700; border:1px solid var(--border); }
.ok { color:var(--ok); }
.warnc { color:var(--warn); }
.bad { color:var(--bad); }
.info { color:var(--info); }
.field { margin-top:12px; }
.label { color:var(--muted2); font-size:12px; letter-spacing:.14em; text-transform:uppercase; margin-bottom:6px; }
.details { display:grid; grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); gap:12px; }
.detail-box { padding:14px 16px; border-radius:18px; border:1px solid var(--border); background:rgba(255,255,255,0.03); }
body[data-theme="light"] .detail-box { background:rgba(255,255,255,0.68); }
.message { margin-top:12px; padding:14px 16px; border-radius:16px; display:none; }
.message.show { display:block; }
.message.good { background:rgba(16,185,129,0.12); border:1px solid rgba(16,185,129,0.24); color:#bbf7d0; }
.message.bad { background:rgba(239,68,68,0.12); border:1px solid rgba(239,68,68,0.24); color:#fecaca; }
body[data-theme="light"] .message.good { color:#166534; }
body[data-theme="light"] .message.bad { color:#991b1b; }
.spacer { flex:1 1 auto; }
@media (max-width: 980px) { .two { grid-template-columns:1fr; } .wrap { width:min(96vw, 1560px); padding:16px; } }
</style>
</head>
<body data-theme="dark">
<div class="wrap">
<div class="nav">
<a class="btn" href="/">Dashboard</a>
<a class="btn" href="/history">History</a>
<a class="btn" href="/alarms">Alarms</a>
<a class="btn" href="/kiosk">Kiosk</a>
<a class="btn" href="/process-capability">Process capability</a>
<a class="btn" href="/reports">Reports</a>
<a class="btn primary" href="/license">License</a>
<div class="spacer"></div>
<button id="theme-toggle" class="btn" type="button">Light theme</button>
<button id="fullscreen-toggle" class="btn" type="button">Enter fullscreen</button>
<button id="refresh-btn" class="btn warn" type="button">Refresh</button>
</div>
<div class="glass card" style="margin-bottom:18px;">
<div class="row">
<div>
<div class="kicker">Force Monitor</div>
<h1 style="margin:8px 0 0 0; font-size:40px; line-height:1;">License Center</h1>
<div class="sub">Status, fingerprint, activation request, local license import, and signed license activation.</div>
</div>
<div class="spacer"></div>
<div class="row">
<span id="mode-badge" class="badge info">mode: loading</span>
<span id="lock-badge" class="badge">locked: --</span>
<span id="tamper-badge" class="badge">tamper: --</span>
</div>
</div>
<div id="action-message" class="message"></div>
</div>
<div class="grid cards">
<div class="glass card"><div class="kicker">Current mode</div><div id="metric-mode" class="value">--</div><div id="metric-mode-sub" class="sub">Loading...</div></div>
<div class="glass card"><div class="kicker">Days remaining</div><div id="metric-days" class="value mono">--</div><div class="sub">Trial or expiry information</div></div>
<div class="glass card"><div class="kicker">Activation configured</div><div id="metric-configured" class="value mono">--</div><div class="sub">Public key present or not</div></div>
<div class="glass card"><div class="kicker">Fingerprint</div><div id="metric-fingerprint" class="value mono" style="font-size:24px;">--</div><div class="sub">Short fingerprint</div></div>
</div>
<div class="grid two">
<div class="glass card">
<div class="row" style="justify-content:space-between; margin-bottom:10px;">
<div>
<div class="kicker">Activation request</div>
<div class="sub">Send this JSON to your private signing tool or license issuer.</div>
</div>
<div class="toolbar">
<button id="copy-request-btn" class="btn" type="button">Copy</button>
<button id="download-request-btn" class="btn good" type="button">Download</button>
</div>
</div>
<pre id="request-json">Loading activation request...</pre>
</div>
<div class="glass card">
<div class="row" style="justify-content:space-between; margin-bottom:10px;">
<div>
<div class="kicker">Activate signed license</div>
<div class="sub">Paste the signed license JSON or load it from a local file.</div>
</div>
<div class="toolbar">
<input id="license-file" type="file" accept="application/json,.json" class="btn" style="padding:8px 12px;">
<button id="activate-btn" class="btn primary" type="button">Activate</button>
</div>
</div>
<textarea id="license-text" class="mono" placeholder='{"app":"force_monitor",...}'></textarea>
</div>
</div>
<div class="glass card" style="margin-top:18px;">
<div class="kicker">Status details</div>
<div class="details" style="margin-top:14px;">
<div class="detail-box"><div class="label">Message</div><div id="detail-message">--</div></div>
<div class="detail-box"><div class="label">Customer</div><div id="detail-customer">--</div></div>
<div class="detail-box"><div class="label">License ID</div><div id="detail-license-id" class="mono">--</div></div>
<div class="detail-box"><div class="label">Hostname</div><div id="detail-hostname" class="mono">--</div></div>
<div class="detail-box"><div class="label">Fingerprint</div><div id="detail-fingerprint" class="mono">--</div></div>
<div class="detail-box"><div class="label">Trial window</div><div id="detail-trial-window">--</div></div>
<div class="detail-box"><div class="label">Expires at</div><div id="detail-expires">--</div></div>
<div class="detail-box"><div class="label">Features</div><div id="detail-features">--</div></div>
</div>
</div>
</div>
<script src="/static/app-common.js"></script>
<script>
let activationRequestText = '';
function escapeHtml(value) {
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
function setTheme(theme) {
const t = theme === 'light' ? 'light' : 'dark';
document.body.setAttribute('data-theme', t);
try { localStorage.setItem('force-monitor-theme', t); } catch (e) {}
const btn = document.getElementById('theme-toggle');
if (btn) btn.textContent = t === 'light' ? 'Dark theme' : 'Light theme';
}
function initTheme() {
let theme = 'dark';
try {
const stored = localStorage.getItem('force-monitor-theme');
if (stored === 'light' || stored === 'dark') theme = stored;
} catch (e) {}
setTheme(theme);
}
function setMessage(text, good) {
const box = document.getElementById('action-message');
if (!box) return;
box.textContent = text || '';
box.className = 'message' + (text ? ' show ' + (good ? 'good' : 'bad') : '');
}
function setDetail(id, value) {
const el = document.getElementById(id);
if (el) el.textContent = value || '--';
}
function setBadge(id, text, klass) {
const el = document.getElementById(id);
if (!el) return;
el.className = 'badge ' + (klass || '');
el.textContent = text;
}
function renderStatus(status) {
const mode = String(status.mode || '--');
const locked = !!status.locked;
const tampered = !!status.tampered;
const activationConfigured = !!status.activation_configured;
const daysRemaining = Number.isFinite(Number(status.days_remaining)) ? String(status.days_remaining) : '--';
document.getElementById('metric-mode').textContent = mode.toUpperCase();
document.getElementById('metric-mode-sub').textContent = status.message || '--';
document.getElementById('metric-days').textContent = daysRemaining;
document.getElementById('metric-configured').textContent = activationConfigured ? 'YES' : 'NO';
document.getElementById('metric-fingerprint').textContent = status.fingerprint_short || '--';
setBadge('mode-badge', 'mode: ' + mode, locked ? 'bad' : (mode === 'licensed' ? 'ok' : 'info'));
setBadge('lock-badge', 'locked: ' + (locked ? 'yes' : 'no'), locked ? 'bad' : 'ok');
setBadge('tamper-badge', 'tamper: ' + (tampered ? 'yes' : 'no'), tampered ? 'bad' : 'ok');
setDetail('detail-message', status.message || '--');
setDetail('detail-customer', status.customer || '--');
setDetail('detail-license-id', status.license_id || '--');
setDetail('detail-hostname', status.hostname || '--');
setDetail('detail-fingerprint', status.fingerprint || '--');
const trialWindow = (status.trial_started_at || '--') + ' → ' + (status.trial_expires_at || '--');
setDetail('detail-trial-window', trialWindow);
setDetail('detail-expires', status.expires_at || '--');
setDetail('detail-features', Array.isArray(status.features) && status.features.length ? status.features.join(', ') : '--');
}
async function refreshStatus() {
try {
const status = await AppUI.fetchJson('/api/license/status', { timeoutMs:8000 });
renderStatus(status);
} catch (err) {
console.warn('License status error:', err);
setMessage('Could not load license status.', false);
document.getElementById('metric-mode').textContent = 'ERROR';
document.getElementById('metric-mode-sub').textContent = err && err.message ? err.message : 'Could not load license status.';
}
}
async function refreshRequest() {
try {
const data = await AppUI.fetchJson('/api/license/request', { timeoutMs:8000 });
activationRequestText = JSON.stringify(data, null, 2);
document.getElementById('request-json').textContent = activationRequestText;
} catch (err) {
console.warn('License request error:', err);
activationRequestText = '';
document.getElementById('request-json').textContent = 'Could not load activation request: ' + (err && err.message ? err.message : 'unknown error');
}
}
async function activateLicense() {
const text = document.getElementById('license-text').value.trim();
if (!text) {
setMessage('Paste a signed license JSON first.', false);
return;
}
setMessage('Activating license...', true);
try {
const res = await fetch('/api/license/activate', {
method:'POST',
headers:{ 'Content-Type':'application/json' },
body: JSON.stringify({ license_text:text })
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errMsg = data && data.error ? data.error : ('HTTP ' + res.status);
setMessage('Activation failed: ' + errMsg, false);
if (data && data.license) renderStatus(data.license);
return;
}
setMessage('License activated successfully.', true);
if (data && data.license) renderStatus(data.license);
await refreshStatus();
} catch (err) {
console.warn('License activate error:', err);
setMessage('Activation request failed.', false);
}
}
async function copyRequest() {
if (!activationRequestText) return;
try {
await navigator.clipboard.writeText(activationRequestText);
setMessage('Activation request copied to clipboard.', true);
} catch (err) {
setMessage('Clipboard copy failed.', false);
}
}
function downloadRequest() {
if (!activationRequestText) return;
const blob = new Blob([activationRequestText], { type:'application/json;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'force-monitor-activation-request.json';
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
function attachFileReader() {
const input = document.getElementById('license-file');
if (!input) return;
input.addEventListener('change', async () => {
const file = input.files && input.files[0];
if (!file) return;
const text = await file.text();
document.getElementById('license-text').value = text;
setMessage('Loaded license file into the text box.', true);
});
}
window.addEventListener('DOMContentLoaded', async () => {
AppUI.initTheme();
AppUI.initFullscreen({ buttonId:'fullscreen-toggle' });
document.getElementById('refresh-btn').addEventListener('click', async () => { setMessage('', true); await Promise.all([refreshStatus(), refreshRequest()]); });
document.getElementById('activate-btn').addEventListener('click', activateLicense);
document.getElementById('copy-request-btn').addEventListener('click', copyRequest);
document.getElementById('download-request-btn').addEventListener('click', downloadRequest);
attachFileReader();
await Promise.all([refreshStatus(), refreshRequest()]);
});
</script>
</body>
</html>