feat(sessions): include active event group sessions in unified session table and mobile deck without removing event deck

This commit is contained in:
Tyler Gillispie 2026-08-27 22:47:18 -07:00
parent 2fa8222062
commit 77a82cbe84
5 changed files with 416 additions and 191 deletions

View File

@ -316,7 +316,7 @@ async function handleCreateEvent(e) {
} finally { } finally {
if (btn) { if (btn) {
btn.disabled = false; btn.disabled = false;
btn.textContent = "🎟️ Create Event"; btn.textContent = "Create Event";
} }
} }
} }

View File

@ -186,9 +186,9 @@ export const WorkshopPassDrawerFragment = ({
type="submit" type="submit"
id="submitEventBtn" id="submitEventBtn"
class="btn-primary" class="btn-primary"
style="min-height: 42px; background: var(--success); border-color: var(--success);" style="min-height: 42px;"
> >
🎟 Create Event Create Event
</button> </button>
<button <button
type="button" type="button"

View File

@ -1,16 +1,23 @@
export const SessionDeckFragment = ({ export const SessionDeckFragment = ({
sessions, sessions,
currentSessionId, currentSessionId,
eventPasses = [],
}: { }: {
sessions: any[]; sessions: any[];
currentSessionId: string; currentSessionId: string;
eventPasses?: any[];
}) => { }) => {
const activeEvents = (eventPasses || []).filter(
(e) => !e.expires_at || new Date(e.expires_at).getTime() > Date.now(),
);
const hasItems = sessions.length > 0 || activeEvents.length > 0;
return ( return (
<div <div
class="mobile-only" class="mobile-only"
style="display: flex; flex-direction: column; gap: 1rem;" style="display: flex; flex-direction: column; gap: 1rem;"
> >
{sessions.length === 0 {!hasItems
? ( ? (
<div class="card" style="text-align: center; padding: 2rem;"> <div class="card" style="text-align: center; padding: 2rem;">
<p style="color: var(--text-muted); margin: 0;"> <p style="color: var(--text-muted); margin: 0;">
@ -19,104 +26,203 @@ export const SessionDeckFragment = ({
</div> </div>
) )
: ( : (
sessions.map((session) => { <>
const isCurrent = session.id === currentSessionId; {/* Event Group Sessions */}
const isAgent = !!session.is_agent; {activeEvents.map((event) => {
const scopes = Array.isArray(session.custom_scopes) const claimed = Number(event.seats_claimed) || 0;
? session.custom_scopes const maxSeats = Number(event.max_seats) || 0;
: []; const capacityStr = maxSeats > 0
? `${claimed}/${maxSeats}`
: `${claimed}/∞`;
return ( return (
<div class="card" key={session.id} style="margin-bottom: 0;"> <div
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;"> class="card"
<div style="display: flex; align-items: center; gap: 0.65rem;"> key={`event-${event.id}`}
<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;"> style="margin-bottom: 0; border-left: 4px solid var(--primary);"
{isAgent ? "🔑" : isCurrent ? "📱" : "💻"} >
</div> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
<div> <div style="display: flex; align-items: center; gap: 0.65rem;">
<div style="font-weight: 700; font-size: 0.95rem; color: var(--text-primary);"> <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;">
{session.label || 🎟
(isCurrent ? "This Device" : "Remote Device")}
</div> </div>
<div style="font-size: 0.75rem; color: var(--text-muted); font-family: monospace;"> <div>
{session.id.substring(0, 14)}... <div style="font-weight: 700; font-size: 0.95rem; color: var(--text-primary);">
{event.name}
</div>
<div style="font-size: 0.75rem; color: var(--text-muted); font-family: monospace;">
PIN: {event.pin_code || "---"} · {event.slug}
</div>
</div> </div>
</div> </div>
</div>
<div style="display: flex; align-items: center; gap: 0.5rem;"> <div style="display: flex; align-items: center; gap: 0.5rem;">
{isAgent <span class="badge badge-info">Event</span>
? <span class="badge badge-info">Delegated</span>
: isCurrent
? <span class="badge badge-success">Active Now</span>
: <span class="badge badge-secondary">Active</span>}
{!isCurrent && (
<button <button
type="button" type="button"
class="btn-danger revoke-btn" class="btn-danger end-event-btn"
data-session-id={session.id} data-event-id={event.id}
data-event-name={event.name}
style="padding: 0.15rem 0.5rem; font-size: 0.75rem; min-height: 24px; border-radius: var(--radius-sm);" style="padding: 0.15rem 0.5rem; font-size: 0.75rem; min-height: 24px; border-radius: var(--radius-sm);"
> >
🗑 Revoke 🗑 Revoke
</button> </button>
</div>
</div>
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 1rem; line-height: 1.6;">
<div style="margin-bottom: 0.25rem;">
<strong>Scope & Seats:</strong> {event.app_name
? `${event.app_name} (${event.role || "viewer"})`
: (event.role || "Event Pass")} ·{" "}
<strong>{capacityStr} Claimed</strong>
</div>
<div>
<span
class="countdown-pill"
data-expires-at={event.expires_at}
>
0h 0m left · (Expires{" "}
{new Date(event.expires_at).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})})
</span>
</div>
</div>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<button
type="button"
class="btn-outline"
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.8rem;"
onclick={`openAttendeesDrawer('${event.id}', '${
encodeURIComponent(event.name)
}')`}
>
👥 Guests ({claimed})
</button>
<button
type="button"
class="btn-outline"
style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.8rem;"
onclick={`rotatePin('${event.id}')`}
>
🔄 PIN
</button>
<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.8rem;"
>
+1h
</button>
</div>
</div>
);
})}
{/* Standard Interactive & 1:1 Delegated Sessions */}
{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>
<div style="display: flex; align-items: center; gap: 0.5rem;">
{isAgent
? <span class="badge badge-info">Delegated</span>
: isCurrent
? <span class="badge badge-success">Active Now</span>
: <span class="badge badge-secondary">Active</span>}
{!isCurrent && (
<button
type="button"
class="btn-danger revoke-btn"
data-session-id={session.id}
style="padding: 0.15rem 0.5rem; font-size: 0.75rem; min-height: 24px; border-radius: var(--radius-sm);"
>
🗑 Revoke
</button>
)}
</div>
</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>
<span
class="countdown-pill"
data-expires-at={session.expires_at}
>
0h 0m left · (Expires{" "}
{new Date(session.expires_at).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})})
</span>
</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>
</div>
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 1rem; line-height: 1.6;">
{isAgent && ( {isAgent && (
<div style="margin-bottom: 0.25rem;"> <div style="display: flex; gap: 0.5rem;">
<strong>Scopes:</strong>{" "} <button
{scopes.length > 0 ? scopes.join(", ") : "Delegated"} type="button"
</div> class="btn-outline"
)} style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.8rem;"
<div> onclick={`extendSession('${session.id}', 1)`}
<span >
class="countdown-pill" +1h Extend
data-expires-at={session.expires_at} </button>
> <button
0h 0m left · (Expires{" "} type="button"
{new Date(session.expires_at).toLocaleTimeString([], { class="btn-outline"
hour: "2-digit", style="flex: 1; justify-content: center; min-height: 38px; font-size: 0.8rem;"
minute: "2-digit", onclick={`openEditScopesModal('${session.id}', '${
})}) session.label || "Delegated"
</span> }', ${JSON.stringify(JSON.stringify(scopes))})`}
</div> >
{session.last_activity_action && ( Scopes
<div> </button>
<strong>Last Active:</strong>{" "}
{session.last_activity_action} ({new Date(
session.last_activity_at || session.created_at,
).toLocaleTimeString()})
</div> </div>
)} )}
</div> </div>
);
{isAgent && ( })}
<div style="display: flex; gap: 0.5rem;"> </>
<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>
</div>
)}
</div>
);
})
)} )}
</div> </div>
); );

View File

@ -154,12 +154,14 @@ export const SessionsPageFragment = ({
<SessionTableFragment <SessionTableFragment
sessions={sessions} sessions={sessions}
currentSessionId={currentSessionId} currentSessionId={currentSessionId}
eventPasses={eventPasses}
/> />
{/* Mobile Adaptive Cards View */} {/* Mobile Adaptive Cards View */}
<SessionDeckFragment <SessionDeckFragment
sessions={sessions} sessions={sessions}
currentSessionId={currentSessionId} currentSessionId={currentSessionId}
eventPasses={eventPasses}
/> />
<style> <style>

View File

@ -1,10 +1,17 @@
export const SessionTableFragment = ({ export const SessionTableFragment = ({
sessions, sessions,
currentSessionId, currentSessionId,
eventPasses = [],
}: { }: {
sessions: any[]; sessions: any[];
currentSessionId: string; currentSessionId: string;
eventPasses?: any[];
}) => { }) => {
const activeEvents = (eventPasses || []).filter(
(e) => !e.expires_at || new Date(e.expires_at).getTime() > Date.now(),
);
const hasItems = sessions.length > 0 || activeEvents.length > 0;
return ( return (
<div class="card desktop-only" style="display: none;"> <div class="card desktop-only" style="display: none;">
<div class="table-container"> <div class="table-container">
@ -13,13 +20,13 @@ export const SessionTableFragment = ({
<tr> <tr>
<th>Type & Label</th> <th>Type & Label</th>
<th>Permissions</th> <th>Permissions</th>
<th>Activity</th> <th>Activity / Capacity</th>
<th>Expires</th> <th>Expires</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{sessions.length === 0 {!hasItems
? ( ? (
<tr> <tr>
<td <td
@ -31,121 +38,231 @@ export const SessionTableFragment = ({
</tr> </tr>
) )
: ( : (
sessions.map((session) => { <>
const isCurrent = session.id === currentSessionId; {/* Event Group Sessions */}
const isAgent = !!session.is_agent; {activeEvents.map((event) => {
const scopes = Array.isArray(session.custom_scopes) const claimed = Number(event.seats_claimed) || 0;
? session.custom_scopes const maxSeats = Number(event.max_seats) || 0;
: []; const capacityStr = maxSeats > 0
? `${claimed}/${maxSeats}`
: `${claimed}/∞`;
return ( return (
<tr key={session.id}> <tr
<td> key={`event-${event.id}`}
<div style="display: flex; align-items: center; gap: 0.5rem;"> style="background: var(--surface-muted);"
<span style="font-size: 1.1rem;"> >
{isAgent ? "🔑" : isCurrent ? "📱" : "💻"} <td>
</span> <div style="display: flex; align-items: center; gap: 0.5rem;">
<div> <span style="font-size: 1.1rem;">🎟</span>
<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> <div>
<span style="color: var(--primary); font-family: monospace; font-size: 0.8rem;"> <strong style="color: var(--text-primary);">
{session.last_activity_action} {event.name}
</span> </strong>
<div style="font-size: 0.75rem; color: var(--text-muted);"> <div style="font-size: 0.75rem; color: var(--text-muted); font-family: monospace;">
{new Date( PIN: {event.pin_code || "---"} · {event.slug}
session.last_activity_at ||
session.created_at,
).toLocaleTimeString()}
</div> </div>
</div> </div>
) </div>
: ( </td>
<span> <td>
{new Date(session.created_at) <span class="badge badge-info">
.toLocaleDateString()} {event.app_name
</span> ? `${event.app_name} (${event.role || "viewer"})`
)} : (event.role || "Event Pass")}
</td> </span>
<td style="color: var(--text-secondary); font-size: 0.85rem;"> </td>
<span <td style="color: var(--text-secondary); font-size: 0.85rem;">
class="countdown-pill" <div>
data-expires-at={session.expires_at} <strong style="color: var(--primary); font-size: 0.85rem;">
> {capacityStr} Seats
0h 0m left · (Expires{" "} </strong>
{new Date(session.expires_at).toLocaleTimeString([], { <div style="font-size: 0.75rem; color: var(--text-muted);">
hour: "2-digit", Claimed
minute: "2-digit", </div>
})}) </div>
</span> </td>
</td> <td style="color: var(--text-secondary); font-size: 0.85rem;">
<td> <span
<div style="display: flex; gap: 0.35rem; align-items: center;"> class="countdown-pill"
{isAgent && ( data-expires-at={event.expires_at}
<> >
<button 0h 0m left · (Expires{" "}
type="button" {new Date(event.expires_at).toLocaleTimeString([], {
class="btn-outline" hour: "2-digit",
style="padding: 0.25rem 0.5rem; font-size: 0.75rem; min-height: 30px;" minute: "2-digit",
onclick={`extendSession('${session.id}', 1)`} })})
title="Extend session by 1 hour" </span>
> </td>
+1h <td>
</button> <div style="display: flex; gap: 0.35rem; align-items: center; flex-wrap: wrap;">
<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 <button
type="button" type="button"
class="btn-danger revoke-btn" class="btn-outline"
data-session-id={session.id} style="padding: 0.25rem 0.5rem; font-size: 0.75rem; min-height: 30px;"
onclick={`openAttendeesDrawer('${event.id}', '${
encodeURIComponent(event.name)
}')`}
title="Manage claimed guest seats"
>
👥 Guests ({claimed})
</button>
<button
type="button"
class="btn-outline"
style="padding: 0.25rem 0.5rem; font-size: 0.75rem; min-height: 30px;"
onclick={`rotatePin('${event.id}')`}
title="Rotate PIN & Slug"
>
🔄 Rotate PIN
</button>
<button
type="button"
class="btn-outline extend-event-btn"
data-event-id={event.id}
style="padding: 0.25rem 0.5rem; font-size: 0.75rem; min-height: 30px;"
title="Extend event and guest sessions by 1 hour"
>
+1h
</button>
<button
type="button"
class="btn-danger end-event-btn"
data-event-id={event.id}
data-event-name={event.name}
style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 30px;" style="padding: 0.25rem 0.65rem; font-size: 0.8rem; min-height: 30px;"
title="End event and revoke all guest sessions"
> >
Revoke Revoke
</button> </button>
)} </div>
</div> </td>
</td> </tr>
</tr> );
); })}
})
{/* Standard Interactive & 1:1 Delegated Sessions */}
{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;">
<span
class="countdown-pill"
data-expires-at={session.expires_at}
>
0h 0m left · (Expires{" "}
{new Date(session.expires_at).toLocaleTimeString(
[],
{
hour: "2-digit",
minute: "2-digit",
},
)})
</span>
</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> </tbody>
</table> </table>