Files
sneakyklaus/docs/designs/v0.3.0/overview.md
Phil Skentelbery 915e77d994 chore: add production deployment config and upgrade path requirements
- Add docker-compose.yml and docker-compose.example.yml for production deployment
- Add .env.example with all required environment variables
- Update architect agent with upgrade path requirements
- Update developer agent with migration best practices
- Add Phase 3 design documents (v0.3.0)
- Add ADR-0006 for participant state management

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-22 19:32:42 -07:00

18 KiB

System Overview - v0.3.0

Version: 0.3.0 Date: 2025-12-22 Status: Phase 3 Design

Introduction

This document describes the design for Phase 3 of Sneaky Klaus, building on the participant authentication foundation established in v0.2.0. This phase focuses on participant self-management capabilities and pre-matching participant experience.

Phase 3 Scope: Implement Epic 6 (Participant Self-Management) and Epic 4.5 (View Participant List Pre-Matching), enabling participants to manage their own profiles and see who else has registered before matching occurs.

Phase Goals

The primary goals for v0.3.0 are:

  1. Enable participant self-service: Allow participants to update their profiles without admin intervention
  2. Support graceful withdrawals: Handle participants who need to drop out before matching
  3. Build social engagement: Let participants see who else is participating (pre-matching only)
  4. Maintain data integrity: Ensure profile changes and withdrawals don't break the exchange flow

User Stories in Scope

Epic 6: Participant Self-Management

  • 6.1 Update Profile: Participants can edit their name and gift ideas before matching
  • 6.2 Withdraw from Exchange: Participants can opt out before registration closes
  • 6.3 Update Reminder Preferences: Participants can toggle reminder emails on/off

Epic 4: Participant Registration (Continuation)

  • 4.5 View Participant List (Pre-Matching): Registered participants can see who else has joined

Out of Scope for v0.3.0

The following features are explicitly not included in this phase:

  • Post-matching participant experience (Epic 11) - deferred to Phase 4
  • Matching system (Epic 8) - deferred to Phase 5
  • Admin exchange management beyond what v0.1.0 provided
  • Participant removal by admin (Epic 9) - deferred to Phase 6
  • Notification emails beyond registration confirmation (Epic 10) - partial in Phase 2, remainder in Phase 7

Architecture Overview

Phase 3 builds incrementally on the v0.2.0 architecture with no new infrastructure components:

graph TB
    subgraph "Phase 3 Additions"
        ProfileUpdate[Profile Update UI]
        WithdrawUI[Withdraw UI]
        ParticipantList[Participant List View]

        ProfileUpdate --> ParticipantRoutes
        WithdrawUI --> ParticipantRoutes
        ParticipantList --> ParticipantRoutes
    end

    subgraph "Existing v0.2.0 Foundation"
        ParticipantRoutes[Participant Routes]
        ParticipantAuth[Participant Auth Decorator]
        ParticipantModel[Participant Model]
        ExchangeModel[Exchange Model]
        Database[(SQLite)]

        ParticipantRoutes --> ParticipantAuth
        ParticipantRoutes --> ParticipantModel
        ParticipantRoutes --> ExchangeModel
        ParticipantModel --> Database
        ExchangeModel --> Database
    end

Technical Design Principles

This phase adheres to the project's core principles established in v0.1.0 and v0.2.0:

  1. No new database tables: All features use existing Participant and Exchange models
  2. No new external dependencies: Pure Flask/SQLAlchemy implementation
  3. Soft deletes: Withdrawals use withdrawn_at timestamp rather than hard deletes
  4. State-based permissions: Operations restricted based on exchange state
  5. TDD approach: Write tests first, implement to pass

Key Design Decisions

1. Profile Update Restrictions

Decision: Profile updates are only allowed when exchange is in draft, registration_open, or registration_closed states. After matching, profiles are locked.

Rationale:

  • Gift ideas are sent to givers during match notification - changes after matching would create inconsistency
  • Name changes after matching could confuse participants
  • Clear state transition prevents data inconsistency

Implementation:

def can_update_profile(participant: Participant) -> bool:
    """Check if participant can update their profile."""
    exchange = participant.exchange
    return exchange.state in ['draft', 'registration_open', 'registration_closed']

