tinywebui 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +20 -13
  2. package/bin/tinywebui.js +69 -63
  3. package/package.json +25 -8
  4. package/public/app.js +40 -14
  5. package/public/chat/composer.js +1 -1
  6. package/public/chat/sidebar.js +8 -5
  7. package/public/index.html +88 -49
  8. package/public/panels/admin.js +40 -1
  9. package/public/panels/model-editor.js +336 -0
  10. package/public/panels/panels.js +37 -12
  11. package/public/panels/settings-home.js +125 -0
  12. package/public/panels/settings.js +124 -36
  13. package/public/shared/attachment_note.js +7 -1
  14. package/public/shared/events.js +1 -0
  15. package/public/shared/features.js +1 -0
  16. package/public/shared/origins.js +1 -0
  17. package/public/shared/usage.js +16 -1
  18. package/public/styles.css +83 -20
  19. package/src/access/auth.js +155 -155
  20. package/src/access/auth_gate.js +39 -38
  21. package/src/access/email.js +17 -0
  22. package/src/access/policy.js +123 -138
  23. package/src/audit.js +1 -1
  24. package/src/automations/automation.js +135 -123
  25. package/src/automations/scheduler.js +138 -127
  26. package/src/chat/compact.js +149 -156
  27. package/src/chat/llm.js +380 -382
  28. package/src/chat/runs.js +162 -151
  29. package/src/chat/tool_executor.js +229 -218
  30. package/src/cli.js +535 -433
  31. package/src/config/approval.js +35 -29
  32. package/src/config/config.js +546 -451
  33. package/src/config/data_dir.js +24 -0
  34. package/src/config/models.js +191 -115
  35. package/src/config/refs.js +32 -0
  36. package/src/config/secrets.js +153 -0
  37. package/src/files/documents.js +57 -68
  38. package/src/files/images.js +30 -31
  39. package/src/harness/agent.js +60 -60
  40. package/src/harness/attribution.js +66 -58
  41. package/src/harness/checkpoint.js +67 -62
  42. package/src/harness/history.js +28 -23
  43. package/src/harness/prompt.js +40 -57
  44. package/src/harness/provider.js +284 -283
  45. package/src/harness/types.js +1 -0
  46. package/src/http.js +98 -66
  47. package/src/log.js +11 -12
  48. package/src/mcp.js +398 -374
  49. package/src/retrieval/embedding.js +228 -240
  50. package/src/retrieval/retrieval.js +182 -173
  51. package/src/routes/admin.js +178 -138
  52. package/src/routes/auth.js +184 -100
  53. package/src/routes/automations.js +99 -82
  54. package/src/routes/chats.js +240 -218
  55. package/src/routes/documents.js +130 -120
  56. package/src/routes/library.js +43 -46
  57. package/src/routes/mcp.js +49 -49
  58. package/src/routes/settings.js +127 -122
  59. package/src/routes/types.js +1 -0
  60. package/src/server.js +428 -392
  61. package/src/store/automations.js +106 -106
  62. package/src/store/chats.js +217 -242
  63. package/src/store/documents.js +181 -185
  64. package/src/store/embeddings.js +52 -54
  65. package/src/store/index.js +95 -49
  66. package/src/store/messages.js +55 -80
  67. package/src/store/schema.js +128 -107
  68. package/src/store/scope.js +18 -17
  69. package/src/store/search.js +25 -27
  70. package/src/store/turns.js +53 -53
  71. package/src/store/usage.js +343 -269
  72. package/src/store/users.js +162 -87
  73. package/src/store/wire.js +50 -41
  74. package/src/tools/ask_tool.js +68 -68
  75. package/src/tools/chat_search_tool.js +38 -44
  76. package/src/tools/context_tool.js +131 -138
  77. package/src/tools/document_tool.js +73 -78
  78. package/src/tools/mcp_tool.js +274 -232
  79. package/src/tools/task_tool.js +53 -50
  80. package/src/tools/types.js +1 -0
  81. package/public/panels/connectors.js +0 -184
  82. package/src/set-password.js +0 -43
