Close create-invite modal after success; drop redundant result panel.
Release rolling / release (push) Successful in 8m4s

Invite strings already appear under Active invites with Copy, so the post-create
panel in the modal was unnecessary and felt stuck open.
This commit is contained in:
Raven Scott
2026-07-14 23:00:52 -04:00
parent 54d3d00901
commit 0e74019acc
2 changed files with 36 additions and 150 deletions
+31 -113
View File
@@ -1723,80 +1723,36 @@ window.applyRoleUI = applyRoleUI;
// Wire access view actions once
document.addEventListener('DOMContentLoaded', () => {
// Invite form (role / TTL / max uses + full pd1 string + Copy invite)
// Invite form (role / TTL / max uses) — result lives in Active invites list, not a second panel
const inviteForm = document.getElementById('invite-peer-form');
function setInviteResultUi({ token, kind, publicKeyHex, role, jti, persistent }) {
const resultBox = document.getElementById('invite-token-result');
const tokenInput = document.getElementById('invite-token-value');
const lenEl = document.getElementById('invite-token-len');
const pkgKeys = document.getElementById('invite-pkg-keys');
const pubEl = document.getElementById('invite-pkg-pubkey');
const grantEl = document.getElementById('invite-grant-meta');
if (!token || !tokenInput) return;
// Full string only — never slice/substring (pd1 invites break if truncated)
const full = String(token);
tokenInput.value = full;
tokenInput.defaultValue = full;
if (lenEl) lenEl.textContent = `${full.length} characters (complete)`;
if (grantEl) {
const bits = [
role ? `role=${role}` : null,
jti ? `jti=${String(jti).slice(0, 12)}` : null,
persistent ? 'persistent (never expires, unlimited reconnects)' : null,
kind ? `kind=${kind}` : null,
].filter(Boolean);
grantEl.textContent = bits.length
? `Capability grant linked to this invite: ${bits.join(' · ')}`
: '';
grantEl.classList.toggle('hidden', !bits.length);
}
resultBox?.classList.remove('hidden');
if (publicKeyHex && pubEl && /^[0-9a-fA-F]{64}$/.test(String(publicKeyHex))) {
pubEl.textContent = String(publicKeyHex).toLowerCase();
pkgKeys?.classList.remove('hidden');
} else {
if (pubEl) pubEl.textContent = '';
pkgKeys?.classList.add('hidden');
}
// Focus + select so user can verify nothing is clipped
requestAnimationFrame(() => {
tokenInput.focus();
tokenInput.select();
tokenInput.setSelectionRange?.(0, full.length);
});
function hideInvitePeerModal() {
const modalEl = document.getElementById('invitePeerModal');
if (!modalEl || typeof bootstrap === 'undefined') return;
const inst = bootstrap.Modal.getInstance(modalEl);
if (inst) inst.hide();
else new bootstrap.Modal(modalEl).hide();
}
async function copyInviteFull(sourceEl) {
const token =
(sourceEl && 'value' in sourceEl ? sourceEl.value : null) ||
document.getElementById('invite-token-value')?.value ||
'';
const full = String(token);
async function copyInviteText(token, { quiet = false } = {}) {
const full = String(token || '');
if (!full) {
showAlert('warning', 'No invite to copy yet — create one first');
if (!quiet) showAlert('warning', 'No invite to copy');
return false;
}
try {
await navigator.clipboard.writeText(full);
showAlert('success', `Invite copied (${full.length} characters, full string)`);
if (!quiet) {
showAlert('success', `Invite copied (${full.length} characters, full string)`);
}
return true;
} catch {
if (sourceEl?.select) {
sourceEl.focus();
sourceEl.select();
sourceEl.setSelectionRange?.(0, full.length);
if (!quiet) {
showAlert(
'warning',
'Could not auto-copy — use Copy invite on the card under Active invites'
);
}
try {
const ok = document.execCommand?.('copy');
if (ok) {
showAlert('success', `Invite copied (${full.length} characters, full string)`);
return true;
}
} catch {
// fall through
}
showAlert('warning', 'Could not copy — select the full invite and copy manually (Cmd/Ctrl+C)');
return false;
}
}
@@ -1820,30 +1776,25 @@ document.addEventListener('DOMContentLoaded', () => {
const res = await manager.request(Methods.invitePeer, { role, ttlHours, maxUses });
const token = res?.data?.token || res?.data?.invite || res?.data?.capability;
const kind = res?.data?.kind || 'invite';
const publicKeyHex = res?.data?.publicKeyHex || null;
const grantRole = res?.data?.role || role;
const jti = res?.data?.jti || null;
const persistent = res?.data?.persistent === true || (ttlHours === 0 && maxUses === 0);
if (token) {
setInviteResultUi({
token,
kind,
publicKeyHex,
role: grantRole,
jti,
persistent,
});
const label =
// Close create form — full string + Copy live under Active invites
hideInvitePeerModal();
const copied = await copyInviteText(token, { quiet: true });
const base =
kind === 'pd1'
? `Invite created for role “${grantRole}” — share the full pd1. string only`
: `Invite created for role “${grantRole} — copy the full grant`;
showAlert('success', label);
// Auto-copy full string for convenience
await copyInviteFull(document.getElementById('invite-token-value'));
? `Invite created for role “${grantRole}” — share the full pd1. string`
: `Invite created for role “${grantRole}`;
showAlert(
'success',
copied
? `${base} (copied · ${String(token).length} characters)`
: `${base}. Use Copy invite on the Active invites card.`
);
} else {
showAlert('danger', 'Server returned an empty invite — try again');
}
loadAccessView();
await loadAccessView();
} catch (err) {
showAlert('danger', err.message || 'Failed to create invite');
} finally {
@@ -1851,39 +1802,6 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
}
document.getElementById('invite-token-copy')?.addEventListener('click', async () => {
await copyInviteFull(document.getElementById('invite-token-value'));
});
document.getElementById('invite-token-select-all')?.addEventListener('click', () => {
const el = document.getElementById('invite-token-value');
if (!el) return;
el.focus();
el.select();
el.setSelectionRange?.(0, el.value.length);
});
document.getElementById('invite-pkg-pubkey-copy')?.addEventListener('click', async () => {
const pk = document.getElementById('invite-pkg-pubkey')?.textContent?.trim() || '';
if (!pk) return;
try {
await navigator.clipboard.writeText(pk);
showAlert('success', `Public key copied (${pk.length} characters)`);
} catch {
showAlert('warning', 'Could not copy public key');
}
});
document.getElementById('invitePeerModal')?.addEventListener('show.bs.modal', () => {
document.getElementById('invite-token-result')?.classList.add('hidden');
document.getElementById('invite-pkg-keys')?.classList.add('hidden');
const tokenInput = document.getElementById('invite-token-value');
if (tokenInput) {
tokenInput.value = '';
tokenInput.defaultValue = '';
}
const lenEl = document.getElementById('invite-token-len');
if (lenEl) lenEl.textContent = '';
const pubEl = document.getElementById('invite-pkg-pubkey');
if (pubEl) pubEl.textContent = '';
});
// First-connect checklist
document.getElementById('first-connect-dismiss')?.addEventListener('click', dismissFirstConnectChecklist);
+5 -37
View File
@@ -4329,45 +4329,13 @@ services:
<div class="form-text text-muted">0 = unlimited (default)</div>
</div>
</div>
<p class="small text-muted mb-3">Grants persist forever by default. Invited clients reconnect automatically with their saved capability + stable identity. Set TTL/uses only if you want the grant to expire.</p>
<div id="invite-token-result" class="invite-token-result hidden">
<label class="form-label" for="invite-token-value">Operator invite <span class="text-muted fw-normal">(pd1.… full string — never truncated)</span></label>
<textarea
id="invite-token-value"
class="form-control bg-dark text-white font-monospace invite-secret-full"
rows="5"
readonly
wrap="soft"
spellcheck="false"
autocomplete="off"
aria-describedby="invite-token-hint invite-token-len"
></textarea>
<div class="d-flex flex-wrap align-items-center gap-2 mt-2">
<button type="button" class="btn btn-primary" id="invite-token-copy" title="Copy full invite to clipboard">
<i class="fas fa-copy me-1"></i>Copy invite
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="invite-token-select-all" title="Select full invite text">
Select all
</button>
<span class="small text-muted" id="invite-token-len"></span>
</div>
<p class="small text-info mt-2 mb-1" id="invite-grant-meta"></p>
<p class="small text-muted mt-1 mb-0" id="invite-token-hint">
Share <strong>only this pd1. string</strong> with the operator. It embeds the public key + capability for the role you chose. Paste the entire string in Add peer (do not use an old saved peer entry).
</p>
<div id="invite-pkg-keys" class="invite-pkg-keys hidden mt-3">
<label class="form-label small text-muted mb-1">Server public key (also inside the package)</label>
<div class="d-flex gap-2 align-items-start">
<code id="invite-pkg-pubkey" class="invite-secret-full flex-grow-1 user-select-all mb-0"></code>
<button type="button" class="btn btn-outline-primary btn-sm flex-shrink-0" id="invite-pkg-pubkey-copy" title="Copy public key">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
</div>
<p class="small text-muted mb-0">
Grants persist forever by default. After create, the full <code>pd1.</code> invite appears under
<strong>Active invites</strong> (with Copy). Invited clients reconnect with their saved capability + stable identity.
</p>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary" id="invite-peer-submit">
<i class="fas fa-ticket me-1"></i>Create invite
</button>