Files
FabledSteward/fabledscryer/templates/base.html
T
2026-04-13 14:51:47 -04:00

355 lines
16 KiB
HTML
Raw 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.0">
<title>{% block title %}Fabled Scryer{% endblock %}</title>
<script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,600;0,700;1,400&display=swap" rel="stylesheet">
<style>
:root {
--bg: #131315;
--bg-card: #1d1d20;
--bg-elevated: #24242a;
--border: #30303a;
--border-mid: #3a3a44;
--text: #d4d0c0;
--text-muted: #b8b8b0;
--text-dim: #8a8a92;
--gold: #c8a840;
--gold-hover: #d8b850;
--gold-dim: #2a2410;
--pewter: #8a8a92;
--accent: var(--gold);
--accent-hover: var(--gold-hover);
--green: #4aa86a;
--green-dim: #162a1c;
--yellow: #d4a840;
--yellow-dim: #2a2410;
--orange: #c87840;
--orange-dim: #2a1a10;
--red: #c84048;
--red-dim: #2a1014;
--font-serif: 'EB Garamond', Georgia, serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text); line-height: 1.5; }
a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-hover); }
code { font-family: ui-monospace, monospace; font-size: 0.875em; background: var(--bg-elevated); padding: 0.1em 0.35em; border-radius: 3px; }
/* Star field */
#star-field { position: fixed; inset: 0; pointer-events: none; z-index: 0; overflow: hidden; }
@keyframes star-orbit { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes star-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
/* Nav */
nav {
background: var(--bg-card);
padding: 0 1.5rem;
display: flex;
align-items: center;
gap: 0;
border-bottom: 1px solid var(--border-mid);
height: 48px;
position: relative;
z-index: 10;
}
nav .brand {
font-family: var(--font-serif);
font-weight: 700;
font-size: 1.1rem;
margin-right: 2rem;
letter-spacing: 0.01em;
display: flex;
align-items: center;
gap: 0.45rem;
color: var(--gold);
text-decoration: none;
}
nav .brand svg { flex-shrink: 0; }
nav a { color: var(--text-muted); font-size: 0.875rem; padding: 0 0.875rem; height: 48px; display: flex; align-items: center; border-bottom: 2px solid transparent; transition: color .15s, border-color .15s; }
nav a:hover { color: var(--text); border-bottom-color: var(--border-mid); }
nav a.nav-end { margin-left: auto; }
/* Layout */
main { padding: 1.5rem 2rem; max-width: 1600px; margin: 0 auto; width: 100%; box-sizing: border-box; position: relative; z-index: 1; }
/* Alerts */
.alert { padding: 0.75rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: 0.9rem; }
.alert-error { background: var(--red-dim); border: 1px solid #6a1820; color: #ffb0b8; }
.alert-success { background: var(--green-dim); border: 1px solid #1a5a28; color: #90f0b0; }
/* Cards */
.card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; margin-bottom: 1rem; }
.card-flush { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; margin-bottom: 1rem; }
/* Typography */
.page-title { font-family: var(--font-serif); font-size: 1.6rem; font-weight: 600; color: #c8b4f8; margin-bottom: 1.5rem; letter-spacing: 0.01em; }
.section-title { font-family: var(--font-serif); font-size: 0.82rem; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.08em; }
h1, h2, h3 { font-family: var(--font-serif); }
/* Tables */
.table { width: 100%; border-collapse: collapse; }
.table th { text-align: left; padding: 0.6rem 1rem; color: var(--text-muted); font-weight: 500; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; border-bottom: 1px solid var(--border-mid); }
.table td { padding: 0.7rem 1rem; border-bottom: 1px solid var(--border); font-size: 0.9rem; }
.table tbody tr:last-child td { border-bottom: none; }
.table tbody tr:hover td { background: var(--bg-elevated); }
.table .td-actions { text-align: right; white-space: nowrap; }
/* Forms */
.form-group { margin-bottom: 1rem; }
label { display: block; margin-bottom: 0.3rem; font-size: 0.85rem; color: var(--text-muted); font-family: var(--font-serif); }
input[type=text], input[type=email], input[type=password], input[type=number], input[type=url], select, textarea {
width: 100%; padding: 0.5rem 0.75rem; background: var(--bg); border: 1px solid var(--border-mid);
border-radius: 5px; color: var(--text); font-size: 0.9rem; font-family: inherit;
transition: border-color .15s;
}
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--accent); }
select { cursor: pointer; }
textarea { resize: vertical; }
/* Buttons */
.btn { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.45rem 1rem; background: var(--accent); color: #fff; border: none; border-radius: 5px; cursor: pointer; font-size: 0.875rem; font-family: inherit; font-weight: 500; transition: background .15s; text-decoration: none; }
.btn:hover { background: var(--accent-hover); color: #fff; }
.btn-sm { padding: 0.3rem 0.65rem; font-size: 0.8rem; }
.btn-ghost { background: transparent; color: var(--text-muted); border: 1px solid var(--border-mid); }
.btn-ghost:hover { background: var(--bg-elevated); color: var(--text); }
.btn-danger { background: #7a1820; color: #ffb0b0; }
.btn-danger:hover { background: #9a2030; }
.btn-warn { background: var(--yellow-dim); color: var(--yellow); border: 1px solid #6a5000; }
.btn-warn:hover { background: #4a4000; }
/* Badges / Status */
.badge { display: inline-block; padding: 0.2em 0.55em; border-radius: 4px; font-size: 0.75rem; font-weight: 600; letter-spacing: 0.03em; }
.badge-green { background: var(--green-dim); color: var(--green); }
.badge-red { background: var(--red-dim); color: #ff8090; }
.badge-yellow { background: var(--yellow-dim); color: var(--yellow); }
.badge-orange { background: var(--orange-dim); color: var(--orange); }
.badge-gold { background: var(--gold-dim); color: var(--gold); }
.badge-dim { background: var(--bg-elevated); color: var(--text-muted); }
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.dot-up { background: var(--green); box-shadow: 0 0 4px var(--green); }
.dot-down { background: var(--red); }
.dot-warn { background: var(--yellow); }
.dot-dim { background: var(--text-dim); }
/* Empty state */
.empty { color: var(--text-muted); font-size: 0.9rem; padding: 2rem; text-align: center; }
/* Stat cards (dashboard) */
.stat-val { font-size: 2rem; font-weight: 700; line-height: 1; }
.stat-label { font-size: 0.8rem; color: var(--text-muted); margin-left: 0.3rem; }
/* Ping pills */
.ping-card { padding: 0.75rem 1.25rem; }
.ping-row { display:flex; align-items:center; gap:1rem; padding:0.45rem 0; border-bottom:1px solid var(--border); }
.ping-row:last-child { border-bottom:none; }
.ping-meta { min-width:120px; max-width:180px; flex-shrink:1; overflow:hidden; }
.ping-name { font-size:0.875rem; font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; display:block; }
.ping-addr { display:block; color:var(--text-muted); font-size:0.75rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.ping-pills { display:flex; gap:2px; flex:1; min-width:0; overflow:hidden; align-items:center; }
.pill { display:inline-block; width:7px; height:22px; border-radius:2px; cursor:default; transition:opacity .1s; }
.pill:hover { opacity:.75; }
.pill-empty { background:var(--bg-elevated); }
.ping-cur { min-width:72px; text-align:right; font-size:0.85rem; font-variant-numeric:tabular-nums; }
.ping-cur-good { color:var(--green); }
.ping-cur-warn { color:var(--yellow); }
.ping-cur-bad { color:var(--orange); }
.ping-cur-down { color:var(--red); font-weight:bold; }
.ping-cur-nd { color:var(--text-dim); }
/* Widget headers */
.widget-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:0.75rem; }
.widget-title { font-family:var(--font-serif); font-size:0.8rem; font-weight:600; color:var(--text-muted); text-transform:uppercase; letter-spacing:0.06em; }
.widget-link { font-size:0.8rem; color:var(--text-muted); }
.widget-link:hover { color:var(--text); }
/* Time range selector */
.range-btn { background:none; border:1px solid var(--border-mid); border-radius:4px; color:var(--text-muted); padding:0.18rem 0.5rem; cursor:pointer; font-size:0.75rem; font-family:inherit; transition:background .1s,color .1s,border-color .1s; }
.range-btn:hover { background:var(--bg-elevated); color:var(--text); border-color:var(--accent); }
.range-btn.active { background:var(--accent); border-color:var(--accent); color:#fff; }
</style>
{% block head %}{% endblock %}
</head>
<body>
{# Star field — orbits around top-left corner, populated by JS below #}
<div id="star-field"></div>
{% if session.user_id is defined %}
<nav>
<a href="/" class="brand">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<circle cx="12" cy="12" r="10.5" fill="none" stroke="#c8a840" stroke-width="1.5"/>
<circle cx="12" cy="12" r="9.3" fill="#8a8a92" opacity="0.1"/>
<path d="M7 10 L7 5.8 L9 7.8 L10.5 5 L12 7.2 L13.5 5 L15 7.8 L17 5.8 L17 10 Z" fill="#c8a840"/>
<ellipse cx="12" cy="15.5" rx="5.2" ry="1" fill="#c8a840"/>
<rect x="9.6" y="15.5" width="1" height="3.8" fill="#c8a840"/>
<rect x="13.4" y="15.5" width="1" height="3.8" fill="#c8a840"/>
</svg>
Fabled Scryer
</a>
<a href="/">Dashboard</a>
<a href="/hosts/">Hosts</a>
<a href="/hosts/uptime">Uptime</a>
<a href="/ping/">Ping</a>
<a href="/dns/">DNS</a>
<a href="/alerts/">Alerts</a>
<a href="/ansible/">Ansible</a>
{% if session.user_role == 'admin' %}
<a href="/settings/">Settings</a>
<a href="/audit/">Audit</a>
{% endif %}
<a href="/logout" class="nav-end">{{ session.username }}</a>
</nav>
{% endif %}
<script>
function setTimeRange(val) {
var input = document.getElementById('time-range');
if (input) input.value = val;
document.querySelectorAll('.range-btn').forEach(function(b) {
b.classList.toggle('active', b.dataset.range === val);
});
var url = new URL(window.location);
url.searchParams.set('range', val);
history.replaceState({}, '', url);
document.body.dispatchEvent(new CustomEvent('rangeChange'));
}
</script>
<main>
{% if plugin_failures and session.user_role == 'admin' %}
<div style="background:color-mix(in srgb,var(--yellow) 12%,var(--bg-elevated));
border:1px solid color-mix(in srgb,var(--yellow) 35%,var(--border));
border-radius:6px;padding:0.6rem 1rem;margin-bottom:1rem;
font-size:0.84rem;display:flex;align-items:flex-start;gap:0.6rem;">
<span style="color:var(--yellow);flex-shrink:0;"></span>
<span>
<strong style="color:var(--text);">
{{ plugin_failures | length }} plugin{{ 's' if plugin_failures | length != 1 }} failed to load.
</strong>
<a href="/settings/plugins/" style="color:var(--accent);margin-left:0.4rem;">View details →</a>
</span>
</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
{% block content %}{% endblock %}
</main>
<script>
(function () {
var field = document.getElementById('star-field');
if (!field) return;
var W = window.innerWidth;
var H = window.innerHeight;
var maxR = Math.sqrt(W * W + H * H);
// Subtle purple glow anchoring the field to the top-left corner
var glow = document.createElement('div');
glow.style.cssText = 'position:absolute;width:500px;height:500px;left:-250px;top:-250px;border-radius:50%;background:radial-gradient(circle,rgba(124,94,244,0.06) 0%,transparent 65%);pointer-events:none;';
field.appendChild(glow);
// Compute polygon points for a 5-pointed star inside a size×size box
function starPoints(size) {
var cx = size / 2, cy = size / 2;
var R = size / 2 * 0.95; // outer radius
var r = R * 0.382; // inner radius (golden ratio)
var pts = [];
for (var i = 0; i < 5; i++) {
var oa = (i * 72 - 90) * Math.PI / 180;
var ia = (i * 72 - 90 + 36) * Math.PI / 180;
pts.push((cx + R * Math.cos(oa)).toFixed(2) + ',' + (cy + R * Math.sin(oa)).toFixed(2));
pts.push((cx + r * Math.cos(ia)).toFixed(2) + ',' + (cy + r * Math.sin(ia)).toFixed(2));
}
return pts.join(' ');
}
// 22 wizard-hat stars: [size(px), opacity, isGold, orbitDuration(s), spinDuration(s)]
var defs = [
[230, 0.09, true, 200, 28],
[144, 0.08, true, 260, 40],
[ 92, 0.10, false, 150, 22],
[190, 0.07, true, 310, 35],
[116, 0.09, false, 175, 18],
[260, 0.07, true, 280, 45],
[ 80, 0.11, false, 135, 20],
[164, 0.08, true, 230, 32],
[104, 0.09, false, 165, 24],
[200, 0.07, true, 295, 38],
[ 70, 0.10, false, 120, 16],
[150, 0.08, true, 245, 30],
[ 88, 0.09, false, 140, 21],
[210, 0.07, true, 320, 42],
[128, 0.08, false, 180, 26],
[ 76, 0.10, true, 160, 19],
[175, 0.07, true, 290, 36],
[ 96, 0.09, false, 155, 23],
[220, 0.07, true, 305, 44],
[110, 0.08, false, 170, 25],
[ 84, 0.09, true, 235, 31],
[140, 0.08, false, 185, 27],
];
var n = defs.length;
defs.forEach(function (def, i) {
var size = def[0], opacity = def[1], isGold = def[2], orbitDur = def[3], spinDur = def[4];
// Spread evenly across a ~110° fan from top-left, small random jitter
var angleDeg = 4 + (i / (n - 1)) * 108 + (Math.random() * 8 - 4);
var angle = angleDeg * Math.PI / 180;
var radius = (0.15 + (i / n) * 0.7 + Math.random() * 0.15) * maxR * 0.88 + 60;
// Star center position in parent coords
var cx = Math.cos(angle) * radius;
var cy = Math.sin(angle) * radius;
var left = cx - size / 2;
var top = cy - size / 2;
var orbitDelay = -(Math.random() * orbitDur);
var spinDelay = -(Math.random() * spinDur);
var color = isGold ? '#c9a84c' : '#c8b4f8';
// Outer wrapper handles orbit — transform-origin points back to (0,0) corner
var wrapper = document.createElement('div');
wrapper.style.cssText = [
'position:absolute',
'left:' + left + 'px',
'top:' + top + 'px',
'width:' + size + 'px',
'height:'+ size + 'px',
'opacity:' + opacity,
'transform-origin:' + (-left) + 'px ' + (-top) + 'px',
'animation:star-orbit ' + orbitDur + 's ' + orbitDelay + 's linear infinite',
'pointer-events:none'
].join(';');
// Inner SVG handles spin — transform-origin defaults to 50% 50% (star center)
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', size);
svg.setAttribute('height', size);
svg.setAttribute('viewBox', '0 0 ' + size + ' ' + size);
svg.style.cssText = 'display:block;animation:star-spin ' + spinDur + 's ' + spinDelay + 's linear infinite;';
var poly = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
poly.setAttribute('points', starPoints(size));
poly.setAttribute('fill', color);
svg.appendChild(poly);
wrapper.appendChild(svg);
field.appendChild(wrapper);
});
}());
</script>
{% block extra_scripts %}{% endblock %}
</body>
</html>