Initial: Fastify API + DB schema + collab setup

- Fastify API on :3000 (tasks, groups, auth, connectors)
- PostgreSQL schema: users, tasks, task_groups, goals, connectors
- 8 default task groups
- JWT auth (register/login/me)
- API Bridge framework with webhooks
- CLAUDE.md + polling scripts
- Collab worker integration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude CLI Agent
2026-03-29 09:57:56 +00:00
commit 9d075455e3
13 changed files with 2430 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules/
.next/
.env
*.log
.claude/

49
CLAUDE.md Normal file
View File

@@ -0,0 +1,49 @@
# Task Team — Claude CLI Agent
## Tvoje role
Jsi vývojový agent na VM-APP (10.10.10.30, public 136.243.43.144).
Pracuješ autonomně na projektu Task Team.
## Notion projekt
- Master: https://www.notion.so/32ebc92a17cd813f9414fd2fae4337a3
- Dev Tasks DB: 659a5381-564a-453a-9e2b-1345c457cca9
- Komunikační protokol: https://www.notion.so/332bc92a17cd8156bc73cb5721be3999
## Pravidla spolupráce
1. Kontroluj Notion Dev Tasks každých 5 minut
2. Vezmi tasky se statusem "Ready for Dev" — implementuj je
3. Po dokončení: změň status na "Done", napiš Server Feedback
4. Pokud potřebuješ upřesnění: vytvoř task "❓ Question:" se statusem "Needs Clarification"
5. Nikdy neimplementuj bez záznamu v Notionu
## Infrastruktura
- VM-DB: 10.10.10.10 (PostgreSQL 18.3:5432, Redis 8.6.2:6379)
- VM-ODOO: 10.10.10.20 (Odoo 19 Enterprise :8069, Community :8070)
- VM-APP: 10.10.10.30 (tento server — Task Team API, micro-apps, n8n, Claude CLI)
- VM-DEV: 10.10.10.40 (Gitea 1.25.5 :3000)
## Tech Stack
- Runtime: Node.js 24 LTS
- API: Fastify
- Frontend: Next.js 14 PWA (micro-apps)
- DB: PostgreSQL 18 @ 10.10.10.10 (user: taskteam, db: taskteam)
- Cache: Redis 8 @ 10.10.10.10
- Automation: n8n
- Proxy: Nginx 1.28.3
- Docker: 29.3.1
## Domény
- api.hasdo.info → :3000
- tasks.hasdo.info → :3001
- cal.hasdo.info → :3002
- plans.hasdo.info → :3003
- goals.hasdo.info → :3004
- chat.hasdo.info → :3005
- n8n.hasdo.info → :5678
## Adresáře
/opt/task-team/api/src/{routes,models,middleware,services}
/opt/task-team/apps/
/opt/task-team/assets/brand/
/opt/task-team/db/migrations/
/opt/n8n/

7
NOTION_POLL_PROMPT.txt Normal file
View File

@@ -0,0 +1,7 @@
Přečti CLAUDE.md. Zkontroluj Notion Dev Tasks databázi (ID: 659a5381-564a-453a-9e2b-1345c457cca9).
Najdi všechny tasky se statusem "Ready for Dev".
Vezmi první podle priority (Critical > High > Medium > Low).
Ihned změň jeho status na "In Progress".
Implementuj ho podle Description.
Po dokončení: změň na "Done" a zapiš Server Feedback ve formátu z komunikačního protokolu.
Pokud není žádný Ready for Dev task — nedělej nic.

12
STATUS_REPORT_PROMPT.txt Normal file
View File

@@ -0,0 +1,12 @@
Vytvoř nový task v Notion Dev Tasks databázi (ID: 659a5381-564a-453a-9e2b-1345c457cca9).
Název: "📊 Status Report [aktuální datum a čas]"
Status: Done
Priorita: Low
Server Feedback musí obsahovat:
- Všechny Done tasky (dnes)
- Všechny In Progress tasky
- Všechny Ready for Dev tasky
- Všechny Needs Clarification tasky
- Výstup: df -h | grep "/$" (disk)
- Výstup: free -h | grep Mem (paměť)
- Výstup: docker ps --format "table {{.Names}}\t{{.Status}}" (containery)

