feat(plugins): Traefik problems first, UniFi/SNMP/fleet on shared shapes, audit names its entities, templates on tokens (#5126)
Traefik: routers are graded from their latest scrape (router_state: crit on any real 5xx share, warn on p95 past the latency critical cutoff or a high 4xx share). Failing and slow routers lead as cards that say why; healthy ones fold into one table that stays open or shut across polls. Proxy counts are stat tiles; certificates are graded by Settings -> Thresholds (tls_days). UniFi: WAN health is a stat strip (latency graded by the thresholds), devices carry status dots, event categories and firewall actions are badges, the alarm count reuses .count-badge (renamed from .nav-count). SNMP: each device wears a status badge and its readings are stat tiles. Agent fleet: the table comes first, with a Status column (Reporting / Silent / Never reported, by the host page's stale_after_seconds), silent agents first, hosts linked, last seen as |ago. Audit: entries name their entity (one query per type, steward/audit/ service.py) and link it; something deleted since keeps the name its entry recorded and says "(deleted)". Tokens: charts read colours through chart-colors.js (stewardColor resolves a token to the rgb the browser computed; Chart.js defaults take the page's text and border colours), so host, history, Traefik and SNMP charts drop their hex. Template hex colours reach 0 (HEX_CEILING = 0), the last rgba pills go, and heavy weights (600/700/bold, 60 of them) become 500, the house pair being 400/500; a guard keeps templates there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+16
-10
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
import json
|
||||
from quart import Blueprint, render_template, request, current_app
|
||||
from sqlalchemy import select
|
||||
from steward.audit.service import entity_labels
|
||||
from steward.auth.middleware import require_role
|
||||
from steward.models.audit import AuditEvent
|
||||
from steward.models.users import UserRole
|
||||
@@ -23,16 +24,21 @@ async def list_events():
|
||||
result = await db.execute(stmt)
|
||||
events = result.scalars().all()
|
||||
|
||||
# Parse detail_json for display
|
||||
parsed = []
|
||||
for e in events:
|
||||
detail = {}
|
||||
if e.detail_json:
|
||||
try:
|
||||
detail = json.loads(e.detail_json)
|
||||
except (ValueError, TypeError):
|
||||
detail = {"raw": e.detail_json}
|
||||
parsed.append({"event": e, "detail": detail})
|
||||
# Parse detail_json for display
|
||||
parsed = []
|
||||
for e in events:
|
||||
detail = {}
|
||||
if e.detail_json:
|
||||
try:
|
||||
detail = json.loads(e.detail_json)
|
||||
except (ValueError, TypeError):
|
||||
detail = {"raw": e.detail_json}
|
||||
parsed.append({"event": e, "detail": detail})
|
||||
labels = await entity_labels(db, parsed)
|
||||
|
||||
for item in parsed:
|
||||
e = item["event"]
|
||||
item["entity"] = labels.get((e.entity_type, str(e.entity_id))) if e.entity_id else None
|
||||
|
||||
return await render_template(
|
||||
"audit/list.html",
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Name the thing each audit entry is about.
|
||||
|
||||
An audit row stores an entity type and id; the page shows the entity's
|
||||
current name, linked to its page, in one query per type for the whole page.
|
||||
An entity deleted since keeps whatever name the entry's own detail recorded.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from steward.ansible.inventory_gen import run_title, scope_labels
|
||||
from steward.models.alerts import AlertRule, MaintenanceWindow
|
||||
from steward.models.ansible import AnsibleRun
|
||||
from steward.models.ansible_inventory import AnsibleGroup, AnsibleTarget
|
||||
from steward.models.ansible_schedule import AnsibleSchedule
|
||||
from steward.models.api_tokens import ApiToken
|
||||
from steward.models.hosts import Host
|
||||
from steward.models.monitors import Monitor
|
||||
|
||||
# entity_type -> (model, its page for an id). A type named by a value rather
|
||||
# than a row (plugin, ansible_playbook) is its own label.
|
||||
_NAMED = {
|
||||
"host": (Host, "/hosts/{}"),
|
||||
"monitor": (Monitor, "/monitors/{}"),
|
||||
"alert_rule": (AlertRule, "/alerts/history?rule={}"),
|
||||
"maintenance_window": (MaintenanceWindow, "/alerts/maintenance"),
|
||||
"ansible_target": (AnsibleTarget, "/ansible/inventory/targets/{}"),
|
||||
"ansible_group": (AnsibleGroup, "/ansible/inventory/groups/{}"),
|
||||
"ansible_schedule": (AnsibleSchedule, "/ansible/schedules"),
|
||||
"api_token": (ApiToken, "/ai-access/"),
|
||||
}
|
||||
_SELF_NAMED = {"plugin": "/settings/plugins/", "ansible_playbook": None}
|
||||
|
||||
|
||||
def _recorded_name(detail: dict) -> str | None:
|
||||
"""A name the entry recorded when it was written, for a deleted entity."""
|
||||
for key in ("name", "new_name", "username", "playbook"):
|
||||
if detail.get(key):
|
||||
return str(detail[key])
|
||||
return None
|
||||
|
||||
|
||||
async def entity_labels(db, items: list[dict]) -> dict[tuple[str, str], dict]:
|
||||
"""{(entity_type, entity_id): {"label", "url", "gone"}} for the audit rows
|
||||
in `items` ({"event", "detail"}), batched one query per type."""
|
||||
ids: dict[str, set[str]] = {}
|
||||
for it in items:
|
||||
e = it["event"]
|
||||
if e.entity_type and e.entity_id:
|
||||
ids.setdefault(e.entity_type, set()).add(str(e.entity_id))
|
||||
|
||||
labels: dict[tuple[str, str], dict] = {}
|
||||
for etype, wanted in ids.items():
|
||||
if etype in _NAMED:
|
||||
model, page = _NAMED[etype]
|
||||
key_col = model.id
|
||||
keys = [int(i) for i in wanted if i.isdigit()] if etype == "api_token" else list(wanted)
|
||||
if not keys:
|
||||
continue
|
||||
for row_id, name in (await db.execute(
|
||||
select(key_col, model.name).where(key_col.in_(keys)))).all():
|
||||
labels[(etype, str(row_id))] = {"label": name, "url": page.format(row_id), "gone": False}
|
||||
elif etype == "ansible_run":
|
||||
runs = (await db.execute(
|
||||
select(AnsibleRun).where(AnsibleRun.id.in_(list(wanted))))).scalars().all()
|
||||
scopes = await scope_labels(db, [r.inventory_scope for r in runs])
|
||||
for r in runs:
|
||||
labels[(etype, r.id)] = {
|
||||
"label": run_title(r.playbook_path, scopes.get(r.inventory_scope, r.inventory_scope)),
|
||||
"url": f"/ansible/runs/{r.id}", "gone": False}
|
||||
elif etype in _SELF_NAMED:
|
||||
for i in wanted:
|
||||
labels[(etype, i)] = {"label": i, "url": _SELF_NAMED[etype], "gone": False}
|
||||
|
||||
# What a lookup did not find was deleted since: keep the name the entry recorded.
|
||||
for it in items:
|
||||
e = it["event"]
|
||||
if not (e.entity_type and e.entity_id):
|
||||
continue
|
||||
key = (e.entity_type, str(e.entity_id))
|
||||
if key not in labels:
|
||||
recorded = _recorded_name(it["detail"] or {})
|
||||
labels[key] = {"label": recorded or f"{str(e.entity_id)[:8]}", "url": None, "gone": True}
|
||||
return labels
|
||||
@@ -0,0 +1,23 @@
|
||||
// Design tokens as colours a <canvas> can paint with. A chart is drawn on a
|
||||
// canvas, which can't read var(--green), so each token is resolved through a
|
||||
// hidden element to the rgb() the browser computed for it.
|
||||
//
|
||||
// Loaded after the stylesheets (a script waits for the sheets above it), so the
|
||||
// tokens are defined by the time this runs. It also gives every Chart.js chart
|
||||
// the page's text and grid colours, so a chart config only names its series.
|
||||
(function () {
|
||||
function stewardColor(name) {
|
||||
var probe = document.createElement('span');
|
||||
probe.style.display = 'none';
|
||||
probe.style.color = 'var(' + name + ')';
|
||||
document.documentElement.appendChild(probe);
|
||||
var value = getComputedStyle(probe).color;
|
||||
probe.remove();
|
||||
return value;
|
||||
}
|
||||
window.stewardColor = stewardColor;
|
||||
if (window.Chart) {
|
||||
Chart.defaults.color = stewardColor('--text-muted');
|
||||
Chart.defaults.borderColor = stewardColor('--border');
|
||||
}
|
||||
})();
|
||||
@@ -86,8 +86,8 @@ code { font-family: var(--fs-font-mono); font-size: 0.875em; background: var(--b
|
||||
color: var(--gold); border-left-color: var(--gold);
|
||||
background: color-mix(in srgb, var(--gold) 8%, transparent);
|
||||
}
|
||||
/* How many alerts need attention, beside the Alerts link. */
|
||||
.nav-count {
|
||||
/* A small count that needs attention: alerts beside the Alerts link, UniFi alarms. */
|
||||
.count-badge {
|
||||
margin-left: auto; padding: 0 0.45em; border-radius: var(--fs-radius-sm);
|
||||
background: var(--red-dim); color: var(--red); font-size: 0.75rem; font-weight: 500;
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
</div>
|
||||
|
||||
<details open>
|
||||
<summary style="cursor:pointer;font-weight:600;">Claude Code</summary>
|
||||
<summary style="cursor:pointer;font-weight:500;">Claude Code</summary>
|
||||
<pre style="{{ pre }}">claude mcp add --transport http steward {{ mcp_url }} \
|
||||
--header "Authorization: Bearer {{ tok }}"</pre>
|
||||
</details>
|
||||
|
||||
<details style="margin-top:0.75rem;">
|
||||
<summary style="cursor:pointer;font-weight:600;">Claude Desktop</summary>
|
||||
<summary style="cursor:pointer;font-weight:500;">Claude Desktop</summary>
|
||||
<div style="font-size:0.8rem;color:var(--text-muted);margin-top:0.4rem;line-height:1.5;">
|
||||
Add to <code>claude_desktop_config.json</code>. It bridges through
|
||||
<code>mcp-remote</code>, which needs Node.js.
|
||||
@@ -40,7 +40,7 @@
|
||||
</details>
|
||||
|
||||
<details style="margin-top:0.75rem;">
|
||||
<summary style="cursor:pointer;font-weight:600;">Open WebUI and other clients</summary>
|
||||
<summary style="cursor:pointer;font-weight:500;">Open WebUI and other clients</summary>
|
||||
<div style="font-size:0.8rem;color:var(--text-muted);margin-top:0.4rem;line-height:1.5;">
|
||||
Add an MCP server using the <strong>Streamable HTTP</strong> transport, with the URL
|
||||
above and bearer authentication set to the token. In Open WebUI this is an external
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<h1 class="page-title">Token created</h1>
|
||||
|
||||
<div class="card" style="border-color:var(--gold);margin-bottom:1rem;">
|
||||
<div style="font-size:0.8rem;font-weight:600;color:var(--gold);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.75rem;">
|
||||
<div style="font-size:0.8rem;font-weight:500;color:var(--gold);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.75rem;">
|
||||
Copy this token now. It can't be shown again.
|
||||
</div>
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
{% set t = row.token %}
|
||||
<tr {% if row.state != 'active' %}style="opacity:0.55;"{% endif %}>
|
||||
<td>
|
||||
<div style="font-weight:600;">{{ t.name }}</div>
|
||||
<div style="font-weight:500;">{{ t.name }}</div>
|
||||
<div style="font-family:ui-monospace,monospace;font-size:0.75rem;color:var(--text-muted);">{{ t.prefix }}…</div>
|
||||
</td>
|
||||
<td><span class="badge badge-dim">{{ t.role.value }}</span></td>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</label>
|
||||
{% endif %}
|
||||
{% if variables %}
|
||||
<div style="margin:0.5rem 0 0.25rem;font-size:0.82rem;color:var(--text-muted);font-weight:600;">
|
||||
<div style="margin:0.5rem 0 0.25rem;font-size:0.82rem;color:var(--text-muted);font-weight:500;">
|
||||
Playbook variables
|
||||
</div>
|
||||
<p style="color:var(--text-dim);font-size:0.76rem;margin:0 0 0.6rem;">
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<span style="color:var(--red);" title="{{ s.last_error }}">launch error</span>
|
||||
{% elif row.last_run %}
|
||||
{% set sc = {"queued":"var(--text-dim)","running":"var(--yellow)","success":"var(--green)","failed":"var(--red)","interrupted":"var(--orange)","cancelled":"var(--orange)"}.get(row.last_run.status.value, "var(--text-muted)") %}
|
||||
<a href="/ansible/runs/{{ row.last_run.id }}" style="color:{{ sc }};font-weight:600;">{{ row.last_run.status.value }}</a>
|
||||
<a href="/ansible/runs/{{ row.last_run.id }}" style="color:{{ sc }};font-weight:500;">{{ row.last_run.status.value }}</a>
|
||||
<span style="color:var(--text-dim);">· {{ s.last_run_at.strftime("%m-%d %H:%M") }}</span>
|
||||
{% else %}
|
||||
<span style="color:var(--text-dim);">never</span>
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Audit Log — Steward{% endblock %}
|
||||
{% from "_macros.html" import page_header, empty_state %}
|
||||
{% block title %}Audit log — Steward{% endblock %}
|
||||
{% block content %}
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem;">
|
||||
<h1 class="page-title" style="margin-bottom:0;">Audit Log</h1>
|
||||
{% call page_header("Audit log", "Who changed what, newest first.") %}
|
||||
<form method="get" style="display:flex;gap:0.5rem;align-items:center;">
|
||||
<input type="text" name="action" value="{{ action_filter }}"
|
||||
placeholder="Filter by action…"
|
||||
placeholder="Filter by action…" aria-label="Filter by action"
|
||||
style="width:200px;font-size:0.85rem;">
|
||||
<button type="submit" class="btn btn-ghost btn-sm">Filter</button>
|
||||
{% if action_filter %}
|
||||
<a href="/audit/" class="btn btn-ghost btn-sm">Clear</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{% if events %}
|
||||
<div class="card-flush">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="min-width:160px;">Time (UTC)</th>
|
||||
<th>When</th>
|
||||
<th>User</th>
|
||||
<th>Action</th>
|
||||
<th>Entity</th>
|
||||
@@ -31,30 +31,21 @@
|
||||
{% set e = item.event %}
|
||||
{% set d = item.detail %}
|
||||
<tr>
|
||||
<td style="font-family:ui-monospace,monospace;font-size:0.8rem;white-space:nowrap;color:var(--text-muted);">
|
||||
{{ e.timestamp.strftime("%Y-%m-%d %H:%M:%S") }}
|
||||
</td>
|
||||
<td style="white-space:nowrap;" title="{{ e.timestamp.strftime('%Y-%m-%d %H:%M:%S UTC') }}">{{ e.timestamp|ago }}</td>
|
||||
<td style="font-size:0.85rem;">{{ e.username }}</td>
|
||||
<td><span class="mono">{{ e.action }}</span></td>
|
||||
<td>
|
||||
{% set cat = e.action.split('.')[0] %}
|
||||
<span style="
|
||||
font-size:0.78rem;font-family:ui-monospace,monospace;
|
||||
padding:0.15rem 0.4rem;border-radius:3px;
|
||||
background:{% if cat == 'auth' %}var(--bg-elevated){% elif cat == 'plugin' %}var(--gold-dim){% elif cat == 'alert' %}var(--red-dim){% elif cat == 'host' %}var(--green-dim){% elif cat == 'settings' %}var(--bg-elevated){% else %}var(--bg-elevated){% endif %};
|
||||
color:{% if cat == 'auth' %}var(--accent){% elif cat == 'plugin' %}var(--gold){% elif cat == 'alert' %}var(--red){% elif cat == 'host' %}var(--green){% elif cat == 'settings' %}var(--text-muted){% else %}var(--text-muted){% endif %};">
|
||||
{{ e.action }}
|
||||
</span>
|
||||
</td>
|
||||
<td style="font-size:0.82rem;color:var(--text-muted);">
|
||||
{% if e.entity_type %}
|
||||
<span style="color:var(--text-dim);">{{ e.entity_type }}/</span>{{ e.entity_id or "" }}
|
||||
{% endif %}
|
||||
{% if item.entity %}
|
||||
<span class="field-hint">{{ e.entity_type|replace('_', ' ') }}</span>
|
||||
{% if item.entity.url %}<a href="{{ item.entity.url }}">{{ item.entity.label }}</a>{% else %}{{ item.entity.label }}{% endif %}
|
||||
{% if item.entity.gone %}<span class="field-hint">(deleted)</span>{% endif %}
|
||||
{% elif e.entity_type %}<span class="field-hint">{{ e.entity_type|replace('_', ' ') }}</span>{% endif %}
|
||||
</td>
|
||||
<td style="font-size:0.8rem;color:var(--text-muted);max-width:340px;">
|
||||
{% if d %}
|
||||
{% for k, v in d.items() %}
|
||||
<span style="color:var(--text-dim);">{{ k }}:</span>
|
||||
<span style="font-family:ui-monospace,monospace;">{{ v }}</span>
|
||||
<span class="mono">{{ v }}</span>
|
||||
{% if not loop.last %} · {% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
@@ -71,8 +62,6 @@
|
||||
{% endif %}
|
||||
</p>
|
||||
{% else %}
|
||||
<div class="card" style="text-align:center;padding:3rem;">
|
||||
<p style="color:var(--text-muted);">No audit events recorded yet.</p>
|
||||
</div>
|
||||
<div class="card">{{ empty_state("No audit events recorded yet.") }}</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='fonts/fonts.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='theme.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='steward.css') }}">
|
||||
<script src="{{ url_for('static', filename='chart-colors.js') }}"></script>
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
@@ -62,7 +63,7 @@
|
||||
<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{% if active_alert_count %} <span class="nav-count" title="{{ active_alert_count }} alert{{ 's' if active_alert_count != 1 }} need{{ 's' if active_alert_count == 1 }} attention">{{ active_alert_count }}</span>{% endif %}</a>
|
||||
<a href="/alerts/" {% if _p.startswith('/alerts') %}class="active" aria-current="page"{% endif %}>Alerts{% if active_alert_count %} <span class="count-badge" title="{{ active_alert_count }} alert{{ 's' if active_alert_count != 1 }} need{{ 's' if active_alert_count == 1 }} attention">{{ active_alert_count }}</span>{% endif %}</a>
|
||||
</div>
|
||||
<div class="nav-group">
|
||||
<div class="nav-group-label">Automation</div>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<summary style="display:flex;align-items:center;justify-content:space-between;gap:0.6rem;
|
||||
padding:0.7rem 0.85rem;cursor:pointer;list-style:none;user-select:none;">
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="font-weight:600;font-size:0.85rem;">{{ w.label }}</div>
|
||||
<div style="font-weight:500;font-size:0.85rem;">{{ w.label }}</div>
|
||||
<div style="font-size:0.74rem;color:var(--text-muted);margin-top:0.1rem;">{{ w.description }}</div>
|
||||
</div>
|
||||
<span class="btn btn-sm" style="flex-shrink:0;pointer-events:none;">Add ▾</span>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;">
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;">
|
||||
<a href="/d/{{ dash.id }}" style="font-weight:600;font-size:0.9rem;">{{ dash.name }}</a>
|
||||
<a href="/d/{{ dash.id }}" style="font-weight:500;font-size:0.9rem;">{{ dash.name }}</a>
|
||||
{% if dash.is_default %}
|
||||
<span class="badge badge-gold" style="font-size:0.68rem;">Your Default</span>
|
||||
{% endif %}
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<details style="position:relative;">
|
||||
<summary class="btn btn-ghost btn-sm" style="cursor:pointer;list-style:none;">Rename</summary>
|
||||
<div style="position:absolute;right:0;top:calc(100% + 4px);background:var(--bg-card);border:1px solid var(--border-mid);border-radius:6px;padding:0.75rem;z-index:50;width:220px;box-shadow:0 4px 16px rgba(0,0,0,0.4);">
|
||||
<div style="position:absolute;right:0;top:calc(100% + 4px);background:var(--bg-card);border:1px solid var(--border-mid);border-radius:6px;padding:0.75rem;z-index:50;width:220px;box-shadow:var(--fs-shadow-3);">
|
||||
<form method="post" action="/dashboards/{{ dash.id }}/rename" style="display:flex;gap:0.4rem;">{{ csrf_field() }}
|
||||
<input type="text" name="name" value="{{ dash.name }}"
|
||||
style="flex:1;font-size:0.85rem;padding:0.3rem 0.5rem;" required>
|
||||
@@ -83,7 +83,7 @@
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;">
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;">
|
||||
<a href="/d/{{ dash.id }}" style="font-weight:600;font-size:0.9rem;">{{ dash.name }}</a>
|
||||
<a href="/d/{{ dash.id }}" style="font-weight:500;font-size:0.9rem;">{{ dash.name }}</a>
|
||||
{% if dash.owner_id is none %}
|
||||
<span class="badge badge-dim" style="font-size:0.68rem;">System</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<div class="card" style="margin-bottom:0;padding:0.85rem 1rem;{% if expired %}opacity:0.5;{% endif %}">
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;">
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="font-weight:600;font-size:0.875rem;">
|
||||
<div style="font-weight:500;font-size:0.875rem;">
|
||||
{{ token.label or "Unnamed link" }}
|
||||
{% if expired %}<span class="badge badge-red" style="font-size:0.68rem;">Expired</span>{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<h1 class="page-title">Share Link Created</h1>
|
||||
|
||||
<div class="card" style="border-color:var(--gold);margin-bottom:1rem;">
|
||||
<div style="font-size:0.8rem;font-weight:600;color:var(--gold);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.75rem;">
|
||||
<div style="font-size:0.8rem;font-weight:500;color:var(--gold);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.75rem;">
|
||||
Copy this link now — it cannot be shown again
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='fonts/fonts.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='theme.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='steward.css') }}">
|
||||
<script src="{{ url_for('static', filename='chart-colors.js') }}"></script>
|
||||
{# The palette, type and widget classes come from steward.css like every
|
||||
other page; only the read-only page's own frame and its grid live here. #}
|
||||
<style>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
{% set hs = host_status.get(host.id) %}
|
||||
{% set ut = uptime.get(host.id, {}) %}
|
||||
<tr>
|
||||
<td><a href="/hosts/{{ host.id }}" style="font-weight:600;">{{ host.name }}</a></td>
|
||||
<td><a href="/hosts/{{ host.id }}" style="font-weight:500;">{{ host.name }}</a></td>
|
||||
<td style="color:var(--text-muted);">{{ host.address }}</td>
|
||||
<td style="text-align:center;">
|
||||
{% if not hs %}
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
style="flex-shrink:0;"
|
||||
title="{% if not hs %}no monitors{% else %}{{ hs.state }}{% endif %}"></span>
|
||||
{% if session.user_id %}
|
||||
<a href="/hosts/{{ host.id }}" style="font-weight:600;font-size:0.85rem;overflow-wrap:anywhere;">{{ host.name }}</a>
|
||||
<a href="/hosts/{{ host.id }}" style="font-weight:500;font-size:0.85rem;overflow-wrap:anywhere;">{{ host.name }}</a>
|
||||
{% else %}
|
||||
<span style="font-weight:600;font-size:0.85rem;overflow-wrap:anywhere;">{{ host.name }}</span>
|
||||
<span style="font-weight:500;font-size:0.85rem;overflow-wrap:anywhere;">{{ host.name }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{# Line 2 — monitors + agent metrics; wraps instead of truncating #}
|
||||
|
||||
@@ -7,15 +7,15 @@
|
||||
|
||||
<div style="display:flex;gap:1rem;margin-bottom:0.65rem;flex-wrap:wrap;">
|
||||
{% if up %}
|
||||
<div><span style="font-size:1.4rem;font-weight:700;line-height:1;color:var(--green);">{{ up }}</span>
|
||||
<div><span style="font-size:1.4rem;font-weight:500;line-height:1;color:var(--green);">{{ up }}</span>
|
||||
<span style="font-size:0.78rem;color:var(--text-muted);margin-left:0.25rem;">up</span></div>
|
||||
{% endif %}
|
||||
{% if down %}
|
||||
<div><span style="font-size:1.4rem;font-weight:700;line-height:1;color:var(--red);">{{ down }}</span>
|
||||
<div><span style="font-size:1.4rem;font-weight:500;line-height:1;color:var(--red);">{{ down }}</span>
|
||||
<span style="font-size:0.78rem;color:var(--text-muted);margin-left:0.25rem;">down</span></div>
|
||||
{% endif %}
|
||||
{% if pending %}
|
||||
<div><span style="font-size:1.4rem;font-weight:700;line-height:1;color:var(--text-dim);">{{ pending }}</span>
|
||||
<div><span style="font-size:1.4rem;font-weight:500;line-height:1;color:var(--text-dim);">{{ pending }}</span>
|
||||
<span style="font-size:0.78rem;color:var(--text-muted);margin-left:0.25rem;">pending</span></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div id="ansible-source-view-{{ idx }}"
|
||||
style="display:flex;align-items:center;gap:0.75rem;padding:0.85rem 1rem;">
|
||||
|
||||
<span style="flex-shrink:0;font-size:0.7rem;font-weight:600;text-transform:uppercase;
|
||||
<span style="flex-shrink:0;font-size:0.7rem;font-weight:500;text-transform:uppercase;
|
||||
letter-spacing:0.06em;padding:0.15em 0.5em;border-radius:3px;
|
||||
background:{% if src.type == 'git' %}var(--fs-accent-faint){% else %}var(--bg-elevated){% endif %};
|
||||
color:{% if src.type == 'git' %}var(--accent){% else %}var(--text-muted){% endif %};">
|
||||
@@ -20,7 +20,7 @@
|
||||
</span>
|
||||
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="font-weight:600;font-size:0.9rem;">{{ src.name }}</div>
|
||||
<div style="font-weight:500;font-size:0.9rem;">{{ src.name }}</div>
|
||||
<div style="font-size:0.78rem;color:var(--text-muted);margin-top:0.1rem;
|
||||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">
|
||||
{% if src.type == 'git' %}
|
||||
@@ -136,7 +136,7 @@
|
||||
<details class="card" style="padding:0;overflow:hidden;">
|
||||
<summary style="display:flex;align-items:center;gap:0.75rem;padding:0.85rem 1rem;
|
||||
cursor:pointer;list-style:none;user-select:none;">
|
||||
<span style="flex:1;font-weight:600;font-size:0.875rem;color:var(--text);">Add Source</span>
|
||||
<span style="flex:1;font-weight:500;font-size:0.875rem;color:var(--text);">Add Source</span>
|
||||
<span class="btn btn-sm" style="pointer-events:none;">New ▾</span>
|
||||
</summary>
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<div style="display:flex;align-items:flex-start;gap:1rem;">
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="display:flex;align-items:center;gap:0.6rem;flex-wrap:wrap;">
|
||||
<span style="font-weight:600;color:var(--text);font-size:0.9rem;">{{ plugin.name }}</span>
|
||||
<span style="font-weight:500;color:var(--text);font-size:0.9rem;">{{ plugin.name }}</span>
|
||||
<span style="font-size:0.75rem;color:var(--text-muted);">v{{ plugin.version }}</span>
|
||||
|
||||
{% if update_available %}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div id="plugin-repo-view-{{ idx }}"
|
||||
style="display:flex;align-items:center;gap:0.75rem;padding:0.85rem 1rem;">
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="font-weight:600;font-size:0.88rem;">{{ repo.name or repo.url }}</div>
|
||||
<div style="font-weight:500;font-size:0.88rem;">{{ repo.name or repo.url }}</div>
|
||||
<div style="font-size:0.77rem;color:var(--text-muted);margin-top:0.1rem;
|
||||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{{ repo.url }}</div>
|
||||
</div>
|
||||
@@ -65,7 +65,7 @@
|
||||
<details class="card" style="padding:0;overflow:hidden;">
|
||||
<summary style="display:flex;align-items:center;gap:0.75rem;padding:0.85rem 1rem;
|
||||
cursor:pointer;list-style:none;user-select:none;">
|
||||
<span style="flex:1;font-weight:600;font-size:0.875rem;color:var(--text);">Add Repository</span>
|
||||
<span style="flex:1;font-weight:500;font-size:0.875rem;color:var(--text);">Add Repository</span>
|
||||
<span class="btn btn-sm" style="pointer-events:none;">New ▾</span>
|
||||
</summary>
|
||||
<form hx-post="/settings/plugins/repos/add"
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
name="plugin.{{ plugin._dir }}.enabled"
|
||||
id="plugin-{{ plugin._dir }}-enabled"
|
||||
{% if plugin._enabled %}checked{% endif %}>
|
||||
<label for="plugin-{{ plugin._dir }}-enabled" style="margin:0;font-weight:600;cursor:pointer;">
|
||||
<label for="plugin-{{ plugin._dir }}-enabled" style="margin:0;font-weight:500;cursor:pointer;">
|
||||
Enable this plugin
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{% endif %}
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="display:flex;align-items:baseline;gap:0.5rem;flex-wrap:wrap;">
|
||||
<span style="font-weight:600;font-size:0.9rem;color:var(--text);">{{ plugin.get('name', plugin._dir) }}</span>
|
||||
<span style="font-weight:500;font-size:0.9rem;color:var(--text);">{{ plugin.get('name', plugin._dir) }}</span>
|
||||
<span style="font-size:0.75rem;color:var(--text-muted);">v{{ plugin.get('version', '?') }}</span>
|
||||
{% if fail_reason %}
|
||||
<span style="font-size:0.72rem;padding:0.1em 0.45em;border-radius:3px;
|
||||
@@ -109,7 +109,7 @@
|
||||
title="{{ orphan.reason or 'Configured but not installed' }}"></span>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="display:flex;align-items:baseline;gap:0.5rem;flex-wrap:wrap;">
|
||||
<span style="font-weight:600;font-size:0.9rem;color:var(--text);">{{ orphan.name }}</span>
|
||||
<span style="font-weight:500;font-size:0.9rem;color:var(--text);">{{ orphan.name }}</span>
|
||||
{% if orphan.reason %}
|
||||
<span style="font-size:0.72rem;padding:0.1em 0.45em;border-radius:3px;
|
||||
background:color-mix(in srgb,var(--red) 15%,var(--bg));color:var(--red);">Failed to load</span>
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
<label>{{ label }} <span style="color:var(--text-muted);font-size:0.8rem;">({{ unit }})</span></label>
|
||||
<div style="display:flex;gap:1.25rem;flex-wrap:wrap;margin-top:0.25rem;">
|
||||
<span style="display:flex;align-items:center;gap:0.4rem;">
|
||||
<span style="color:var(--yellow);font-size:0.8rem;font-weight:600;">warn</span>
|
||||
<span style="color:var(--yellow);font-size:0.8rem;font-weight:500;">warn</span>
|
||||
<input type="number" name="{{ warn_field }}" step="{{ step }}"
|
||||
value="{{ settings[warn_key] }}" style="max-width:110px;">
|
||||
</span>
|
||||
<span style="display:flex;align-items:center;gap:0.4rem;">
|
||||
<span style="color:var(--red);font-size:0.8rem;font-weight:600;">crit</span>
|
||||
<span style="color:var(--red);font-size:0.8rem;font-weight:500;">crit</span>
|
||||
<input type="number" name="{{ crit_field }}" step="{{ step }}"
|
||||
value="{{ settings[crit_key] }}" style="max-width:110px;">
|
||||
</span>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<span title="{{ e.name }}" style="{{ nm_style }}">{{ e.name }}</span>
|
||||
{% endif %}
|
||||
<span style="color:var(--text-dim);font-size:0.7rem;text-transform:uppercase;">{{ e.kind }}</span>
|
||||
<span style="color:{% if e.status == 'down' %}var(--red){% else %}var(--yellow){% endif %};font-size:0.75rem;font-weight:600;">{{ e.status }}</span>
|
||||
<span style="color:{% if e.status == 'down' %}var(--red){% else %}var(--yellow){% endif %};font-size:0.75rem;font-weight:500;">{{ e.status }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if problems|length > 8 %}
|
||||
|
||||
Reference in New Issue
Block a user