33356ca978
state.js's currentTeam() was hard-coded to teams[0] and never really meant "the team currently selected" — team.js's settings page and queue.js's filter chips each kept their own separate, unsynchronized notion of "which team" instead, so picking one on one page had no effect on the other. Replaces both with a single state.selectedTeamID, set only through the new setSelectedTeam (persisted in localStorage, unlike the queue's old per-tab sessionStorage filter) and broadcast to listeners via onTeamChange. A new teamselector.js control — a coloured dot plus the team's name, or "All teams" — sits at the top of both the desktop sidebar and the mobile topbar, opening the existing bottom-sheet menu to switch. Shown only once someone is in more than one team, matching every other team-aware control in this app. Colours come from a new teamColorClass() in format.js, hashing a team's id into the six-colour rc1..rc6 palette already used for the rota's per-person chips, so no schema or API change is needed. The queue's team filter chips pick up the same colours.
166 lines
8.6 KiB
HTML
166 lines
8.6 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>
|
|
<!-- Why a sign-in failed, when the identity provider sent the browser back
|
|
here with ?sso_error=. Kept apart from the password form's own error. -->
|
|
<p class="form-error" id="sso-error" role="alert" hidden></p>
|
|
<!-- A plain link, not a fetch: the browser has to navigate to the provider,
|
|
and the page's CSP allows no connection to anywhere else. -->
|
|
<a class="btn btn-primary btn-block" id="sso-link" href="/api/oidc/login" hidden>Sign in with SSO</a>
|
|
<p class="login-divider" id="login-or" hidden><span>or</span></p>
|
|
<div class="login-password" id="password-login">
|
|
<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>
|
|
</div>
|
|
</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>
|
|
<!-- Which team the app is scoped to. Hidden unless the signed-in user is
|
|
in more than one; teamselector.js fills it in and wires the click. -->
|
|
<button class="nav-team-selector" id="team-selector" type="button" hidden></button>
|
|
<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="/stats" data-section="stats" aria-label="Stats">
|
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20h16M7 20v-7M12 20V6M17 20v-10"/></svg>
|
|
<span class="nav-label">Stats</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">
|
|
<div class="topbar-left">
|
|
<button class="btn btn-ghost btn-icon" id="menu-btn" type="button" aria-label="Menu" aria-haspopup="menu">
|
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"/></svg>
|
|
<span class="nav-badge menu-btn-badge" data-badge hidden></span>
|
|
</button>
|
|
<button class="team-selector-mobile" id="team-selector-mobile" type="button" hidden></button>
|
|
<h1 class="topbar-title" id="topbar-title">Queue</h1>
|
|
</div>
|
|
<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-stats" class="view view-page" data-view="stats" 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>
|
|
<!-- Approve a terminal's sign-in, at /device?code=...: the page its prompt links to. -->
|
|
<section id="view-device" class="view view-page" data-view="device" hidden></section>
|
|
</div>
|
|
|
|
<dialog id="sheet" class="sheet"></dialog>
|
|
<div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
|
|
</body>
|
|
</html>
|