1805
api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

31
api/package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "task-team-api",
"version": "1.0.0",
"description": "Task Team API Server",
"main": "index.js",
"scripts": {
"start": "node src/index.js",
"dev": "node --watch src/index.js",
"test": "echo No tests yet"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"@fastify/cors": "^11.2.0",
"@fastify/helmet": "^13.0.2",
"@fastify/jwt": "^10.0.0",
"@fastify/swagger": "^9.7.0",
"@fastify/swagger-ui": "^5.2.5",
"bcrypt": "^6.0.0",
"dotenv": "^17.3.1",
"fastify": "^5.8.4",
"pg": "^8.20.0",
"redis": "^5.11.0",
"uuid": "^13.0.0"
},
"devDependencies": {
"nodemon": "^3.1.14"
}
}

41
api/src/index.js Normal file
View File

@@ -0,0 +1,41 @@
// Task Team — API Server — 2026-03-29
require('dotenv').config();
const Fastify = require('fastify');
const cors = require('@fastify/cors');
const jwt = require('@fastify/jwt');
const { Pool } = require('pg');
const app = Fastify({ logger: true });
// Database pool
const pool = new Pool({
connectionString: process.env.DATABASE_URL || 'postgresql://taskteam:TaskTeam2026!@10.10.10.10:5432/taskteam'
});
// Plugins
app.register(cors, { origin: true });
app.register(jwt, { secret: process.env.JWT_SECRET || 'taskteam-jwt-secret-2026' });
// Decorate with db
app.decorate('db', pool);
// Health check
app.get('/health', async () => ({ status: 'ok', timestamp: new Date().toISOString() }));
// Register routes
app.register(require('./routes/tasks'), { prefix: '/api/v1' });
app.register(require('./routes/groups'), { prefix: '/api/v1' });
app.register(require('./routes/auth'), { prefix: '/api/v1' });
app.register(require('./routes/connectors'), { prefix: '/api/v1' });
// Start
const start = async () => {
try {
await app.listen({ port: process.env.PORT || 3000, host: '0.0.0.0' });
console.log('Task Team API listening on port ' + (process.env.PORT || 3000));
} catch (err) {
app.log.error(err);
process.exit(1);
}
};
start();

29
api/src/routes/auth.js Normal file
View File

@@ -0,0 +1,29 @@
// Task Team — Auth Routes — 2026-03-29
async function authRoutes(app) {
// Simple JWT auth for now, Supabase integration later
app.post('/auth/register', async (req) => {
const { email, name, phone, password } = req.body;
const { rows } = await app.db.query(
'INSERT INTO users (email, name, phone) VALUES ($1, $2, $3) RETURNING id, email, name',
[email, name, phone]
);
const token = app.jwt.sign({ id: rows[0].id, email: rows[0].email });
return { data: { user: rows[0], token } };
});
app.post('/auth/login', async (req) => {
const { email } = req.body;
const { rows } = await app.db.query('SELECT id, email, name FROM users WHERE email = $1', [email]);
if (!rows.length) throw { statusCode: 401, message: 'User not found' };
const token = app.jwt.sign({ id: rows[0].id, email: rows[0].email });
return { data: { user: rows[0], token } };
});
app.get('/auth/me', { preHandler: [async (req) => { await req.jwtVerify() }] }, async (req) => {
const { rows } = await app.db.query('SELECT id, email, name, phone, language, settings FROM users WHERE id = $1', [req.user.id]);
if (!rows.length) throw { statusCode: 404, message: 'User not found' };
return { data: rows[0] };
});
}
module.exports = authRoutes;

