Ansible schedule form: structured playbook-variable fields + first-item dropdown defaults #2
+113
-40
@@ -51,37 +51,76 @@ code { font-family: ui-monospace, monospace; font-size: 0.875em; background: var
|
||||
radial-gradient(ellipse 50% 40% at 20% 80%, rgba(200, 168, 64, 0.025) 0%, transparent 65%);
|
||||
}
|
||||
|
||||
/* 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;
|
||||
/* ── App shell: persistent left sidebar + content column ───────────────────── */
|
||||
.app-shell { display: flex; min-height: 100vh; position: relative; z-index: 1; }
|
||||
.sidebar {
|
||||
width: 220px; flex-shrink: 0; background: var(--bg-card);
|
||||
border-right: 1px solid var(--border-mid);
|
||||
display: flex; flex-direction: column;
|
||||
position: sticky; top: 0; height: 100vh; overflow-y: auto; z-index: 20;
|
||||
}
|
||||
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;
|
||||
.sidebar .brand {
|
||||
font-family: var(--font-serif); font-weight: 700; font-size: 1.15rem;
|
||||
letter-spacing: 0.01em; color: var(--gold); text-decoration: none;
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
padding: 0.85rem 1.25rem; border-bottom: 1px solid var(--border);
|
||||
}
|
||||
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; }
|
||||
.sidebar .brand svg { flex-shrink: 0; }
|
||||
.side-nav { flex: 1; padding: 0.75rem 0; }
|
||||
.nav-group { margin-bottom: 0.9rem; }
|
||||
.nav-group-label {
|
||||
font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.09em;
|
||||
color: var(--text-dim); font-weight: 600; padding: 0 1.25rem; margin-bottom: 0.3rem;
|
||||
}
|
||||
.side-nav a {
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
padding: 0.4rem 1.25rem; font-size: 0.9rem; color: var(--text-muted);
|
||||
border-left: 2px solid transparent; text-decoration: none;
|
||||
transition: color .12s, background .12s, border-color .12s;
|
||||
}
|
||||
.side-nav a:hover { color: var(--text); background: var(--bg-elevated); }
|
||||
.side-nav a.active {
|
||||
color: var(--gold); border-left-color: var(--gold);
|
||||
background: color-mix(in srgb, var(--gold) 8%, transparent);
|
||||
}
|
||||
.side-user { border-top: 1px solid var(--border); padding: 0.7rem 1.25rem; }
|
||||
.side-user a {
|
||||
color: var(--text-muted); font-size: 0.84rem; text-decoration: none;
|
||||
display: flex; align-items: center; gap: 0.4rem;
|
||||
}
|
||||
.side-user a:hover { color: var(--text); }
|
||||
.app-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
||||
.topbar { display: none; } /* slim mobile bar; shown only under 900px */
|
||||
.nav-scrim { display: none; }
|
||||
|
||||
/* Layout */
|
||||
main { padding: 1.5rem 2rem; max-width: 1600px; margin: 0 auto; width: 100%; box-sizing: border-box; position: relative; z-index: 1; }
|
||||
main { padding: 1.5rem 2rem; max-width: 1400px; margin: 0 auto; width: 100%; box-sizing: border-box; position: relative; z-index: 1; }
|
||||
|
||||
/* Responsive: sidebar slides off-canvas under 900px, toggled by ☰ */
|
||||
@media (max-width: 900px) {
|
||||
.sidebar {
|
||||
position: fixed; left: 0; top: 0; bottom: 0; height: 100%;
|
||||
transform: translateX(-100%); transition: transform .2s ease;
|
||||
box-shadow: 0 0 30px rgba(0,0,0,0.5);
|
||||
}
|
||||
body.nav-open .sidebar { transform: translateX(0); }
|
||||
.topbar {
|
||||
display: flex; align-items: center; gap: 0.75rem; height: 48px; padding: 0 1rem;
|
||||
background: var(--bg-card); border-bottom: 1px solid var(--border-mid);
|
||||
position: sticky; top: 0; z-index: 15;
|
||||
}
|
||||
.sidebar-toggle {
|
||||
background: none; border: none; color: var(--text-muted); font-size: 1.3rem;
|
||||
cursor: pointer; line-height: 1; padding: 0.25rem;
|
||||
}
|
||||
.topbar .brand-sm {
|
||||
font-family: var(--font-serif); color: var(--gold); font-weight: 700;
|
||||
font-size: 1.05rem; text-decoration: none;
|
||||
}
|
||||
body.nav-open .nav-scrim {
|
||||
display: block; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 18;
|
||||
}
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert { padding: 0.75rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: 0.9rem; }
|
||||
@@ -224,8 +263,10 @@ body.dash-editing .widget-drawer { transform:translateY(0); }
|
||||
|
||||
<div id="candle-glow"></div>
|
||||
|
||||
{% set _p = request.path %}
|
||||
<div class="app-shell">
|
||||
{% if session.user_id is defined %}
|
||||
<nav>
|
||||
<aside class="sidebar">
|
||||
<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"/>
|
||||
@@ -237,18 +278,45 @@ body.dash-editing .widget-drawer { transform:translateY(0); }
|
||||
</svg>
|
||||
Steward
|
||||
</a>
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/status">Status</a>
|
||||
<a href="/hosts/">Hosts</a>
|
||||
<a href="/monitors/">Monitors</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>
|
||||
<nav class="side-nav" aria-label="Primary">
|
||||
<div class="nav-group">
|
||||
<div class="nav-group-label">Overview</div>
|
||||
<a href="/" {% if _p == '/' %}class="active" aria-current="page"{% endif %}>Dashboard</a>
|
||||
<a href="/status" {% if _p.startswith('/status') %}class="active" aria-current="page"{% endif %}>Status</a>
|
||||
</div>
|
||||
<div class="nav-group">
|
||||
<div class="nav-group-label">Infrastructure</div>
|
||||
<a href="/hosts/" {% if _p.startswith('/hosts') %}class="active" aria-current="page"{% endif %}>Hosts</a>
|
||||
{# plugin links land here in slice 2 (get_nav hook) #}
|
||||
</div>
|
||||
<div class="nav-group">
|
||||
<div class="nav-group-label">Monitoring</div>
|
||||
<a href="/monitors/" {% if _p.startswith('/monitors') %}class="active" aria-current="page"{% endif %}>Monitors</a>
|
||||
<a href="/alerts/" {% if _p.startswith('/alerts') %}class="active" aria-current="page"{% endif %}>Alerts</a>
|
||||
</div>
|
||||
<div class="nav-group">
|
||||
<div class="nav-group-label">Automation</div>
|
||||
<a href="/ansible/" {% if _p.startswith('/ansible') %}class="active" aria-current="page"{% endif %}>Ansible</a>
|
||||
</div>
|
||||
{% if session.user_role == 'admin' %}
|
||||
<div class="nav-group">
|
||||
<div class="nav-group-label">Admin</div>
|
||||
<a href="/settings/" {% if _p.startswith('/settings') %}class="active" aria-current="page"{% endif %}>Settings</a>
|
||||
<a href="/audit/" {% if _p.startswith('/audit') %}class="active" aria-current="page"{% endif %}>Audit</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</nav>
|
||||
<div class="side-user">
|
||||
<a href="/logout" title="Log out">⏻ {{ session.username }}</a>
|
||||
</div>
|
||||
</aside>
|
||||
{% endif %}
|
||||
<div class="app-main">
|
||||
{% if session.user_id is defined %}
|
||||
<div class="topbar">
|
||||
<button class="sidebar-toggle" onclick="document.body.classList.toggle('nav-open')" aria-label="Toggle navigation">☰</button>
|
||||
<a href="/" class="brand-sm">Steward</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
@@ -314,6 +382,11 @@ function setTimeRange(val) {
|
||||
{% block breadcrumb %}{% endblock %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>{# .app-main #}
|
||||
</div>{# .app-shell #}
|
||||
{% if session.user_id is defined %}
|
||||
<div class="nav-scrim" onclick="document.body.classList.remove('nav-open')"></div>
|
||||
{% endif %}
|
||||
|
||||
{% block extra_scripts %}{% endblock %}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Syntax-parse every first-party template so a broken tag fails the unit lane.
|
||||
|
||||
The app only renders a handful of templates in unit tests (e.g. the login page),
|
||||
so a Jinja syntax error elsewhere would otherwise ship green. env.parse() checks
|
||||
syntax without resolving extends/includes/imports — enough to catch an unbalanced
|
||||
or mistyped tag across the whole template tree (e.g. the base.html layout).
|
||||
"""
|
||||
import pathlib
|
||||
|
||||
import jinja2
|
||||
|
||||
import steward
|
||||
|
||||
_ROOT = pathlib.Path(steward.__file__).parent / "templates"
|
||||
|
||||
|
||||
def test_all_steward_templates_parse():
|
||||
env = jinja2.Environment()
|
||||
files = sorted(_ROOT.rglob("*.html"))
|
||||
assert files, "no steward templates found"
|
||||
for f in files:
|
||||
env.parse(f.read_text()) # raises TemplateSyntaxError on a bad tag
|
||||
Reference in New Issue
Block a user