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;
|
||||
banner.textContent = msg;
|
||||
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.color = isError ? "var(--danger-text)" : "var(--success-text)";
|
||||
banner.style.border = isError
|
||||
? "1px solid var(--danger-border)"
|
||||
: "1px solid var(--success-border)";
|
||||
setTimeout(() => {
|
||||
if (banner._timeout) clearTimeout(banner._timeout);
|
||||
banner._timeout = setTimeout(() => {
|
||||
banner.style.display = "none";
|
||||
}, 5000);
|
||||
}, 4000);
|
||||
}
|
||||
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() {
|
||||
const drawer = document.getElementById("delegateDrawer");
|
||||
if (!drawer) return;
|
||||
drawer.style.display = "block";
|
||||
drawer.scrollIntoView({ behavior: "smooth" });
|
||||
|
||||
const eventCreateState = document.getElementById("eventCreateState");
|
||||
if (eventCreateState) eventCreateState.style.display = "block";
|
||||
const eventHandoffState = document.getElementById("eventHandoffState");
|
||||
if (eventHandoffState) eventHandoffState.style.display = "none";
|
||||
const eventForm = document.getElementById("eventForm");
|
||||
if (eventForm) eventForm.reset();
|
||||
|
||||
resetDirectPassForm();
|
||||
resetWorkshopForm();
|
||||
switchDelegateTab("tabDirectPass");
|
||||
}
|
||||
globalThis.openDelegateDrawer = openDelegateDrawer;
|
||||
@ -36,6 +80,8 @@ globalThis.openDelegateDrawer = openDelegateDrawer;
|
||||
function closeDelegateDrawer() {
|
||||
const drawer = document.getElementById("delegateDrawer");
|
||||
if (drawer) drawer.style.display = "none";
|
||||
resetDirectPassForm();
|
||||
resetWorkshopForm();
|
||||
}
|
||||
globalThis.closeDelegateDrawer = closeDelegateDrawer;
|
||||
|
||||
@ -132,7 +178,7 @@ async function handleDelegateSession(e) {
|
||||
const hoursInput = document.getElementById("delegateHours");
|
||||
const lifespanHours = parseInt(hoursInput ? hoursInput.value : "1", 10) || 1;
|
||||
const modeInput = document.getElementById("delegateMode");
|
||||
let mode = modeInput ? modeInput.value : "read_only";
|
||||
let mode = modeInput ? modeInput.value : "read";
|
||||
const targetAppInput = document.getElementById("delegateTargetApp");
|
||||
const targetApp = targetAppInput ? targetAppInput.value : "";
|
||||
|
||||
@ -179,8 +225,13 @@ async function handleDelegateSession(e) {
|
||||
if (curlElem) {
|
||||
curlElem.textContent = '-H "Authorization: Bearer ' + data.token + '"';
|
||||
}
|
||||
|
||||
const createEl = document.getElementById("singleSessionCreateState");
|
||||
if (createEl) createEl.style.display = "none";
|
||||
const handoffModal = document.getElementById("handoffModal");
|
||||
if (handoffModal) handoffModal.style.display = "block";
|
||||
|
||||
setTabLock(true);
|
||||
showNotice("Delegated session created for: " + data.label, false);
|
||||
} else {
|
||||
showNotice(data.error || "Failed to delegate session", true);
|
||||
@ -255,6 +306,7 @@ async function handleCreateEvent(e) {
|
||||
const handoffState = document.getElementById("eventHandoffState");
|
||||
if (handoffState) handoffState.style.display = "block";
|
||||
|
||||
setTabLock(true);
|
||||
showNotice("Workshop pass created: " + ev.name, false);
|
||||
} else {
|
||||
showNotice(data.error || "Failed to create event pass", true);
|
||||
@ -319,8 +371,9 @@ function copyHandoff(type) {
|
||||
globalThis.copyHandoff = copyHandoff;
|
||||
|
||||
function closeHandoffModal() {
|
||||
const modal = document.getElementById("handoffModal");
|
||||
if (modal) modal.style.display = "none";
|
||||
resetDirectPassForm();
|
||||
const drawer = document.getElementById("delegateDrawer");
|
||||
if (drawer) drawer.style.display = "none";
|
||||
if (globalThis.location) globalThis.location.reload();
|
||||
}
|
||||
globalThis.closeHandoffModal = closeHandoffModal;
|
||||
@ -466,12 +519,12 @@ async function rotatePin(eventId) {
|
||||
}
|
||||
}
|
||||
|
||||
showNotice("Event credentials rotated successfully", false);
|
||||
showNotice("PIN rotated successfully", false);
|
||||
} else {
|
||||
showNotice(data.error || "Failed to rotate credentials", true);
|
||||
showNotice(data.error || "Failed to rotate PIN", true);
|
||||
}
|
||||
} catch (_err) {
|
||||
showNotice("Network error rotating credentials", true);
|
||||
showNotice("Network error rotating PIN", true);
|
||||
}
|
||||
}
|
||||
globalThis.rotatePin = rotatePin;
|
||||
@ -885,6 +938,31 @@ function bindEventCockpitActions() {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (res.ok && data.success) {
|
||||
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(() => {
|
||||
if (globalThis.location) globalThis.location.reload();
|
||||
}, 600);
|
||||
@ -927,12 +1005,12 @@ function bindEventCockpitActions() {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
showNotice(data.error || "Failed to end event", true);
|
||||
e.currentTarget.disabled = false;
|
||||
e.currentTarget.textContent = "🔴 End & Revoke All";
|
||||
e.currentTarget.textContent = "End Event";
|
||||
}
|
||||
} catch (_err) {
|
||||
showNotice("Network error ending event", true);
|
||||
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;
|
||||
|
||||
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 (
|
||||
<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;">
|
||||
@ -36,7 +44,7 @@ export const EventCockpitDeckFragment = (
|
||||
</div>
|
||||
|
||||
<div id="eventDeckContainer" class="grid-view">
|
||||
{eventPasses.map((event) => {
|
||||
{activePasses.map((event) => {
|
||||
const expDate = new Date(event.expires_at);
|
||||
const timeString = expDate.toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
@ -192,7 +200,6 @@ export const EventCockpitDeckFragment = (
|
||||
class="btn-outline"
|
||||
aria-label={`Manage attendees for ${event.name}`}
|
||||
style="justify-content: center; min-height: 38px; font-size: 0.85rem;"
|
||||
data-on-click={`@get("/api/events/${event.id}/attendees")`}
|
||||
onclick={`openAttendeesDrawer('${event.id}', '${
|
||||
event.name.replace(/'/g, "\\'")
|
||||
}')`}
|
||||
@ -204,7 +211,6 @@ export const EventCockpitDeckFragment = (
|
||||
class="btn-outline"
|
||||
aria-label={`Add 5 seats to ${event.name}`}
|
||||
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)`}
|
||||
>
|
||||
+5 Seats
|
||||
@ -214,17 +220,15 @@ export const EventCockpitDeckFragment = (
|
||||
<button
|
||||
type="button"
|
||||
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;"
|
||||
data-on-click={`@post("/api/events/${event.id}/rotate-pin")`}
|
||||
onclick={`rotatePin('${event.id}')`}
|
||||
>
|
||||
🔄 Rotate Credentials
|
||||
🔄 Rotate PIN
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline extend-event-btn"
|
||||
data-on-click={`@post("/api/events/${event.id}/extend")`}
|
||||
data-event-id={event.id}
|
||||
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;"
|
||||
@ -234,7 +238,6 @@ export const EventCockpitDeckFragment = (
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger end-event-btn"
|
||||
data-on-click={`@post("/api/events/${event.id}/end")`}
|
||||
data-event-id={event.id}
|
||||
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;"
|
||||
@ -279,7 +282,6 @@ export const EventCockpitDeckFragment = (
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
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}', '${
|
||||
event.name.replace(/'/g, "\\'")
|
||||
}')`}
|
||||
@ -289,7 +291,6 @@ export const EventCockpitDeckFragment = (
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline extend-event-btn"
|
||||
data-on-click={`@post("/api/events/${event.id}/extend")`}
|
||||
data-event-id={event.id}
|
||||
style="padding: 0.25rem 0.5rem; font-size: 0.8rem; min-height: 28px;"
|
||||
>
|
||||
@ -301,6 +302,57 @@ export const EventCockpitDeckFragment = (
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
export const WorkshopPassDrawerFragment = ({ apps }: { apps: any[] }) => {
|
||||
export const WorkshopPassDrawerFragment = ({
|
||||
apps = [],
|
||||
}: {
|
||||
apps?: any[];
|
||||
}) => {
|
||||
return (
|
||||
<div id="tabWorkshopPass" style="display: none;">
|
||||
<div id="eventCreateState" style="display: block;">
|
||||
@ -146,10 +150,10 @@ export const WorkshopPassDrawerFragment = ({ apps }: { apps: any[] }) => {
|
||||
<input type="hidden" id="eventLifespanHours" value="3" />
|
||||
</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);">
|
||||
<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>
|
||||
<div style="margin-top: 0.75rem; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
||||
<div>
|
||||
@ -201,7 +205,7 @@ export const WorkshopPassDrawerFragment = ({ apps }: { apps: any[] }) => {
|
||||
{/* Event Launch Success State */}
|
||||
<div
|
||||
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; align-items: center; gap: 0.5rem; flex: 1; min-width: 0;">
|
||||
@ -301,14 +305,24 @@ export const WorkshopPassDrawerFragment = ({ apps }: { apps: any[] }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
style="width: 100%; min-height: 38px; justify-content: center; font-size: 0.85rem;"
|
||||
onclick="closeEventHandoffModal()"
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
<div style="display: flex; gap: 0.75rem;">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-primary"
|
||||
style="flex: 1; min-height: 38px; justify-content: center; font-size: 0.85rem;"
|
||||
onclick="resetWorkshopForm()"
|
||||
>
|
||||
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>
|
||||
);
|
||||
|
||||
@ -7,204 +7,226 @@ export const DirectPassDrawerFragment = ({
|
||||
}) => {
|
||||
return (
|
||||
<div id="tabDirectPass">
|
||||
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0 0 1rem 0;">
|
||||
Mint an isolated, scoped child session for 1-click magic links, CLI
|
||||
tools, team members, or AI assistants without exposing your primary
|
||||
passkeys.
|
||||
</p>
|
||||
{/* Creation State */}
|
||||
<div id="singleSessionCreateState" style="display: block;">
|
||||
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0 0 1rem 0;">
|
||||
Mint an isolated, scoped child session for 1-click magic links, CLI
|
||||
tools, team members, or AI assistants without exposing your primary
|
||||
passkeys.
|
||||
</p>
|
||||
|
||||
<form
|
||||
id="delegateForm"
|
||||
onsubmit="handleDelegateSession(event)"
|
||||
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);"
|
||||
<form
|
||||
id="delegateForm"
|
||||
onsubmit="handleDelegateSession(event)"
|
||||
style="margin-top: 1rem;"
|
||||
>
|
||||
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem; font-size: 0.85rem; color: var(--text-primary);">
|
||||
Select Allowed Custom Scopes:
|
||||
</label>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem;">
|
||||
<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 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>
|
||||
<label style="display: flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; cursor: pointer;">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="scope-checkbox"
|
||||
value="write:events"
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
id="delegateLabel"
|
||||
placeholder="e.g. Friend Demo Pass, Antigravity Assistant, CI/CD Runner, Terminal CLI"
|
||||
required
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{/* 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>
|
||||
|
||||
{/* 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
|
||||
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; align-items: center; gap: 0.5rem;">
|
||||
@ -222,7 +244,7 @@ export const DirectPassDrawerFragment = ({
|
||||
below.
|
||||
</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 */}
|
||||
<div>
|
||||
<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>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-primary"
|
||||
class="btn-outline"
|
||||
style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;"
|
||||
data-ignore
|
||||
onclick="copyHandoff('magic')"
|
||||
@ -293,14 +315,24 @@ export const DirectPassDrawerFragment = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn-outline"
|
||||
style="width: 100%; min-height: 38px; justify-content: center; font-size: 0.85rem;"
|
||||
onclick="closeHandoffModal()"
|
||||
>
|
||||
Done (Session is Active)
|
||||
</button>
|
||||
<div style="display: flex; gap: 0.75rem;">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-primary"
|
||||
style="flex: 1; min-height: 38px; justify-content: center; font-size: 0.85rem;"
|
||||
onclick="resetDirectPassForm()"
|
||||
>
|
||||
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>
|
||||
);
|
||||
|
||||
@ -6,6 +6,10 @@ import {
|
||||
SessionDeckFragment,
|
||||
SessionTableFragment,
|
||||
} from "./fragments.tsx";
|
||||
import {
|
||||
EventCockpitDeckFragment,
|
||||
WorkshopPassDrawerFragment,
|
||||
} from "../events/fragments.tsx";
|
||||
|
||||
Deno.test("[Sessions] GET /dashboard unauthenticated redirects to /login", async () => {
|
||||
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 />;
|
||||
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), "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", () => {
|
||||
const html = <ScopeModalFragment apps={[]} />;
|
||||
assertStringIncludes(String(html), "Update Scopes for:");
|
||||
|
||||
@ -34,11 +34,11 @@ export const SessionsPageFragment = ({
|
||||
id="status-banner"
|
||||
role="status"
|
||||
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 */}
|
||||
<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);">
|
||||
{/* Top Action Bar with Sticky Docking directly beneath main navbar (top: 57px) */}
|
||||
<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>
|
||||
<h1 style="font-size: 1.75rem; font-weight: 700; margin: 0 0 0.25rem 0; color: var(--text-primary);">
|
||||
Sessions & Events
|
||||
|
||||
@ -2,77 +2,77 @@
|
||||
|
||||
### 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
|
||||
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.
|
||||
|
||||
### 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.
|
||||
- [ ] `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.
|
||||
- [ ] `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.
|
||||
- [ ] `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".
|
||||
- [ ] `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".
|
||||
|
||||
### 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".
|
||||
- [ ] `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".
|
||||
|
||||
### 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.
|
||||
- [ ] `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
|
||||
(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.
|
||||
- [ ] `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)".
|
||||
|
||||
### 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
|
||||
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-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.
|
||||
- [ ] `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.
|
||||
|
||||
### 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-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)"
|
||||
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.
|
||||
- [ ] `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.
|
||||
|
||||
### 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.
|
||||
- [ ] `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.
|
||||
- [ ] `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.
|
||||
- [ ] `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
|
||||
extended.
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user