View File

@@ -0,0 +1,55 @@
// Task Team — API Bridge / Connectors — 2026-03-29
async function connectorRoutes(app) {
// List connectors
app.get('/connectors', async (req) => {
const { rows } = await app.db.query('SELECT id, type, enabled, last_sync_at, created_at FROM connectors ORDER BY created_at DESC');
return { data: rows };
});
// Get connector
app.get('/connectors/:id', async (req) => {
const { rows } = await app.db.query('SELECT * FROM connectors WHERE id = $1', [req.params.id]);
if (!rows.length) throw { statusCode: 404, message: 'Connector not found' };
return { data: rows[0] };
});
// Create connector
app.post('/connectors', async (req) => {
const { type, config, user_id } = req.body;
const { rows } = await app.db.query(
'INSERT INTO connectors (user_id, type, config) VALUES ($1, $2, $3) RETURNING *',
[user_id, type, JSON.stringify(config || {})]
);
return { data: rows[0] };
});
// Sync connector
app.post('/connectors/:id/sync', async (req) => {
const { rows } = await app.db.query('SELECT * FROM connectors WHERE id = $1', [req.params.id]);
if (!rows.length) throw { statusCode: 404, message: 'Connector not found' };
// TODO: dispatch sync job to n8n or Redis queue
await app.db.query('UPDATE connectors SET last_sync_at = NOW() WHERE id = $1', [req.params.id]);
return { status: 'sync_started', connector: rows[0].type };
});
// Generic webhook
app.post('/connectors/webhook/:type', async (req) => {
const { type } = req.params;
// Log webhook
app.log.info({ type, body: req.body }, 'Webhook received');
// TODO: process based on type (odoo, moodle, pohoda, generic)
return { status: 'received', type };
});
// Toggle connector
app.put('/connectors/:id/toggle', async (req) => {
const { rows } = await app.db.query(
'UPDATE connectors SET enabled = NOT enabled, updated_at = NOW() WHERE id = $1 RETURNING id, type, enabled',
[req.params.id]
);
if (!rows.length) throw { statusCode: 404, message: 'Connector not found' };
return { data: rows[0] };
});
}
module.exports = connectorRoutes;

62
api/src/routes/groups.js Normal file
View File

@@ -0,0 +1,62 @@
// Task Team — Groups CRUD — 2026-03-29
async function groupRoutes(app) {
app.get('/groups', async (req) => {
const { rows } = await app.db.query(
'SELECT * FROM task_groups ORDER BY order_index ASC'
);
return { data: rows };
});
app.get('/groups/:id', async (req) => {
const { rows } = await app.db.query('SELECT * FROM task_groups WHERE id = $1', [req.params.id]);
if (!rows.length) throw { statusCode: 404, message: 'Group not found' };
return { data: rows[0] };
});
app.post('/groups', async (req) => {
const { name, color, icon, order_index, time_zones, user_id } = req.body;
const { rows } = await app.db.query(
'INSERT INTO task_groups (user_id, name, color, icon, order_index, time_zones) VALUES ($1,$2,$3,$4,$5,$6) RETURNING *',
[user_id, name, color, icon || '', order_index || 0, JSON.stringify(time_zones || [])]
);
return { data: rows[0] };
});
app.put('/groups/:id', async (req) => {
const { name, color, icon, order_index, time_zones } = req.body;
const { rows } = await app.db.query(
`UPDATE task_groups SET name=COALESCE($1,name), color=COALESCE($2,color), icon=COALESCE($3,icon),
order_index=COALESCE($4,order_index), time_zones=COALESCE($5,time_zones), updated_at=NOW()
WHERE id=$6 RETURNING *`,
[name, color, icon, order_index, time_zones ? JSON.stringify(time_zones) : null, req.params.id]
);
if (!rows.length) throw { statusCode: 404, message: 'Group not found' };
return { data: rows[0] };
});
app.delete('/groups/:id', async (req) => {
const { rowCount } = await app.db.query('DELETE FROM task_groups WHERE id = $1', [req.params.id]);
if (!rowCount) throw { statusCode: 404, message: 'Group not found' };
return { status: 'deleted' };
});
app.put('/groups/reorder', async (req) => {
const { order } = req.body; // [{id, order_index}]
const client = await app.db.connect();
try {
await client.query('BEGIN');
for (const item of order) {
await client.query('UPDATE task_groups SET order_index=$1, updated_at=NOW() WHERE id=$2', [item.order_index, item.id]);
}
await client.query('COMMIT');
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release();
}
return { status: 'ok' };
});
}
module.exports = groupRoutes;

