fix(events): ensure getEventWithAttendees matches attendees by FK and short username, returns seats_claimed, and updates guest drawer

This commit is contained in:
Tyler Gillispie 2026-08-27 23:41:54 -07:00
parent 2bfbdd602b
commit 64f758e7cb
4 changed files with 61 additions and 5 deletions

View File

@ -735,12 +735,20 @@ async function openAttendeesDrawer(eventId, eventName) {
if (res.ok && data.success) {
const attendees = data.attendees || [];
const claimedElem = document.getElementById("guestDrawerClaimed");
if (claimedElem) claimedElem.textContent = attendees.length;
if (data.event) {
const claimedElem = document.getElementById("guestDrawerClaimed");
if (claimedElem) {
claimedElem.textContent = data.event.seats_claimed !== undefined
? data.event.seats_claimed
: attendees.length;
}
const maxElem = document.getElementById("guestDrawerMax");
if (maxElem) maxElem.textContent = data.event.max_seats;
if (maxElem) {
maxElem.textContent = Number(data.event.max_seats) > 0
? data.event.max_seats
: "∞";
}
const expiresAtElem = document.getElementById("guestDrawerExpiresAt");
if (expiresAtElem && data.event.expires_at) {

View File

@ -216,7 +216,11 @@ eventsActionsRoutes.get("/api/events/:id/attendees", async (c) => {
success: true,
attendees: data.attendees,
event: {
id: data.event.id,
name: data.event.name,
slug: data.event.slug,
max_seats: data.event.max_seats,
seats_claimed: data.event.seats_claimed,
expires_at: data.event.expires_at,
},
});

View File

@ -237,18 +237,22 @@ export async function getEventWithAttendees(
isGlobalAdmin: boolean,
) {
const event = await sqlWrapper.sql`
SELECT id, slug, max_seats, expires_at FROM event_passes
SELECT id, name, slug, max_seats, seats_claimed, expires_at FROM event_passes
WHERE id = ${eventId}
AND (created_by = ${userId} OR ${isGlobalAdmin})
`.then((res: any) => res[0]);
if (!event) return null;
const shortId = String(event.id).split("-")[0];
const shortPattern = `guest_${shortId}_%`;
const attendees = await sqlWrapper.sql`
SELECT 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
FROM sessions s
JOIN users u ON s.user_id = u.id
WHERE u.event_pass_id = ${eventId}
OR u.username LIKE ${shortPattern}
ORDER BY s.created_at DESC
`;

View File

@ -10,6 +10,7 @@ import {
EventCockpitDeckFragment,
WorkshopPassDrawerFragment,
} from "../events/fragments.tsx";
import { AppLaunchpadPageFragment } from "../dashboard/launchpad_fragments.tsx";
Deno.test("[Sessions] GET /dashboard unauthenticated redirects to /login", async () => {
const req = new Request("http://localhost/dashboard");
@ -155,3 +156,42 @@ Deno.test("[Sessions] GET /dashboard/passkeys unauthenticated redirects to /logi
assertEquals(res.status, 302);
assertEquals(res.headers.get("location"), "/login");
});
Deno.test("[Sessions] AppLaunchpadPageFragment renders authorized workload cards and roles", () => {
const mockApps = [
{
id: "app-1",
name: "ed-droid",
description: "Autonomous Agent UI",
domain: "ed.atyg.org",
role: "Guest (Viewer)",
},
{
id: "app-2",
name: "grafana",
description: "Metrics Dashboard",
domain: "grafana.atyg.org",
role: "Admin",
},
];
const html = <AppLaunchpadPageFragment apps={mockApps} isAdmin={false} />;
const str = String(html);
assertStringIncludes(str, "Application Launchpad");
assertStringIncludes(str, "ed-droid");
assertStringIncludes(str, "ed.atyg.org");
assertStringIncludes(str, "Guest (Viewer)");
assertStringIncludes(str, "https://ed.atyg.org");
assertStringIncludes(str, "grafana");
assertStringIncludes(str, "badge-success");
});
Deno.test("[Sessions] AppLaunchpadPageFragment renders empty state when no apps authorized", () => {
const html = <AppLaunchpadPageFragment apps={[]} isAdmin={false} />;
const str = String(html);
assertStringIncludes(str, "No Authorized Applications");
assertStringIncludes(
str,
"You do not have active role grants for any workloads.",
);
});