fix(sessions): resolve attendees drawer TypeError and PostgreSQL interval syntax error
This commit is contained in:
parent
0a3c147880
commit
f6b5dd3992
@ -224,7 +224,7 @@ eventRoutes.post("/api/events/:id/extend", async (c) => {
|
||||
try {
|
||||
const eventResult = await sqlWrapper.sql`
|
||||
UPDATE event_passes
|
||||
SET expires_at = GREATEST(expires_at, NOW()) + interval '${extendHours} hours'
|
||||
SET expires_at = GREATEST(expires_at, NOW()) + INTERVAL '1 hour' * ${extendHours}
|
||||
WHERE id = ${eventId} AND (created_by = ${user.userId} OR ${await isGlobalAdmin(
|
||||
user.userId,
|
||||
)}) AND is_active = TRUE
|
||||
@ -242,7 +242,7 @@ eventRoutes.post("/api/events/:id/extend", async (c) => {
|
||||
|
||||
const sessionResult = await sqlWrapper.sql`
|
||||
UPDATE sessions
|
||||
SET expires_at = expires_at + interval '${extendHours} hours'
|
||||
SET expires_at = expires_at + INTERVAL '1 hour' * ${extendHours}
|
||||
WHERE user_id IN (
|
||||
SELECT id FROM users WHERE event_pass_id = ${eventId}
|
||||
)
|
||||
|
||||
@ -544,7 +544,9 @@ Deno.test("Multi-Claim Event Passes & Join Endpoints", async (t) => {
|
||||
: String(strings);
|
||||
if (
|
||||
query.includes("UPDATE event_passes") &&
|
||||
query.includes("expires_at = GREATEST(expires_at, NOW()) + interval")
|
||||
query.includes(
|
||||
"expires_at = GREATEST(expires_at, NOW()) + INTERVAL '1 hour'",
|
||||
)
|
||||
) {
|
||||
updateEventCalled = true;
|
||||
return Promise.resolve([{
|
||||
@ -554,7 +556,7 @@ Deno.test("Multi-Claim Event Passes & Join Endpoints", async (t) => {
|
||||
}
|
||||
if (
|
||||
query.includes("UPDATE sessions") &&
|
||||
query.includes("expires_at = expires_at + interval")
|
||||
query.includes("expires_at = expires_at + INTERVAL '1 hour'")
|
||||
) {
|
||||
updateSessionsCalled = true;
|
||||
return Promise.resolve([{ id: "sess-1" }, { id: "sess-2" }]);
|
||||
@ -663,4 +665,72 @@ Deno.test("Multi-Claim Event Passes & Join Endpoints", async (t) => {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await t.step(
|
||||
"GET /api/events/:id/attendees handles 404 for non-existent or unauthorized event",
|
||||
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;
|
||||
sqlWrapper.sql = (() => Promise.resolve([])) as any;
|
||||
|
||||
try {
|
||||
const res = await app.request("/api/events/non-existent-id/attendees", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: "Bearer admin-session",
|
||||
},
|
||||
});
|
||||
|
||||
assertEquals(res.status, 404);
|
||||
const json = await res.json();
|
||||
assertEquals(json.error, "Event not found or unauthorized");
|
||||
} finally {
|
||||
sqlWrapper.sql = originalSql;
|
||||
valkeyGetStub.restore();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await t.step(
|
||||
"POST /api/events/:id/extend handles 404 for non-existent or inactive event",
|
||||
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;
|
||||
sqlWrapper.sql = (() => Promise.resolve([])) as any;
|
||||
|
||||
try {
|
||||
const res = await app.request("/api/events/non-existent-id/extend", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer admin-session",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ extendHours: 1 }),
|
||||
});
|
||||
|
||||
assertEquals(res.status, 404);
|
||||
const json = await res.json();
|
||||
assertEquals(json.error, "Event not found, inactive, or unauthorized");
|
||||
} finally {
|
||||
sqlWrapper.sql = originalSql;
|
||||
valkeyGetStub.restore();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@ -0,0 +1,149 @@
|
||||
# TASK METADATA
|
||||
|
||||
- **Target Files:**
|
||||
- `server/db.ts`
|
||||
- `server/routes/events.ts`
|
||||
- `server/routes/sessions.ts`
|
||||
- `server/routes/forward_auth.ts`
|
||||
- `ui/components/events/VouchingModal.tsx`
|
||||
- `ui/components/sessions/EventGuestsDrawer.tsx`
|
||||
- `ui/components/sessions/SessionsScript.tsx`
|
||||
- `server/tests/vouching.test.ts`
|
||||
- **Core Objective:** Implement a decentralized Peer-to-Peer (P2P) Vouching Web
|
||||
of Trust for high-security events, where joining guests enter a quarantined
|
||||
read-only state until verified by a peer or host via QR/Emoji pairing, with
|
||||
recursive cascade revocation.
|
||||
- **Dependencies:**
|
||||
- Existing `event_passes` schema and `users.event_pass_id` relational linking.
|
||||
- Valkey L1/L2 cache for real-time session state propagation.
|
||||
- **Additional Important Notes:**
|
||||
- Must remain opt-in per event via `require_vouching BOOLEAN DEFAULT FALSE`.
|
||||
- UI must remain 100% React-free Hono SSR JSX with vanilla JavaScript DOM
|
||||
state manipulation.
|
||||
|
||||
---
|
||||
|
||||
## 2. Architectural Considerations & Risks
|
||||
|
||||
### Risks
|
||||
|
||||
1. **Vouching Graph Cycles & Endless Loops:** If attendee A vouches for B, B
|
||||
vouches for C, and C attempts to vouch for A, cyclical graphs could corrupt
|
||||
hierarchy metrics or create infinite loops during cascade revocation.
|
||||
- _Mitigation:_ Enforce an acyclic tree constraint. A user can only be
|
||||
vouched for once (setting an immutable `vouched_by UUID` pointing to their
|
||||
verified parent). Vouchers must already be in an `active` (non-quarantined)
|
||||
state.
|
||||
2. **Cascade Revocation Overhead & Blast Radius:** If an organizer revokes a
|
||||
rogue voucher at the root of a large subtree, revoking dozens of downstream
|
||||
guest sessions could block the event loop or leave orphaned cache records.
|
||||
- _Mitigation:_ Use a PostgreSQL Recursive Common Table Expression (CTE) to
|
||||
fetch all downstream descendant session IDs in a single atomic query
|
||||
(`WITH RECURSIVE subordinates AS (...)`), followed by bulk atomic cache
|
||||
eviction in Valkey using pipelined `UNLINK`.
|
||||
3. **UX Friction in Standard Workshops:** Mandating vouching for open public
|
||||
demos or casual workshops adds unnecessary friction.
|
||||
- _Mitigation:_ Make P2P vouching an opt-in toggle (`require_vouching`) in
|
||||
the `[ Delegate Session ]` creation drawer.
|
||||
|
||||
### Alternatives
|
||||
|
||||
- **Centralized Host-Only Approval Queue:** Instead of P2P vouching, all
|
||||
attendees could wait in a lobby for the host to click "Approve". While
|
||||
simpler, this creates an operational bottleneck for workshops with 50+
|
||||
attendees. P2P vouching allows any already-admitted participant to onboard
|
||||
their neighbor, enabling rapid, distributed verification.
|
||||
- **WebRTC / Bluetooth Proximity:** We considered WebRTC or WebBluetooth for
|
||||
physical co-location verification. However, browser support and permission
|
||||
prompts make this fragile. A 3-emoji visual sequence + 1-click QR code camera
|
||||
scan provides instant, high-friction-to-bots physical co-presence verification
|
||||
with zero native permission dependencies.
|
||||
|
||||
---
|
||||
|
||||
## 3. Proposed Implementation
|
||||
|
||||
### Phase 1: Database Schema & Entity Relationships (`server/db.ts`)
|
||||
|
||||
1. Add vouching metadata columns to `event_passes`:
|
||||
- `require_vouching BOOLEAN DEFAULT FALSE`
|
||||
- `vouch_depth_limit INT DEFAULT 2` (0 = host-only, 1 = direct guests only,
|
||||
2+ = transitive)
|
||||
2. Add verification tracking to `users`:
|
||||
- `is_quarantined BOOLEAN DEFAULT FALSE`
|
||||
- `vouched_by UUID REFERENCES users(id) ON DELETE SET NULL`
|
||||
- `vouched_at TIMESTAMP WITH TIME ZONE`
|
||||
3. Add ephemeral vouching challenge table `vouch_challenges`:
|
||||
- `id UUID PRIMARY KEY DEFAULT gen_random_uuid()`
|
||||
- `target_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE`
|
||||
- `event_id UUID NOT NULL REFERENCES event_passes(id) ON DELETE CASCADE`
|
||||
- `emoji_sequence TEXT NOT NULL` (e.g. `🚀-🦊-⚡`)
|
||||
- `pairing_code TEXT UNIQUE NOT NULL` (6-char alphanumeric code or QR token)
|
||||
- `created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()`
|
||||
- `expires_at TIMESTAMP WITH TIME ZONE NOT NULL`
|
||||
|
||||
### Phase 2: Ingress Quarantine & Verification API (`server/routes/events.ts`)
|
||||
|
||||
1. **Quarantined Ingress:**
|
||||
- In `POST /api/join` and `GET /join/:slug`, if
|
||||
`event.require_vouching = TRUE`, set `users.is_quarantined = TRUE` and tag
|
||||
the session with custom scope `['guest:quarantined']`.
|
||||
- Issue a `vouch_challenge` returning the 3-emoji sequence, QR payload, and
|
||||
pairing code.
|
||||
2. **ForwardAuth Guard:**
|
||||
- In `server/routes/forward_auth.ts`, check if the session contains
|
||||
`guest:quarantined`. If quarantined, block access to internal apps (return
|
||||
403 or redirect to `/vouch/pending`).
|
||||
3. **Vouching Endpoints:**
|
||||
- `POST /api/events/:id/vouch/verify`:
|
||||
- Authenticated voucher submits the 3-emoji sequence or scans the QR
|
||||
pairing token.
|
||||
- Verify the voucher is an active (non-quarantined) participant of the same
|
||||
event.
|
||||
- Check `vouch_depth_limit`.
|
||||
- Atomically update `users.is_quarantined = FALSE`, set
|
||||
`users.vouched_by = voucher.id`, delete the challenge, and broadcast
|
||||
session upgrade to Valkey.
|
||||
|
||||
### Phase 3: Recursive Cascade Revocation (`server/routes/sessions.ts`)
|
||||
|
||||
1. Refactor `DELETE /api/sessions/:id`:
|
||||
- If the target session belongs to a guest user who has vouched for others,
|
||||
execute a recursive CTE query:
|
||||
```sql
|
||||
WITH RECURSIVE tree AS (
|
||||
SELECT id, event_pass_id FROM users WHERE id = ${targetUserId}
|
||||
UNION ALL
|
||||
SELECT u.id, u.event_pass_id FROM users u
|
||||
INNER JOIN tree t ON u.vouched_by = t.id
|
||||
)
|
||||
SELECT s.id as session_id, t.id as user_id FROM sessions s
|
||||
JOIN tree t ON s.user_id = t.id;
|
||||
```
|
||||
- Delete all discovered sessions in PostgreSQL and evict all matching keys
|
||||
from Valkey.
|
||||
|
||||
### Phase 4: UI & Mobile Pairing Interface
|
||||
|
||||
1. **Attendee Waiting Screen (`ui/components/events/QuarantineLobby.tsx`):**
|
||||
- Render large high-contrast 3-emoji sequence and QR code: "Show this to the
|
||||
workshop host or an admitted attendee to unlock full access".
|
||||
- Poll or listen for activation event via SSE/fetch.
|
||||
2. **Voucher Action Modal (`ui/components/events/VouchingModal.tsx`):**
|
||||
- Admitted attendees see a `[ 🤝 Vouch for Peer ]` button in their top bar or
|
||||
Guest Drawer.
|
||||
- Opens simple camera QR scanner or 3-emoji selector grid to confirm the peer
|
||||
in front of them.
|
||||
3. **Guest Drawer Web of Trust Tree:**
|
||||
- In `EventGuestsDrawer.tsx`, show vouching tree indentation / badge
|
||||
(`Vouched by Tyler G`).
|
||||
|
||||
### Phase 5: Quality Gates & Integration Testing
|
||||
|
||||
1. Authored unit and integration tests in `server/tests/vouching.test.ts`:
|
||||
- Verify unvouched attendees cannot access ForwardAuth-protected endpoints.
|
||||
- Verify valid emoji/QR verification lifts quarantine immediately.
|
||||
- Verify cascading revocation cleans up all child and grandchild sessions
|
||||
recursively.
|
||||
2. Run `deno fmt`, `deno task lint`, `deno task check`, and
|
||||
`deno test -A --no-check`.
|
||||
@ -0,0 +1,122 @@
|
||||
# TASK METADATA
|
||||
|
||||
- **Target Files:**
|
||||
- `server/pow.ts`
|
||||
- `server/routes/events.ts`
|
||||
- `server/valkey.ts`
|
||||
- `ui/components/events/JoinPage.tsx`
|
||||
- `ui/components/events/JoinScript.tsx`
|
||||
- `ui/components/events/EventLandingPage.tsx`
|
||||
- `server/tests/pow.test.ts`
|
||||
- **Core Objective:** Implement a transparent, sub-50ms client-side WebCrypto
|
||||
(`crypto.subtle`) SHA-256 Proof-of-Work (PoW) ingress shield to protect
|
||||
6-digit workshop PIN codes and event slugs against distributed botnet
|
||||
brute-forcing and denial-of-service.
|
||||
- **Dependencies:**
|
||||
- Valkey 8 for atomic replay cache (`SET ... NX EX 60`).
|
||||
- Standard WebCrypto APIs supported across modern browsers and Deno.
|
||||
- **Additional Important Notes:**
|
||||
- PoW computation must be entirely transparent to human users (solved in
|
||||
background Web Workers or async microtasks during page load/typing).
|
||||
- Headless CLI clients (`curl ... | source`) must be provided with a
|
||||
streamlined solver or signed temporary ingress token.
|
||||
|
||||
---
|
||||
|
||||
## 2. Architectural Considerations & Risks
|
||||
|
||||
### Risks
|
||||
|
||||
1. **Client-Side CPU Exhaustion & Mobile Battery Drain:** If the cryptographic
|
||||
difficulty target is set too high, mobile devices or older laptops might
|
||||
freeze the main thread or experience noticeable latency before submitting a
|
||||
PIN.
|
||||
- _Mitigation:_ Calibrate the default baseline difficulty to ~16–18 leading
|
||||
zero bits (~20–40ms CPU time on standard mobile silicon). Run computation
|
||||
inside a Web Worker or async `requestAnimationFrame` chunks to guarantee
|
||||
zero UI frame drops.
|
||||
2. **Replay Attacks on Solved Nonces:** An attacker could precompute a single
|
||||
valid PoW solution and reuse it across thousands of simultaneous requests to
|
||||
brute-force the 6-digit PIN.
|
||||
- _Mitigation:_ Bind challenges cryptographically to a short-lived
|
||||
timestamped seed
|
||||
(`HMAC-SHA256(secret, client_ip + timestamp_minute + event_id)`). Record
|
||||
solved nonces in Valkey with a strict 60-second TTL via
|
||||
`SET key nonce NX EX 60` to enforce single-use execution.
|
||||
3. **Dynamic Threat Scaling:** Static difficulty is either too easy for large
|
||||
botnets or too heavy for low-powered mobile devices.
|
||||
- _Mitigation:_ Implement adaptive difficulty scaling in `server/pow.ts`:
|
||||
Normal traffic requires 16 bits; if an IP or specific event encounters $>3$
|
||||
failed attempts within 60s, automatically scale difficulty to 22 bits for
|
||||
that subnet/event.
|
||||
|
||||
### Alternatives
|
||||
|
||||
- **Cloudflare Turnstile / Google reCAPTCHA:** Heavy external dependency
|
||||
requiring network egress, proprietary script tags, and tracking cookies.
|
||||
Rejected. A self-contained, zero-dependency SHA-256 PoW preserves Auth-Yes's
|
||||
100% sovereign, zero-external-dependency IAM architecture.
|
||||
- **Strict IP Rate Limiting Only:** In distributed botnet attacks, attackers
|
||||
rotate thousands of residential proxies, making single-IP rate limiting
|
||||
ineffective. PoW shifts the economic and compute cost onto the attacker.
|
||||
|
||||
---
|
||||
|
||||
## 3. Proposed Implementation
|
||||
|
||||
### Phase 1: PoW Challenge Engine (`server/pow.ts`)
|
||||
|
||||
1. Implement challenge minting:
|
||||
- `generateChallenge(clientIp: string, eventId?: string, difficultyBits = 16)`:
|
||||
- Mint challenge payload:
|
||||
`{ salt: string, timestamp: number, difficulty: number, signature: string }`.
|
||||
- Sign payload using internal HMAC key.
|
||||
2. Implement validation:
|
||||
- `verifyProofOfWork(challenge: PoWChallenge, nonce: number, clientIp: string)`:
|
||||
- Verify HMAC signature on salt/timestamp (ensure timestamp within 120s
|
||||
window).
|
||||
- Compute `SHA-256(salt + nonce)`.
|
||||
- Check that the leading $N$ bits of the digest are zero.
|
||||
- Atomically store `nonce` in Valkey
|
||||
(`SET pow:${salt}:${nonce} 1 NX EX 120`); if key already exists, reject
|
||||
as replayed.
|
||||
|
||||
### Phase 2: Ingress Route Protection (`server/routes/events.ts`)
|
||||
|
||||
1. Add challenge endpoint:
|
||||
- `GET /api/join/challenge`: Returns
|
||||
`{ salt, timestamp, difficulty, signature }`.
|
||||
2. Update `POST /api/join`:
|
||||
- Require `pow_salt`, `pow_nonce`, `pow_timestamp`, `pow_signature` in JSON
|
||||
body.
|
||||
- Validate PoW _before_ checking the PIN code or touching PostgreSQL.
|
||||
- If invalid or missing, immediately return 400
|
||||
(`Bad Request: Invalid Proof-of-Work Challenge`) without consuming DB
|
||||
connections.
|
||||
3. Adaptive Difficulty Escalation:
|
||||
- Track failed attempts per IP and per event in Valkey. Increase `difficulty`
|
||||
dynamically if failure rate spikes.
|
||||
|
||||
### Phase 3: Client-Side WebCrypto Solver (`ui/components/events/JoinScript.tsx`)
|
||||
|
||||
1. **Background Web Worker Solver:**
|
||||
- When the user lands on `/join` or `/e/:slug`, automatically fetch
|
||||
`/api/join/challenge` in the background.
|
||||
- Spawn a lightweight Web Worker executing WebCrypto
|
||||
`crypto.subtle.digest('SHA-256', ...)` in a tight loop to find the nonce
|
||||
while the user types their 6-digit PIN.
|
||||
- Attach solved nonce payload seamlessly to the form submit handler.
|
||||
2. **Headless CLI Compatibility:**
|
||||
- For CLI joins (`/join/:slug?format=env`), embed a micro-solver script in
|
||||
the piped shell payload or provide a signed 1-click token for terminal
|
||||
workflows.
|
||||
|
||||
### Phase 4: Quality Gates & Benchmarks
|
||||
|
||||
1. Comprehensive test suite in `server/tests/pow.test.ts`:
|
||||
- Unit tests for challenge generation, difficulty checks, expiration, and
|
||||
replay prevention.
|
||||
- Performance benchmark asserting solver verification on server takes
|
||||
$<0.5\text{ms}$.
|
||||
2. Run `deno fmt`, `deno task lint`, `deno task check`, and
|
||||
`deno test -A --no-check`.
|
||||
@ -0,0 +1,109 @@
|
||||
# TASK METADATA
|
||||
|
||||
- **Target Files:**
|
||||
- `server/routes/events.ts`
|
||||
- `ui/components/sessions/EventCockpitDeck.tsx`
|
||||
- `ui/components/sessions/EventGuestsDrawer.tsx`
|
||||
- `ui/components/sessions/SessionsScript.tsx`
|
||||
- `ui/components/SessionsPage.tsx`
|
||||
- `server/tests/events.test.ts`
|
||||
- `ui/ui_scripts.test.ts`
|
||||
- **Core Objective:** Clean up the Sessions & Events dashboard by archiving
|
||||
expired passes into a collapsible `<details>` accordion with a 1-click
|
||||
`[ 🔄 Reopen (+1h) ]` action, and enrich the Guest Drawer with real-time
|
||||
attendee telemetry and activity logs.
|
||||
- **Dependencies:** None.
|
||||
- **Additional Important Notes:**
|
||||
- Must remain 100% pure React-free Hono SSR JSX.
|
||||
- Distinguish between naturally expired events (`expires_at < NOW()`,
|
||||
`is_active = TRUE`, reopenable) vs manually terminated events
|
||||
(`is_active = FALSE`, permanent archive).
|
||||
|
||||
---
|
||||
|
||||
## 2. Architectural Considerations & Risks
|
||||
|
||||
### Risks
|
||||
|
||||
1. **Dashboard Clutter & Information Overload:** As organizers host dozens of
|
||||
workshops over weeks, active and dead events pile up on the main screen,
|
||||
pushing active management tools below the fold.
|
||||
- _Mitigation:_ Partition the SQL query and SSR view into two decks:
|
||||
`activePasses` (top priority grid/compact cards) and `expiredPasses`
|
||||
(collapsed under a clean `<details>` drawer showing count
|
||||
`[ 📁 Expired Passes (N) ]`).
|
||||
2. **Re-opening State Ambiguity:** If an organizer re-opens an event that
|
||||
expired 3 days ago, adding 1 hour via `expires_at + interval '1h'` leaves it
|
||||
in the past.
|
||||
- _Mitigation:_ Use
|
||||
`SET expires_at = GREATEST(expires_at, NOW()) + interval '1 hour'` in
|
||||
PostgreSQL to guarantee the event opens immediately for 60 minutes from the
|
||||
moment clicked.
|
||||
3. **Telemetry Performance in Large Guest Drawers:** Querying full session
|
||||
activity logs for 100+ attendees could slow down
|
||||
`GET /api/events/:id/attendees`.
|
||||
- _Mitigation:_ Select only `last_activity_action`, `last_activity_at`, and
|
||||
`is_agent` joined directly on `sessions` indexed by `user_id`.
|
||||
|
||||
### Alternatives
|
||||
|
||||
- **Permanent Deletion of Expired Events:** We considered automatically dropping
|
||||
expired events from the database. However, organizers frequently need to audit
|
||||
past workshop attendees or re-open events that ran over schedule. Keeping
|
||||
expired passes in an archived state maintains audit integrity.
|
||||
|
||||
---
|
||||
|
||||
## 3. Proposed Implementation
|
||||
|
||||
### Phase 1: Split Active vs. Expired Events Query (`ui/components/SessionsPage.tsx`)
|
||||
|
||||
1. Update `event_passes` query in `SessionsPage.tsx`:
|
||||
- Split passes into
|
||||
`activeEvents = eventPasses.filter(e => e.is_active && (!e.expires_at || new Date(e.expires_at) > new Date()))`.
|
||||
- `expiredEvents = eventPasses.filter(e => !e.is_active || (e.expires_at && new Date(e.expires_at) <= new Date()))`.
|
||||
2. Pass both arrays into `EventCockpitDeck.tsx`.
|
||||
|
||||
### Phase 2: Collapsible Expired Deck & 1-Click Reopen (`ui/components/sessions/EventCockpitDeck.tsx`)
|
||||
|
||||
1. **Active Deck:**
|
||||
- Render active events in standard Grid or 2-Row Compact view.
|
||||
2. **Archived Deck (`[ 📁 Expired Passes (N) ]`):**
|
||||
- Render a native `<details>` element below the active deck.
|
||||
- For each expired pass with `is_active = TRUE`, display an amber/gray card
|
||||
with an instant `[ 🔄 Reopen (+1h) ]` button.
|
||||
- For manually ended events (`is_active = FALSE`), show a muted
|
||||
`[ Terminated ]` badge without a reopen button.
|
||||
3. **Client Script Handler (`SessionsScript.tsx`):**
|
||||
- Add `reopenEvent(eventId)` calling `POST /api/events/:id/extend` with
|
||||
`{ extendHours: 1 }`.
|
||||
- Dynamically transition the card or reload to place it back into the active
|
||||
deck.
|
||||
|
||||
### Phase 3: Rich Attendee Telemetry in Guest Drawer (`ui/components/sessions/EventGuestsDrawer.tsx`)
|
||||
|
||||
1. **Backend Payload Enrichment (`server/routes/events.ts`):**
|
||||
- In `GET /api/events/:id/attendees`, select:
|
||||
- `s.last_activity_at`
|
||||
- `s.last_activity_action` (e.g. `ForwardAuth Ingress (ed-droid)`,
|
||||
`API Token Read`)
|
||||
- `s.is_agent`
|
||||
2. **UI Card Formatting:**
|
||||
- On each attendee card in the drawer, render:
|
||||
- **Header:** `Seat #[N]` + `🟢 Active / ⏸️ Paused` badge + inline
|
||||
`[ 🗑️ Revoke ]`.
|
||||
- **Line 1 (Inverted Join Time):**
|
||||
`Joined Today · 2:15 PM · [ Active 1h 45m ]`.
|
||||
- **Line 2 (Action Telemetry):**
|
||||
`Last Action: ForwardAuth (ed-droid) · 2m ago · 💻 Web` (with device icon
|
||||
or agent robot emoji if `is_agent = true`).
|
||||
|
||||
### Phase 4: Quality Gates & Verification
|
||||
|
||||
1. Unit tests in `server/tests/events.test.ts`:
|
||||
- Test reopening naturally expired events with
|
||||
`GREATEST(expires_at, NOW()) + 1h`.
|
||||
- Test telemetry fields in `/attendees` endpoint.
|
||||
2. UI syntax tests in `ui/ui_scripts.test.ts`.
|
||||
3. Run `deno fmt`, `deno task lint`, `deno task check`, and
|
||||
`deno test -A --no-check`.
|
||||
@ -0,0 +1,91 @@
|
||||
# TASK METADATA
|
||||
|
||||
- **Target Files:**
|
||||
- `spire_ffi/src/lib.rs`
|
||||
- `spire_ffi/Cargo.toml`
|
||||
- `server/spire.ts`
|
||||
- `server/routes/rpc.ts`
|
||||
- `server/tests/spire_workload.test.ts`
|
||||
- `infra/spire/docker-compose.test.yml`
|
||||
- **Core Objective:** Establish a comprehensive integration harness and
|
||||
automated testing pipeline for the Rust SPIFFE/mTLS FFI crate (`spire_ffi/`),
|
||||
enabling live X.509 SVID validation and ConnectRPC mutual TLS attestation
|
||||
against a containerized SPIRE agent.
|
||||
- **Dependencies:**
|
||||
- Rust toolchain (`cargo`, `rustc`).
|
||||
- Deno FFI (`--allow-ffi`).
|
||||
- Containerized SPIRE Server & Agent in `infra/spire/`.
|
||||
- **Additional Important Notes:**
|
||||
- Must provide a seamless mock fallback for standard development environments
|
||||
when `libspire_ffi.so` is not built, while enforcing strict validation in
|
||||
mesh-enabled environments.
|
||||
|
||||
---
|
||||
|
||||
## 2. Architectural Considerations & Risks
|
||||
|
||||
### Risks
|
||||
|
||||
1. **Native Dynamic Library Portability:** C dynamic libraries (`.so`, `.dylib`,
|
||||
`.dll`) compiled on one architecture/OS cannot run on others without
|
||||
cross-compilation.
|
||||
- _Mitigation:_ Keep `server/spire.ts` resilient with an explicit graceful
|
||||
fallback mock layer when `libspire_ffi` is missing. Provide a dedicated
|
||||
task script (`deno task build:ffi`) that compiles `spire_ffi` locally using
|
||||
`cargo build --release`.
|
||||
2. **Workload API Unix Domain Socket Timeouts:** If the SPIRE agent daemon
|
||||
restarts or socket permissions change, FFI calls to fetch X.509 SVIDs could
|
||||
block or hang worker threads.
|
||||
- _Mitigation:_ Enforce non-blocking socket reads with strict timeouts (e.g.
|
||||
500ms) inside Rust FFI functions before returning to Deno.
|
||||
|
||||
### Alternatives
|
||||
|
||||
- **Pure TypeScript gRPC Client for SPIRE Workload API:** We considered writing
|
||||
a pure TypeScript gRPC client. However, SPIRE's Workload API communicates over
|
||||
Unix Domain Sockets with strict OS-level credential passing (`SO_PEERCRED`),
|
||||
which is far more performant and natively handled via the official
|
||||
`spire-api-sdk` crate in Rust.
|
||||
|
||||
---
|
||||
|
||||
## 3. Proposed Implementation
|
||||
|
||||
### Phase 1: Rust FFI Hardening (`spire_ffi/src/lib.rs`)
|
||||
|
||||
1. Enhance `spire_ffi`:
|
||||
- Implement
|
||||
`fetch_x509_svid(socket_path: *const c_char, timeout_ms: u32) -> FfiResult`.
|
||||
- Implement
|
||||
`validate_spiffe_id(client_cert_der: *const u8, cert_len: usize, expected_spiffe_id: *const c_char) -> bool`.
|
||||
- Ensure all string buffers and error structures are safely allocated and
|
||||
freed across the FFI boundary (`free_ffi_string`).
|
||||
|
||||
### Phase 2: Deno FFI Binding & Fallback Layer (`server/spire.ts`)
|
||||
|
||||
1. Load dynamic library:
|
||||
- Search `./libspire_ffi.so`, `./spire_ffi/target/release/libspire_ffi.so`,
|
||||
and system library paths.
|
||||
- If missing, log a warning and activate the hermetic mock provider for local
|
||||
unit testing.
|
||||
2. Expose high-level TypeScript API:
|
||||
- `getWorkloadSvid(socketPath?: string): Promise<{ spiffeId: string, certChain: Uint8Array, privateKey: Uint8Array }>`
|
||||
- `verifyClientSpiffeId(cert: Uint8Array, expectedId: string): boolean`
|
||||
|
||||
### Phase 3: ConnectRPC Integration & Test Harness
|
||||
|
||||
1. In `server/routes/rpc.ts`:
|
||||
- Bind incoming ConnectRPC service requests to `verifyClientSpiffeId` when
|
||||
running behind mTLS proxies.
|
||||
2. In `infra/spire/docker-compose.test.yml`:
|
||||
- Add minimal SPIRE Server & Agent test configuration with a registered test
|
||||
workload entry (`spiffe://system.local/auth-yes-tester`).
|
||||
3. In `server/tests/spire_workload.test.ts`:
|
||||
- Test live X.509 SVID acquisition, parsing, and ConnectRPC authorization
|
||||
against the running test SPIRE container.
|
||||
|
||||
### Phase 4: Quality Gates
|
||||
|
||||
1. Run `cargo test` in `spire_ffi/`.
|
||||
2. Run `deno fmt`, `deno task lint`, `deno task check`, and
|
||||
`deno test -A --no-check`.
|
||||
@ -0,0 +1,165 @@
|
||||
# TASK METADATA
|
||||
|
||||
- **Target Files:**
|
||||
- `ui/components/SessionsPage.tsx`
|
||||
- `ui/components/sessions/DirectPassDrawer.tsx`
|
||||
- `ui/components/sessions/WorkshopDrawer.tsx`
|
||||
- `ui/components/sessions/SessionsScript.tsx`
|
||||
- `ui/ui_scripts.test.ts`
|
||||
- **Core Objective:** Resolve Round 4 UI defects by docking the sticky
|
||||
sub-header beneath the main app top bar, standardizing single session creation
|
||||
to full parity with workshop passes, eliminating `<summary>` double arrows
|
||||
while preserving `(Optional)` labels, and implementing a rapid creation flow
|
||||
(`[ Create Another ]` + `[ OK ]`) with tab-locking during handoff states.
|
||||
- **Dependencies:** None.
|
||||
- **Additional Important Notes:** Must remain 100% pure React-free Hono SSR JSX.
|
||||
All state transitions in `SessionsScript.tsx` must use native vanilla
|
||||
JavaScript DOM APIs.
|
||||
|
||||
---
|
||||
|
||||
## 2. Architectural Considerations & Risks
|
||||
|
||||
### Risks
|
||||
|
||||
1. **Navbar Collision on Mobile Viewports:** On narrow screens, the top app bar
|
||||
height might vary depending on wrapped elements.
|
||||
- _Mitigation:_ Use `top: var(--top-bar-height, 57px); z-index: 30;` with a
|
||||
solid background (`var(--surface-bg)`) to prevent cards from scrolling
|
||||
through transparent gaps or overlapping the fixed `.top-bar` (which sits at
|
||||
`z-index: 40`).
|
||||
2. **State Leakage Across Tabs:** If a user mints a Workshop Pass, the handoff
|
||||
screen appears. If they click the "Single Pass" tab while the event handoff
|
||||
is still visible, the form displays a dirty or stale state.
|
||||
- _Mitigation:_ Lock the tab switcher (`pointer-events: none; opacity: 0.5;`
|
||||
or visually disable the buttons) while in either `handoffModal` or
|
||||
`eventHandoffState`. Tab navigation is automatically re-enabled when the
|
||||
user clicks `[ Create Another ]` or closes the drawer.
|
||||
3. **Double Arrow Rendering:** Native `<details><summary>` elements
|
||||
automatically render disclosure triangles in modern browsers.
|
||||
- _Mitigation:_ Purge all explicit `▸` characters from `<summary>` text
|
||||
strings across both drawer templates.
|
||||
|
||||
### Alternatives
|
||||
|
||||
- **Closing the Drawer Immediately on Creation:** We considered closing the
|
||||
drawer right after minting and letting the user copy links from the main page
|
||||
deck. However, showing the 1-click link, PIN, and CLI export immediately upon
|
||||
creation in a dedicated handoff card provides essential visual confirmation
|
||||
and zero-friction copying before returning to the dashboard.
|
||||
|
||||
---
|
||||
|
||||
## 3. Proposed Implementation
|
||||
|
||||
### Phase 1: Viewport Docking (`ui/components/SessionsPage.tsx`)
|
||||
|
||||
1. Locate the `Sessions & Events` sub-header block in `SessionsPage.tsx`.
|
||||
2. Update the sticky styling from `top: 0; z-index: 40;` to:
|
||||
```css
|
||||
position: sticky;
|
||||
top: var(--top-bar-height, 57px);
|
||||
z-index: 30;
|
||||
background: var(--surface-bg, #0f172a);
|
||||
padding: 0.75rem 0;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
```
|
||||
|
||||
### Phase 2: Single Pass Drawer Parity & Form Hiding (`ui/components/sessions/DirectPassDrawer.tsx`)
|
||||
|
||||
1. Wrap the Single Pass creation form in
|
||||
`<div id="delegateCreateState" style="display: block;">`.
|
||||
2. Update the submit button:
|
||||
- Label: **`[ Create Session ]`** (replace `"Mint & Delegate Session"`).
|
||||
- Standardize styling to match `btn-primary`.
|
||||
3. In the Accordion `<summary>`:
|
||||
- Remove the `▸` character.
|
||||
- Label as: **`"Customize App Permissions & Scopes (Optional)"`**.
|
||||
4. In `#handoffModal`:
|
||||
- Change the 1-Click Link copy button from `btn-primary` to uniform
|
||||
`btn-outline`.
|
||||
- Update footer actions:
|
||||
- Replace the single `"Done (Session is Active)"` button with a dual-action
|
||||
flex container:
|
||||
- **`[ Create Another ]`**
|
||||
(`type="button" class="btn-outline" onclick="createAnotherSession()"`)
|
||||
- **`[ OK ]`**
|
||||
(`type="button" class="btn-primary" onclick="closeDelegateDrawer()"`)
|
||||
|
||||
### Phase 3: Workshop Drawer Parity & Accordion Cleanliness (`ui/components/sessions/WorkshopDrawer.tsx`)
|
||||
|
||||
1. Update the submit button:
|
||||
- Label: **`[ Create Event ]`** (replace `"🎟️ Launch Workshop Pass"`).
|
||||
2. In the Custom Vanity Slug & PIN `<summary>`:
|
||||
- Remove the `▸` character.
|
||||
- Label as: **`"Custom Vanity Slug & PIN Code (Optional)"`**.
|
||||
3. In `#eventHandoffState`:
|
||||
- Update footer actions to match Single Pass:
|
||||
- **`[ Create Another ]`**
|
||||
(`type="button" class="btn-outline" onclick="createAnotherEvent()"`)
|
||||
- **`[ OK ]`**
|
||||
(`type="button" class="btn-primary" onclick="closeDelegateDrawer()"`)
|
||||
|
||||
### Phase 4: Client State Machine & Tab Locking (`ui/components/sessions/SessionsScript.tsx`)
|
||||
|
||||
1. **Single Session Submit Handler (`handleDelegateSession`):**
|
||||
- On success, set `#delegateCreateState.style.display = 'none'` and
|
||||
`#handoffModal.style.display = 'block'`.
|
||||
- Call `lockDelegationTabs(true)` to prevent switching tabs while viewing the
|
||||
handoff token.
|
||||
2. **Event Creation Submit Handler (`handleCreateEvent`):**
|
||||
- On success, set `#eventCreateState.style.display = 'none'` and
|
||||
`#eventHandoffState.style.display = 'block'`.
|
||||
- Call `lockDelegationTabs(true)`.
|
||||
3. **Tab Locking Helpers:**
|
||||
- `lockDelegationTabs(locked)`: Toggles `disabled` state and
|
||||
`opacity: 0.5; pointer-events: none;` on `#tabBtnDirectPass` and
|
||||
`#tabBtnWorkshopPass`.
|
||||
4. **"Create Another" Handlers:**
|
||||
- `createAnotherSession()`:
|
||||
- Reset `#delegateForm` input values (default back to 1 Hour and
|
||||
Read-Only).
|
||||
- `#handoffModal.style.display = 'none'`.
|
||||
- `#delegateCreateState.style.display = 'block'`.
|
||||
- `lockDelegationTabs(false)`.
|
||||
- Focus `#delegateLabel`.
|
||||
- `createAnotherEvent()`:
|
||||
- Reset `#eventForm` input values (default back to 50 seats and 3 hours).
|
||||
- `#eventHandoffState.style.display = 'none'`.
|
||||
- `#eventCreateState.style.display = 'block'`.
|
||||
- `lockDelegationTabs(false)`.
|
||||
- Focus `#eventName`.
|
||||
5. **Drawer Open/Close Reset:**
|
||||
- When calling `openDelegateDrawer()`, ensure `lockDelegationTabs(false)` is
|
||||
invoked and both form states are reset to visible State 1.
|
||||
|
||||
### Phase 5: Expired Events Accordion & 1-Click Reopen (`ui/components/sessions/EventCockpitDeck.tsx`)
|
||||
|
||||
1. **Active vs. Expired Separation:**
|
||||
- Filter `eventPasses` into `activeEvents` and `expiredEvents`
|
||||
(`expires_at <= NOW()` or `is_active = FALSE`).
|
||||
2. **Collapsible Accordion:**
|
||||
- Below the active deck, render a native `<details>` block:
|
||||
```html
|
||||
<details
|
||||
style="margin-top: 1.5rem; background: var(--surface-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); padding: 0.75rem 1rem;">
|
||||
<summary style="font-weight: 600; font-size: 0.95rem; cursor: pointer; color: var(--text-secondary);">
|
||||
📁 Expired Events ({expiredEvents.length})
|
||||
</summary>
|
||||
...
|
||||
</details>
|
||||
```
|
||||
3. **1-Click Reopen Action:**
|
||||
- For naturally expired events (`is_active = TRUE`), render a dedicated
|
||||
**`[ 🔄 Reopen (+1h) ]`** button calling `POST /api/events/:id/extend` with
|
||||
`extendHours: 1`.
|
||||
|
||||
### Phase 6: Quality Gates & Network/Error Mock Testing
|
||||
|
||||
1. Run `deno test -A --no-check server/tests/events.test.ts` to assert all
|
||||
extend, attendees, and 404/403 failure paths pass.
|
||||
2. Run `deno test -A --no-check ui/ui_scripts.test.ts` to verify zero JavaScript
|
||||
syntax or parsing errors.
|
||||
3. Run `deno fmt`, `deno task lint`, `deno task check`, and
|
||||
`deno test -A --no-check`.
|
||||
53
tasks/ui-audit-4.md
Normal file
53
tasks/ui-audit-4.md
Normal file
@ -0,0 +1,53 @@
|
||||
# UI & UX Live Audit Log — Round 4 (`ui-audit-4.md`)
|
||||
|
||||
**Date:** 2026-08-27 (Session: 10:45)\
|
||||
**Target Environments:** `https://auth.atyg.org` | `https://ed-droid.atyg.org`\
|
||||
**Scope:** Live verification of Phase 7 & 8 Deliverables (Attendee Relational
|
||||
Decoupling, Sticky Action Bar, Dynamic Credential Rotation DOM Sync, Guest
|
||||
Drawer Event Meta Payload, Legacy DB Migration Backfills) and discovery/triage
|
||||
of new Round 4 defects.
|
||||
|
||||
---
|
||||
|
||||
## 1. Core Verification Focus Areas
|
||||
|
||||
| # | Feature / Flow | Target Behavior | Status |
|
||||
| :---- | :-------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :----------------- |
|
||||
| **1** | **Sticky Action Bar** | Pinned `Sessions & Events` header and `[ Delegate Session ]` trigger stick cleanly to top viewport on deep scroll without clipping cards. | ⏳ Ready for Audit |
|
||||
| **2** | **Live Credential Rotation Sync** | Clicking `[ 🔄 Rotate ]` hot-swaps the numeric PIN, Slug, Direct Link, and CLI command in DOM (Grid & Compact) preserving proxy origin. | ⏳ Ready for Audit |
|
||||
| **3** | **Guest Drawer Live Meta** | Guest Drawer renders live countdown (`⏳ Xh Ym left`) and accurate seat fraction (`N / Max Seats`) with zero `0 / 0` placeholders. | ⏳ Ready for Audit |
|
||||
| **4** | **Relational Session Revocation** | Revoking or ending an event pass cleanly terminates only matching attendee sessions without string `LIKE` leakage or orphan records. | ⏳ Ready for Audit |
|
||||
| **5** | **Legacy Pass Compatibility** | Pre-existing guest accounts seamlessly bind to `event_pass_id` via the backfill migration so older passes can be revoked/extended. | ⏳ Ready for Audit |
|
||||
|
||||
---
|
||||
|
||||
## 2. Live Observation & Findings Log
|
||||
|
||||
_Record live observations, visual feedback, quirks, UI anomalies, and proposed
|
||||
action items here._
|
||||
|
||||
| Timestamp | Scenario / Screen | Component / Flow | Observation / Finding | Resolution / Action Item |
|
||||
| :-------- | :---------------------------------------- | :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `10:45` | Setup | System | Opened `ui-audit-4.md` log for Round 4 live verification and defect triage. | Ready for live observation inputs. |
|
||||
| `11:17` | Layout & Viewport Collision | `SessionsPage.tsx` vs `AuthenticatedLayout.tsx` | **Sticky Sub-Header Overwrites Main Navigation:** The `Sessions & Events` sub-header has `position: sticky; top: 0; z-index: 40;`, which collides directly with the fixed `.top-bar` navbar (also at `top: 0`), overwriting the brand badge, nav links, and user menu on scroll. | **Action Item:** Offset sub-header to `top: var(--top-bar-height, 57px); z-index: 30;` so it docks cleanly right beneath the main top bar without clipping or obscuring navigation. |
|
||||
| `11:20` | Information Architecture & Parity | `DirectPassDrawer.tsx` vs `WorkshopDrawer.tsx` | **Single Session Minting Lacks Phase 6/8 UX Parity:**<br>1. **Button Copy:** Handoff button says `"Done (Session is Active)"` instead of standardized **`[ OK ]`**.<br>2. **Copy Button Styling:** 1-Click link copy button is solid blue `btn-primary` while CLI/cURL are `btn-outline`. Standardize all to uniform `btn-outline`.<br>3. **Form Does Not Hide on Mint:** Creating a single session keeps `#delegateForm` visible above the handoff card, leaving the stale `[ Cancel ]` button visible after creation.<br>4. **Action Button Phrasing:** Use explicit, confidence-building submit labels: **`[ Create Session ]`** for Single Pass and **`[ Create Event ]`** for Multi-User Workshop. | **Action Item:** Bring `DirectPassDrawer.tsx` to 100% parity with `WorkshopDrawer.tsx` by hiding `#delegateForm` upon generation, displaying a dedicated handoff card with uniform `btn-outline` copy buttons, and standardized `[ Create Session ]` / `[ OK ]` triggers. |
|
||||
| `11:23` | Visual Glitch & Redundant Copy | `DirectPassDrawer.tsx` & `WorkshopDrawer.tsx` | **Double Arrow (`> >`) & Wordy Accordion Titles:**<br>1. Native `<details><summary>` already renders a disclosure triangle. Hardcoding `▸` in the text produces a broken double arrow (`> >`).<br>2. Clean up accordion text while explicitly retaining the `(Optional)` tag. | **Action Item:**<br>• Remove hardcoded `▸` character from all `<summary>` blocks.<br>• Label as **`"Customize App Permissions & Scopes (Optional)"`** in Single Pass and **`"Custom Vanity Slug & PIN Code (Optional)"`** in Workshop Pass. |
|
||||
| `11:26` | Workflow Architecture & State Transitions | `SessionsScript.tsx` & Drawers | **Rapid Creation Workflow & Tab Locking in Handoff:**<br>1. Power users creating multiple sessions or workshop passes need an instant way to mint a second token without closing and reopening the drawer.<br>2. Switching tabs while in a handoff state reveals stale inputs from the other mode. | **Action Item (Dual Actions + Handoff Tab Lock):**<br>• In both single and event handoff screens, provide two text buttons: **`[ Create Another ]`** (`btn-outline`, resets form to State 1 in 1 click) and **`[ OK ]`** (`btn-primary`, closes drawer).<br>• Disable/lock the top tab switcher while in the Handoff state so users cannot switch tabs until they either click `[ Create Another ]` or close the drawer. |
|
||||
| `13:11` | Backend SQL Bug & 500 Error | `server/routes/events.ts` (`/extend`) | **SQL Syntax Error on `+1h Extend`:** In `server/routes/events.ts:227` and `245`, `interval '${extendHours} hours'` inside tagged SQL literals evaluates to invalid parameterized SQL (`$1` inside quotes), throwing a Postgres syntax error and returning 500 ("Network error extending event"). | **Action Item:** Replace `interval '${extendHours} hours'` with valid parameterized PostgreSQL interval multiplication: `INTERVAL '1 hour' * ${extendHours}`. |
|
||||
| `13:13` | Telemetry & Ingress API | `server/routes/events.ts` (`/attendees`) & Script | **"Network Error Loading Attendees" & Stale Drawer Expiration:**<br>1. In `openAttendeesDrawer()`, if the endpoint returns an error or non-JSON payload, `res.json()` throws a SyntaxError which surfaces as a misleading "Network error".<br>2. The Guest Drawer header does not dynamically update its countdown timer or expiration badge when an event is extended. | **Action Item:**<br>• Wrap `res.json()` defensively and surface true backend error payloads.<br>• Update `guestDrawerExpiresAt` and trigger `updateAllCountdowns()` when extending an event. |
|
||||
| `13:15` | IA & Lifecycle Completeness | `EventCockpitDeck.tsx` & Queries | **Missing Expired Events Accordion:** Expired event passes currently remain mixed or hidden rather than cleanly archived in a dedicated collapsible `<details>` section with a 1-click `[ 🔄 Reopen (+1h) ]` button. | **Action Item:** Integrate the `[ 📁 Expired Passes (N) ]` collapsible accordion into `EventCockpitDeck.tsx` with instant 1-click reopen capability (`GREATEST(expires_at, NOW()) + 1h`). |
|
||||
| `13:17` | Architecture & Data Model | `SessionsPage.tsx` | **Events vs. Sessions List Separation:** Multi-claim event passes live in the top `Events` Cockpit deck, while individual 1:1 sessions (passkeys, device logins, agent tokens) live in the bottom `Sessions` table/deck. Guest attendee sessions live inside each event's `[ 👥 Manage Guests ]` drawer. | **Status / Clarification:** Clarified architectural separation between Event pass factories (top deck) vs individual session tokens (bottom table). |
|
||||
|
||||
---
|
||||
|
||||
## 3. Retained Action & Security Backlog
|
||||
|
||||
- [ ] **🛡️ Peer-to-Peer Vouching (Web of Trust):** Opt-in quarantine state for
|
||||
event attendees with QR/3-Emoji neighbor verification and cascade
|
||||
revocation.
|
||||
- [ ] **⚡ Transparent Client Proof-of-Work (PoW):** Background WebCrypto
|
||||
SHA-256 challenge on `/api/join` to block botnet PIN brute-forcing.
|
||||
- [ ] **📁 Expired Events Archive & 1-Click Reopen:** Collapsible `<details>`
|
||||
section for expired passes with instant `[ 🔄 Reopen (+1h) ]` button.
|
||||
- [ ] **📊 Rich Attendee Telemetry:** Display last activity action, relative
|
||||
time, and device/agent badge in guest drawer cards.
|
||||
@ -32,7 +32,8 @@ export const EventGuestsDrawer = () => {
|
||||
>
|
||||
Event Pass · <span id="guestDrawerClaimed">0</span> /{" "}
|
||||
<span id="guestDrawerMax">0</span> Claimed Seats ·{" "}
|
||||
<span id="guestDrawerCountdown">⏳ 0h 0m left</span>
|
||||
<span id="guestDrawerCountdown">⏳ 0h 0m left</span>{" "}
|
||||
· (<span id="guestDrawerExpiresAt">--:--</span>)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -427,26 +427,44 @@ export const SessionsScript = () => {
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/events/' + eventId + '/attendees');
|
||||
const data = await res.json();
|
||||
let data = {};
|
||||
try {
|
||||
data = await res.json();
|
||||
} catch (_) {
|
||||
// Non-JSON payload
|
||||
}
|
||||
|
||||
if (res.ok && data.success) {
|
||||
const attendees = data.attendees;
|
||||
const attendees = data.attendees || [];
|
||||
|
||||
// Update Context Header with null safety
|
||||
const claimedElem = document.getElementById('guestDrawerClaimed');
|
||||
if (claimedElem) claimedElem.textContent = attendees.length;
|
||||
|
||||
// Update Context Header
|
||||
// In a real app we would get the true max seats and expires time from the event payload.
|
||||
// For this UI, we can derive it or keep it simple.
|
||||
document.getElementById('guestDrawerClaimed').textContent = attendees.length;
|
||||
if (data.event) {
|
||||
document.getElementById('guestDrawerMax').textContent = data.event.max_seats;
|
||||
const expDate = new Date(data.event.expires_at);
|
||||
document.getElementById('guestDrawerExpiresAt').textContent = expDate.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
||||
document.getElementById('guestDrawerCountdown').setAttribute('data-expires-at', data.event.expires_at);
|
||||
const maxElem = document.getElementById('guestDrawerMax');
|
||||
if (maxElem) maxElem.textContent = data.event.max_seats;
|
||||
|
||||
const expiresAtElem = document.getElementById('guestDrawerExpiresAt');
|
||||
if (expiresAtElem && data.event.expires_at) {
|
||||
const expDate = new Date(data.event.expires_at);
|
||||
expiresAtElem.textContent = expDate.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
||||
}
|
||||
|
||||
const countdownElem = document.getElementById('guestDrawerCountdown');
|
||||
if (countdownElem && data.event.expires_at) {
|
||||
countdownElem.setAttribute('data-expires-at', data.event.expires_at);
|
||||
}
|
||||
|
||||
// Trigger countdown update
|
||||
if (typeof updateAllCountdowns === 'function') updateAllCountdowns();
|
||||
}
|
||||
|
||||
const contentElem = document.getElementById('attendeesDrawerContent');
|
||||
if (attendees.length === 0) {
|
||||
document.getElementById('attendeesDrawerContent').innerHTML = '<div style="padding: 1rem; text-align: center; color: var(--text-muted);">No attendees currently active.</div>';
|
||||
if (contentElem) {
|
||||
contentElem.innerHTML = '<div style="padding: 1rem; text-align: center; color: var(--text-muted);">No attendees currently active.</div>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -528,10 +546,17 @@ export const SessionsScript = () => {
|
||||
});
|
||||
|
||||
} else {
|
||||
document.getElementById('attendeesDrawerContent').innerHTML = '<div style="padding: 1rem; color: var(--danger-text);">Failed to load attendees: ' + (data.error || 'Unknown error') + '</div>';
|
||||
const contentElem = document.getElementById('attendeesDrawerContent');
|
||||
if (contentElem) {
|
||||
contentElem.innerHTML = '<div style="padding: 1rem; color: var(--danger-text);">Failed to load attendees: ' + (data.error || 'HTTP ' + res.status) + '</div>';
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
document.getElementById('attendeesDrawerContent').innerHTML = '<div style="padding: 1rem; color: var(--danger-text);">Network error loading attendees.</div>';
|
||||
console.error('[Attendees Drawer Error]', err);
|
||||
const contentElem = document.getElementById('attendeesDrawerContent');
|
||||
if (contentElem) {
|
||||
contentElem.innerHTML = '<div style="padding: 1rem; color: var(--danger-text);">Error loading attendees: ' + (err && err.message ? err.message : 'Network failure') + '</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -565,12 +590,17 @@ export const SessionsScript = () => {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ extendHours: 1 })
|
||||
});
|
||||
if (res.ok) {
|
||||
let data = {};
|
||||
try {
|
||||
data = await res.json();
|
||||
} catch (_) {
|
||||
// Non-JSON payload
|
||||
}
|
||||
if (res.ok && data.success) {
|
||||
showNotice('Event extended by 1 hour!', false);
|
||||
setTimeout(() => window.location.reload(), 600);
|
||||
} else {
|
||||
const data = await res.json();
|
||||
showNotice(data.error || 'Failed to extend event', true);
|
||||
showNotice(data.error || 'Failed to extend event (HTTP ' + res.status + ')', true);
|
||||
e.currentTarget.disabled = false;
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user