88ab5b917e
Renames the Python package directory, CLI command, env var prefix, docker-compose service/container/image, Postgres role/db, and all visible branding. Marketing form is "Fabled Steward". Clean break from the previous rebrand: drops the fabledscryer→roundtable import shim in __init__.py and the FABLEDSCRYER_* env var fallback in config.py and migrations/env.py. Env vars are now STEWARD_* only. Heads-up for existing deployments: - Postgres user/db renamed fabledscryer → steward in docker-compose.yml. Existing volumes need the role/db renamed inside Postgres, or override POSTGRES_USER/POSTGRES_DB to keep the old names. - Host-agent systemd unit is now steward-agent.service. Existing agents keep running under the old name; reinstall to switch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
592 lines
23 KiB
Python
592 lines
23 KiB
Python
from __future__ import annotations
|
|
import hashlib
|
|
import json
|
|
import secrets
|
|
from datetime import datetime, timezone, timedelta
|
|
from urllib.parse import urlencode
|
|
from quart import Blueprint, render_template, current_app, abort, redirect, request, session
|
|
from sqlalchemy import select, func, update
|
|
from steward.auth.middleware import require_role, current_user_id
|
|
from steward.models.users import UserRole
|
|
from steward.models.hosts import Host
|
|
from steward.models.monitors import PingResult, DnsResult
|
|
from steward.models.dashboard import Dashboard, DashboardWidget, DashboardShareToken
|
|
from steward.core.settings import public_base_url
|
|
from steward.core.widgets import get_available_widgets, WIDGET_REGISTRY
|
|
|
|
dashboard_bp = Blueprint("dashboard", __name__)
|
|
|
|
|
|
# ── Access helpers ────────────────────────────────────────────────────────────
|
|
|
|
def _can_view(dash: Dashboard, user_id: str, user_role: str) -> bool:
|
|
if user_role == UserRole.admin:
|
|
return True
|
|
if dash.owner_id is None: # system dashboard
|
|
return True
|
|
if dash.owner_id == user_id: # own dashboard
|
|
return True
|
|
return dash.is_shared # shared by another user
|
|
|
|
|
|
def _can_edit(dash: Dashboard, user_id: str, user_role: str) -> bool:
|
|
if user_role == UserRole.admin:
|
|
return True
|
|
return dash.owner_id == user_id
|
|
|
|
|
|
# ── DB helpers ────────────────────────────────────────────────────────────────
|
|
|
|
async def _get_widgets(db, dashboard_id: int) -> list[DashboardWidget]:
|
|
result = await db.execute(
|
|
select(DashboardWidget)
|
|
.where(DashboardWidget.dashboard_id == dashboard_id)
|
|
.order_by(DashboardWidget.position)
|
|
)
|
|
return list(result.scalars())
|
|
|
|
|
|
async def _normalise_positions(db, dashboard_id: int) -> None:
|
|
for i, w in enumerate(await _get_widgets(db, dashboard_id)):
|
|
w.position = i
|
|
|
|
|
|
async def _accessible_dashboards(db, user_id: str, user_role: str) -> list[Dashboard]:
|
|
"""All dashboards visible to this user, ordered by id."""
|
|
result = await db.execute(select(Dashboard).order_by(Dashboard.id))
|
|
all_dashes = list(result.scalars())
|
|
return [d for d in all_dashes if _can_view(d, user_id, user_role)]
|
|
|
|
|
|
async def _resolve_default(db, user_id: str, user_role: str) -> Dashboard | None:
|
|
"""
|
|
1. User's own dashboard with is_default=True
|
|
2. System dashboard (owner_id=None) with is_default=True
|
|
3. First accessible dashboard
|
|
"""
|
|
result = await db.execute(
|
|
select(Dashboard)
|
|
.where(Dashboard.owner_id == user_id, Dashboard.is_default.is_(True))
|
|
.limit(1)
|
|
)
|
|
dash = result.scalar_one_or_none()
|
|
if dash:
|
|
return dash
|
|
|
|
result = await db.execute(
|
|
select(Dashboard)
|
|
.where(Dashboard.owner_id.is_(None), Dashboard.is_default.is_(True))
|
|
.limit(1)
|
|
)
|
|
dash = result.scalar_one_or_none()
|
|
if dash:
|
|
return dash
|
|
|
|
accessible = await _accessible_dashboards(db, user_id, user_role)
|
|
return accessible[0] if accessible else None
|
|
|
|
|
|
async def _get_or_create_system_default(db) -> Dashboard:
|
|
"""Ensure a system (owner_id=None) default dashboard exists."""
|
|
result = await db.execute(
|
|
select(Dashboard).where(Dashboard.owner_id.is_(None)).limit(1)
|
|
)
|
|
dash = result.scalar_one_or_none()
|
|
if dash is None:
|
|
dash = Dashboard(name="Default", owner_id=None, is_default=True, is_shared=True)
|
|
db.add(dash)
|
|
await db.flush()
|
|
plugins_cfg = current_app.config.get("PLUGINS", {})
|
|
for pos, w in enumerate(get_available_widgets(plugins_cfg)):
|
|
db.add(DashboardWidget(
|
|
dashboard_id=dash.id, widget_key=w["key"], position=pos,
|
|
))
|
|
return dash
|
|
|
|
|
|
async def _get_summary_stats(db) -> dict:
|
|
latest_ping_subq = (
|
|
select(PingResult.host_id, func.max(PingResult.probed_at).label("max_at"))
|
|
.group_by(PingResult.host_id).subquery()
|
|
)
|
|
pr = await db.execute(
|
|
select(PingResult).join(
|
|
latest_ping_subq,
|
|
(PingResult.host_id == latest_ping_subq.c.host_id)
|
|
& (PingResult.probed_at == latest_ping_subq.c.max_at),
|
|
)
|
|
)
|
|
pings = list(pr.scalars())
|
|
ping_up = sum(1 for p in pings if p.status.value == "up")
|
|
ping_down = sum(1 for p in pings if p.status.value == "down")
|
|
|
|
latest_dns_subq = (
|
|
select(DnsResult.host_id, func.max(DnsResult.resolved_at).label("max_at"))
|
|
.group_by(DnsResult.host_id).subquery()
|
|
)
|
|
dr = await db.execute(
|
|
select(DnsResult).join(
|
|
latest_dns_subq,
|
|
(DnsResult.host_id == latest_dns_subq.c.host_id)
|
|
& (DnsResult.resolved_at == latest_dns_subq.c.max_at),
|
|
)
|
|
)
|
|
dns = list(dr.scalars())
|
|
dns_ok = sum(1 for d in dns if d.status.value == "resolved")
|
|
dns_fail = sum(1 for d in dns if d.status.value != "resolved")
|
|
total = (await db.execute(select(func.count()).select_from(Host))).scalar()
|
|
|
|
return dict(ping_up=ping_up, ping_down=ping_down,
|
|
dns_ok=dns_ok, dns_fail=dns_fail, total_hosts=total)
|
|
|
|
|
|
def _resolve_widgets(widgets: list[DashboardWidget]) -> list[dict]:
|
|
plugins_cfg = current_app.config.get("PLUGINS", {})
|
|
out = []
|
|
for w in widgets:
|
|
defn = WIDGET_REGISTRY.get(w.widget_key)
|
|
if defn is None:
|
|
continue
|
|
if defn["plugin"] and not plugins_cfg.get(defn["plugin"], {}).get("enabled", False):
|
|
continue
|
|
# Build stored config (falls back to empty; defaults live in the registry)
|
|
config: dict = {}
|
|
if w.config_json:
|
|
try:
|
|
config = json.loads(w.config_json)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
# Always append wid so widget templates can create unique canvas IDs
|
|
url_params = {**config, "wid": w.id}
|
|
hx_url = defn["hx_url"] + "?" + urlencode(url_params)
|
|
out.append({
|
|
"db": w,
|
|
"defn": defn,
|
|
"hx_url": hx_url,
|
|
"title": w.title or defn["label"],
|
|
})
|
|
return out
|
|
|
|
|
|
# ── Edit panel helpers ────────────────────────────────────────────────────────
|
|
|
|
async def _get_panels_data(db, dashboard_id: int) -> tuple:
|
|
result = await db.execute(select(Dashboard).where(Dashboard.id == dashboard_id))
|
|
dash = result.scalar_one_or_none()
|
|
if dash is None:
|
|
return None, [], []
|
|
widgets = await _get_widgets(db, dashboard_id)
|
|
plugins_cfg = current_app.config.get("PLUGINS", {})
|
|
available = get_available_widgets(plugins_cfg)
|
|
# Multiple instances of the same widget type are allowed — show all available
|
|
resolved = [
|
|
{"db": w, "defn": WIDGET_REGISTRY[w.widget_key], "title": w.title or WIDGET_REGISTRY[w.widget_key]["label"]}
|
|
for w in widgets if w.widget_key in WIDGET_REGISTRY
|
|
]
|
|
return dash, resolved, available
|
|
|
|
|
|
async def _panels_response(dashboard_id: int):
|
|
async with current_app.db_sessionmaker() as db:
|
|
dash, resolved, addable = await _get_panels_data(db, dashboard_id)
|
|
return await render_template(
|
|
"dashboard/_edit_panels.html",
|
|
dashboard=dash, widgets=resolved, addable=addable,
|
|
)
|
|
|
|
|
|
# ── Root redirect ─────────────────────────────────────────────────────────────
|
|
|
|
@dashboard_bp.get("/")
|
|
@require_role(UserRole.viewer)
|
|
async def index():
|
|
user_id = current_user_id()
|
|
user_role = session.get("user_role", "viewer")
|
|
async with current_app.db_sessionmaker() as db:
|
|
async with db.begin():
|
|
await _get_or_create_system_default(db)
|
|
dash = await _resolve_default(db, user_id, user_role)
|
|
if dash is None:
|
|
abort(404)
|
|
return redirect(f"/d/{dash.id}")
|
|
|
|
|
|
# ── Dashboard view ────────────────────────────────────────────────────────────
|
|
|
|
@dashboard_bp.get("/d/<int:dash_id>")
|
|
@require_role(UserRole.viewer)
|
|
async def view(dash_id: int):
|
|
user_id = current_user_id()
|
|
user_role = session.get("user_role", "viewer")
|
|
|
|
async with current_app.db_sessionmaker() as db:
|
|
result = await db.execute(select(Dashboard).where(Dashboard.id == dash_id))
|
|
dash = result.scalar_one_or_none()
|
|
if dash is None or not _can_view(dash, user_id, user_role):
|
|
abort(404)
|
|
|
|
stats = await _get_summary_stats(db)
|
|
widgets = await _get_widgets(db, dash_id)
|
|
accessible = await _accessible_dashboards(db, user_id, user_role)
|
|
|
|
resolved = _resolve_widgets(widgets)
|
|
poll_interval = current_app.config.get("MONITORS_POLL_INTERVAL", 60)
|
|
can_edit = _can_edit(dash, user_id, user_role)
|
|
|
|
return await render_template(
|
|
"dashboard/index.html",
|
|
dashboard=dash,
|
|
all_dashboards=accessible,
|
|
widgets=resolved,
|
|
poll_interval=poll_interval,
|
|
can_edit=can_edit,
|
|
**stats,
|
|
)
|
|
|
|
|
|
# ── Dashboard management ──────────────────────────────────────────────────────
|
|
|
|
@dashboard_bp.get("/dashboards/")
|
|
@require_role(UserRole.viewer)
|
|
async def list_dashboards():
|
|
user_id = current_user_id()
|
|
user_role = session.get("user_role", "viewer")
|
|
|
|
async with current_app.db_sessionmaker() as db:
|
|
async with db.begin():
|
|
await _get_or_create_system_default(db)
|
|
accessible = await _accessible_dashboards(db, user_id, user_role)
|
|
|
|
my_dashes = [d for d in accessible if d.owner_id == user_id]
|
|
shared_dashes = [d for d in accessible if d.owner_id != user_id]
|
|
|
|
return await render_template(
|
|
"dashboard/list.html",
|
|
my_dashboards=my_dashes,
|
|
shared_dashboards=shared_dashes,
|
|
user_id=user_id,
|
|
user_role=user_role,
|
|
)
|
|
|
|
|
|
@dashboard_bp.post("/dashboards/create")
|
|
@require_role(UserRole.viewer)
|
|
async def create_dashboard():
|
|
user_id = current_user_id()
|
|
form = await request.form
|
|
name = (form.get("name") or "New Dashboard").strip()[:128]
|
|
async with current_app.db_sessionmaker() as db:
|
|
async with db.begin():
|
|
dash = Dashboard(name=name, owner_id=user_id, is_default=False, is_shared=False)
|
|
db.add(dash)
|
|
await db.flush()
|
|
dash_id = dash.id
|
|
return redirect(f"/d/{dash_id}/edit")
|
|
|
|
|
|
@dashboard_bp.post("/dashboards/<int:dash_id>/rename")
|
|
@require_role(UserRole.viewer)
|
|
async def rename_dashboard(dash_id: int):
|
|
user_id = current_user_id()
|
|
user_role = session.get("user_role", "viewer")
|
|
form = await request.form
|
|
name = (form.get("name") or "").strip()[:128]
|
|
if name:
|
|
async with current_app.db_sessionmaker() as db:
|
|
async with db.begin():
|
|
result = await db.execute(select(Dashboard).where(Dashboard.id == dash_id))
|
|
dash = result.scalar_one_or_none()
|
|
if dash and _can_edit(dash, user_id, user_role):
|
|
dash.name = name
|
|
return redirect("/dashboards/")
|
|
|
|
|
|
@dashboard_bp.post("/dashboards/<int:dash_id>/toggle-share")
|
|
@require_role(UserRole.viewer)
|
|
async def toggle_share(dash_id: int):
|
|
user_id = current_user_id()
|
|
user_role = session.get("user_role", "viewer")
|
|
async with current_app.db_sessionmaker() as db:
|
|
async with db.begin():
|
|
result = await db.execute(select(Dashboard).where(Dashboard.id == dash_id))
|
|
dash = result.scalar_one_or_none()
|
|
if dash and _can_edit(dash, user_id, user_role):
|
|
dash.is_shared = not dash.is_shared
|
|
return redirect("/dashboards/")
|
|
|
|
|
|
@dashboard_bp.post("/dashboards/<int:dash_id>/set-default")
|
|
@require_role(UserRole.viewer)
|
|
async def set_default_dashboard(dash_id: int):
|
|
user_id = current_user_id()
|
|
user_role = session.get("user_role", "viewer")
|
|
async with current_app.db_sessionmaker() as db:
|
|
async with db.begin():
|
|
result = await db.execute(select(Dashboard).where(Dashboard.id == dash_id))
|
|
dash = result.scalar_one_or_none()
|
|
if dash is None or not _can_view(dash, user_id, user_role):
|
|
abort(404)
|
|
# Clear is_default on all dashboards this user owns
|
|
await db.execute(
|
|
update(Dashboard)
|
|
.where(Dashboard.owner_id == user_id)
|
|
.values(is_default=False)
|
|
)
|
|
# If setting a system/shared dashboard as default, we instead create
|
|
# a personal preference record by marking the system one (safe since
|
|
# system dashboards are shared — the flag just signals this user's preference)
|
|
dash.is_default = True
|
|
return redirect("/dashboards/")
|
|
|
|
|
|
@dashboard_bp.post("/dashboards/<int:dash_id>/delete")
|
|
@require_role(UserRole.viewer)
|
|
async def delete_dashboard(dash_id: int):
|
|
user_id = current_user_id()
|
|
user_role = session.get("user_role", "viewer")
|
|
async with current_app.db_sessionmaker() as db:
|
|
async with db.begin():
|
|
result = await db.execute(select(Dashboard).where(Dashboard.id == dash_id))
|
|
dash = result.scalar_one_or_none()
|
|
if dash is None or not _can_edit(dash, user_id, user_role):
|
|
abort(403)
|
|
# Count remaining owned by this user (don't delete their last one if any)
|
|
owned_count = (await db.execute(
|
|
select(func.count()).select_from(Dashboard)
|
|
.where(Dashboard.owner_id == user_id)
|
|
)).scalar()
|
|
if dash.owner_id == user_id and owned_count <= 1:
|
|
pass # refuse to delete user's last owned dashboard
|
|
else:
|
|
await db.delete(dash)
|
|
return redirect("/dashboards/")
|
|
|
|
|
|
# ── Dashboard edit ────────────────────────────────────────────────────────────
|
|
|
|
@dashboard_bp.get("/d/<int:dash_id>/edit")
|
|
@require_role(UserRole.viewer)
|
|
async def edit(dash_id: int):
|
|
user_id = current_user_id()
|
|
user_role = session.get("user_role", "viewer")
|
|
async with current_app.db_sessionmaker() as db:
|
|
dash, resolved, addable = await _get_panels_data(db, dash_id)
|
|
if dash is None or not _can_edit(dash, user_id, user_role):
|
|
abort(403)
|
|
return await render_template(
|
|
"dashboard/edit.html",
|
|
dashboard=dash, widgets=resolved, addable=addable,
|
|
)
|
|
|
|
|
|
@dashboard_bp.post("/d/<int:dash_id>/edit/add/<widget_key>")
|
|
@require_role(UserRole.viewer)
|
|
async def add_widget(dash_id: int, widget_key: str):
|
|
user_id = current_user_id()
|
|
user_role = session.get("user_role", "viewer")
|
|
defn = WIDGET_REGISTRY.get(widget_key)
|
|
if defn is None:
|
|
abort(400)
|
|
form = await request.form
|
|
title = form.get("title", "").strip() or None
|
|
# Parse configured params
|
|
config: dict = {}
|
|
for p in defn.get("params", []):
|
|
val = form.get(f"param_{p['key']}")
|
|
if val is not None:
|
|
if isinstance(p["default"], int):
|
|
try:
|
|
config[p["key"]] = int(val)
|
|
except ValueError:
|
|
config[p["key"]] = p["default"]
|
|
else:
|
|
config[p["key"]] = val
|
|
config_json = json.dumps(config) if config else None
|
|
async with current_app.db_sessionmaker() as db:
|
|
async with db.begin():
|
|
result = await db.execute(select(Dashboard).where(Dashboard.id == dash_id))
|
|
dash = result.scalar_one_or_none()
|
|
if dash is None or not _can_edit(dash, user_id, user_role):
|
|
abort(403)
|
|
widgets = await _get_widgets(db, dash_id)
|
|
next_pos = max((w.position for w in widgets), default=-1) + 1
|
|
db.add(DashboardWidget(
|
|
dashboard_id=dash_id, widget_key=widget_key, position=next_pos,
|
|
title=title, config_json=config_json,
|
|
))
|
|
return await _panels_response(dash_id)
|
|
|
|
|
|
@dashboard_bp.post("/d/<int:dash_id>/edit/remove/<int:widget_id>")
|
|
@require_role(UserRole.viewer)
|
|
async def remove_widget(dash_id: int, widget_id: int):
|
|
user_id = current_user_id()
|
|
user_role = session.get("user_role", "viewer")
|
|
async with current_app.db_sessionmaker() as db:
|
|
async with db.begin():
|
|
result = await db.execute(select(Dashboard).where(Dashboard.id == dash_id))
|
|
dash = result.scalar_one_or_none()
|
|
if dash is None or not _can_edit(dash, user_id, user_role):
|
|
abort(403)
|
|
result = await db.execute(
|
|
select(DashboardWidget).where(DashboardWidget.id == widget_id)
|
|
)
|
|
widget = result.scalar_one_or_none()
|
|
if widget and widget.dashboard_id == dash_id:
|
|
await db.delete(widget)
|
|
await _normalise_positions(db, dash_id)
|
|
return await _panels_response(dash_id)
|
|
|
|
|
|
@dashboard_bp.post("/d/<int:dash_id>/edit/reorder")
|
|
@require_role(UserRole.viewer)
|
|
async def reorder_widgets(dash_id: int):
|
|
user_id = current_user_id()
|
|
user_role = session.get("user_role", "viewer")
|
|
data = await request.get_json(silent=True)
|
|
if not data or not isinstance(data.get("order"), list):
|
|
abort(400)
|
|
order: list[int] = data["order"]
|
|
async with current_app.db_sessionmaker() as db:
|
|
async with db.begin():
|
|
result = await db.execute(select(Dashboard).where(Dashboard.id == dash_id))
|
|
dash = result.scalar_one_or_none()
|
|
if dash is None or not _can_edit(dash, user_id, user_role):
|
|
abort(403)
|
|
widgets = await _get_widgets(db, dash_id)
|
|
widget_map = {w.id: w for w in widgets}
|
|
for pos, wid in enumerate(order):
|
|
if wid in widget_map:
|
|
widget_map[wid].position = pos
|
|
return ("", 204)
|
|
|
|
|
|
# ── Share tokens ──────────────────────────────────────────────────────────────
|
|
|
|
@dashboard_bp.get("/d/<int:dash_id>/share")
|
|
@require_role(UserRole.viewer)
|
|
async def manage_share_tokens(dash_id: int):
|
|
user_id = current_user_id()
|
|
user_role = session.get("user_role", "viewer")
|
|
async with current_app.db_sessionmaker() as db:
|
|
result = await db.execute(select(Dashboard).where(Dashboard.id == dash_id))
|
|
dash = result.scalar_one_or_none()
|
|
if dash is None or not _can_edit(dash, user_id, user_role):
|
|
abort(403)
|
|
result = await db.execute(
|
|
select(DashboardShareToken)
|
|
.where(DashboardShareToken.dashboard_id == dash_id)
|
|
.order_by(DashboardShareToken.created_at.desc())
|
|
)
|
|
tokens = list(result.scalars())
|
|
now = datetime.now(timezone.utc)
|
|
return await render_template(
|
|
"dashboard/share.html",
|
|
dashboard=dash, tokens=tokens, now=now,
|
|
)
|
|
|
|
|
|
@dashboard_bp.post("/d/<int:dash_id>/share/create")
|
|
@require_role(UserRole.viewer)
|
|
async def create_share_token(dash_id: int):
|
|
user_id = current_user_id()
|
|
user_role = session.get("user_role", "viewer")
|
|
async with current_app.db_sessionmaker() as db:
|
|
result = await db.execute(select(Dashboard).where(Dashboard.id == dash_id))
|
|
dash = result.scalar_one_or_none()
|
|
if dash is None or not _can_edit(dash, user_id, user_role):
|
|
abort(403)
|
|
|
|
form = await request.form
|
|
label = (form.get("label") or "").strip()[:128]
|
|
expires_in = form.get("expires_in", "")
|
|
|
|
expires_at = None
|
|
if expires_in.isdigit() and int(expires_in) > 0:
|
|
expires_at = datetime.now(timezone.utc) + timedelta(days=int(expires_in))
|
|
|
|
raw_token = secrets.token_urlsafe(32)
|
|
token_hash = hashlib.sha256(raw_token.encode()).hexdigest()
|
|
|
|
async with current_app.db_sessionmaker() as db:
|
|
async with db.begin():
|
|
db.add(DashboardShareToken(
|
|
dashboard_id=dash_id,
|
|
token_hash=token_hash,
|
|
label=label,
|
|
expires_at=expires_at,
|
|
created_by=user_id,
|
|
created_at=datetime.now(timezone.utc),
|
|
))
|
|
|
|
async with current_app.db_sessionmaker() as db:
|
|
result = await db.execute(select(Dashboard).where(Dashboard.id == dash_id))
|
|
dash = result.scalar_one_or_none()
|
|
|
|
share_url = f"{public_base_url(request)}/share/{raw_token}"
|
|
return await render_template(
|
|
"dashboard/share_created.html",
|
|
dashboard=dash,
|
|
raw_token=raw_token,
|
|
label=label,
|
|
expires_at=expires_at,
|
|
share_url=share_url,
|
|
)
|
|
|
|
|
|
@dashboard_bp.post("/d/<int:dash_id>/share/revoke/<int:token_id>")
|
|
@require_role(UserRole.viewer)
|
|
async def revoke_share_token(dash_id: int, token_id: int):
|
|
user_id = current_user_id()
|
|
user_role = session.get("user_role", "viewer")
|
|
async with current_app.db_sessionmaker() as db:
|
|
async with db.begin():
|
|
result = await db.execute(select(Dashboard).where(Dashboard.id == dash_id))
|
|
dash = result.scalar_one_or_none()
|
|
if dash is None or not _can_edit(dash, user_id, user_role):
|
|
abort(403)
|
|
result = await db.execute(
|
|
select(DashboardShareToken).where(DashboardShareToken.id == token_id)
|
|
)
|
|
token = result.scalar_one_or_none()
|
|
if token and token.dashboard_id == dash_id:
|
|
await db.delete(token)
|
|
return redirect(f"/d/{dash_id}/share")
|
|
|
|
|
|
# ── Public share view (no login required) ────────────────────────────────────
|
|
|
|
@dashboard_bp.get("/share/<raw_token>")
|
|
async def public_share(raw_token: str):
|
|
token_hash = hashlib.sha256(raw_token.encode()).hexdigest()
|
|
now = datetime.now(timezone.utc)
|
|
|
|
async with current_app.db_sessionmaker() as db:
|
|
result = await db.execute(
|
|
select(DashboardShareToken)
|
|
.where(DashboardShareToken.token_hash == token_hash)
|
|
)
|
|
share = result.scalar_one_or_none()
|
|
if share is None:
|
|
abort(404)
|
|
if share.expires_at is not None and share.expires_at <= now:
|
|
abort(410)
|
|
result = await db.execute(
|
|
select(Dashboard).where(Dashboard.id == share.dashboard_id)
|
|
)
|
|
dash = result.scalar_one_or_none()
|
|
if dash is None:
|
|
abort(404)
|
|
widgets = await _get_widgets(db, dash.id)
|
|
|
|
resolved = _resolve_widgets(widgets)
|
|
poll_interval = current_app.config.get("MONITORS_POLL_INTERVAL", 60)
|
|
|
|
return await render_template(
|
|
"dashboard/share_view.html",
|
|
dashboard=dash,
|
|
widgets=resolved,
|
|
poll_interval=poll_interval,
|
|
raw_token=raw_token,
|
|
)
|