package/public/index.html CHANGED
@@ -19,7 +19,6 @@
19
19
  <img class="logo" src="favicon.svg" alt="" aria-hidden="true">
20
20
  <span class="brand-name">TinyWebUI</span>
21
21
  </button>
22
- <button id="sideCollapse" class="icon-btn" type="button" aria-label="Hide sidebar" title="Hide sidebar"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 4v16"/></svg></button>
23
22
  </div>
24
23
  <div class="side-scroll">
25
24
  <nav class="nav">
@@ -85,92 +84,121 @@
85
84
  </dialog>
86
85
 
87
86
  <main>
88
- <button id="menu" class="icon-btn" type="button" aria-label="Show sidebar" aria-expanded="false" aria-controls="side"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 4v16"/></svg></button>
87
+ <button id="menu" class="icon-btn" type="button" aria-label="Toggle sidebar" title="Toggle sidebar" aria-expanded="false" aria-controls="side"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 4v16"/></svg></button>
89
88
  <section id="settings">
90
89
  <div class="automation-head">
91
- <h2>Settings</h2>
90
+ <button id="setBack" type="button" class="set-back" hidden aria-label="Back to settings">‹ Back</button>
91
+ <h2 id="setTitle">Settings</h2>
92
92
  <span id="saveMsg" class="usage"></span>
93
93
  <button id="cancel">Close</button>
94
94
  <button class="primary" id="save">Save changes</button>
95
95
  </div>
96
96
  <div class="settings-content">
97
+
98
+ <div class="set-view" data-view="home" data-title="Settings">
99
+ <div id="adminConnection" hidden>
100
+ <div class="set-divider set-first"><h2>Connections &amp; models</h2><p>Each endpoint and the models it serves. Open a model to set its prompt, sampling and caching.</p></div>
101
+ <div id="endpointCards"></div>
102
+ <div class="set-add"><button id="addConnector" type="button">+ Add connector</button></div>
103
+ </div>
104
+ <nav class="set-nav" aria-label="More settings">
105
+ <button type="button" class="set-link" data-open="defaults"><span><b>Defaults for all models</b><small>Prompt, sampling, tool turns and caching every model inherits.</small></span></button>
106
+ <button type="button" class="set-link" data-open="conversation"><span><b>Conversation</b><small>Timezone, questions, tool approval and how long chats are trimmed.</small></span></button>
107
+ <button type="button" class="set-link mine" data-open="prefs" id="myPrefsLink"><span><b>My preferences</b><small>Only for you: your instructions and theme.</small></span></button>
108
+ </nav>
109
+ </div>
110
+
111
+ <div class="set-view" data-view="endpoint" data-title="Default endpoint" hidden>
97
112
  <section class="set-group">
98
- <header><h3>Model</h3><p>Which model answers, and how it samples.</p></header>
113
+ <header><h3>Default endpoint</h3><p>The OpenAI-compatible API used by every model that does not name a connector.</p></header>
99
114
  <div class="set-grid">
100
- <div class="span-2"><label for="model">Model id</label><input id="model" spellcheck="false"><small>Also switchable from the model pill in the message box.</small></div>
101
- <div><label for="temperature">Temperature</label><input id="temperature" type="number" step="0.1" min="0" max="2" placeholder="Provider default"></div>
102
- <div><label for="maxTokens">Max output tokens</label><input id="maxTokens" type="number" min="1" placeholder="No limit"></div>
103
- <div class="span-2"><label for="timezone">Timezone</label><select id="timezone"><option value="">Server's timezone</option></select><small>The zone the model is told today's date in.</small></div>
115
+ <div class="span-2"><label for="baseUrl">API URL</label><input id="baseUrl" spellcheck="false" placeholder="http://localhost:11434/v1"></div>
116
+ <div class="span-2"><label for="apiKey">API key</label><input id="apiKey" type="password" autocomplete="off" spellcheck="false"><small id="apiKeyNote">Optional. Write-only: kept in the secrets file, never shown again. Leave blank to keep the current key.</small></div>
117
+ <div class="span-2"><button id="testDefault" type="button">Test connection</button> <span id="testDefaultMsg" class="usage"></span></div>
104
118
  </div>
