252 lines
12 KiB
Markdown
252 lines
12 KiB
Markdown
# TASK METADATA
|
||
|
||
- **Target Files:**
|
||
- `server/routes/events.ts`
|
||
- `ui/components/SessionsPage.tsx`
|
||
- `ui/components/sessions/EventCockpitDeck.tsx`
|
||
- `ui/components/sessions/WorkshopDrawer.tsx`
|
||
- `ui/components/sessions/EventGuestsDrawer.tsx`
|
||
- `ui/components/sessions/SessionDeck.tsx`
|
||
- `ui/components/sessions/SessionTable.tsx`
|
||
- `ui/components/sessions/SessionsScript.tsx`
|
||
- `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 ]`.
|
||
- **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.
|
||
|
||
---
|
||
|
||
## 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.
|
||
|
||
- **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.
|
||
|
||
---
|
||
|
||
## 3. Proposed Implementation
|
||
|
||
### Phase 1: Universal Ingress Credential Rotation & Backend Math Fix (`server/routes/events.ts`)
|
||
|
||
1. **Universal Ingress Rotation (`POST /api/events/:id/rotate-pin`):**
|
||
- Update endpoint to rotate **BOTH** `pin_code` AND the random slug suffix:
|
||
```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 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}...`
|
||
- 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.
|
||
|
||
2. **Resilient Event Extension Math (`POST /api/events/:id/extend`):**
|
||
- Fix SQL to calculate new expiry from `GREATEST(expires_at, NOW())`:
|
||
```sql
|
||
UPDATE event_passes
|
||
SET expires_at = GREATEST(expires_at, NOW()) + interval '${extendHours} hours'
|
||
WHERE id = ${eventId} AND (created_by = ${user.userId} OR ${isAdmin}) AND is_active = TRUE
|
||
RETURNING slug, expires_at
|
||
```
|
||
|
||
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`.
|
||
|
||
---
|
||
|
||
### Phase 2: Page Hierarchy, Nomenclature & Heading Cleanup (`SessionsPage.tsx`)
|
||
|
||
1. **Page Title:**
|
||
- Set top `<h1>` 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 `<h2>Event Passes</h2>` from `SessionsPage.tsx`. The
|
||
section header is exclusively rendered inside `EventCockpitDeck.tsx` as
|
||
`<h2>Events</h2>` alongside the view toggle.
|
||
3. **Sessions Section:**
|
||
- Retain `<h2>Sessions</h2>` heading with subtitle _"Direct device logins,
|
||
passkey authentications, and delegated agent tokens."_
|
||
|
||
---
|
||
|
||
### Phase 3: Bounded 2-Row Compact Cards & Grid Card Polish (`EventCockpitDeck.tsx`)
|
||
|
||
1. **Section Header & High-Contrast View Toggle:**
|
||
- Header: `<h2>Events</h2>`.
|
||
- 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;`
|
||
- 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;`.
|
||
- **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 ]`.
|
||
- **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.
|
||
|
||
3. **Grid Card Polish:**
|
||
- Replace detached CLI `<details>` 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.
|
||
- Fix double arrow marker bug (`list-style: none;`).
|
||
- 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).
|
||
- Exact Time: `h:mm A` (e.g. `10:39 PM`).
|
||
- Scaled Urgency Badge:
|
||
- $<1$h: Amber `[ 45m left ]`
|
||
- $<24$h: Amber/Green `[ 2h 45m left ]`
|
||
- 1–60d: Green `[ 28d left ]`
|
||
- 2–12 mos: Green `[ 4.5 mos left ]` (no `142d`)
|
||
- $>1$ yr: Green `[ 1.2 yrs left ]` (no `420d`)
|
||
- Expired: Red `[ Expired ]`
|
||
- Tooltip: `title="${fullISODate}"` on hover/long-press.
|
||
- Format: `[Date] · [Time] · [ Colored Urgency Badge ]`.
|
||
|
||
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 ·`
|
||
<span style="background:rgba(34,197,94,0.15);color:#16a34a;padding:2px 6px;border-radius:4px;font-size:0.75rem;font-weight:600;">Active
|
||
7h 54m</span>.
|
||
- **Line 2:**
|
||
`Last Action: ${lastAction || 'ForwardAuth Ingress'} · ${timeAgo} · 💻 Web`
|
||
(or `📟 CLI`).
|
||
- Remove redundant `guest_slug_seat` visual text (keep in tooltip only).
|
||
|
||
---
|
||
|
||
### Phase 5: Delegation Drawer Overhaul (`WorkshopDrawer.tsx` & `SessionsScript.tsx`)
|
||
|
||
1. **Drawer Nomenclature & WAI-ARIA High-Contrast Tabs:**
|
||
- Drawer Header: `<h2>Delegate Session</h2>` 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_
|
||
- **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"`.
|
||
|
||
2. **Handoff State & Button Minimalist Polish:**
|
||
- 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`.
|
||
- Clear form inputs (`eventName`, `eventSlug`, `eventPinCode`, etc.).
|
||
- Reset tab selection to `Single Session`.
|
||
|
||
---
|
||
|
||
## 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`.
|
||
- Verify old PIN and old slug are rejected at `/api/events/join` with 404.
|
||
- Verify new PIN and new slug succeed at `/api/events/join`.
|
||
- Verify existing authenticated guest sessions remain valid.
|
||
2. **Expired Event Extend Math Test (`server/tests/events.test.ts`):**
|
||
- Insert expired event with `expires_at = NOW() - interval '3 hours'`.
|
||
- 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).
|
||
- Test `formatNaturalJoinTime` inverted duration math.
|
||
- Test drawer state machine reset logic.
|
||
|
||
### Quality Gate Commands
|
||
|
||
```bash
|
||
deno fmt --check
|
||
deno task lint
|
||
deno task check
|
||
deno test -A --no-check server/tests/events.test.ts ui/ui_scripts.test.ts
|
||
```
|