Files
terdut-server/internal/web/static/index.html
T
Niklas Ye d728af53b1
CI / chart (pull_request) Successful in 1s
CI / security (pull_request) Successful in 13s
CI / test (pull_request) Successful in 2m19s
Put a team's own settings in the web UI
Closes #17. Everything a team owner configures was API-only: escalation,
integrations, dead man's switches, membership, and the rota -- which the
on-call view still described as the TUI's job, and the TUI has been broken
against this server since teams landed. Setting up the feature this whole
line of work exists for meant using curl.

A Team tab now holds all of it, one team at a time, with a picker for
somebody in more than one. An owner edits; a member sees the same page
without the controls, because the server refuses their writes anyway --
hiding a button is a courtesy to the reader, not the thing enforcing
anything.

The escalation editor holds a draft and sends the whole ladder, because
the API replaces it wholesale: the levels are an order, and patching one
rung leaves the numbering of the others undecided. Adding a level
defaults to five minutes and the rota, which is the shape almost every
ladder starts as.

An integration key is returned exactly once, so creating one opens a
panel that says so, shows the URL large with a copy button, and renders
the Alertmanager receiver snippet with the URL already in it -- the next
thing anybody does with that key is paste it into a config. The panel
stays until it is dismissed rather than disappearing on the next
re-render.

The incident view gains where an incident is on the ladder and when the
next page is due, which is the question somebody looking at an
unacknowledged incident actually has. The API carries it: the incident
payload now includes escalation_level and escalation_due_at, the latter
computed in the incident SELECT by joining the level's timeout, so a list
costs no extra queries.

Verified against a live server by making every call the page makes,
including the writes: the six reads the Team tab issues, a two-level
ladder saved and read back, an integration created and its key returned
once, three days of rota assigned, switches set, and an incident showing
level 1 with a due time five minutes out.

Claude-Session: https://claude.ai/code/session_01RHPj4ggeFdEjKKfm4SHbD7
2026-09-20 21:22:24 +02:00

103 lines
4.8 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#f5f6f8" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#0f1115" media="(prefers-color-scheme: dark)">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="terdut">
<title>terdut</title>
<link rel="manifest" href="/manifest.webmanifest">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="stylesheet" href="/app.css">
<script type="module" src="/js/app.js"></script>
</head>
<body>
<div id="boot" class="boot" aria-busy="true"><span class="spinner"></span></div>
<main id="login" class="login" hidden>
<form id="login-form" class="login-card" autocomplete="on">
<div class="login-brand">
<img src="/icon.svg" alt="" width="40" height="40">
<h1>terdut</h1>
</div>
<label>
<span>Username</span>
<input name="username" autocomplete="username" autocapitalize="none" spellcheck="false" required>
</label>
<label>
<span>Password</span>
<input name="password" type="password" autocomplete="current-password" required>
</label>
<p class="form-error" role="alert" hidden></p>
<button class="btn btn-primary btn-block" type="submit">Sign in</button>
<p class="login-hint">No password yet? Ask an admin to set one, or run
<code>PUT /api/users/{id}/password</code> with your API key.</p>
</form>
</main>
<div id="app" class="app" hidden>
<nav class="nav" aria-label="Sections">
<a class="nav-brand" href="/">
<img src="/icon.svg" alt="" width="28" height="28">
<span>terdut</span>
</a>
<a class="nav-link" href="/" data-section="queue">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h10"/></svg>
<span class="nav-label">Queue</span>
<span class="nav-badge" data-badge hidden></span>
</a>
<a class="nav-link" href="/oncall" data-section="oncall">
<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="5" width="17" height="15" rx="2"/><path d="M3.5 10h17M8 3v4M16 3v4"/></svg>
<span class="nav-label">On-call</span>
</a>
<a class="nav-link" href="/alerts" data-section="alerts">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 16V11a6 6 0 0 1 12 0v5l1.5 2h-15z"/><path d="M10 20.5a2 2 0 0 0 4 0"/></svg>
<span class="nav-label">Alerts</span>
</a>
<a class="nav-link" href="/team" data-section="team">
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="9" cy="8" r="3"/><circle cx="17" cy="9" r="2.5"/><path d="M3 19a6 6 0 0 1 12 0M15 19a5 5 0 0 1 6-4"/></svg>
<span class="nav-label">Team</span>
</a>
<!-- Hidden unless the signed-in user is a system administrator; app.js
unhides it once /api/me says so. The server refuses every admin
endpoint regardless, so this is a courtesy and not a gate. -->
<a class="nav-link" href="/admin" data-section="admin" id="nav-admin" hidden>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3l7 3v6c0 4-3 7-7 9-4-2-7-5-7-9V6z"/></svg>
<span class="nav-label">Admin</span>
</a>
<a class="nav-link" href="/more" data-section="more">
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="8" r="3.5"/><path d="M5 20a7 7 0 0 1 14 0"/></svg>
<span class="nav-label">Account</span>
</a>
</nav>
<header class="topbar">
<h1 class="topbar-title" id="topbar-title">Queue</h1>
<span class="open-pill" id="open-pill" hidden></span>
</header>
<section id="view-queue" class="view view-queue" data-view="queue">
<div class="pane pane-list">
<div class="chips" id="queue-filters" role="tablist" aria-label="Filter"></div>
<div id="queue-list" class="list"></div>
</div>
<div class="pane pane-detail" id="detail" aria-live="polite"></div>
</section>
<section id="view-oncall" class="view view-page" data-view="oncall" hidden></section>
<section id="view-alerts" class="view view-page" data-view="alerts" hidden></section>
<section id="view-team" class="view view-page" data-view="team" hidden></section>
<section id="view-admin" class="view view-page" data-view="admin" hidden></section>
<section id="view-more" class="view view-page" data-view="more" hidden></section>
</div>
<dialog id="sheet" class="sheet"></dialog>
<div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
</body>
</html>