Files
lifeos-dev/CLAUDE.md

18 KiB

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:

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:

cd /opt/lifeos/dev && git add . && git commit -m "description" && git push origin main

Push uses PAT (personal access token) as password.


Database Access

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

docker exec lifeos-db psql -U postgres -d lifeos_dev -c "\d tasks"

Backup Before Risky Changes

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:

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:

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.

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:

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.

    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.

/* 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

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.

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:

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.