# CLAUDE.md ## Project Identity Life OS is a single-user personal productivity web app. Server-rendered monolith. No SPA, no frontend framework, no build pipeline. You are working on the codebase directly on the production server via mounted Docker volume. **Live URL:** https://lifeos-dev.invixiom.com **Server:** defiant-01, 46.225.166.142, Ubuntu 24.04 **Repo:** github.com/mdombaugh/lifeos-dev (main branch) --- ## Tech Stack - Do Not Deviate - **Python 3.12 / FastAPI** (async) / **SQLAlchemy 2.0 async** with raw SQL via `text()` - **NO ORM models** - **PostgreSQL 16** in Docker container `lifeos-db`, full-text search via tsvector/tsquery - **Jinja2** server-rendered HTML templates - **Vanilla CSS/JS** only. No npm. No React. No Tailwind. No build tools. - **asyncpg** driver - CSS custom properties for dark/light theme (`[data-theme='dark']` / `[data-theme='light']`) **Hard rules:** - No fetch/XHR from JavaScript. Forms use standard HTML POST with 303 redirect (PRG pattern). - JavaScript handles ONLY UI state: sidebar collapse, search modal, timer display, theme toggle, drag-to-reorder. - All data operations go through BaseRepository or raw SQL. Never introduce an ORM model layer. --- ## Working Directory All application code lives at `/opt/lifeos/dev/` on the server, mounted into the Docker container as `/app`. The container runs `uvicorn main:app --host 0.0.0.0 --port 8003 --workers 1 --reload`, so any file change is picked up immediately. No container rebuild needed. ``` /opt/lifeos/dev/ main.py # FastAPI app init, 18 router includes .env # DB creds (never commit) Dockerfile requirements.txt core/ __init__.py database.py # Async engine, session factory, get_db dependency base_repository.py # Generic CRUD with soft deletes sidebar.py # Domain > area > project nav tree + badge counts routers/ domains.py, areas.py, projects.py, tasks.py notes.py, links.py, focus.py, capture.py, contacts.py search.py, admin.py, lists.py files.py, meetings.py, decisions.py, weblinks.py appointments.py, time_tracking.py templates/ base.html # Shell: topbar, sidebar, JS includes dashboard.html, search.html, trash.html [entity].html, [entity]_form.html, [entity]_detail.html (42 templates total - all extend base.html) static/ style.css # ~1040 lines app.js # ~190 lines tests/ introspect.py # Route discovery engine form_factory.py # Form data generation registry.py # Route registry + seed mapping conftest.py # Fixtures: DB, client, 15 seed entities test_smoke_dynamic.py # Auto-parametrized GET tests test_crud_dynamic.py # Auto-parametrized POST tests test_business_logic.py # Hand-written behavioral tests route_report.py # CLI route dump run_tests.sh # Test runner ``` --- ## How to Make Changes **Read before writing.** Before modifying any file, read it first. The codebase has consistent patterns. Match them exactly. **Do not create deploy scripts or heredoc wrappers.** You have direct filesystem access to `/opt/lifeos/dev/`. Edit files in place. Hot reload handles the rest. **After changes, verify:** ```bash docker logs lifeos-dev --tail 10 # Check for import/syntax errors curl -s -o /dev/null -w "%{http_code}" http://localhost:8003/ # Should be 200 ``` **Commit when asked:** ```bash cd /opt/lifeos/dev && git add . && git commit -m "description" && git push origin main ``` Push uses PAT (personal access token) as password. --- ## Database Access ```bash # Query docker exec lifeos-db psql -U postgres -d lifeos_dev -c "SQL HERE" # Inspect table schema (DO THIS before writing code that touches a table) docker exec lifeos-db psql -U postgres -d lifeos_dev -c "\d table_name" # List all tables docker exec lifeos-db psql -U postgres -d lifeos_dev -c "\dt" ``` **Connection string (in .env):** ``` DATABASE_URL=postgresql+asyncpg://postgres:UCTOQDZiUhN8U@lifeos-db:5432/lifeos_dev FILE_STORAGE_PATH=/opt/lifeos/files/dev ENVIRONMENT=development ``` **Three databases exist in lifeos-db:** - `lifeos_dev` - Active development (this is what you work with) - `lifeos_prod` - Production data (DO NOT TOUCH) - `lifeos_test` - Test suite database **Always verify table schemas against the live DB before writing code.** The .sql files in the project-docs folder may be stale. The database is the source of truth: ```bash docker exec lifeos-db psql -U postgres -d lifeos_dev -c "\d tasks" ``` --- ## Backup Before Risky Changes ```bash mkdir -p /opt/lifeos/backups docker exec lifeos-db pg_dump -U postgres -d lifeos_dev -Fc -f /tmp/lifeos_dev_backup.dump docker cp lifeos-db:/tmp/lifeos_dev_backup.dump /opt/lifeos/backups/lifeos_dev_$(date +%Y%m%d_%H%M%S).dump ``` **Restore:** ```bash docker exec lifeos-db psql -U postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'lifeos_dev' AND pid <> pg_backend_pid();" docker exec lifeos-db psql -U postgres -c "DROP DATABASE lifeos_dev;" docker exec lifeos-db psql -U postgres -c "CREATE DATABASE lifeos_dev;" docker cp /opt/lifeos/backups/FILENAME.dump lifeos-db:/tmp/restore.dump docker exec lifeos-db pg_restore -U postgres -d lifeos_dev /tmp/restore.dump docker restart lifeos-dev ``` --- ## BaseRepository Pattern - Use It All CRUD goes through `core/base_repository.py`. Read this file to understand the API. Key methods: - `list(filters={}, sort='sort_order')` - Auto-filters `is_deleted=false` - `get(id)` - Single record by UUID - `create(data)` - Auto-sets created_at, updated_at, is_deleted=false - `update(id, data)` - Auto-sets updated_at - `soft_delete(id)` - Sets is_deleted=true, deleted_at=now() - `restore(id)` - Reverses soft_delete - `permanent_delete(id)` - Actual SQL DELETE (admin only) - `bulk_soft_delete(ids)`, `reorder(ids)`, `count(filters)`, `list_deleted()` **Usage:** ```python repo = BaseRepository("tasks", db) items = await repo.list(filters={"project_id": project_id}) ``` **Nullable fields gotcha:** When a form field should allow setting a value to empty/null, the field name MUST be in the `nullable_fields` set in `core/base_repository.py`. Otherwise `update()` silently skips null values. Check this set before adding new nullable form fields. --- ## Router Pattern - Follow Exactly Every router follows this structure. Do not invent new patterns. ```python from fastapi import APIRouter, Request, Form, Depends from fastapi.responses import RedirectResponse from fastapi.templating import Jinja2Templates from core.base_repository import BaseRepository from core.database import get_db from core.sidebar import get_sidebar_data router = APIRouter(prefix='/things', tags=['things']) templates = Jinja2Templates(directory='templates') @router.get('/') async def list_things(request: Request, db=Depends(get_db)): repo = BaseRepository("things", db) items = await repo.list() sidebar = await get_sidebar_data(db) return templates.TemplateResponse('things.html', { 'request': request, 'items': items, 'sidebar': sidebar }) @router.post('/create') async def create_thing(request: Request, db=Depends(get_db), title: str = Form(...), description: str = Form(None)): repo = BaseRepository("things", db) item = await repo.create({'title': title, 'description': description}) return RedirectResponse(url=f'/things/{item["id"]}', status_code=303) ``` **Every route MUST call `get_sidebar_data(db)` and pass `sidebar` to the template.** The sidebar navigation breaks without this. --- ## Adding a New Entity - Checklist 1. Create `routers/entity_name.py` using the pattern above 2. Add import + `app.include_router(router)` in `main.py` 3. Create templates in `templates/`: list, form, detail (all extend `base.html`) 4. Add nav link in `templates/base.html` sidebar section 5. Add entity config to `SEARCH_ENTITIES` dict in `routers/search.py` 6. Add entity config to `TRASH_ENTITIES` dict in `routers/admin.py` 7. Add any new nullable fields to `nullable_fields` in `core/base_repository.py` 8. If entity has a new table, add seed fixture to `tests/conftest.py` and prefix mapping to `tests/registry.py` 9. Test: visit the list page, create an item, edit it, delete it, verify it appears in trash and search --- ## Universal Column Conventions Every table follows this structure. When creating new tables, match it exactly: ```sql id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- [foreign keys, nullable where optional] -- [content fields] tags TEXT[], sort_order INT NOT NULL DEFAULT 0, is_deleted BOOL NOT NULL DEFAULT false, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- searchable tables also get: -- search_vector TSVECTOR (maintained by trigger) -- CREATE INDEX idx_tablename_search ON tablename USING GIN(search_vector); ``` --- ## Key Enums / Values **Task status:** `open | in_progress | blocked | done | cancelled` **Task priority:** `1=critical, 2=high, 3=normal, 4=low` **Project status:** `active | on_hold | completed | archived` **Process type:** `workflow | checklist` --- ## Known Traps 1. **`time_entries` has no `updated_at` column.** BaseRepository.soft_delete() will fail on this table. Use direct SQL instead. Same for restore. 2. **Junction tables use raw SQL, not BaseRepository.** ```python await db.execute(text("INSERT INTO contact_meetings (contact_id, meeting_id, role) VALUES (:c, :m, :r) ON CONFLICT DO NOTHING"), {...}) ``` 3. **Timer constraint:** Only one timer runs at a time. `get_running_task_id()` in `routers/tasks.py` queries `time_entries WHERE end_at IS NULL`. Starting a new timer auto-stops the running one. 4. **SSL cert path** on Nginx uses `kasm.invixiom.com-0001` (not `kasm.invixiom.com`). 5. **No pagination.** All list views load all rows. Fine at current data volume. 6. **No CSRF protection.** Single-user system. 7. **Schema SQL files may be stale.** Always verify against live DB, not the .sql files in project-docs. 8. **Test suite not fully green.** Async event loop fixes and seed data corrections are in progress. --- ## CSS Design Tokens When adding styles, use these existing CSS custom properties. Do not hardcode colors. ```css /* Dark theme */ --bg: #0D0E13; --surface: #14161F; --surface2: #1A1D28; --border: #252836; --text: #DDE1F5; --muted: #5A6080; --accent: #4F6EF7; --accent-soft: rgba(79,110,247,.12); --green: #22C98A; --amber: #F5A623; --red: #F05252; --purple: #9B7FF5; /* Light theme */ --bg: #F0F2F8; --surface: #FFFFFF; --surface2: #F7F8FC; --border: #E3E6F0; --text: #171926; --muted: #8892B0; --accent: #4F6EF7; --accent-soft: rgba(79,110,247,.10); --green: #10B981; --amber: #F59E0B; --red: #DC2626; ``` --- ## Docker / Infrastructure ### Containers | Container | Image | Port | Purpose | |-----------|-------|------|---------| | lifeos-db | postgres:16-alpine | 5432 (internal) | PostgreSQL: lifeos_dev + lifeos_prod + lifeos_test | | lifeos-dev | lifeos-app | 8003 | Dev application (hot reload) | | lifeos-prod | lifeos-app | 8002 | Prod application (NOT YET DEPLOYED) | ### How the Dev Container Runs ```bash docker run -d \ --name lifeos-dev \ --network lifeos_network \ --restart unless-stopped \ --env-file .env \ -p 8003:8003 \ -v /opt/lifeos/dev/files:/opt/lifeos/files/dev \ -v /opt/lifeos/dev:/app \ lifeos-app \ uvicorn main:app --host 0.0.0.0 --port 8003 --workers 1 --reload ``` ### Nginx Host-level (not containerized). Config at `/etc/nginx/sites-available/invixiom`. - `lifeos-dev.invixiom.com` -> `localhost:8003` - `lifeos.invixiom.com` -> `localhost:8002` (pending) - SSL cert at `/etc/letsencrypt/live/kasm.invixiom.com-0001/` ### Docker Network `lifeos_network` (172.21.0.0/16) - bridges lifeos-db, lifeos-dev, lifeos-prod --- ## Test Suite Tests use dynamic introspection. They discover routes from the live app at runtime. No hardcoded routes. ```bash docker exec lifeos-dev bash /app/tests/run_tests.sh # Full suite docker exec lifeos-dev bash /app/tests/run_tests.sh report # Route introspection dump docker exec lifeos-dev bash /app/tests/run_tests.sh smoke # All GET endpoints docker exec lifeos-dev bash /app/tests/run_tests.sh crud # All POST create/edit/delete docker exec lifeos-dev bash /app/tests/run_tests.sh logic # Business logic docker exec lifeos-dev bash /app/tests/run_tests.sh fast # Smoke, stop on first fail docker exec lifeos-dev bash /app/tests/run_tests.sh -k "timer" # Keyword filter ``` **After schema changes, reset the test DB:** ```bash docker exec lifeos-db psql -U postgres -c "DROP DATABASE IF EXISTS lifeos_test;" docker exec lifeos-db psql -U postgres -c "CREATE DATABASE lifeos_test;" docker exec lifeos-db pg_dump -U postgres -d lifeos_dev --schema-only -f /tmp/s.sql docker exec lifeos-db psql -U postgres -d lifeos_test -f /tmp/s.sql -q ``` --- ## Current Build State (as of 2026-03-01) **Phase 1, Tier 3 in progress.** 2 of 6 Tier 3 features complete. ### What's Built 18 routers, 42 templates. Core CRUD for: domains, areas, projects, tasks, notes, links, focus, capture, contacts, search, admin/trash, lists, files, meetings, decisions, weblinks, appointments, time_tracking. Working features: collapsible sidebar nav tree, task filtering/sorting/priority/status/context, daily focus, markdown notes with preview, file uploads with inline preview, global search (Cmd/K) via tsvector, admin trash with restore, time tracking with topbar timer pill, timer play/stop on task rows and detail pages. ### What's NOT Built Yet **Tier 3 remaining (4 features, build in this order):** 1. **Processes / Process Runs** - Most complex. 4 tables: processes, process_steps, process_runs, process_run_steps. Template CRUD, run instantiation (copies steps as immutable snapshot), step completion, task generation (all_at_once vs step_by_step). 2. **Calendar view** - Unified `/calendar`: appointments (start_at) + meetings (meeting_date) + tasks (due_date). No new tables. Read-only. 3. **Time budgets** - Simple CRUD: domain_id + weekly_hours + effective_from. Dashboard overcommitment warnings. 4. **Eisenhower matrix** - Derived 2x2 grid. Priority 1-2 = Important, 3-4 = Not. Due <=7d = Urgent. Clickable quadrants. **Tier 4 (later):** Releases/milestones, dependencies (DAG with cycle detection), task templates, note wiki-linking, note folders, bulk actions, CSV export, drag-to-reorder, reminders, dashboard metrics. **Phase 2:** DAG visualization, MCP/AI gateway (dual: MCP + OpenAI function calling), note version history. **Phase 3:** Mobile improvements, authentication, browser extension. --- ## Conversation History | Session | What Was Built | |---------|----------------| | Pre-build | Architecture doc (50 tables, 3-phase plan), server config, schema design | | Convo 1 | Foundation: 9 routers (domains, areas, projects, tasks, notes, links, focus, capture, contacts), base templates, sidebar, dashboard | | Convo 2 | 7 more routers: search, admin/trash, lists, files, meetings, decisions, weblinks | | Convo 3 | Tier 3 start: appointments CRUD, time tracking with topbar timer pill | | Convo 4 | Timer buttons on task rows and detail pages (completes time tracking UX) | | Test1 | Dynamic introspection-based test suite (11 files, 121 routes discovered) | | Test2 | Test suite debugging: async event loop fixes, seed data corrections (in progress) | --- ## Database Schema Overview (48 tables in dev) ### Core Hierarchy domains, areas, projects, tasks ### Content notes, note_folders, lists, list_items, links, files, weblinks, weblink_folders ### CRM & Meetings contacts, appointments, meetings, decisions ### Time Management time_entries, time_blocks, time_budgets, daily_focus ### Processes (tables exist, CRUD not built yet) processes, process_steps, process_runs, process_run_steps ### System capture, context_types, reminders, dependencies, releases, milestones, task_templates, task_template_items ### Junction Tables (17) note_projects, note_links, file_mappings, release_projects, release_domains, contact_tasks, contact_projects, contact_lists, contact_list_items, contact_appointments, contact_meetings, decision_projects, decision_contacts, meeting_tasks, process_run_tasks, folder_weblinks, note_version_history --- ## Reference Documents These files are in the `project-docs/` folder alongside this CLAUDE.md. Consult them when you need deeper context: | File | What It Contains | |------|------------------| | `lifeos-development-status-convo4.md` | **App source of truth.** Complete inventory of routers, templates, deploy patterns, what's remaining. | | `lifeos-development-status-test1.md` | **Test source of truth.** Test architecture, seed data, introspection details. | | `lifeos-conversation-context-convo4.md` | Quick context card for app development. | | `lifeos-conversation-context-convo-test1.md` | Quick context card for test development. | | `lifeos-architecture.docx` | Full system spec. 50 tables, all subsystems, UI patterns, build plan. | | `life-os-server-config.docx` | Server infrastructure: containers, ports, networks, Nginx, SSL details. | | `lifeos_r1_full_schema.sql` | Intended R1 schema (may not match actual DB - always verify against live). | | `lifeos_schema_r1.sql` | Schema reference (same caveat). | | `lifeos_r0_to_r1_migration.sql` | Migration script from old Supabase schema. | | `lifeos-setup.sh` | Repeatable server infrastructure setup script. | | `setup_dev_database.sh` | Dev database setup. | | `setup_prod_database.sh` | Prod database setup. | | `lifeos-database-backup.md` | Backup and restore commands. | | `lifeos-v2-migration-plan.docx` | V2 migration planning. | | `_liefos-dev-test_results1.txt` | First test deploy output (introspection verification). | --- ## Design Principles - **KISS.** Simple wins. Complexity must earn its place. - **Logical deletes everywhere.** No data permanently destroyed without admin confirmation. - **Generic over specific.** Shared base code. Config-driven where possible. - **Consistent patterns.** Every list, form, detail view follows identical conventions. - **Search is first-class.** Every new entity gets added to global search. - **Context travels with navigation.** Drill-down pre-fills context into create forms. - **Single source of truth.** One system for everything.