From 87708ae195a97084265951bad753f2186db814a9 Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Mon, 23 Feb 2026 03:13:51 +0800 Subject: [PATCH] Remove .claude directory from git tracking Already in .gitignore but files were tracked from before. Local files preserved, just untracked from the repository. Co-Authored-By: Claude Opus 4.6 --- .claude/QA/weather_coordinates_review.md | 281 ----------------- .claude/context/progress.md | 384 ----------------------- .claude/projects/ui_refresh.md | 251 --------------- 3 files changed, 916 deletions(-) delete mode 100644 .claude/QA/weather_coordinates_review.md delete mode 100644 .claude/context/progress.md delete mode 100644 .claude/projects/ui_refresh.md diff --git a/.claude/QA/weather_coordinates_review.md b/.claude/QA/weather_coordinates_review.md deleted file mode 100644 index 01c39dc..0000000 --- a/.claude/QA/weather_coordinates_review.md +++ /dev/null @@ -1,281 +0,0 @@ -# Weather Coordinate-Based Lookup - Code Review - -**Feature:** Replace plain-text weather city input with geocoding search + coordinate-based weather fetch -**Reviewer:** Senior Code Reviewer -**Date:** 2026-02-21 -**Files reviewed:** 6 - ---- - -## Summary - -This feature replaces the free-text weather city input with a search-and-select location picker backed by OpenWeatherMap's geocoding API. Settings now store `weather_lat` and `weather_lon` alongside `weather_city`, and the weather endpoint prefers coordinates over city name for more accurate results. The implementation is solid overall -- the backend correctly uses `run_in_executor` for blocking I/O (addressing CRIT-2 from the Phase 2 review), the cache now keys on coordinates, and the frontend location picker is well-crafted with debounced search, click-outside dismissal, and proper cache invalidation. However, there are a few issues around input validation, deprecated API usage, and a previous review finding (WARN-1) that was only partially addressed. - ---- - -## Critical Issues - -### [CRIT-1] No validation on latitude/longitude values in schema - -**File:** `backend/app/schemas/settings.py` (lines 30-31) - -```python -weather_lat: float | None = None -weather_lon: float | None = None -``` - -Latitude must be in [-90, 90] and longitude in [-180, 180]. The schema accepts any float, meaning a malformed or malicious request could send `weather_lat: 99999` which would be persisted to the database and forwarded to the OpenWeatherMap API. While the API would reject it, the invalid data would be stored permanently. - -**Fix:** Add field validators: -```python -from pydantic import field_validator - -class SettingsUpdate(BaseModel): - # ... existing fields ... - weather_lat: float | None = None - weather_lon: float | None = None - - @field_validator('weather_lat') - @classmethod - def validate_lat(cls, v: float | None) -> float | None: - if v is not None and (v < -90 or v > 90): - raise ValueError('Latitude must be between -90 and 90') - return v - - @field_validator('weather_lon') - @classmethod - def validate_lon(cls, v: float | None) -> float | None: - if v is not None and (v < -180 or v > 180): - raise ValueError('Longitude must be between -180 and 180') - return v -``` - ---- - -## Warnings - -### [WARN-1] `asyncio.get_event_loop()` is deprecated in Python 3.12 - -**File:** `backend/app/routers/weather.py` (lines 36, 85) - -```python -loop = asyncio.get_event_loop() -results = await loop.run_in_executor(None, _fetch_json, url) -``` - -`asyncio.get_event_loop()` emits a deprecation warning in Python 3.10+ when called outside of an async context, and its behavior is unreliable. Inside an async function the running loop is available, but the idiomatic replacement is `asyncio.get_running_loop()`: - -```python -loop = asyncio.get_running_loop() -``` - -This is a simple 1-word change on two lines and future-proofs the code. - -### [WARN-2] Cache invalidation on location change is implicit, not explicit - -**File:** `backend/app/routers/weather.py` (lines 72-78) - -```python -cache_key = f"{lat},{lon}" if use_coords else city -if _cache.get("expires_at") and now < _cache["expires_at"] and _cache.get("cache_key") == cache_key: - return _cache["data"] -``` - -The Phase 2 review flagged WARN-1 (cache not invalidated on city change). This implementation improves it by keying the cache on coordinates/city, so a location change will naturally miss the cache. This is good. However, the old cached data still occupies memory until the next fetch overwrites it -- a minor concern for a single-user app but worth documenting. - -Additionally, using a plain `dict` with string keys is fragile. If `lat` or `lon` is `None` due to a race or unexpected state, the cache key becomes `"None,None"` which could match incorrectly. - -**Recommendation:** Add an explicit check: -```python -if lat is None or lon is None: - cache_key = city -else: - cache_key = f"{lat},{lon}" -``` - -Wait -- this is already handled by the `use_coords` boolean. The current logic is correct. Disregard the race concern; the ternary is safe. - -### [WARN-3] Search endpoint lacks rate limiting - -**File:** `backend/app/routers/weather.py` (lines 26-53) - -The `/search` endpoint calls OpenWeatherMap's geocoding API on every request. The frontend debounces at 300ms, but there's no backend throttle. A user rapidly typing triggers multiple API calls. For a single-user app this is unlikely to hit OWM's rate limits, but the 300ms debounce on the frontend is the only protection. - -**Recommendation:** This is acceptable for a single-user app. No action needed unless OWM rate limit errors appear in production. - -### [WARN-4] Clearing location sends `null` but `SettingsUpdate` uses `Optional` with `exclude_unset` - -**File:** `frontend/src/components/settings/SettingsPage.tsx` (lines 86-98) -**File:** `backend/app/routers/settings.py` (line 29) - -```typescript -await updateSettings({ - weather_city: null, - weather_lat: null, - weather_lon: null, -}); -``` - -The backend uses `model_dump(exclude_unset=True)`, so explicitly sending `null` values will include them in the update dict -- which correctly sets the DB columns to `NULL`. This works as intended. However, this relies on the frontend correctly sending `null` rather than omitting the keys. If `updateSettings` ever strips `null` values before sending, the clear operation would silently fail. - -**Recommendation:** This works correctly today. Add a comment in `handleLocationClear` noting that explicit `null` is required for the backend to clear the fields. - -### [WARN-5] `Float` column precision for coordinates - -**File:** `backend/app/models/settings.py` (lines 16-17) - -```python -weather_lat: Mapped[float | None] = mapped_column(Float, nullable=True, default=None) -weather_lon: Mapped[float | None] = mapped_column(Float, nullable=True, default=None) -``` - -SQLAlchemy's `Float` maps to PostgreSQL `DOUBLE PRECISION` (8 bytes, ~15 digits). For geographic coordinates this is more than adequate (~1mm precision). No issue here -- just documenting that the type choice is appropriate. - ---- - -## Suggestions - -### [SUG-1] Weather search response should use a Pydantic model - -**File:** `backend/app/routers/weather.py` (lines 26-49) - -The `/search` endpoint returns a raw list of dicts. Adding a response model would document the API and catch shape issues: - -```python -from pydantic import BaseModel - -class GeoSearchResult(BaseModel): - name: str - state: str - country: str - lat: float - lon: float - -@router.get("/search", response_model=list[GeoSearchResult]) -``` - -This mirrors the Phase 2 review's SUG-7 recommendation for the main weather endpoint. - -### [SUG-2] Dashboard weather query should check coordinates, not just city - -**File:** `frontend/src/components/dashboard/DashboardPage.tsx` (line 77) - -```typescript -enabled: !!settings?.weather_city, -``` - -The backend now supports coordinate-based lookup without a city name (line 68-69 of weather.py checks both). The frontend enable condition should match: - -```typescript -enabled: !!(settings?.weather_city || (settings?.weather_lat != null && settings?.weather_lon != null)), -``` - -In practice, the current `handleLocationSelect` always sets all three (`weather_city`, `weather_lat`, `weather_lon`) together, so this is unlikely to diverge. But for correctness, the enable condition should match what the backend accepts. - -### [SUG-3] Debounce cleanup on unmount - -**File:** `frontend/src/components/settings/SettingsPage.tsx` (lines 34, 64-65) - -```typescript -const debounceRef = useRef>(); -// ... -if (debounceRef.current) clearTimeout(debounceRef.current); -debounceRef.current = setTimeout(() => searchLocations(value), 300); -``` - -If the component unmounts while a debounce timer is pending, the callback fires on an unmounted component, potentially causing a React state update warning. Add cleanup: - -```typescript -useEffect(() => { - return () => { - if (debounceRef.current) clearTimeout(debounceRef.current); - }; -}, []); -``` - -### [SUG-4] Search endpoint geocoding URL uses HTTP, not HTTPS - -**File:** `backend/app/routers/weather.py` (line 37) - -```python -url = f"http://api.openweathermap.org/geo/1.0/direct?q={urllib.parse.quote(q)}&limit=5&appid={api_key}" -``` - -The geocoding endpoint uses `http://` while the weather endpoints (lines 93-94) use `https://`. The API key is sent in the query string over an unencrypted connection. This means the API key could be intercepted in transit. - -**Fix:** Change to `https://`: -```python -url = f"https://api.openweathermap.org/geo/1.0/direct?q={urllib.parse.quote(q)}&limit=5&appid={api_key}" -``` - -### [SUG-5] `weather_city` display could be null when coordinates exist - -**File:** `frontend/src/components/settings/SettingsPage.tsx` (line 275) - -```tsx -{settings?.weather_city} -``` - -If somehow `weather_lat` and `weather_lon` are set but `weather_city` is null (e.g., direct API call), the location badge would display empty text. Add a fallback: - -```tsx -{settings?.weather_city || `${settings?.weather_lat}, ${settings?.weather_lon}`} -``` - -### [SUG-6] Consider parallel fetch for weather current + forecast - -**File:** `backend/app/routers/weather.py` (lines 96-99) - -```python -current_data, forecast_data = await asyncio.gather( - loop.run_in_executor(None, _fetch_json, current_url), - loop.run_in_executor(None, _fetch_json, forecast_url), -) -``` - -This is already using `asyncio.gather` for parallel execution -- great improvement from the Phase 2 sequential pattern. No action needed. - ---- - -## Positive Observations - -1. **CRIT-2 from Phase 2 is fully resolved.** Both the `/search` and `/` endpoints now use `run_in_executor` for blocking `urllib` calls, and the main weather endpoint uses `asyncio.gather` to fetch current + forecast in parallel. This is a significant improvement. - -2. **Cache keying is well-designed.** The `cache_key` switches between `"{lat},{lon}"` and `city` based on what's available, correctly invalidating when the user switches locations without needing an explicit cache clear. - -3. **Frontend UX is polished.** The search-and-select pattern with debounced input, loading spinner, click-outside dismissal, and clear button is a substantial UX improvement over a plain text input. The dropdown key uses `${loc.lat}-${loc.lon}-${i}` which handles duplicate city names correctly. - -4. **Settings update is atomic.** `handleLocationSelect` sends `weather_city`, `weather_lat`, and `weather_lon` in a single `updateSettings` call, preventing partial state where coordinates exist without a city name. - -5. **Migration is clean and reversible.** The Alembic migration correctly adds nullable float columns with a straightforward downgrade path. - -6. **Type definitions stay in sync.** The `GeoLocation` interface and `Settings` type updates accurately mirror the backend schema changes. - -7. **Error handling in the frontend is user-friendly.** Both `handleLocationSelect` and `handleLocationClear` catch errors and show toast notifications, and the search function silently clears results on failure rather than showing error UI. - ---- - -## Summary of Action Items - -| Priority | ID | Issue | Effort | -|----------|------|-------|--------| -| CRITICAL | CRIT-1 | No lat/lon validation in Pydantic schema | 15 lines | -| WARNING | WARN-1 | Deprecated `get_event_loop()` - use `get_running_loop()` | 2 lines | -| SUGGESTION | SUG-4 | Geocoding URL uses HTTP, leaks API key in transit | 1 line | -| SUGGESTION | SUG-1 | No response_model on search endpoint | 10 lines | -| SUGGESTION | SUG-2 | Dashboard enables weather query on city only, not coords | 1 line | -| SUGGESTION | SUG-3 | Debounce timer not cleaned up on unmount | 4 lines | -| SUGGESTION | SUG-5 | Location badge has no fallback for null city name | 1 line | - ---- - -## Previous Review Items Status - -| Phase 2 ID | Status | Notes | -|------------|--------|-------| -| CRIT-1 (double path prefix) | RESOLVED | Weather router now uses `@router.get("/")` | -| CRIT-2 (blocking urllib) | RESOLVED | Uses `run_in_executor` + `asyncio.gather` | -| WARN-1 (cache invalidation) | RESOLVED | Cache keys on coordinates/city | -| WARN-2 (API key in errors) | RESOLVED | Error messages now use generic strings | -| WARN-5 (stale settings state) | NOT ADDRESSED | Still present for other settings fields | -| SUG-7 (no response_model) | NOT ADDRESSED | Still no Pydantic model for weather/search responses | diff --git a/.claude/context/progress.md b/.claude/context/progress.md deleted file mode 100644 index ce62f3e..0000000 --- a/.claude/context/progress.md +++ /dev/null @@ -1,384 +0,0 @@ -# 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 -- [x] `docker-compose.yaml` - 3-service architecture (db, backend, frontend) -- [x] `.env` / `.env.example` - Environment configuration -- [x] `.gitignore` - Root gitignore -- [x] Backend: FastAPI app skeleton (`app/main.py`, `config.py`, `database.py`) -- [x] Backend: `requirements.txt` with all Python dependencies -- [x] Backend: Alembic migration setup (`alembic.ini`, `env.py`, initial migration) -- [x] Backend: `start.sh` startup script -- [x] Backend: `Dockerfile` - Python 3.12-slim -- [x] Frontend: Vite + React scaffold (`package.json`, `vite.config.ts`, `tsconfig.json`) -- [x] Frontend: Tailwind CSS + PostCSS configuration -- [x] Frontend: `index.html` entry point -- [x] Frontend: `nginx.conf` - SPA serving + API proxy -- [x] Frontend: `Dockerfile` - Multi-stage node build + nginx serve -- [x] Verify `docker-compose up --build` boots all services - -### Phase 2: Auth & Layout -- [x] Backend: Settings model + schema -- [x] Backend: Auth router (PIN setup, login, logout, status with bcrypt + itsdangerous) -- [x] Frontend: `LockScreen.tsx` - PIN setup/login UI -- [x] Frontend: `useAuth.ts` hook (TanStack Query) -- [x] Frontend: `AppLayout.tsx` + `Sidebar.tsx` - Main layout with navigation -- [x] Frontend: Accent color theming system (`useTheme.ts`, CSS custom properties) -- [x] Frontend: 12 shadcn/ui components (button, card, input, dialog, select, badge, etc.) - -### Phase 3: Core Features -- [x] Backend: Todo model, schema, router (CRUD + toggle + filters) -- [x] Frontend: `TodosPage`, `TodoList`, `TodoItem`, `TodoForm` -- [x] Backend: Calendar Event model, schema, router (CRUD + date range filter) -- [x] Frontend: `CalendarPage` (FullCalendar integration), `EventForm` -- [x] Frontend: FullCalendar dark theme CSS overrides in `index.css` -- [x] Backend: Reminder model, schema, router (CRUD + dismiss) -- [x] Frontend: `RemindersPage`, `ReminderList`, `ReminderForm` - -### Phase 4: Project Management -- [x] Backend: Project model with tasks relationship + cascade delete -- [x] Backend: ProjectTask model, schema, router (nested CRUD under projects) -- [x] Frontend: `ProjectsPage`, `ProjectCard`, `ProjectDetail`, `ProjectForm`, `TaskForm` - -### Phase 5: People & Locations -- [x] Backend: Person model, schema, router (CRUD + search) -- [x] Frontend: `PeoplePage`, `PersonCard`, `PersonForm` -- [x] Backend: Location model, schema, router (CRUD + category filter) -- [x] Frontend: `LocationsPage`, `LocationCard`, `LocationForm` - -### Phase 6: Dashboard & Upcoming -- [x] Backend: Dashboard aggregation endpoint (stats, events, todos, reminders) -- [x] Backend: Upcoming endpoint (unified items sorted by date) -- [x] Frontend: `DashboardPage` with all widgets + upcoming integration -- [x] Frontend: `StatsWidget` (projects, people, locations counts) -- [x] Frontend: `UpcomingWidget` (unified timeline with type icons) -- [x] Frontend: `TodoWidget` (upcoming todos with priority badges) -- [x] Frontend: `CalendarWidget` (today's events with color indicators) -- [x] Frontend: Active reminders section in dashboard - -### Phase 6b: Project Subtasks -- [x] Backend: Self-referencing `parent_task_id` FK on `project_tasks` with CASCADE delete -- [x] Backend: Alembic migration `002_add_subtasks.py` -- [x] Backend: Schema updates — `parent_task_id` in create, nested `subtasks` in response, `model_rebuild()` -- [x] Backend: Chained `selectinload` for two-level subtask loading, parent validation on create -- [x] Frontend: `ProjectTask` type updated with `parent_task_id` and `subtasks` -- [x] Frontend: `TaskForm` accepts `parentTaskId` prop, context-aware dialog title -- [x] Frontend: `ProjectDetail` — expand/collapse chevrons, subtask progress bars, indented subtask cards - -### Phase 7: Settings & Polish -- [x] Backend: Settings router (get/update settings, change PIN) -- [x] Frontend: `SettingsPage` (accent color picker, upcoming range, PIN change) -- [x] Integration fixes: All frontend API paths match backend routes -- [x] Integration fixes: HTTP methods (PUT for updates, PATCH for toggle/dismiss) -- [x] Integration fixes: Type definitions match backend response schemas -- [x] Integration testing (end-to-end CRUD verification) <-- POST-BUILD -- [x] Final styling pass (responsive sidebar, empty states, loading skeletons) - -### Phase 8: UI Refresh — Stages 1-2 (Dashboard & Global Shell) -- [x] Dashboard overhaul: contextual greeting, week timeline, stat cards, upcoming widget, weather -- [x] Typography: Sora headings + DM Sans body -- [x] Sidebar refinement: accent hover states, border-left active indicator, collapse animation -- [x] Custom scrollbar styling, staggered animations, skeleton loading states -- [x] Stylesheet (`stylesheet.md`) established as design system reference - -### Phase 9: Calendar Redesign & Improvements -- [x] Multi-calendar backend with virtual birthday events from People -- [x] Calendar sidebar with visibility toggles and color indicators -- [x] Sheet component for slide-in form panels (replaced Dialog for all 4 forms) -- [x] All-day event fixes: slim CSS bars, exclusive end-date offset handling -- [x] Materialized recurring events: backend service, recurrence UI, scope dialog -- [x] LocationPicker with OSM Nominatim + local DB search (EventForm + LocationForm) -- [x] First day of week setting (Sunday/Monday) with FullCalendar sync -- [x] Dashboard parent template exclusion, DayBriefing null guards + night logic -- [x] Recurrence crash fixes: null stripping, _rule_int helper, first-occurrence generation - -### Phase 10: UI Refresh — Stage 3+ (Remaining Pages) -- [ ] Stage 4: CRUD pages (Todos, Reminders, Projects) — refined filters, cards, empty states -- [ ] Stage 5: Entity pages (People, Locations) — avatar placeholders, consistent badges -- [ ] Stage 6: Settings & Login — full-width layout, UMBRA branding -- [ ] Stage 7: Final polish — consistency audit, animation review, accessibility pass - ---- - -## 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: -- [x] C1: CORS `allow_origins=["*"]` with `allow_credentials=True` — already restricted to `["http://localhost:5173"]` (`main.py`) -- [x] C2: `datetime.now(timezone.utc)` in naive column — changed to `datetime.now()` (`todos.py`) -- [x] C3: Session cookie missing `secure` flag — added `secure=True` + `_set_session_cookie` helper (`auth.py`) -- [x] C4: No PIN length validation on backend — added `field_validator` for min 4 chars (`schemas/settings.py`) - -### High: -- [x] H1: No brute-force protection on login — added in-memory rate limiting (5 attempts / 5 min) (`auth.py`) -- [x] H2: `echo=True` on SQLAlchemy engine — set to `False` (`database.py`) -- [x] 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) -- [x] H5: Upcoming events missing lower bound filter — added `>= today_start` (`dashboard.py`) -- [x] H6: `ReminderForm.tsx` doesn't slice `remind_at` — added `.slice(0, 16)` for datetime-local input - -### Medium: -- [x] M1: Default `SECRET_KEY` is predictable — added stderr warning on startup (`config.py`) -- [x] M3: `create_all` in lifespan conflicts with Alembic — removed (`main.py`) -- [x] M6: No confirmation dialog before destructive actions — added `window.confirm()` on all delete buttons -- [x] M7: Authenticated users can still navigate to `/login` — added `Navigate` redirect in `LockScreen.tsx` -- [x] L1: Error handling in LockScreen used `error: any` — replaced with `getErrorMessage` helper - -## Code Review Findings — Round 2 (Senior Review) - -### Critical: -- [x] C1: Default SECRET_KEY only warns, doesn't block production — added env-aware fail-fast (`config.py`) -- [x] C2: `secure=True` cookie breaks HTTP development — made configurable via `COOKIE_SECURE` setting (`auth.py`, `config.py`) -- [x] C3: No enum validation on status/priority fields — added `Literal` types (`schemas/project_task.py`, `todo.py`, `project.py`) -- [x] C4: Race condition in PIN setup (TOCTOU) — added `select().with_for_update()` (`auth.py`) - -### High: -- [x] 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) -- [x] H4: No `withCredentials` on Axios for Vite dev — added to `api.ts` -- [x] 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) -- [x] M2: Dashboard uses `.toISOString()` violating CLAUDE.md rules — replaced with local date formatter (`DashboardPage.tsx`) -- [x] M3: No CSP header in nginx — added CSP + Referrer-Policy, removed deprecated X-XSS-Protection (`nginx.conf`) -- [x] M4: Event date filtering misses range-spanning events — fixed range overlap logic (`events.py`) -- [x] M5: `accent_color` accepts arbitrary strings — added `Literal` validation for allowed colors (`schemas/settings.py`) -- [x] M6: Logout `delete_cookie` doesn't match `set_cookie` attributes — matched all cookie params (`auth.py`) -- [x] M7: bcrypt silently truncates PIN at 72 bytes — added max 72 char validation (`schemas/settings.py`) - -### Low: -- [x] L1: `as any` type casts in frontend forms — replaced with proper `Type['field']` casts (`TaskForm.tsx`, `ProjectForm.tsx`) -- [x] 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) -- [x] L6: `X-XSS-Protection` header is deprecated — removed, replaced with CSP (`nginx.conf`) -- [x] 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): -3. ~~**Alembic migration test**~~ - DONE: Tables create correctly on first boot -4. ~~**Auth flow test**~~ - DONE: PIN setup works, PIN change works, session persistence works, logout added -5. ~~**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): -6. ~~**Responsive sidebar**~~ - DONE: Mobile hamburger menu with overlay, desktop collapse/expand -7. ~~**Toast notifications**~~ - DONE: All forms now show meaningful error messages via `getErrorMessage()` -8. ~~**Empty states**~~ - DONE: All pages show icon + message + action button when empty -9. ~~**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 -│ ├── 003_add_location_to_events.py -│ ├── 004_add_starred_field.py -│ ├── 005_add_weather_fields.py -│ ├── 006_add_calendars.py -│ ├── 007_add_recurrence_fields.py -│ └── 008_add_first_day_of_week.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 - │ └── calendar.py - ├── services/ - │ └── recurrence.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/ (14 components: + sheet, location-picker) - ├── layout/ (AppLayout, Sidebar) - ├── auth/ (LockScreen) - ├── dashboard/ (DashboardPage, StatsWidget, UpcomingWidget, TodoWidget, CalendarWidget, ProjectsWidget) - ├── todos/ (TodosPage, TodoList, TodoItem, TodoForm) - ├── calendar/ (CalendarPage, CalendarSidebar, 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 -```bash -# 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 diff --git a/.claude/projects/ui_refresh.md b/.claude/projects/ui_refresh.md deleted file mode 100644 index 4739d50..0000000 --- a/.claude/projects/ui_refresh.md +++ /dev/null @@ -1,251 +0,0 @@ -# UMBRA UI Refresh — Planning Document - -## Current UI Audit (2026-02-20) - -### Global Observations -- **Theme:** Dark background (~#0a0a0a), dark card surfaces (~#1a1a2e), blue/cyan accent -- **Sidebar:** Fixed left ~200px, solid blue highlight on active item, Lucide icons + labels, collapse chevron -- **Typography:** System sans-serif, white headings, muted gray subtext — functional but lacks personality -- **Cards:** Rounded borders with subtle gray stroke, no shadows, no hover effects, no transitions -- **Layout:** Full-width content area with no max-width constraint — cards stretch awkwardly on wide displays -- **Action buttons:** Consistent "+ Add X" blue filled buttons top-right on every page — good pattern -- **Empty states:** Centered icon + message — functional but lifeless -- **Scrollbars:** Default browser scrollbars — clash with dark theme - -### Page-by-Page Notes - -#### Login -- Centered dark card, lock icon, PIN input, blue "Unlock" button -- Clean and minimal — works well structurally -- Missing UMBRA branding/identity — feels like a generic auth screen -- No ambient atmosphere (no gradients, no glow, no texture) - -#### Dashboard -- 4 stat cards across top (Total Projects, In Progress, People, Locations) with colored Lucide icons -- "Upcoming (7 days)" section — flat list of events with "Event" badge right-aligned -- Bottom row: "Upcoming Todos" + "Today's Events" side by side -- **Issues:** - - Stat cards are flat rectangles with label/number/icon — no visual weight or hierarchy - - Icon colors (purple, purple, green, orange) feel arbitrary — no semantic meaning - - Upcoming list wastes horizontal space — event name far left, badge far right, nothing in between - - Bottom widgets have massive empty space when few/no items - - No week summary or schedule preview — missed opportunity for at-a-glance context - - "Welcome back. Here's your overview." is generic placeholder text - - No time-of-day awareness or contextual greeting - -#### Todos -- Search bar + "All Priorities" dropdown + category filter + "Show completed" checkbox -- All controls on single line — functional but cramped and visually flat -- Empty state: centered checkbox icon + message -- **Issues:** Filter controls are unstyled native elements mixed with custom — inconsistent - -#### Calendar (Month) -- FullCalendar month view with colored event dots/pills -- Color coding: blue (default), purple, green (multi-day), orange, yellow -- **Issues:** - - Prev/next navigation arrows are tiny unstyled squares (~16px) — nearly invisible - - "today" button is plain text with no visual emphasis - - Event text truncates on small cells with no tooltip or expand affordance - - Multi-day events (green/orange bars) visually dominate and obscure day cells - - No visual distinction for today's date in month grid - -#### Calendar (Week) -- Time grid with events as colored blocks — time + title visible -- Today column has yellowish-brown tint overlay -- **Issues:** - - The "today" tint is murky (#8B8000-ish overlay) — looks dirty rather than highlighted - - Past-time columns also have this tint, making it confusing - - No current-time indicator line - -#### Calendar (Day) -- Full-day column with same murky brown-yellow background -- **Issues:** When empty, entire content area is just a brown column — unappealing - - No visual distinction between past hours and future hours - -#### Reminders -- Tab bar: Active / Dismissed / All — clean blue active state -- Empty state similar to Todos -- Very sparse — large empty space below tabs - -#### Projects -- 3-column card grid -- Each card: ALL-CAPS name, status badge pill, "Progress" label with thin bar, task count, calendar icon + due date -- **Issues:** - - ALL-CAPS names are aggressive and hard to scan - - Status badges ("not started" / "in progress") are small outlined pills — low contrast - - Progress bars are extremely thin (~2px) and low-contrast against dark surface - - "Progress" label + "0/3 tasks" on same line wastes vertical space - - Cards have uneven internal spacing — description shows on some, not others - - No hover state or click affordance — cards don't feel interactive - -#### People -- 3-column card grid -- Each card: name, category badge (Friends = blue, Work = gray), birthday, edit/delete icons top-right -- **Issues:** - - Cards feel sparse — just 3 lines of info in a large rectangle - - No avatar placeholder — missed opportunity for visual anchoring - - Edit (pencil) and delete (trash) icons are small and far from the name - - Category badge colors don't match Locations page (Work = gray here, purple there) - -#### Locations -- 3-column card grid with location pin icon beside name -- Category badges: home = blue, work = purple -- Address shown as subtitle -- **Issues:** Badge color system inconsistent with People page - -#### Settings -- Three stacked sections: Appearance, Dashboard, Security -- Left-aligned at ~50% width, entire right half is empty -- Accent color picker: 5 colored circles (cyan, blue, purple, orange, green) -- **Issues:** - - Massive dead space on right — feels incomplete - - Color circles have no labels or tooltips - - Sections could use visual separation beyond just spacing - ---- - -## Design Critique (Frontend Design Review) - -### 1. Surface & Depth — "Everything lives on the same plane" -The biggest visual issue is that every element — cards, sidebar, content area, stat widgets — exists at the same depth. There are no shadows, no layering, no elevation changes. Modern dark UIs use subtle elevation to create hierarchy: -- Cards should float slightly above the background with soft box-shadows (`0 1px 3px rgba(0,0,0,0.3)`) -- The sidebar should feel like a separate layer with a subtle border or shadow on its right edge -- Hover states should lift cards slightly (`translateY(-1px)` + shadow increase) -- Active/selected states should have a gentle inner glow or border luminance shift - -### 2. Typography — "Functional but forgettable" -The current type system is plain system sans-serif with two weights (bold headings, normal body). For a personal life manager named "UMBRA," typography should reinforce brand identity: -- Consider a distinctive heading font (geometric sans like Outfit, Sora, or General Sans) -- Body text needs slightly more line-height for readability on dark backgrounds (1.6 minimum) -- The ALL-CAPS project names are too aggressive — use sentence case with semibold weight instead -- Stat card numbers should be larger and use tabular figures for alignment -- Muted text (#6b7280-range) is too low-contrast on dark backgrounds — bump to #9ca3af minimum - -### 3. Color System — "Arbitrary accents without semantic meaning" -Colors are applied without a clear system: -- Stat card icons use purple/purple/green/orange — why purple twice? No semantic connection -- Category badges are inconsistent: Work is gray on People, purple on Locations -- The calendar today overlay (#8B8000-ish) clashes with the cool-toned theme -- **Recommendation:** Establish a semantic color palette: - - Accent (cyan/blue): interactive elements, active states, links - - Success (emerald): completed, on-track - - Warning (amber): upcoming, due soon - - Danger (rose): overdue, delete - - Neutral (slate): secondary text, borders, disabled - - Category colors should be consistent app-wide (same color for "Work" everywhere) - -### 4. Spacing & Density — "Too much air, not enough content" -- Dashboard stat cards have generous internal padding but minimal content — lots of dead space -- The Upcoming list is a single-column spanning the full width with text far-left and badge far-right -- Settings page uses only half the viewport width -- Cards on People/Locations show 3 lines of info in ~150px tall containers -- **Recommendation:** Use a tighter grid system. Dashboard should use a 2-column or asymmetric layout below stats. Cards should breathe but not float in empty space. Consider max-width containers (1200-1400px). - -### 5. Interactivity — "Static pages, not a living app" -Zero motion, zero feedback: -- No hover states on any cards or interactive elements -- No transitions on page changes -- No loading states or skeleton screens -- No micro-interactions (checkbox toggle, card expand, badge pulse) -- Scrollbars are default browser chrome — jarring in dark theme -- **Recommendation:** Add CSS transitions on all interactive elements (150-200ms ease). Custom scrollbar styling with `scrollbar-color` and `::-webkit-scrollbar`. Subtle scale/shadow on card hover. - -### 6. Dashboard Specific — "A bulletin board, not a command center" -The dashboard is the first thing you see and it should feel alive: -- Missing: week-at-a-glance schedule/timeline -- Missing: contextual greeting (time of day + name) -- Missing: quick actions or shortcuts -- Stat cards don't link context — "5 Projects" but which ones matter right now? -- Bottom widgets (Upcoming Todos / Today's Events) are equal-width but Today's Events is more urgent -- **Recommendation:** Redesign dashboard as a "morning briefing" — today's schedule, this week's priorities, active project progress, quick-add shortcuts - -### 7. Component Quality — "Default HTML with Tailwind" -Several components feel like unstyled defaults: -- Calendar nav arrows are default FullCalendar squares — need custom styled buttons -- Filter dropdowns on Todos look like native `