105
119
  </section>
120
+ </div>
121
+
122
+ <div class="set-view" data-view="connector" data-title="Connector" hidden><div id="connectorSlot"></div></div>
123
+ <div class="set-view" data-view="model" data-title="Model" hidden><div id="modelSlot"></div></div>
124
+
125
+ <div class="set-view" data-view="defaults" data-title="Defaults for all models" hidden>
126
+ <section class="set-group" id="defaultModelGroup">
127
+ <header><h3>Default model</h3><p>The model new chats start with. Also switchable from the model pill in the message box.</p></header>
128
+ <div id="defaultModelField"><label for="model">Model id</label><input id="model" spellcheck="false"></div>
129
+ </section>
106
130
 
107
131
  <section class="set-group">
108
- <header><h3>Instructions</h3><p>The system prompt sent at the start of every chat.</p></header>
132
+ <header><h3>Prompt</h3><p>The system prompt sent at the start of every chat.</p></header>
109
133
  <textarea id="systemPrompt" rows="10" aria-label="System prompt"></textarea>
110
134
  </section>
111
135
 
112
136
  <section class="set-group">
113
- <header><h3>Tools</h3><p>How far the model can go on its own before handing back.</p></header>
137
+ <header><h3>Sampling</h3></header>
138
+ <div class="set-grid">
139
+ <div><label for="temperature">Temperature</label><input id="temperature" type="number" step="0.1" min="0" max="2" placeholder="Provider default"></div>
140
+ <div><label for="maxTokens">Max output tokens</label><input id="maxTokens" type="number" min="1" placeholder="No limit"></div>
141
+ </div>
142
+ </section>
143
+
144
+ <section class="set-group">
145
+ <header><h3>Tools</h3></header>
114
146
  <div class="set-grid">
115
147
  <div><label for="maxToolRounds">Max tool turns</label><input id="maxToolRounds" type="number" min="1" max="50"><small>Tool calls allowed per reply.</small></div>
116
- <div><label for="askUserTimeoutSeconds">Question timeout (s)</label><input id="askUserTimeoutSeconds" type="number" min="0" max="86400"><small>How long a question from the model waits before it continues on its own. 0: wait until answered.</small></div>
117
- <div><label for="toolApproval">Ask before running</label><select id="toolApproval"><option value="writes">MCP tools that write</option><option value="all">Every MCP tool call</option><option value="off">Never ask</option></select><small>Built-in tools never ask. Scheduled runs refuse calls that would.</small></div>
118
148
  </div>
119
149
  </section>
120
150
 
121
151
  <section class="set-group">
122
- <header><h3>Context &amp; caching</h3><p>Keeps long chats fast and cheap by trimming old tool output.</p></header>
152
+ <header><h3>Caching</h3><p>Keeps the repeated start of a chat cheap to resend.</p></header>
123
153
  <div class="set-grid">
