tinywebui 0.1.4 → 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.
- package/README.md +20 -13
- package/bin/tinywebui.js +69 -53
- package/package.json +25 -8
- package/public/app.js +40 -34
- package/public/chat/composer.js +1 -1
- package/public/chat/sidebar.js +8 -5
- package/public/index.html +88 -53
- package/public/panels/admin.js +40 -1
- package/public/panels/model-editor.js +336 -0
- package/public/panels/panels.js +37 -12
- package/public/panels/settings-home.js +125 -0
- package/public/panels/settings.js +124 -36
- package/public/shared/attachment_note.js +7 -1
- package/public/shared/events.js +1 -0
- package/public/shared/features.js +1 -0
- package/public/shared/origins.js +1 -0
- package/public/shared/usage.js +16 -1
- package/public/styles.css +84 -21
- package/src/access/auth.js +155 -155
- package/src/access/auth_gate.js +39 -38
- package/src/access/email.js +17 -0
- package/src/access/policy.js +123 -136
- package/src/audit.js +1 -1
- package/src/automations/automation.js +135 -123
- package/src/automations/scheduler.js +138 -127
- package/src/chat/compact.js +149 -156
- package/src/chat/llm.js +380 -382
- package/src/chat/runs.js +162 -151
- package/src/chat/tool_executor.js +229 -218
- package/src/cli.js +535 -433
- package/src/config/approval.js +35 -29
- package/src/config/config.js +546 -454
- package/src/config/data_dir.js +24 -0
- package/src/config/models.js +191 -115
- package/src/config/refs.js +32 -0
- package/src/config/secrets.js +153 -0
- package/src/files/documents.js +57 -68
- package/src/files/images.js +30 -31
- package/src/harness/agent.js +60 -60
- package/src/harness/attribution.js +66 -58
- package/src/harness/checkpoint.js +67 -62
- package/src/harness/history.js +28 -23
- package/src/harness/prompt.js +40 -57
- package/src/harness/provider.js +284 -283
- package/src/harness/types.js +1 -0
- package/src/http.js +90 -72
- package/src/log.js +11 -12
- package/src/mcp.js +398 -374
- package/src/retrieval/embedding.js +228 -240
- package/src/retrieval/retrieval.js +182 -173
- package/src/routes/admin.js +178 -138
- package/src/routes/auth.js +184 -111
- package/src/routes/automations.js +99 -82
- package/src/routes/chats.js +240 -218
- package/src/routes/documents.js +130 -120
- package/src/routes/library.js +43 -46
- package/src/routes/mcp.js +49 -49
- package/src/routes/settings.js +127 -122
- package/src/routes/types.js +1 -0
- package/src/server.js +426 -415
- package/src/store/automations.js +106 -106
- package/src/store/chats.js +217 -242
- package/src/store/documents.js +181 -185
- package/src/store/embeddings.js +52 -54
- package/src/store/index.js +95 -49
- package/src/store/messages.js +55 -80
- package/src/store/schema.js +128 -107
- package/src/store/scope.js +18 -17
- package/src/store/search.js +25 -27
- package/src/store/turns.js +53 -53
- package/src/store/usage.js +343 -269
- package/src/store/users.js +162 -87
- package/src/store/wire.js +50 -41
- package/src/tools/ask_tool.js +68 -68
- package/src/tools/chat_search_tool.js +38 -44
- package/src/tools/context_tool.js +131 -138
- package/src/tools/document_tool.js +73 -78
- package/src/tools/mcp_tool.js +274 -232
- package/src/tools/task_tool.js +53 -50
- package/src/tools/types.js +1 -0
- package/public/panels/connectors.js +0 -184
- 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="
|
|
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
|
-
<
|
|
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 & 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>
|
|
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="
|
|
101
|
-
<div><label for="
|
|
102
|
-
<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>
|
|
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>
|
|
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>
|
|
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="
|
|
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
|
-
<
|
|
136
|
-
|
|
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
|
|
142
|
-
<div class="span-2"><label for="
|
|
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>
|
|
149
|
-
<div
|
|
150
|
-
|
|
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>
|
|
155
|
-
<div
|
|
156
|
-
|
|
178
|
+
<header><h3>Questions & 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>
|
|
186
|
+
<header><h3>Context & 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="
|
|
163
|
-
<div><label for="
|
|
164
|
-
<div><label for="
|
|
165
|
-
<div><label for="
|
|
166
|
-
<div><label for="
|
|
167
|
-
<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-
|
|
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
|
|
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
|
|
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,24 +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
|
-
<div id="loginLink" class="login-card" hidden>
|
|
242
|
-
<h2>TinyWebUI</h2>
|
|
243
|
-
<p>Open the sign-in link printed in the terminal where TinyWebUI is running.</p>
|
|
244
|
-
</div>
|
|
245
274
|
</div>
|
|
246
275
|
|
|
247
276
|
<section id="admin" aria-label="Admin">
|
|
248
|
-
<div class="automation-head"><h2>Admin</h2
|
|
277
|
+
<div class="automation-head"><h2>Admin</h2></div>
|
|
249
278
|
<div class="statistics-content">
|
|
250
279
|
<section class="statistics-block">
|
|
251
280
|
<div class="sec-h">users <span id="adminCount" class="usage"></span></div>
|
|
252
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>
|
|
253
288
|
<div id="adminUsers" class="admin-users"></div>
|
|
254
289
|
</section>
|
|
255
290
|
<section id="adminPolicy" class="statistics-block">
|
|
@@ -270,7 +305,7 @@
|
|
|
270
305
|
</section>
|
|
271
306
|
|
|
272
307
|
<section id="automations" aria-label="Automations">
|
|
273
|
-
<div class="automation-head"><h2>Automations</h2><button id="new-automation" class="primary" type="button">New automation</button
|
|
308
|
+
<div class="automation-head"><h2>Automations</h2><button id="new-automation" class="primary" type="button">New automation</button></div>
|
|
274
309
|
<div class="automation-content">
|
|
275
310
|
<div id="automation-create-slot"></div>
|
|
276
311
|
<div id="automation-list" class="automation-list"></div>
|
package/public/panels/admin.js
CHANGED
|
@@ -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 === '
|
|
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;
|