UMBRA/progress.md
Kyle Pope 27c65ce40d Fix Round 2 code review findings: type safety, security, and correctness
Backend:
- Add Literal types for status/priority fields (project_task, todo, project schemas)
- Add AccentColor Literal validation to prevent CSS injection (settings schema)
- Add PIN max-length (72 char bcrypt limit) validation
- Fix event date filtering to use correct range overlap logic
- Add revocation check to auth_status endpoint for consistency
- Config: env-aware SECRET_KEY fail-fast, configurable COOKIE_SECURE

Frontend:
- Add withCredentials to axios for cross-origin cookie support
- Replace .toISOString() with local date formatter in DashboardPage
- Replace `as any` casts with proper indexed type access in forms
- Nginx: add CSP, Referrer-Policy headers; remove deprecated X-XSS-Protection
- Nginx: duplicate security headers in static asset location block

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 15:18:49 +08:00

17 KiB

UMBRA - Project Progress

Overview

Personal life administration web app with dark theme, accent color customization, and Docker deployment.

Stack: React + TypeScript | FastAPI + SQLAlchemy | PostgreSQL | Docker Compose


Milestone Tracker

Phase 1: Scaffolding & Infrastructure

  • docker-compose.yaml - 3-service architecture (db, backend, frontend)
  • .env / .env.example - Environment configuration
  • .gitignore - Root gitignore
  • Backend: FastAPI app skeleton (app/main.py, config.py, database.py)
  • Backend: requirements.txt with all Python dependencies
  • Backend: Alembic migration setup (alembic.ini, env.py, initial migration)
  • Backend: start.sh startup script
  • Backend: Dockerfile - Python 3.12-slim
  • Frontend: Vite + React scaffold (package.json, vite.config.ts, tsconfig.json)
  • Frontend: Tailwind CSS + PostCSS configuration
  • Frontend: index.html entry point
  • Frontend: nginx.conf - SPA serving + API proxy
  • Frontend: Dockerfile - Multi-stage node build + nginx serve
  • Verify docker-compose up --build boots all services

Phase 2: Auth & Layout

  • Backend: Settings model + schema
  • Backend: Auth router (PIN setup, login, logout, status with bcrypt + itsdangerous)
  • Frontend: LockScreen.tsx - PIN setup/login UI
  • Frontend: useAuth.ts hook (TanStack Query)
  • Frontend: AppLayout.tsx + Sidebar.tsx - Main layout with navigation
  • Frontend: Accent color theming system (useTheme.ts, CSS custom properties)
  • Frontend: 12 shadcn/ui components (button, card, input, dialog, select, badge, etc.)

Phase 3: Core Features

  • Backend: Todo model, schema, router (CRUD + toggle + filters)
  • Frontend: TodosPage, TodoList, TodoItem, TodoForm
  • Backend: Calendar Event model, schema, router (CRUD + date range filter)
  • Frontend: CalendarPage (FullCalendar integration), EventForm
  • Frontend: FullCalendar dark theme CSS overrides in index.css
  • Backend: Reminder model, schema, router (CRUD + dismiss)
  • Frontend: RemindersPage, ReminderList, ReminderForm

Phase 4: Project Management

  • Backend: Project model with tasks relationship + cascade delete
  • Backend: ProjectTask model, schema, router (nested CRUD under projects)
  • Frontend: ProjectsPage, ProjectCard, ProjectDetail, ProjectForm, TaskForm

Phase 5: People & Locations

  • Backend: Person model, schema, router (CRUD + search)
  • Frontend: PeoplePage, PersonCard, PersonForm
  • Backend: Location model, schema, router (CRUD + category filter)
  • Frontend: LocationsPage, LocationCard, LocationForm