124
- <div><label for="compactThreshold">Compact history above</label><input id="compactThreshold" type="number" min="0" step="1000"><small>Tokens. Old tool results are stubbed past this. 0 turns it off.</small></div>
125
- <div><label for="keepTurns">Always keep last</label><input id="keepTurns" type="number" min="1" max="20"><small>Recent turns whose tool results stay in full.</small></div>
126
- <div><label for="maxInlineChars">Stub single results above</label><input id="maxInlineChars" type="number" min="0" step="1000"><small>Characters. The turn that fetched it still reads it whole.</small></div>
127
- <div><label for="maxTurnChars">Stub even this turn above</label><input id="maxTurnChars" type="number" min="0" step="10000"><small>Characters. A result this big is stubbed right away, even for the turn that fetched it.</small></div>
128
- <div><label for="compactMinSaved">Only compact if it saves</label><input id="compactMinSaved" type="number" min="0" step="1000"><small>Characters. Compacting costs one cache miss, so small savings are skipped.</small></div>
129
- <div><label for="maxHistoryTokens">Hard history limit</label><input id="maxHistoryTokens" type="number" min="0" step="10000"><small>Tokens. Past this, the oldest turns stop being sent (they stay visible). 0 turns it off.</small></div>
130
- <div><label for="cacheTtl">Prompt cache lifetime</label><select id="cacheTtl"><option value="5m">5 minutes</option><option value="1h">1 hour</option></select><small>Anthropic models. 1 hour costs more to write, survives longer pauses.</small></div>
154
+ <div class="admin-only"><label for="cache">Prompt caching</label><select id="cache"><option value="true">On</option><option value="false">Off</option></select></div>
131
155
  <div><label for="cacheMode">Cache style</label><select id="cacheMode"><option value="auto">Automatic</option><option value="implicit">Stable prefix only</option><option value="explicit">Anthropic cache_control</option><option value="rolling">OpenRouter automatic</option><option value="off">Off</option></select><small>Automatic is right for OpenRouter and local models; set it for other gateways.</small></div>
156
+ <div><label for="cacheTtl">Prompt cache lifetime</label><select id="cacheTtl"><option value="5m">5 minutes</option><option value="1h">1 hour</option></select><small>Anthropic models. 1 hour costs more to write, survives longer pauses.</small></div>
132
157
  </div>
133
158
  </section>
134
159
 
135
- <div id="adminSettings" hidden>
136
- <div class="set-divider"><h2>Admin</h2><p id="adminSettingsNote">Deployment-wide: these apply to everyone and are stored in the config file, so copying it reproduces this setup.</p></div>
137
-
138
- <section class="set-group">
139
- <header><h3>Default endpoint</h3><p>The OpenAI-compatible API used by every model that does not name a connector.</p></header>
160
+ <section class="set-group admin-only">
161
+ <header><h3>Advanced</h3><p>Rarely changed.</p></header>
140
162
  <div class="set-grid">
141
- <div class="span-2"><label for="baseUrl">API URL</label><input id="baseUrl" spellcheck="false" placeholder="https://openrouter.ai/api/v1"></div>
142
- <div class="span-2"><label for="apiKey">API key</label><input id="apiKey" type="password" autocomplete="off" spellcheck="false"><small id="apiKeyNote">Write-only: stored in the config file, never shown again. Leave blank to keep the current key.</small></div>
143
- <div class="span-2"><button id="testDefault" type="button">Test connection</button> <span id="testDefaultMsg" class="usage"></span></div>
163
+ <div><label for="reasoningReplay">Replay past reasoning</label><select id="reasoningReplay"><option value="auto">Automatic</option><option value="omit">Never</option><option value="reasoning_content">reasoning_content</option><option value="reasoning_details">reasoning_details</option></select></div>
164
+ <div class="span-2"><label for="extraBody">Extra request body (JSON)</label><textarea id="extraBody" rows="4" spellcheck="false" placeholder="{}"></textarea><small>Merged into every completion request, e.g. gateway routing.</small></div>
144
165
  </div>
145
166
  </section>
167
+ </div>
146
168
 
169
+ <div class="set-view" data-view="conversation" data-title="Conversation" hidden>
147
170
  <section class="set-group">
148
- <header><h3>Connectors</h3><p>More endpoints, each with its own URL and key. A model picks one by name.</p></header>
149
- <div id="connectorsBox"></div>
150
- <button id="addConnector" type="button">Add connector</button>
171
+ <header><h3>Date</h3></header>
172
+ <div class="set-grid">
173
+ <div class="span-2"><label for="timezone">Timezone</label><select id="timezone"><option value="">Server's timezone</option></select><small>The zone the model is told today's date in.</small></div>
174
+ </div>
151
175
  </section>
152
176
 
153
177
  <section class="set-group">
