Compare commits

...

7 Commits

Author SHA1 Message Date
1ef1125b4a docs(tasks): add and format phase 1, 2, and 3 task specifications from jules 2026-08-26 15:06:57 -07:00
cc38815867
Merge pull request #46 from mrteye/feat-event-join-planning-11918200632794003938
story(events): add phase 2 event overhaul planning document
2026-08-26 15:04:38 -07:00
e357a26abb
Merge pull request #45 from mrteye/feature-guest-ingress-task-plan-17304586254181847688
docs: add task plan for phase 1 of events guest ingress
2026-08-26 15:04:30 -07:00
b2ad719252
Merge pull request #47 from mrteye/feat-ui-sessions-layout-drawer-machine-plan-6316789782106551879
feat(ui): generate task plan for sessions layout and drawer machine
2026-08-26 15:04:05 -07:00
google-labs-jules[bot]
5c1be5657e feat(ui): generate task plan for sessions layout and drawer machine
Generates a planning task file in `tasks/new/` detailing the architecture, requirements, and steps for Phase 3 of the Event & Session Overhaul, specifically addressing the page hierarchy, the 2-state drawer machine, and layout bugs.

Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
2026-08-26 22:01:09 +00:00
google-labs-jules[bot]
25451ff172 story(events): add phase 2 event overhaul planning document
Adds a new task specification in `tasks/new/` documenting the plan for Phase 2 Event Overhaul,
including PIN discovery, input normalization, rate limiting, and NAT-safe idempotent re-entry
for event joins.

Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
2026-08-26 22:01:03 +00:00
google-labs-jules[bot]
4ec15fe35f docs: add task plan for phase 1 of events guest ingress
Adds the task definition markdown file `2026-0826.01.jul.story.events.phase-1-guest-ingress-1400.md` detailing the architectural scope and plan for enabling Traefik ForwardAuth guest ingress, bridging custom scopes to the Launchpad UI via UNION query, and wiring event claim audit logs using event.id.

Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
2026-08-26 22:00:56 +00:00
3 changed files with 254 additions and 0 deletions

View File

@ -0,0 +1,85 @@
# TASK METADATA
- **Target Files:** `server/routes/auth_forward.ts`, `ui/db_queries.ts`,
`ui/mod.ts`, `server/routes/events.ts`, `server/tests/forward_auth.test.ts`,
`server/tests/events.test.ts`
- **Core Objective:** Implement Phase 1 of the Event & Session Overhaul: Enable
Traefik ForwardAuth guest ingress for scoped event attendees, bridge session
custom scopes to the Launchpad UI, and wire audit logging on event seat
claims.
- **Dependencies:** None.
- **Additional Important Notes:** Follow zero-trust default-deny principles for
ForwardAuth. Guest sessions minting needs to correctly log to the Merkle audit
ledger for SIEM visibility.
---
### 2. Architectural Considerations & Risks
- **Risks:**
- **ForwardAuth Security:** Modifying ForwardAuth to accept `guest` accounts
risks inadvertently allowing guests to access applications they are not
explicitly scoped for. Strict validation of `auth.customScopes` against the
requested `appRecord.name` is critical.
- **Launchpad Leakage:** The `getDashboardApps` query modification must be
robust. If the UNION query is not correctly structured, it could leak
visibility of unregistered or unauthorized apps to guests.
- **Audit Logging Integrity:** Failure to capture the audit events properly
during the `join` phase would break SIEM visibility. Using the non-blocking
`auditWrapper.auditLog` is required to avoid impacting user request latency
while ensuring compliance.
- **Alternatives:**
- We considered a bypass in `ui/db_queries.ts` where guests completely skip
the `grants` JOIN. However, using a `UNION` is structurally superior as it
seamlessly supports both pure guests and regular active users who might hold
delegated scoped passes. It provides a unified data retrieval flow.
### 3. Proposed Implementation
#### Phase 1: ForwardAuth Guest Ingress (`server/routes/auth_forward.ts`)
1. **Permit Guest Accounts:** Update the account status validation to allow
`user.account_status === 'guest'` in addition to `'active'`.
2. **Custom Scopes Validation:** If the user is a guest (or relying on custom
scopes), verify that `auth.customScopes` contains the explicit application
grant (e.g., `app:${appRecord.name}`). If not, return a 403 Forbidden.
3. **Header Injection:** For guest sessions, compute the scopes to inject into
`X-Forwarded-Scopes` by taking the comma-joined list of `auth.customScopes`
(`auth.customScopes.join(",")`). This propagates the app scope (e.g.,
`app:ed-droid`) and role (e.g., `viewer`) to downstream reverse proxies.
#### Phase 2: Launchpad App Query (`ui/db_queries.ts` & `ui/mod.ts`)
1. **Function Signature Update:** Update
`getDashboardApps(userId: string, isAdmin: boolean, customScopes?: string[])`
to accept the session's custom scopes.
2. **UNION Query:** Update the non-admin SQL query to use a `UNION`.
- The first part queries explicitly granted apps via the `grants` table.
- The second part parses the provided `customScopes` array to extract allowed
app names (e.g., stripping the `app:` prefix), querying the `apps` table
for those matching names, and assigning them a pseudo-role such as
`"Guest (Viewer)"` (or extracting the role from the scopes if possible).
3. **UI Integration:** In `ui/mod.ts`, pass `auth.customScopes` from the
resolved session into `getDashboardApps`.
#### Phase 3: Join Audit Logging (`server/routes/events.ts`)
1. **Audit `POST /api/join` (Web):** Immediately after minting the guest
session, invoke
`auditWrapper.auditLog(guestUuid, "event_seat_claimed", event.id, { slug: event.slug, name: event.name, seatNumber: event.seats_claimed, method: "web" }, getClientIp(c))`.
2. **Audit `GET /join/:slug` (CLI):** Similarly, wire
`auditWrapper.auditLog(guestUuid, "event_seat_claimed", event.id, { slug: event.slug, name: event.name, seatNumber: event.seats_claimed, method: "cli" }, getClientIp(c))`
into this endpoint before returning the token.
#### Phase 4: Unit Testing
1. **ForwardAuth Tests (`server/tests/forward_auth.test.ts`):**
- Add tests simulating a guest session with an `app:{name}` scope to ensure a
`200 OK` response with correctly formatted `X-Forwarded-Scopes`.
- Add tests simulating a guest session without the necessary `app:{name}`
scope to ensure a `403 Forbidden` response.
2. **Events Tests (`server/tests/events.test.ts`):**
- Ensure the `POST /api/join` and `GET /join/:slug` assertions capture the
`auditLog` spy calls and verify the exact payload (`event_seat_claimed`
action, `event.id` as resource, correct metadata structure).

