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>
125 lines
5.1 KiB
HTML
125 lines
5.1 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}AI access — Steward{% endblock %}
|
|
{% block head %}
|
|
<style>
|
|
.ai-access-grid { display:grid; grid-template-columns:minmax(0,1fr) 320px; gap:1.5rem; align-items:start; }
|
|
.ai-access-grid .card-flush { overflow-x:auto; }
|
|
@media (max-width: 900px) { .ai-access-grid { grid-template-columns:minmax(0,1fr); } }
|
|
</style>
|
|
{% endblock %}
|
|
{% block content %}
|
|
{% set role_help = {
|
|
"viewer": "Reads everything you can see. Changes nothing.",
|
|
"operator": "Also runs playbooks and changes monitors and alerts.",
|
|
"admin": "Full access, the same as an admin in the UI.",
|
|
} %}
|
|
<h1 class="page-title">AI access</h1>
|
|
<p style="color:var(--text-muted);font-size:0.9rem;max-width:720px;margin:-0.5rem 0 1.5rem;line-height:1.55;">
|
|
A token lets an AI client such as Claude Code connect to Steward over MCP and act as
|
|
<strong style="color:var(--text);">you</strong>, limited to the role you give the token.
|
|
Everything a token changes is written to the audit log under your name.
|
|
</p>
|
|
|
|
<div class="ai-access-grid">
|
|
|
|
<div>
|
|
<div class="section-title" style="margin-bottom:0.75rem;">Your tokens</div>
|
|
{% if rows %}
|
|
<div class="card-flush">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Role</th>
|
|
<th>Created</th>
|
|
<th>Last used</th>
|
|
<th>Expires</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for row in rows %}
|
|
{% set t = row.token %}
|
|
<tr {% if row.state != 'active' %}style="opacity:0.55;"{% endif %}>
|
|
<td>
|
|
<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>
|
|
<td style="white-space:nowrap;">{{ t.created_at.strftime("%Y-%m-%d") }}</td>
|
|
<td style="white-space:nowrap;">
|
|
{% if t.last_used_at %}{{ t.last_used_at.strftime("%Y-%m-%d %H:%M") }} UTC{% else %}<span style="color:var(--text-muted);">Never</span>{% endif %}
|
|
</td>
|
|
<td style="white-space:nowrap;">
|
|
{% if row.state == 'revoked' %}<span class="badge badge-red">Revoked</span>
|
|
{% elif row.state == 'expired' %}<span class="badge badge-red">Expired</span>
|
|
{% elif t.expires_at %}{{ t.expires_at.strftime("%Y-%m-%d") }}
|
|
{% else %}<span style="color:var(--text-muted);">Never</span>{% endif %}
|
|
</td>
|
|
<td class="td-actions">
|
|
{% if row.state == 'active' %}
|
|
<form method="post" action="/ai-access/tokens/{{ t.id }}/revoke" style="margin:0;"
|
|
data-confirm="Revoke “{{ t.name }}”? Any client using it stops working immediately.">{{ csrf_field() }}
|
|
<button type="submit" class="btn btn-danger btn-sm">Revoke</button>
|
|
</form>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="card empty">
|
|
You have no tokens yet. Create one on the right, then paste it into your AI client.
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div>
|
|
<div class="section-title" style="margin-bottom:0.75rem;">Create a token</div>
|
|
<div class="card" style="margin-bottom:0;">
|
|
<form method="post" action="/ai-access/tokens">{{ csrf_field() }}
|
|
<div class="form-group">
|
|
<label for="tok-name">Name</label>
|
|
<input type="text" id="tok-name" name="name" required maxlength="128"
|
|
placeholder="e.g. Claude Code on my laptop">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="tok-role">Role</label>
|
|
<select id="tok-role" name="role">
|
|
{% for r in grantable %}
|
|
<option value="{{ r.value }}" {% if loop.first %}selected{% endif %}>{{ r.value|capitalize }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<div style="font-size:0.78rem;color:var(--text-muted);margin-top:0.4rem;line-height:1.5;">
|
|
{% for r in grantable %}
|
|
<div><strong style="color:var(--text);">{{ r.value|capitalize }}:</strong> {{ role_help[r.value] }}</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="tok-exp">Expires after</label>
|
|
<select id="tok-exp" name="expires_in_days">
|
|
{% for value, label in expiry_choices %}
|
|
<option value="{{ value }}">{{ label }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<button type="submit" class="btn">Create token</button>
|
|
</form>
|
|
</div>
|
|
<div style="font-size:0.78rem;color:var(--text-muted);margin-top:0.75rem;line-height:1.5;">
|
|
The token is shown <strong>once</strong>. A token never acts above your own role
|
|
({{ user_role.value }}), even if your role is lowered later.
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div style="max-width:720px;margin-top:1.5rem;">
|
|
{% set token = none %}
|
|
{% include "ai_access/_connect.html" %}
|
|
</div>
|
|
{% endblock %}
|