Phase 6: Dashboard & Upcoming

  • Backend: Dashboard aggregation endpoint (stats, events, todos, reminders)
  • Backend: Upcoming endpoint (unified items sorted by date)
  • Frontend: DashboardPage with all widgets + upcoming integration
  • Frontend: StatsWidget (projects, people, locations counts)
  • Frontend: UpcomingWidget (unified timeline with type icons)
  • Frontend: TodoWidget (upcoming todos with priority badges)
  • Frontend: CalendarWidget (today's events with color indicators)
  • Frontend: Active reminders section in dashboard

Phase 6b: Project Subtasks

  • Backend: Self-referencing parent_task_id FK on project_tasks with CASCADE delete
  • Backend: Alembic migration 002_add_subtasks.py
  • Backend: Schema updates — parent_task_id in create, nested subtasks in response, model_rebuild()
  • Backend: Chained selectinload for two-level subtask loading, parent validation on create
  • Frontend: ProjectTask type updated with parent_task_id and subtasks
  • Frontend: TaskForm accepts parentTaskId prop, context-aware dialog title
  • Frontend: ProjectDetail — expand/collapse chevrons, subtask progress bars, indented subtask cards

Phase 7: Settings & Polish

  • Backend: Settings router (get/update settings, change PIN)
  • Frontend: SettingsPage (accent color picker, upcoming range, PIN change)
  • Integration fixes: All frontend API paths match backend routes
  • Integration fixes: HTTP methods (PUT for updates, PATCH for toggle/dismiss)
  • Integration fixes: Type definitions match backend response schemas
  • Integration testing (end-to-end CRUD verification) <-- POST-BUILD
  • Final styling pass (responsive sidebar, empty states, loading skeletons)

Integration Fixes Applied

These were caught during review and fixed:

Issue Fix
Settings field upcoming_days_range Renamed to upcoming_days
CalendarEvent fields start/end Renamed to start_datetime/end_datetime
Reminder field dismissed Renamed to is_dismissed, added is_active
Project status values Changed from active/on_hold to not_started/in_progress/completed
Calendar API path /calendar/events Fixed to /events
All update forms using api.patch Fixed to api.put (backend uses PUT)
Todo toggle using generic PATCH Fixed to api.patch('/todos/{id}/toggle')
Reminder dismiss using generic PATCH Fixed to api.patch('/reminders/{id}/dismiss')
Settings update using PATCH Fixed to api.put
PIN change path /auth/change-pin Fixed to /settings/pin
Dashboard data shape mismatch Aligned TodoWidget, CalendarWidget with actual API response
Missing UpcomingWidget in dashboard Added with /api/upcoming fetch

Post-Build Fixes Applied

These were found during first Docker build and integration testing:

Issue Fix
Person.relationship column shadowed SQLAlchemy's relationship() function Aliased import to sa_relationship in person.py
Missing backend/app/__init__.py Created empty __init__.py for Python package recognition
Backend port 8000 not exposed in docker-compose.yaml Added ports, healthcheck, and condition: service_healthy
get_db() redundant session.close() inside async with Removed finally: await session.close()
datetime.utcnow() deprecated in Python 3.12 Replaced with datetime.now(timezone.utc) in todos.py
Calendar date selection wiped start/end fields Added formatInitialDate() to convert date-only to datetime-local format
Dashboard "today's events" used server UTC time Added client_date query param so frontend sends its local date
Calendar drag-and-drop didn't persist Added eventDrop handler with backend PUT call
Timed event drag-and-drop sent timezone-aware datetimes to naive DB column Used toLocalDatetime() helper instead of .toISOString()
All-day event dates empty when editing (datetime vs date format mismatch) Added formatForInput() to normalize values for date vs datetime-local inputs
Project create/update MissingGreenlet error (lazy load in async context) Re-fetch with selectinload(Project.tasks) after commit in projects.py
Generic error toasts gave no useful information Added getErrorMessage() helper to api.ts, updated all 8 form components
Sidebar not responsive on mobile Split into desktop sidebar + mobile overlay with hamburger menu in AppLayout
Plain "Loading..." text on all pages Created Skeleton, ListSkeleton, GridSkeleton, DashboardSkeleton components
Basic empty states with no visual cue Created EmptyState component with icon, message, and action button across all pages
No logout button Added logout button to sidebar footer with LogOut icon and destructive hover style
Todo category filter was case sensitive Changed to case-insensitive comparison with .toLowerCase()
Dialog/popup forms too narrow and cramped Widened DialogContent from max-w-lg to max-w-xl with mobile margin

Code Review Findings — Round 1 (Senior Review)

Critical:

  • C1: CORS allow_origins=["*"] with allow_credentials=True — already restricted to ["http://localhost:5173"] (main.py)
  • C2: datetime.now(timezone.utc) in naive column — changed to datetime.now() (todos.py)
  • C3: Session cookie missing secure flag — added secure=True + _set_session_cookie helper (auth.py)
  • C4: No PIN length validation on backend — added field_validator for min 4 chars (schemas/settings.py)

High:

  • H1: No brute-force protection on login — added in-memory rate limiting (5 attempts / 5 min) (auth.py)
  • H2: echo=True on SQLAlchemy engine — set to False (database.py)
  • H3: Double commit pattern — removed auto-commit from get_db, routers handle commits (database.py)
  • H4: Person.relationship column shadows SQLAlchemy name — deferred (requires migration + schema changes across stack)
  • H5: Upcoming events missing lower bound filter — added >= today_start (dashboard.py)
  • H6: ReminderForm.tsx doesn't slice remind_at — added .slice(0, 16) for datetime-local input

Medium:

  • M1: Default SECRET_KEY is predictable — added stderr warning on startup (config.py)
  • M3: create_all in lifespan conflicts with Alembic — removed (main.py)
  • M6: No confirmation dialog before destructive actions — added window.confirm() on all delete buttons
  • M7: Authenticated users can still navigate to /login — added Navigate redirect in LockScreen.tsx
  • L1: Error handling in LockScreen used error: any — replaced with getErrorMessage helper

Code Review Findings — Round 2 (Senior Review)

Critical:

  • C1: Default SECRET_KEY only warns, doesn't block production — added env-aware fail-fast (config.py)
  • C2: secure=True cookie breaks HTTP development — made configurable via COOKIE_SECURE setting (auth.py, config.py)
  • C3: No enum validation on status/priority fields — added Literal types (schemas/project_task.py, todo.py, project.py)
  • C4: Race condition in PIN setup (TOCTOU) — added select().with_for_update() (auth.py)

High:

  • H1: Rate limiter memory leak — added stale key cleanup, del empty entries (auth.py)
  • H2: Dashboard runs 7 sequential DB queries — deferred (asyncpg single-session limitation)
  • H3: Subtask eager loading fragile at 2 levels — accepted (business logic enforces single nesting)
  • H4: No withCredentials on Axios for Vite dev — added to api.ts
  • H5: Logout doesn't invalidate session server-side — added in-memory _revoked_sessions set (auth.py)

Medium:

  • M1: TodosPage fetches all then filters client-side — deferred (acceptable for personal app scale)
  • M2: Dashboard uses .toISOString() violating CLAUDE.md rules — replaced with local date formatter (DashboardPage.tsx)
  • M3: No CSP header in nginx — added CSP + Referrer-Policy, removed deprecated X-XSS-Protection (nginx.conf)
  • M4: Event date filtering misses range-spanning events — fixed range overlap logic (events.py)
  • M5: accent_color accepts arbitrary strings — added Literal validation for allowed colors (schemas/settings.py)
  • M6: Logout delete_cookie doesn't match set_cookie attributes — matched all cookie params (auth.py)
  • M7: bcrypt silently truncates PIN at 72 bytes — added max 72 char validation (schemas/settings.py)

Low:

  • L1: as any type casts in frontend forms — replaced with proper Type['field'] casts (TaskForm.tsx, ProjectForm.tsx)
  • L2: Unused imports in events.py — false positive, all imports are used
  • L3: Upcoming endpoint mixes date/datetime string sorting — deferred (works correctly for ISO format)
  • L4: Backend port 8000 exposed directly, bypassing nginx — deferred (useful for dev)
  • L5: parseInt(id!) without NaN validation — deferred (low risk, route-level protection)
  • L6: X-XSS-Protection header is deprecated — removed, replaced with CSP (nginx.conf)
  • L7: Missing Referrer-Policy header — added strict-origin-when-cross-origin (nginx.conf)

Outstanding Items (Resume Here If Halted)

Critical (blocks deployment):

  1. Docker build verification - DONE: All 3 services boot successfully
  2. npm install verification - DONE: Frontend packages install correctly

Important (blocks functionality):

  1. Alembic migration test - DONE: Tables create correctly on first boot
  2. Auth flow test - DONE: PIN setup works, PIN change works, session persistence works, logout added
  3. End-to-end CRUD test - DONE: All features verified — Calendar, Projects, Todos (create/edit/filter/search), People (CRUD + search), Locations (CRUD + filter), Reminders (CRUD + dismiss), Settings (accent color + PIN)

Nice to have (polish):

  1. Responsive sidebar - DONE: Mobile hamburger menu with overlay, desktop collapse/expand
  2. Toast notifications - DONE: All forms now show meaningful error messages via getErrorMessage()
  3. Empty states - DONE: All pages show icon + message + action button when empty
  4. Loading skeletons - DONE: Animated skeleton placeholders replace plain "Loading..." text

File Inventory (100+ files total)

Backend (40 files)

backend/
├── Dockerfile
├── requirements.txt
├── start.sh
├── .gitignore
├── alembic.ini
├── alembic/
│   ├── env.py
│   ├── script.py.mako
│   └── versions/
│       ├── 001_initial_migration.py
│       └── 002_add_subtasks.py
└── app/
    ├── main.py
    ├── config.py
    ├── database.py
    ├── models/
    │   ├── __init__.py
    │   ├── settings.py
    │   ├── todo.py
    │   ├── calendar_event.py
    │   ├── reminder.py
    │   ├── project.py
    │   ├── project_task.py
    │   ├── person.py
    │   └── location.py
    ├── schemas/
    │   ├── __init__.py
    │   ├── settings.py
    │   ├── todo.py
    │   ├── calendar_event.py
    │   ├── reminder.py
    │   ├── project.py
    │   ├── project_task.py
    │   ├── person.py
    │   └── location.py
    └── routers/
        ├── __init__.py
        ├── auth.py
        ├── dashboard.py
        ├── todos.py
        ├── events.py
        ├── reminders.py
        ├── projects.py
        ├── people.py
        ├── locations.py
        └── settings.py

Frontend (60+ files)

frontend/
├── Dockerfile
├── nginx.conf
├── package.json
├── vite.config.ts
├── tsconfig.json
├── tsconfig.node.json
├── postcss.config.js
├── tailwind.config.ts
├── components.json
├── index.html
├── .gitignore
└── src/
    ├── main.tsx
    ├── App.tsx
    ├── index.css (includes FullCalendar dark overrides)
    ├── lib/
    │   ├── utils.ts
    │   └── api.ts
    ├── hooks/
    │   ├── useAuth.ts
    │   ├── useSettings.ts
    │   └── useTheme.ts
    ├── types/
    │   └── index.ts
    └── components/
        ├── ui/ (12 components)
        ├── layout/ (AppLayout, Sidebar)
        ├── auth/ (LockScreen)
        ├── dashboard/ (DashboardPage, StatsWidget, UpcomingWidget, TodoWidget, CalendarWidget, ProjectsWidget)
        ├── todos/ (TodosPage, TodoList, TodoItem, TodoForm)
        ├── calendar/ (CalendarPage, EventForm)
        ├── reminders/ (RemindersPage, ReminderList, ReminderForm)
        ├── projects/ (ProjectsPage, ProjectCard, ProjectDetail, ProjectForm, TaskForm)
        ├── people/ (PeoplePage, PersonCard, PersonForm)
        ├── locations/ (LocationsPage, LocationCard, LocationForm)
        └── settings/ (SettingsPage)

Root

docker-compose.yaml
.env / .env.example
.gitignore
progress.md

How to Resume Development

If development is halted, pick up from the Outstanding Items section above:

  1. Check which items are still marked incomplete in the Milestone Tracker
  2. Address Critical items first (Docker build verification)
  3. Then Important items (auth flow, CRUD testing)
  4. Finally Polish items (responsive, loading states)

Quick Start Commands

# Build and run everything
docker-compose up --build

# Rebuild just backend after changes
docker-compose up --build backend

# Rebuild just frontend after changes
docker-compose up --build frontend

# View logs
docker-compose logs -f

# View specific service logs
docker-compose logs -f backend
docker-compose logs -f frontend

# Reset database (destructive)
docker-compose down -v && docker-compose up --build

# Stop everything
docker-compose down

First-Time Setup

  1. Run docker-compose up --build
  2. Navigate to http://localhost
  3. You'll see the PIN setup screen (first run)
  4. Create a PIN and you'll be redirected to the dashboard
  5. Use the sidebar to navigate between features