88
api/src/routes/tasks.js Normal file
View File

@@ -0,0 +1,88 @@
// Task Team — Tasks CRUD — 2026-03-29
async function taskRoutes(app) {
// List tasks
app.get('/tasks', async (req, reply) => {
const { status, group_id, limit = 50, offset = 0 } = req.query;
let query = 'SELECT t.*, tg.name as group_name, tg.color as group_color, tg.icon as group_icon FROM tasks t LEFT JOIN task_groups tg ON t.group_id = tg.id WHERE 1=1';
const params = [];
if (status) { params.push(status); query += ` AND t.status = $${params.length}`; }
if (group_id) { params.push(group_id); query += ` AND t.group_id = $${params.length}`; }
query += ' ORDER BY t.scheduled_at ASC NULLS LAST, t.priority DESC, t.created_at DESC';
params.push(limit); query += ` LIMIT $${params.length}`;
params.push(offset); query += ` OFFSET $${params.length}`;
const { rows } = await app.db.query(query, params);
return { data: rows, total: rows.length };
});
// Get single task
app.get('/tasks/:id', async (req) => {
const { rows } = await app.db.query(
'SELECT t.*, tg.name as group_name, tg.color as group_color FROM tasks t LEFT JOIN task_groups tg ON t.group_id = tg.id WHERE t.id = $1',
[req.params.id]
);
if (!rows.length) throw { statusCode: 404, message: 'Task not found' };
return { data: rows[0] };
});
// Create task
app.post('/tasks', async (req) => {
const { title, description, status, group_id, priority, scheduled_at, due_at, assigned_to } = req.body;
const { rows } = await app.db.query(
`INSERT INTO tasks (title, description, status, group_id, priority, scheduled_at, due_at, assigned_to)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`,
[title, description || '', status || 'pending', group_id, priority || 'medium', scheduled_at, due_at, assigned_to || []]
);
return { data: rows[0] };
});
// Update task
app.put('/tasks/:id', async (req) => {
const fields = req.body;
const sets = [];
const params = [];
let i = 1;
for (const [key, value] of Object.entries(fields)) {
if (['title','description','status','group_id','priority','scheduled_at','due_at','assigned_to','completed_at'].includes(key)) {
sets.push(`${key} = $${i}`);
params.push(value);
i++;
}
}
if (fields.status === 'completed' && !fields.completed_at) {
sets.push(`completed_at = NOW()`);
}
sets.push(`updated_at = NOW()`);
params.push(req.params.id);
const { rows } = await app.db.query(
`UPDATE tasks SET ${sets.join(', ')} WHERE id = $${i} RETURNING *`, params
);
if (!rows.length) throw { statusCode: 404, message: 'Task not found' };
return { data: rows[0] };
});
// Delete task
app.delete('/tasks/:id', async (req) => {
const { rowCount } = await app.db.query('DELETE FROM tasks WHERE id = $1', [req.params.id]);
if (!rowCount) throw { statusCode: 404, message: 'Task not found' };
return { status: 'deleted' };
});
// Task comments
app.get('/tasks/:id/comments', async (req) => {
const { rows } = await app.db.query(
'SELECT * FROM task_comments WHERE task_id = $1 ORDER BY created_at ASC', [req.params.id]
);
return { data: rows };
});
app.post('/tasks/:id/comments', async (req) => {
const { content, is_ai } = req.body;
const { rows } = await app.db.query(
'INSERT INTO task_comments (task_id, content, is_ai) VALUES ($1, $2, $3) RETURNING *',
[req.params.id, content, is_ai || false]
);
return { data: rows[0] };
});
}
module.exports = taskRoutes;

