Files
hyprcrd/relay/ui/layout.php
T
thandClaude Opus 4.6 a67bedc6f2 relay: burger menu for mobile, admin reachable, badge matches header buttons
The relay's nav row used to flatten on mobile by hiding admin / user
identity / compose buttons outright, leaving only "account" + "logout".
Admin was completely unreachable from a phone. Replaces that with a
proper burger menu and exposes every nav target.

* layout.php: new <button id="nav-burger"> with a ph-list icon between
  the spacer and #nav-buttons. Inline `style="display:flex"` removed
  from #nav-buttons — display is now CSS-driven, so the burger
  popover and the desktop row don't fight each other.
* app.js showView(): the mobile-block on `name === 'admin'` is gone.
  Burger button visibility is now mirrored against the same
  setup/login/change-password gating that hides #nav-buttons. Picking
  any view also clears `.menu-open` so the popover collapses behind
  the freshly-shown view.
* app.js toggleNavMenu(): exposed on window for the inline onclick.
  Document-level click outside closes the menu.
* style.css: default `#nav-buttons:not(.hidden)` becomes flex (same
  semantics as the old inline). #nav-burger is `display:none` on
  desktop, `inline-flex` on mobile. Mobile media query drops the old
  display:none rules for #nav-admin / #view-admin / #nav-compose,
  keeps #nav-user hidden (redundant with the menu), and adds a
  `.menu-open` rule that turns #nav-buttons into a popover anchored
  to the header (position:absolute, right:12px, surface bg, border,
  shadow, z-index 50). Buttons inside the popover go full-width
  left-aligned. header { position:relative } so the absolute
  positioning has an anchor.
* style.css: also #view-admin .section gets a simple display:block
  override on mobile so the desktop grid-area layout doesn't break
  the page when admin is opened from a phone.

header .user .badge sizing (carried over from the previous turn): the
admin badge in the relay header now matches the .nav-btn box-model
exactly (5px 10px padding, 9px font, 1px border, 3px radius, line-
height:1, pixel font), so it sits flush with the nav buttons instead
of being a tiny pill in the middle of the row.

chore: bump version to 0.9.8

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 23:40:26 +02:00

188 lines
9.4 KiB
PHP

