fix: race condition hardening across auth, counters, and data fetching (#102)

* fix: prevent race conditions in token operations and auth flows

Backend:
- Refresh token rotation: use atomic UPDATE...WHERE revoked_at IS NULL
  to prevent concurrent refresh requests from both succeeding
- Account invite codes: SELECT FOR UPDATE to prevent double-spend
- Platform invite codes: SELECT FOR UPDATE to prevent double-spend
- Password reset tokens: SELECT FOR UPDATE to prevent double-use
- Email verification tokens: SELECT FOR UPDATE to prevent double-use

Frontend:
- Token refresh subscriber arrays: swap before iterating so a throwing
  callback doesn't leave the queue in a dirty state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: atomic counters, plan limit re-check, and double-submit guard

Backend:
- Tree usage_count: use SQL-level UPDATE (Tree.usage_count + 1) instead
  of Python-level increment to prevent lost updates under concurrency
- Tag usage_count: same SQL-level atomic increment/decrement in both
  create_tree and update_tree (delete_tree already used this pattern)
- Plan tree limit: re-check count after db.flush() to close the TOCTOU
  window where two concurrent creates could both pass the pre-check

Frontend:
- TreeEditorPage: add isSaving early-return guard inside handleSaveDraft
  and handlePublish callbacks so Ctrl+S can't bypass the button disabled
  prop and fire duplicate save requests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: prevent stale API responses from overwriting newer data

- SessionHistoryPage: move loadSessions into effect with cancelled flag
  so rapid filter/tab changes discard outdated responses
- TreeLibraryPage: add request ID ref to loadTrees so stale responses
  from previous filter selections are discarded
- QuickStartPage: add request ID ref to debounced search so out-of-order
  responses don't overwrite newer search results

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add flexible intake design — deferred variables + prepared sessions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit was merged in pull request #102.
This commit is contained in:
chihlasm
2026-03-10 01:57:22 -04:00
committed by GitHub
parent 5095b0d8df
commit 4727106141
9 changed files with 305 additions and 98 deletions

View File

@@ -0,0 +1,139 @@
# Plan: Flexible Intake — Deferred Variables + Prepared Sessions
## Context
The current intake form on procedural flows is a blocking modal that forces engineers to enter all variables before the flow starts. This creates friction because:
- Engineers don't always have all the information upfront
- Information often lives in PSA tickets, RMM tools, or was communicated verbally
- Sometimes a lead/PM has the info and wants to set up the session for an engineer to execute later
**Goal:** Replace the blocking intake modal with two complementary workflows:
1. **Deferred Variables** — start the flow immediately, fill variables inline as you encounter them
2. **Prepared Sessions** — pre-fill variables ahead of time, optionally assign to an engineer, execute later
---
## Design
### Workflow 1: Deferred Variables (Start Now, Fill Later)
- "Start Flow" launches the session immediately — no intake modal
- Variables begin empty
- When a step references `[VAR:server_name]` and it's unfilled, an **inline input prompt** renders in place — visually prominent with the field's label, help text, and styling that stands out (cyan border, slight glow)
- Once filled, the value persists and resolves everywhere in the session
- Engineers can also open a **"Session Variables" side panel** at any time to see/edit all variables
- At **session completion**, if required variables are still empty → soft warning with a prompt to fill them (for complete export documentation), but not a hard block
### Workflow 2: Prepared Sessions (Set Up Ahead, Execute Later)
- From a flow's detail page: "Prepare Session" action opens a form to fill variables + assign an engineer
- Creates a session in `prepared` state — `started_at` is null, variables populated, no steps executed
- **Assignment:** Preparer can assign to a specific engineer on their team (or leave unassigned)
- **Personal queue:** Engineers see prepared sessions assigned to them in a dedicated section (Quick Start page or Session History tab)
- Clicking a prepared session opens the flow with variables pre-populated; execution begins normally
- Unassigned prepared sessions are visible to all team members
### Data Model Changes
**Session model additions:**
- `prepared_by_id` — UUID FK to users, nullable. Who created the prepared session.
- `assigned_to_id` — UUID FK to users, nullable. Who should execute it.
- Use existing convention: `started_at IS NULL` = prepared, `started_at IS NOT NULL, completed_at IS NULL` = active, `completed_at IS NOT NULL` = completed
**Session variables become mutable:**
- `session_variables` is currently write-once at session creation
- New endpoint: `PATCH /sessions/{id}/variables` — updates individual variables during an active session
- Only the session owner (or assigned engineer) can update variables
**Migration:** One migration adding `prepared_by_id` and `assigned_to_id` columns with FK constraints.
### Variable Resolution Changes
**Backend:**
- No changes to export pipeline — it already resolves variables from `session_variables`
- New `PATCH /sessions/{id}/variables` endpoint accepts partial variable updates
- Session creation no longer validates required intake fields (they can be filled later)
**Frontend — `resolveVariables()` in `lib/variableResolver.ts`:**
- Currently returns a plain string with `[VAR:x]` replaced
- New behavior: also identify unresolved variables so `StepDetail` can render inline prompts
**Frontend — `StepDetail.tsx`:**
- When rendering step content, unresolved `[VAR:x]` references render as inline input components
- Inline prompt design: input field with the field's label as placeholder, cyan border, subtle glow background to make them visually prominent and easy to spot
- On blur/enter: calls `PATCH /sessions/{id}/variables` → re-renders step with resolved value
- Lookup field metadata (label, field_type, help_text, options) from the intake form definition in the tree snapshot
**Frontend — Session Variables Panel:**
- Existing "View Parameters" button becomes "Session Variables" — now editable
- Shows all intake form fields with filled/unfilled status
- Unfilled required fields highlighted
- Editing a field here updates the session and re-resolves all visible steps
### API Changes
| Method | Endpoint | Description |
|--------|----------|-------------|
| `PATCH` | `/sessions/{id}/variables` | Update one or more session variables (partial dict merge) |
| `POST` | `/sessions` | Remove required-field validation for intake forms (allow empty start) |
| `GET` | `/sessions` | Add `assigned_to_id` and `status=prepared` filter params |
| `POST` | `/sessions/prepare` | New endpoint: create a prepared session with variables + optional assignee |
### UI Changes
| Location | Change |
|----------|--------|
| **Flow detail page** | "Start Flow" no longer shows intake modal. Add "Prepare Session" option (dropdown or secondary button) |
| **ProceduralNavigationPage** | Remove `IntakeFormModal` gating. Add "Session Variables" panel button. Inline prompts on steps with unfilled variables |
| **StepDetail** | Render inline input prompts for unresolved `[VAR:x]` references |
| **Quick Start page** | New "Prepared for You" section showing assigned prepared sessions |
| **Session History** | New "Prepared" tab/filter showing prepared sessions |
| **Prepare Session form** | New modal/page: select flow, fill variables, assign engineer, save |
| **Session completion** | Soft warning if required variables still empty |
### What Gets Removed
- `IntakeFormModal.tsx` — no longer used as a blocking gate (may repurpose as the "Prepare Session" form)
- Required-field validation in `POST /sessions` for intake form fields
- The `showIntakeForm` / intake modal state in `ProceduralNavigationPage`
---
## Implementation Phases
### Phase 1: Mutable Variables + Inline Prompts
**Files:** `sessions.py`, `variableResolver.ts`, `StepDetail.tsx`, `ProceduralNavigationPage.tsx`
1. Add `PATCH /sessions/{id}/variables` endpoint
2. Remove intake form required-field blocking from `POST /sessions`
3. Update `resolveVariables()` to identify unresolved variables
4. Build inline variable prompt component for `StepDetail`
5. Make "View Parameters" panel editable
6. Remove `IntakeFormModal` gating from `ProceduralNavigationPage`
### Phase 2: Prepared Sessions
**Files:** `sessions.py`, `session.py` (schemas), migration, `PrepareSessionModal.tsx`, `QuickStartPage.tsx`, `SessionHistoryPage.tsx`
1. Migration: add `prepared_by_id`, `assigned_to_id` to sessions table
2. `POST /sessions/prepare` endpoint
3. `GET /sessions` filter support for `assigned_to_id` and prepared status
4. Prepare Session modal/form (reuse IntakeFormModal field rendering)
5. "Prepared for You" section on Quick Start
6. "Prepared" filter on Session History
### Phase 3: Polish
1. Soft completion warning for unfilled required variables
2. Prepared session staleness indicator (optional)
3. Notification when a session is prepared/assigned to you (optional, future)
---
## Verification
- Start a procedural flow without filling any variables → flow starts immediately, no modal
- Navigate to a step with `[VAR:server_name]` → see inline input prompt
- Fill the variable inline → value resolves across all steps
- Open Session Variables panel → see all fields, edit one → reflected in steps
- Prepare a session from flow detail page → assign to another engineer
- Log in as assigned engineer → see prepared session in Quick Start queue
- Click prepared session → flow opens with variables pre-filled, execute normally
- Complete a session with one unfilled required variable → see soft warning
- Export session → variables resolved in output, unfilled ones show as `[VAR:x]` or blank