Add Projects, Milestones, RAG auto-inject, push notifications, PWA, tag normalisation

## Projects & Milestones (Phases A + G)
- New models: Project, Milestone (Project → Milestone → Task hierarchy)
- notes table: project_id + milestone_id FKs; parent_id FK constraint activated
- Migrations: 0017 (projects), 0018 (push_subscriptions), 0019 (events), 0020 (milestones)
- Services: projects.py, milestones.py (CRUD + progress tracking)
- Routes: /api/projects + /api/projects/<id>/milestones
- LLM tools: create/list/get/update project; create/list milestone; project + milestone + parent_task params on note/task tools
- Frontend: ProjectListView (stacked milestone bars), ProjectView (milestone-grouped kanban), ProjectSelector, MilestoneSelector, NoteEditorView + TaskEditorView updated

## RAG Auto-injection (Phase B)
- Notes ≥0.60 cosine similarity auto-injected into system prompt (max 3, 800 chars each)
- excluded_note_ids param; ChatView "Auto-included" sidebar section

## Summarisation improvements (Phase C)
- Threshold 20→30, keep-recent 6→8, max_tokens 200→400
- Two-pass summarisation for histories >50 messages

## Browser push notifications (Phase E)
- PushSubscription model + migration; pywebpush dependency
- /api/push routes; VAPID config; fire-and-forget on generation complete
- Frontend: sw.js, push store, Settings toggle

## PWA manifest (Phase F)
- manifest.json, Apple meta tags, service worker registration in main.ts

## Tag normalisation
- All tags lowercased + deduplicated at backend (create_note/update_note) and frontend (TagInput sanitize)
- Note/Task types gain project_id + milestone_id fields; store signatures updated

## CalDAV
- Radicale embedded server reverted; back to user-configured external CalDAV

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 20:52:21 -05:00
parent 3d7be5888e
commit 012eb1d46b
52 changed files with 4319 additions and 62 deletions
+48
View File
@@ -0,0 +1,48 @@
"""Add projects table and project_id to notes."""
from alembic import op
revision = "0017"
down_revision = "0016"
def upgrade() -> None:
op.execute("""
CREATE TABLE IF NOT EXISTS projects (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL DEFAULT '',
goal TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'active',
color TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
)
""")
op.execute("CREATE INDEX IF NOT EXISTS ix_projects_user_id ON projects(user_id)")
op.execute("CREATE INDEX IF NOT EXISTS ix_projects_status ON projects(status)")
op.execute("ALTER TABLE notes ADD COLUMN IF NOT EXISTS project_id INTEGER REFERENCES projects(id) ON DELETE SET NULL")
op.execute("CREATE INDEX IF NOT EXISTS ix_notes_project_id ON notes(project_id)")
# Add FK constraint to existing parent_id column (was nullable with no FK)
# Only add if constraint doesn't already exist
op.execute("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'notes_parent_id_fkey'
) THEN
ALTER TABLE notes ADD CONSTRAINT notes_parent_id_fkey
FOREIGN KEY (parent_id) REFERENCES notes(id) ON DELETE SET NULL;
END IF;
END $$;
""")
def downgrade() -> None:
op.execute("ALTER TABLE notes DROP CONSTRAINT IF EXISTS notes_parent_id_fkey")
op.execute("DROP INDEX IF EXISTS ix_notes_project_id")
op.execute("ALTER TABLE notes DROP COLUMN IF EXISTS project_id")
op.execute("DROP INDEX IF EXISTS ix_projects_status")
op.execute("DROP INDEX IF EXISTS ix_projects_user_id")
op.execute("DROP TABLE IF EXISTS projects")
@@ -0,0 +1,28 @@
"""Add push_subscriptions table for web push notifications."""
from alembic import op
revision = "0018"
down_revision = "0017"
def upgrade() -> None:
op.execute("""
CREATE TABLE IF NOT EXISTS push_subscriptions (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
endpoint TEXT NOT NULL,
p256dh TEXT NOT NULL,
auth TEXT NOT NULL,
user_agent TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
last_used TIMESTAMP WITH TIME ZONE,
UNIQUE(user_id, endpoint)
)
""")
op.execute("CREATE INDEX IF NOT EXISTS ix_push_subscriptions_user_id ON push_subscriptions(user_id)")
def downgrade() -> None:
op.execute("DROP INDEX IF EXISTS ix_push_subscriptions_user_id")
op.execute("DROP TABLE IF EXISTS push_subscriptions")
+37
View File
@@ -0,0 +1,37 @@
"""Add events table for internal CalDAV (Radicale) linkage."""
from alembic import op
revision = "0019"
down_revision = "0018"
def upgrade() -> None:
op.execute("""
CREATE TABLE IF NOT EXISTS events (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
project_id INTEGER REFERENCES projects(id) ON DELETE SET NULL,
uid TEXT NOT NULL,
title TEXT NOT NULL DEFAULT '',
start_dt TIMESTAMP WITH TIME ZONE NOT NULL,
end_dt TIMESTAMP WITH TIME ZONE,
all_day BOOLEAN NOT NULL DEFAULT FALSE,
description TEXT NOT NULL DEFAULT '',
location TEXT NOT NULL DEFAULT '',
recurrence TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(user_id, uid)
)
""")
op.execute("CREATE INDEX IF NOT EXISTS ix_events_user_id ON events(user_id)")
op.execute("CREATE INDEX IF NOT EXISTS ix_events_project_id ON events(project_id)")
op.execute("CREATE INDEX IF NOT EXISTS ix_events_start_dt ON events(start_dt)")
def downgrade() -> None:
op.execute("DROP INDEX IF EXISTS ix_events_start_dt")
op.execute("DROP INDEX IF EXISTS ix_events_project_id")
op.execute("DROP INDEX IF EXISTS ix_events_user_id")
op.execute("DROP TABLE IF EXISTS events")
+38
View File
@@ -0,0 +1,38 @@
"""Add milestones table and milestone_id to notes."""
from alembic import op
revision = "0020"
down_revision = "0019"
def upgrade() -> None:
op.execute("""
CREATE TABLE IF NOT EXISTS milestones (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
title TEXT NOT NULL DEFAULT '',
description TEXT,
status TEXT NOT NULL DEFAULT 'active',
order_index INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
)
""")
op.execute("CREATE INDEX IF NOT EXISTS ix_milestones_user_id ON milestones(user_id)")
op.execute("CREATE INDEX IF NOT EXISTS ix_milestones_project_id ON milestones(project_id)")
op.execute("""
ALTER TABLE notes
ADD COLUMN IF NOT EXISTS milestone_id INTEGER REFERENCES milestones(id) ON DELETE SET NULL
""")
op.execute("CREATE INDEX IF NOT EXISTS ix_notes_milestone_id ON notes(milestone_id)")
def downgrade() -> None:
op.execute("DROP INDEX IF EXISTS ix_notes_milestone_id")
op.execute("ALTER TABLE notes DROP COLUMN IF EXISTS milestone_id")
op.execute("DROP INDEX IF EXISTS ix_milestones_project_id")
op.execute("DROP INDEX IF EXISTS ix_milestones_user_id")
op.execute("DROP TABLE IF EXISTS milestones")