2. Withdrawal Rules

Decision: Withdrawals are only allowed before registration closes. After registration closes, admin intervention required.

Rationale:

  • Pre-closure: minimal impact, just removes one participant
  • Post-closure: admin likely already configuring exclusions or has matched
  • Post-matching: requires re-match, admin should make this decision
  • Clear deadline prevents last-minute dropouts

Implementation:

def can_withdraw(participant: Participant) -> bool:
    """Check if participant can withdraw themselves."""
    exchange = participant.exchange
    return exchange.state in ['draft', 'registration_open']

3. Withdrawal Implementation (Soft Delete)

Decision: Use existing withdrawn_at timestamp field, don't delete participant records.

Rationale:

  • Data model already supports soft deletes (v0.2.0 design)
  • Preserves audit trail of who registered
  • Prevents re-use of email during same exchange
  • Allows admin to see withdrawal history
  • Simplifies database constraints (no cascade delete issues)

Implementation:

def withdraw_participant(participant: Participant):
    """Mark participant as withdrawn."""
    participant.withdrawn_at = datetime.utcnow()
    db.session.commit()

4. Participant List Visibility

Decision: Show participant list to all registered participants, but only show names (not emails or gift ideas).

Rationale:

  • Social aspect: participants want to know who's participating
  • Privacy: emails are admin-only
  • Security: gift ideas are for givers only (post-matching)
  • Pre-matching only: post-matching handled in Phase 4

Display Rules:

  • Show: participant names, count
  • Hide: emails, gift ideas, withdrawn participants
  • Filter: exclude withdrawn participants from list

5. Reminder Preference Changes

Decision: Allow reminder preference changes at any time before exchange completes.