137
collab-worker.py Normal file
View File

@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""
Task Team Collab Worker — HTTP-based agent that polls mngmt for tasks,
executes them with Claude CLI, reports results back.
"""
import os
import sys
import time
import json
import subprocess
import logging
import signal
from datetime import datetime
import requests
# Config
MNGMT_URL = os.environ.get("COLLAB_URL", "https://mngmt.it-enterprise.pro")
TOKEN = os.environ.get("COLLAB_TOKEN", "")
HOSTNAME = os.environ.get("HOSTNAME", "unknown")
CLAUDE_BIN = os.environ.get("CLAUDE_BIN", "/usr/bin/claude")
WORK_DIR = os.environ.get("WORK_DIR", "/opt/task-team")
POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL", "30"))
API = f"{MNGMT_URL}/api/v2/collab"
HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
log = logging.getLogger("collab-worker")
running = True
def stop(sig, frame):
global running
running = False
signal.signal(signal.SIGTERM, stop)
signal.signal(signal.SIGINT, stop)
def heartbeat():
try:
r = requests.post(f"{API}/heartbeat", headers=HEADERS,
json={"node": HOSTNAME, "server": "taskteam"}, timeout=10)
return r.status_code == 200
except Exception as e:
log.warning(f"Heartbeat failed: {e}")
return False
def get_tasks():
try:
r = requests.get(f"{API}/tasks", headers=HEADERS, timeout=10)
if r.status_code == 200:
data = r.json()
return [t for t in data.get("data", []) if t.get("status") == "open" and
(t.get("assignee") == HOSTNAME or t.get("assignee") is None)]
return []
except Exception as e:
log.warning(f"Get tasks failed: {e}")
return []
def claim_task(task_id):
try:
r = requests.post(f"{API}/tasks/{task_id}/claim", headers=HEADERS, timeout=10)
return r.status_code == 200
except:
return False
def submit_result(task_id, result, status="completed"):
try:
r = requests.post(f"{API}/tasks/{task_id}/submit", headers=HEADERS,
json={"result": result, "status": status}, timeout=30)
return r.status_code == 200
except Exception as e:
log.warning(f"Submit failed: {e}")
return False
def execute_claude(prompt, work_dir=WORK_DIR, max_turns=10):
"""Execute Claude CLI and return output."""
cmd = [CLAUDE_BIN, "--dangerously-skip-permissions",
"-p", prompt, "--max-turns", str(max_turns)]
try:
result = subprocess.run(cmd, capture_output=True, text=True,
timeout=600, cwd=work_dir, errors='replace')
output = result.stdout[-10000:] if len(result.stdout) > 10000 else result.stdout
if result.returncode != 0 and result.stderr:
output += f"\n[STDERR]: {result.stderr[-2000:]}"
return output, result.returncode == 0
except subprocess.TimeoutExpired:
return "[ERROR] Claude CLI timed out after 600s", False
except Exception as e:
return f"[ERROR] {str(e)}", False
def process_task(task):
task_id = task["id"]
title = task.get("title", "")
description = task.get("description", "")
log.info(f"Processing task #{task_id}: {title}")
if not claim_task(task_id):
log.warning(f"Failed to claim task #{task_id}")
return
prompt = f"TASK: {title}\n\nDESCRIPTION:\n{description}\n\nExecute this task and report results."
output, success = execute_claude(prompt)
ts = datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC")
feedback = f"{ts}\n💻 {HOSTNAME}\n{'🟢' if success else '🔴'} {'Done' if success else 'Failed'}\n\n{output}"
submit_result(task_id, feedback, "completed" if success else "failed")
log.info(f"Task #{task_id} {'completed' if success else 'failed'}")
def main():
log.info(f"Collab Worker started: {HOSTNAME}, polling every {POLL_INTERVAL}s")
if not TOKEN:
log.error("COLLAB_TOKEN not set!")
sys.exit(1)
while running:
heartbeat()
tasks = get_tasks()
for task in tasks:
if not running:
break
process_task(task)
for _ in range(POLL_INTERVAL):
if not running:
break
time.sleep(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,109 @@
-- Task Team — Initial Schema — 2026-03-29
-- Extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Users
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email VARCHAR(255) UNIQUE,
phone VARCHAR(50),
name VARCHAR(255) NOT NULL,
avatar_url TEXT,
auth_provider VARCHAR(50) DEFAULT 'email',
auth_provider_id TEXT,
language VARCHAR(5) DEFAULT 'cs',
settings JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Task groups
CREATE TABLE task_groups (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
color VARCHAR(20) NOT NULL,
icon VARCHAR(10),
order_index INTEGER DEFAULT 0,
time_zones JSONB DEFAULT '[]',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tasks
CREATE TABLE tasks (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
group_id UUID REFERENCES task_groups(id) ON DELETE SET NULL,
title VARCHAR(500) NOT NULL,
description TEXT DEFAULT '',
status VARCHAR(20) DEFAULT 'pending',
priority VARCHAR(10) DEFAULT 'medium',
scheduled_at TIMESTAMPTZ,
due_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
assigned_to UUID[],
attachments JSONB DEFAULT '[]',
external_id TEXT,
external_source VARCHAR(50),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Task comments
CREATE TABLE task_comments (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
task_id UUID REFERENCES tasks(id) ON DELETE CASCADE,
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
content TEXT NOT NULL,
is_ai BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Goals
CREATE TABLE goals (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
title VARCHAR(500) NOT NULL,
target_date TIMESTAMPTZ,
progress_pct INTEGER DEFAULT 0,
group_id UUID REFERENCES task_groups(id) ON DELETE SET NULL,
plan JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Connectors
CREATE TABLE connectors (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(50) NOT NULL,
config JSONB DEFAULT '{}',
enabled BOOLEAN DEFAULT true,
last_sync_at TIMESTAMPTZ,
sync_log JSONB DEFAULT '[]',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_tasks_user ON tasks(user_id);
CREATE INDEX idx_tasks_group ON tasks(group_id);
CREATE INDEX idx_tasks_status ON tasks(status);
CREATE INDEX idx_tasks_scheduled ON tasks(scheduled_at);
CREATE INDEX idx_task_groups_user ON task_groups(user_id);
CREATE INDEX idx_task_comments_task ON task_comments(task_id);
CREATE INDEX idx_goals_user ON goals(user_id);
-- Insert default groups
INSERT INTO task_groups (id, user_id, name, color, icon, order_index, time_zones) VALUES
(uuid_generate_v4(), NULL, 'Prace', '#3B82F6', 'B', 0, '[]'),
(uuid_generate_v4(), NULL, 'Nakup', '#10B981', 'S', 1, '[]'),
(uuid_generate_v4(), NULL, 'Study', '#8B5CF6', 'L', 2, '[]'),
(uuid_generate_v4(), NULL, 'Plany', '#F59E0B', 'M', 3, '[]'),
(uuid_generate_v4(), NULL, 'Sport', '#F97316', 'R', 4, '[]'),
(uuid_generate_v4(), NULL, 'Duchovni', '#D4A017', 'D', 5, '[]'),
(uuid_generate_v4(), NULL, 'Domaci', '#92400E', 'H', 6, '[]'),
(uuid_generate_v4(), NULL, 'Relax', '#06B6D4', 'X', 7, '[]');