22 KiB
Life OS - Claude Code Prompts for DEV Enhancements
Target Environment: DEV only (lifeos-dev.invixiom.com)
Codebase Location: /opt/lifeos/dev/
Hot Reload: Changes to files in /opt/lifeos/dev/ are picked up automatically by uvicorn --reload. No container rebuild needed.
Database: lifeos_dev on container lifeos-db (PostgreSQL 16)
PROMPT 1: Task Detail Page - Add Tabs
Context: The task detail page at /tasks/{task_id} currently shows task metadata but does NOT use the tabbed layout. The project detail page at /projects/{project_id} (template: project_detail.html) IS the reference implementation for tabbed detail views. It uses a ?tab= query parameter to switch between tabs, with tab content rendered server-side via Jinja2 conditionals.
Codebase facts:
- Stack: Python 3.12, FastAPI, SQLAlchemy async, Jinja2 templates, vanilla JS/CSS
- Template to modify:
templates/task_detail.html - Router to modify:
routers/tasks.py(thetask_detailGET handler at/tasks/{task_id}) - Reference template:
templates/project_detail.html- copy the tab-strip HTML structure and?tab=query param pattern exactly - CSS:
static/style.cssalready has.tab-stripand.tab-contentclasses from the project detail implementation - BaseRepository pattern:
core/base_repository.pyprovides generic CRUD. All queries useis_deleted = falsefiltering automatically.
Existing DB tables/junctions (already in schema, may not have app code):
notes- hasdomain_id,project_idcolumns. Notes don't have atask_idFK directly. You'll need to either add atask_idFK column to notes OR use a polymorphic junction (e.g.,note_tasksor usefile_mappingspattern withcontext_type='task'). Prefer adding a nullabletask_id UUID REFERENCES tasks(id)column to thenotestable for simplicity.weblinks- hasdomain_id,project_id. Same situation as notes - add nullabletask_id UUID REFERENCES tasks(id).file_mappings- polymorphic junction:file_id, context_type, context_id. Already supportscontext_type='task'. Files tab should queryfile_mappings WHERE context_type='task' AND context_id={task_id}joined tofiles.lists- hasdomain_id,project_id. Add nullabletask_id UUID REFERENCES tasks(id).decisions- hasdomain_id,project_id. Add nullabletask_id UUID REFERENCES tasks(id).processes/process_runs- These tables exist in the R1 schema but process CRUD is NOT yet built. The Processes tab should show: (a) a way to apply a process template to this task (create a process_run with context), and (b) list existing process_runs linked to this task. For now, since process CRUD doesn't exist yet, create the tab structure with an empty state placeholder that says "Process management coming soon" and a TODO comment in the code.contact_tasks- junction table already exists with columns:contact_id, task_id, role, created_at. This is the Contacts + Roles tab data source.
What to build:
-
Migration SQL - Create a migration file at
/opt/lifeos/migrations/that adds:ALTER TABLE notes ADD COLUMN task_id UUID REFERENCES tasks(id);ALTER TABLE weblinks ADD COLUMN task_id UUID REFERENCES tasks(id);ALTER TABLE lists ADD COLUMN task_id UUID REFERENCES tasks(id);ALTER TABLE decisions ADD COLUMN task_id UUID REFERENCES tasks(id);- Create indexes on these new FK columns.
- Apply to
lifeos_devdatabase via:docker exec -i lifeos-db psql -U postgres -d lifeos_dev < /opt/lifeos/migrations/NNNN_task_detail_tabs.sql
-
Router (
routers/tasks.py) - Modify the task detail GET handler:- Accept
tabquery parameter (default: "overview") - Tabs: overview, notes, weblinks, files, lists, decisions, processes, contacts
- For each tab, query the relevant data and pass to template context
- For contacts tab: query
contact_tasksjoined tocontactsWHEREtask_id = {id} - For files tab: query
file_mappingsWHEREcontext_type='task' AND context_id={id}joined tofiles - For notes/weblinks/lists/decisions: query WHERE
task_id = {id} AND is_deleted = false
- Accept
-
Template (
templates/task_detail.html) - Restructure to matchproject_detail.htmltab pattern:- Keep existing task header (title, metadata, action buttons including timer)
- Add tab-strip below header with all tab names
- Active tab highlighted via CSS class
- Tab links use
?tab=tabnamepattern - Each tab section wrapped in Jinja2
{% if tab == 'notes' %}...{% endif %}blocks - Overview tab contains what's currently shown (description, subtasks, metadata)
- Each entity tab shows a list of related items with an "+ Add" button
- Contacts tab shows contact name + role with ability to add/remove
-
Contact + Role management on tasks:
- Add endpoint
POST /tasks/{task_id}/contacts/addacceptingcontact_idandroleform fields. Insert intocontact_tasks. - Add endpoint
POST /tasks/{task_id}/contacts/{contact_id}/removeto delete fromcontact_tasks. - The contacts tab should show a form with a contact dropdown (all non-deleted contacts) and a role text input, plus a list of currently linked contacts with remove buttons.
- Role should be a free-text field (not a fixed set yet - we'll define role sets later).
- Add endpoint
-
Update note/weblink/list/decision create forms to accept optional
task_idand pre-fill it when creating from within a task detail page. The "+ Add" buttons on each tab should link to the respective create form with?task_id={task_id}pre-filled.
Do NOT:
- Touch production database or files
- Build full process CRUD (just placeholder tab)
- Change the existing task list page (
tasks.html) - Add any npm/frontend build tooling
Test: After implementation, verify at https://lifeos-dev.invixiom.com/tasks/{any_task_id} that all tabs render, switching works via URL param, and the contacts add/remove flow works.
PROMPT 2: Project Detail Page - Add Missing Tabs
Context: The project detail page at /projects/{project_id} already has a working tabbed layout with tabs for Tasks, Notes, Links (weblinks). It needs additional tabs for: Files, Lists, Processes, Contacts + Roles.
Codebase facts:
- Template:
templates/project_detail.html(already has tab-strip pattern) - Router:
routers/projects.py(the detail GET handler already accepts?tab=param) - Existing tabs: tasks, notes, links/weblinks
file_mappingssupportscontext_type='project'alreadycontact_projectsjunction table exists:contact_id, project_id, role, created_atliststable hasproject_idFK already- Process CRUD not yet built
What to build:
-
Router (
routers/projects.py) - Extend the detail handler:- Add tab cases for: files, lists, processes, contacts
- Files: query
file_mappings WHERE context_type='project' AND context_id={project_id}joined tofiles - Lists: query
lists WHERE project_id={id} AND is_deleted=false - Processes: placeholder (empty state)
- Contacts: query
contact_projectsjoined tocontactsWHEREproject_id={id}
-
Template (
templates/project_detail.html) - Add new tabs to existing tab-strip:- Files tab: list of attached files with upload link (
/files/upload?context_type=project&context_id={project_id}) - Lists tab: list of project lists with link to create (
/lists/create?project_id={project_id}) - Processes tab: empty state placeholder "Process management coming soon"
- Contacts tab: same pattern as Task contacts - dropdown + role input + list with remove
- Files tab: list of attached files with upload link (
-
Contact + Role management on projects:
POST /projects/{project_id}/contacts/add- insert intocontact_projectsPOST /projects/{project_id}/contacts/{contact_id}/remove- delete fromcontact_projects- Same UI pattern as task contacts tab
Do NOT:
- Modify existing working tabs (tasks, notes, links)
- Build process CRUD
Test: Verify at https://lifeos-dev.invixiom.com/projects/{any_project_id} that new tabs appear and function.
PROMPT 3: Meeting Detail Page - Add Tabs
Context: The meeting detail page at /meetings/{meeting_id} exists but may not have the full tabbed layout. It needs tabs for: Notes, Weblinks, Files, Lists, Processes, Contacts + Roles. Meetings already have an action items / tasks relationship via the meeting_tasks junction table.
Codebase facts:
- Template:
templates/meeting_detail.html - Router:
routers/meetings.py contact_meetingsjunction exists:contact_id, meeting_id, role, created_at(role values: organizer|attendee|optional)meeting_tasksjunction exists:meeting_id, task_id, source(source: discussed|action_item)meetingstable hasnotes_bodycolumn for inline meeting notes- Notes, weblinks, lists don't have a
meeting_idFK column
What to build:
-
Migration SQL - Add to same or new migration file:
ALTER TABLE notes ADD COLUMN meeting_id UUID REFERENCES meetings(id);ALTER TABLE weblinks ADD COLUMN meeting_id UUID REFERENCES meetings(id);ALTER TABLE lists ADD COLUMN meeting_id UUID REFERENCES meetings(id);- Indexes on new columns.
- Apply to
lifeos_dev.
-
Router (
routers/meetings.py) - Add/extend detail handler with?tab=param:- Tabs: overview (existing content: agenda, notes_body, action items), notes, weblinks, files, lists, processes, contacts
- Query patterns same as tasks/projects for each tab type
- Files via
file_mappings WHERE context_type='meeting' - Contacts via
contact_meetingsjoined tocontacts
-
Template (
templates/meeting_detail.html) - Restructure to tab layout matching project_detail.html pattern:- Overview tab: existing meeting content (agenda, meeting notes, action items list)
- All other tabs: same list + add pattern as task/project detail
-
Contact + Role management on meetings:
POST /meetings/{meeting_id}/contacts/add- insert intocontact_meetings, role defaults to "attendee"POST /meetings/{meeting_id}/contacts/{contact_id}/remove- delete fromcontact_meetings- Role dropdown with predefined values: organizer, attendee, optional
Test: Verify at https://lifeos-dev.invixiom.com/meetings/{any_meeting_id}.
PROMPT 4: Lists Page - Dynamic Domain/Project Filter + Contact Roles
Context: The lists page at /lists/ has domain and project dropdown filters. Currently the project dropdown shows ALL projects regardless of which domain is selected. It should filter dynamically.
Codebase facts:
- Template:
templates/lists.html - Router:
routers/lists.py(or wherever list CRUD lives) - The filter uses HTML
<select>elements that auto-submit on change (pattern fromapp.js) projectstable hasdomain_idFK (nullable - some projects may have domain_id = NULL via unassigned area)areastable hasdomain_idFKcontact_listsjunction exists:contact_id, list_id, role, created_at
What to build:
-
Dynamic project filtering by domain - Two approaches, pick the simpler one:
- Option A (recommended - vanilla JS): Add a small JS block to
lists.html(orapp.js) that intercepts the domain dropdownchangeevent. When domain changes, fetch projects for that domain via a new lightweight API endpointGET /api/projects?domain_id={id}that returns JSON[{id, name}]. Rebuild the project<select>options dynamically. Include projects wheredomain_idmatches OR wheredomain_id IS NULL(unassigned). If no domain selected, show all projects. - Option B (server-side only): On domain change, the form auto-submits (existing behavior). The router filters the project dropdown options server-side based on the selected domain_id and passes filtered projects to the template. This is simpler but causes a page reload.
- Go with Option A for better UX. Create the API endpoint in the lists router or a shared API router.
- Option A (recommended - vanilla JS): Add a small JS block to
-
Contact + Role management on lists:
- On the list detail page (
list_detail.html), add a Contacts section (or tab if it has tabs). POST /lists/{list_id}/contacts/add- insert intocontact_listsPOST /lists/{list_id}/contacts/{contact_id}/remove- delete fromcontact_lists- Same UI pattern: contact dropdown + role text input + list with remove buttons.
- On the list detail page (
Test: Go to https://lifeos-dev.invixiom.com/lists/, select a domain, verify the project dropdown updates to only show projects in that domain plus unassigned ones. Test adding/removing contacts on a list detail page.
PROMPT 5: Eisenhower Page - Dynamic Height + Filters
Context: The Eisenhower matrix page at /eisenhower/ displays tasks in a 2x2 grid (Important+Urgent, Important+Not Urgent, Not Important+Urgent, Not Important+Not Urgent). Currently the quadrant cards have a fixed minimum height which wastes space or clips content.
Codebase facts:
- Template:
templates/eisenhower.html(if it exists) or it may be part of another template - Router:
routers/eisenhower.pyor may be inrouters/tasks.py - CSS in
static/style.css - Current quadrant logic: priority 1-2 = Important, 3-4 = Not Important. Due date <= 7 days = Urgent, > 7 days or null = Not Urgent.
- Tasks are filtered: only
status IN ('open', 'in_progress', 'blocked')andis_deleted = false
What to build:
-
Dynamic height for quadrant cards:
- Remove any
min-heightor fixedheightCSS on the Eisenhower quadrant containers. - Use CSS that allows quadrants to grow with content:
min-height: 200px(reasonable minimum) but nomax-heightrestriction. Or use CSS Grid withgrid-template-rows: auto autoso rows size to content. - Each card within a quadrant should be compact (task title, priority dot, due date - single line or two lines max).
- If a quadrant has many tasks, it grows. If empty, it shows a small empty state.
- Remove any
-
Add filters:
- Add a filter bar above the 2x2 grid with dropdowns for:
- Domain - filter tasks by domain_id
- Project - filter tasks by project_id (dynamic based on domain selection, same pattern as Prompt 4)
- Status - filter by status (open, in_progress, blocked)
- Context - filter by context (GTD execution context from context_types table)
- Filters use query parameters:
/eisenhower/?domain_id=X&project_id=Y&status=Z&context=W - Router applies these filters to the task query before distributing into quadrants.
- Use the same auto-submit-on-change pattern used elsewhere in the app.
- Add a filter bar above the 2x2 grid with dropdowns for:
Test: Verify at https://lifeos-dev.invixiom.com/eisenhower/ that quadrants resize with content and filters narrow the displayed tasks.
PROMPT 6: Focus Page - Add Domain/Area/Project Filters
Context: The focus page at /focus/ shows the daily focus list (tasks selected for today). Currently there are no filters to narrow which available tasks are shown for adding to focus.
Codebase facts:
- Template:
templates/focus.html - Router:
routers/focus.py - The page has two sections: (1) Today's focus items (tasks already added), (2) Available tasks to add
daily_focustable:id, task_id, focus_date, completed, sort_order, is_deleted, created_at- Tasks have
domain_id,project_idFKs. Tasks also havearea_idFK.
What to build:
-
Filter bar for available tasks section:
- Add dropdowns above the "Available Tasks" section for: Domain, Area (filtered by selected domain), Project (filtered by selected domain/area)
- Same dynamic filtering pattern as Prompts 4 and 5
- Filters apply only to the "available tasks to add" list, NOT to the already-focused items
- Query params:
/focus/?domain_id=X&area_id=Y&project_id=Z&focus_date=YYYY-MM-DD
-
Router changes:
- Accept domain_id, area_id, project_id query params
- Apply as WHERE clauses when querying available (unfocused) tasks
- Pass filter values + dropdown options to template context
Test: Verify at https://lifeos-dev.invixiom.com/focus/ that filter dropdowns appear and narrow the available tasks list.
PROMPT 7: Change History (Recently Changed Items)
Context: We need a "Change History" view so the user can see recently modified items across the entire system. Every table in Life OS has created_at and updated_at TIMESTAMPTZ columns (except time_entries which is missing updated_at). This gives us a simple approach: query updated_at across all entity tables and show a reverse-chronological feed.
Codebase facts:
- All main entity tables have
updated_at: domains, areas, projects, tasks, notes, contacts, meetings, decisions, lists, weblinks, appointments, links, files, processes, capture time_entriesdoes NOT haveupdated_at- exclude from change history- BaseRepository handles all CRUD and sets
updated_at = now()on every update - Sidebar navigation is built in
core/sidebar.py
What to build:
-
New router: Create
routers/history.pywith prefix/historyGET /history/- renders the change history page- Query: For each entity table,
SELECT id, 'entity_type' as type, title/name as label, updated_at, created_at FROM {table} WHERE is_deleted = false ORDER BY updated_at DESC LIMIT 20 - Union all results, sort by
updated_at DESC, take top 50 (or paginate) - For each item, determine if it was "created" (updated_at = created_at or within 1 second) or "modified"
- Pass to template as a list of
{type, id, label, updated_at, action}dicts
-
Template: Create
templates/history.html- Page title: "Change History" or "Recent Changes"
- Filter by: entity type dropdown, date range
- Each row shows: timestamp, action icon (created/modified), entity type badge, item name as clickable link to detail page, relative time ("2 minutes ago", "yesterday")
- Use the standard list-row styling
-
Sidebar link: Add "History" to the sidebar navigation in
core/sidebar.py(orbase.htmldirectly), in the utility section near Search/Capture/Admin. -
Register router in
main.py:app.include_router(history.router)
Architecture note: This is NOT a full audit log with field-level diffs. It's a simple "what changed recently" view derived from updated_at timestamps. A full audit log (with before/after values) would require triggers or middleware - defer that to a later phase.
Test: Verify at https://lifeos-dev.invixiom.com/history/ that recently created/modified items appear in reverse chronological order with working links.
PROMPT 8: Fix Search - Partial Word Matching
Context: The global search currently uses PostgreSQL tsvector/tsquery full-text search. This is great for natural language search but does NOT support partial word matching. Searching for "Sys" does not return items containing "System" because tsquery requires complete lexemes.
Codebase facts:
- Search router:
routers/search.py - API endpoint:
GET /search/api?q=X&entity_type=Y&limit=Z - Page endpoint:
GET /search/?q=X - Every searchable table has a
search_vector TSVECTORcolumn with GIN index - Search triggers maintain the tsvector automatically
- Current query pattern likely uses
plainto_tsquery('english', query)orto_tsquery
The fix:
The search needs to support partial prefix matching. PostgreSQL supports this via :* suffix on tsquery terms.
-
Modify the search query building in
routers/search.py:- Instead of
plainto_tsquery('english', query), build a prefix query - For a search term like "Sys", construct:
to_tsquery('english', 'Sys:*') - For multi-word queries like "Sys Admin", construct:
to_tsquery('english', 'Sys:* & Admin:*')(AND all terms with prefix matching) - Implementation: split the query string on whitespace, strip non-alphanumeric chars from each term, append
:*to each, join with& - Handle edge cases: empty query, single character (skip search for < 2 chars), special characters
- Instead of
-
Also add an ILIKE fallback for when tsvector doesn't match (tsvector strips stop words and stems, so very short or unusual terms might not match):
- After the tsvector query, if results are sparse (< 3 results), do a supplemental
WHERE title ILIKE '%{query}%' OR name ILIKE '%{query}%'query - Deduplicate results (tsvector results first, then ILIKE additions)
- This ensures "Sys" matches "System" even if the stemmer does something unexpected
- After the tsvector query, if results are sparse (< 3 results), do a supplemental
-
Update the search query for EACH entity that's searched. The pattern should be consistent - build a helper function like:
def build_search_condition(query: str): terms = query.strip().split() if not terms: return None tsquery_str = ' & '.join(f"{term}:*" for term in terms if len(term) >= 2) return tsquery_str
Do NOT:
- Change the search_vector triggers or GIN indexes (they're fine)
- Add any new database tables
- Change the search UI/template (just the backend query logic)
Test: Go to https://lifeos-dev.invixiom.com/search/?q=Sys and verify it returns items containing "System", "Systematic", "Sys" etc. Test with "Inv" to match "Invoice", "Investment" etc.
General Notes for All Prompts
- Git: After each prompt's changes are verified working, run:
cd /opt/lifeos/dev && git add . && git commit -m "descriptive message" && git push origin main - Database migrations: Always apply to
lifeos_devonly. Command pattern:docker exec -i lifeos-db psql -U postgres -d lifeos_dev < migration_file.sql - Restart if needed:
docker restart lifeos-dev(though hot reload should handle most changes) - Logs:
docker logs lifeos-dev --tail 30to debug errors - Contact role pattern: For now, roles are free-text on tasks/projects/lists. On meetings, roles are constrained to: organizer, attendee, optional. This will be standardized later with a defined role set.
- Empty states: Every tab/section that can be empty should show a clean empty state message with a CTA button to add the first item.