154
- <header><h3>Models</h3><p>The catalog. Once any model is listed, only listed models can be picked, and people see the label, never the provider id.</p></header>
155
- <div id="modelsBox"></div>
156
- <button id="addModel" type="button">Add model</button>
178
+ <header><h3>Questions &amp; approval</h3><p>When the model waits for you.</p></header>
179
+ <div class="set-grid">
180
+ <div><label for="askUserTimeoutSeconds">Question timeout (s)</label><input id="askUserTimeoutSeconds" type="number" min="0" max="86400"><small>How long a question from the model waits before it continues on its own. 0: wait until answered.</small></div>
181
+ <div><label for="toolApproval">Ask before running</label><select id="toolApproval"><option value="writes">MCP tools that write</option><option value="all">Every MCP tool call</option><option value="off">Never ask</option></select><small>Built-in tools never ask. Scheduled runs refuse calls that would.</small></div>
182
+ </div>
157
183
  </section>
158
184
 
159
185
  <section class="set-group">
160
- <header><h3>Advanced model behaviour</h3><p>Rarely changed. Per-model values in the catalog override these.</p></header>
186
+ <header><h3>Context &amp; compaction</h3><p>Keeps long chats fast and cheap by trimming old tool output.</p></header>
161
187
  <div class="set-grid">
162
- <div><label for="llmCompaction">Summarise dropped turns</label><select id="llmCompaction"><option value="true">Yes, with the chat's model</option><option value="false">No, just drop them</option></select></div>
163
- <div><label for="compactionMaxTokens">Summary length cap</label><input id="compactionMaxTokens" type="number" min="256" step="256"><small>Output tokens for that summary request.</small></div>
164
- <div><label for="expandCharBudget">Expand budget</label><input id="expandCharBudget" type="number" min="0" step="1000"><small>Characters one expand_context call may return.</small></div>
165
- <div><label for="cache">Prompt caching</label><select id="cache"><option value="true">On</option><option value="false">Off</option></select></div>
166
- <div><label for="reasoningReplay">Replay past reasoning</label><select id="reasoningReplay"><option value="auto">Automatic</option><option value="omit">Never</option><option value="reasoning_content">reasoning_content</option><option value="reasoning_details">reasoning_details</option></select></div>
167
- <div class="span-2"><label for="extraBody">Extra request body (JSON)</label><textarea id="extraBody" rows="4" spellcheck="false" placeholder="{}"></textarea><small>Merged into every completion request, e.g. gateway routing.</small></div>
188
+ <div><label for="compactThreshold">Compact history above</label><input id="compactThreshold" type="number" min="0" step="1000"><small>Tokens. Old tool results are stubbed past this. 0 turns it off.</small></div>
189
+ <div><label for="keepTurns">Always keep last</label><input id="keepTurns" type="number" min="1" max="20"><small>Recent turns whose tool results stay in full.</small></div>
190
+ <div><label for="maxInlineChars">Stub single results above</label><input id="maxInlineChars" type="number" min="0" step="1000"><small>Characters. The turn that fetched it still reads it whole.</small></div>
191
+ <div><label for="maxTurnChars">Stub even this turn above</label><input id="maxTurnChars" type="number" min="0" step="10000"><small>Characters. A result this big is stubbed right away, even for the turn that fetched it.</small></div>
192
+ <div><label for="compactMinSaved">Only compact if it saves</label><input id="compactMinSaved" type="number" min="0" step="1000"><small>Characters. Compacting costs one cache miss, so small savings are skipped.</small></div>
193
+ <div><label for="maxHistoryTokens">Hard history limit</label><input id="maxHistoryTokens" type="number" min="0" step="10000"><small>Tokens. Past this, the oldest turns stop being sent (they stay visible). 0 turns it off.</small></div>
194
+ <div class="admin-only"><label for="llmCompaction">Summarise dropped turns</label><select id="llmCompaction"><option value="true">Yes, with the chat's model</option><option value="false">No, just drop them</option></select></div>
195
+ <div class="admin-only"><label for="compactionMaxTokens">Summary length cap</label><input id="compactionMaxTokens" type="number" min="256" step="256"><small>Output tokens for that summary request.</small></div>
196
+ <div class="admin-only"><label for="expandCharBudget">Expand budget</label><input id="expandCharBudget" type="number" min="0" step="1000"><small>Characters one expand_context call may return.</small></div>
168
197
  </div>
