Close create-invite modal after success; drop redundant result panel.
Release rolling / release (push) Successful in 8m4s
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:
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user