Files
lifeos-prod/CLAUDE.md
2026-03-03 00:44:33 +00:00

444 lines
18 KiB
Markdown

# 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.