Issue #6: Multiple routers per file made main.py verbose.
Changes:
- Add 'routers' list export to booking.py and invites.py
- Update main.py to iterate over router lists for multi-router modules
- Keep explicit registration for single-router modules
- Cleaner separation between simple and complex route modules
Issue #5: Date validation logic was duplicated across availability
and booking routes.
Changes:
- Add date_validation.py with shared utilities:
- get_bookable_date_range: returns (min_date, max_date) tuple
- validate_date_in_range: validates date with contextual errors
- Update routes/availability.py to use shared utilities
- Update routes/booking.py to use shared utilities
- Remove redundant _get_date_range_bounds and _get_bookable_date_range
- Error messages now include context (book, set availability, etc.)
Issue #4: Pagination logic was repeated across multiple routes.
Changes:
- Add pagination.py with reusable utilities:
- calculate_total_pages: computes page count from total/per_page
- calculate_offset: computes offset for given page
- create_paginated_response: builds PaginatedResponse with metadata
- Update routes/audit.py to use pagination utilities
- Update routes/booking.py to use pagination utilities
- Update routes/invites.py to use pagination utilities
The utilities handle the common pagination math while routes
still manage their own query logic (filters, joins, ordering).
Issue #3: The frontend Permission enum was manually duplicated from
the backend. While full generation isn't practical, this change
ties the frontend constants to the generated OpenAPI types for
compile-time validation.
Changes:
- Update ConstantsResponse schema to use actual Permission/InviteStatus
enums (enables OpenAPI to include enum values)
- Import enums in schemas.py (no circular dependency issue)
- Update auth-context.tsx to derive PermissionType from generated schema
- Update meta route to return enum instances instead of string values
- Permission values are now type-checked against the OpenAPI schema
If a permission is added to the backend but not to the frontend's
Permission object, TypeScript will fail to compile. This provides
a safety net without requiring a complex build-time generation step.
Issue #2: The profile route used a custom role-based check instead
of the permission-based pattern used everywhere else.
Changes:
- Add MANAGE_OWN_PROFILE permission to backend Permission enum
- Add permission to ROLE_REGULAR role definition
- Update profile routes to use require_permission(MANAGE_OWN_PROFILE)
- Remove custom require_regular_user dependency
- Update frontend Permission constant and profile page
- Update invites page to use permission instead of role check
- Update profile tests with proper permission mocking
This ensures consistent authorization patterns across all routes.
Tests that call POST /api/counter/increment now explicitly request
the mock_enqueue_job fixture. This prevents the mock from masking
issues in other tests that don't need it.
- Added cleanup function to kill background processes
- Trap on EXIT ensures cleanup happens on normal exit, errors, or Ctrl+C
- Prevents orphaned backend/worker processes if script is interrupted
- Added ASYNCPG_DATABASE_URL constant in database.py
- Updated jobs.py to import from database module
- Updated worker.py to import from database module
- Removed duplicate URL parsing logic from both files
- Removed brittle 1-second waitForTimeout
- Use toPass() with polling to wait for job outcome
- Check for specific user's email instead of exact row count
- More robust under slow worker conditions or CI load
- Changed from using scalars().all() with lazy='joined' relationship
- Now uses explicit join similar to other audit endpoints
- Guarantees single query regardless of SQLAlchemy async behavior
- Added get_job_pool() for lazy pool initialization
- Added close_job_pool() for graceful shutdown
- Hooked pool shutdown into FastAPI lifespan
- Reuses connections instead of creating new ones per enqueue
- Update scripts/e2e.sh to start worker alongside backend
- Create frontend/e2e/random-jobs.spec.ts with 3 tests:
- Counter increment creates random job outcome visible to admin
- Admin can view empty random jobs list
- Regular user cannot access random jobs page
- Create /admin/random-jobs/page.tsx with outcomes table
- Add 'admin-random-jobs' to PageId type in Header
- Add 'Random Jobs' nav item to ADMIN_NAV_ITEMS
- Display: ID, Job ID, Triggered By, Value, Duration, Status, Created At
- Uses VIEW_AUDIT permission
- Add RandomNumberOutcomeResponse schema to schemas.py
- Add GET /api/audit/random-jobs endpoint to routes/audit.py
- Returns list of outcomes (newest first, no pagination)
- Requires VIEW_AUDIT permission
- Add tests: admin can access, regular user forbidden, unauthenticated 401
- Add RandomNumberOutcome model to models.py
- Update worker.py to execute job logic:
- Generate random number 0-100
- Record execution duration
- Store outcome in database
- Add test_jobs.py with unit tests for job handler logic
- Add backend/jobs.py with enqueue_random_number_job function
- Modify counter increment endpoint to enqueue job after incrementing
- Add mock_enqueue_job fixture to conftest.py for all tests
- Add test_increment_enqueues_job_with_user_id to verify correct user_id
- Job is enqueued synchronously; failure causes request to fail
- Add pgqueuer dependency to pyproject.toml
- Create worker.py with schema installation and job handler registration
- Add make worker command to Makefile
- Update make dev to run worker alongside backend/frontend
- Use has_table() check for idempotent schema installation
- Register 'random_number' job handler (placeholder that logs for now)
- Add ruff as dev dependency
- Configure ruff in pyproject.toml with strict 88-char line limit
- Ignore B008 (FastAPI Depends pattern is standard)
- Allow longer lines in tests for readability
- Fix all lint issues in source files
- Add Makefile targets: lint-backend, format-backend, fix-backend
- Replaced borderColor with full border property in dayCardActive
- Replaced borderColor with full border property in dayCardSource
- Replaced borderColor with full border property in dayCardTarget
- Prevents React warning about mixing shorthand and non-shorthand properties
- Fixes console error when using 'Clear all' button on availability page
- Changed formatTime back to use toLocaleTimeString (local time)
- Changed formatDateTime back to use toLocaleString (local time)
- App now displays all times in user's local timezone
- Backend still stores times in UTC, frontend converts for display
- Changed formatDateTime to use UTC methods instead of toLocaleString
- Prevents timezone conversion when displaying appointment times
- Now booking at 11:45 shows as 11:45 in appointments page, not 12:45
- Consistent with formatTime which already uses UTC
- Manual formatting to match previous format: 'Mon, Jan 15, 11:45'
- Memoized dates array using useMemo to prevent recreation on every render
- Removed dates from useEffect dependency array since it's now stable
- Prevents cascade of API requests when opening booking page
- Dates only recalculate when minAdvanceDays or maxAdvanceDays change
- Fetch availability for all dates on page load
- Track which dates have available slots in state
- Disable date buttons that have no availability
- Add visual styling for disabled dates (reduced opacity, not-allowed cursor)
- Prevent clicking on dates with no availability
- Improves UX by showing which dates are bookable at a glance
- Changed formatTime() to use UTC methods instead of toLocaleTimeString()
- Prevents timezone conversion when displaying booking slots
- Now admin sets 9-17 and user sees 9-17, regardless of timezone
- Fixes 1-hour offset issue when user timezone differs from UTC
- Replaced borderColor with full border property in dateButtonSelected
- Replaced borderColor with full border property in slotButtonSelected
- Prevents React warning about mixing shorthand and non-shorthand properties
- Fixes console error when clicking elements on booking page
- Updated test to match new descriptive error message format
- Changed from 'not within available' to 'not within any available time ranges'
- All tests now passing
- Created frontend/e2e/helpers/auth.ts with shared auth utilities
- Extracted getRequiredEnv, REGULAR_USER, ADMIN_USER, clearAuth, loginUser
- Updated all three e2e test files to use shared helpers
- Reduced code duplication across test files
- Added specific slot time and date to availability error message
- Added appointment ID and context to 'not found' errors
- Added formatted appointment time to past appointment cancellation errors
- Added date context to slot overlap error messages
- All errors now provide actionable information to users
- Replaced all UTC imports with timezone imports
- Changed all datetime.now(UTC) to datetime.now(timezone.utc)
- Consistent with booking.py and more explicit about timezone usage
- Updated models.py, routes/auth.py, and routes/invites.py
- Added explicit joinedload(Appointment.user) to admin appointment queries
- Makes the eager loading intention clear and explicit
- Replaced comment-based documentation with actual query options