169
198
  </section>
170
199
  </div>
171
200
 
172
- <div class="set-divider mine" id="myPrefsHead"><h2>My preferences</h2><p>Only for you.</p></div>
173
-
201
+ <div class="set-view" data-view="prefs" data-title="My preferences" hidden>
174
202
  <section class="set-group mine" id="myInstructionsGroup" hidden>
175
203
  <header><h3>My instructions</h3><p>Added after the system prompt on your chats only.</p></header>
176
204
  <textarea id="myInstructions" rows="5" maxlength="4000" aria-label="My instructions"></textarea>
@@ -183,10 +211,12 @@
183
211
  </div>
184
212
  </section>
185
213
  </div>
214
+
215
+ </div>
186
216
  </section>
187
217
 
188
218
  <section id="mcp" aria-label="MCP servers">
189
- <div class="automation-head"><h2>MCP servers</h2><button id="close-mcp">close</button></div>
219
+ <div class="automation-head"><h2>MCP servers</h2></div>
190
220
  <div class="settings-content">
191
221
  <div class="sec">
192
222
  <div class="sec-h">config file <span id="mcpPath" class="usage"></span></div>
@@ -204,7 +234,7 @@
204
234
  </section>
205
235
 
206
236
  <section id="statistics" aria-label="Statistics">
207
- <div class="automation-head"><h2>Statistics</h2><span id="statisticsFilter"></span><button id="close-statistics">close</button></div>
237
+ <div class="automation-head"><h2>Statistics</h2><span id="statisticsFilter"></span></div>
208
238
  <div class="statistics-content">
209
239
  <section class="statistics-block">
210
240
  <div class="sec-h">cost and averages <span class="usage">provider-reported costs only</span></div>
@@ -232,20 +262,29 @@
232
262
 
233
263
  <div id="login" hidden>
234
264
  <form id="loginForm" class="login-card">
235
- <h2>TinyWebUI</h2>
265
+ <h2 id="loginTitle">TinyWebUI</h2>
266
+ <div id="loginHelp" class="usage"></div>
267
+ <label id="loginEmailLabel" for="loginEmail" hidden>Email</label>
268
+ <input id="loginEmail" type="email" autocomplete="email" hidden>
236
269
  <label for="loginPassword">Password</label>
237
270
  <input id="loginPassword" type="password" autocomplete="current-password" required>
238
- <button class="primary" type="submit">Sign in</button>
271
+ <button id="loginSubmit" class="primary" type="submit">Sign in</button>
239
272
  <div id="loginMsg" class="usage" role="alert"></div>
240
273
  </form>
241
274
  </div>
242
275
 
243
276
  <section id="admin" aria-label="Admin">
244
- <div class="automation-head"><h2>Admin</h2><button id="close-admin">close</button></div>
277
+ <div class="automation-head"><h2>Admin</h2></div>
245
278
  <div class="statistics-content">
246
279
  <section class="statistics-block">
247
280
  <div class="sec-h">users <span id="adminCount" class="usage"></span></div>
248
281
  <div id="adminMsg" class="usage"></div>
282
+ <form id="adminInvite" class="invite-form" hidden>
283
+ <label for="inviteEmail">Invite by email <span class="usage">optional</span></label>
284
+ <div><input id="inviteEmail" type="email" autocomplete="off" placeholder="person@example.com"><button class="primary" type="submit">Create invite</button></div>
285
+ <div id="inviteResult" class="usage"></div>
286
+ <div id="adminInvites" class="admin-users"></div>
287
+ </form>
249
288
  <div id="adminUsers" class="admin-users"></div>
