a6fa673e08
The Admin tab's team list was growing controls the way the user list did beforeac9af8e: a Rename button behind window.prompt, a Delete beside it, and -- on the Users page, of all places -- an invite form with a team picker in front of it. The picker was the admission that an invite is a fact about a team rather than about the server, and a prompt() is the wrong place to read a 409 about a name already taken. So a team is now a subject with a page, at /admin/teams/{id}, the mirror of /admin/users/{id}: when it was created, how many are in it and how much is open, a field to rename it, the members with their roles, the invites into it, and deletion. The list goes back to being a list, and the name in it is the way in. The member list is the one thing there that needed a new endpoint. GET /api/teams/{id}/members is requireTeamMember and answers 404 to an administrator who is not in the team, and that stays exactly as it is: member means membership and nothing else. Reading a team's shape is a different question from reading its work, so it gets an endpoint of its own under AdminOnly -- GET /api/admin/teams/{id}, returning {"team", "members"} -- rather than an exception carved into that rule. It is a wrapper and not a team with the members hung off it, because "members" already means a count on the list endpoint and one name must not be a number in one answer and an array in the next. The query and its ordering are copied from handleListTeamMembers so the two answers to "who is in this team" cannot disagree. An administrator still sees none of that team's incidents, alerts or rota. Nothing about what the flag may do changed; it could already rename and delete any team, and staff one it is not in. Rename now trims what it is given, as creation has always trimmed. Before this, " " was a legal name to rename a team to but not to create one with, which is one rule stated twice and applied once. Nobody has looked at this in a browser, the caveatac9af8eand07914d5both carried. What is checked is the wiring: admin_test.go covers the new endpoint for an administrator outside the team, the 404 the member-only endpoint still gives that same administrator, the 403 for a member who is not one, a 404 for a team that does not exist, a 400 for an id that is not a number, and the trim; the module graph evaluates at /admin/teams/{id}, and the server serves index.html there, so a reload survives. Claude-Session: https://claude.ai/code/session_01RHPj4ggeFdEjKKfm4SHbD7
140 lines
6.7 KiB
HTML
140 lines
6.7 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>
|
|
<p class="login-hint" id="signup-link" hidden>
|
|
No account? <a href="/signup">Create one</a>.</p>
|
|
</form>
|
|
|
|
<!-- Sign-up. Shown instead of the login card at /signup, and only offers
|
|
what the server allows: an invite link, or open sign-up. -->
|
|
<form id="signup-form" class="login-card" autocomplete="on" hidden>
|
|
<div class="login-brand">
|
|
<img src="/icon.svg" alt="" width="40" height="40">
|
|
<h1>terdut</h1>
|
|
</div>
|
|
<p class="signup-intro" id="signup-intro"></p>
|
|
<label>
|
|
<span>Username</span>
|
|
<input name="username" autocomplete="username" autocapitalize="none" spellcheck="false" required>
|
|
</label>
|
|
<label>
|
|
<span>Email</span>
|
|
<input name="email" type="email" autocomplete="email" required>
|
|
</label>
|
|
<label>
|
|
<span>Password</span>
|
|
<input name="password" type="password" autocomplete="new-password" minlength="10" required>
|
|
</label>
|
|
<label id="signup-team-label" hidden>
|
|
<span>Team name</span>
|
|
<input name="team_name" autocomplete="off">
|
|
</label>
|
|
<p class="form-error" role="alert" hidden></p>
|
|
<button class="btn btn-primary btn-block" type="submit">Create account</button>
|
|
<p class="login-hint">Already have one? <a href="/">Sign in</a>.</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" aria-label="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" aria-label="On-call">
|
|
<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" aria-label="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" aria-label="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" aria-label="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" aria-label="Account">
|
|
<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>
|
|
<!-- One person, at /admin/users/{id}: reached from the Admin tab's user
|
|
list, and a section of its own so a deep link survives a reload. -->
|
|
<section id="view-adminuser" class="view view-page" data-view="adminuser" hidden></section>
|
|
<!-- One team, at /admin/teams/{id}: who is in it and the invites into it,
|
|
which the Team tab cannot show for a team you are not a member of. -->
|
|
<section id="view-adminteam" class="view view-page" data-view="adminteam" 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>
|