View File

@ -0,0 +1,84 @@
### 1. TASK METADATA
- **Target Files:** `server/routes/events.ts`, `ui/components/LoginPage.tsx`,
`ui/components/RegisterPage.tsx`, `ui/components/EventJoinPage.tsx`
- **Core Objective:** Implement Phase 2 Event Overhaul: Add PIN discovery links,
normalize input, apply tiered Valkey rate limiting, and implement NAT-safe
idempotent re-entry on join.
- **Dependencies:** `server/middleware.ts` (for getClientIp),
`server/ratelimit.ts` (for rate limiting logic).
- **Additional Important Notes:** Frontend changes should mirror backend
normalization. Re-entry must seamlessly return an existing active guest
session linked to the correct event without incrementing `seats_claimed`.
---
### 2. Architectural Considerations & Risks
- **Risks:**
- If idempotent re-entry is flawed, returning a mismatched event session could
allow access to an event a user didn't register for, or incorrectly redirect
them. We must strictly verify the existing session is a guest session
belonging to the target event.
- Rate limiting logic might accidentally block legitimate NAT'ed traffic if
thresholds are set too low; 5 fail / 3 success per minute per IP should be
monitored.
- Normalization on the backend must be implemented securely using robust
string replacements to prevent SQL injection or unexpected matching
behaviors, especially with PostgreSQL parameterized queries
(`sqlWrapper.sql`).
- **Alternatives:**
- For normalization, instead of just runtime stripping, we could normalize
upon creation and store both normalized and display values. However,
stripping at runtime with `REPLACE(pin_code, '-', '')` and `LOWER(slug)` is
an acceptable and low-friction approach for our scale.
- Rate limiting could use sliding window algorithms, but Valkey-backed fixed
windows via the existing `rateLimitWrapper` are more efficient and standard
for our stack.
### 3. Proposed Implementation
#### Phase 1: Login & Registration Discovery Links
- **File:** `ui/components/LoginPage.tsx`
- Add a "Join with PIN" link next to the existing "Register with Invite" in
the footer links section to ensure unauthenticated users can easily discover
the join portal.
- **File:** `ui/components/RegisterPage.tsx`
- Add a similar "Join with PIN" link to the footer for consistency across
entry points.
#### Phase 2: Input Normalization (Frontend & Backend)
- **File:** `ui/components/EventJoinPage.tsx`
- Enhance the UI script to tolerate input formatting, naturally handling
hyphens, spaces, and case differences during submission, providing immediate
visual feedback without rejecting valid raw input (e.g. `241881` vs
`241-881`).
- **File:** `server/routes/events.ts` (`POST /api/join` & `GET /join/:slug`)
- Strip whitespace, hyphens, and force lowercase on the incoming code on the
backend (`code.trim().replace(/[-\s]/g, '').toLowerCase()`).
- Update the `sqlWrapper` query to match normalized inputs:
`WHERE (LOWER(slug) = ${code} OR REPLACE(pin_code, '-', '') = ${code})`.
#### Phase 3: Anti-DoS Rate Limiting
- **File:** `server/routes/events.ts` (`POST /api/join`)
- Use `getClientIp(c)` from our middleware to determine the client IP address.
- Mount tiered Valkey rate limits using the keys `ratelimit:join:fail:<ip>`
(limit: 5 per 60s) and `ratelimit:join:success:<ip>` (limit: 3 per 60s).
- Apply the rate limiting logic prior to executing any database queries to
prevent database DoS on brute-force PIN attempts.
#### Phase 4: NAT-Safe Idempotent Re-entry
- **File:** `server/routes/events.ts` (`POST /api/join`)
- Before claiming a seat and generating a new session, check for an existing
valid user session using `getAuthenticatedUser(c)`.
- If a valid session exists, query the database to determine if the `user_id`
is linked to a guest account for the target event (e.g., username matches
`guest_<event.slug>_%`).
- If the user is already a guest of this event, bypass the `seats_claimed + 1`
update and early-return `{ success: true, sessionId, redirectUrl }`, keeping
the existing session intact.

