Compose: attach files (encrypted, hex-named, linked via relay_uid), CC-self checkbox tags copy with recipient name. Outbox section shows sent messages with delete. Chat view: session-based threads/history/ send endpoints, split layout with sidebar, read receipts, 5s poll. Mobile: threads as horizontal scroll bar above messages. Fix chat input pushed offscreen by adding min-height:0 on flex container. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
247 lines
13 KiB
PHP
247 lines
13 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>
|
|
<i class="ph ph-broadcast brand-icon"></i>
|
|
<h1>hyprcrd relay</h1>
|
|
<button id="header-status" class="header-status pixel" onclick="jumpToInbox()" title="jump to inbox"></button>
|
|
<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-chat" class="nav-btn" onclick="showView('chat')"><i class="ph ph-chat-circle-dots"></i> chat</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">
|
|
<i class="ph ph-broadcast auth-icon"></i>
|
|
<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 class="field-hint">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">
|
|
<i class="ph ph-key auth-icon"></i>
|
|
<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>
|
|
<div id="compose-recipient-wrap" style="position:relative;">
|
|
<button id="compose-recipient-btn" type="button" class="input" style="text-align:left;cursor:pointer;display:flex;align-items:center;justify-content:space-between;">
|
|
<span id="compose-recipient-label">select…</span>
|
|
<i class="ph ph-caret-down" style="font-size:10px;color:var(--dim);"></i>
|
|
</button>
|
|
<input type="hidden" id="compose-recipient" value="">
|
|
<div id="compose-recipient-dropdown" class="hidden" style="position:absolute;top:100%;left:0;right:0;margin-top:4px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:0 8px 24px rgba(0,0,0,0.4);z-index:50;max-height:200px;overflow-y:auto;padding:4px 0;"></div>
|
|
</div>
|
|
</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 id="compose-attachments" class="compose-attachments"></div>
|
|
<label class="checkbox" id="compose-cc-self-wrap" style="margin-top:4px;">
|
|
<input type="checkbox" id="compose-cc-self"> keep a copy (tagged with recipient name)
|
|
</label>
|
|
<div class="actions">
|
|
<button class="btn btn-secondary" onclick="clearCompose()"><i class="ph ph-x"></i> clear</button>
|
|
<button id="compose-attach-btn" class="btn btn-secondary"><i class="ph ph-paperclip"></i> attach</button>
|
|
<input type="file" id="compose-file" multiple class="hidden">
|
|
<span class="spacer"></span>
|
|
<button class="btn btn-primary" onclick="sendCompose()"><i class="ph ph-paper-plane-tilt"></i> send</button>
|
|
</div>
|
|
<div id="compose-status" class="compose-status-bar"></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>
|
|
<span style="flex:1"></span>
|
|
<button id="inbox-delete-all" class="btn btn-danger hidden" style="padding:3px 10px;font-size:9px;" onclick="deleteAllMessages()"><i class="ph ph-trash"></i> delete all</button>
|
|
</h2>
|
|
<div id="inbox-list"></div>
|
|
</div>
|
|
<div id="outbox-section" class="section">
|
|
<h2><i class="ph ph-paper-plane-tilt"></i> sent <span class="count" id="outbox-count">0</span></h2>
|
|
<div id="outbox-list"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Forced password change (first login) ── -->
|
|
<div id="view-change-password" class="auth-form hidden">
|
|
<i class="ph ph-key auth-icon"></i>
|
|
<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 class="field-hint" style="line-height:1.6;font-size:11px;color:var(--muted);">
|
|
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">
|
|
<i class="ph ph-warning"></i>
|
|
<span>no secret set yet — your hyprcrd clients can't connect until you generate or set one below</span>
|
|
</div>
|
|
<div id="account-current" class="hidden account-secret-block">
|
|
<label class="field-label">current secret</label>
|
|
<div class="account-secret-row">
|
|
<input id="account-secret-display" type="password" readonly class="input">
|
|
<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 class="account-secret-grid">
|
|
<div class="field">
|
|
<label>set your own secret</label>
|
|
<div class="account-secret-row">
|
|
<input id="account-secret-input" type="text" placeholder="min 8 chars" class="input">
|
|
<button class="btn btn-primary" onclick="saveAccountSecret()"><i class="ph ph-floppy-disk"></i> save</button>
|
|
</div>
|
|
</div>
|
|
<div class="account-divider">— 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 class="account-secret-actions">
|
|
<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 class="field">
|
|
<label for="new-username">username</label>
|
|
<input type="text" id="new-username" placeholder="alice" autocomplete="off">
|
|
</div>
|
|
<div class="field">
|
|
<label for="new-password">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 class="field-hint">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>
|
|
|
|
<!-- ── Chat view ── -->
|
|
<div id="view-chat" class="hidden">
|
|
<div class="chat-layout">
|
|
<div class="chat-sidebar">
|
|
<div class="chat-sidebar-header">
|
|
<span class="pixel" style="font-size:10px;color:var(--accent);">conversations</span>
|
|
</div>
|
|
<div id="chat-thread-list"></div>
|
|
</div>
|
|
<div class="chat-main">
|
|
<div id="chat-header" class="chat-header">
|
|
<span id="chat-peer-name" class="pixel" style="font-size:11px;color:var(--bright);">select a conversation</span>
|
|
</div>
|
|
<div id="chat-messages" class="chat-messages"></div>
|
|
<div id="chat-input-bar" class="chat-input-bar hidden">
|
|
<input type="text" id="chat-input" placeholder="type a message..." autocomplete="off">
|
|
<button id="chat-send-btn" class="btn btn-primary" style="padding:6px 14px;"><i class="ph ph-paper-plane-tilt"></i></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<!-- Confirm dialog (replaces native confirm()) -->
|
|
<div id="relay-confirm" class="hidden" style="position:fixed;inset:0;z-index:300;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;padding:24px;">
|
|
<div style="background:var(--surface);border:1px solid var(--border);border-radius:6px;box-shadow:0 16px 48px rgba(0,0,0,0.5);padding:24px;max-width:380px;width:100%;text-align:center;">
|
|
<p id="relay-confirm-msg" style="font-size:12px;color:var(--text);line-height:1.5;margin-bottom:18px;"></p>
|
|
<div style="display:flex;gap:8px;justify-content:center;">
|
|
<button id="relay-confirm-no" class="btn btn-secondary">Cancel</button>
|
|
<button id="relay-confirm-yes" class="btn btn-danger">Confirm</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="toast-stack"></div>
|
|
|
|
<script src="ui/app.js"></script>
|
|
</body>
|
|
</html>
|