diff --git a/server/routes/events.ts b/server/routes/events.ts index feacf7f..aebe12b 100644 --- a/server/routes/events.ts +++ b/server/routes/events.ts @@ -31,36 +31,50 @@ eventRoutes.post("/api/events/:id/rotate-pin", async (c) => { const eventId = c.req.param("id"); - // Generate new PIN - const randPin = Math.floor(100000 + Math.random() * 900000).toString(); - const newPinCode = randPin.substring(0, 3) + "-" + randPin.substring(3); - try { - const eventResult = await sqlWrapper.sql` - UPDATE event_passes - SET pin_code = ${newPinCode} + const event = await sqlWrapper.sql` + SELECT slug, name FROM event_passes WHERE id = ${eventId} AND (created_by = ${user.userId} OR ${await isGlobalAdmin(user.userId)}) AND is_active = TRUE - RETURNING pin_code `; - if (!eventResult || eventResult.length === 0) { + if (!event || event.length === 0) { return c.json( { error: "Event not found, inactive, or unauthorized" }, 404, ); } + // Generate new PIN + const randPin = Math.floor(100000 + Math.random() * 900000).toString(); + const newPinCode = randPin.substring(0, 3) + "-" + randPin.substring(3); + + // Generate new Slug + const baseSlug = event[0].slug.replace(/-[a-z0-9]{4}$/, ""); + const newSuffix = Math.random().toString(36).substring(2, 6); + const newSlug = `${baseSlug}-${newSuffix}`; + + const updateResult = await sqlWrapper.sql` + UPDATE event_passes + SET pin_code = ${newPinCode}, slug = ${newSlug} + WHERE id = ${eventId} + RETURNING pin_code, slug + `; + auditWrapper.auditLog( user.userId, - "event_pin_rotated", + "event_ingress_rotated", eventId, {}, getClientIp(c), ); - return c.json({ success: true, pinCode: eventResult[0].pin_code }); + return c.json({ + success: true, + pinCode: updateResult[0].pin_code, + slug: updateResult[0].slug, + }); } catch (e: any) { console.error("[Events] Failed to rotate event PIN:", e); return c.json({ error: "Failed to rotate PIN" }, 500); @@ -206,7 +220,7 @@ eventRoutes.post("/api/events/:id/extend", async (c) => { try { const eventResult = await sqlWrapper.sql` UPDATE event_passes - SET expires_at = expires_at + interval '${extendHours} hours' + SET expires_at = GREATEST(expires_at, NOW()) + interval '${extendHours} hours' WHERE id = ${eventId} AND (created_by = ${user.userId} OR ${await isGlobalAdmin( user.userId, )}) AND is_active = TRUE diff --git a/server/tests/events.test.ts b/server/tests/events.test.ts index 79b6351..4b70fd1 100644 --- a/server/tests/events.test.ts +++ b/server/tests/events.test.ts @@ -232,6 +232,59 @@ Deno.test("Multi-Claim Event Passes & Join Endpoints", async (t) => { }, ); + await t.step( + "POST /api/events/:id/rotate-pin rotates both pin and slug", + async () => { + const valkeyGetStub = stub(valkey, "get", (key: any) => { + if (String(key) === "admin-session") { + return Promise.resolve( + JSON.stringify({ uuid: "admin-uuid", username: "tylerg" }), + ); + } + return Promise.resolve(null); + }); + + const originalSql = sqlWrapper.sql; + let updateEventCalled = false; + let newSlug = ""; + + sqlWrapper.sql = ((strings: any, ..._values: any[]) => { + const query = Array.isArray(strings) + ? strings.join("?") + : String(strings); + if (query.includes("SELECT slug, name FROM event_passes")) { + return Promise.resolve([{ slug: "deno-lab-1a2b", name: "Deno Lab" }]); + } + if (query.includes("UPDATE event_passes") && query.includes("slug =")) { + updateEventCalled = true; + newSlug = _values[1]; // second bound parameter is the slug + return Promise.resolve([{ pin_code: _values[0], slug: newSlug }]); + } + return Promise.resolve([]); + }) as any; + + try { + const res = await app.request("/api/events/evt-123/rotate-pin", { + method: "POST", + headers: { + Authorization: "Bearer admin-session", + }, + }); + + assertEquals(res.status, 200); + const json = await res.json(); + assert(json.success === true); + assert(updateEventCalled); + assert(json.slug.startsWith("deno-lab-")); + assert(json.slug !== "deno-lab-1a2b"); + assertEquals(json.slug, newSlug); + } finally { + sqlWrapper.sql = originalSql; + valkeyGetStub.restore(); + } + }, + ); + await t.step( "POST /api/join enforces 5 failed attempts rate limit per IP", async () => { @@ -491,7 +544,7 @@ Deno.test("Multi-Claim Event Passes & Join Endpoints", async (t) => { : String(strings); if ( query.includes("UPDATE event_passes") && - query.includes("expires_at = expires_at + interval") + query.includes("expires_at = GREATEST(expires_at, NOW()) + interval") ) { updateEventCalled = true; return Promise.resolve([{ diff --git a/tasks/new/2026-0826.06.gem.feat.ui.sessions-and-events-final-polish-2305.ph6.md b/tasks/new/2026-0826.06.gem.feat.ui.sessions-and-events-final-polish-2305.ph6.md index 0c8b084..ca26dd0 100644 --- a/tasks/new/2026-0826.06.gem.feat.ui.sessions-and-events-final-polish-2305.ph6.md +++ b/tasks/new/2026-0826.06.gem.feat.ui.sessions-and-events-final-polish-2305.ph6.md @@ -12,28 +12,57 @@ - `server/tests/events.test.ts` - `ui/ui_scripts.test.ts` - **Core Objective:** Implement Phase 6 Final Polish & Hardening: - 1. Unify nomenclature & IA across pages and drawers (`Sessions & Events`, `Delegate Session`, `Events`, `Sessions`). - 2. Implement universal ingress credential rotation (rotates both 6-digit PIN and event slug suffix simultaneously). - 3. Fix expired event extend math bug using `GREATEST(expires_at, NOW()) + interval`. - 4. Replace broken compact mode with strictly bounded, 2-Row Compact Cards with inline copy pills and zero text/border collisions. - 5. Implement standardized natural `ExpiryBadge` format (`[Date] Β· [Time] Β· [Urgency Badge]`) and symmetrical inverted start-time telemetry in the Guest Drawer. - 6. Upgrade segmented view mode toggles and WAI-ARIA delegation tabs with high-contrast active states. - 7. Reset drawer state machine on open and replace verbose/mock buttons with clean `[ OK ]`. + 1. Unify nomenclature & IA across pages and drawers (`Sessions & Events`, + `Delegate Session`, `Events`, `Sessions`). + 2. Implement universal ingress credential rotation (rotates both 6-digit PIN + and event slug suffix simultaneously). + 3. Fix expired event extend math bug using + `GREATEST(expires_at, NOW()) + interval`. + 4. Replace broken compact mode with strictly bounded, 2-Row Compact Cards with + inline copy pills and zero text/border collisions. + 5. Implement standardized natural `ExpiryBadge` format + (`[Date] Β· [Time] Β· [Urgency Badge]`) and symmetrical inverted start-time + telemetry in the Guest Drawer. + 6. Upgrade segmented view mode toggles and WAI-ARIA delegation tabs with + high-contrast active states. + 7. Reset drawer state machine on open and replace verbose/mock buttons with + clean `[ OK ]`. - **Dependencies:** None. -- **Additional Important Notes:** Must remain 100% pure React-free Hono SSR JSX. All client interactions in `SessionsScript.tsx` must use native vanilla JavaScript DOM APIs. +- **Additional Important Notes:** Must remain 100% pure React-free Hono SSR JSX. + All client interactions in `SessionsScript.tsx` must use native vanilla + JavaScript DOM APIs. --- ## 2. Architectural Considerations & Risks - **Risks:** - - **Ingress Rotation Leakage & Active Session Disruption:** When rotating event ingress credentials (`pin_code` and `slug`), existing active guest sessions must remain valid and uninterrupted. Only subsequent unauthenticated join attempts using the old PIN, old Direct Link, or old CLI command must be rejected (404 / Invalid Code). - - **Expired Event Extending Edge Cases:** When an organizer extends an event that expired in the past, `expires_at + interval '1 hour'` would leave the expiry in the past. Using `GREATEST(expires_at, NOW()) + interval '${extendHours} hours'` guarantees the new expiration time is set relative to the current moment. - - **DOM Boundary & Overflow Bleed:** The previous compact mode forced elements into a single unconstrained horizontal flex line, causing text collisions and input boxes bleeding off the desktop screen. The new 2-Row Compact Card must enforce strict CSS bounding (`box-sizing: border-box; max-width: 100%; overflow: hidden; text-overflow: ellipsis;`). - - **State Machine Staleness:** Reopening the delegation drawer must unconditionally reset the form back to State 1 (clean creation form with `Single Session` default tab) rather than leaving the stale success screen visible. + - **Ingress Rotation Leakage & Active Session Disruption:** When rotating + event ingress credentials (`pin_code` and `slug`), existing active guest + sessions must remain valid and uninterrupted. Only subsequent + unauthenticated join attempts using the old PIN, old Direct Link, or old CLI + command must be rejected (404 / Invalid Code). + - **Expired Event Extending Edge Cases:** When an organizer extends an event + that expired in the past, `expires_at + interval '1 hour'` would leave the + expiry in the past. Using + `GREATEST(expires_at, NOW()) + interval '${extendHours} hours'` guarantees + the new expiration time is set relative to the current moment. + - **DOM Boundary & Overflow Bleed:** The previous compact mode forced elements + into a single unconstrained horizontal flex line, causing text collisions + and input boxes bleeding off the desktop screen. The new 2-Row Compact Card + must enforce strict CSS bounding + (`box-sizing: border-box; max-width: 100%; overflow: hidden; text-overflow: ellipsis;`). + - **State Machine Staleness:** Reopening the delegation drawer must + unconditionally reset the form back to State 1 (clean creation form with + `Single Session` default tab) rather than leaving the stale success screen + visible. - **Alternatives:** - - *Single-Line vs. 2-Row Compact Cards:* Single-line cards inevitably drop essential copy actions or clip text on viewports under 1200px. A structured 2-Row Compact Card (~70px height) preserves 100% of copy handoffs (PIN, Link, CLI) and key metrics (Seats, Status, Expiry) while preventing all layout collisions. + - _Single-Line vs. 2-Row Compact Cards:_ Single-line cards inevitably drop + essential copy actions or clip text on viewports under 1200px. A structured + 2-Row Compact Card (~70px height) preserves 100% of copy handoffs (PIN, + Link, CLI) and key metrics (Seats, Status, Expiry) while preventing all + layout collisions. --- @@ -46,17 +75,20 @@ ```typescript const randPin = Math.floor(100000 + Math.random() * 900000).toString(); const newPinCode = randPin.substring(0, 3) + "-" + randPin.substring(3); - + // Extract base slug prefix and generate fresh random 4-char suffix - const event = await sqlWrapper.sql`SELECT slug, name FROM event_passes WHERE id = ${eventId}...`; + const event = await sqlWrapper + .sql`SELECT slug, name FROM event_passes WHERE id = ${eventId}...`; const baseSlug = event[0].slug.replace(/-[a-z0-9]{4}$/, ""); const newSuffix = Math.random().toString(36).substring(2, 6); const newSlug = `${baseSlug}-${newSuffix}`; ``` - - Update database: `UPDATE event_passes SET pin_code = ${newPinCode}, slug = ${newSlug} WHERE id = ${eventId}...` + - Update database: + `UPDATE event_passes SET pin_code = ${newPinCode}, slug = ${newSlug} WHERE id = ${eventId}...` - Audit log `event_ingress_rotated`. - Return `{ success: true, pinCode: newPinCode, slug: newSlug }`. - - Note: Existing active attendee sessions (`username LIKE 'guest_...'`) authenticate via session cookies/Valkey tokens and are unaffected. + - Note: Existing active attendee sessions (`username LIKE 'guest_...'`) + authenticate via session cookies/Valkey tokens and are unaffected. 2. **Resilient Event Extension Math (`POST /api/events/:id/extend`):** - Fix SQL to calculate new expiry from `GREATEST(expires_at, NOW())`: @@ -68,18 +100,24 @@ ``` 3. **Guest Attendee Telemetry (`GET /api/events/:id/attendees`):** - - Ensure query returns `s.id, s.label, s.is_paused, s.created_at, s.expires_at, s.last_activity_at, s.last_activity_action, u.username, u.display_name`. + - Ensure query returns + `s.id, s.label, s.is_paused, s.created_at, s.expires_at, s.last_activity_at, s.last_activity_action, u.username, u.display_name`. --- ### Phase 2: Page Hierarchy, Nomenclature & Heading Cleanup (`SessionsPage.tsx`) 1. **Page Title:** - - Set top `

` in `SessionsPage.tsx` to **`Sessions & Events`** with subtitle *"Manage logins, mint 1:1 delegated tokens, or launch multi-claim workshop events."* + - Set top `

` in `SessionsPage.tsx` to **`Sessions & Events`** with + subtitle _"Manage logins, mint 1:1 delegated tokens, or launch multi-claim + workshop events."_ 2. **Remove Duplicate Headings:** - - Remove redundant `

Event Passes

` from `SessionsPage.tsx`. The section header is exclusively rendered inside `EventCockpitDeck.tsx` as `

Events

` alongside the view toggle. + - Remove redundant `

Event Passes

` from `SessionsPage.tsx`. The + section header is exclusively rendered inside `EventCockpitDeck.tsx` as + `

Events

` alongside the view toggle. 3. **Sessions Section:** - - Retain `

Sessions

` heading with subtitle *"Direct device logins, passkey authentications, and delegated agent tokens."* + - Retain `

Sessions

` heading with subtitle _"Direct device logins, + passkey authentications, and delegated agent tokens."_ --- @@ -88,33 +126,46 @@ 1. **Section Header & High-Contrast View Toggle:** - Header: `

Events

`. - Toggle buttons (`[ πŸ—‚οΈ Grid ]` and `[ πŸ“‹ Compact ]`): - - Active style: `background: var(--primary); color: #ffffff; font-weight: 700; box-shadow: 0 1px 3px rgba(0,0,0,0.3); border-radius: var(--radius-sm);` - - Inactive style: `background: transparent; color: var(--text-muted); opacity: 0.75;` + - Active style: + `background: var(--primary); color: #ffffff; font-weight: 700; box-shadow: 0 1px 3px rgba(0,0,0,0.3); border-radius: var(--radius-sm);` + - Inactive style: + `background: transparent; color: var(--text-muted); opacity: 0.75;` - Include `aria-pressed="true/false"`. 2. **2-Row Compact Card Layout (`.compact-view .event-card`):** - - Container: Strictly bounded flex/grid (~70px height), `box-sizing: border-box; overflow: hidden; padding: 0.75rem 1rem;`. + - Container: Strictly bounded flex/grid (~70px height), + `box-sizing: border-box; overflow: hidden; padding: 0.75rem 1rem;`. - **Row 1 (Metadata Header):** - - Left: Event title with ellipsis (`max-width: 280px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 700; color: var(--text-primary);`). - - Center/Right: Subtle dot `Β·` + `renderExpiryPill(expiresAt)` + Subtle dot `Β·` + `[ N/Max Seats ]` + `[ Status Badge ]`. + - Left: Event title with ellipsis + (`max-width: 280px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 700; color: var(--text-primary);`). + - Center/Right: Subtle dot `Β·` + `renderExpiryPill(expiresAt)` + Subtle dot + `Β·` + `[ N/Max Seats ]` + `[ Status Badge ]`. - **Row 2 (1-Click Handoffs & Action Pinned Right):** - - Left (Handoff Pills): `[ PIN: 241-881 (Copy) ]`, `[ Link (Copy) ]`, `[ CLI (Copy) ]` using compact inline pills (`padding: 2px 6px; font-size: 0.75rem;`). - - Right (Action Buttons): Compact `[ πŸ‘₯ Guests (N) ]` and `[ πŸ”„ +1h ]` (or `[ πŸ”„ Reopen (+1h) ]` if expired). - - **Delete Mock Code:** Completely remove the `[ β–Έ Details ]` button and its `alert(...)` placeholder. + - Left (Handoff Pills): `[ PIN: 241-881 (Copy) ]`, `[ Link (Copy) ]`, + `[ CLI (Copy) ]` using compact inline pills + (`padding: 2px 6px; font-size: 0.75rem;`). + - Right (Action Buttons): Compact `[ πŸ‘₯ Guests (N) ]` and `[ πŸ”„ +1h ]` (or + `[ πŸ”„ Reopen (+1h) ]` if expired). + - **Delete Mock Code:** Completely remove the `[ β–Έ Details ]` button and its + `alert(...)` placeholder. 3. **Grid Card Polish:** - - Replace detached CLI `
` box with a unified **Integrated Expanding CLI Component**: + - Replace detached CLI `
` box with a unified **Integrated Expanding + CLI Component**: - Single-line snippet when collapsed with `[ Copy ]` and `[ β–Ύ ]` toggle. - - Expands downward into multiline highlighted command block on toggle click. + - Expands downward into multiline highlighted command block on toggle + click. - Fix double arrow marker bug (`list-style: none;`). - - Standardize `[ πŸ”„ Rotate Credentials ]` button to invoke multi-field rotation. + - Standardize `[ πŸ”„ Rotate Credentials ]` button to invoke multi-field + rotation. --- ### Phase 4: Standardized Natural Expiry & Start-Time Telemetry (`SessionsScript.tsx` & Drawers) 1. **Natural Expiry Formatter Helper (`formatNaturalExpiry(expiresAt)`):** - - Natural Date String: `Today` (if $<24$h), `Tomorrow` (if $<48$h), `MMM D` (if same year), `MMM D, YYYY` (if different year). + - Natural Date String: `Today` (if $<24$h), `Tomorrow` (if $<48$h), `MMM D` + (if same year), `MMM D, YYYY` (if different year). - Exact Time: `h:mm A` (e.g. `10:39 PM`). - Scaled Urgency Badge: - $<1$h: Amber `[ 45m left ]` @@ -126,11 +177,17 @@ - Tooltip: `title="${fullISODate}"` on hover/long-press. - Format: `[Date] Β· [Time] Β· [ Colored Urgency Badge ]`. -2. **Inverted Symmetrical Start-Time Formatter (`formatNaturalJoinTime(createdAt)`):** +2. **Inverted Symmetrical Start-Time Formatter + (`formatNaturalJoinTime(createdAt)`):** - In `EventGuestsDrawer.tsx`, render: - - **Header:** Bold `Seat #[N]` + `🟒 Active / ⏸️ Paused` badge + `[ ⏸️ Pause ]` + `[ πŸ—‘οΈ Revoke ]`. - - **Line 1:** `Joined Today Β· 2:15 PM Β· ` Active 7h 54m. - - **Line 2:** `Last Action: ${lastAction || 'ForwardAuth Ingress'} Β· ${timeAgo} Β· πŸ’» Web` (or `πŸ“Ÿ CLI`). + - **Header:** Bold `Seat #[N]` + `🟒 Active / ⏸️ Paused` badge + + `[ ⏸️ Pause ]` + `[ πŸ—‘οΈ Revoke ]`. + - **Line 1:** `Joined Today Β· 2:15 PM Β·` + Active + 7h 54m. + - **Line 2:** + `Last Action: ${lastAction || 'ForwardAuth Ingress'} Β· ${timeAgo} Β· πŸ’» Web` + (or `πŸ“Ÿ CLI`). - Remove redundant `guest_slug_seat` visual text (keep in tooltip only). --- @@ -138,22 +195,27 @@ ### Phase 5: Delegation Drawer Overhaul (`WorkshopDrawer.tsx` & `SessionsScript.tsx`) 1. **Drawer Nomenclature & WAI-ARIA High-Contrast Tabs:** - - Drawer Header: `

Delegate Session

` with subtitle *"Mint a 1:1 delegated token or launch a multi-seat workshop event."* + - Drawer Header: `

Delegate Session

` with subtitle _"Mint a 1:1 + delegated token or launch a multi-seat workshop event."_ - Tabs (`role="tablist"`): - **Tab 1 (`role="tab"`):** `Single Session` - - Subtitle hint: *For agents, CI/CD, or 1:1 delegation* + - Subtitle hint: _For agents, CI/CD, or 1:1 delegation_ - **Tab 2 (`role="tab"`):** `Multi-Claim Event` - - Subtitle hint: *For workshops, teams & guest pools* - - High-Contrast Active State: Solid `var(--primary)` background with white text (`#ffffff`), `aria-selected="true"`. - - Inactive State: Translucent muted background (`opacity: 0.75`), `aria-selected="false"`. + - Subtitle hint: _For workshops, teams & guest pools_ + - High-Contrast Active State: Solid `var(--primary)` background with white + text (`#ffffff`), `aria-selected="true"`. + - Inactive State: Translucent muted background (`opacity: 0.75`), + `aria-selected="false"`. 2. **Handoff State & Button Minimalist Polish:** - - Standardize all 3 copy buttons to uniform `btn-outline` (`Copy PIN`, `Copy URL`, `Copy 1-Liner`). + - Standardize all 3 copy buttons to uniform `btn-outline` (`Copy PIN`, + `Copy URL`, `Copy 1-Liner`). - Replace `"Dismiss"` with a clean, minimal button **`[ OK ]`**. 3. **State Machine Reset on Open:** - In `SessionsScript.tsx:openDelegateDrawer()`: - - Reset `#eventCreateState` to `display: block` and `#eventHandoffState` to `display: none`. + - Reset `#eventCreateState` to `display: block` and `#eventHandoffState` to + `display: none`. - Clear form inputs (`eventName`, `eventSlug`, `eventPinCode`, etc.). - Reset tab selection to `Single Session`. @@ -162,6 +224,7 @@ ## 4. Verification Plan ### Automated Tests + 1. **Universal Credential Rotation Test (`server/tests/events.test.ts`):** - Create event pass -> Call `POST /api/events/:id/rotate-pin`. - Verify response returns new `pinCode` AND new `slug`. @@ -173,11 +236,13 @@ - Call `POST /api/events/:id/extend` with `extendHours: 1`. - Verify `expires_at > NOW()` (approximately `NOW() + 1 hour`). 3. **UI Script & Formatter Tests (`ui/ui_scripts.test.ts`):** - - Unit test `formatNaturalExpiry` across all time horizons (<1h, <24h, 28d, 142d -> `4.5 mos`, 420d -> `1.2 yrs`, expired). + - Unit test `formatNaturalExpiry` across all time horizons (<1h, <24h, 28d, + 142d -> `4.5 mos`, 420d -> `1.2 yrs`, expired). - Test `formatNaturalJoinTime` inverted duration math. - Test drawer state machine reset logic. ### Quality Gate Commands + ```bash deno fmt --check deno task lint diff --git a/ui/components/SessionsPage.tsx b/ui/components/SessionsPage.tsx index 7acee41..9d1e2e6 100644 --- a/ui/components/SessionsPage.tsx +++ b/ui/components/SessionsPage.tsx @@ -37,11 +37,11 @@ export const SessionsPage = ({

- Sessions & Passes + Sessions & Events

- Manage logins, mint 1:1 ephemeral links & CLI tokens, or launch - multi-claim workshop event passes. + Manage logins, mint 1:1 delegated tokens, or launch multi-claim + workshop events.

@@ -57,9 +57,6 @@ export const SessionsPage = ({
-

- Event Passes -

{/* Delegate Session Drawer */} @@ -71,12 +68,12 @@ export const SessionsPage = ({
-

- Create New Pass / Session -

+ Delegate Session +
+

+ Mint a 1:1 delegated token or launch a multi-seat workshop event. +

-
+
@@ -175,11 +194,13 @@ export const SessionsPage = ({ transition: all 0.15s; background: transparent; color: var(--text-secondary); + opacity: 0.75; } .delegate-tab-btn.active { - background: var(--surface-card); - color: var(--primary); + background: var(--primary); + color: #ffffff; box-shadow: var(--shadow-xs); + opacity: 1; } `} diff --git a/ui/components/sessions/EventCockpitDeck.tsx b/ui/components/sessions/EventCockpitDeck.tsx index 83f88c3..377d8da 100644 --- a/ui/components/sessions/EventCockpitDeck.tsx +++ b/ui/components/sessions/EventCockpitDeck.tsx @@ -14,6 +14,7 @@ export const EventCockpitDeck = ({ eventPasses }: { eventPasses: any[] }) => { class="view-mode-btn active" onclick="setEventViewMode('grid')" aria-label="Grid View" + aria-pressed="true" > πŸ—‚οΈ Grid @@ -23,6 +24,7 @@ export const EventCockpitDeck = ({ eventPasses }: { eventPasses: any[] }) => { class="view-mode-btn" onclick="setEventViewMode('compact')" aria-label="Compact View" + aria-pressed="false" > πŸ“‹ Compact @@ -125,29 +127,31 @@ export const EventCockpitDeck = ({ eventPasses }: { eventPasses: any[] }) => {
-
- - CLI: - - - curl -sSL {Deno.env.get("RP_ID") - ? `https://${Deno.env.get("RP_ID")}` - : ""}/join/{event.slug}?format=env | source /dev/stdin - - -
- -
- - β–Έ Expand full CLI command +
+ + + CLI: + + + curl -sSL {Deno.env.get("RP_ID") + ? `https://${Deno.env.get("RP_ID")}` + : ""}/join/{event.slug}?format=env | source /dev/stdin + + + + [ β–Ύ ] +