feat(ui): add maintenance page for task management
Some checks failed
CI / build-and-test (push) Failing after -31m53s

* Implement maintenance page with task and log display
* Add backfill and metadata retry functionality
* Integrate grid component for project display in thoughts page
* Update types for maintenance tasks and logs
* Enhance sidebar and shell for new maintenance navigation
This commit is contained in:
2026-04-26 23:13:41 +02:00
parent b39cd3ba72
commit 927a118338
48 changed files with 2228 additions and 868 deletions

View File

@@ -0,0 +1,19 @@
create table if not exists thoughts (
id bigserial primary key,
guid uuid not null default gen_random_uuid(),
content text not null,
embedding vector(1536),
metadata jsonb default '{}'::jsonb,
created_at timestamptz default now(),
updated_at timestamptz default now(),
constraint thoughts_guid_unique unique (guid)
);
create index if not exists thoughts_embedding_hnsw_idx
on thoughts using hnsw (embedding vector_cosine_ops);
create index if not exists thoughts_metadata_gin_idx
on thoughts using gin (metadata);
create index if not exists thoughts_created_at_idx
on thoughts (created_at desc);

View File

@@ -0,0 +1,15 @@
create table if not exists projects (
id bigserial primary key,
guid uuid not null default gen_random_uuid(),
name text not null unique,
description text,
created_at timestamptz default now(),
last_active_at timestamptz default now(),
constraint projects_guid_unique unique (guid)
);
alter table thoughts add column if not exists project_id uuid references projects(guid);
alter table thoughts add column if not exists archived_at timestamptz;
create index if not exists thoughts_project_id_idx on thoughts (project_id);
create index if not exists thoughts_archived_at_idx on thoughts (archived_at);

View File

@@ -0,0 +1,10 @@
create table if not exists thought_links (
from_id bigint not null references thoughts(id) on delete cascade,
to_id bigint not null references thoughts(id) on delete cascade,
relation text not null,
created_at timestamptz default now(),
primary key (from_id, to_id, relation)
);
create index if not exists thought_links_from_idx on thought_links (from_id);
create index if not exists thought_links_to_idx on thought_links (to_id);

View File

@@ -0,0 +1,31 @@
create or replace function match_thoughts(
query_embedding vector(1536),
match_threshold float default 0.7,
match_count int default 10,
filter jsonb default '{}'::jsonb
)
returns table (
id uuid,
content text,
metadata jsonb,
similarity float,
created_at timestamptz
)
language plpgsql
as $$
begin
return query
select
t.guid,
t.content,
t.metadata,
1 - (t.embedding <=> query_embedding) as similarity,
t.created_at
from thoughts t
where 1 - (t.embedding <=> query_embedding) > match_threshold
and t.archived_at is null
and (filter = '{}'::jsonb or t.metadata @> filter)
order by t.embedding <=> query_embedding
limit match_count;
end;
$$;

View File

@@ -0,0 +1,16 @@
create table if not exists embeddings (
id bigserial primary key,
guid uuid not null default gen_random_uuid(),
thought_id uuid not null references thoughts(guid) on delete cascade,
model text not null,
dim int not null,
embedding vector not null,
created_at timestamptz default now(),
updated_at timestamptz default now(),
constraint embeddings_guid_unique unique (guid),
constraint embeddings_thought_model_unique unique (thought_id, model)
);
create index if not exists embeddings_thought_id_idx on embeddings (thought_id);
alter table thoughts drop column if exists embedding;

View File

@@ -0,0 +1,34 @@
create or replace function match_thoughts(
query_embedding vector,
match_threshold float default 0.7,
match_count int default 10,
filter jsonb default '{}'::jsonb,
embedding_model text default ''
)
returns table (
id uuid,
content text,
metadata jsonb,
similarity float,
created_at timestamptz
)
language plpgsql
as $$
begin
return query
select
t.guid,
t.content,
t.metadata,
1 - (e.embedding <=> query_embedding) as similarity,
t.created_at
from thoughts t
join embeddings e on e.thought_id = t.guid
where 1 - (e.embedding <=> query_embedding) > match_threshold
and t.archived_at is null
and (embedding_model = '' or e.model = embedding_model)
and (filter = '{}'::jsonb or t.metadata @> filter)
order by e.embedding <=> query_embedding
limit match_count;
end;
$$;