View File

@ -0,0 +1,85 @@
# TASK METADATA
- **Target Files:** `ui/components/SessionsPage.tsx`,
`ui/components/sessions/WorkshopDrawer.tsx`,
`ui/components/sessions/SessionsScript.tsx`
- **Core Objective:** Overhaul visual hierarchy of `/dashboard/sessions` and
enforce strict 2-state mutually exclusive rendering in `WorkshopDrawer.tsx`.
- **Dependencies:** None.
- **Additional Important Notes:** Must use pure vanilla JavaScript for DOM
manipulation (no React/framework state). Ensure copy pills trigger accessible
notifications (aria-live/toast). Fix double emoji bug in JS. Ensure mobile
title wrapping. All changes must pass `ui/ui_scripts.test.ts`.
---
## Architectural Considerations & Risks
- **Risks:**
- **DOM Manipulation Regression:** Because this UI operates strictly on
vanilla JavaScript, changing the layout and grouping DOM elements within new
containers (like `#eventCreateState` and `#eventHandoffState`) risks
breaking existing DOM ID bindings in `SessionsScript.tsx` if IDs are changed
or misplaced. We must strictly preserve all existing DOM IDs.
- **Test Compatibility:** The client-side scripts are tested via hermetic
`new Function()` execution in `ui/ui_scripts.test.ts` (`validateJsSyntax`).
The script modifications must remain structurally valid and strictly avoid
any unsupported syntax or external framework dependencies.
- **Mobile Layout Breakage:** When fixing the long titles to wrap correctly,
we must ensure CSS properties like `word-break: break-word` and
`overflow-wrap: break-word` don't inadvertently stretch flex containers
horizontally on small screens.
- **Alternatives:**
- The proposed container-based 2-state machine approach for
`WorkshopDrawer.tsx` is native and robust. It's the most appropriate
solution given our constraints (no client-side frameworks). Using a
class-based toggle could also work, but explicit element display
manipulation is clearer for the specific 2-state requirement.
## Proposed Implementation
### 1. Page Hierarchy Update (`ui/components/SessionsPage.tsx`)
- Relocate the main header block (containing `<h1>Active Sessions & Passes</h1>`
and the `[ 🔑 Delegate Session ]` button) to be the uppermost visible element
below the `#status-banner`.
- Move the `EventCockpitDeck` component to render below the new header block.
- Keep the `SessionTable` and `SessionDeck` at the bottom.
### 2. WorkshopDrawer 2-State Machine (`ui/components/sessions/WorkshopDrawer.tsx`)
- Wrap the initial creation form (`#eventForm`), including inputs, submit
button, and cancel button, into a new container `div` with
`id="eventCreateState"`. Default this container to `display: block`.
- Rename or wrap the `#eventHandoffModal` content into a new container `div`
with `id="eventHandoffState"`. Default this container to `display: none`.
- In `#eventHandoffState`, ensure it contains the 3 cards (PIN + /join, Direct
Link, CLI 1-Liner) and update the final button to be a single "Dismiss"
button.
- Ensure `createdEventTitle` has `overflow-wrap: break-word` and
`word-break: break-word` along with a max-width to allow long titles to wrap
cleanly on mobile screens.
### 3. JavaScript Logic Update (`ui/components/sessions/SessionsScript.tsx`)
- **State Toggling:** Update `handleCreateEvent` so that upon successful
creation, it sets
`document.getElementById('eventCreateState').style.display = 'none'` and
`document.getElementById('eventHandoffState').style.display = 'block'`.
- **Drawer Reset:** Ensure when the delegate drawer is closed (via
`closeDelegateDrawer` or the new Dismiss button), the states are reset:
`#eventCreateState` to block, `#eventHandoffState` to none.
- **Emoji Fix:** Remove the hardcoded `'🎟️ '` prefix from the event title
injection line:
`document.getElementById('createdEventTitle').textContent = ev.name + ' Live!';`
- **Accessibility / Toast:** Ensure `copyEventHandoff` function already triggers
`showNotice` (which acts as a toast). Verify ARIA roles for the banner if
necessary, or just rely on the existing `showNotice` function which is already
in place.
### 4. Quality Gates
- Run Deno tests: `deno test --allow-all` (specifically validating
`ui/ui_scripts.test.ts`).
- Run code formatting: `deno fmt`.