Rationale:

  • Low-impact change (doesn't affect matching or other participants)
  • User preference should be flexible
  • No technical reason to restrict after matching
  • Allows opting out if circumstances change

Implementation: Simple boolean toggle, no state restrictions.

6. Form Validation Strategy

Decision: Use server-side validation with WTForms, add client-side hints for UX.

Rationale:

  • Consistent with Phase 2 implementation decisions
  • Security: never trust client-side validation
  • UX: client-side provides immediate feedback
  • Progressive enhancement: works without JavaScript

Data Flow

Profile Update Flow

sequenceDiagram
    participant P as Participant Browser
    participant F as Flask App
    participant DB as SQLite Database

    P->>F: GET /participant/profile/edit
    F->>F: Check @participant_required
    F->>DB: Load participant & exchange
    F->>F: Check can_update_profile()
    F-->>P: Render edit form (or error)

    P->>F: POST /participant/profile/edit (name, gift_ideas)
    F->>F: Validate form
    F->>F: Check can_update_profile()
    F->>DB: Update participant record
    DB-->>F: Success
    F-->>P: Redirect to dashboard with success message

Withdrawal Flow

sequenceDiagram
    participant P as Participant Browser
    participant F as Flask App
    participant DB as SQLite Database
    participant E as Email Service

    P->>F: POST /participant/withdraw (with confirmation)
    F->>F: Check @participant_required
    F->>DB: Load participant & exchange
    F->>F: Check can_withdraw()
    F->>DB: Set withdrawn_at timestamp
    DB-->>F: Success
    F->>E: Send withdrawal confirmation email
    E-->>F: Email sent
    F->>F: Clear session (log out participant)
    F-->>P: Redirect to public page with confirmation

Participant List Flow

sequenceDiagram
    participant P as Participant Browser
    participant F as Flask App
    participant DB as SQLite Database

    P->>F: GET /participant/dashboard
    F->>F: Check @participant_required
    F->>DB: Load participant's exchange
    F->>DB: Query active participants (withdrawn_at IS NULL)
    DB-->>F: Participant list (names only)
    F-->>P: Render dashboard with participant list

State Machine Impact

Phase 3 doesn't add new exchange states, but adds participant-level state:

stateDiagram-v2
    [*] --> Registered: Registration
    Registered --> UpdatedProfile: Edit profile
    UpdatedProfile --> UpdatedProfile: Edit again
    UpdatedProfile --> Withdrawn: Withdraw
    Registered --> Withdrawn: Withdraw
    UpdatedProfile --> Matched: Admin matches
    Registered --> Matched: Admin matches
    Withdrawn --> [*]

    note right of Withdrawn
        Soft delete: withdrawn_at set
        Cannot re-activate
        Must re-register with new email
    end note

    note right of UpdatedProfile
        Only before matching
        Name and gift_ideas editable
    end note

Component Design

Routes (participant_bp)

New routes added to existing src/routes/participant.py:

Route Method Auth Description
/participant/profile/edit GET, POST @participant_required Edit profile (name, gift ideas)
/participant/preferences POST @participant_required Update reminder preference
/participant/withdraw POST @participant_required Withdraw from exchange

Existing routes used:

  • /participant/dashboard - enhanced to show participant list

Forms (src/forms/participant.py)

New forms:

ProfileUpdateForm:

class ProfileUpdateForm(FlaskForm):
    name = StringField('Name', validators=[DataRequired(), Length(max=255)])
    gift_ideas = TextAreaField('Gift Ideas', validators=[Length(max=10000)])
    submit = SubmitField('Save Changes')

ReminderPreferenceForm:

class ReminderPreferenceForm(FlaskForm):
    reminder_enabled = BooleanField('Send me reminders')
    submit = SubmitField('Update Preferences')

WithdrawForm:

class WithdrawForm(FlaskForm):
    confirm = BooleanField('I understand this cannot be undone',
                           validators=[DataRequired()])
    submit = SubmitField('Withdraw from Exchange')

Templates

New templates in templates/participant/:

  • profile_edit.html - Profile update form
  • withdraw.html - Withdrawal confirmation page (with warnings)
  • participant_list.html - Reusable component for displaying participant names

Enhanced templates:

  • dashboard.html - Add participant list section

Email Templates

New email template in templates/emails/participant/:

  • withdrawal_confirmation.html - Sent when participant withdraws
  • withdrawal_confirmation.txt - Plain text version

Security Considerations

1. Authorization Checks

Every participant operation must verify:

  • User is authenticated as participant (@participant_required)
  • User owns the resource (participant_id matches session)
  • Exchange state allows the operation (can_update_profile, can_withdraw)

2. CSRF Protection

All POST operations require CSRF tokens (WTForms automatic).

3. Input Validation

  • Name: 1-255 characters, required
  • Gift ideas: 0-10,000 characters, optional
  • All inputs sanitized via Jinja2 auto-escaping

4. Privacy

Participant list shows:

  • Names only (public within exchange)
  • Not emails (admin-only)
  • Not gift ideas (giver-only post-matching)
  • Not withdrawn participants (respect withdrawal privacy)

5. Rate Limiting

No new rate limiting needed:

  • Profile updates: legitimate user operation, no abuse vector
  • Withdrawals: self-limiting (can only withdraw once)
  • Participant list: read-only, authenticated

Testing Strategy

Unit Tests

Test business logic in isolation:

  • can_update_profile() logic for each exchange state
  • can_withdraw() logic for each exchange state
  • Soft delete implementation (withdrawn_at timestamp)
  • Participant list filtering (exclude withdrawn)

Integration Tests

Test full request/response cycles:

  • Profile update happy path
  • Profile update when locked (post-matching)
  • Withdrawal happy path
  • Withdrawal when not allowed
  • Participant list rendering

Test Data Setup

Reuse existing test fixtures from Phase 2, add:

  • Multiple participants in same exchange
  • Participants in different states (registered, withdrawn)
  • Exchanges in different states

Coverage Target

Maintain 80%+ coverage established in previous phases.

Error Handling

Expected Errors

Scenario HTTP Status User Message Action
Update profile after matching 400 "Your profile is locked after matching" Redirect to dashboard
Withdraw after registration closes 400 "Withdrawal deadline has passed. Contact admin." Redirect to dashboard
Invalid form data 400 Field-specific errors Re-render form with errors
Already withdrawn 400 "You have already withdrawn" Redirect to public page

Unexpected Errors

  • Database errors: Log, flash generic error, redirect safely
  • Missing participant record: Clear session, redirect to landing page
  • Orphaned session: Clear session, redirect to landing page

Performance Considerations

Database Queries

Participant List Query:

# Efficient query with single DB hit
participants = Participant.query.filter(
    Participant.exchange_id == exchange_id,
    Participant.withdrawn_at.is_(None)
).order_by(Participant.name).all()

Profile Load:

  • Already loaded by @participant_required decorator (uses g.participant)
  • No additional query needed

Caching

Not needed for v0.3.0:

  • Participant counts are small (3-100 typical)
  • Updates are infrequent
  • Reads are authenticated (no public caching)

Migration Requirements

No database migrations required for v0.3.0.

All fields already exist:

  • participant.name - editable
  • participant.gift_ideas - editable
  • participant.reminder_enabled - editable
  • participant.withdrawn_at - set on withdrawal

Deployment Impact

Breaking Changes

None. v0.3.0 is fully backward compatible with v0.2.0.

Configuration Changes

None. No new environment variables or configuration needed.

Data Migration

None. Existing data fully compatible.

User Experience Flow

Participant Journey (Pre-Matching)

  1. Register (Phase 2)

    • Receive confirmation email with magic link
  2. View Dashboard (Phase 2 + Phase 3)

    • See own information
    • NEW: See list of other participants (names only)
  3. Update Profile (Phase 3)

    • Click "Edit Profile" from dashboard
    • Update name or gift ideas
    • Save changes
    • See confirmation message
  4. Change Reminder Preference (Phase 3)

    • Toggle reminder checkbox on dashboard
    • See confirmation message
  5. Withdraw (if needed) (Phase 3)

    • Click "Withdraw" from dashboard
    • See warning about permanence
    • Confirm withdrawal
    • Receive confirmation email
    • Logged out and redirected to public page

Admin Experience Impact

Phase 3 adds visibility for admins:

Exchange Detail View (Enhancement)

Add to existing exchange detail page:

  • Show participant list with withdrawn indicator
  • Display: "5 active, 1 withdrawn"
  • Allow admin to see withdrawn participants (grayed out)

Implementation Note: This is a minor enhancement to existing admin routes, not a major feature. Can be implemented alongside participant features or deferred to Phase 6 (Admin Participant Management).

Future Considerations

Phase 4 Dependencies

Phase 3 sets up foundations for Phase 4 (Post-Matching Experience):

  • Participant list already implemented (will be reused post-matching)
  • Profile lock logic (can_update_profile) prevents changes after matching
  • Dashboard structure ready to show match assignment

Potential Enhancements (Out of Scope)

Not included in v0.3.0 but possible future additions:

  • Email notification to admin when participant withdraws (Epic 10.5)
  • Allow participants to indicate dietary restrictions or allergies
  • Allow participants to add profile pictures
  • Allow participants to message admin (anonymous contact form)

Acceptance Criteria

Phase 3 is complete when:

  1. Participants can update their name and gift ideas before matching
  2. Participants cannot update profile after matching occurs
  3. Participants can withdraw before registration closes
  4. Participants cannot withdraw after registration closes
  5. Withdrawn participants receive confirmation email
  6. Withdrawn participants are logged out and removed from participant list
  7. Participants can view list of other registered participants (names only)
  8. Participant list excludes withdrawn participants
  9. Participants can toggle reminder preferences at any time
  10. All operations require participant authentication
  11. All operations validate exchange state appropriately
  12. All user stories have passing integration tests
  13. Code coverage remains at 80%+

Implementation Phases

Recommended implementation order (TDD, vertical slices):

Phase 3.1: Participant List View

  • Story 4.5: View Participant List (Pre-Matching)
  • Simplest feature, no state changes
  • Sets up dashboard enhancements

Phase 3.2: Profile Updates

  • Story 6.1: Update Profile
  • Core self-management feature
  • Tests state-based permissions

Phase 3.3: Reminder Preferences

  • Story 6.3: Update Reminder Preferences
  • Simple toggle, low risk
  • Quick win

Phase 3.4: Withdrawal

  • Story 6.2: Withdraw from Exchange
  • Most complex (state changes, email, logout)
  • Benefits from previous features being solid

References


End of Phase 3 Design Overview