refactor: rename package directory fabledscryer → roundtable
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
{# dashboard/_edit_panels.html — returned by all mutation routes and included by edit.html #}
|
||||
<div id="edit-panels" style="display:grid;grid-template-columns:1fr 340px;gap:1.5rem;align-items:start;">
|
||||
|
||||
{# ── Current layout ───────────────────────────────────────────────────── #}
|
||||
<div>
|
||||
<div class="section-title" style="margin-bottom:0.75rem;">Current Layout</div>
|
||||
{% if widgets %}
|
||||
<div id="widget-sort-list" data-dash-id="{{ dashboard.id }}" style="display:grid;gap:0.5rem;">
|
||||
{% for item in widgets %}
|
||||
<div class="card" data-widget-id="{{ item.db.id }}"
|
||||
style="margin-bottom:0;display:flex;align-items:center;gap:0.75rem;padding:0.85rem 1rem;">
|
||||
|
||||
<div class="drag-handle" style="flex-shrink:0;cursor:grab;color:var(--text-muted);
|
||||
font-size:1.1rem;line-height:1;user-select:none;"
|
||||
title="Drag to reorder">⠿</div>
|
||||
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="font-weight:600;font-size:0.9rem;">{{ item.title }}</div>
|
||||
<div style="font-size:0.78rem;color:var(--text-muted);margin-top:0.1rem;">{{ item.defn.description }}</div>
|
||||
</div>
|
||||
|
||||
<button hx-post="/d/{{ dashboard.id }}/edit/remove/{{ item.db.id }}"
|
||||
hx-target="#edit-panels" hx-swap="outerHTML"
|
||||
class="btn btn-danger btn-sm" style="flex-shrink:0;">Remove</button>
|
||||
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card" style="color:var(--text-muted);text-align:center;padding:2rem;font-size:0.9rem;">
|
||||
No widgets yet. Draw one from the right to begin.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# ── Available widgets ────────────────────────────────────────────────── #}
|
||||
<div>
|
||||
<div class="section-title" style="margin-bottom:0.75rem;">Available Widgets</div>
|
||||
{% if addable %}
|
||||
<div style="display:grid;gap:0.5rem;">
|
||||
{% for w in addable %}
|
||||
<div class="card" style="margin-bottom:0;padding:0;overflow:hidden;">
|
||||
<details>
|
||||
<summary style="display:flex;align-items:center;justify-content:space-between;gap:0.75rem;
|
||||
padding:0.85rem 1rem;cursor:pointer;list-style:none;user-select:none;">
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="font-weight:600;font-size:0.875rem;">{{ w.label }}</div>
|
||||
<div style="font-size:0.77rem;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>
|
||||
</summary>
|
||||
|
||||
<form method="post"
|
||||
action="/d/{{ dashboard.id }}/edit/add/{{ w.key }}"
|
||||
hx-post="/d/{{ dashboard.id }}/edit/add/{{ w.key }}"
|
||||
hx-target="#edit-panels"
|
||||
hx-swap="outerHTML"
|
||||
style="padding:0.75rem 1rem;padding-top:0;border-top:1px solid var(--border);">
|
||||
<div style="display:grid;gap:0.5rem;padding-top:0.75rem;">
|
||||
|
||||
<div>
|
||||
<label style="font-size:0.75rem;color:var(--text-muted);display:block;margin-bottom:0.2rem;">
|
||||
Widget title <span style="color:var(--text-dim);">(optional)</span>
|
||||
</label>
|
||||
<input type="text" name="title" placeholder="{{ w.label }}"
|
||||
style="width:100%;padding:0.32rem 0.6rem;background:var(--bg);
|
||||
border:1px solid var(--border-mid);border-radius:4px;
|
||||
color:var(--text);font-size:0.85rem;">
|
||||
</div>
|
||||
|
||||
{% for p in w.params %}
|
||||
<div>
|
||||
<label style="font-size:0.75rem;color:var(--text-muted);display:block;margin-bottom:0.2rem;">
|
||||
{{ p.label }}
|
||||
</label>
|
||||
{% if p.type == "select" %}
|
||||
<select name="param_{{ p.key }}"
|
||||
style="width:100%;padding:0.32rem 0.6rem;background:var(--bg);
|
||||
border:1px solid var(--border-mid);border-radius:4px;
|
||||
color:var(--text);font-size:0.85rem;">
|
||||
{% for opt in p.options %}
|
||||
<option value="{{ opt.value }}" {% if opt.value == p.default %}selected{% endif %}>
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<button type="submit" class="btn btn-sm" style="width:100%;margin-top:0.1rem;">
|
||||
Add to Dashboard
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="color:var(--text-muted);font-size:0.85rem;padding:1rem 0;">
|
||||
No plugin widgets available. Enable plugins in Settings → Plugins.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,43 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Edit: {{ dashboard.name }} — Fabled Scryer{% endblock %}
|
||||
{% block content %}
|
||||
<div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:1.5rem;">
|
||||
<h1 class="page-title" style="margin-bottom:0;">Edit: {{ dashboard.name }}</h1>
|
||||
<a href="/d/{{ dashboard.id }}" class="btn btn-ghost btn-sm">Done</a>
|
||||
</div>
|
||||
{% include "dashboard/_edit_panels.html" %}
|
||||
{% endblock %}
|
||||
{% block extra_scripts %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
function initSortable() {
|
||||
var el = document.getElementById('widget-sort-list');
|
||||
if (!el) return;
|
||||
var dashId = el.dataset.dashId;
|
||||
Sortable.create(el, {
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
onEnd: function () {
|
||||
var order = Array.from(el.querySelectorAll('[data-widget-id]'))
|
||||
.map(function (row) { return parseInt(row.dataset.widgetId, 10); });
|
||||
fetch('/d/' + dashId + '/edit/reorder', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ order: order }),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Init on first load and after every HTMX swap (add/remove re-renders the list)
|
||||
document.addEventListener('DOMContentLoaded', initSortable);
|
||||
document.addEventListener('htmx:afterSwap', initSortable);
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
.sortable-ghost { opacity: 0.4; }
|
||||
.drag-handle:active { cursor: grabbing; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,88 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ dashboard.name }} — Fabled Scryer{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{# ── Header ────────────────────────────────────────────────────────────────── #}
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem;gap:1rem;flex-wrap:wrap;">
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;">
|
||||
<h1 class="page-title" style="margin-bottom:0;">{{ dashboard.name }}</h1>
|
||||
{% if dashboard.is_default %}
|
||||
<span class="badge badge-gold" style="font-size:0.7rem;">Default</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;">
|
||||
{# Dashboard switcher — shown when more than one dashboard exists #}
|
||||
{% if all_dashboards | length > 1 %}
|
||||
<select onchange="window.location='/d/'+this.value"
|
||||
style="font-size:0.82rem;padding:0.3rem 0.6rem;background:var(--bg-card);border:1px solid var(--border-mid);border-radius:5px;color:var(--text);cursor:pointer;">
|
||||
{% for d in all_dashboards %}
|
||||
<option value="{{ d.id }}" {% if d.id == dashboard.id %}selected{% endif %}>{{ d.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
{% if can_edit %}
|
||||
<a href="/d/{{ dashboard.id }}/edit" class="btn btn-ghost btn-sm">Edit</a>
|
||||
<a href="/d/{{ dashboard.id }}/share" class="btn btn-ghost btn-sm">Share</a>
|
||||
{% endif %}
|
||||
<a href="/dashboards/" class="btn btn-ghost btn-sm">Dashboards</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Summary strip ─────────────────────────────────────────────────────────── #}
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:0.75rem;margin-bottom:1.5rem;">
|
||||
<div class="card" style="margin-bottom:0;">
|
||||
<div class="section-title" style="margin-bottom:0.5rem;">Hosts</div>
|
||||
<span class="stat-val">{{ total_hosts }}</span>
|
||||
<span class="stat-label">total</span>
|
||||
<div style="margin-top:0.5rem;"><a href="/hosts/" style="font-size:0.8rem;">Manage →</a></div>
|
||||
</div>
|
||||
<div class="card" style="margin-bottom:0;">
|
||||
<div class="section-title" style="margin-bottom:0.5rem;">Ping</div>
|
||||
{% if ping_up + ping_down == 0 %}
|
||||
<span style="color:var(--text-muted);font-size:0.85rem;">No hosts</span>
|
||||
{% else %}
|
||||
<span class="stat-val" style="color:var(--green);">{{ ping_up }}</span><span class="stat-label">up</span>
|
||||
{% if ping_down %} <span class="stat-val" style="color:var(--red);">{{ ping_down }}</span><span class="stat-label">down</span>{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card" style="margin-bottom:0;">
|
||||
<div class="section-title" style="margin-bottom:0.5rem;">DNS</div>
|
||||
{% if dns_ok + dns_fail == 0 %}
|
||||
<span style="color:var(--text-muted);font-size:0.85rem;">No hosts</span>
|
||||
{% else %}
|
||||
<span class="stat-val" style="color:var(--green);">{{ dns_ok }}</span><span class="stat-label">ok</span>
|
||||
{% if dns_fail %} <span class="stat-val" style="color:var(--red);">{{ dns_fail }}</span><span class="stat-label">failed</span>{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card" style="margin-bottom:0;">
|
||||
<div class="section-title" style="margin-bottom:0.5rem;">Alerts</div>
|
||||
<a href="/alerts/" style="font-size:0.85rem;">View rules →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Widget grid ───────────────────────────────────────────────────────────── #}
|
||||
{% if widgets %}
|
||||
<div style="column-width:380px;column-count:3;column-gap:1rem;">
|
||||
{% for item in widgets %}
|
||||
<div class="card ping-card" style="break-inside:avoid;margin-bottom:1rem;">
|
||||
<div class="widget-header">
|
||||
<span class="widget-title">{{ item.title }}</span>
|
||||
<a href="{{ item.defn.detail_url }}" class="widget-link">Details →</a>
|
||||
</div>
|
||||
<div id="widget-{{ item.db.id }}"
|
||||
hx-get="{{ item.hx_url }}"
|
||||
hx-trigger="load{% if item.defn.poll %}, every {{ poll_interval }}s{% endif %}"
|
||||
hx-swap="innerHTML">
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card" style="text-align:center;padding:3rem;">
|
||||
<div style="font-size:1.1rem;color:var(--text-muted);margin-bottom:1rem;">No widgets on this dashboard yet.</div>
|
||||
{% if can_edit %}
|
||||
<a href="/d/{{ dashboard.id }}/edit" class="btn">Add Widgets</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,132 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboards — Fabled Scryer{% endblock %}
|
||||
{% block content %}
|
||||
<div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:1.5rem;">
|
||||
<h1 class="page-title" style="margin-bottom:0;">Dashboards</h1>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 300px;gap:1.5rem;align-items:start;">
|
||||
|
||||
{# ── Left: dashboard lists ─────────────────────────────────────────── #}
|
||||
<div style="display:grid;gap:1.5rem;">
|
||||
|
||||
{# My dashboards #}
|
||||
<div>
|
||||
<div class="section-title" style="margin-bottom:0.75rem;">My Dashboards</div>
|
||||
{% if my_dashboards %}
|
||||
<div style="display:grid;gap:0.5rem;">
|
||||
{% for dash in my_dashboards %}
|
||||
<div class="card" style="margin-bottom:0;padding:0.85rem 1rem;">
|
||||
<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>
|
||||
{% if dash.is_default %}
|
||||
<span class="badge badge-gold" style="font-size:0.68rem;">Your Default</span>
|
||||
{% endif %}
|
||||
{% if dash.is_shared %}
|
||||
<span class="badge badge-dim" style="font-size:0.68rem;">Shared</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:0.4rem;flex-shrink:0;">
|
||||
<a href="/d/{{ dash.id }}/edit" class="btn btn-ghost btn-sm">Edit</a>
|
||||
|
||||
<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);">
|
||||
<form method="post" action="/dashboards/{{ dash.id }}/rename" style="display:flex;gap:0.4rem;">
|
||||
<input type="text" name="name" value="{{ dash.name }}"
|
||||
style="flex:1;font-size:0.85rem;padding:0.3rem 0.5rem;" required>
|
||||
<button type="submit" class="btn btn-sm">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<form method="post" action="/dashboards/{{ dash.id }}/toggle-share" style="margin:0;" title="{{ 'Make private' if dash.is_shared else 'Share with all users' }}">
|
||||
<button type="submit" class="btn btn-ghost btn-sm">{{ '🔒' if dash.is_shared else '🔗' }}</button>
|
||||
</form>
|
||||
|
||||
{% if not dash.is_default %}
|
||||
<form method="post" action="/dashboards/{{ dash.id }}/set-default" style="margin:0;" title="Set as my default">
|
||||
<button type="submit" class="btn btn-ghost btn-sm">★</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if my_dashboards | length > 1 %}
|
||||
<form method="post" action="/dashboards/{{ dash.id }}/delete" style="margin:0;"
|
||||
onsubmit="return confirm('Delete \'{{ dash.name }}\'?')">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="color:var(--text-muted);font-size:0.85rem;padding:0.5rem 0;">
|
||||
You don't have any personal dashboards yet.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Shared / system dashboards #}
|
||||
{% if shared_dashboards %}
|
||||
<div>
|
||||
<div class="section-title" style="margin-bottom:0.75rem;">Shared Dashboards</div>
|
||||
<div style="display:grid;gap:0.5rem;">
|
||||
{% for dash in shared_dashboards %}
|
||||
<div class="card" style="margin-bottom:0;padding:0.85rem 1rem;">
|
||||
<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>
|
||||
{% if dash.owner_id is none %}
|
||||
<span class="badge badge-dim" style="font-size:0.68rem;">System</span>
|
||||
{% endif %}
|
||||
{% if dash.is_default %}
|
||||
<span class="badge badge-gold" style="font-size:0.68rem;">Global Default</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:0.4rem;flex-shrink:0;">
|
||||
<a href="/d/{{ dash.id }}" class="btn btn-ghost btn-sm">View</a>
|
||||
<form method="post" action="/dashboards/{{ dash.id }}/set-default" style="margin:0;" title="Set as my default">
|
||||
<button type="submit" class="btn btn-ghost btn-sm">★</button>
|
||||
</form>
|
||||
{# Admins can also edit system dashboards #}
|
||||
{% if user_role == 'admin' %}
|
||||
<a href="/d/{{ dash.id }}/edit" class="btn btn-ghost btn-sm">Edit</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{# ── Right: create new dashboard ──────────────────────────────────── #}
|
||||
<div>
|
||||
<div class="section-title" style="margin-bottom:0.75rem;">New Dashboard</div>
|
||||
<div class="card" style="margin-bottom:0;">
|
||||
<form method="post" action="/dashboards/create">
|
||||
<div class="form-group" style="margin-bottom:0.75rem;">
|
||||
<label>Name</label>
|
||||
<input type="text" name="name" placeholder="e.g. Network Overview" required>
|
||||
</div>
|
||||
<button type="submit" class="btn">Create</button>
|
||||
</form>
|
||||
</div>
|
||||
<div style="font-size:0.78rem;color:var(--text-muted);margin-top:0.75rem;line-height:1.5;">
|
||||
New dashboards are private by default.<br>
|
||||
Use 🔗 to share with all users.<br>
|
||||
Use ★ to set as your default view.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,78 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Share: {{ dashboard.name }} — Fabled Scryer{% endblock %}
|
||||
{% block content %}
|
||||
<div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:1.5rem;">
|
||||
<h1 class="page-title" style="margin-bottom:0;">Share: {{ dashboard.name }}</h1>
|
||||
<a href="/d/{{ dashboard.id }}" class="btn btn-ghost btn-sm">Back to Dashboard</a>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 320px;gap:1.5rem;align-items:start;max-width:900px;">
|
||||
|
||||
{# ── Active tokens ─────────────────────────────────────────────────── #}
|
||||
<div>
|
||||
<div class="section-title" style="margin-bottom:0.75rem;">Active Share Links</div>
|
||||
{% if tokens %}
|
||||
<div style="display:grid;gap:0.5rem;">
|
||||
{% for token in tokens %}
|
||||
{% set expired = token.expires_at is not none and token.expires_at <= now %}
|
||||
<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;">
|
||||
{{ token.label or "Unnamed link" }}
|
||||
{% if expired %}<span class="badge badge-red" style="font-size:0.68rem;">Expired</span>{% endif %}
|
||||
</div>
|
||||
<div style="font-size:0.77rem;color:var(--text-muted);margin-top:0.15rem;">
|
||||
Created {{ token.created_at.strftime("%Y-%m-%d") }}
|
||||
{% if token.expires_at %}
|
||||
· Expires {{ token.expires_at.strftime("%Y-%m-%d") }}
|
||||
{% else %}
|
||||
· Never expires
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="/d/{{ dashboard.id }}/share/revoke/{{ token.id }}"
|
||||
style="margin:0;" onsubmit="return confirm('Revoke this share link?')">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Revoke</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="color:var(--text-muted);font-size:0.85rem;padding:0.5rem 0;">
|
||||
No active share links. Create one on the right.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# ── Create new token ──────────────────────────────────────────────── #}
|
||||
<div>
|
||||
<div class="section-title" style="margin-bottom:0.75rem;">Create Share Link</div>
|
||||
<div class="card" style="margin-bottom:0;">
|
||||
<form method="post" action="/d/{{ dashboard.id }}/share/create">
|
||||
<div class="form-group" style="margin-bottom:0.75rem;">
|
||||
<label>Label <span style="color:var(--text-muted);font-size:0.78rem;">(optional)</span></label>
|
||||
<input type="text" name="label" placeholder="e.g. Public display screen">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0.75rem;">
|
||||
<label>Expires after</label>
|
||||
<select name="expires_in">
|
||||
<option value="">Never</option>
|
||||
<option value="1">1 day</option>
|
||||
<option value="7">7 days</option>
|
||||
<option value="30">30 days</option>
|
||||
<option value="90">90 days</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn">Generate Link</button>
|
||||
</form>
|
||||
</div>
|
||||
<div style="font-size:0.78rem;color:var(--text-muted);margin-top:0.75rem;line-height:1.5;">
|
||||
The link is shown <strong>once</strong> after creation.<br>
|
||||
Revoke it at any time to cut off access immediately.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Share Link Created — Fabled Scryer{% endblock %}
|
||||
{% block content %}
|
||||
<div style="max-width:640px;">
|
||||
<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;">
|
||||
Copy this link now — it cannot be shown again
|
||||
</div>
|
||||
|
||||
{% set share_url = request.host_url.rstrip('/') + '/share/' + raw_token %}
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;">
|
||||
<input type="text" id="share-url" value="{{ share_url }}" readonly
|
||||
style="flex:1;font-family:ui-monospace,monospace;font-size:0.8rem;background:var(--bg);color:var(--text);">
|
||||
<button onclick="navigator.clipboard.writeText(document.getElementById('share-url').value).then(()=>this.textContent='Copied!')"
|
||||
class="btn btn-ghost btn-sm" style="white-space:nowrap;">Copy</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:0.75rem;font-size:0.82rem;color:var(--text-muted);">
|
||||
{% if label %}<strong>Label:</strong> {{ label }} · {% endif %}
|
||||
{% if expires_at %}<strong>Expires:</strong> {{ expires_at.strftime("%Y-%m-%d") }}
|
||||
{% else %}<strong>Expires:</strong> Never{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:0.75rem;">
|
||||
<a href="/d/{{ dashboard.id }}/share" class="btn btn-ghost">Manage Share Links</a>
|
||||
<a href="/d/{{ dashboard.id }}" class="btn btn-ghost">Back to Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,98 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ dashboard.name }} — Fabled Scryer</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=Libertinus+Serif:ital,wght@0,400;0,600;0,700;1,400&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #07071a; --bg-card: #0f0f24; --bg-elevated: #17172e;
|
||||
--border: #1e1e3e; --border-mid: #28284c;
|
||||
--text: #d2cef0; --text-muted: #6858a0; --text-dim: #282860;
|
||||
--accent: #7c5ef4; --gold: #c9a84c;
|
||||
--green: #26d96b; --green-dim: #0e3a1e;
|
||||
--yellow: #e6b818; --red: #e83848; --red-dim: #3a0e14;
|
||||
--orange: #e07828;
|
||||
--font-serif: 'Libertinus Serif', 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; }
|
||||
code { font-family: ui-monospace, monospace; font-size: 0.875em; background: var(--bg-elevated); padding: 0.1em 0.35em; border-radius: 3px; }
|
||||
.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; }
|
||||
.section-title { font-family: var(--font-serif); font-size: 0.8rem; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
.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); }
|
||||
.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; }
|
||||
.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); }
|
||||
.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; }
|
||||
.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-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; }
|
||||
.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; }
|
||||
header { background: var(--bg-card); padding: 0 1.5rem; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--border-mid); height: 48px; font-family: var(--font-serif); }
|
||||
main { padding: 1.5rem 2rem; max-width: 1600px; margin: 0 auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<span style="font-weight:700;font-size:1rem;background:linear-gradient(110deg,#c8b4f8,#c9a84c);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;">
|
||||
{{ dashboard.name }}
|
||||
</span>
|
||||
<span style="font-size:0.78rem;color:var(--text-muted);">Fabled Scryer · read-only view</span>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{% if widgets %}
|
||||
<div style="column-width:380px;column-count:3;column-gap:1rem;">
|
||||
{% for item in widgets %}
|
||||
<div class="card ping-card" style="break-inside:avoid;margin-bottom:1rem;">
|
||||
<div class="widget-header">
|
||||
<span class="widget-title">{{ item.title }}</span>
|
||||
</div>
|
||||
{# Pass share token as &s= param so require_role allows through #}
|
||||
<div id="widget-{{ item.db.id }}"
|
||||
hx-get="{{ item.hx_url }}&s={{ raw_token }}"
|
||||
hx-trigger="load{% if item.defn.poll %}, every {{ poll_interval }}s{% endif %}"
|
||||
hx-swap="innerHTML">
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="color:var(--text-muted);text-align:center;padding:4rem;font-size:0.9rem;">
|
||||
This dashboard has no widgets.
|
||||
</div>
|
||||
{% endif %}
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user