2069 lines
77 KiB
Markdown
2069 lines
77 KiB
Markdown
# Life OS - Comprehensive System Design Document
|
|
|
|
**Version:** 1.0
|
|
**Date:** 2026-03-02
|
|
**Purpose:** Complete rebuild reference. Contains everything needed to reconstruct this system from ground zero.
|
|
|
|
---
|
|
|
|
# Part 1: System Overview
|
|
|
|
## 1. Project Identity & Purpose
|
|
|
|
Life OS is a **single-user personal productivity web application**. It is a unified system for managing every dimension of personal productivity: tasks, projects, notes, contacts, meetings, decisions, time tracking, file management, and more.
|
|
|
|
**Design Philosophy:**
|
|
- **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 entity gets PostgreSQL full-text search via tsvector/tsquery.
|
|
- **Context travels with navigation.** Drill-down pre-fills context into create forms.
|
|
- **Single source of truth.** One system for everything.
|
|
|
|
**Live URL:** `https://lifeos-dev.invixiom.com`
|
|
**Server:** defiant-01, 46.225.166.142, Ubuntu 24.04
|
|
**Repository:** `github.com/mdombaugh/lifeos-dev` (main branch)
|
|
|
|
---
|
|
|
|
## 2. Architecture Overview
|
|
|
|
Life OS is a **server-rendered monolith**. There is no SPA, no frontend framework, and no build pipeline.
|
|
|
|
**Key Architectural Decisions:**
|
|
|
|
| Decision | Choice | Rationale |
|
|
|----------|--------|-----------|
|
|
| Rendering | Server-side (Jinja2) | Simplicity; no JS framework to maintain |
|
|
| Data mutations | HTML form POST with 303 redirect (PRG) | Standard web pattern; no XHR/fetch for data ops |
|
|
| JavaScript role | UI state only (sidebar, search, timer, theme) | Minimal client JS; zero fetch for data changes |
|
|
| ORM | None. Raw SQL via `text()` | Full control; no migration tooling needed |
|
|
| CRUD abstraction | Generic BaseRepository class | One class handles all 48 tables |
|
|
| Deletion | Soft delete everywhere | `is_deleted` + `deleted_at`; admin trash for recovery |
|
|
| Search | PostgreSQL tsvector/tsquery | Built-in, no external search service |
|
|
| Authentication | None (single user) | Phase 3 roadmap item |
|
|
| CSRF | None (single user) | Not needed for single-user system |
|
|
|
|
**Request Flow:**
|
|
```
|
|
Browser → Nginx (SSL termination) → Docker container (uvicorn) → FastAPI router
|
|
→ BaseRepository / raw SQL → PostgreSQL → Jinja2 template → HTML response
|
|
```
|
|
|
|
**Data Mutation Flow (PRG Pattern):**
|
|
```
|
|
Browser submits HTML <form> via POST → Router processes Form() params
|
|
→ BaseRepository.create/update/soft_delete → 303 redirect → GET page
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Technology Stack
|
|
|
|
Every dependency with exact version:
|
|
|
|
### Python Dependencies (requirements.txt)
|
|
|
|
| Package | Version | Purpose |
|
|
|---------|---------|---------|
|
|
| fastapi | 0.115.6 | Web framework (async) |
|
|
| uvicorn[standard] | 0.34.0 | ASGI server with hot reload |
|
|
| sqlalchemy[asyncio] | 2.0.36 | SQL toolkit (async engine, `text()` only - NO ORM models) |
|
|
| asyncpg | 0.30.0 | PostgreSQL async driver |
|
|
| jinja2 | 3.1.4 | Template engine |
|
|
| python-multipart | 0.0.18 | Form data parsing |
|
|
| python-dotenv | 1.0.1 | Environment variable loading |
|
|
| pydantic | 2.10.3 | Data validation (FastAPI dependency) |
|
|
| pyyaml | 6.0.2 | Configuration parsing |
|
|
| aiofiles | 24.1.0 | Async file I/O for uploads |
|
|
|
|
### Infrastructure
|
|
|
|
| Component | Version | Purpose |
|
|
|-----------|---------|---------|
|
|
| Python | 3.12-slim (Docker) | Application runtime |
|
|
| PostgreSQL | 16-alpine (Docker) | Database (3 databases: dev, prod, test) |
|
|
| Nginx | Host-level (Ubuntu) | Reverse proxy, SSL termination |
|
|
| Docker | Host-level | Container runtime |
|
|
| Let's Encrypt | Auto-renew | SSL certificates |
|
|
|
|
### Frontend (Zero Build)
|
|
|
|
| Component | Source | Purpose |
|
|
|-----------|--------|---------|
|
|
| Vanilla CSS | `/static/style.css` | All styling, ~1200 lines |
|
|
| Vanilla JS | `/static/app.js` | UI interactions only, ~190 lines |
|
|
| DM Sans | Google Fonts CDN | Body font |
|
|
| JetBrains Mono | Google Fonts CDN | Code/monospace font |
|
|
|
|
**Hard Rules:**
|
|
- No npm. No React. No Tailwind. No build tools.
|
|
- No fetch/XHR for data mutations. Forms use standard POST with 303 redirect.
|
|
- JavaScript handles ONLY: sidebar collapse, search modal, timer display, theme toggle, mobile nav, filter auto-submit, delete confirmation.
|
|
|
|
---
|
|
|
|
# Part 2: Infrastructure
|
|
|
|
## 4. Server & Network Topology
|
|
|
|
**Server:** defiant-01 (46.225.166.142)
|
|
**OS:** Ubuntu 24.04
|
|
**Network:** `lifeos_network` (Docker bridge, 172.21.0.0/16)
|
|
|
|
```
|
|
Internet
|
|
│
|
|
▼
|
|
┌────────────────┐
|
|
│ Nginx (host) │ SSL termination
|
|
│ Port 80/443 │
|
|
└───────┬────────┘
|
|
│
|
|
┌────────────┴────────────┐
|
|
│ │
|
|
▼ ▼
|
|
┌─────────────────┐ ┌──────────────────┐
|
|
│ lifeos-dev │ │ lifeos-prod │
|
|
│ Port 8003 │ │ Port 8002 │
|
|
│ (hot reload) │ │ (not yet deployed)
|
|
└────────┬────────┘ └────────┬─────────┘
|
|
│ │
|
|
└───────────┬───────────┘
|
|
▼
|
|
┌─────────────────┐
|
|
│ lifeos-db │
|
|
│ PostgreSQL 16 │
|
|
│ Port 5432 │
|
|
│ (internal) │
|
|
└─────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Docker Configuration
|
|
|
|
### Dockerfile
|
|
|
|
```dockerfile
|
|
FROM python:3.12-slim
|
|
WORKDIR /app
|
|
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
gcc \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
COPY requirements.txt .
|
|
RUN pip install --no-cache-dir -r requirements.txt
|
|
|
|
COPY . .
|
|
RUN mkdir -p /opt/lifeos/files
|
|
|
|
EXPOSE 8000
|
|
|
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
|
|
```
|
|
|
|
### Container Run Commands
|
|
|
|
**Dev Container (hot reload, volume mount):**
|
|
```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
|
|
```
|
|
|
|
The volume mount `-v /opt/lifeos/dev:/app` means any file change on the host is immediately visible inside the container. Combined with `--reload`, uvicorn detects changes and restarts automatically. No container rebuild needed.
|
|
|
|
**Database Container:**
|
|
```bash
|
|
docker run -d \
|
|
--name lifeos-db \
|
|
--network lifeos_network \
|
|
--restart unless-stopped \
|
|
-e POSTGRES_PASSWORD=UCTOQDZiUhN8U \
|
|
-v lifeos_pgdata:/var/lib/postgresql/data \
|
|
postgres:16-alpine
|
|
```
|
|
|
|
### Environment Variables (.env)
|
|
|
|
```
|
|
DATABASE_URL=postgresql+asyncpg://postgres:UCTOQDZiUhN8U@lifeos-db:5432/lifeos_dev
|
|
FILE_STORAGE_PATH=/opt/lifeos/files/dev
|
|
ENVIRONMENT=development
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Nginx & SSL
|
|
|
|
**Config location:** `/etc/nginx/sites-available/invixiom`
|
|
|
|
**Domain routing:**
|
|
- `lifeos-dev.invixiom.com` → `localhost:8003` (dev)
|
|
- `lifeos.invixiom.com` → `localhost:8002` (prod, pending)
|
|
|
|
**SSL certificate path:** `/etc/letsencrypt/live/kasm.invixiom.com-0001/`
|
|
|
|
Note: The cert uses the `kasm.invixiom.com-0001` path (not `kasm.invixiom.com`). This is a known quirk of the Let's Encrypt renewal structure on this server.
|
|
|
|
---
|
|
|
|
## 7. Database Configuration
|
|
|
|
**Three databases in lifeos-db:**
|
|
|
|
| Database | Purpose | Used By |
|
|
|----------|---------|---------|
|
|
| `lifeos_dev` | Active development | lifeos-dev container |
|
|
| `lifeos_prod` | Production data (DO NOT TOUCH) | lifeos-prod container |
|
|
| `lifeos_test` | Test suite | pytest via `tests/conftest.py` |
|
|
|
|
**Connection pooling (SQLAlchemy):**
|
|
```python
|
|
engine = create_async_engine(
|
|
DATABASE_URL,
|
|
echo=os.getenv("ENVIRONMENT") == "development",
|
|
pool_size=5,
|
|
max_overflow=10,
|
|
pool_pre_ping=True,
|
|
)
|
|
```
|
|
|
|
**Session management:**
|
|
```python
|
|
async_session_factory = async_sessionmaker(
|
|
engine, class_=AsyncSession, expire_on_commit=False,
|
|
)
|
|
|
|
async def get_db():
|
|
async with async_session_factory() as session:
|
|
try:
|
|
yield session
|
|
await session.commit()
|
|
except Exception:
|
|
await session.rollback()
|
|
raise
|
|
finally:
|
|
await session.close()
|
|
```
|
|
|
|
---
|
|
|
|
# Part 3: Database Schema
|
|
|
|
## 8. Schema Conventions
|
|
|
|
Every table follows these universal conventions:
|
|
|
|
### Standard Column Set
|
|
```sql
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
-- [entity-specific columns]
|
|
tags TEXT[], -- Array of string tags
|
|
sort_order INT NOT NULL DEFAULT 0, -- Manual ordering
|
|
is_deleted BOOL NOT NULL DEFAULT false, -- Soft delete flag
|
|
deleted_at TIMESTAMPTZ, -- When soft-deleted
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- Auto-set on insert
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- Auto-set on update
|
|
```
|
|
|
|
### Searchable Tables Add
|
|
```sql
|
|
search_vector TSVECTOR -- Full-text search column
|
|
-- Plus GIN index:
|
|
CREATE INDEX idx_tablename_search ON tablename USING GIN(search_vector);
|
|
-- Plus trigger:
|
|
CREATE TRIGGER trg_tablename_search
|
|
BEFORE INSERT OR UPDATE ON tablename
|
|
FOR EACH ROW EXECUTE FUNCTION update_tablename_search();
|
|
```
|
|
|
|
### Soft Delete Pattern
|
|
- `is_deleted = false` filter applied automatically by BaseRepository
|
|
- `deleted_at` timestamp records when deletion occurred
|
|
- `soft_delete()` sets both fields
|
|
- `restore()` clears both fields
|
|
- `permanent_delete()` does actual SQL DELETE (admin only)
|
|
|
|
### Foreign Key Conventions
|
|
- Required parent references use `ON DELETE CASCADE`
|
|
- Optional references use `ON DELETE SET NULL`
|
|
- Junction tables use composite PKs with CASCADE on both sides
|
|
|
|
---
|
|
|
|
## 9. Core Entity Tables (31 tables)
|
|
|
|
### domains
|
|
Life areas at the highest level (e.g., "Career", "Health", "Family").
|
|
```sql
|
|
CREATE TABLE domains (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name TEXT NOT NULL,
|
|
color TEXT, -- Hex color for UI badges
|
|
description TEXT,
|
|
icon TEXT,
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
is_deleted BOOL NOT NULL DEFAULT false,
|
|
deleted_at TIMESTAMPTZ,
|
|
search_vector TSVECTOR,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX idx_domains_search ON domains USING GIN(search_vector);
|
|
CREATE INDEX idx_domains_sort ON domains USING btree(sort_order);
|
|
```
|
|
**Search trigger:** Indexes `name`.
|
|
|
|
### areas
|
|
Subcategories within domains (e.g., "Software Engineering" under "Career").
|
|
```sql
|
|
CREATE TABLE areas (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
domain_id UUID NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
icon TEXT,
|
|
color TEXT,
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
is_deleted BOOL NOT NULL DEFAULT false,
|
|
deleted_at TIMESTAMPTZ,
|
|
search_vector TSVECTOR,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX idx_areas_search ON areas USING GIN(search_vector);
|
|
CREATE INDEX idx_areas_sort ON areas USING btree(domain_id, sort_order);
|
|
```
|
|
**Search trigger:** Indexes `name`, `description`.
|
|
|
|
### projects
|
|
Concrete initiatives with status tracking and timelines.
|
|
```sql
|
|
CREATE TABLE projects (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
domain_id UUID NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
|
area_id UUID REFERENCES areas(id) ON DELETE SET NULL,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
status TEXT NOT NULL DEFAULT 'active', -- active|on_hold|completed|archived
|
|
priority INT NOT NULL DEFAULT 3, -- 1=critical, 2=high, 3=normal, 4=low
|
|
start_date DATE,
|
|
target_date DATE,
|
|
completed_at TIMESTAMPTZ,
|
|
color TEXT,
|
|
tags TEXT[],
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
is_deleted BOOL NOT NULL DEFAULT false,
|
|
deleted_at TIMESTAMPTZ,
|
|
search_vector TSVECTOR,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX idx_projects_search ON projects USING GIN(search_vector);
|
|
CREATE INDEX idx_projects_sort ON projects USING btree(domain_id, sort_order);
|
|
CREATE INDEX idx_projects_area_sort ON projects USING btree(area_id, sort_order);
|
|
CREATE INDEX idx_projects_status ON projects USING btree(status);
|
|
```
|
|
**Search trigger:** Indexes `name`, `description`, `tags`.
|
|
|
|
### tasks
|
|
The core work unit. Supports subtasks, contexts, time estimates, and status workflow.
|
|
```sql
|
|
CREATE TABLE tasks (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
domain_id UUID REFERENCES domains(id) ON DELETE CASCADE,
|
|
area_id UUID REFERENCES areas(id) ON DELETE SET NULL,
|
|
project_id UUID REFERENCES projects(id) ON DELETE SET NULL,
|
|
release_id UUID REFERENCES releases(id) ON DELETE SET NULL,
|
|
parent_id UUID REFERENCES tasks(id) ON DELETE SET NULL, -- Subtask support
|
|
title TEXT NOT NULL,
|
|
description TEXT,
|
|
priority INT NOT NULL DEFAULT 3, -- 1=critical..4=low
|
|
status TEXT NOT NULL DEFAULT 'open', -- open|in_progress|blocked|done|cancelled
|
|
due_date DATE,
|
|
deadline TIMESTAMPTZ,
|
|
recurrence TEXT,
|
|
estimated_minutes INT,
|
|
energy_required TEXT, -- high|medium|low
|
|
context TEXT, -- @home, @office, @errands, etc.
|
|
is_custom_context BOOL NOT NULL DEFAULT false,
|
|
waiting_for_contact_id UUID REFERENCES contacts(id) ON DELETE SET NULL,
|
|
waiting_since DATE,
|
|
import_batch_id UUID,
|
|
tags TEXT[],
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
is_deleted BOOL NOT NULL DEFAULT false,
|
|
deleted_at TIMESTAMPTZ,
|
|
completed_at TIMESTAMPTZ,
|
|
search_vector TSVECTOR,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX idx_tasks_search ON tasks USING GIN(search_vector);
|
|
CREATE INDEX idx_tasks_status ON tasks USING btree(status);
|
|
CREATE INDEX idx_tasks_priority ON tasks USING btree(priority);
|
|
CREATE INDEX idx_tasks_due_date ON tasks USING btree(due_date);
|
|
CREATE INDEX idx_tasks_domain_sort ON tasks USING btree(domain_id, sort_order);
|
|
CREATE INDEX idx_tasks_project_sort ON tasks USING btree(project_id, sort_order);
|
|
CREATE INDEX idx_tasks_parent_sort ON tasks USING btree(parent_id, sort_order);
|
|
```
|
|
**Search trigger:** Indexes `title`, `description`, `tags`.
|
|
|
|
### notes
|
|
Markdown-based notes linked to domains, projects, tasks, meetings, or folders.
|
|
```sql
|
|
CREATE TABLE notes (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
domain_id UUID REFERENCES domains(id) ON DELETE CASCADE,
|
|
project_id UUID REFERENCES projects(id) ON DELETE SET NULL,
|
|
folder_id UUID REFERENCES note_folders(id) ON DELETE SET NULL,
|
|
meeting_id UUID REFERENCES meetings(id) ON DELETE SET NULL,
|
|
task_id UUID REFERENCES tasks(id) ON DELETE SET NULL,
|
|
title TEXT NOT NULL,
|
|
body TEXT,
|
|
content_format TEXT NOT NULL DEFAULT 'rich',
|
|
is_meeting_note BOOL NOT NULL DEFAULT false,
|
|
tags TEXT[],
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
is_deleted BOOL NOT NULL DEFAULT false,
|
|
deleted_at TIMESTAMPTZ,
|
|
search_vector TSVECTOR,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX idx_notes_search ON notes USING GIN(search_vector);
|
|
CREATE INDEX idx_notes_task_id ON notes USING btree(task_id);
|
|
```
|
|
**Search trigger:** Indexes `title`, `body`, `tags`.
|
|
|
|
### note_folders
|
|
Hierarchical folder system for organizing notes.
|
|
```sql
|
|
CREATE TABLE note_folders (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
parent_id UUID REFERENCES note_folders(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
auto_generated BOOL NOT NULL DEFAULT false,
|
|
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()
|
|
);
|
|
```
|
|
|
|
### lists
|
|
Checklists and general-purpose lists linked to domains, projects, tasks, or meetings.
|
|
```sql
|
|
CREATE TABLE lists (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
domain_id UUID REFERENCES domains(id) ON DELETE CASCADE,
|
|
area_id UUID REFERENCES areas(id) ON DELETE SET NULL,
|
|
project_id UUID REFERENCES projects(id) ON DELETE SET NULL,
|
|
task_id UUID REFERENCES tasks(id) ON DELETE SET NULL,
|
|
meeting_id UUID REFERENCES meetings(id) ON DELETE SET NULL,
|
|
name TEXT NOT NULL,
|
|
list_type TEXT NOT NULL DEFAULT 'checklist',
|
|
description TEXT,
|
|
tags TEXT[],
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
is_deleted BOOL NOT NULL DEFAULT false,
|
|
deleted_at TIMESTAMPTZ,
|
|
search_vector TSVECTOR,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX idx_lists_search ON lists USING GIN(search_vector);
|
|
CREATE INDEX idx_lists_task_id ON lists USING btree(task_id);
|
|
CREATE INDEX idx_lists_meeting_id ON lists USING btree(meeting_id);
|
|
```
|
|
**Search trigger:** Indexes `name`, `description`, `tags`.
|
|
|
|
### list_items
|
|
Individual items within a list. Supports nesting via `parent_item_id`.
|
|
```sql
|
|
CREATE TABLE list_items (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
list_id UUID NOT NULL REFERENCES lists(id) ON DELETE CASCADE,
|
|
parent_item_id UUID REFERENCES list_items(id) ON DELETE SET NULL,
|
|
content TEXT NOT NULL,
|
|
completed BOOL NOT NULL DEFAULT false,
|
|
completed_at TIMESTAMPTZ,
|
|
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()
|
|
);
|
|
CREATE INDEX idx_list_items_sort ON list_items USING btree(list_id, sort_order);
|
|
CREATE INDEX idx_list_items_parent_sort ON list_items USING btree(parent_item_id, sort_order);
|
|
```
|
|
|
|
### links
|
|
Quick-access bookmarks organized by domain/area/project.
|
|
```sql
|
|
CREATE TABLE links (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
domain_id UUID REFERENCES domains(id) ON DELETE CASCADE,
|
|
area_id UUID REFERENCES areas(id) ON DELETE SET NULL,
|
|
project_id UUID REFERENCES projects(id) ON DELETE SET NULL,
|
|
label TEXT NOT NULL,
|
|
url TEXT NOT NULL,
|
|
description TEXT,
|
|
tags TEXT[],
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
is_deleted BOOL NOT NULL DEFAULT false,
|
|
deleted_at TIMESTAMPTZ,
|
|
search_vector TSVECTOR,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX idx_links_search ON links USING GIN(search_vector);
|
|
```
|
|
**Search trigger:** Indexes `label`, `url`, `description`, `tags`.
|
|
|
|
### files
|
|
Uploaded file metadata. Actual files stored on disk at `FILE_STORAGE_PATH`.
|
|
```sql
|
|
CREATE TABLE files (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
filename TEXT NOT NULL, -- Stored filename (UUID-based)
|
|
original_filename TEXT NOT NULL, -- User's original filename
|
|
storage_path TEXT NOT NULL, -- Full disk path
|
|
mime_type TEXT,
|
|
size_bytes INT,
|
|
description TEXT,
|
|
tags TEXT[],
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
is_deleted BOOL NOT NULL DEFAULT false,
|
|
deleted_at TIMESTAMPTZ,
|
|
search_vector TSVECTOR,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX idx_files_search ON files USING GIN(search_vector);
|
|
```
|
|
**Search trigger:** Indexes `original_filename`, `description`, `tags`.
|
|
|
|
### weblinks
|
|
Similar to links but with task/meeting association and folder organization.
|
|
```sql
|
|
CREATE TABLE weblinks (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
label TEXT NOT NULL,
|
|
url TEXT NOT NULL,
|
|
description TEXT,
|
|
task_id UUID REFERENCES tasks(id) ON DELETE SET NULL,
|
|
meeting_id UUID REFERENCES meetings(id) ON DELETE SET NULL,
|
|
tags TEXT[],
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
is_deleted BOOL NOT NULL DEFAULT false,
|
|
deleted_at TIMESTAMPTZ,
|
|
search_vector TSVECTOR,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX idx_weblinks_search ON weblinks USING GIN(search_vector);
|
|
CREATE INDEX idx_weblinks_task_id ON weblinks USING btree(task_id);
|
|
CREATE INDEX idx_weblinks_meeting_id ON weblinks USING btree(meeting_id);
|
|
```
|
|
**Search trigger:** Indexes `label`, `url`, `description`, `tags`.
|
|
|
|
### weblink_folders
|
|
Hierarchical folder system for organizing weblinks.
|
|
```sql
|
|
CREATE TABLE weblink_folders (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
parent_id UUID REFERENCES weblink_folders(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
auto_generated BOOL NOT NULL DEFAULT false,
|
|
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()
|
|
);
|
|
CREATE INDEX idx_weblink_folders_sort ON weblink_folders USING btree(parent_id, sort_order);
|
|
```
|
|
|
|
### contacts
|
|
People linked to tasks, projects, meetings, and decisions.
|
|
```sql
|
|
CREATE TABLE contacts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
first_name TEXT NOT NULL,
|
|
last_name TEXT,
|
|
company TEXT,
|
|
role TEXT,
|
|
email TEXT,
|
|
phone TEXT,
|
|
notes TEXT,
|
|
tags TEXT[],
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
is_deleted BOOL NOT NULL DEFAULT false,
|
|
deleted_at TIMESTAMPTZ,
|
|
search_vector TSVECTOR,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX idx_contacts_search ON contacts USING GIN(search_vector);
|
|
```
|
|
**Search trigger:** Indexes `first_name`, `last_name`, `company`, `email`, `tags`.
|
|
|
|
### appointments
|
|
Calendar events with start/end times and recurrence.
|
|
```sql
|
|
CREATE TABLE appointments (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
title TEXT NOT NULL,
|
|
description TEXT,
|
|
location TEXT,
|
|
start_at TIMESTAMPTZ NOT NULL,
|
|
end_at TIMESTAMPTZ,
|
|
all_day BOOL NOT NULL DEFAULT false,
|
|
recurrence TEXT,
|
|
tags TEXT[],
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
is_deleted BOOL NOT NULL DEFAULT false,
|
|
deleted_at TIMESTAMPTZ,
|
|
search_vector TSVECTOR,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX idx_appointments_search ON appointments USING GIN(search_vector);
|
|
CREATE INDEX idx_appointments_start ON appointments USING btree(start_at);
|
|
```
|
|
**Search trigger:** Indexes `title`, `description`, `location`, `tags`.
|
|
|
|
### meetings
|
|
Meetings with agenda, transcript, notes, and linked action items.
|
|
```sql
|
|
CREATE TABLE meetings (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
parent_id UUID REFERENCES meetings(id) ON DELETE SET NULL, -- Recurring meeting series
|
|
title TEXT NOT NULL,
|
|
meeting_date DATE NOT NULL,
|
|
start_at TIMESTAMPTZ,
|
|
end_at TIMESTAMPTZ,
|
|
location TEXT,
|
|
status TEXT NOT NULL DEFAULT 'scheduled', -- scheduled|completed|cancelled
|
|
priority INT,
|
|
recurrence TEXT,
|
|
agenda TEXT,
|
|
transcript TEXT,
|
|
notes_body TEXT,
|
|
tags TEXT[],
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
is_deleted BOOL NOT NULL DEFAULT false,
|
|
deleted_at TIMESTAMPTZ,
|
|
search_vector TSVECTOR,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX idx_meetings_search ON meetings USING GIN(search_vector);
|
|
CREATE INDEX idx_meetings_date ON meetings USING btree(meeting_date);
|
|
```
|
|
**Search trigger:** Indexes `title`, `agenda`, `notes_body`, `tags`.
|
|
|
|
### decisions
|
|
Decision records with rationale, impact, status, and optional supersession chain.
|
|
```sql
|
|
CREATE TABLE decisions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
title TEXT NOT NULL,
|
|
rationale TEXT,
|
|
status TEXT NOT NULL DEFAULT 'proposed', -- proposed|accepted|rejected|superseded
|
|
impact TEXT NOT NULL DEFAULT 'medium', -- low|medium|high
|
|
decided_at DATE,
|
|
task_id UUID REFERENCES tasks(id) ON DELETE SET NULL,
|
|
meeting_id UUID REFERENCES meetings(id) ON DELETE SET NULL,
|
|
superseded_by_id UUID REFERENCES decisions(id) ON DELETE SET NULL,
|
|
tags TEXT[],
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
is_deleted BOOL NOT NULL DEFAULT false,
|
|
deleted_at TIMESTAMPTZ,
|
|
search_vector TSVECTOR,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX idx_decisions_search ON decisions USING GIN(search_vector);
|
|
CREATE INDEX idx_decisions_task_id ON decisions USING btree(task_id);
|
|
```
|
|
**Search trigger:** Indexes `title`, `rationale`, `tags`.
|
|
|
|
### time_entries
|
|
Time tracking records. **Note:** No `updated_at` column - this is a known exception.
|
|
```sql
|
|
CREATE TABLE time_entries (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
start_at TIMESTAMPTZ NOT NULL,
|
|
end_at TIMESTAMPTZ, -- NULL = timer is running
|
|
duration_minutes INT,
|
|
notes TEXT,
|
|
is_deleted BOOL NOT NULL DEFAULT false,
|
|
deleted_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
-- NO updated_at column! BaseRepository.soft_delete() will fail. Use direct SQL.
|
|
);
|
|
CREATE INDEX idx_time_entries_task ON time_entries USING btree(task_id);
|
|
```
|
|
|
|
### time_blocks
|
|
Scheduled time blocks for planning (distinct from time_entries which track actual work).
|
|
```sql
|
|
CREATE TABLE time_blocks (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
task_id UUID REFERENCES tasks(id) ON DELETE SET NULL,
|
|
title TEXT NOT NULL,
|
|
context TEXT,
|
|
energy TEXT,
|
|
start_at TIMESTAMPTZ NOT NULL,
|
|
end_at TIMESTAMPTZ NOT NULL,
|
|
is_deleted BOOL NOT NULL DEFAULT false,
|
|
deleted_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
```
|
|
|
|
### time_budgets
|
|
Weekly hour allocations per domain with effective dating.
|
|
```sql
|
|
CREATE TABLE time_budgets (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
domain_id UUID NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
|
weekly_hours NUMERIC NOT NULL,
|
|
effective_from DATE NOT NULL,
|
|
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()
|
|
);
|
|
```
|
|
|
|
### daily_focus
|
|
Links tasks to specific dates as "focus items" for that day.
|
|
```sql
|
|
CREATE TABLE daily_focus (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
focus_date DATE NOT NULL,
|
|
task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
slot INT,
|
|
completed BOOL NOT NULL DEFAULT false,
|
|
note 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()
|
|
);
|
|
CREATE INDEX idx_daily_focus_date ON daily_focus USING btree(focus_date);
|
|
```
|
|
|
|
### processes
|
|
Template definitions for repeatable workflows/checklists.
|
|
```sql
|
|
CREATE TABLE processes (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
process_type TEXT NOT NULL DEFAULT 'checklist', -- workflow|checklist
|
|
category TEXT,
|
|
status TEXT NOT NULL DEFAULT 'draft', -- draft|active|archived
|
|
tags TEXT[],
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
is_deleted BOOL NOT NULL DEFAULT false,
|
|
deleted_at TIMESTAMPTZ,
|
|
search_vector TSVECTOR,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX idx_processes_search ON processes USING GIN(search_vector);
|
|
```
|
|
**Search trigger:** Indexes `name`, `description`, `tags`.
|
|
|
|
### process_steps
|
|
Individual steps within a process template.
|
|
```sql
|
|
CREATE TABLE process_steps (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
process_id UUID NOT NULL REFERENCES processes(id) ON DELETE CASCADE,
|
|
title TEXT NOT NULL,
|
|
instructions TEXT,
|
|
expected_output TEXT,
|
|
estimated_days INT,
|
|
context 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()
|
|
);
|
|
```
|
|
|
|
### process_runs
|
|
Instances of running a process. Copies steps as immutable snapshots.
|
|
```sql
|
|
CREATE TABLE process_runs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
process_id UUID NOT NULL REFERENCES processes(id) ON DELETE CASCADE,
|
|
title TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'not_started', -- not_started|in_progress|completed|cancelled
|
|
process_type TEXT NOT NULL,
|
|
task_generation TEXT NOT NULL DEFAULT 'all_at_once', -- all_at_once|step_by_step
|
|
project_id UUID REFERENCES projects(id) ON DELETE SET NULL,
|
|
contact_id UUID REFERENCES contacts(id) ON DELETE SET NULL,
|
|
started_at TIMESTAMPTZ,
|
|
completed_at TIMESTAMPTZ,
|
|
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()
|
|
);
|
|
```
|
|
|
|
### process_run_steps
|
|
Immutable step copies within a run, with completion tracking.
|
|
```sql
|
|
CREATE TABLE process_run_steps (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
run_id UUID NOT NULL REFERENCES process_runs(id) ON DELETE CASCADE,
|
|
title TEXT NOT NULL,
|
|
instructions TEXT,
|
|
status TEXT NOT NULL DEFAULT 'pending', -- pending|in_progress|completed|skipped
|
|
completed_by_id UUID REFERENCES contacts(id) ON DELETE SET NULL,
|
|
completed_at TIMESTAMPTZ,
|
|
notes 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()
|
|
);
|
|
```
|
|
|
|
### capture
|
|
Quick-capture inbox for unprocessed thoughts/items.
|
|
```sql
|
|
CREATE TABLE capture (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
raw_text TEXT NOT NULL,
|
|
processed BOOL NOT NULL DEFAULT false,
|
|
converted_to_type TEXT, -- task|note|project|contact|decision|weblink|list_item
|
|
converted_to_id UUID,
|
|
area_id UUID REFERENCES areas(id) ON DELETE SET NULL,
|
|
project_id UUID REFERENCES projects(id) ON DELETE SET NULL,
|
|
list_id UUID REFERENCES lists(id) ON DELETE SET NULL,
|
|
import_batch_id UUID,
|
|
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()
|
|
);
|
|
CREATE INDEX idx_capture_processed ON capture USING btree(processed);
|
|
```
|
|
|
|
### context_types
|
|
System-defined context labels for tasks (e.g., @home, @office, @errands).
|
|
```sql
|
|
CREATE TABLE context_types (
|
|
id SERIAL PRIMARY KEY,
|
|
value TEXT NOT NULL UNIQUE,
|
|
label TEXT NOT NULL,
|
|
description TEXT,
|
|
is_system BOOL NOT NULL DEFAULT true,
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
is_deleted BOOL NOT NULL DEFAULT false,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
```
|
|
|
|
### reminders
|
|
Future-scheduled reminders linked to any entity type.
|
|
```sql
|
|
CREATE TABLE reminders (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
entity_type TEXT NOT NULL,
|
|
entity_id UUID NOT NULL,
|
|
remind_at TIMESTAMPTZ NOT NULL,
|
|
note TEXT,
|
|
delivered BOOL NOT NULL DEFAULT false,
|
|
is_deleted BOOL NOT NULL DEFAULT false,
|
|
deleted_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX idx_reminders_entity ON reminders USING btree(entity_type, entity_id);
|
|
```
|
|
|
|
### dependencies
|
|
Generic dependency relationships between any entity types. Supports cycle detection (Phase 2).
|
|
```sql
|
|
CREATE TABLE dependencies (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
blocker_type TEXT NOT NULL,
|
|
blocker_id UUID NOT NULL,
|
|
dependent_type TEXT NOT NULL,
|
|
dependent_id UUID NOT NULL,
|
|
dependency_type TEXT NOT NULL DEFAULT 'finish_to_start',
|
|
lag_days INT NOT NULL DEFAULT 0,
|
|
note TEXT,
|
|
is_deleted BOOL NOT NULL DEFAULT false,
|
|
deleted_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
UNIQUE (blocker_type, blocker_id, dependent_type, dependent_id, dependency_type),
|
|
CHECK (NOT (blocker_type = dependent_type AND blocker_id = dependent_id))
|
|
);
|
|
CREATE INDEX idx_dependencies_blocker ON dependencies USING btree(blocker_type, blocker_id);
|
|
CREATE INDEX idx_dependencies_dependent ON dependencies USING btree(dependent_type, dependent_id);
|
|
```
|
|
|
|
### releases
|
|
Versioned release milestones spanning multiple projects.
|
|
```sql
|
|
CREATE TABLE releases (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name TEXT NOT NULL,
|
|
version_label TEXT,
|
|
description TEXT,
|
|
status TEXT NOT NULL DEFAULT 'planned', -- planned|in_progress|released|cancelled
|
|
target_date DATE,
|
|
released_at DATE,
|
|
release_notes TEXT,
|
|
tags TEXT[],
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
is_deleted BOOL NOT NULL DEFAULT false,
|
|
deleted_at TIMESTAMPTZ,
|
|
search_vector TSVECTOR,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
```
|
|
**Search trigger:** Indexes `name`, `description`, `version_label`, `tags`.
|
|
|
|
### milestones
|
|
Date-based checkpoints within releases or projects.
|
|
```sql
|
|
CREATE TABLE milestones (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
release_id UUID REFERENCES releases(id) ON DELETE SET NULL,
|
|
project_id UUID REFERENCES projects(id) ON DELETE SET NULL,
|
|
name TEXT NOT NULL,
|
|
target_date DATE NOT NULL,
|
|
completed_at DATE,
|
|
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()
|
|
);
|
|
```
|
|
|
|
### task_templates
|
|
Reusable task templates with preset fields.
|
|
```sql
|
|
CREATE TABLE task_templates (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
priority INT,
|
|
estimated_minutes INT,
|
|
energy_required TEXT,
|
|
context TEXT,
|
|
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()
|
|
);
|
|
```
|
|
|
|
### task_template_items
|
|
Individual sub-items within a task template.
|
|
```sql
|
|
CREATE TABLE task_template_items (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
template_id UUID NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
|
|
title TEXT NOT NULL,
|
|
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()
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## 10. Junction Tables (16 tables)
|
|
|
|
All junction tables use composite primary keys and CASCADE deletes on both sides.
|
|
|
|
```sql
|
|
-- Note <-> Project (many-to-many)
|
|
CREATE TABLE note_projects (
|
|
note_id UUID NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
|
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
is_primary BOOL NOT NULL DEFAULT false,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (note_id, project_id)
|
|
);
|
|
|
|
-- Note <-> Note (wiki-style links)
|
|
CREATE TABLE note_links (
|
|
source_note_id UUID NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
|
target_note_id UUID NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (source_note_id, target_note_id)
|
|
);
|
|
|
|
-- File <-> Any Entity (polymorphic mapping)
|
|
CREATE TABLE file_mappings (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
file_id UUID NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
context_type TEXT NOT NULL, -- 'task', 'project', 'note', etc.
|
|
context_id UUID NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
UNIQUE (file_id, context_type, context_id)
|
|
);
|
|
CREATE INDEX idx_file_mappings_context ON file_mappings USING btree(context_type, context_id);
|
|
|
|
-- Release <-> Project
|
|
CREATE TABLE release_projects (
|
|
release_id UUID NOT NULL REFERENCES releases(id) ON DELETE CASCADE,
|
|
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (release_id, project_id)
|
|
);
|
|
|
|
-- Release <-> Domain
|
|
CREATE TABLE release_domains (
|
|
release_id UUID NOT NULL REFERENCES releases(id) ON DELETE CASCADE,
|
|
domain_id UUID NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (release_id, domain_id)
|
|
);
|
|
|
|
-- Contact <-> Task
|
|
CREATE TABLE contact_tasks (
|
|
contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
role TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (contact_id, task_id)
|
|
);
|
|
|
|
-- Contact <-> Project
|
|
CREATE TABLE contact_projects (
|
|
contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
role TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (contact_id, project_id)
|
|
);
|
|
|
|
-- Contact <-> List
|
|
CREATE TABLE contact_lists (
|
|
contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
list_id UUID NOT NULL REFERENCES lists(id) ON DELETE CASCADE,
|
|
role TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (contact_id, list_id)
|
|
);
|
|
|
|
-- Contact <-> List Item
|
|
CREATE TABLE contact_list_items (
|
|
contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
list_item_id UUID NOT NULL REFERENCES list_items(id) ON DELETE CASCADE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (contact_id, list_item_id)
|
|
);
|
|
|
|
-- Contact <-> Appointment
|
|
CREATE TABLE contact_appointments (
|
|
contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
appointment_id UUID NOT NULL REFERENCES appointments(id) ON DELETE CASCADE,
|
|
role TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (contact_id, appointment_id)
|
|
);
|
|
|
|
-- Contact <-> Meeting
|
|
CREATE TABLE contact_meetings (
|
|
contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
meeting_id UUID NOT NULL REFERENCES meetings(id) ON DELETE CASCADE,
|
|
role TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (contact_id, meeting_id)
|
|
);
|
|
|
|
-- Decision <-> Project
|
|
CREATE TABLE decision_projects (
|
|
decision_id UUID NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
|
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (decision_id, project_id)
|
|
);
|
|
|
|
-- Decision <-> Contact
|
|
CREATE TABLE decision_contacts (
|
|
decision_id UUID NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
|
contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
role TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (decision_id, contact_id)
|
|
);
|
|
|
|
-- Meeting <-> Task (action items)
|
|
CREATE TABLE meeting_tasks (
|
|
meeting_id UUID NOT NULL REFERENCES meetings(id) ON DELETE CASCADE,
|
|
task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
source TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (meeting_id, task_id)
|
|
);
|
|
|
|
-- Process Run Step <-> Task
|
|
CREATE TABLE process_run_tasks (
|
|
run_step_id UUID NOT NULL REFERENCES process_run_steps(id) ON DELETE CASCADE,
|
|
task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (run_step_id, task_id)
|
|
);
|
|
|
|
-- Folder <-> Weblink
|
|
CREATE TABLE folder_weblinks (
|
|
folder_id UUID NOT NULL REFERENCES weblink_folders(id) ON DELETE CASCADE,
|
|
weblink_id UUID NOT NULL REFERENCES weblinks(id) ON DELETE CASCADE,
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (folder_id, weblink_id)
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## 11. Database Functions & Triggers
|
|
|
|
### Search Vector Update Functions
|
|
|
|
15 entity-specific search functions, each following the same pattern. The function builds a tsvector from relevant text columns and stores it in `search_vector`.
|
|
|
|
**Pattern:**
|
|
```sql
|
|
CREATE OR REPLACE FUNCTION update_{table}_search() RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.search_vector := to_tsvector('pg_catalog.english',
|
|
coalesce(NEW.{col1}, '') || ' ' || coalesce(NEW.{col2}, '') || ' ' ||
|
|
coalesce(array_to_string(NEW.tags, ' '), ''));
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER trg_{table}_search
|
|
BEFORE INSERT OR UPDATE ON {table}
|
|
FOR EACH ROW EXECUTE FUNCTION update_{table}_search();
|
|
```
|
|
|
|
**Per-table indexed columns:**
|
|
|
|
| Function | Table | Indexed Columns |
|
|
|----------|-------|----------------|
|
|
| `update_domains_search` | domains | name |
|
|
| `update_areas_search` | areas | name, description |
|
|
| `update_projects_search` | projects | name, description, tags |
|
|
| `update_tasks_search` | tasks | title, description, tags |
|
|
| `update_notes_search` | notes | title, body, tags |
|
|
| `update_contacts_search` | contacts | first_name, last_name, company, email, tags |
|
|
| `update_meetings_search` | meetings | title, agenda, notes_body, tags |
|
|
| `update_decisions_search` | decisions | title, rationale, tags |
|
|
| `update_links_search` | links | label, url, description, tags |
|
|
| `update_weblinks_search` | weblinks | label, url, description, tags |
|
|
| `update_files_search` | files | original_filename, description, tags |
|
|
| `update_lists_search` | lists | name, description, tags |
|
|
| `update_appointments_search` | appointments | title, description, location, tags |
|
|
| `update_processes_search` | processes | name, description, tags |
|
|
| `update_releases_search` | releases | name, description, version_label, tags |
|
|
|
|
There is also a generic `update_search_vector()` function as a fallback that attempts to index `title`, `description`, `name`, and `tags` with exception handling for missing columns.
|
|
|
|
---
|
|
|
|
## 12. Entity Relationship Map
|
|
|
|
```
|
|
domains (1)─────────────┬──────(M) areas
|
|
│ │ │
|
|
│ │ └──(M) projects ◄──── areas.area_id
|
|
│ │ │
|
|
├──(M) projects │ ├──(M) tasks
|
|
├──(M) tasks │ ├──(M) notes
|
|
├──(M) notes │ ├──(M) links
|
|
├──(M) links │ ├──(M) lists
|
|
├──(M) lists │ └──(M) milestones
|
|
└──(M) time_budgets │
|
|
│
|
|
tasks ──────────────────┬──(M) time_entries
|
|
│ ├──(M) daily_focus
|
|
│ ├──(M) notes (via task_id)
|
|
│ ├──(M) lists (via task_id)
|
|
│ ├──(M) weblinks (via task_id)
|
|
│ ├──(M) decisions (via task_id)
|
|
│ ├──(M) subtasks (self-ref via parent_id)
|
|
│ └──(M) time_blocks
|
|
│
|
|
contacts ───────────────┬──(M) contact_tasks ──── tasks
|
|
│ ├──(M) contact_projects ── projects
|
|
│ ├──(M) contact_meetings ── meetings
|
|
│ ├──(M) contact_appointments ── appointments
|
|
│ ├──(M) contact_lists ──── lists
|
|
│ └──(M) decision_contacts ── decisions
|
|
│
|
|
meetings ───────────────┬──(M) meeting_tasks ──── tasks
|
|
│ ├──(M) contact_meetings ── contacts
|
|
│ ├──(M) notes (via meeting_id)
|
|
│ ├──(M) lists (via meeting_id)
|
|
│ ├──(M) weblinks (via meeting_id)
|
|
│ └──(M) decisions (via meeting_id)
|
|
│
|
|
processes ──────────────┬──(M) process_steps
|
|
│ └──(M) process_runs
|
|
│ ├──(M) process_run_steps
|
|
│ └──(M) process_run_tasks ── tasks
|
|
│
|
|
files ──────────────────── file_mappings ──── (polymorphic: any entity)
|
|
│
|
|
releases ───────────────┬──(M) release_projects ── projects
|
|
│ ├──(M) release_domains ── domains
|
|
│ ├──(M) tasks (via release_id)
|
|
│ └──(M) milestones
|
|
```
|
|
|
|
---
|
|
|
|
# Part 4: Backend Application
|
|
|
|
## 13. Application Bootstrap (main.py)
|
|
|
|
### Initialization Flow
|
|
|
|
```python
|
|
# 1. Load environment variables
|
|
load_dotenv()
|
|
|
|
# 2. Create FastAPI app with lifespan
|
|
app = FastAPI(title="Life OS", version="1.0.0", lifespan=lifespan)
|
|
|
|
# 3. Mount static files
|
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
|
|
# 4. Add ASGI middleware for environment context
|
|
app.add_middleware(RequestContextMiddleware)
|
|
|
|
# 5. Define dashboard route (/)
|
|
# 6. Define health check (/health)
|
|
# 7. Include all 23 routers
|
|
```
|
|
|
|
### Lifespan Handler
|
|
```python
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
ok = await check_db() # Verify DB connectivity at startup
|
|
print("Database connection OK" if ok else "WARNING: Database check failed")
|
|
yield
|
|
```
|
|
|
|
### RequestContextMiddleware
|
|
Pure ASGI middleware (not BaseHTTPMiddleware, which causes TaskGroup issues with asyncpg):
|
|
```python
|
|
class RequestContextMiddleware:
|
|
async def __call__(self, scope, receive, send):
|
|
if scope["type"] == "http":
|
|
scope.setdefault("state", {})["environment"] = self.environment
|
|
await self.app(scope, receive, send)
|
|
```
|
|
|
|
### Dashboard Route (/)
|
|
The dashboard queries:
|
|
1. Today's focus items (daily_focus JOIN tasks, projects, domains)
|
|
2. Overdue tasks (due_date < today, not done/cancelled, limit 10)
|
|
3. Upcoming tasks (due in next 7 days, limit 10)
|
|
4. Task stats (open count, done this week, in progress)
|
|
|
|
### Router Registration Order
|
|
23 routers included in this order:
|
|
1. domains, areas, projects, tasks, notes, links
|
|
2. focus, capture, contacts, search, admin, lists
|
|
3. files, meetings, decisions, weblinks
|
|
4. appointments, time_tracking, processes, calendar
|
|
5. time_budgets, eisenhower, history
|
|
|
|
---
|
|
|
|
## 14. Core Modules
|
|
|
|
### database.py - Connection Management
|
|
|
|
**Engine Configuration:**
|
|
```python
|
|
engine = create_async_engine(
|
|
DATABASE_URL,
|
|
echo=(ENVIRONMENT == "development"), # SQL logging in dev
|
|
pool_size=5,
|
|
max_overflow=10,
|
|
pool_pre_ping=True, # Verify connections before use
|
|
)
|
|
```
|
|
|
|
**Session Factory:**
|
|
```python
|
|
async_session_factory = async_sessionmaker(
|
|
engine, class_=AsyncSession, expire_on_commit=False,
|
|
)
|
|
```
|
|
|
|
**get_db() Dependency:**
|
|
FastAPI dependency that yields an async session with auto-commit/rollback:
|
|
```python
|
|
async def get_db():
|
|
async with async_session_factory() as session:
|
|
try:
|
|
yield session
|
|
await session.commit()
|
|
except Exception:
|
|
await session.rollback()
|
|
raise
|
|
```
|
|
|
|
### base_repository.py - Generic CRUD
|
|
|
|
The `BaseRepository` class provides all CRUD operations for any table. It uses raw SQL via `text()` - no ORM models.
|
|
|
|
**Constructor:** `BaseRepository(table_name: str, db: AsyncSession)`
|
|
|
|
**Methods:**
|
|
|
|
| Method | Signature | Behavior |
|
|
|--------|-----------|----------|
|
|
| `list()` | `(filters, sort, sort_dir, page, per_page, include_deleted)` | SELECT with auto `is_deleted=false` filter. Supports pagination, sorting, and key-value filters. Special filter values: `None` → `IS NULL`, `"__notnull__"` → `IS NOT NULL`. |
|
|
| `count()` | `(filters, include_deleted)` | COUNT with same filter logic |
|
|
| `get()` | `(id: UUID)` | SELECT by ID. Validates UUID format. Returns dict or None. |
|
|
| `create()` | `(data: dict)` | INSERT with auto `created_at`, `updated_at`, `is_deleted=false`. Coerces ISO date strings to Python date/datetime (required by asyncpg). Returns full row. |
|
|
| `update()` | `(id: UUID, data: dict)` | UPDATE with auto `updated_at`. **Nullable fields gotcha:** Only fields listed in `nullable_fields` set can be set to NULL. All other NULL values are silently skipped. |
|
|
| `soft_delete()` | `(id: UUID)` | Sets `is_deleted=true`, `deleted_at=now()`, `updated_at=now()` |
|
|
| `restore()` | `(id: UUID)` | Sets `is_deleted=false`, `deleted_at=NULL`, `updated_at=now()` |
|
|
| `permanent_delete()` | `(id: UUID)` | Actual SQL DELETE. Admin only. |
|
|
| `bulk_soft_delete()` | `(ids: list[str])` | Batch soft delete |
|
|
| `list_deleted()` | `()` | Lists all soft-deleted rows, ordered by deleted_at DESC |
|
|
| `reorder()` | `(id_order: list[str])` | Updates sort_order based on position (10, 20, 30...) |
|
|
|
|
**Nullable Fields Set:**
|
|
```python
|
|
nullable_fields = {
|
|
"description", "notes", "body", "area_id", "project_id",
|
|
"parent_id", "parent_item_id", "release_id", "due_date", "deadline", "tags",
|
|
"context", "folder_id", "meeting_id", "completed_at",
|
|
"waiting_for_contact_id", "waiting_since", "color",
|
|
"rationale", "decided_at", "superseded_by_id",
|
|
"start_at", "end_at", "location", "agenda", "transcript", "notes_body",
|
|
"priority", "recurrence", "mime_type",
|
|
"category", "instructions", "expected_output", "estimated_days",
|
|
"contact_id", "started_at",
|
|
"weekly_hours", "effective_from",
|
|
"task_id", "meeting_id",
|
|
}
|
|
```
|
|
|
|
**Date Coercion:**
|
|
asyncpg requires native Python `date`/`datetime` objects, not strings. The `_coerce_value()` helper auto-converts ISO format strings (`YYYY-MM-DD`, `YYYY-MM-DDTHH:MM`) to Python types.
|
|
|
|
### sidebar.py - Navigation Data
|
|
|
|
Builds the domain > area > project tree for the sidebar. Called by every route.
|
|
|
|
```python
|
|
async def get_sidebar_data(db: AsyncSession) -> dict:
|
|
# Returns:
|
|
{
|
|
"domain_tree": [
|
|
{
|
|
"id": UUID, "name": str, "color": str,
|
|
"areas": [
|
|
{"id": UUID, "name": str, "projects": [...]}
|
|
],
|
|
"standalone_projects": [...] # Projects with no area
|
|
}
|
|
],
|
|
"capture_count": int, # Unprocessed capture items
|
|
"focus_count": int, # Incomplete focus items for today
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 15. Router Pattern
|
|
|
|
Every router follows this exact structure:
|
|
|
|
```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')
|
|
|
|
# LIST - GET /things/
|
|
@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,
|
|
'page_title': 'Things', 'active_nav': 'things',
|
|
})
|
|
|
|
# CREATE FORM - GET /things/create
|
|
@router.get('/create')
|
|
async def create_form(request: Request, db=Depends(get_db)):
|
|
sidebar = await get_sidebar_data(db)
|
|
return templates.TemplateResponse('thing_form.html', {
|
|
'request': request, 'item': None, 'sidebar': sidebar,
|
|
})
|
|
|
|
# CREATE - POST /things/create
|
|
@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)
|
|
|
|
# DETAIL - GET /things/{id}
|
|
@router.get('/{thing_id}')
|
|
async def detail(request: Request, thing_id: str, db=Depends(get_db)):
|
|
repo = BaseRepository("things", db)
|
|
item = await repo.get(thing_id)
|
|
if not item:
|
|
return RedirectResponse(url='/things', status_code=303)
|
|
sidebar = await get_sidebar_data(db)
|
|
return templates.TemplateResponse('thing_detail.html', {
|
|
'request': request, 'item': item, 'sidebar': sidebar,
|
|
})
|
|
|
|
# EDIT FORM - GET /things/{id}/edit
|
|
@router.get('/{thing_id}/edit')
|
|
async def edit_form(request: Request, thing_id: str, db=Depends(get_db)):
|
|
repo = BaseRepository("things", db)
|
|
item = await repo.get(thing_id)
|
|
sidebar = await get_sidebar_data(db)
|
|
return templates.TemplateResponse('thing_form.html', {
|
|
'request': request, 'item': item, 'sidebar': sidebar,
|
|
})
|
|
|
|
# EDIT - POST /things/{id}/edit
|
|
@router.post('/{thing_id}/edit')
|
|
async def edit_thing(request: Request, thing_id: str, db=Depends(get_db),
|
|
title: str = Form(...), description: str = Form(None)):
|
|
repo = BaseRepository("things", db)
|
|
await repo.update(thing_id, {'title': title, 'description': description})
|
|
return RedirectResponse(url=f'/things/{thing_id}', status_code=303)
|
|
|
|
# DELETE - POST /things/{id}/delete
|
|
@router.post('/{thing_id}/delete')
|
|
async def delete_thing(request: Request, thing_id: str, db=Depends(get_db)):
|
|
repo = BaseRepository("things", db)
|
|
await repo.soft_delete(thing_id)
|
|
return RedirectResponse(url='/things', status_code=303)
|
|
```
|
|
|
|
**Every route MUST call `get_sidebar_data(db)` and pass `sidebar` to the template.**
|
|
|
|
---
|
|
|
|
## 16. Router Reference (23 routers)
|
|
|
|
### Standard CRUD Routers
|
|
|
|
These follow the exact pattern above (list, create form, create, detail, edit form, edit, delete):
|
|
|
|
| Router | Prefix | Table | Extra Endpoints |
|
|
|--------|--------|-------|-----------------|
|
|
| domains | /domains | domains | - |
|
|
| areas | /areas | areas | - |
|
|
| projects | /projects | projects | `GET /api/by-domain` (JSON, for dynamic dropdowns) |
|
|
| notes | /notes | notes | - |
|
|
| links | /links | links | - |
|
|
| contacts | /contacts | contacts | - |
|
|
| lists | /lists | lists | `POST /{id}/items/add`, `POST /items/{id}/toggle`, `POST /items/{id}/delete` |
|
|
| meetings | /meetings | meetings | `POST /{id}/create-action` (create task from meeting) |
|
|
| decisions | /decisions | decisions | - |
|
|
| weblinks | /weblinks | weblinks | `POST /folders/create`, folder management |
|
|
| appointments | /appointments | appointments | - |
|
|
| processes | /processes | processes | `POST /{id}/steps/add`, `POST /steps/{id}/edit`, `POST /{id}/start-run` |
|
|
| time_budgets | /time-budgets | time_budgets | - |
|
|
|
|
### Specialized Routers
|
|
|
|
**tasks** (`/tasks`) - Most complex router:
|
|
- Standard CRUD (list, create, detail, edit, delete)
|
|
- `POST /quick-add` - Minimal task creation (title + domain_id only)
|
|
- `POST /{id}/toggle` - Toggle done/open status
|
|
- `POST /{id}/complete` - Mark as done with completed_at
|
|
- `GET /` with filters: status, priority, domain_id, project_id, sort, context
|
|
- Detail page with tabs: overview, notes, weblinks, files, lists, decisions, contacts
|
|
- `POST /{id}/contacts/add` - Link contact to task
|
|
- `POST /{id}/contacts/{contact_id}/remove` - Unlink contact
|
|
|
|
**focus** (`/focus`) - Daily focus management:
|
|
- `GET /` - Show today's focus + available tasks
|
|
- `POST /add` - Add task to focus (task_id + focus_date)
|
|
- `POST /{id}/toggle` - Toggle focus item complete
|
|
- `POST /{id}/remove` - Remove from focus
|
|
|
|
**capture** (`/capture`) - Quick capture inbox:
|
|
- `GET /` - List unprocessed captures
|
|
- `POST /add` - Create capture (raw_text, supports multi-line splitting)
|
|
- `GET /{id}/convert` - Show conversion options
|
|
- `POST /{id}/convert/{type}` - Convert to task/note/project/contact/decision/weblink/list_item
|
|
- `POST /{id}/dismiss` - Mark as processed without converting
|
|
- `POST /batch/{batch_id}/undo` - Undo batch conversion
|
|
|
|
**search** (`/search`) - Global search:
|
|
- `GET /` - Search page with HTML results
|
|
- `GET /api` - JSON search API for modal (returns `{results: [...]}`)
|
|
- Searches across configurable `SEARCH_ENTITIES` dict
|
|
- Uses tsvector/tsquery with `plainto_tsquery()` for safety
|
|
|
|
**admin** (`/admin`) - Trash management:
|
|
- `GET /trash` - List soft-deleted items across all entities
|
|
- `POST /trash/{table}/{id}/restore` - Restore soft-deleted item
|
|
- `POST /trash/{table}/{id}/permanent-delete` - Hard delete
|
|
- `POST /trash/empty` - Empty all trash
|
|
|
|
**time_tracking** (`/time`) - Timer and time log:
|
|
- `GET /` - Time log page with day filter
|
|
- `POST /start` - Start timer for task (auto-stops any running timer)
|
|
- `POST /stop` - Stop running timer
|
|
- `POST /manual` - Create manual time entry
|
|
- `GET /running` - JSON endpoint returning running timer state
|
|
- `POST /{id}/delete` - Delete time entry (uses direct SQL, no BaseRepository)
|
|
|
|
**calendar** (`/calendar`) - Read-only unified calendar:
|
|
- `GET /` - Month view combining appointments, meetings, and tasks with due dates
|
|
- Query params: `year`, `month`
|
|
|
|
**eisenhower** (`/eisenhower`) - Eisenhower matrix view:
|
|
- `GET /` - 2x2 grid classifying tasks by priority (1-2=important) and urgency (due <=7 days)
|
|
- Query params: `domain_id`, `project_id`, `status`, `context`
|
|
|
|
**history** (`/history`) - Change history feed:
|
|
- `GET /` - Reverse-chronological feed of recently modified items across all entities
|
|
- Query params: `entity_type` filter
|
|
|
|
---
|
|
|
|
## 17. Business Logic
|
|
|
|
### Timer Constraints
|
|
- **Only one timer can run at a time.** Starting a new timer auto-stops any running timer.
|
|
- Running timer detected by: `SELECT * FROM time_entries WHERE end_at IS NULL AND is_deleted = false`
|
|
- Stop sets `end_at = now()` and computes `duration_minutes`
|
|
- The topbar timer pill polls `/time/running` every 30 seconds
|
|
|
|
### Capture Conversions
|
|
- Multi-line `raw_text` is split on newlines, creating multiple capture items
|
|
- Each line in a batch gets the same `import_batch_id`
|
|
- Single-line captures get no batch_id
|
|
- Conversion creates the target entity and marks capture as `processed=true`
|
|
- Batch undo reverses all conversions for a batch_id
|
|
|
|
### Task Status Transitions
|
|
- Toggle: `open` ↔ `done` (sets/clears `completed_at`)
|
|
- Complete action: Sets `status=done`, `completed_at=now()`
|
|
- Edit can change to any valid status
|
|
- Setting status to `done` auto-sets `completed_at`
|
|
- Setting status from `done` to anything else clears `completed_at`
|
|
|
|
### Focus Sync
|
|
- Adding a task to daily_focus doesn't change its status
|
|
- Toggling a focus item as complete marks `completed=true` on the daily_focus row
|
|
- Focus items for today are shown on the dashboard
|
|
|
|
### Search Algorithm
|
|
- Uses PostgreSQL `plainto_tsquery('english', query)` for safe query parsing
|
|
- Searches `search_vector @@ query` with tsvector ranking
|
|
- Results grouped by entity type with configurable display columns
|
|
- No special characters interpreted (safe from injection)
|
|
|
|
---
|
|
|
|
## 18. Key Enums & Constants
|
|
|
|
| Field | Valid Values | Default |
|
|
|-------|-------------|---------|
|
|
| Task status | `open`, `in_progress`, `blocked`, `done`, `cancelled` | `open` |
|
|
| Task priority | `1` (critical), `2` (high), `3` (normal), `4` (low) | `3` |
|
|
| Project status | `active`, `on_hold`, `completed`, `archived` | `active` |
|
|
| Meeting status | `scheduled`, `completed`, `cancelled` | `scheduled` |
|
|
| Decision status | `proposed`, `accepted`, `rejected`, `superseded` | `proposed` |
|
|
| Decision impact | `low`, `medium`, `high` | `medium` |
|
|
| Process type | `workflow`, `checklist` | `checklist` |
|
|
| Process status | `draft`, `active`, `archived` | `draft` |
|
|
| Process run status | `not_started`, `in_progress`, `completed`, `cancelled` | `not_started` |
|
|
| Run step status | `pending`, `in_progress`, `completed`, `skipped` | `pending` |
|
|
| Task generation | `all_at_once`, `step_by_step` | `all_at_once` |
|
|
| Release status | `planned`, `in_progress`, `released`, `cancelled` | `planned` |
|
|
| Area status | `active` (default) | `active` |
|
|
| Content format | `rich` (default) | `rich` |
|
|
|
|
---
|
|
|
|
# Part 5: Frontend
|
|
|
|
## 19. Template Architecture
|
|
|
|
All templates live in `/opt/lifeos/dev/templates/` and extend `base.html`.
|
|
|
|
### base.html Shell Structure
|
|
```
|
|
<html data-theme="dark">
|
|
<head>
|
|
<link rel="stylesheet" href="/static/style.css">
|
|
<link href="fonts.googleapis.com/css2?DM+Sans+JetBrains+Mono">
|
|
</head>
|
|
<body>
|
|
<div class="app-layout">
|
|
<aside class="sidebar">
|
|
<div class="sidebar-header">LifeOS logo</div>
|
|
<nav class="sidebar-nav">
|
|
[14 quick-access links with SVG icons]
|
|
[Domain tree: domain > area > project hierarchy]
|
|
[Admin section: History, Trash, Manage, Theme toggle]
|
|
</nav>
|
|
</aside>
|
|
<main class="main-content">
|
|
<header class="topbar">
|
|
[DEV badge if development]
|
|
[Timer pill (hidden by default)]
|
|
[Quick capture form]
|
|
[Search trigger button]
|
|
</header>
|
|
<div class="page-content">
|
|
{% block content %}{% endblock %}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
[Search modal overlay]
|
|
[Mobile bottom nav bar]
|
|
[Mobile more panel overlay]
|
|
<script src="/static/app.js"></script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
### Template Inventory (51 files)
|
|
|
|
**Core:** base.html, dashboard.html
|
|
**List views (18):** tasks, notes, projects, contacts, areas, domains, links, lists, meetings, decisions, weblinks, appointments, time_entries, files, processes, process_runs, capture, time_budgets
|
|
**Detail views (9):** task_detail, project_detail, note_detail, contact_detail, meeting_detail, appointment_detail, decision_detail, process_run_detail, list_detail
|
|
**Forms (15):** task_form, note_form, project_form, contact_form, area_form, domain_form, link_form, list_form, meeting_form, decision_form, weblink_form, weblink_folder_form, appointment_form, processes_form, time_budgets_form
|
|
**Specialized (7):** focus, calendar, eisenhower, search, trash, history, capture_convert
|
|
**File management (2):** file_preview, file_upload
|
|
|
|
---
|
|
|
|
## 20. CSS Design System
|
|
|
|
### Design Tokens (Custom Properties)
|
|
|
|
**Dark Theme (`[data-theme="dark"]`):**
|
|
```css
|
|
--bg: #0D0E13; --surface: #14161F; --surface2: #1A1D28;
|
|
--surface3: #222533; --border: #252836; --border-light: #2E3244;
|
|
--text: #DDE1F5; --text-secondary: #9CA3C4; --muted: #5A6080;
|
|
--accent: #4F6EF7; --accent-hover: #6380FF; --accent-soft: rgba(79,110,247,.12);
|
|
--green: #22C98A; --green-soft: rgba(34,201,138,.12);
|
|
--amber: #F5A623; --amber-soft: rgba(245,166,35,.12);
|
|
--red: #F05252; --red-soft: rgba(240,82,82,.12);
|
|
--purple: #9B7FF5; --purple-soft: rgba(155,127,245,.12);
|
|
--shadow: 0 2px 8px rgba(0,0,0,.3);
|
|
--shadow-lg: 0 8px 32px rgba(0,0,0,.4);
|
|
```
|
|
|
|
**Light Theme (`[data-theme="light"]`):**
|
|
```css
|
|
--bg: #F0F2F8; --surface: #FFFFFF; --surface2: #F7F8FC;
|
|
--text: #171926; --muted: #8892B0;
|
|
/* Same accent/semantic colors with adjusted soft variants */
|
|
```
|
|
|
|
**Root Variables (theme-independent):**
|
|
```css
|
|
--font-body: 'DM Sans', -apple-system, sans-serif;
|
|
--font-mono: 'JetBrains Mono', monospace;
|
|
--radius: 8px; --radius-sm: 4px; --radius-lg: 12px;
|
|
--sidebar-width: 260px; --topbar-height: 52px;
|
|
--transition: 150ms ease;
|
|
```
|
|
|
|
### Color Usage by Context
|
|
|
|
| Context | Color |
|
|
|---------|-------|
|
|
| Priority 1 (Critical) | `--red` |
|
|
| Priority 2 (High) | `--amber` |
|
|
| Priority 3 (Normal) | `--accent` |
|
|
| Priority 4 (Low) | `--muted` |
|
|
| Status: open | `--accent` |
|
|
| Status: in_progress | `--amber` |
|
|
| Status: blocked | `--red` |
|
|
| Status: done/completed | `--green` |
|
|
| Status: cancelled/archived | `--muted` |
|
|
| Status: on_hold | `--purple` |
|
|
| Calendar: appointments | `--amber` |
|
|
| Calendar: meetings | `--purple` |
|
|
| Calendar: tasks | `--accent` |
|
|
|
|
### Responsive Breakpoint
|
|
|
|
**Single breakpoint: `max-width: 768px`**
|
|
|
|
Mobile overrides:
|
|
- Sidebar: `display: none`
|
|
- Main content: `margin-left: 0`
|
|
- Form grid: 1 column
|
|
- Dashboard grid: 1 column
|
|
- Page content: `padding: 16px`
|
|
- Mobile bottom nav: `display: flex` (56px fixed bottom)
|
|
- Body: `padding-bottom: 60px`
|
|
|
|
---
|
|
|
|
## 21. JavaScript Interactions (app.js, ~190 lines)
|
|
|
|
All JS runs on `DOMContentLoaded`. No framework. No build step.
|
|
|
|
### Theme Toggle
|
|
- Reads/saves theme to `localStorage['lifeos-theme']`
|
|
- Sets `data-theme` attribute on `<html>`
|
|
- Toggle function called from sidebar button
|
|
|
|
### Sidebar Domain Collapse
|
|
- Click `.domain-header` → toggle `.collapsed` on `.domain-children`
|
|
- State persisted per domain in `localStorage['sidebar-{domainId}']`
|
|
- Restored on page load
|
|
|
|
### Filter Auto-Submit
|
|
- Any `<select data-auto-submit>` → on `change`, submits parent `<form>`
|
|
|
|
### Delete Confirmation
|
|
- Any `<form data-confirm="message">` → on `submit`, shows `confirm()` dialog
|
|
|
|
### Search Modal
|
|
- **Open:** Cmd/Ctrl+K or click search trigger
|
|
- **Close:** Escape key or click backdrop
|
|
- **Live search:** 200ms debounce, fetches `/search/api?q={query}&limit=8`
|
|
- **Navigation:** Arrow keys move `.active` class through results, Enter navigates
|
|
- **Results:** Grouped by entity type, shows name + context + status badge
|
|
- **HTML escaping:** `escHtml()` helper for safe rendering
|
|
|
|
### Timer Pill
|
|
- **Poll:** `/time/running` every 30 seconds
|
|
- **Display:** If running, shows task name + elapsed time (h:mm:ss)
|
|
- **Update:** 1-second interval updates elapsed display client-side
|
|
- **State:** `timerStartAt` (Date), `timerInterval` (setInterval ID)
|
|
|
|
### Mobile More Panel
|
|
- `#mobMoreBtn` click → toggle `.open` on `#mobMore` + `#mobOverlay`
|
|
- Overlay click → close panel
|
|
|
|
---
|
|
|
|
## 22. Page Patterns
|
|
|
|
### List View Pattern
|
|
```
|
|
Page Header (title + count + "New" button)
|
|
└── Quick Add form (optional, for tasks)
|
|
└── Filters bar (select dropdowns with auto-submit)
|
|
└── Card
|
|
└── List rows (checkbox + priority dot + title + tags + meta + status + actions)
|
|
└── Empty state (if no items)
|
|
```
|
|
|
|
### Form View Pattern
|
|
```
|
|
Breadcrumb (entity list > "New"/"Edit")
|
|
└── Page Header
|
|
└── Card
|
|
└── Form (grid: 2 columns on desktop, 1 on mobile)
|
|
└── Form groups (label + input/select/textarea)
|
|
└── Full-width fields (description, tags)
|
|
└── Form actions (Submit + Cancel)
|
|
```
|
|
|
|
### Detail View Pattern
|
|
```
|
|
Breadcrumb (domain > project > item)
|
|
└── Detail Header (title + action buttons)
|
|
└── Detail Meta (status + priority + domain + project + dates)
|
|
└── Description card (if exists)
|
|
└── Tags row (if exists)
|
|
└── Tab Strip (overview, notes, weblinks, files, lists, etc.)
|
|
└── Tab Content (varies by tab)
|
|
└── Footer metadata (created/completed timestamps)
|
|
```
|
|
|
|
---
|
|
|
|
## 23. Responsive & Mobile
|
|
|
|
### Desktop Layout
|
|
- Fixed sidebar (260px) + flexible main content
|
|
- Sticky topbar (52px)
|
|
- Two-column form grids
|
|
- Hover states reveal action buttons on list rows
|
|
|
|
### Mobile Layout (< 768px)
|
|
- Sidebar hidden
|
|
- Bottom nav bar (56px) with 5 items: Home, Focus, Tasks, Capture, More
|
|
- "More" button opens slide-up panel with remaining navigation
|
|
- Forms collapse to single column
|
|
- Quick capture input expands to full width
|
|
- Font-size: 16px on inputs to prevent iOS zoom
|
|
- Touch targets: min 44px height for buttons
|
|
|
|
---
|
|
|
|
# Part 6: Testing
|
|
|
|
## 24. Test Architecture
|
|
|
|
The test suite uses **dynamic introspection** - it discovers routes from the live FastAPI app at runtime. No hardcoded routes.
|
|
|
|
### Test Files
|
|
|
|
| File | Lines | Purpose |
|
|
|------|-------|---------|
|
|
| `conftest.py` | 361 | Fixtures, seed data, client setup |
|
|
| `registry.py` | 67 | Route discovery, classification, seed mapping |
|
|
| `introspect.py` | 357 | Route introspection engine |
|
|
| `form_factory.py` | 204 | Automatic form data generation |
|
|
| `test_smoke_dynamic.py` | 76 | Auto-parametrized GET tests |
|
|
| `test_crud_dynamic.py` | 168 | Auto-parametrized POST tests |
|
|
| `test_business_logic.py` | 2,635 | Hand-written behavioral tests |
|
|
| `route_report.py` | 65 | CLI route dump utility |
|
|
| `run_tests.sh` | 23 | Test runner script |
|
|
|
|
### Introspection Engine (introspect.py)
|
|
|
|
Walks the live FastAPI app and extracts metadata for every route:
|
|
- Path, methods, endpoint name
|
|
- Route kind classification (LIST, DETAIL, CREATE, EDIT, DELETE, TOGGLE, ACTION, etc.)
|
|
- Form fields with names, types, required/optional status, defaults
|
|
- Query parameters
|
|
- File upload detection
|
|
|
|
### Route Registry (registry.py)
|
|
|
|
Classifies routes into buckets for parametrized tests:
|
|
- `GET_NO_PARAMS`: All GET routes without path parameters
|
|
- `GET_WITH_PARAMS`: All GET routes with `{id}` placeholders
|
|
- `POST_CREATE`: Routes ending in `/create`
|
|
- `POST_EDIT`: Routes ending in `/{id}/edit`
|
|
- `POST_DELETE`: Routes ending in `/{id}/delete`
|
|
- `POST_ACTION`: All other POST routes
|
|
|
|
Maps route prefixes to seed data via `PREFIX_TO_SEED`.
|
|
|
|
### Form Factory (form_factory.py)
|
|
|
|
Generates valid POST form data automatically from introspected Form fields using multi-level heuristics:
|
|
1. FK fields → seed UUIDs
|
|
2. Date/time fields → generated dynamically
|
|
3. Field name patterns → 40+ hardcoded values
|
|
4. Type-based fallback
|
|
5. Required string fallback
|
|
|
|
### Seed Data (conftest.py)
|
|
|
|
Session-scoped fixture inserts 19 fixed-UUID entities:
|
|
- Core: domain, area, project, task
|
|
- CRM: contact, meeting, decision, appointment
|
|
- Content: note, list, link, weblink, weblink_folder
|
|
- System: capture, focus, process, process_step, process_run, time_budget, file
|
|
|
|
Fixed UUIDs (e.g., `a0000000-0000-0000-0000-000000000001`) ensure stable references.
|
|
|
|
---
|
|
|
|
## 25. Test Suite Reference
|
|
|
|
### Test Types
|
|
|
|
**Smoke Tests** (`test_smoke_dynamic.py`):
|
|
- Verify all GET endpoints return 200
|
|
- Test detail routes with valid and invalid UUIDs
|
|
- Auto-parametrized from introspected routes
|
|
|
|
**CRUD Tests** (`test_crud_dynamic.py`):
|
|
- Verify POST create/edit/delete routes redirect (303)
|
|
- Auto-generate form data via form_factory
|
|
- Test order: create → edit → action → delete
|
|
|
|
**Business Logic Tests** (`test_business_logic.py`):
|
|
- 10+ test classes, ~50 tests total
|
|
- Timer constraints, task filtering, status transitions
|
|
- Capture conversions, meeting relationships
|
|
- Admin trash lifecycle, soft delete behavior
|
|
- Search behavior, sidebar integrity, focus workflow
|
|
|
|
### Running Tests
|
|
|
|
```bash
|
|
docker exec lifeos-dev bash /app/tests/run_tests.sh # Full suite
|
|
docker exec lifeos-dev bash /app/tests/run_tests.sh smoke # GET endpoints
|
|
docker exec lifeos-dev bash /app/tests/run_tests.sh crud # POST endpoints
|
|
docker exec lifeos-dev bash /app/tests/run_tests.sh logic # Business logic
|
|
docker exec lifeos-dev bash /app/tests/run_tests.sh report # Route dump
|
|
docker exec lifeos-dev bash /app/tests/run_tests.sh fast # Smoke, stop first fail
|
|
docker exec lifeos-dev bash /app/tests/run_tests.sh -k "timer" # Keyword filter
|
|
```
|
|
|
|
### Resetting Test DB After Schema Changes
|
|
|
|
```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
|
|
```
|
|
|
|
---
|
|
|
|
# Part 7: Operations
|
|
|
|
## 26. Deployment & Hot Reload
|
|
|
|
The dev container uses a volume mount (`-v /opt/lifeos/dev:/app`) plus uvicorn `--reload`. This means:
|
|
|
|
1. Edit any file in `/opt/lifeos/dev/` on the host
|
|
2. Uvicorn detects the change and restarts automatically
|
|
3. No container rebuild needed
|
|
4. No deploy script needed
|
|
|
|
**Verification after changes:**
|
|
```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
|
|
```
|
|
|
|
---
|
|
|
|
## 27. Backup & Restore
|
|
|
|
### Backup
|
|
```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
|
|
```
|
|
|
|
---
|
|
|
|
## 28. Build Roadmap
|
|
|
|
### Current State (as of 2026-03-02)
|
|
- **Phase 1, Tier 3 complete.** All 6 Tier 3 features built.
|
|
- 23 routers, 51 templates, 48 database tables
|
|
- 121+ discovered routes
|
|
|
|
### What's Built
|
|
Core CRUD for: domains, areas, projects, tasks, notes, links, focus, capture, contacts, search, admin/trash, lists, files, meetings, decisions, weblinks, appointments, time_tracking, processes, calendar, time_budgets, eisenhower, history.
|
|
|
|
Working features: collapsible sidebar nav tree, task filtering/sorting/priority/status/context, daily focus, markdown notes, file uploads with 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, unified calendar view, Eisenhower matrix, time budgets with actual vs budgeted, process templates with runs, change history feed.
|
|
|
|
### Remaining Roadmap
|
|
|
|
**Tier 4:**
|
|
- Releases/milestones CRUD
|
|
- Task 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
|
|
|
|
---
|
|
|
|
## Known Traps & Gotchas
|
|
|
|
1. **`time_entries` has no `updated_at` column.** BaseRepository.soft_delete() will fail. Use direct SQL for delete/restore on this table.
|
|
|
|
2. **Junction tables use raw SQL, not BaseRepository.** They have composite PKs and no `id` column.
|
|
|
|
3. **Timer constraint:** Only one timer at a time. `time_entries WHERE end_at IS NULL` detects running timer.
|
|
|
|
4. **Nullable fields in BaseRepository:** If a form field should allow NULL, add it to the `nullable_fields` set. Otherwise `update()` silently skips null values.
|
|
|
|
5. **Schema .sql files may be stale.** Always verify against live DB with `\d table_name`.
|
|
|
|
6. **No pagination.** All list views load all rows. Fine at current data volume.
|
|
|
|
7. **No CSRF protection.** Single-user system.
|
|
|
|
8. **SSL cert path** uses `kasm.invixiom.com-0001` (not `kasm.invixiom.com`).
|
|
|
|
9. **Test suite not fully green.** Async event loop fixes and seed data corrections are in progress.
|
|
|
|
10. **asyncpg requires native Python types.** ISO date strings must be converted to `date`/`datetime` objects before passing to queries. BaseRepository's `_coerce_value()` handles this automatically.
|
|
|
|
---
|
|
|
|
*Document generated 2026-03-02. This is the single source of truth for rebuilding the Life OS system from ground zero.*
|