commit 4beeb86740f568bc3a2a6249b3b2d0b13a9327ac
parent 106610f051dd4bd74524c117c0fab57146d56f52
Author: finwo <finwo@pm.me>
Date: Sun, 20 Sep 2026 20:52:02 +0200
fix UI: show form errors, replace native confirms with HTML5 dialogs
Diffstat:
2 files changed, 55 insertions(+), 12 deletions(-)
diff --git a/src/conductor/ui/pages.js b/src/conductor/ui/pages.js
@@ -151,7 +151,7 @@ export function loginPage({ error }) {
<div class="panel narrow">
<h2>Sign in</h2>
${notice(error)}
- <form hx-post="/login" hx-target="body" hx-swap="none">
+ <form hx-post="/login" hx-target="body" hx-swap="outerHTML">
<label>username<input name="username" autocomplete="username" required autofocus></label>
<label>password<input name="password" type="password" autocomplete="current-password" required></label>
<div class="actions"><button type="submit">sign in</button></div>
@@ -313,12 +313,28 @@ export function projectPage({ project, variables, triggerUrl, secret, user, rete
<button hx-post="/projects/${project.id}/trigger-secret" hx-target="body" hx-swap="none">
rotate trigger secret
</button>
- <button class="danger"
- hx-delete="/projects/${project.id}"
- hx-confirm="Delete ${project.id} and all of its jobs?"
- hx-swap="none">delete project</button>
+ <button class="danger" onclick="setupDeleteProjectDialog('${project.id}')">delete project</button>
</div>
</div>
+
+ <dialog id="deleteProjectDialog">
+ <p>Delete "<span id="deleteProjectId"></span>" and all of its jobs?</p>
+ <div class="actions">
+ <button type="button" id="deleteProjectConfirm" class="danger">delete project</button>
+ <button type="button" onclick="this.closest('dialog').close()">cancel</button>
+ </div>
+ </dialog>
+
+ <script>
+ function setupDeleteProjectDialog(id) {
+ document.getElementById('deleteProjectId').textContent = id;
+ document.getElementById('deleteProjectConfirm').onclick = function() {
+ htmx.ajax('DELETE', '/projects/' + id, { target: 'body', swap: 'none' });
+ document.getElementById('deleteProjectDialog').close();
+ };
+ document.getElementById('deleteProjectDialog').showModal();
+ }
+ </script>
`;
}
@@ -429,16 +445,17 @@ export function workersTable(tokens, user) {
// --- users ---
-export function usersPage({ users, localLogin }) {
+export function usersPage({ users, localLogin, error }) {
return html`
<h2>Users</h2>
${localLogin ? '' : html`<p class="muted">
Authentication goes through the identity provider, so these accounts are inactive.
</p>`}
+ ${error ? notice(error) : ''}
<div class="panel">
<h3>Add a user</h3>
- <form hx-post="/users" hx-target="body" hx-swap="none">
+ <form hx-post="/users" hx-target="body" hx-swap="outerHTML">
<div class="grid">
<label>username<input name="username" required></label>
<label>password<input name="password" type="password" required></label>
@@ -451,6 +468,25 @@ export function usersPage({ users, localLogin }) {
</div>
${usersTable(users)}
+
+ <dialog id="removeUserDialog">
+ <p id="removeUserMessage"></p>
+ <div class="actions">
+ <button type="button" id="removeUserConfirm" class="danger">remove</button>
+ <button type="button" onclick="this.closest('dialog').close()">cancel</button>
+ </div>
+ </dialog>
+
+ <script>
+ function setupRemoveUserDialog(id, message) {
+ document.getElementById('removeUserMessage').textContent = message;
+ document.getElementById('removeUserConfirm').onclick = function() {
+ htmx.ajax('DELETE', '/users/' + id, { target: '#users', swap: 'outerHTML' });
+ document.getElementById('removeUserDialog').close();
+ };
+ document.getElementById('removeUserDialog').showModal();
+ }
+ </script>
`;
}
@@ -469,8 +505,7 @@ export function usersTable(users) {
<td class="row-actions">
<button class="link" hx-patch="/users/${u.id}" hx-vals='{"disabled": ${u.disabled === 1 ? 'false' : 'true'}}'
hx-target="#users" hx-swap="outerHTML">${u.disabled === 1 ? 'enable' : 'disable'}</button>
- <button class="link danger" hx-delete="/users/${u.id}"
- hx-confirm="${deleteWarning(u)}" hx-target="#users" hx-swap="outerHTML">remove</button>
+ <button class="link danger" onclick="setupRemoveUserDialog('${u.id}', ${JSON.stringify(deleteWarning(u))})">remove</button>
</td>
</tr>`)}
</tbody>
diff --git a/src/conductor/ui/routes.js b/src/conductor/ui/routes.js
@@ -392,8 +392,11 @@ export default async function uiRoutes(fastify, services) {
const { username, password } = req.body ?? {};
const user = await users.authenticate(String(username ?? ''), String(password ?? ''));
if (!user) {
- return reply.code(401).header('content-type', 'text/html; charset=utf-8')
- .send(toHtml(loginPage({ error: 'Invalid username or password.' })));
+ return page(reply, {
+ title: 'sign in',
+ user: null,
+ body: loginPage({ error: 'Invalid username or password.' }),
+ });
}
const ttl = cfg.auth.session_ttl;
@@ -691,7 +694,12 @@ export default async function uiRoutes(fastify, services) {
role: req.body?.role === 'admin' ? 'admin' : 'user',
});
} catch (e) {
- return fail(reply, e.message);
+ return page(reply, {
+ title: 'users',
+ user: await currentUser(req),
+ active: 'users',
+ body: usersPage({ users: await users.listWithHoldings(), localLogin: auth.localLogin, error: e.message }),
+ });
}
return refresh(reply);
});