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:
Tyler Gillispie 2026-08-27 22:24:10 -07:00
parent ed9bdb8852
commit b38f122df4
7 changed files with 501 additions and 267 deletions

View File

@ -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";
}
});
});

View File

@ -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>
);

View File

@ -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>
);

View File

@ -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>
);

View File

@ -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 &amp; 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 &amp; 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:");

View File

@ -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

View File

@ -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.