<?php
if (!defined('RELAY_BOOTSTRAPPED')) { http_response_code(404); exit; }
?><!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#1e293b">
<title>hyprcrd relay</title>
<link rel="stylesheet" href="fonts/phosphor.css">
<link rel="stylesheet" href="ui/style.css">
</head>
<body>
<header>
<span class="dot"></span>
<h1>hyprcrd relay</h1>
<span id="header-status" class="pixel" style="font-size:9px;color:var(--dim);"></span>
<span class="spacer"></span>
<button id="nav-burger" class="nav-btn hidden" aria-label="menu" onclick="toggleNavMenu()"><i class="ph ph-list"></i></button>
<div id="nav-buttons" class="hidden">
<span id="nav-user" class="user"></span>
<button id="nav-compose" class="nav-btn active" onclick="showView('compose')"><i class="ph ph-pencil-line"></i> compose</button>
<button id="nav-account" class="nav-btn" onclick="showView('account')"><i class="ph ph-user-circle"></i> account</button>
<button id="nav-admin" class="nav-btn hidden" onclick="showView('admin')"><i class="ph ph-users"></i> users</button>
<button class="nav-btn danger" onclick="doLogout()"><i class="ph ph-sign-out"></i> logout</button>
</div>
</header>
<main>
<!-- ── Setup view (first install) ── -->
<div id="view-setup" class="auth-form hidden">
<h2>welcome to relay</h2>
<p>This is the first time the relay is being set up. The user you create here will become the <strong>admin</strong>. You'll also set the <strong>super-shared secret</strong> — a relay-wide key used to route messages between tenants.</p>
<div class="field">
<label>admin username</label>
<input type="text" id="setup-username" placeholder="alice" autocomplete="username">
</div>
<div class="field">
<label>admin password</label>
<input type="password" id="setup-password" placeholder="min 4 chars" autocomplete="new-password">
</div>
<div class="field">
<label>super-shared secret</label>
<input type="password" id="setup-super-secret" placeholder="min 12 chars" autocomplete="new-password">
<p style="font-size:9px;color:var(--dim);margin-top:4px;line-height:1.4;">Used by the relay to encrypt messages at rest and route them between tenants. Set once, can't be changed without re-setup.</p>
</div>
<button class="btn btn-primary" onclick="doSetup()"><i class="ph ph-user-circle-plus"></i> create admin + activate relay</button>
<div id="setup-err" class="err"></div>
</div>
<!-- ── Login view ── -->
<div id="view-login" class="auth-form hidden">
<h2>login</h2>
<div class="field">
<label>username</label>
<input type="text" id="login-username" autocomplete="username">
</div>
<div class="field">
<label>password</label>
<input type="password" id="login-password" autocomplete="current-password">
</div>
<button class="btn btn-primary" onclick="doLogin()"><i class="ph ph-sign-in"></i> unlock</button>
<div id="login-err" class="err"></div>
</div>
<!-- ── Compose view ── -->
<div id="view-compose" class="hidden">
<div class="compose-card">
<h2><i class="ph ph-paper-plane-tilt"></i> compose</h2>
<div class="field" id="compose-recipient-field">
<label>recipient</label>
<select id="compose-recipient"></select>
</div>
<div class="field">
<label>title (optional)</label>
<input type="text" id="compose-title" autocomplete="off">
</div>
<div class="field">
<label>body</label>
<textarea id="compose-body" placeholder="write something..."></textarea>
</div>
<div class="actions">
<button class="btn btn-secondary" onclick="clearCompose()"><i class="ph ph-x"></i> clear</button>
<span class="spacer"></span>
<span id="compose-status" class="status"></span>
<button class="btn btn-primary" onclick="sendCompose()"><i class="ph ph-paper-plane-tilt"></i> send</button>
</div>
</div>
<div id="inbox-section" class="section">
<h2><i class="ph ph-tray"></i> your inbox <span class="count" id="inbox-count">0</span></h2>
<div id="inbox-list"></div>
</div>
</div>
<!-- ── Forced password change (first login) ── -->
<div id="view-change-password" class="auth-form hidden">
<h2>change your password</h2>
<p>This is your first login. Please choose a new password before you can use the relay.</p>
<div class="field">
<label>current password</label>
<input type="password" id="cp-current" placeholder="the one the admin gave you" autocomplete="current-password">
</div>
<div class="field">
<label>new password</label>
<input type="password" id="cp-new" placeholder="min 4 chars" autocomplete="new-password">
</div>
<div class="field">
<label>confirm new password</label>
<input type="password" id="cp-new2" placeholder="repeat" autocomplete="new-password">
</div>
<button class="btn btn-primary" onclick="doChangePassword()"><i class="ph ph-key"></i> change password</button>
<div id="cp-err" class="err"></div>
</div>
<!-- ── Account view ── -->
<div id="view-account" class="hidden">
<div class="section">
<h2><i class="ph ph-user-circle"></i> your client secret</h2>
<p style="font-size:11px;color:var(--muted);line-height:1.5;margin-bottom:14px;">
Your hyprcrd clients use this secret to encrypt messages before sending them to the relay.
All your devices share the same secret. Other tenants never see it. The relay stores it
encrypted with the master key so it can route your messages.
</p>
<div id="account-no-secret" class="hidden notice warn" style="margin-bottom:14px;">
⚠ no secret set yet — your hyprcrd clients can't connect until you generate or set one below
</div>
<div id="account-current" class="hidden" style="margin-bottom:14px;">
<label class="field-label" style="display:block;margin-bottom:4px;">current secret</label>
<div style="display:flex;gap:8px;align-items:stretch;">
<input id="account-secret-display" type="password" readonly class="w-full" style="flex:1;background:var(--bg);border:1px solid var(--border);color:var(--bright);padding:9px 12px;font-size:13px;font-family:'JetBrains Mono',monospace;border-radius:3px;">
<button class="btn btn-secondary" onclick="toggleAccountSecret()" id="account-secret-toggle"><i class="ph ph-eye"></i> show</button>
<button class="btn btn-secondary" onclick="copyAccountSecret()"><i class="ph ph-copy"></i> copy</button>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr;gap:14px;">
<div>
<label class="field-label">set your own secret</label>
<div style="display:flex;gap:8px;">
<input id="account-secret-input" type="text" placeholder="min 8 chars" style="flex:1;background:var(--bg);border:1px solid var(--border);color:var(--bright);padding:9px 12px;font-size:13px;font-family:'JetBrains Mono',monospace;border-radius:3px;">
<button class="btn btn-primary" onclick="saveAccountSecret()"><i class="ph ph-floppy-disk"></i> save</button>
</div>
</div>
<div style="text-align:center;color:var(--dim);font-family:var(--pixel);font-size:9px;">— or —</div>
<button class="btn btn-secondary" onclick="generateAccountSecret()"><i class="ph ph-dice-five"></i> generate random secret</button>
</div>
<div id="account-status" class="status" style="margin-top:12px;"></div>
<div style="margin-top:24px;padding-top:14px;border-top:1px solid var(--border);">
<button class="btn btn-danger" onclick="clearAccountSecret()"><i class="ph ph-x-circle"></i> clear secret (disconnect all clients)</button>
</div>
</div>
</div>
<!-- ── Admin view ── -->
<div id="view-admin" class="hidden">
<div class="section">
<h2><i class="ph ph-users-three"></i> users <span class="count" id="users-count">0</span></h2>
<div id="users-list"></div>
<div class="create-user-form">
<div>
<label for="new-username" class="field-label">username</label>
<input type="text" id="new-username" placeholder="alice" autocomplete="off">
</div>
<div>
<label for="new-password" class="field-label">password</label>
<input type="password" id="new-password" placeholder="min 4 chars" autocomplete="new-password">
</div>
<label class="checkbox">
<input type="checkbox" id="new-is-admin"> grant admin privileges
</label>
<p style="font-size:9px;color:var(--dim);line-height:1.4;margin-top:2px;">The new user sets their own client secret on first login.</p>
<div class="actions">
<button class="btn btn-primary" onclick="createUser()"><i class="ph ph-user-plus"></i> create user</button>
<span id="create-user-status" class="status"></span>
</div>
</div>
</div>
</div>
</main>
<div class="toast" id="toast"></div>
<script src="ui/app.js"></script>
</body>
</html>