feat(backend): books, tasks, calendar feed, time tracking, profiles, admin API

- Schema: books, tasks (book/page links, status, priority, due_at),
  time_entries (open entry = running timer), user profile columns
  (avatar, locale, theme, bg_image, bg_blur)
- Books CRUD; pages can live in a book (book_id, validated same-workspace)
- Tasks CRUD with status filter and due_at range query for the calendar
- Time API: start (409 if running), stop (computes duration), list with
  open timer, update, delete
- Users: fuzzy discovery for invites, PATCH /me profile/appearance,
  password change; members list now includes avatars
- Admin API (is_admin guard): overview counts, user list, grant/revoke
  admin, delete user (self-demote/delete blocked)
This commit is contained in:
x3
2026-09-22 13:56:25 +02:00
parent 663f7c16cc
commit b852583499
11 changed files with 589 additions and 14 deletions
+65
View File
@@ -76,6 +76,63 @@ CREATE TABLE IF NOT EXISTS app_settings (
-- Global admin flag (first user created via web installer)
ALTER TABLE users ADD COLUMN IF NOT EXISTS is_admin BOOLEAN NOT NULL DEFAULT FALSE;
-- Profile / appearance / locale settings (edited via settings window)
ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar TEXT NOT NULL DEFAULT '';
ALTER TABLE users ADD COLUMN IF NOT EXISTS locale TEXT NOT NULL DEFAULT 'en';
ALTER TABLE users ADD COLUMN IF NOT EXISTS theme TEXT NOT NULL DEFAULT 'system';
ALTER TABLE users ADD COLUMN IF NOT EXISTS bg_image TEXT NOT NULL DEFAULT '';
ALTER TABLE users ADD COLUMN IF NOT EXISTS bg_blur BOOLEAN NOT NULL DEFAULT TRUE;
-- Books: independent containers that hold pages (collapsible in sidebar)
CREATE TABLE IF NOT EXISTS books (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
title TEXT NOT NULL DEFAULT 'Untitled book' CHECK (char_length(title) BETWEEN 1 AND 200),
icon TEXT NOT NULL DEFAULT '📚',
position DOUBLE PRECISION NOT NULL DEFAULT 0,
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_books_workspace ON books(workspace_id);
-- Pages can optionally live inside a book (book_id NULL = top-level page)
ALTER TABLE pages ADD COLUMN IF NOT EXISTS book_id UUID REFERENCES books(id) ON DELETE CASCADE;
CREATE INDEX IF NOT EXISTS idx_pages_book ON pages(book_id);
-- Tasks: can link to a book and/or a page, optional deadline (feeds the calendar)
CREATE TABLE IF NOT EXISTS tasks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
book_id UUID REFERENCES books(id) ON DELETE SET NULL,
page_id UUID REFERENCES pages(id) ON DELETE SET NULL,
title TEXT NOT NULL CHECK (char_length(title) BETWEEN 1 AND 300),
status TEXT NOT NULL DEFAULT 'todo' CHECK (status IN ('todo','doing','done')),
priority TEXT NOT NULL DEFAULT 'normal' CHECK (priority IN ('low','normal','high')),
due_at TIMESTAMPTZ NULL,
position DOUBLE PRECISION NOT NULL DEFAULT 0,
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_tasks_workspace ON tasks(workspace_id);
CREATE INDEX IF NOT EXISTS idx_tasks_due ON tasks(workspace_id, due_at);
-- Time tracking entries (open entry = running timer, ended_at IS NULL)
CREATE TABLE IF NOT EXISTS time_entries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
book_id UUID REFERENCES books(id) ON DELETE SET NULL,
page_id UUID REFERENCES pages(id) ON DELETE SET NULL,
description TEXT NOT NULL DEFAULT '',
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
ended_at TIMESTAMPTZ NULL,
duration_seconds INTEGER NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_time_ws_user ON time_entries(workspace_id, user_id);
-- Ensure workspace owner is always a member with role=owner
CREATE OR REPLACE FUNCTION ensure_owner_membership() RETURNS trigger AS $$
BEGIN
@@ -110,3 +167,11 @@ FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
DROP TRIGGER IF EXISTS trg_pages_touch ON pages;
CREATE TRIGGER trg_pages_touch BEFORE UPDATE ON pages
FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
DROP TRIGGER IF EXISTS trg_books_touch ON books;
CREATE TRIGGER trg_books_touch BEFORE UPDATE ON books
FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
DROP TRIGGER IF EXISTS trg_tasks_touch ON tasks;
CREATE TRIGGER trg_tasks_touch BEFORE UPDATE ON tasks
FOR EACH ROW EXECUTE FUNCTION touch_updated_at();