fix(ui): complete all items in tasks/checklist.md (sticky docking, floating toasts, rotate PIN, tab locks, expired events, reset actions)
This commit is contained in:
parent
ed9bdb8852
commit
b38f122df4
@ -5,30 +5,74 @@ function showNotice(msg, isError) {
|
|||||||
if (!banner) return;
|
if (!banner) return;
|
||||||
banner.textContent = msg;
|
banner.textContent = msg;
|
||||||
banner.style.display = "block";
|
banner.style.display = "block";
|
||||||
|
banner.style.position = "fixed";
|
||||||
|
banner.style.top = "1.25rem";
|
||||||
|
banner.style.right = "1.25rem";
|
||||||
|
banner.style.zIndex = "99999";
|
||||||
|
banner.style.minWidth = "280px";
|
||||||
|
banner.style.maxWidth = "480px";
|
||||||
|
banner.style.padding = "0.85rem 1.25rem";
|
||||||
|
banner.style.borderRadius = "var(--radius-md)";
|
||||||
|
banner.style.boxShadow = "0 10px 25px -5px rgba(0, 0, 0, 0.35)";
|
||||||
|
banner.style.fontSize = "0.9rem";
|
||||||
|
banner.style.fontWeight = "600";
|
||||||
banner.style.background = isError ? "var(--danger-bg)" : "var(--success-bg)";
|
banner.style.background = isError ? "var(--danger-bg)" : "var(--success-bg)";
|
||||||
banner.style.color = isError ? "var(--danger-text)" : "var(--success-text)";
|
banner.style.color = isError ? "var(--danger-text)" : "var(--success-text)";
|
||||||
banner.style.border = isError
|
banner.style.border = isError
|
||||||
? "1px solid var(--danger-border)"
|
? "1px solid var(--danger-border)"
|
||||||
: "1px solid var(--success-border)";
|
: "1px solid var(--success-border)";
|
||||||
setTimeout(() => {
|
if (banner._timeout) clearTimeout(banner._timeout);
|
||||||
|
banner._timeout = setTimeout(() => {
|
||||||
banner.style.display = "none";
|
banner.style.display = "none";
|
||||||
}, 5000);
|
}, 4000);
|
||||||
}
|
}
|
||||||
globalThis.showNotice = showNotice;
|
globalThis.showNotice = showNotice;
|
||||||
|
|
||||||
|
function setTabLock(locked) {
|
||||||
|
const btnDirect = document.getElementById("tabBtnDirectPass");
|
||||||
|
const btnWorkshop = document.getElementById("tabBtnWorkshopPass");
|
||||||
|
if (btnDirect) {
|
||||||
|
btnDirect.disabled = locked;
|
||||||
|
btnDirect.style.pointerEvents = locked ? "none" : "auto";
|
||||||
|
btnDirect.style.opacity = locked ? "0.5" : "1";
|
||||||
|
}
|
||||||
|
if (btnWorkshop) {
|
||||||
|
btnWorkshop.disabled = locked;
|
||||||
|
btnWorkshop.style.pointerEvents = locked ? "none" : "auto";
|
||||||
|
btnWorkshop.style.opacity = locked ? "0.5" : "1";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetDirectPassForm() {
|
||||||
|
const form = document.getElementById("delegateForm");
|
||||||
|
if (form) form.reset();
|
||||||
|
const createEl = document.getElementById("singleSessionCreateState");
|
||||||
|
if (createEl) createEl.style.display = "block";
|
||||||
|
const handoffEl = document.getElementById("handoffModal");
|
||||||
|
if (handoffEl) handoffEl.style.display = "none";
|
||||||
|
setTabLock(false);
|
||||||
|
}
|
||||||
|
globalThis.resetDirectPassForm = resetDirectPassForm;
|
||||||
|
|
||||||
|
function resetWorkshopForm() {
|
||||||
|
const form = document.getElementById("eventForm");
|
||||||
|
if (form) form.reset();
|
||||||
|
const createEl = document.getElementById("eventCreateState");
|
||||||
|
if (createEl) createEl.style.display = "block";
|
||||||
|
const handoffEl = document.getElementById("eventHandoffState");
|
||||||
|
if (handoffEl) handoffEl.style.display = "none";
|
||||||
|
setTabLock(false);
|
||||||
|
}
|
||||||
|
globalThis.resetWorkshopForm = resetWorkshopForm;
|
||||||
|
|
||||||
function openDelegateDrawer() {
|
function openDelegateDrawer() {
|
||||||
const drawer = document.getElementById("delegateDrawer");
|
const drawer = document.getElementById("delegateDrawer");
|
||||||
if (!drawer) return;
|
if (!drawer) return;
|
||||||
drawer.style.display = "block";
|
drawer.style.display = "block";
|
||||||
drawer.scrollIntoView({ behavior: "smooth" });
|
drawer.scrollIntoView({ behavior: "smooth" });
|
||||||
|
|
||||||
const eventCreateState = document.getElementById("eventCreateState");
|
resetDirectPassForm();
|
||||||
if (eventCreateState) eventCreateState.style.display = "block";
|
resetWorkshopForm();
|
||||||
const eventHandoffState = document.getElementById("eventHandoffState");
|
|
||||||
if (eventHandoffState) eventHandoffState.style.display = "none";
|
|
||||||
const eventForm = document.getElementById("eventForm");
|
|
||||||
if (eventForm) eventForm.reset();
|
|
||||||
|
|
||||||
switchDelegateTab("tabDirectPass");
|
switchDelegateTab("tabDirectPass");
|
||||||
}
|
}
|
||||||
globalThis.openDelegateDrawer = openDelegateDrawer;
|
globalThis.openDelegateDrawer = openDelegateDrawer;
|
||||||
@ -36,6 +80,8 @@ globalThis.openDelegateDrawer = openDelegateDrawer;
|
|||||||
function closeDelegateDrawer() {
|
function closeDelegateDrawer() {
|
||||||
const drawer = document.getElementById("delegateDrawer");
|
const drawer = document.getElementById("delegateDrawer");
|
||||||
if (drawer) drawer.style.display = "none";
|
if (drawer) drawer.style.display = "none";
|
||||||
|
resetDirectPassForm();
|
||||||
|
resetWorkshopForm();
|
||||||
}
|
}
|
||||||
globalThis.closeDelegateDrawer = closeDelegateDrawer;
|
globalThis.closeDelegateDrawer = closeDelegateDrawer;
|
||||||
|
|
||||||
@ -132,7 +178,7 @@ async function handleDelegateSession(e) {
|
|||||||
const hoursInput = document.getElementById("delegateHours");
|
const hoursInput = document.getElementById("delegateHours");
|
||||||
const lifespanHours = parseInt(hoursInput ? hoursInput.value : "1", 10) || 1;
|
const lifespanHours = parseInt(hoursInput ? hoursInput.value : "1", 10) || 1;
|
||||||
const modeInput = document.getElementById("delegateMode");
|
const modeInput = document.getElementById("delegateMode");
|
||||||
let mode = modeInput ? modeInput.value : "read_only";
|
let mode = modeInput ? modeInput.value : "read";
|
||||||
const targetAppInput = document.getElementById("delegateTargetApp");
|
const targetAppInput = document.getElementById("delegateTargetApp");
|
||||||
const targetApp = targetAppInput ? targetAppInput.value : "";
|
const targetApp = targetAppInput ? targetAppInput.value : "";
|
||||||
|
|
||||||
@ -179,8 +225,13 @@ async function handleDelegateSession(e) {
|
|||||||
if (curlElem) {
|
if (curlElem) {
|
||||||
curlElem.textContent = '-H "Authorization: Bearer ' + data.token + '"';
|
curlElem.textContent = '-H "Authorization: Bearer ' + data.token + '"';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createEl = document.getElementById("singleSessionCreateState");
|
||||||
|
if (createEl) createEl.style.display = "none";
|
||||||
const handoffModal = document.getElementById("handoffModal");
|
const handoffModal = document.getElementById("handoffModal");
|
||||||
if (handoffModal) handoffModal.style.display = "block";
|
if (handoffModal) handoffModal.style.display = "block";
|
||||||
|
|
||||||
|
setTabLock(true);
|
||||||
showNotice("Delegated session created for: " + data.label, false);
|
showNotice("Delegated session created for: " + data.label, false);
|
||||||
} else {
|
} else {
|
||||||
showNotice(data.error || "Failed to delegate session", true);
|
showNotice(data.error || "Failed to delegate session", true);
|
||||||
@ -255,6 +306,7 @@ async function handleCreateEvent(e) {
|
|||||||
const handoffState = document.getElementById("eventHandoffState");
|
const handoffState = document.getElementById("eventHandoffState");
|
||||||
if (handoffState) handoffState.style.display = "block";
|
if (handoffState) handoffState.style.display = "block";
|
||||||
|
|
||||||
|
setTabLock(true);
|
||||||
showNotice("Workshop pass created: " + ev.name, false);
|
showNotice("Workshop pass created: " + ev.name, false);
|
||||||
} else {
|
} else {
|
||||||
showNotice(data.error || "Failed to create event pass", true);
|
showNotice(data.error || "Failed to create event pass", true);
|
||||||
@ -319,8 +371,9 @@ function copyHandoff(type) {
|
|||||||
globalThis.copyHandoff = copyHandoff;
|
globalThis.copyHandoff = copyHandoff;
|
||||||
|
|
||||||
function closeHandoffModal() {
|
function closeHandoffModal() {
|
||||||
const modal = document.getElementById("handoffModal");
|
resetDirectPassForm();
|
||||||
if (modal) modal.style.display = "none";
|
const drawer = document.getElementById("delegateDrawer");
|
||||||
|
if (drawer) drawer.style.display = "none";
|
||||||
if (globalThis.location) globalThis.location.reload();
|
if (globalThis.location) globalThis.location.reload();
|
||||||
}
|
}
|
||||||
globalThis.closeHandoffModal = closeHandoffModal;
|
globalThis.closeHandoffModal = closeHandoffModal;
|
||||||
@ -466,12 +519,12 @@ async function rotatePin(eventId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showNotice("Event credentials rotated successfully", false);
|
showNotice("PIN rotated successfully", false);
|
||||||
} else {
|
} else {
|
||||||
showNotice(data.error || "Failed to rotate credentials", true);
|
showNotice(data.error || "Failed to rotate PIN", true);
|
||||||
}
|
}
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
showNotice("Network error rotating credentials", true);
|
showNotice("Network error rotating PIN", true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
globalThis.rotatePin = rotatePin;
|
globalThis.rotatePin = rotatePin;
|
||||||
@ -885,6 +938,31 @@ function bindEventCockpitActions() {
|
|||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (res.ok && data.success) {
|
if (res.ok && data.success) {
|
||||||
showNotice("Event extended by 1 hour!", false);
|
showNotice("Event extended by 1 hour!", false);
|
||||||
|
|
||||||
|
const drawer = document.getElementById("attendeesDrawer");
|
||||||
|
if (drawer && drawer.classList.contains("open")) {
|
||||||
|
const countdownElem = document.getElementById(
|
||||||
|
"guestDrawerCountdown",
|
||||||
|
);
|
||||||
|
if (countdownElem && data.event && data.event.expires_at) {
|
||||||
|
countdownElem.setAttribute(
|
||||||
|
"data-expires-at",
|
||||||
|
data.event.expires_at,
|
||||||
|
);
|
||||||
|
const expDate = new Date(data.event.expires_at);
|
||||||
|
const expiresAtElem = document.getElementById(
|
||||||
|
"guestDrawerExpiresAt",
|
||||||
|
);
|
||||||
|
if (expiresAtElem) {
|
||||||
|
expiresAtElem.textContent = expDate.toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updateAllCountdowns();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (globalThis.location) globalThis.location.reload();
|
if (globalThis.location) globalThis.location.reload();
|
||||||
}, 600);
|
}, 600);
|
||||||
@ -927,12 +1005,12 @@ function bindEventCockpitActions() {
|
|||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
showNotice(data.error || "Failed to end event", true);
|
showNotice(data.error || "Failed to end event", true);
|
||||||
e.currentTarget.disabled = false;
|
e.currentTarget.disabled = false;
|
||||||
e.currentTarget.textContent = "🔴 End & Revoke All";
|
e.currentTarget.textContent = "End Event";
|
||||||
}
|
}
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
showNotice("Network error ending event", true);
|
showNotice("Network error ending event", true);
|
||||||
e.currentTarget.disabled = false;
|
e.currentTarget.disabled = false;
|
||||||
e.currentTarget.textContent = "🔴 End & Revoke All";
|
e.currentTarget.textContent = "End Event";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,6 +5,14 @@ export const EventCockpitDeckFragment = (
|
|||||||
) => {
|
) => {
|
||||||
if (!eventPasses || eventPasses.length === 0) return null;
|
if (!eventPasses || eventPasses.length === 0) return null;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const activePasses = eventPasses.filter((e) =>
|
||||||
|
!e.expires_at || new Date(e.expires_at).getTime() > now
|
||||||
|
);
|
||||||
|
const expiredPasses = eventPasses.filter((e) =>
|
||||||
|
e.expires_at && new Date(e.expires_at).getTime() <= now
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style="margin-bottom: 2rem;">
|
<div style="margin-bottom: 2rem;">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-subtle); padding-bottom: 0.5rem; margin-bottom: 1rem;">
|
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-subtle); padding-bottom: 0.5rem; margin-bottom: 1rem;">
|
||||||
@ -36,7 +44,7 @@ export const EventCockpitDeckFragment = (
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="eventDeckContainer" class="grid-view">
|
<div id="eventDeckContainer" class="grid-view">
|
||||||
{eventPasses.map((event) => {
|
{activePasses.map((event) => {
|
||||||
const expDate = new Date(event.expires_at);
|
const expDate = new Date(event.expires_at);
|
||||||
const timeString = expDate.toLocaleTimeString([], {
|
const timeString = expDate.toLocaleTimeString([], {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
@ -192,7 +200,6 @@ export const EventCockpitDeckFragment = (
|
|||||||
class="btn-outline"
|
class="btn-outline"
|
||||||
aria-label={`Manage attendees for ${event.name}`}
|
aria-label={`Manage attendees for ${event.name}`}
|
||||||
style="justify-content: center; min-height: 38px; font-size: 0.85rem;"
|
style="justify-content: center; min-height: 38px; font-size: 0.85rem;"
|
||||||
data-on-click={`@get("/api/events/${event.id}/attendees")`}
|
|
||||||
onclick={`openAttendeesDrawer('${event.id}', '${
|
onclick={`openAttendeesDrawer('${event.id}', '${
|
||||||
event.name.replace(/'/g, "\\'")
|
event.name.replace(/'/g, "\\'")
|
||||||
}')`}
|
}')`}
|
||||||
@ -204,7 +211,6 @@ export const EventCockpitDeckFragment = (
|
|||||||
class="btn-outline"
|
class="btn-outline"
|
||||||
aria-label={`Add 5 seats to ${event.name}`}
|
aria-label={`Add 5 seats to ${event.name}`}
|
||||||
style="justify-content: center; min-height: 38px; font-size: 0.85rem;"
|
style="justify-content: center; min-height: 38px; font-size: 0.85rem;"
|
||||||
data-on-click={`@post("/api/events/${event.id}/expand")`}
|
|
||||||
onclick={`expandSeats('${event.id}', 5)`}
|
onclick={`expandSeats('${event.id}', 5)`}
|
||||||
>
|
>
|
||||||
+5 Seats
|
+5 Seats
|
||||||
@ -214,17 +220,15 @@ export const EventCockpitDeckFragment = (
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-outline"
|
class="btn-outline"
|
||||||
aria-label={`Rotate Credentials for ${event.name}`}
|
aria-label={`Rotate PIN for ${event.name}`}
|
||||||
style="justify-content: center; min-height: 38px; font-size: 0.82rem; padding: 0 0.25rem;"
|
style="justify-content: center; min-height: 38px; font-size: 0.82rem; padding: 0 0.25rem;"
|
||||||
data-on-click={`@post("/api/events/${event.id}/rotate-pin")`}
|
|
||||||
onclick={`rotatePin('${event.id}')`}
|
onclick={`rotatePin('${event.id}')`}
|
||||||
>
|
>
|
||||||
🔄 Rotate Credentials
|
🔄 Rotate PIN
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-outline extend-event-btn"
|
class="btn-outline extend-event-btn"
|
||||||
data-on-click={`@post("/api/events/${event.id}/extend")`}
|
|
||||||
data-event-id={event.id}
|
data-event-id={event.id}
|
||||||
aria-label={`Extend lifespan by 1 hour for ${event.name}`}
|
aria-label={`Extend lifespan by 1 hour for ${event.name}`}
|
||||||
style="justify-content: center; min-height: 38px; font-size: 0.82rem; padding: 0 0.25rem;"
|
style="justify-content: center; min-height: 38px; font-size: 0.82rem; padding: 0 0.25rem;"
|
||||||
@ -234,7 +238,6 @@ export const EventCockpitDeckFragment = (
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-danger end-event-btn"
|
class="btn-danger end-event-btn"
|
||||||
data-on-click={`@post("/api/events/${event.id}/end")`}
|
|
||||||
data-event-id={event.id}
|
data-event-id={event.id}
|
||||||
aria-label={`End event ${event.name} and revoke all attendees`}
|
aria-label={`End event ${event.name} and revoke all attendees`}
|
||||||
style="justify-content: center; min-height: 38px; font-size: 0.82rem; padding: 0 0.25rem;"
|
style="justify-content: center; min-height: 38px; font-size: 0.82rem; padding: 0 0.25rem;"
|
||||||
@ -279,7 +282,6 @@ export const EventCockpitDeckFragment = (
|
|||||||
type="button"
|
type="button"
|
||||||
class="btn-outline"
|
class="btn-outline"
|
||||||
style="padding: 0.25rem 0.5rem; font-size: 0.8rem; min-height: 28px;"
|
style="padding: 0.25rem 0.5rem; font-size: 0.8rem; min-height: 28px;"
|
||||||
data-on-click={`@get("/api/events/${event.id}/attendees")`}
|
|
||||||
onclick={`openAttendeesDrawer('${event.id}', '${
|
onclick={`openAttendeesDrawer('${event.id}', '${
|
||||||
event.name.replace(/'/g, "\\'")
|
event.name.replace(/'/g, "\\'")
|
||||||
}')`}
|
}')`}
|
||||||
@ -289,7 +291,6 @@ export const EventCockpitDeckFragment = (
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-outline extend-event-btn"
|
class="btn-outline extend-event-btn"
|
||||||
data-on-click={`@post("/api/events/${event.id}/extend")`}
|
|
||||||
data-event-id={event.id}
|
data-event-id={event.id}
|
||||||
style="padding: 0.25rem 0.5rem; font-size: 0.8rem; min-height: 28px;"
|
style="padding: 0.25rem 0.5rem; font-size: 0.8rem; min-height: 28px;"
|
||||||
>
|
>
|
||||||
@ -301,6 +302,57 @@ export const EventCockpitDeckFragment = (
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Collapsible Expired Events Section */}
|
||||||
|
{expiredPasses.length > 0 && (
|
||||||
|
<details style="margin-top: 1.5rem; border: 1px solid var(--border-subtle); border-radius: var(--radius-md); padding: 0.75rem 1rem; background: var(--surface-muted);">
|
||||||
|
<summary style="font-weight: 600; font-size: 0.95rem; color: var(--text-secondary); cursor: pointer;">
|
||||||
|
📁 Expired Events ({expiredPasses.length})
|
||||||
|
</summary>
|
||||||
|
<div style="margin-top: 1rem; display: flex; flex-direction: column; gap: 0.75rem;">
|
||||||
|
{expiredPasses.map((event) => (
|
||||||
|
<div
|
||||||
|
key={event.id}
|
||||||
|
class="card"
|
||||||
|
style="padding: 0.75rem 1rem; margin: 0; display: flex; justify-content: space-between; align-items: center; opacity: 0.85; border-left: 3px solid var(--text-muted); flex-wrap: wrap; gap: 0.75rem;"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.25rem;">
|
||||||
|
<strong style="color: var(--text-primary); font-size: 0.95rem;">
|
||||||
|
{event.name}
|
||||||
|
</strong>
|
||||||
|
<span class="badge badge-secondary">Expired</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 0.8rem; color: var(--text-secondary);">
|
||||||
|
Expired {new Date(event.expires_at).toLocaleDateString()} ·
|
||||||
|
{" "}
|
||||||
|
{event.seats_claimed} claimed seats
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-outline extend-event-btn"
|
||||||
|
data-event-id={event.id}
|
||||||
|
style="padding: 0.3rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
|
||||||
|
>
|
||||||
|
🔄 +1h Reopen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-danger end-event-btn"
|
||||||
|
data-event-id={event.id}
|
||||||
|
style="padding: 0.3rem 0.65rem; font-size: 0.8rem; min-height: 32px;"
|
||||||
|
>
|
||||||
|
End Event
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
|
||||||
<style>{EVENT_COCKPIT_CSS}</style>
|
<style>{EVENT_COCKPIT_CSS}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
export const WorkshopPassDrawerFragment = ({ apps }: { apps: any[] }) => {
|
export const WorkshopPassDrawerFragment = ({
|
||||||
|
apps = [],
|
||||||
|
}: {
|
||||||
|
apps?: any[];
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div id="tabWorkshopPass" style="display: none;">
|
<div id="tabWorkshopPass" style="display: none;">
|
||||||
<div id="eventCreateState" style="display: block;">
|
<div id="eventCreateState" style="display: block;">
|
||||||
@ -146,10 +150,10 @@ export const WorkshopPassDrawerFragment = ({ apps }: { apps: any[] }) => {
|
|||||||
<input type="hidden" id="eventLifespanHours" value="3" />
|
<input type="hidden" id="eventLifespanHours" value="3" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Custom Codes Accordion */}
|
{/* Custom Codes Accordion (Optional) */}
|
||||||
<details style="margin-bottom: 1.5rem; background: var(--surface-muted); padding: 0.75rem 1rem; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle);">
|
<details style="margin-bottom: 1.5rem; background: var(--surface-muted); padding: 0.75rem 1rem; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle);">
|
||||||
<summary style="font-size: 0.875rem; font-weight: 600; color: var(--text-primary); cursor: pointer;">
|
<summary style="font-size: 0.875rem; font-weight: 600; color: var(--text-primary); cursor: pointer;">
|
||||||
▸ Custom Vanity Slug & Custom PIN Code (Optional)
|
Custom Vanity Slug & PIN Code (Optional)
|
||||||
</summary>
|
</summary>
|
||||||
<div style="margin-top: 0.75rem; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
<div style="margin-top: 0.75rem; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
||||||
<div>
|
<div>
|
||||||
@ -201,7 +205,7 @@ export const WorkshopPassDrawerFragment = ({ apps }: { apps: any[] }) => {
|
|||||||
{/* Event Launch Success State */}
|
{/* Event Launch Success State */}
|
||||||
<div
|
<div
|
||||||
id="eventHandoffState"
|
id="eventHandoffState"
|
||||||
style="display: none; margin-top: 1.5rem; padding: 1.25rem; background: var(--surface-card); border: 2px solid var(--success); border-radius: var(--radius-md); box-shadow: var(--shadow-md);"
|
style="display: none; padding: 1.25rem; background: var(--surface-card); border: 2px solid var(--success); border-radius: var(--radius-md); box-shadow: var(--shadow-md);"
|
||||||
>
|
>
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
|
||||||
<div style="display: flex; align-items: center; gap: 0.5rem; flex: 1; min-width: 0;">
|
<div style="display: flex; align-items: center; gap: 0.5rem; flex: 1; min-width: 0;">
|
||||||
@ -301,14 +305,24 @@ export const WorkshopPassDrawerFragment = ({ apps }: { apps: any[] }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<div style="display: flex; gap: 0.75rem;">
|
||||||
type="button"
|
<button
|
||||||
class="btn-outline"
|
type="button"
|
||||||
style="width: 100%; min-height: 38px; justify-content: center; font-size: 0.85rem;"
|
class="btn-primary"
|
||||||
onclick="closeEventHandoffModal()"
|
style="flex: 1; min-height: 38px; justify-content: center; font-size: 0.85rem;"
|
||||||
>
|
onclick="resetWorkshopForm()"
|
||||||
OK
|
>
|
||||||
</button>
|
Create Another
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-outline"
|
||||||
|
style="flex: 1; min-height: 38px; justify-content: center; font-size: 0.85rem;"
|
||||||
|
onclick="closeEventHandoffModal()"
|
||||||
|
>
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,204 +7,226 @@ export const DirectPassDrawerFragment = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div id="tabDirectPass">
|
<div id="tabDirectPass">
|
||||||
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0 0 1rem 0;">
|
{/* Creation State */}
|
||||||
Mint an isolated, scoped child session for 1-click magic links, CLI
|
<div id="singleSessionCreateState" style="display: block;">
|
||||||
tools, team members, or AI assistants without exposing your primary
|
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0 0 1rem 0;">
|
||||||
passkeys.
|
Mint an isolated, scoped child session for 1-click magic links, CLI
|
||||||
</p>
|
tools, team members, or AI assistants without exposing your primary
|
||||||
|
passkeys.
|
||||||
|
</p>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
id="delegateForm"
|
id="delegateForm"
|
||||||
onsubmit="handleDelegateSession(event)"
|
onsubmit="handleDelegateSession(event)"
|
||||||
style="margin-top: 1rem;"
|
style="margin-top: 1rem;"
|
||||||
>
|
|
||||||
{/* Label Input */}
|
|
||||||
<div style="margin-bottom: 1.25rem;">
|
|
||||||
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);">
|
|
||||||
Session Label / Purpose *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="delegateLabel"
|
|
||||||
placeholder="e.g. Friend Demo Pass, Antigravity Assistant, CI/CD Runner, Terminal CLI"
|
|
||||||
required
|
|
||||||
style="width: 100%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Direct 1-Click Destination */}
|
|
||||||
<div style="margin-bottom: 1.25rem;">
|
|
||||||
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);">
|
|
||||||
1-Click Destination App (Optional)
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="delegateTargetApp"
|
|
||||||
onchange="handleTargetAppChange(this.value)"
|
|
||||||
style="width: 100%; padding: 0.5rem 0.75rem; background: var(--surface-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); color: var(--text-primary); font-size: 0.9rem;"
|
|
||||||
>
|
|
||||||
<option value="">Auth-Yes Central Dashboard (Default)</option>
|
|
||||||
{apps.map((app) => (
|
|
||||||
<option value={app.name} key={app.id}>
|
|
||||||
{app.name} ({app.domain || "Internal App"})
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<p style="font-size: 0.75rem; color: var(--text-muted); margin: 0.35rem 0 0 0;">
|
|
||||||
Selecting an app scopes the pass and automatically redirects the
|
|
||||||
recipient to that app upon 1-click redemption.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Lifespan Presets */}
|
|
||||||
<div style="margin-bottom: 1.25rem;">
|
|
||||||
<label style="display: block; font-weight: 600; margin-bottom: 0.45rem; font-size: 0.875rem; color: var(--text-secondary);">
|
|
||||||
Lifespan & Auto-Expiration
|
|
||||||
</label>
|
|
||||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="pill-btn lifespan-pill active"
|
|
||||||
data-hours="1"
|
|
||||||
onclick="selectLifespan(this, 1)"
|
|
||||||
>
|
|
||||||
⚡ 1 Hour (Quick Task)
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="pill-btn lifespan-pill"
|
|
||||||
data-hours="12"
|
|
||||||
onclick="selectLifespan(this, 12)"
|
|
||||||
>
|
|
||||||
🛠️ 12 Hours (Work Session)
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="pill-btn lifespan-pill"
|
|
||||||
data-hours="24"
|
|
||||||
onclick="selectLifespan(this, 24)"
|
|
||||||
>
|
|
||||||
📅 24 Hours
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="pill-btn lifespan-pill"
|
|
||||||
data-hours="168"
|
|
||||||
onclick="selectLifespan(this, 168)"
|
|
||||||
>
|
|
||||||
🗓️ 7 Days
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<input type="hidden" id="delegateHours" value="1" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Access Mode Presets */}
|
|
||||||
<div style="margin-bottom: 1.25rem;">
|
|
||||||
<label style="display: block; font-weight: 600; margin-bottom: 0.45rem; font-size: 0.875rem; color: var(--text-secondary);">
|
|
||||||
Access Mode & Permissions
|
|
||||||
</label>
|
|
||||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="pill-btn mode-pill active"
|
|
||||||
data-mode="read"
|
|
||||||
onclick="selectMode(this, 'read')"
|
|
||||||
>
|
|
||||||
👁️ Read-Only Viewer
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="pill-btn mode-pill"
|
|
||||||
data-mode="standard"
|
|
||||||
onclick="selectMode(this, 'standard')"
|
|
||||||
>
|
|
||||||
⚙️ Standard Worker
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="pill-btn mode-pill"
|
|
||||||
data-mode="custom"
|
|
||||||
onclick="selectMode(this, 'custom')"
|
|
||||||
>
|
|
||||||
🎛️ Custom Scopes
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<input type="hidden" id="delegateMode" value="read" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Granular Scope Matrix */}
|
|
||||||
<div
|
|
||||||
id="customScopeMatrix"
|
|
||||||
style="display: none; margin-bottom: 1.5rem; background: var(--surface-muted); padding: 1rem; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle);"
|
|
||||||
>
|
>
|
||||||
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem; font-size: 0.85rem; color: var(--text-primary);">
|
{/* Label Input */}
|
||||||
Select Allowed Custom Scopes:
|
<div style="margin-bottom: 1.25rem;">
|
||||||
</label>
|
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);">
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem;">
|
Session Label / Purpose *
|
||||||
<label style="display: flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; cursor: pointer;">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="scope-checkbox"
|
|
||||||
value="read:events"
|
|
||||||
/>
|
|
||||||
<span>read:events</span>
|
|
||||||
</label>
|
</label>
|
||||||
<label style="display: flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; cursor: pointer;">
|
<input
|
||||||
<input
|
type="text"
|
||||||
type="checkbox"
|
id="delegateLabel"
|
||||||
class="scope-checkbox"
|
placeholder="e.g. Friend Demo Pass, Antigravity Assistant, CI/CD Runner, Terminal CLI"
|
||||||
value="write:events"
|
required
|
||||||
/>
|
style="width: 100%;"
|
||||||
<span>write:events</span>
|
/>
|
||||||
</label>
|
|
||||||
<label style="display: flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; cursor: pointer;">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="scope-checkbox"
|
|
||||||
value="read:sessions"
|
|
||||||
/>
|
|
||||||
<span>read:sessions</span>
|
|
||||||
</label>
|
|
||||||
<label style="display: flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; cursor: pointer;">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="scope-checkbox"
|
|
||||||
value="write:sessions"
|
|
||||||
/>
|
|
||||||
<span>write:sessions</span>
|
|
||||||
</label>
|
|
||||||
{isAdmin && (
|
|
||||||
<label style="display: flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; cursor: pointer; color: var(--danger-text);">
|
|
||||||
<input type="checkbox" class="scope-checkbox" value="admin" />
|
|
||||||
<span>admin (Full Control)</span>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; gap: 0.75rem;">
|
{/* Direct 1-Click Destination */}
|
||||||
<button
|
<div style="margin-bottom: 1.25rem;">
|
||||||
type="submit"
|
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);">
|
||||||
id="submitDelegateBtn"
|
1-Click Destination App (Optional)
|
||||||
class="btn-primary"
|
</label>
|
||||||
style="min-height: 42px;"
|
<select
|
||||||
>
|
id="delegateTargetApp"
|
||||||
Create Session
|
onchange="handleTargetAppChange(this.value)"
|
||||||
</button>
|
style="width: 100%; padding: 0.5rem 0.75rem; background: var(--surface-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); color: var(--text-primary); font-size: 0.9rem;"
|
||||||
<button
|
>
|
||||||
type="button"
|
<option value="">Auth-Yes Central Dashboard (Default)</option>
|
||||||
class="btn-outline"
|
{apps.map((app) => (
|
||||||
onclick="closeDelegateDrawer()"
|
<option value={app.name} key={app.id}>
|
||||||
style="min-height: 42px;"
|
{app.name} ({app.domain || "Internal App"})
|
||||||
>
|
</option>
|
||||||
Cancel
|
))}
|
||||||
</button>
|
</select>
|
||||||
</div>
|
<p style="font-size: 0.75rem; color: var(--text-muted); margin: 0.35rem 0 0 0;">
|
||||||
</form>
|
Selecting an app scopes the pass and automatically redirects the
|
||||||
|
recipient to that app upon 1-click redemption.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Hand-Off Card */}
|
{/* Lifespan Presets */}
|
||||||
|
<div style="margin-bottom: 1.25rem;">
|
||||||
|
<label style="display: block; font-weight: 600; margin-bottom: 0.45rem; font-size: 0.875rem; color: var(--text-secondary);">
|
||||||
|
Lifespan & Auto-Expiration
|
||||||
|
</label>
|
||||||
|
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pill-btn lifespan-pill active"
|
||||||
|
data-hours="1"
|
||||||
|
onclick="selectLifespan(this, 1)"
|
||||||
|
>
|
||||||
|
⚡ 1 Hour (Quick Task)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pill-btn lifespan-pill"
|
||||||
|
data-hours="12"
|
||||||
|
onclick="selectLifespan(this, 12)"
|
||||||
|
>
|
||||||
|
🛠️ 12 Hours (Work Session)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pill-btn lifespan-pill"
|
||||||
|
data-hours="24"
|
||||||
|
onclick="selectLifespan(this, 24)"
|
||||||
|
>
|
||||||
|
📅 24 Hours
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pill-btn lifespan-pill"
|
||||||
|
data-hours="168"
|
||||||
|
onclick="selectLifespan(this, 168)"
|
||||||
|
>
|
||||||
|
🗓️ 7 Days
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" id="delegateHours" value="1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Access Mode Presets */}
|
||||||
|
<div style="margin-bottom: 1.25rem;">
|
||||||
|
<label style="display: block; font-weight: 600; margin-bottom: 0.45rem; font-size: 0.875rem; color: var(--text-secondary);">
|
||||||
|
Access Mode & Permissions
|
||||||
|
</label>
|
||||||
|
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pill-btn mode-pill active"
|
||||||
|
data-mode="read"
|
||||||
|
onclick="selectMode(this, 'read')"
|
||||||
|
>
|
||||||
|
👁️ Read-Only Viewer
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pill-btn mode-pill"
|
||||||
|
data-mode="standard"
|
||||||
|
onclick="selectMode(this, 'standard')"
|
||||||
|
>
|
||||||
|
⚙️ Standard Worker
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pill-btn mode-pill"
|
||||||
|
data-mode="custom"
|
||||||
|
onclick="selectMode(this, 'custom')"
|
||||||
|
>
|
||||||
|
🎛️ Custom Scopes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" id="delegateMode" value="read" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progressive Disclosure for Custom App Scopes */}
|
||||||
|
<details
|
||||||
|
id="customScopesSection"
|
||||||
|
style="margin-bottom: 1.25rem; border: 1px solid var(--border-subtle); border-radius: var(--radius-md); padding: 0.75rem 1rem; background: var(--surface-muted);"
|
||||||
|
>
|
||||||
|
<summary style="cursor: pointer; font-weight: 600; font-size: 0.875rem; color: var(--text-primary);">
|
||||||
|
Customize App Permissions & Scopes (Optional)
|
||||||
|
</summary>
|
||||||
|
<div style="margin-top: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem;">
|
||||||
|
<p style="margin: 0 0 0.5rem 0; font-size: 0.8rem; color: var(--text-secondary);">
|
||||||
|
Select which subsidiary microservices and IAM capabilities this
|
||||||
|
delegated token is permitted to access:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.5rem;">
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; color: var(--text-primary); cursor: pointer;">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="scope-checkbox"
|
||||||
|
value="read:audit"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
Read Audit Ledger
|
||||||
|
</label>
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; color: var(--text-primary); cursor: pointer;">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="scope-checkbox"
|
||||||
|
value="read:users"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
Read Users List
|
||||||
|
</label>
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; color: var(--text-primary); cursor: pointer;">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="scope-checkbox"
|
||||||
|
value="read:apps"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
Read Apps Registry
|
||||||
|
</label>
|
||||||
|
{apps.map((app) => (
|
||||||
|
<label
|
||||||
|
key={app.id}
|
||||||
|
style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; color: var(--text-primary); cursor: pointer;"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="scope-checkbox"
|
||||||
|
value={`app:${app.name}`}
|
||||||
|
/>
|
||||||
|
{app.name} ({app.domain || "Internal"})
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
{isAdmin && (
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; color: var(--danger-text); cursor: pointer;">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="scope-checkbox"
|
||||||
|
value="admin"
|
||||||
|
/>
|
||||||
|
admin (Full Control)
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 0.75rem;">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
id="submitDelegateBtn"
|
||||||
|
class="btn-primary"
|
||||||
|
style="min-height: 42px;"
|
||||||
|
>
|
||||||
|
Create Session
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-outline"
|
||||||
|
onclick="closeDelegateDrawer()"
|
||||||
|
style="min-height: 42px;"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hand-Off Card (Revealed upon generation) */}
|
||||||
<div
|
<div
|
||||||
id="handoffModal"
|
id="handoffModal"
|
||||||
style="display: none; margin-top: 1.5rem; padding: 1.25rem; background: var(--surface-card); border: 1px solid var(--primary); border-radius: var(--radius-md); box-shadow: var(--shadow-md);"
|
style="display: none; padding: 1.25rem; background: var(--surface-card); border: 1px solid var(--primary); border-radius: var(--radius-md); box-shadow: var(--shadow-md);"
|
||||||
>
|
>
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
|
||||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||||
@ -222,7 +244,7 @@ export const DirectPassDrawerFragment = ({
|
|||||||
below.
|
below.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style="display: flex; flex-direction: column; gap: 0.75rem; margin-bottom: 1rem;">
|
<div style="display: flex; flex-direction: column; gap: 0.75rem; margin-bottom: 1.25rem;">
|
||||||
{/* 1-Click Magic Link */}
|
{/* 1-Click Magic Link */}
|
||||||
<div>
|
<div>
|
||||||
<label style="display: block; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.25rem;">
|
<label style="display: block; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.25rem;">
|
||||||
@ -236,7 +258,7 @@ export const DirectPassDrawerFragment = ({
|
|||||||
</code>
|
</code>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-primary"
|
class="btn-outline"
|
||||||
style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;"
|
style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;"
|
||||||
data-ignore
|
data-ignore
|
||||||
onclick="copyHandoff('magic')"
|
onclick="copyHandoff('magic')"
|
||||||
@ -293,14 +315,24 @@ export const DirectPassDrawerFragment = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<div style="display: flex; gap: 0.75rem;">
|
||||||
type="button"
|
<button
|
||||||
class="btn-outline"
|
type="button"
|
||||||
style="width: 100%; min-height: 38px; justify-content: center; font-size: 0.85rem;"
|
class="btn-primary"
|
||||||
onclick="closeHandoffModal()"
|
style="flex: 1; min-height: 38px; justify-content: center; font-size: 0.85rem;"
|
||||||
>
|
onclick="resetDirectPassForm()"
|
||||||
Done (Session is Active)
|
>
|
||||||
</button>
|
Create Another
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-outline"
|
||||||
|
style="flex: 1; min-height: 38px; justify-content: center; font-size: 0.85rem;"
|
||||||
|
onclick="closeHandoffModal()"
|
||||||
|
>
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,6 +6,10 @@ import {
|
|||||||
SessionDeckFragment,
|
SessionDeckFragment,
|
||||||
SessionTableFragment,
|
SessionTableFragment,
|
||||||
} from "./fragments.tsx";
|
} from "./fragments.tsx";
|
||||||
|
import {
|
||||||
|
EventCockpitDeckFragment,
|
||||||
|
WorkshopPassDrawerFragment,
|
||||||
|
} from "../events/fragments.tsx";
|
||||||
|
|
||||||
Deno.test("[Sessions] GET /dashboard unauthenticated redirects to /login", async () => {
|
Deno.test("[Sessions] GET /dashboard unauthenticated redirects to /login", async () => {
|
||||||
const req = new Request("http://localhost/dashboard");
|
const req = new Request("http://localhost/dashboard");
|
||||||
@ -80,10 +84,64 @@ Deno.test("[Sessions] DirectPassDrawerFragment renders options and form", () =>
|
|||||||
];
|
];
|
||||||
const html = <DirectPassDrawerFragment apps={mockApps} isAdmin />;
|
const html = <DirectPassDrawerFragment apps={mockApps} isAdmin />;
|
||||||
assertStringIncludes(String(html), "Create Session");
|
assertStringIncludes(String(html), "Create Session");
|
||||||
|
assertStringIncludes(String(html), "singleSessionCreateState");
|
||||||
|
assertStringIncludes(String(html), "handoffModal");
|
||||||
|
assertStringIncludes(String(html), "Create Another");
|
||||||
|
assertStringIncludes(String(html), "OK");
|
||||||
|
assertStringIncludes(
|
||||||
|
String(html),
|
||||||
|
"Customize App Permissions & Scopes (Optional)",
|
||||||
|
);
|
||||||
assertStringIncludes(String(html), "ed-droid");
|
assertStringIncludes(String(html), "ed-droid");
|
||||||
assertStringIncludes(String(html), "1-Click Destination App");
|
assertStringIncludes(String(html), "1-Click Destination App");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test("[Sessions] WorkshopPassDrawerFragment renders event options and form", () => {
|
||||||
|
const mockApps = [
|
||||||
|
{ id: "app-1", name: "ed-droid", domain: "ed.atyg.org" },
|
||||||
|
];
|
||||||
|
const html = <WorkshopPassDrawerFragment apps={mockApps} />;
|
||||||
|
assertStringIncludes(String(html), "Create Event");
|
||||||
|
assertStringIncludes(String(html), "eventCreateState");
|
||||||
|
assertStringIncludes(String(html), "eventHandoffState");
|
||||||
|
assertStringIncludes(String(html), "Create Another");
|
||||||
|
assertStringIncludes(String(html), "OK");
|
||||||
|
assertStringIncludes(
|
||||||
|
String(html),
|
||||||
|
"Custom Vanity Slug & PIN Code (Optional)",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("[Sessions] EventCockpitDeckFragment handles active and expired events", () => {
|
||||||
|
const activePass = {
|
||||||
|
id: "ev-1",
|
||||||
|
name: "Active Hackathon",
|
||||||
|
pin_code: "123-456",
|
||||||
|
slug: "active-hack",
|
||||||
|
seats_claimed: 5,
|
||||||
|
max_seats: 50,
|
||||||
|
expires_at: new Date(Date.now() + 3600000).toISOString(),
|
||||||
|
};
|
||||||
|
const expiredPass = {
|
||||||
|
id: "ev-2",
|
||||||
|
name: "Old Workshop",
|
||||||
|
pin_code: "789-012",
|
||||||
|
slug: "old-workshop",
|
||||||
|
seats_claimed: 10,
|
||||||
|
max_seats: 25,
|
||||||
|
expires_at: new Date(Date.now() - 3600000).toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const html = (
|
||||||
|
<EventCockpitDeckFragment eventPasses={[activePass, expiredPass]} />
|
||||||
|
);
|
||||||
|
assertStringIncludes(String(html), "Active Hackathon");
|
||||||
|
assertStringIncludes(String(html), "Rotate PIN");
|
||||||
|
assertStringIncludes(String(html), "Expired Events (1)");
|
||||||
|
assertStringIncludes(String(html), "Old Workshop");
|
||||||
|
assertStringIncludes(String(html), "+1h Reopen");
|
||||||
|
});
|
||||||
|
|
||||||
Deno.test("[Sessions] ScopeModalFragment renders selectable permission matrix", () => {
|
Deno.test("[Sessions] ScopeModalFragment renders selectable permission matrix", () => {
|
||||||
const html = <ScopeModalFragment apps={[]} />;
|
const html = <ScopeModalFragment apps={[]} />;
|
||||||
assertStringIncludes(String(html), "Update Scopes for:");
|
assertStringIncludes(String(html), "Update Scopes for:");
|
||||||
|
|||||||
@ -34,11 +34,11 @@ export const SessionsPageFragment = ({
|
|||||||
id="status-banner"
|
id="status-banner"
|
||||||
role="status"
|
role="status"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
style="display: none; margin-bottom: 1rem; padding: 0.75rem 1rem; border-radius: var(--radius-md); font-size: 0.9rem;"
|
style="display: none; position: fixed; top: 1.25rem; right: 1.25rem; z-index: 99999; min-width: 280px; max-width: 480px; padding: 0.85rem 1.25rem; border-radius: var(--radius-md); box-shadow: 0 10px 25px -5px rgba(0,0,0,0.35); font-size: 0.9rem; font-weight: 600;"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Top Action Bar with Sticky Positioning */}
|
{/* Top Action Bar with Sticky Docking directly beneath main navbar (top: 57px) */}
|
||||||
<div style="position: sticky; top: 0; z-index: 40; background: var(--surface-bg, #0f172a); padding: 0.75rem 0; margin-bottom: 1.5rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; border-bottom: 1px solid var(--border-subtle);">
|
<div style="position: sticky; top: 57px; z-index: 30; background: var(--surface-canvas); padding: 0.85rem 0; margin-bottom: 1.5rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; border-bottom: 1px solid var(--border-subtle);">
|
||||||
<div>
|
<div>
|
||||||
<h1 style="font-size: 1.75rem; font-weight: 700; margin: 0 0 0.25rem 0; color: var(--text-primary);">
|
<h1 style="font-size: 1.75rem; font-weight: 700; margin: 0 0 0.25rem 0; color: var(--text-primary);">
|
||||||
Sessions & Events
|
Sessions & Events
|
||||||
|
|||||||
@ -2,77 +2,77 @@
|
|||||||
|
|
||||||
### Sticky Navigation Header
|
### Sticky Navigation Header
|
||||||
|
|
||||||
- [ ] `sticky-sub-header-docking`: Dock the sticky `Sessions & Events`
|
- [x] `sticky-sub-header-docking`: Dock the sticky `Sessions & Events`
|
||||||
sub-header directly beneath the main navigation bar so it scrolls smoothly
|
sub-header directly beneath the main navigation bar so it scrolls smoothly
|
||||||
without overlapping or covering top navigation elements.
|
without overlapping or covering top navigation elements.
|
||||||
- [ ] `sticky-sub-header-opaque-bg`: Ensure the sticky sub-header has an opaque
|
- [x] `sticky-sub-header-opaque-bg`: Ensure the sticky sub-header has an opaque
|
||||||
background so underlying cards do not bleed through on scroll.
|
background so underlying cards do not bleed through on scroll.
|
||||||
|
|
||||||
### Single Session Creation (DirectPassDrawer)
|
### Single Session Creation (DirectPassDrawer)
|
||||||
|
|
||||||
- [ ] `single-session-form-hiding`: Hide the single session creation form upon
|
- [x] `single-session-form-hiding`: Hide the single session creation form upon
|
||||||
minting so only the completed handoff card is displayed.
|
minting so only the completed handoff card is displayed.
|
||||||
- [ ] `single-session-cancel-button-removal`: Remove the Cancel button from the
|
- [x] `single-session-cancel-button-removal`: Remove the Cancel button from the
|
||||||
view once a single session has been created.
|
view once a single session has been created.
|
||||||
- [ ] `single-session-copy-button-styling`: Style the 1-Click Link copy button
|
- [x] `single-session-copy-button-styling`: Style the 1-Click Link copy button
|
||||||
using the standard outline style matching other copy actions.
|
using the standard outline style matching other copy actions.
|
||||||
- [ ] `single-session-submit-button-label`: Label the single session drawer
|
- [x] `single-session-submit-button-label`: Label the single session drawer
|
||||||
submit button as "Create Session".
|
submit button as "Create Session".
|
||||||
- [ ] `single-session-handoff-ok-button`: Label the single session handoff
|
- [x] `single-session-handoff-ok-button`: Label the single session handoff
|
||||||
completion button as "OK".
|
completion button as "OK".
|
||||||
|
|
||||||
### Multi-Claim Event Creation (WorkshopDrawer)
|
### Multi-Claim Event Creation (WorkshopDrawer)
|
||||||
|
|
||||||
- [ ] `event-submit-button-label`: Label the multi-claim event drawer submit
|
- [x] `event-submit-button-label`: Label the multi-claim event drawer submit
|
||||||
button as "Create Event".
|
button as "Create Event".
|
||||||
- [ ] `event-handoff-ok-button`: Label the multi-claim event handoff completion
|
- [x] `event-handoff-ok-button`: Label the multi-claim event handoff completion
|
||||||
button as "OK".
|
button as "OK".
|
||||||
|
|
||||||
### Dropdown Accordion Headers
|
### Dropdown Accordion Headers
|
||||||
|
|
||||||
- [ ] `single-session-accordion-arrow-fix`: Eliminate the broken double arrow
|
- [x] `single-session-accordion-arrow-fix`: Eliminate the broken double arrow
|
||||||
(`> >`) from the single session optional settings dropdown header.
|
(`> >`) from the single session optional settings dropdown header.
|
||||||
- [ ] `single-session-accordion-label`: Label the single session optional
|
- [x] `single-session-accordion-label`: Label the single session optional
|
||||||
settings dropdown header as "Customize App Permissions & Scopes
|
settings dropdown header as "Customize App Permissions & Scopes
|
||||||
(Optional)".
|
(Optional)".
|
||||||
- [ ] `workshop-accordion-arrow-fix`: Eliminate the broken double arrow (`> >`)
|
- [x] `workshop-accordion-arrow-fix`: Eliminate the broken double arrow (`> >`)
|
||||||
from the workshop pass optional settings dropdown header.
|
from the workshop pass optional settings dropdown header.
|
||||||
- [ ] `workshop-accordion-label`: Label the workshop pass optional settings
|
- [x] `workshop-accordion-label`: Label the workshop pass optional settings
|
||||||
dropdown header as "Custom Vanity Slug & PIN Code (Optional)".
|
dropdown header as "Custom Vanity Slug & PIN Code (Optional)".
|
||||||
|
|
||||||
### Rapid Creation & Tab State
|
### Rapid Creation & Tab State
|
||||||
|
|
||||||
- [ ] `single-session-create-another-button`: Add a "Create Another" button on
|
- [x] `single-session-create-another-button`: Add a "Create Another" button on
|
||||||
the single session handoff card that clears the form and starts a new
|
the single session handoff card that clears the form and starts a new
|
||||||
session in one click.
|
session in one click.
|
||||||
- [ ] `event-create-another-button`: Add a "Create Another" button on the event
|
- [x] `event-create-another-button`: Add a "Create Another" button on the event
|
||||||
handoff card that clears the form and starts a new event in one click.
|
handoff card that clears the form and starts a new event in one click.
|
||||||
- [ ] `handoff-tab-lock-state`: Disable switching between Single Session and
|
- [x] `handoff-tab-lock-state`: Disable switching between Single Session and
|
||||||
Multi-Claim Event tabs while viewing a completed handoff card.
|
Multi-Claim Event tabs while viewing a completed handoff card.
|
||||||
- [ ] `handoff-tab-unlock-on-reset`: Re-enable tab switching when the user
|
- [x] `handoff-tab-unlock-on-reset`: Re-enable tab switching when the user
|
||||||
clicks "Create Another" or closes the drawer.
|
clicks "Create Another" or closes the drawer.
|
||||||
|
|
||||||
### Expired Events Management
|
### Expired Events Management
|
||||||
|
|
||||||
- [ ] `expired-events-query-split`: Separate event passes into active events and
|
- [x] `expired-events-query-split`: Separate event passes into active events and
|
||||||
expired events based on their expiration timestamp and status.
|
expired events based on their expiration timestamp and status.
|
||||||
- [ ] `expired-events-collapsible-accordion`: Organize expired events into a
|
- [x] `expired-events-collapsible-accordion`: Organize expired events into a
|
||||||
dedicated collapsible `<details>` section titled "📁 Expired Events (N)"
|
dedicated collapsible `<details>` section titled "📁 Expired Events (N)"
|
||||||
below active events.
|
below active events.
|
||||||
- [ ] `expired-events-card-styling`: Display expired event passes with distinct
|
- [x] `expired-events-card-styling`: Display expired event passes with distinct
|
||||||
muted styling and expired status badges.
|
muted styling and expired status badges.
|
||||||
- [ ] `expired-events-1click-reopen`: Provide a one-click "+1h Reopen" button on
|
- [x] `expired-events-1click-reopen`: Provide a one-click "+1h Reopen" button on
|
||||||
naturally expired events to add time and reactivate them.
|
naturally expired events to add time and reactivate them.
|
||||||
|
|
||||||
### Guest Drawer Telemetry & Sync
|
### Guest Drawer Telemetry & Sync
|
||||||
|
|
||||||
- [ ] `guest-drawer-telemetry-backend`: Fetch the last activity action, last
|
- [x] `guest-drawer-telemetry-backend`: Fetch the last activity action, last
|
||||||
activity timestamp, and agent flag for each attendee.
|
activity timestamp, and agent flag for each attendee.
|
||||||
- [ ] `guest-drawer-last-action-display`: Display the attendee's last recorded
|
- [x] `guest-drawer-last-action-display`: Display the attendee's last recorded
|
||||||
action and relative time on their guest card.
|
action and relative time on their guest card.
|
||||||
- [ ] `guest-drawer-client-badge`: Display a client icon (such as Web or CLI) on
|
- [x] `guest-drawer-client-badge`: Display a client icon (such as Web or CLI) on
|
||||||
each attendee card based on their recorded action.
|
each attendee card based on their recorded action.
|
||||||
- [ ] `guest-drawer-timer-sync-on-extend`: Dynamically update the countdown
|
- [x] `guest-drawer-timer-sync-on-extend`: Dynamically update the countdown
|
||||||
timer and expiration time in the open guest drawer when an event is
|
timer and expiration time in the open guest drawer when an event is
|
||||||
extended.
|
extended.
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user