auth-yes/ui/components/SessionsPage.tsx
google-labs-jules[bot] 64a9b2cd1d refactor(ui): Redesign and consolidate sessions and passes drawer
- Removed the dual top buttons from SessionsPage.tsx.
- Removed the `#eventDrawer` and wrapped its fields, buttons, and modal under `<form id="eventForm">` inside `#tabWorkshopPass` of `#delegateDrawer`.
- Wrapped the 1:1 Direct pass form fields, buttons, and modal under `<form id="delegateForm">` inside `#tabDirectPass` of `#delegateDrawer`.
- Added a vanilla Javascript tab switching logic for UI interaction without client-side frameworks.
- Removed deprecated `openEventDrawer` and `closeEventDrawer`.
- Replaced the single main button to trigger `openDelegateDrawer`.

Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
2026-08-25 22:53:20 +00:00

1549 lines
66 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { AuthenticatedLayout } from "./AuthenticatedLayout.tsx";
export const SessionsPage = ({
sessions,
currentSessionId,
apps = [],
isAdmin = false,
eventPasses = [],
}: {
sessions: any[];
currentSessionId: string;
apps?: any[];
isAdmin?: boolean;
eventPasses?: any[];
}) => {
return (
<AuthenticatedLayout
title="Sessions & Delegations"
currentPath="/dashboard/sessions"
isAdmin={isAdmin}
>
<div
id="status-banner"
style="display: none; margin-bottom: 1rem; padding: 0.75rem 1rem; border-radius: var(--radius-md); font-size: 0.9rem;"
/>
{eventPasses && eventPasses.length > 0 && (
<div style="margin-bottom: 2rem;">
<h2 style="font-size: 1.25rem; font-weight: 700; margin: 0 0 1rem 0; color: var(--text-primary); border-bottom: 1px solid var(--border-subtle); padding-bottom: 0.5rem;">
Event Cockpit Deck (Live Workshops)
</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1rem;">
{eventPasses.map((event) => (
<div
key={event.id}
class="card"
style="border-left: 4px solid var(--primary); margin: 0;"
>
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.75rem;">
<div>
<h3 style="margin: 0 0 0.25rem 0; font-size: 1.1rem; color: var(--text-primary);">
{event.name}
</h3>
<div style="font-size: 0.8rem; color: var(--text-secondary);">
Expires: {new Date(event.expires_at).toLocaleString()}
</div>
</div>
<span class="badge badge-success">Active</span>
</div>
{/* Progress Bar for Seats */}
<div style="margin-bottom: 1rem;">
<div style="display: flex; justify-content: space-between; font-size: 0.8rem; margin-bottom: 0.25rem; color: var(--text-secondary);">
<span>Seats Claimed</span>
<strong style="color: var(--text-primary);">
{event.seats_claimed} /{" "}
{event.max_seats === 0 ? "∞" : event.max_seats}
</strong>
</div>
<div style="width: 100%; height: 8px; background: var(--surface-muted); border-radius: 4px; overflow: hidden;">
<div
style={`height: 100%; background: var(--primary); width: ${
event.max_seats > 0
? Math.min(
(event.seats_claimed / event.max_seats) * 100,
100,
)
: 100
}%;`}
>
</div>
</div>
</div>
{/* Quick Copy Snippets */}
<div style="display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem;">
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span style="font-size: 0.8rem; font-weight: 600; width: 60px; color: var(--text-muted);">
PIN:
</span>
<code style="flex: 1; padding: 0.35rem 0.5rem; background: var(--surface-muted); border-radius: var(--radius-sm); font-family: monospace; font-size: 0.9rem; text-align: center; letter-spacing: 2px;">
{event.pin_code}
</code>
<button
type="button"
class="btn-outline"
style="padding: 0.25rem 0.5rem; font-size: 0.75rem; min-height: 28px;"
onclick={`copyText('${event.pin_code}')`}
>
Copy
</button>
</div>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span style="font-size: 0.8rem; font-weight: 600; width: 60px; color: var(--text-muted);">
Link:
</span>
<code style="flex: 1; padding: 0.35rem 0.5rem; background: var(--surface-muted); border-radius: var(--radius-sm); font-family: monospace; font-size: 0.75rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
/e/{event.slug}
</code>
<button
type="button"
class="btn-outline"
style="padding: 0.25rem 0.5rem; font-size: 0.75rem; min-height: 28px;"
onclick={`copyText(window.location.origin + '/e/${event.slug}')`}
>
Copy
</button>
</div>
</div>
{/* Cockpit Actions */}
<div style="display: flex; gap: 0.5rem;">
<button
type="button"
class="btn-outline extend-event-btn"
data-event-id={event.id}
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.85rem;"
>
+1h Extend
</button>
<button
type="button"
class="btn-danger end-event-btn"
data-event-id={event.id}
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.85rem;"
>
🔴 End & Revoke All
</button>
</div>
</div>
))}
</div>
</div>
)}
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem;">
<div>
<h1 style="font-size: 1.75rem; font-weight: 700; margin: 0 0 0.5rem 0; color: var(--text-primary);">
Active Sessions & Passes
</h1>
<p style="color: var(--text-secondary); margin: 0; font-size: 0.95rem;">
Manage logins, mint 1:1 ephemeral links & CLI tokens, or launch
multi-claim workshop event passes.
</p>
</div>
<div style="display: flex; gap: 0.75rem; flex-wrap: wrap;">
<button
type="button"
class="btn-primary"
style="min-height: 42px; box-shadow: var(--shadow-sm); display: inline-flex; align-items: center; gap: 0.4rem;"
onclick="openDelegateDrawer()"
>
<span style="font-size: 1.1rem;">🔑</span>
<span>+ Delegate Session</span>
</button>
</div>
</div>
{/* Delegate Session Drawer */}
<div
id="delegateDrawer"
class="card"
style="display: none; border-left: 4px solid var(--primary); margin-bottom: 1.5rem;"
>
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
<div style="width: 100%;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h3 style="margin: 0 0 0.25rem 0; color: var(--text-primary);">
Create New Pass / Session
</h3>
<button
type="button"
onclick="closeDelegateDrawer()"
style="background: none; border: none; font-size: 1.3rem; color: var(--text-muted); cursor: pointer;"
>
&times;
</button>
</div>
<div style="display: flex; background: var(--surface-muted); padding: 4px; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle); gap: 4px; margin-top: 1rem; margin-bottom: 1rem;">
<button
type="button"
id="tabBtnDirectPass"
class="delegate-tab-btn active"
onclick="switchDelegateTab('tabDirectPass')"
>
🔑 1:1 Direct Pass
</button>
<button
type="button"
id="tabBtnWorkshopPass"
class="delegate-tab-btn"
onclick="switchDelegateTab('tabWorkshopPass')"
>
🎟 Multi-Claim Workshop Pass
</button>
</div>
</div>
</div>
<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>
<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 (App Selection) */}
<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}>
{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_only"
onclick="selectMode(this, 'read_only')"
>
👁 Read-Only (Audit & Inspection)
</button>
<button
type="button"
class="pill-btn mode-pill"
data-mode="operator"
onclick="selectMode(this, 'operator')"
>
Operator (App Actions)
</button>
{isAdmin && (
<button
type="button"
class="pill-btn mode-pill"
data-mode="admin"
onclick="selectMode(this, 'admin')"
>
👑 Full Admin
</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_only" />
</div>
{/* Progressive Disclosure: Specific App & Capability 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 Specific App Grants & Capabilities (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>
))}
</div>
</div>
</details>
<div style="display: flex; gap: 0.75rem;">
<button
type="submit"
id="submitDelegateBtn"
class="btn-primary"
style="min-height: 42px;"
>
Mint & Delegate Session
</button>
<button
type="button"
class="btn-outline"
onclick="closeDelegateDrawer()"
style="min-height: 42px;"
>
Cancel
</button>
</div>
</form>
{/* 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);"
>
<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;">
<span style="font-size: 1.2rem;">🔑</span>
<strong style="color: var(--text-primary); font-size: 1rem;">
Session Delegated Successfully!
</strong>
</div>
<span class="badge badge-success">Active Now</span>
</div>
<p style="font-size: 0.85rem; color: var(--text-secondary); margin: 0 0 1rem 0;">
Copy this token, 1-click magic link, or CLI export string. You can
revoke or extend this session anytime from the active session
cards below.
</p>
<div style="display: flex; flex-direction: column; gap: 0.75rem; margin-bottom: 1rem;">
{/* 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;">
1-Click Magic Link (Web / Friend / Interview)
</label>
<div style="display: flex; gap: 0.5rem;">
<code
id="handoffMagicLinkText"
style="flex: 1; padding: 0.5rem 0.75rem; background: var(--surface-muted); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); font-family: monospace; font-size: 0.85rem; overflow-x: auto; white-space: nowrap; color: var(--success);"
>
</code>
<button
type="button"
class="btn-primary"
style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;"
onclick="copyHandoff('magic')"
>
Copy Link
</button>
</div>
</div>
{/* 1-Tap Copy CLI */}
<div>
<label style="display: block; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.25rem;">
Terminal Environment Export (CLI)
</label>
<div style="display: flex; gap: 0.5rem;">
<code
id="handoffCliText"
style="flex: 1; padding: 0.5rem 0.75rem; background: var(--surface-muted); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); font-family: monospace; font-size: 0.85rem; overflow-x: auto; white-space: nowrap; color: var(--text-primary);"
>
</code>
<button
type="button"
class="btn-outline"
style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;"
onclick="copyHandoff('cli')"
>
Copy CLI
</button>
</div>
</div>
{/* 1-Tap Copy cURL Header */}
<div>
<label style="display: block; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.25rem;">
HTTP Authorization Header
</label>
<div style="display: flex; gap: 0.5rem;">
<code
id="handoffCurlText"
style="flex: 1; padding: 0.5rem 0.75rem; background: var(--surface-muted); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); font-family: monospace; font-size: 0.85rem; overflow-x: auto; white-space: nowrap; color: var(--text-primary);"
>
</code>
<button
type="button"
class="btn-outline"
style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;"
onclick="copyHandoff('curl')"
>
Copy Header
</button>
</div>
</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>
</div>
<div id="tabWorkshopPass" style="display: none;">
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0 0 1rem 0;">
Generate a shared event pass with a universal 6-digit PIN, vanity
link (<code style="font-size: 0.8rem;">/e/slug</code>), and CLI
1-liner for workshops, hackathons, and multi-user kiosks.
</p>
<form
id="eventForm"
onsubmit="handleCreateEvent(event)"
style="margin-top: 1rem;"
>
{/* Event Name */}
<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);">
Workshop / Event Name *
</label>
<input
type="text"
id="eventName"
placeholder="e.g. Deno & SPIRE Zero-Trust Workshop, Elite Fleet Live Demo"
required
style="width: 100%;"
/>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-bottom: 1.25rem;">
{/* Target App */}
<div>
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);">
Target App Destination
</label>
<select
id="eventAppId"
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="">Universal Sandbox (Default)</option>
{apps.map((app) => (
<option value={app.id}>
{app.name} ({app.domain || "Internal"})
</option>
))}
</select>
</div>
{/* Guest Role */}
<div>
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);">
Attendee Access Role
</label>
<select
id="eventRole"
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="viewer">Viewer (Read-Only Demo)</option>
<option value="operator">Operator (Workshop Actions)</option>
<option value="user">Standard User</option>
</select>
</div>
</div>
{/* Seat Capacity 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);">
Seat Capacity Limit
</label>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;">
<button
type="button"
class="pill-btn seat-pill"
onclick="selectSeats(this, 25)"
>
25 Seats
</button>
<button
type="button"
class="pill-btn seat-pill active"
onclick="selectSeats(this, 50)"
>
50 Seats
</button>
<button
type="button"
class="pill-btn seat-pill"
onclick="selectSeats(this, 100)"
>
100 Seats
</button>
<button
type="button"
class="pill-btn seat-pill"
onclick="selectSeats(this, 0)"
>
Unlimited
</button>
<input
type="number"
id="eventMaxSeats"
value="50"
min="0"
max="10000"
style="width: 100px; padding: 0.35rem 0.5rem; text-align: center;"
title="Max seats (0 = unlimited)"
/>
</div>
</div>
{/* Duration 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);">
Event Duration & Expiration
</label>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<button
type="button"
class="pill-btn event-lifespan-pill"
onclick="selectEventLifespan(this, 1)"
>
1 Hour
</button>
<button
type="button"
class="pill-btn event-lifespan-pill active"
onclick="selectEventLifespan(this, 3)"
>
🛠 3 Hours (Standard Workshop)
</button>
<button
type="button"
class="pill-btn event-lifespan-pill"
onclick="selectEventLifespan(this, 12)"
>
📅 12 Hours (All-Day Hackathon)
</button>
<button
type="button"
class="pill-btn event-lifespan-pill"
onclick="selectEventLifespan(this, 24)"
>
🗓 24 Hours
</button>
</div>
<input type="hidden" id="eventLifespanHours" value="3" />
</div>
{/* 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)
</summary>
<div style="margin-top: 0.75rem; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
<div>
<label style="display: block; font-size: 0.8rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 0.25rem;">
Vanity Slug (e.g. /e/deno-lab)
</label>
<input
type="text"
id="eventSlug"
placeholder="auto-generated if blank"
style="width: 100%; font-size: 0.85rem;"
/>
</div>
<div>
<label style="display: block; font-size: 0.8rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 0.25rem;">
Custom 6-Digit PIN (e.g. 749-123)
</label>
<input
type="text"
id="eventPinCode"
placeholder="auto-generated if blank"
style="width: 100%; font-size: 0.85rem;"
/>
</div>
</div>
</details>
<div style="display: flex; gap: 0.75rem;">
<button
type="submit"
id="submitEventBtn"
class="btn-primary"
style="min-height: 42px; background: var(--success); border-color: var(--success);"
>
🎟 Launch Workshop Pass
</button>
<button
type="button"
class="btn-outline"
onclick="closeDelegateDrawer()"
style="min-height: 42px;"
>
Cancel
</button>
</div>
</form>
{/* Event Launch Success Modal / Card */}
<div
id="eventHandoffModal"
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);"
>
<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;">
<span style="font-size: 1.3rem;">🎟</span>
<strong
id="createdEventTitle"
style="color: var(--text-primary); font-size: 1.1rem;"
>
Workshop Pass Live!
</strong>
</div>
<span class="badge badge-success">Active Now</span>
</div>
<p style="font-size: 0.85rem; color: var(--text-secondary); margin: 0 0 1rem 0;">
Share the 6-digit PIN, 1-click link, or CLI 1-liner with
attendees. You can monitor live claimed seats and trigger the
master kill switch from the Event Cockpit below.
</p>
<div style="display: flex; flex-direction: column; gap: 0.75rem; margin-bottom: 1.25rem;">
{/* PIN Code */}
<div>
<label style="display: block; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.25rem;">
6-Digit Universal PIN (for /join or Kiosk Entry)
</label>
<div style="display: flex; gap: 0.5rem;">
<code
id="eventHandoffPin"
style="flex: 1; padding: 0.5rem 0.75rem; background: var(--surface-muted); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); font-family: monospace; font-size: 1.1rem; text-align: center; letter-spacing: 3px; font-weight: 700; color: var(--primary);"
>
</code>
<button
type="button"
class="btn-primary"
style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;"
onclick="copyEventHandoff('pin')"
>
Copy PIN
</button>
</div>
</div>
{/* 1-Click Vanity Link */}
<div>
<label style="display: block; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.25rem;">
Direct 1-Click Workshop Entrance URL
</label>
<div style="display: flex; gap: 0.5rem;">
<code
id="eventHandoffLink"
style="flex: 1; padding: 0.5rem 0.75rem; background: var(--surface-muted); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); font-family: monospace; font-size: 0.85rem; overflow-x: auto; white-space: nowrap; color: var(--success);"
>
</code>
<button
type="button"
class="btn-outline"
style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;"
onclick="copyEventHandoff('link')"
>
Copy URL
</button>
</div>
</div>
{/* CLI 1-Liner */}
<div>
<label style="display: block; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.25rem;">
Terminal 1-Liner (Environment Injector)
</label>
<div style="display: flex; gap: 0.5rem;">
<code
id="eventHandoffCli"
style="flex: 1; padding: 0.5rem 0.75rem; background: var(--surface-muted); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); font-family: monospace; font-size: 0.8rem; overflow-x: auto; white-space: nowrap; color: var(--text-primary);"
>
</code>
<button
type="button"
class="btn-outline"
style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;"
onclick="copyEventHandoff('cli')"
>
Copy 1-Liner
</button>
</div>
</div>
</div>
<button
type="button"
class="btn-outline"
style="width: 100%; min-height: 38px; justify-content: center; font-size: 0.85rem;"
onclick="closeEventHandoffModal()"
>
Done & Refresh Cockpit
</button>
</div>
</div>
</div>
{/* Edit Scopes Modal */}
<div
id="editScopesModal"
style="display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.6); z-index: 9999; justify-content: center; align-items: center;"
>
<div style="background: var(--surface-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); width: 90%; max-width: 500px; padding: 1.5rem; box-shadow: var(--shadow-lg);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h3 style="margin: 0; font-size: 1.1rem; color: var(--text-primary);">
Update Scopes for:{" "}
<span id="editScopesLabel" style="color: var(--primary);"></span>
</h3>
<button
type="button"
onclick="closeEditScopesModal()"
style="background: none; border: none; font-size: 1.2rem; cursor: pointer; color: var(--text-muted);"
>
&times;
</button>
</div>
<input type="hidden" id="editScopesSessionId" value="" />
<div style="display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1.25rem;">
<label style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; color: var(--text-primary); cursor: pointer;">
<input
type="checkbox"
id="scope_read_audit"
class="edit-scope-chk"
value="read:audit"
/>
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"
id="scope_read_users"
class="edit-scope-chk"
value="read:users"
/>
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"
id="scope_read_apps"
class="edit-scope-chk"
value="read:apps"
/>
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="edit-scope-chk"
value={`app:${app.name}`}
/>
Access {app.name}
</label>
))}
</div>
<div style="display: flex; justify-content: flex-end; gap: 0.5rem;">
<button
type="button"
class="btn-outline"
onclick="closeEditScopesModal()"
>
Cancel
</button>
<button
type="button"
class="btn-primary"
onclick="saveUpdatedScopes()"
>
Save Scopes
</button>
</div>
</div>
</div>
{/* Desktop Table View (≥ 768px) */}
<div class="card desktop-only" style="display: none;">
<div class="table-container">
<table>
<thead>
<tr>
<th>Type & Label</th>
<th>Permissions</th>
<th>Activity</th>
<th>Expires</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{sessions.length === 0
? (
<tr>
<td
colSpan={5}
style="text-align: center; padding: 2rem; color: var(--text-muted);"
>
No active sessions found.
</td>
</tr>
)
: (
sessions.map((session) => {
const isCurrent = session.id === currentSessionId;
const isAgent = !!session.is_agent;
const scopes = Array.isArray(session.custom_scopes)
? session.custom_scopes
: [];
return (
<tr key={session.id}>
<td>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span style="font-size: 1.1rem;">
{isAgent ? "🔑" : isCurrent ? "📱" : "💻"}
</span>
<div>
<strong style="color: var(--text-primary);">
{session.label ||
(isCurrent ? "This Device" : "Remote Device")}
</strong>
<div style="font-size: 0.75rem; color: var(--text-muted); font-family: monospace;">
{session.id.substring(0, 14)}...
</div>
</div>
</div>
</td>
<td>
{isAgent
? (
<span class="badge badge-info">
{scopes.length > 0
? `${scopes.length} Scopes`
: "Delegated"}
</span>
)
: (
<span class="badge badge-success">
Interactive
</span>
)}
</td>
<td style="color: var(--text-secondary); font-size: 0.85rem;">
{session.last_activity_action
? (
<div>
<span style="color: var(--primary); font-family: monospace; font-size: 0.8rem;">
{session.last_activity_action}
</span>
<div style="font-size: 0.75rem; color: var(--text-muted);">
{new Date(
session.last_activity_at ||
session.created_at,
).toLocaleTimeString()}
</div>
</div>
)
: (
<span>
{new Date(session.created_at)
.toLocaleDateString()}
</span>
)}
</td>
<td style="color: var(--text-secondary); font-size: 0.85rem;">
{new Date(session.expires_at).toLocaleString()}
</td>
<td>
<div style="display: flex; gap: 0.35rem; align-items: center;">
{isAgent && (
<>
<button
type="button"
class="btn-outline"
style="padding: 0.25rem 0.5rem; font-size: 0.75rem; min-height: 30px;"
onclick={`extendSession('${session.id}', 1)`}
title="Extend session by 1 hour"
>
+1h
</button>
<button
type="button"
class="btn-outline"
style="padding: 0.25rem 0.5rem; font-size: 0.75rem; min-height: 30px;"
onclick={`openEditScopesModal('${session.id}', '${
session.label || "Delegated"
}', ${
JSON.stringify(JSON.stringify(scopes))
})`}
title="Edit permissions"
>
Scopes
</button>
</>
)}
{!isCurrent && (
<button
type="button"
class="btn-danger revoke-btn"
data-session-id={session.id}
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 30px;"
>
Revoke
</button>
)}
</div>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
{/* Mobile Adaptive Cards View (< 768px) */}
<div
class="mobile-only"
style="display: flex; flex-direction: column; gap: 1rem;"
>
{sessions.length === 0
? (
<div class="card" style="text-align: center; padding: 2rem;">
<p style="color: var(--text-muted); margin: 0;">
No active sessions found.
</p>
</div>
)
: (
sessions.map((session) => {
const isCurrent = session.id === currentSessionId;
const isAgent = !!session.is_agent;
const scopes = Array.isArray(session.custom_scopes)
? session.custom_scopes
: [];
return (
<div class="card" key={session.id} style="margin-bottom: 0;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
<div style="display: flex; align-items: center; gap: 0.65rem;">
<div style="display: flex; align-items: center; justify-content: center; width: 38px; height: 38px; background: var(--surface-muted); border-radius: var(--radius-md); font-size: 1.2rem;">
{isAgent ? "🔑" : isCurrent ? "📱" : "💻"}
</div>
<div>
<div style="font-weight: 700; font-size: 0.95rem; color: var(--text-primary);">
{session.label ||
(isCurrent ? "This Device" : "Remote Device")}
</div>
<div style="font-size: 0.75rem; color: var(--text-muted); font-family: monospace;">
{session.id.substring(0, 14)}...
</div>
</div>
</div>
{isAgent
? <span class="badge badge-info">Delegated</span>
: isCurrent
? <span class="badge badge-success">Active Now</span>
: <span class="badge badge-secondary">Active</span>}
</div>
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 1rem; line-height: 1.6;">
{isAgent && (
<div style="margin-bottom: 0.25rem;">
<strong>Scopes:</strong>{" "}
{scopes.length > 0 ? scopes.join(", ") : "Delegated"}
</div>
)}
<div>
<strong>Expires:</strong>{" "}
{new Date(session.expires_at).toLocaleString()}
</div>
{session.last_activity_action && (
<div>
<strong>Last Active:</strong>{" "}
{session.last_activity_action} ({new Date(
session.last_activity_at || session.created_at,
).toLocaleTimeString()})
</div>
)}
</div>
<div style="display: flex; gap: 0.5rem;">
{isAgent && (
<>
<button
type="button"
class="btn-outline"
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.8rem;"
onclick={`extendSession('${session.id}', 1)`}
>
+1h Extend
</button>
<button
type="button"
class="btn-outline"
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.8rem;"
onclick={`openEditScopesModal('${session.id}', '${
session.label || "Delegated"
}', ${JSON.stringify(JSON.stringify(scopes))})`}
>
Scopes
</button>
</>
)}
{!isCurrent && (
<button
type="button"
class="btn-danger revoke-btn"
data-session-id={session.id}
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.85rem;"
>
Revoke
</button>
)}
</div>
</div>
);
})
)}
</div>
<style>
{`
@media (min-width: 768px) {
.desktop-only { display: block !important; }
.mobile-only { display: none !important; }
}
@media (max-width: 767px) {
.desktop-only { display: none !important; }
.mobile-only { display: flex !important; }
}
.pill-btn {
background: var(--surface-card);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-full);
padding: 0.4rem 0.85rem;
font-size: 0.85rem;
font-weight: 600;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.15s ease;
}
.pill-btn:hover {
border-color: var(--primary);
color: var(--primary);
}
.pill-btn.active {
background: var(--primary-light);
border-color: var(--primary);
color: var(--primary);
}
.delegate-tab-btn {
flex: 1;
padding: 0.5rem 0.75rem;
border-radius: var(--radius-sm);
border: none;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
background: transparent;
color: var(--text-secondary);
}
.delegate-tab-btn.active {
background: var(--surface-card);
color: var(--primary);
box-shadow: var(--shadow-xs);
}
`}
</style>
<script
dangerouslySetInnerHTML={{
__html: `
let lastMintedToken = "";
function showNotice(msg, isError) {
const banner = document.getElementById('status-banner');
banner.textContent = msg;
banner.style.display = 'block';
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(() => { banner.style.display = 'none'; }, 5000);
}
function openDelegateDrawer() {
document.getElementById('delegateDrawer').style.display = 'block';
document.getElementById('delegateDrawer').scrollIntoView({ behavior: 'smooth' });
}
function closeDelegateDrawer() {
document.getElementById('delegateDrawer').style.display = 'none';
}
function switchDelegateTab(tabId) {
document.getElementById('tabDirectPass').style.display = 'none';
document.getElementById('tabWorkshopPass').style.display = 'none';
document.getElementById('tabBtnDirectPass').classList.remove('active');
document.getElementById('tabBtnWorkshopPass').classList.remove('active');
document.getElementById(tabId).style.display = 'block';
if (tabId === 'tabDirectPass') {
document.getElementById('tabBtnDirectPass').classList.add('active');
} else {
document.getElementById('tabBtnWorkshopPass').classList.add('active');
}
}
function selectSeats(btn, count) {
document.querySelectorAll('.seat-pill').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('eventMaxSeats').value = count;
}
function selectEventLifespan(btn, hours) {
document.querySelectorAll('.event-lifespan-pill').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('eventLifespanHours').value = hours;
}
function handleTargetAppChange(appName) {
if (appName) {
const scopeVal = 'app:' + appName;
document.querySelectorAll('.scope-checkbox').forEach(cb => {
if (cb.value === scopeVal) cb.checked = true;
});
}
}
function selectLifespan(btn, hours) {
document.querySelectorAll('.lifespan-pill').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('delegateHours').value = hours;
}
function selectMode(btn, mode) {
document.querySelectorAll('.mode-pill').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('delegateMode').value = mode;
const accordion = document.getElementById('customScopesSection');
if (mode === 'custom') {
accordion.open = true;
}
}
async function handleDelegateSession(e) {
e.preventDefault();
const label = document.getElementById('delegateLabel').value.trim();
const lifespanHours = parseInt(document.getElementById('delegateHours').value) || 1;
let mode = document.getElementById('delegateMode').value;
const targetApp = document.getElementById('delegateTargetApp') ? document.getElementById('delegateTargetApp').value : '';
const customScopes = [];
if (targetApp) {
customScopes.push('app:' + targetApp);
}
if (mode === 'custom' || targetApp) {
document.querySelectorAll('.scope-checkbox:checked').forEach(cb => {
if (!customScopes.includes(cb.value)) {
customScopes.push(cb.value);
}
});
if (targetApp && mode !== 'custom' && mode !== 'admin') {
mode = 'custom';
}
}
const btn = document.getElementById('submitDelegateBtn');
btn.disabled = true;
btn.textContent = 'Minting...';
try {
const res = await fetch('/api/sessions/delegate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ label, lifespanHours, mode, customScopes }),
});
const data = await res.json();
if (res.ok) {
lastMintedToken = data.token;
document.getElementById('handoffMagicLinkText').textContent = window.location.origin + '/pass?token=' + data.token;
document.getElementById('handoffCliText').textContent = 'export AUTH_YES_TOKEN="' + data.token + '"';
document.getElementById('handoffCurlText').textContent = '-H "Authorization: Bearer ' + data.token + '"';
document.getElementById('handoffModal').style.display = 'block';
showNotice('Delegated session created for: ' + data.label, false);
} else {
showNotice(data.error || 'Failed to delegate session', true);
}
} catch (err) {
showNotice('Network error delegating session', true);
} finally {
btn.disabled = false;
btn.textContent = 'Mint & Delegate Session';
}
}
async function handleCreateEvent(e) {
e.preventDefault();
const name = document.getElementById('eventName').value.trim();
const appId = document.getElementById('eventAppId').value || null;
const role = document.getElementById('eventRole').value || 'viewer';
const maxSeats = parseInt(document.getElementById('eventMaxSeats').value) || 0;
const lifespanHours = parseInt(document.getElementById('eventLifespanHours').value) || 3;
const slug = document.getElementById('eventSlug').value.trim() || undefined;
const pinCode = document.getElementById('eventPinCode').value.trim() || undefined;
const btn = document.getElementById('submitEventBtn');
btn.disabled = true;
btn.textContent = 'Launching...';
try {
const res = await fetch('/api/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, appId, role, maxSeats, lifespanHours, slug, pinCode }),
});
const data = await res.json();
if (res.ok && data.success) {
const ev = data.event;
document.getElementById('createdEventTitle').textContent = '🎟️ ' + ev.name + ' Live!';
document.getElementById('eventHandoffPin').textContent = ev.pin_code;
document.getElementById('eventHandoffLink').textContent = window.location.origin + '/e/' + ev.slug;
document.getElementById('eventHandoffCli').textContent = 'curl -sSL ' + window.location.origin + '/join/' + ev.slug + '?format=env | source /dev/stdin';
document.getElementById('eventHandoffModal').style.display = 'block';
showNotice('Workshop pass created: ' + ev.name, false);
} else {
showNotice(data.error || 'Failed to create event pass', true);
}
} catch (err) {
showNotice('Network error creating event pass', true);
} finally {
btn.disabled = false;
btn.textContent = '🎟️ Launch Workshop Pass';
}
}
function copyEventHandoff(type) {
let text = '';
if (type === 'pin') text = document.getElementById('eventHandoffPin').textContent;
if (type === 'link') text = document.getElementById('eventHandoffLink').textContent;
if (type === 'cli') text = document.getElementById('eventHandoffCli').textContent;
navigator.clipboard.writeText(text);
showNotice('Copied to clipboard: ' + text, false);
}
function closeEventHandoffModal() {
document.getElementById('eventHandoffModal').style.display = 'none';
window.location.reload();
}
function copyHandoff(type) {
let text = '';
if (type === 'link') text = document.getElementById('handoffLinkText').textContent;
if (type === 'cli') text = document.getElementById('handoffCliText').textContent;
if (type === 'curl') text = document.getElementById('handoffCurlText').textContent;
if (type === 'magic') text = document.getElementById('handoffMagicLinkText').textContent;
navigator.clipboard.writeText(text);
showNotice('Copied to clipboard: ' + text, false);
}
function closeHandoffModal() {
document.getElementById('handoffModal').style.display = 'none';
window.location.reload();
}
async function extendSession(sessionId, hours) {
try {
const res = await fetch('/api/sessions/' + sessionId + '/extend', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ extendHours: hours }),
});
if (res.ok) {
showNotice('Session extended by ' + hours + ' hour(s)!', false);
setTimeout(() => window.location.reload(), 600);
} else {
const data = await res.json();
showNotice(data.error || 'Failed to extend session', true);
}
} catch (err) {
showNotice('Network error extending session', true);
}
}
function openEditScopesModal(sessionId, label, scopesJson) {
const scopes = JSON.parse(scopesJson || '[]');
document.getElementById('editScopesSessionId').value = sessionId;
document.getElementById('editScopesLabel').textContent = label;
document.querySelectorAll('.edit-scope-chk').forEach(cb => {
cb.checked = scopes.includes(cb.value);
});
document.getElementById('editScopesModal').style.display = 'flex';
}
function closeEditScopesModal() {
document.getElementById('editScopesModal').style.display = 'none';
}
async function saveUpdatedScopes() {
const sessionId = document.getElementById('editScopesSessionId').value;
const customScopes = [];
document.querySelectorAll('.edit-scope-chk:checked').forEach(cb => {
customScopes.push(cb.value);
});
try {
const res = await fetch('/api/sessions/' + sessionId + '/scopes', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ customScopes }),
});
if (res.ok) {
showNotice('Session scopes updated!', false);
setTimeout(() => window.location.reload(), 600);
} else {
const data = await res.json();
showNotice(data.error || 'Failed to update scopes', true);
}
} catch (err) {
showNotice('Network error updating scopes', true);
}
}
function copyText(text) {
navigator.clipboard.writeText(text);
showNotice('Copied to clipboard!', false);
}
// Event Cockpit Actions
document.querySelectorAll('.extend-event-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
const eventId = e.currentTarget.getAttribute('data-event-id');
e.currentTarget.disabled = true;
try {
const res = await fetch('/api/events/' + eventId + '/extend', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ extendHours: 1 })
});
if (res.ok) {
showNotice('Event extended by 1 hour!', false);
setTimeout(() => window.location.reload(), 600);
} else {
const data = await res.json();
showNotice(data.error || 'Failed to extend event', true);
e.currentTarget.disabled = false;
}
} catch (err) {
showNotice('Network error extending event', true);
e.currentTarget.disabled = false;
}
});
});
document.querySelectorAll('.end-event-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
if (!confirm('Are you sure you want to end this workshop? This will immediately revoke ALL active guest sessions and they will lose access.')) return;
const eventId = e.currentTarget.getAttribute('data-event-id');
e.currentTarget.disabled = true;
e.currentTarget.textContent = 'Revoking...';
try {
const res = await fetch('/api/events/' + eventId + '/end', {
method: 'POST',
});
if (res.ok) {
showNotice('Workshop ended and all sessions revoked.', false);
setTimeout(() => window.location.reload(), 600);
} else {
const data = await res.json();
showNotice(data.error || 'Failed to end event', true);
e.currentTarget.disabled = false;
e.currentTarget.textContent = '🔴 End & Revoke All';
}
} catch (err) {
showNotice('Network error ending event', true);
e.currentTarget.disabled = false;
e.currentTarget.textContent = '🔴 End & Revoke All';
}
});
});
// Revoke handler
document.querySelectorAll('.revoke-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
if (!confirm('Revoke this session immediately?')) return;
const sessionId = e.currentTarget.getAttribute('data-session-id');
const originalText = e.currentTarget.textContent;
e.currentTarget.textContent = 'Revoking...';
e.currentTarget.disabled = true;
try {
const res = await fetch('/api/sessions/' + sessionId, {
method: 'DELETE'
});
if (res.ok) {
window.location.reload();
} else {
const data = await res.json();
showNotice(data.error || 'Failed to revoke session', true);
e.currentTarget.textContent = originalText;
e.currentTarget.disabled = false;
}
} catch (err) {
showNotice('Network error revoking session', true);
e.currentTarget.textContent = originalText;
e.currentTarget.disabled = false;
}
});
});
`,
}}
>
</script>
</AuthenticatedLayout>
);
};