View File

@@ -0,0 +1,3 @@
-- Full-text search index on thought content for semantic fallback when no embeddings exist.
create index if not exists thoughts_content_fts_idx
on thoughts using gin(to_tsvector('simple', content));

View File

@@ -0,0 +1,42 @@
-- Extension 1: Household Knowledge Base
-- Stores household facts and vendor contacts (single-user, no RLS)
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TABLE IF NOT EXISTS household_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
category TEXT,
location TEXT,
details JSONB NOT NULL DEFAULT '{}',
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS household_vendors (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
service_type TEXT,
phone TEXT,
email TEXT,
website TEXT,
notes TEXT,
rating INTEGER CHECK (rating >= 1 AND rating <= 5),
last_used DATE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_household_items_category ON household_items(category);
CREATE INDEX IF NOT EXISTS idx_household_vendors_service ON household_vendors(service_type);
DROP TRIGGER IF EXISTS update_household_items_updated_at ON household_items;
CREATE TRIGGER update_household_items_updated_at
BEFORE UPDATE ON household_items
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

View File

@@ -0,0 +1,56 @@
-- Extension 2: Home Maintenance Tracker
-- Tracks recurring maintenance tasks and logs completed work (single-user, no RLS)
CREATE TABLE IF NOT EXISTS maintenance_tasks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
category TEXT,
frequency_days INTEGER,
last_completed TIMESTAMPTZ,
next_due TIMESTAMPTZ,
priority TEXT NOT NULL DEFAULT 'medium' CHECK (priority IN ('low', 'medium', 'high', 'urgent')),
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS maintenance_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
task_id UUID NOT NULL REFERENCES maintenance_tasks(id) ON DELETE CASCADE,
completed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
performed_by TEXT,
cost DECIMAL(10, 2),
notes TEXT,
next_action TEXT
);
CREATE INDEX IF NOT EXISTS idx_maintenance_tasks_next_due ON maintenance_tasks(next_due);
CREATE INDEX IF NOT EXISTS idx_maintenance_logs_task ON maintenance_logs(task_id, completed_at DESC);
DROP TRIGGER IF EXISTS update_maintenance_tasks_updated_at ON maintenance_tasks;
CREATE TRIGGER update_maintenance_tasks_updated_at
BEFORE UPDATE ON maintenance_tasks
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE OR REPLACE FUNCTION update_task_after_maintenance_log()
RETURNS TRIGGER AS $$
DECLARE
task_frequency INTEGER;
BEGIN
SELECT frequency_days INTO task_frequency FROM maintenance_tasks WHERE id = NEW.task_id;
UPDATE maintenance_tasks
SET last_completed = NEW.completed_at,
next_due = CASE
WHEN task_frequency IS NOT NULL THEN NEW.completed_at + (task_frequency || ' days')::INTERVAL
ELSE NULL
END,
updated_at = now()
WHERE id = NEW.task_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS update_task_after_log ON maintenance_logs;
CREATE TRIGGER update_task_after_log
AFTER INSERT ON maintenance_logs
FOR EACH ROW EXECUTE FUNCTION update_task_after_maintenance_log();

View File

@@ -0,0 +1,42 @@
-- Extension 3: Family Calendar
-- Multi-person family scheduling (single-user, no RLS)
CREATE TABLE IF NOT EXISTS family_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
relationship TEXT,
birth_date DATE,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS activities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
family_member_id UUID REFERENCES family_members(id) ON DELETE SET NULL,
title TEXT NOT NULL,
activity_type TEXT,
day_of_week TEXT,
start_time TIME,
end_time TIME,
start_date DATE,
end_date DATE,
location TEXT,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS important_dates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
family_member_id UUID REFERENCES family_members(id) ON DELETE SET NULL,
title TEXT NOT NULL,
date_value DATE NOT NULL,
recurring_yearly BOOLEAN NOT NULL DEFAULT false,
reminder_days_before INTEGER NOT NULL DEFAULT 7,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_activities_dow ON activities(day_of_week);
CREATE INDEX IF NOT EXISTS idx_activities_member ON activities(family_member_id);
CREATE INDEX IF NOT EXISTS idx_activities_dates ON activities(start_date, end_date);
CREATE INDEX IF NOT EXISTS idx_important_dates_date ON important_dates(date_value);

