| 1 | <meta charset="utf-8" /> |
| 2 | <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| 3 | <title>__GROUP_NAME__ · admin</title> |
| 4 | <link rel="stylesheet" href="/static/style.css" /> |
| 5 | |
| 6 | <!-- Apply the saved theme before first paint (see web/static/ui.js — the canonical |
| 7 | list; this copy must stay in sync). A local preference, never sent to the server. --> |
| 8 | <script nonce="__CSP_NONCE__"> |
| 9 | (function () { |
| 10 | try { |
| 11 | var THEMES = ["litroom", "lagoon", "sunny", "plum", "linen", "classic"]; |
| 12 | var t = localStorage.getItem("lchat_theme"); |
| 13 | if (THEMES.indexOf(t) < 0) t = "classic"; |
| 14 | var a = localStorage.getItem("lchat_appearance"); |
| 15 | var root = document.documentElement; |
| 16 | root.dataset.theme = t; |
| 17 | root.style.colorScheme = a === "light" ? "light" : a === "dark" ? "dark" : "light dark"; |
| 18 | } catch (e) {} |
| 19 | })(); |
| 20 | </script> |
| 21 | |
| 22 | <div class="admin" id="admin"> |
| 23 | <h1>__GROUP_NAME__ admin</h1> |
| 24 | <p class="sub">Create secret invite links. Each one lets someone pick a screen name and join the chat.</p> |
| 25 | |
| 26 | <div id="locked" class="card hidden"> |
| 27 | <h1>Admin link required</h1> |
| 28 | <p>Open the admin link that was printed when you started the server. It contains your admin token.</p> |
| 29 | </div> |
| 30 | |
| 31 | <div id="console" class="hidden"> |
| 32 | <section> |
| 33 | <h2>Your admin link</h2> |
| 34 | <p class="sub" style="margin:0 0 8px"> |
| 35 | Opens this admin console on any device. It carries your admin token, which |
| 36 | authorises managing the roster — it is <strong>not</strong> a decryption key, |
| 37 | so it can't read any messages. Save it somewhere safe (e.g. a password manager). |
| 38 | </p> |
| 39 | <div class="key-box mono" id="masterLink"></div> |
| 40 | <div class="row"><button class="secondary" id="copyMaster">Copy admin link</button></div> |
| 41 | </section> |
| 42 | |
| 43 | <section> |
| 44 | <h2>This space</h2> |
| 45 | <p class="sub" style="margin:0 0 8px">The name is shown as the app name on a Home Screen and as the chat's title. The description greets people on their invite's welcome card and lives under ⋮ → About this space. The administrator name & contact email are optional — they're shown to members (e.g. by Eleven's welcome tour) so people know who runs the space and how to reach you.</p> |
| 46 | <form id="nameForm" autocomplete="off"> |
| 47 | <label class="field-label" for="groupName">Group name</label> |
| 48 | <input id="groupName" placeholder="e.g. Family Chat" maxlength="60" style="display:block;width:100%;margin:0 0 8px" /> |
| 49 | <label class="field-label" for="groupDesc">Description (optional)</label> |
| 50 | <textarea id="groupDesc" placeholder="What is this space? Members and the people they invite can read this." maxlength="500" rows="3" style="display:block;width:100%;margin:0 0 8px"></textarea> |
| 51 | <label class="field-label" for="adminName">Administrator (optional)</label> |
| 52 | <input id="adminName" placeholder="e.g. Paul Campbell" maxlength="60" style="display:block;width:100%;margin:0 0 8px" /> |
| 53 | <label class="field-label" for="adminEmail">Contact email (optional)</label> |
| 54 | <input id="adminEmail" type="email" placeholder="e.g. paul@example.com" maxlength="120" style="display:block;width:100%;margin:0 0 8px" /> |
| 55 | <button type="submit">Save</button> |
| 56 | </form> |
| 57 | </section> |
| 58 | |
| 59 | <section> |
| 60 | <h2>Server photo</h2> |
| 61 | <p class="sub" style="margin:0 0 8px"> |
| 62 | An optional photo for this chat, shown when people are invited, while they |
| 63 | join, and on the passkey-backup screen. Like the name, it's public |
| 64 | branding — it isn't encrypted, so don't use anything private. |
| 65 | </p> |
| 66 | <div class="row" style="align-items:center;gap:12px;margin:0 0 8px"> |
| 67 | <img id="serverAvatarPreview" alt="" class="hidden" |
| 68 | style="width:56px;height:56px;border-radius:14px;object-fit:cover" /> |
| 69 | <button type="button" id="serverAvatarPick">Upload photo</button> |
| 70 | <button type="button" class="secondary hidden" id="serverAvatarRemove">Remove</button> |
| 71 | </div> |
| 72 | <input type="file" id="serverAvatarFile" accept="image/png,image/jpeg,image/webp" style="display:none" /> |
| 73 | <p id="serverAvatarStatus" class="sub" style="margin:0" aria-live="polite"></p> |
| 74 | </section> |
| 75 | |
| 76 | <section> |
| 77 | <h2>Who can invite</h2> |
| 78 | <p class="sub" style="margin:0 0 8px"> |
| 79 | By default only you (the admin) can invite people. Turn this on to let members |
| 80 | invite each other — each member can then start a chat with someone new and |
| 81 | share a join link themselves. The server never sees any key either way. |
| 82 | </p> |
| 83 | <label class="toggle-row"> |
| 84 | <input type="checkbox" id="userInvites" /> |
| 85 | <span>Let members invite other people</span> |
| 86 | </label> |
| 87 | </section> |
| 88 | |
| 89 | <section> |
| 90 | <h2>Features</h2> |
| 91 | <p class="sub" style="margin:0 0 8px"> |
| 92 | Switch off the parts of the app your space doesn't want. Existing chats |
| 93 | keep working either way — turning something off just stops new use: AI off |
| 94 | means helpers answer commands only, bots off stops new bots being created, |
| 95 | apps off hides them from the New-chat menu, member-made apps off keeps the |
| 96 | built-in apps but stops members building their own. |
| 97 | </p> |
| 98 | <label class="toggle-row" style="margin:0 0 8px"> |
| 99 | <input type="checkbox" id="aiEnabled" /> |
| 100 | <span>Enable AI</span> |
| 101 | </label> |
| 102 | <label class="toggle-row" style="margin:0 0 8px"> |
| 103 | <input type="checkbox" id="botsEnabled" /> |
| 104 | <span>Enable bots</span> |
| 105 | </label> |
| 106 | <label class="toggle-row" style="margin:0 0 8px"> |
| 107 | <input type="checkbox" id="appsEnabled" /> |
| 108 | <span>Enable apps</span> |
| 109 | </label> |
| 110 | <label class="toggle-row"> |
| 111 | <input type="checkbox" id="memberAppsEnabled" /> |
| 112 | <span>Let members make their own apps</span> |
| 113 | </label> |
| 114 | </section> |
| 115 | |
| 116 | <section> |
| 117 | <h2>Updates</h2> |
| 118 | <p class="sub" style="margin:0 0 8px"> |
| 119 | The server can keep itself up to date. Stable is well tested and what most |
| 120 | spaces should run; Beta gets features a little earlier; Edge is the bleeding |
| 121 | edge. Updates install in seconds and never touch your data. |
| 122 | </p> |
| 123 | <p id="updateStatus" class="sub" style="margin:0 0 8px"></p> |
| 124 | <label class="toggle-row" style="margin:0 0 8px"> |
| 125 | <input type="checkbox" id="updateAuto" /> |
| 126 | <span>Install updates automatically</span> |
| 127 | </label> |
| 128 | <label class="toggle-row" style="margin:0 0 8px"> |
| 129 | <input type="checkbox" id="updateDownload" /> |
| 130 | <span>Download new versions automatically</span> |
| 131 | </label> |
| 132 | <label class="field-label" for="updateChannel">Release channel</label> |
| 133 | <select id="updateChannel" style="display:block;width:100%;margin:0 0 8px"> |
| 134 | <option value="stable">Stable — well tested (recommended)</option> |
| 135 | <option value="beta">Beta — near-stable, newer features</option> |
| 136 | <option value="edge">Edge — the bleeding edge</option> |
| 137 | </select> |
| 138 | <label class="field-label" for="updateMajor">Stay on major version</label> |
| 139 | <input id="updateMajor" inputmode="numeric" placeholder="any" style="display:block;width:100%;margin:0 0 4px" /> |
| 140 | <p class="sub" style="margin:0 0 8px"> |
| 141 | Leave empty to follow every release. With a number set, small updates keep |
| 142 | arriving but a release crossing that major version waits for your say-so. |
| 143 | </p> |
| 144 | <p id="updateFeedback" class="sub" style="margin:0" aria-live="polite"></p> |
| 145 | </section> |
| 146 | |
| 147 | <section id="agentSection"> |
| 148 | <h2>Add an agent</h2> |
| 149 | <p class="sub" style="margin:0 0 8px">Adds an AI bot as a member. This mints a one-time link the agent sidecar uses to set up the bot's identity; it then appears in chats as a member with a BOT badge. The sidecar for this instance must be running.</p> |
| 150 | <form id="agentForm" autocomplete="off"> |
| 151 | <input id="agentLabel" placeholder="Bot name (e.g. Claude)" maxlength="60" style="display:block;width:100%;margin:0 0 8px" /> |
| 152 | <input id="agentDescription" placeholder="What this helper does, in one line — members see this in the Helpers list" maxlength="200" style="display:block;width:100%;margin:0 0 8px" /> |
| 153 | <select id="agentModel" style="display:block;width:100%;margin:0 0 8px"> |
| 154 | <option value="">Default model (Sonnet)</option> |
| 155 | <option value="claude-opus-4-8">Opus — most capable</option> |
| 156 | <option value="claude-sonnet-5">Sonnet — balanced</option> |
| 157 | <option value="claude-haiku-4-5">Haiku — fast & cheap</option> |
| 158 | </select> |
| 159 | <textarea id="agentPrompt" rows="4" placeholder="Personality / instructions (system prompt). Leave blank for a friendly default. A safety + privacy preamble is always applied on top." style="display:block;width:100%;margin:0 0 8px"></textarea> |
| 160 | <textarea id="agentDocs" rows="3" placeholder="Reference the bot should always know — house rules, an FAQ, etc. Optional." style="display:block;width:100%;margin:0 0 8px"></textarea> |
| 161 | <select id="agentListen" style="display:block;width:100%;margin:0 0 8px"> |
| 162 | <option value="">Replies in groups only when @mentioned (default)</option> |
| 163 | <option value="name">Also replies when spoken to by name (“Claude, …”)</option> |
| 164 | </select> |
| 165 | <label class="toggle-row" style="margin:0 0 8px"> |
| 166 | <input type="checkbox" id="agentAvailable" checked /> |
| 167 | <span>Show in everyone's Helpers list (untick to add it to chats yourself)</span> |
| 168 | </label> |
| 169 | <button type="submit">Add agent</button> |
| 170 | </form> |
| 171 | </section> |
| 172 | |
| 173 | <section> |
| 174 | <h2>Apps</h2> |
| 175 | <p class="sub" style="margin:0 0 8px"> |
| 176 | Install custom mini-apps for this space — each is a single JavaScript |
| 177 | module file, just like the built-in To-do or Polls. Members' open apps |
| 178 | refresh automatically when you install or remove one. |
| 179 | </p> |
| 180 | <p class="sub" style="margin:0 0 8px"> |
| 181 | <strong>Install only code you trust.</strong> An installed app runs its |
| 182 | code on every member's device, with access to everything that device can |
| 183 | decrypt — messages, names, and photos included. |
| 184 | </p> |
| 185 | <div id="appsList" style="margin:0 0 10px"><p class="sub">Loading…</p></div> |
| 186 | <div class="row" style="margin:0 0 8px"> |
| 187 | <button type="button" id="appPick">Install from file…</button> |
| 188 | </div> |
| 189 | <input type="file" id="appFile" accept=".js,text/javascript" style="display:none" /> |
| 190 | <label class="field-label" for="appName">Or paste the source</label> |
| 191 | <input id="appName" placeholder="App name (lowercase letters, digits, dashes)" maxlength="60" style="display:block;width:100%;margin:0 0 8px" /> |
| 192 | <textarea id="appSource" rows="6" placeholder="// paste the app module's JavaScript here" class="mono" style="display:block;width:100%;margin:0 0 8px"></textarea> |
| 193 | <button type="button" id="appInstall">Install</button> |
| 194 | <p id="appsStatus" class="sub" style="margin:8px 0 0" aria-live="polite"></p> |
| 195 | </section> |
| 196 | |
| 197 | <section> |
| 198 | <h2>Notifications</h2> |
| 199 | <p class="sub" style="margin:0 0 8px">Send a test notification to everyone who has them enabled — a quick way to check delivery.</p> |
| 200 | <div class="row"><button class="secondary" id="testPush">Send test notification</button></div> |
| 201 | </section> |
| 202 | |
| 203 | <section> |
| 204 | <h2>Shared key</h2> |
| 205 | <p class="sub" style="margin:0 0 8px">Every member's name and photo is locked with one shared key, passed member to member. If everyone who held it is gone for good, newer members stay locked out — invites included. Resetting mints a fresh key the next time anyone opens the app.</p> |
| 206 | <p class="sub" style="margin:0 0 8px"><strong>Resetting is permanent:</strong> profile photos, "about me" notes and member-built apps locked with the old key can't be recovered — people re-add them. Names come back on their own the next time each member opens the app. Open groups close to invite-only (reopen them afterwards). Messages are untouched — chats have their own keys.</p> |
| 207 | <div class="row"><button class="secondary" id="resetInstanceKey">Reset the shared key…</button></div> |
| 208 | <p id="resetKeyStatus" class="sub" style="margin:8px 0 0" aria-live="polite"></p> |
| 209 | </section> |
| 210 | |
| 211 | <section> |
| 212 | <h2>Delegate admin role</h2> |
| 213 | <p class="sub" style="margin:0 0 10px">Create an invite that makes the claimer an admin. They can then access this console with their device token (no need to share your admin link). Use this to share admin duties or to recover access if you lose your token.</p> |
| 214 | <form id="createAdminForm" class="inline-form" autocomplete="off"> |
| 215 | <input id="adminLabel" placeholder="Their name (e.g. Sophie)" maxlength="60" /> |
| 216 | <button type="submit">Create admin invite</button> |
| 217 | </form> |
| 218 | </section> |
| 219 | |
| 220 | <section> |
| 221 | <h2>People</h2> |
| 222 | <div id="bootstrapInvite"> |
| 223 | <p class="sub" style="margin:0 0 10px">Invite the first person with a short, single-use link (and QR) — they pick a screen name and their device makes its own keys, so the server never sees any key. After someone joins, people are added from inside the chat.</p> |
| 224 | <form id="createForm" class="inline-form" autocomplete="off"> |
| 225 | <input id="label" placeholder="Their name (e.g. Sophie)" maxlength="60" /> |
| 226 | <!-- Cohort invites: hidden until the space has access levels. --> |
| 227 | <select id="inviteTier" class="hidden" aria-label="Access level for this invite"></select> |
| 228 | <button type="submit">Create invite</button> |
| 229 | </form> |
| 230 | </div> |
| 231 | <p id="inviteGuidance" class="sub hidden" style="margin:0 0 10px">People are added from inside the chat now — turn on "Let members invite other people" above, then use "Invite someone" when starting a new chat. That way the new person is connected the moment they join.</p> |
| 232 | <div id="people" style="margin-top:16px"><p class="sub">Loading…</p></div> |
| 233 | </section> |
| 234 | </div> |
| 235 | </div> |
| 236 | <div id="toast" class="toast"></div> |
| 237 | |
| 238 | <script type="module" src="/static/admin.js"></script> |
| 239 | |