Compare commits
2 Commits
f912241937
...
9a90f4493c
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a90f4493c | |||
|
|
296bc6a9c3 |
@ -1,4 +1,8 @@
|
|||||||
import { AuthenticatedLayout } from "./AuthenticatedLayout.tsx";
|
import { AuthenticatedLayout } from "./AuthenticatedLayout.tsx";
|
||||||
|
import { EventCockpitDeck } from "./sessions/EventCockpitDeck.tsx";
|
||||||
|
import { DirectPassDrawer } from "./sessions/DirectPassDrawer.tsx";
|
||||||
|
import { WorkshopDrawer } from "./sessions/WorkshopDrawer.tsx";
|
||||||
|
import { ScopeModal } from "./sessions/ScopeModal.tsx";
|
||||||
|
|
||||||
export const SessionsPage = ({
|
export const SessionsPage = ({
|
||||||
sessions,
|
sessions,
|
||||||
@ -24,114 +28,7 @@ export const SessionsPage = ({
|
|||||||
style="display: none; margin-bottom: 1rem; padding: 0.75rem 1rem; border-radius: var(--radius-md); font-size: 0.9rem;"
|
style="display: none; margin-bottom: 1rem; padding: 0.75rem 1rem; border-radius: var(--radius-md); font-size: 0.9rem;"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{eventPasses && eventPasses.length > 0 && (
|
<EventCockpitDeck eventPasses={eventPasses} />
|
||||||
<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 style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem;">
|
||||||
<div>
|
<div>
|
||||||
@ -199,703 +96,11 @@ export const SessionsPage = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="tabDirectPass">
|
<DirectPassDrawer apps={apps} isAdmin={isAdmin} />
|
||||||
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0 0 1rem 0;">
|
<WorkshopDrawer apps={apps} />
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
{/* Direct 1-Click Destination (App Selection) */}
|
<ScopeModal apps={apps} />
|
||||||
<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);"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</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) */}
|
{/* Desktop Table View (≥ 768px) */}
|
||||||
<div class="card desktop-only" style="display: none;">
|
<div class="card desktop-only" style="display: none;">
|
||||||
|
|||||||
323
ui/components/sessions/DirectPassDrawer.tsx
Normal file
323
ui/components/sessions/DirectPassDrawer.tsx
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
export const DirectPassDrawer = ({
|
||||||
|
apps = [],
|
||||||
|
isAdmin = false,
|
||||||
|
}: {
|
||||||
|
apps?: any[];
|
||||||
|
isAdmin?: boolean;
|
||||||
|
}) => {
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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} 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_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>
|
||||||
|
);
|
||||||
|
};
|
||||||
112
ui/components/sessions/EventCockpitDeck.tsx
Normal file
112
ui/components/sessions/EventCockpitDeck.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
export const EventCockpitDeck = ({ eventPasses }: { eventPasses: any[] }) => {
|
||||||
|
if (!eventPasses || eventPasses.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
86
ui/components/sessions/ScopeModal.tsx
Normal file
86
ui/components/sessions/ScopeModal.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
export const ScopeModal = ({ apps = [] }: { apps?: any[] }) => {
|
||||||
|
return (
|
||||||
|
<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);"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
302
ui/components/sessions/WorkshopDrawer.tsx
Normal file
302
ui/components/sessions/WorkshopDrawer.tsx
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
export const WorkshopDrawer = ({ apps = [] }: { apps?: any[] }) => {
|
||||||
|
return (
|
||||||
|
<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} key={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>
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user