diff --git a/Plugin/unraid/pages/auth.php b/Plugin/unraid/pages/auth.php
index e653f86..a2dd468 100644
--- a/Plugin/unraid/pages/auth.php
+++ b/Plugin/unraid/pages/auth.php
@@ -108,6 +108,17 @@ require_once dirname(__DIR__) . '/include/ai_chat.php';
.vv-au-av.none { font-size:9px;color:#4a4a4a;text-align:center;line-height:22px;font-weight:bold;
letter-spacing:.02em; }
.vv-au-av.big { width:88px;height:88px;border-radius:5px;line-height:88px;font-size:26px; }
+
+/* ── Editor sections ─────────────────────────────────────────────────────── */
+/* Below the field block and its Save, so the split between "staged until Save details" and
+ "applies when you press its own button" is a visible line rather than a convention. */
+.vv-au-sect { margin-top:6px;padding-top:10px;border-top:1px solid #222; }
+.vv-au-sect-h { font-size:10px;color:#444;text-transform:uppercase;letter-spacing:.06em;
+ margin:10px 0 5px; }
+.vv-au-sect-h:first-child { margin-top:0; }
+/* Wider only where it has to be — the editor now carries a photo beside a form. The other dialogs
+ on this page are single columns and would just get airier. */
+.vv-au-modal.wide { max-width:620px; }
.vv-au-user-email{ font-size:10px;color:#444;flex:1; }
.vv-au-user-acts { display:flex;gap:4px;margin-left:auto;flex-shrink:0; }
@@ -396,8 +407,12 @@ function _post(params, cb) {
.catch(e => cb({ ok: false, error: String(e) }));
}
-function _modal(html) {
- document.getElementById('vv-au-modal').innerHTML = html;
+// wide is for the one dialog that carries a photo beside a form. Reset on every call rather than
+// only set, or a narrow dialog opened after the editor would inherit its width.
+function _modal(html, wide) {
+ const m = document.getElementById('vv-au-modal');
+ m.innerHTML = html;
+ m.classList.toggle('wide', !!wide);
document.getElementById('vv-au-overlay').classList.add('open');
}
function _closeModal() {
@@ -654,7 +669,7 @@ document.getElementById('vv-au-panel-proxies').addEventListener('click', async e
});
// ── Users ─────────────────────────────────────────────────────────────────────
-function _loadUsers() {
+function _loadUsers(done) {
const loading = document.getElementById('vv-au-users-loading');
const list = document.getElementById('vv-au-users-list');
const empty = document.getElementById('vv-au-users-empty');
@@ -664,9 +679,12 @@ function _loadUsers() {
_get('lldap_users', r => {
loading.style.display = 'none';
- if (!r.ok) { loading.innerHTML = ''+_esc(r.error)+''; loading.style.display='block'; return; }
+ // done() fires on every path including failure and the empty list. An open editor waits on it
+ // to redraw its sections, and a refresh that silently never called back would leave the dialog
+ // showing the state from before the change with no indication anything had happened.
+ if (!r.ok) { loading.innerHTML = ''+_esc(r.error)+''; loading.style.display='block'; if (done) done(); return; }
_users = r.users || [];
- if (!_users.length) { empty.style.display = 'block'; return; }
+ if (!_users.length) { empty.style.display = 'block'; if (done) done(); return; }
list.innerHTML = _users.map(u => {
const grpBadges = (u.groups||[]).map(g => `${_esc(g.displayName)}`).join('');
// Only fetched for the users who have one — has_avatar comes from the attribute names, so
@@ -693,6 +711,7 @@ function _loadUsers() {
`;
}).join('');
+ if (done) done();
});
}
@@ -734,9 +753,45 @@ function _userModal(uid) {
` : ''}
-
-
-
`);
+
+
+
+
+ ${u ? `
+
+
Groups
+
+
+
Password
+
+
+
+
+
+
+
Photo
+
+
+
+
+
Any image; cropped square, ${VV_AVATAR_PX}px, saved as JPEG.
+
+
+
+
+
+
+
` : ''}`, !!u);
+
+ if (u) _userModalExtras(u);
document.getElementById('um-cancel').onclick = _closeModal;
document.getElementById('um-save').onclick = () => {
@@ -753,8 +808,15 @@ function _userModal(uid) {
btn.disabled = true; btn.textContent = 'Saving…';
const done = r => {
- if (!r.ok) { _showModalErr('um-err', r.error||'Save failed'); btn.disabled=false; btn.textContent=u?'Save':'Create'; return; }
- _closeModal(); _loadUsers();
+ if (!r.ok) { _showModalErr('um-err', r.error||'Save failed'); btn.disabled=false; btn.textContent=u?'Save details':'Create'; return; }
+ // A create is finished when it succeeds, so the dialog closes. An edit is not — the sections
+ // below are the reason it is worth staying in, and closing on the first Save would put the
+ // operator straight back to reopening it.
+ if (!u) { _closeModal(); _loadUsers(); return; }
+ _showModalErr('um-err', '');
+ btn.disabled = false; btn.textContent = 'Saved ✓';
+ setTimeout(() => { btn.textContent = 'Save details'; }, 2500);
+ _loadUsers();
};
// first_name/last_name are always sent from this form, including empty, because the form does
// offer them — an empty box here means "clear it", which the endpoint turns into a proper
@@ -764,6 +826,144 @@ function _userModal(uid) {
};
}
+// ── The editor's live sections ────────────────────────────────────────────────
+// Groups, password and photo, wired inside the open Edit User dialog. Each acts immediately and
+// reports in place; none of them closes the dialog, because the reason they are here at all is
+// that having to leave the editor to reach them was the complaint.
+//
+// The background lists are refreshed too, so the row behind the dialog is not left showing the
+// groups or the photo the user had a moment ago.
+function _userModalExtras(u) {
+ const uid = u.id;
+
+ // Re-read from _users after a refresh rather than trusting the copy captured when the dialog
+ // opened — a group added here changes the object the next redraw has to render from.
+ const cur = () => _users.find(x => x.id === uid) || u;
+
+ const drawGroups = () => {
+ const me = cur();
+ const mine = me.groups || [];
+ const gids = new Set(mine.map(g => g.id));
+ const free = _groups.filter(g => !gids.has(g.id));
+ document.getElementById('um-groups').innerHTML =
+ `