View File

@@ -0,0 +1,54 @@
-- Extension 4: Meal Planning
-- Recipes, weekly meal plans, and shopping lists (single-user, no RLS)
CREATE TABLE IF NOT EXISTS recipes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
cuisine TEXT,
prep_time_minutes INTEGER,
cook_time_minutes INTEGER,
servings INTEGER,
ingredients JSONB NOT NULL DEFAULT '[]',
instructions JSONB NOT NULL DEFAULT '[]',
tags TEXT[] NOT NULL DEFAULT '{}',
rating INTEGER CHECK (rating >= 1 AND rating <= 5),
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS meal_plans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
week_start DATE NOT NULL,
day_of_week TEXT NOT NULL,
meal_type TEXT NOT NULL CHECK (meal_type IN ('breakfast', 'lunch', 'dinner', 'snack')),
recipe_id UUID REFERENCES recipes(id) ON DELETE SET NULL,
custom_meal TEXT,
servings INTEGER,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS shopping_lists (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
week_start DATE NOT NULL UNIQUE,
items JSONB NOT NULL DEFAULT '[]',
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_recipes_cuisine ON recipes(cuisine);
CREATE INDEX IF NOT EXISTS idx_recipes_tags ON recipes USING GIN (tags);
CREATE INDEX IF NOT EXISTS idx_meal_plans_week ON meal_plans(week_start);
CREATE INDEX IF NOT EXISTS idx_shopping_lists_week ON shopping_lists(week_start);
DROP TRIGGER IF EXISTS update_recipes_updated_at ON recipes;
CREATE TRIGGER update_recipes_updated_at
BEFORE UPDATE ON recipes
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
DROP TRIGGER IF EXISTS update_shopping_lists_updated_at ON shopping_lists;
CREATE TRIGGER update_shopping_lists_updated_at
BEFORE UPDATE ON shopping_lists
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

View File

@@ -0,0 +1,71 @@
-- Extension 5: Professional CRM
-- Contacts, interaction logs, and opportunities (single-user, no RLS)
CREATE TABLE IF NOT EXISTS professional_contacts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
company TEXT,
title TEXT,
email TEXT,
phone TEXT,
linkedin_url TEXT,
how_we_met TEXT,
tags TEXT[] NOT NULL DEFAULT '{}',
notes TEXT,
last_contacted TIMESTAMPTZ,
follow_up_date DATE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS contact_interactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
contact_id UUID NOT NULL REFERENCES professional_contacts(id) ON DELETE CASCADE,
interaction_type TEXT NOT NULL CHECK (interaction_type IN ('meeting', 'email', 'call', 'coffee', 'event', 'linkedin', 'other')),
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
summary TEXT NOT NULL,
follow_up_needed BOOLEAN NOT NULL DEFAULT false,
follow_up_notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS opportunities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
contact_id UUID REFERENCES professional_contacts(id) ON DELETE SET NULL,
title TEXT NOT NULL,
description TEXT,
stage TEXT NOT NULL DEFAULT 'identified' CHECK (stage IN ('identified', 'in_conversation', 'proposal', 'negotiation', 'won', 'lost')),
value DECIMAL(12, 2),
expected_close_date DATE,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_contacts_last_contacted ON professional_contacts(last_contacted);
CREATE INDEX IF NOT EXISTS idx_contacts_follow_up ON professional_contacts(follow_up_date) WHERE follow_up_date IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_interactions_contact ON contact_interactions(contact_id, occurred_at DESC);
CREATE INDEX IF NOT EXISTS idx_opportunities_stage ON opportunities(stage);
DROP TRIGGER IF EXISTS update_professional_contacts_updated_at ON professional_contacts;
CREATE TRIGGER update_professional_contacts_updated_at
BEFORE UPDATE ON professional_contacts
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
DROP TRIGGER IF EXISTS update_opportunities_updated_at ON opportunities;
CREATE TRIGGER update_opportunities_updated_at
BEFORE UPDATE ON opportunities
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE OR REPLACE FUNCTION update_last_contacted()
RETURNS TRIGGER AS $$
BEGIN
UPDATE professional_contacts SET last_contacted = NEW.occurred_at WHERE id = NEW.contact_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS update_contact_last_contacted ON contact_interactions;
CREATE TRIGGER update_contact_last_contacted
AFTER INSERT ON contact_interactions
FOR EACH ROW EXECUTE FUNCTION update_last_contacted();

View File

@@ -0,0 +1,20 @@
create table if not exists stored_files (
id bigserial primary key,
guid uuid not null default gen_random_uuid(),
thought_id uuid references thoughts(guid) on delete set null,
project_id uuid references projects(guid) on delete set null,
name text not null,
media_type text not null,
kind text not null default 'file',
encoding text not null default 'base64',
size_bytes bigint not null,
sha256 text not null,
content bytea not null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint stored_files_guid_unique unique (guid)
);
create index if not exists stored_files_thought_id_idx on stored_files (thought_id);
create index if not exists stored_files_project_id_idx on stored_files (project_id);
create index if not exists stored_files_sha256_idx on stored_files (sha256);

View File

@@ -0,0 +1,39 @@
create table if not exists agent_skills (
id uuid primary key default gen_random_uuid(),
name text not null,
description text not null default '',
content text not null,
tags text[] not null default '{}',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint agent_skills_name_unique unique (name)
);
create table if not exists agent_guardrails (
id uuid primary key default gen_random_uuid(),
name text not null,
description text not null default '',
content text not null,
severity text not null default 'medium' check (severity in ('low', 'medium', 'high', 'critical')),
tags text[] not null default '{}',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint agent_guardrails_name_unique unique (name)
);
create table if not exists project_skills (
project_id uuid not null references projects(guid) on delete cascade,
skill_id uuid not null references agent_skills(id) on delete cascade,
created_at timestamptz not null default now(),
primary key (project_id, skill_id)
);
create table if not exists project_guardrails (
project_id uuid not null references projects(guid) on delete cascade,
guardrail_id uuid not null references agent_guardrails(id) on delete cascade,
created_at timestamptz not null default now(),
primary key (project_id, guardrail_id)
);
create index if not exists project_skills_project_id_idx on project_skills (project_id);
create index if not exists project_guardrails_project_id_idx on project_guardrails (project_id);

View File

@@ -0,0 +1,26 @@
-- Migration: 018_chat_histories
-- Adds a dedicated table for saving and retrieving agent chat histories.
CREATE TABLE IF NOT EXISTS chat_histories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id TEXT NOT NULL,
title TEXT,
channel TEXT,
agent_id TEXT,
project_id UUID REFERENCES projects(guid) ON DELETE SET NULL,
messages JSONB NOT NULL DEFAULT '[]',
summary TEXT,
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_chat_histories_session_id ON chat_histories(session_id);
CREATE INDEX IF NOT EXISTS idx_chat_histories_project_id ON chat_histories(project_id) WHERE project_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_chat_histories_channel ON chat_histories(channel) WHERE channel IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_chat_histories_agent_id ON chat_histories(agent_id) WHERE agent_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_chat_histories_created_at ON chat_histories(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_chat_histories_fts
ON chat_histories
USING GIN (to_tsvector('simple', coalesce(title, '') || ' ' || coalesce(summary, '')));

View File

@@ -0,0 +1,14 @@
-- Migration: 019_tool_annotations
-- Adds a table for model-authored usage notes per tool.
create table if not exists tool_annotations (
id bigserial primary key,
tool_name text not null,
notes text not null default '',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint tool_annotations_tool_name_unique unique (tool_name)
);
grant all on table public.tool_annotations to amcs;
grant usage, select on sequence tool_annotations_id_seq to amcs;