250
289
  </section>
251
290
  <section id="adminPolicy" class="statistics-block">
@@ -266,7 +305,7 @@
266
305
  </section>
267
306
 
268
307
  <section id="automations" aria-label="Automations">
269
- <div class="automation-head"><h2>Automations</h2><button id="new-automation" class="primary" type="button">New automation</button><button id="close-automations">close</button></div>
308
+ <div class="automation-head"><h2>Automations</h2><button id="new-automation" class="primary" type="button">New automation</button></div>
270
309
  <div class="automation-content">
271
310
  <div id="automation-create-slot"></div>
272
311
  <div id="automation-list" class="automation-list"></div>
@@ -16,7 +16,7 @@ let adminViewRequest = 0;
16
16
  /** Wires Log out for whoever is signed in (nav visibility is core/access.js's). */
17
17
  export function initAdmin() {
18
18
  me = whoami();
19
- if (me.logoutUrl || me.authMode === 'single') {
19
+ if (me.logoutUrl || me.authMode === 'multiuser') {
20
20
  const btn = $('logout');
21
21
  btn.hidden = false;
22
22
  btn.onclick = async () => {
@@ -25,10 +25,48 @@ export function initAdmin() {
25
25
  if (me.logoutUrl) location.href = me.logoutUrl; else location.reload();
26
26
  };
27
27
  }
28
+ if (me.authMode === 'multiuser') {
29
+ const form = $('adminInvite');
30
+ form.hidden = false;
31
+ form.onsubmit = async (event) => {
32
+ event.preventDefault();
33
+ const result = $('inviteResult');
34
+ result.textContent = '';
35
+ try {
36
+ const out = await api.post('/api/admin/invites', { email: $('inviteEmail').value.trim() || null });
37
+ const url = new URL(out.path, location.origin).href;
38
+ const link = Object.assign(el('a'), { href: url, textContent: url });
39
+ result.replaceChildren('Share this link once: ', link);
40
+ $('inviteEmail').value = '';
41
+ await loadInvites();
42
+ } catch (err) { result.textContent = err.message; }
43
+ };
44
+ }
28
45
  }
29
46
 
30
47
  const fmt = (ts) => (ts ? new Date(ts).toLocaleString() : 'never');
31
48
 
49
+ async function loadInvites() {
50
+ if (me?.authMode !== 'multiuser') return;
51
+ const box = $('adminInvites');
52
+ let out;
53
+ try { out = await api.get('/api/admin/invites'); }
54
+ catch (err) { box.textContent = err.message; return; }
55
+ box.replaceChildren(...out.invites.map((invite) => {
56
+ const line = el('div', 'admin-row');
57
+ const label = el('div', 'admin-who');
58
+ label.appendChild(Object.assign(el('strong'), { textContent: invite.email || 'Anyone with the link' }));
59
+ label.appendChild(Object.assign(el('span', 'usage'), { textContent: `expires ${fmt(invite.expiresAt)}` }));
60
+ const revoke = Object.assign(el('button'), { type: 'button', textContent: 'revoke' });
61
+ revoke.onclick = async () => {
62
+ try { await api.del(`/api/admin/invites/${encodeURIComponent(invite.id)}`); await loadInvites(); }
63
+ catch (err) { $('inviteResult').textContent = err.message; }
64
+ };
65
+ line.append(label, revoke);
66
+ return line;
67
+ }));
68
+ }
69
+
32
70
  function select(value, options, label, onChange) {
33
71
  const sel = el('select');
34
72
  sel.setAttribute('aria-label', label);
@@ -220,6 +258,7 @@ export async function loadAdmin() {
220
258
  $('adminUsers').parentElement.hidden = false;
221
259
  $('adminPolicy').hidden = false;
222
260
  loadPolicy();
261
+ loadInvites();
223
262
  const box = $('adminUsers');
224
263
  $('adminMsg').textContent = '';
225
264
  let out;