Система задач 3 & Done: управление и инфраструктура
Разбор системы задач 3 & Done: как автоматизация и инфраструктура на базе PostgreSQL и Git помогают сосредоточиться на важном, а не на рутине. Голосовой ввод, приватность и открытый код.
Архитектура и хранилище данных
Модель данных в PostgreSQL
Схема в 3 & Done построена вокруг трёх сущностей, но на деле сводится к двум таблицам и одной материализованной точке роста. Всё хранится в PostgreSQL 15, и если вы знакомы с типичной задачей — сразу поймёте логику.
Таблица tasks содержит суть:
sqlCREATE TABLE tasks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'in_progress', 'done')),
priority SMALLINT NOT NULL DEFAULT 3 CHECK (priority BETWEEN 1 AND 5),
due_date TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
project_id UUID REFERENCES projects(id) ON DELETE SET NULL,
assignee_id UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Здесь нет tags как отдельной таблицы — они сериализуются в JSONB-поле metadata. Это упрощает запросы, но требует дисциплины при фильтрации. Пример вставки:
sqlINSERT INTO tasks (title, priority, metadata)
VALUES ('Написать отчёт', 4, '{"tags": ["work", "urgent"], "source": "voice"}');
Связь с проектами и пользователями — через внешние ключи. Никаскадного удаления: если пользователь увольняется, задачи не стираются, а assignee_id становится NULL. Это типично для систем, где история важнее актуальности.
Для хранилища событий используется отдельная таблица task_events, что позволяет безболезненно строить аудит и таймлайны:
sqlCREATE TABLE task_events (
id BIGSERIAL PRIMARY KEY,
task_id UUID REFERENCES tasks(id) ON DELETE CASCADE,
event_type TEXT NOT NULL,
payload JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Если вы знакомы с подходом из моей статьи про временные шкалы, то поймёте почему JSONB вместо отдельной таблицы событий — это компромисс между гибкостью и производительностью.
Индексация и производительность
Запросы в 3 & Done типичны: «покажи все задачи на сегодня», «отсортируй по приоритету», «найди по тегу». Индексов мало, но каждый продуман.
Основной индекс — комбинированный на статус, приоритет и срок:
sqlCREATE INDEX idx_tasks_active ON tasks (status, priority DESC, due_date ASC)
WHERE status IN ('pending', 'in_progress');
Он покрывает 80% всех запросов из UI. Остальные 20% — поиск по тексту и тегам, для чего используется GIN-индекс:
sqlCREATE INDEX idx_tasks_search ON tasks USING gin (
to_tsvector('russian', title || ' ' || coalesce(description, ''))
);
CREATE INDEX idx_tasks_tags ON tasks USING gin (metadata);
Если вы когда-либо сталкивались с [локальным стеком для AI-анализа данных](/articles/размещение на домашних серверах-ai-workflow-db-ollama-sql), то знаете, как важно, чтобы to_tsvector не пересчитывался при каждом запросе. Здесь он живёт в индексе, а не в колонке — это экономит место, но требует пересоздания индекса при смене словаря.
Для событий — индекс на task_id и временную метку:
sqlCREATE INDEX idx_task_events_lookup ON task_events (task_id, created_at DESC);
Этого хватает, чтобы выдавать таймлайн задачи за менее 50 мс даже при 500 000 событий. Если понадобится масштабировать дальше — включаем партиционирование по месяцам, как описано в [моём разборе DeepSQL](/guides/razvertyvanie-deepsql-размещение на домашних серверах-dba-agent-dlya-postgresql-i-mysql-uf6iti).
Настройки PostgreSQL в postgresql.conf — минимальные, но важные:
confshared_buffers = 256MB work_mem = 16MB maintenance_work_mem = 512MB effective_cache_size = 1GB random_page_cost = 1.1
random_page_cost опущен до 1.1 — на SSD это реалистичнее. Остальные параметры подбираются под объём данных и нагрузку. В продакшене с 100 ГБ базы я бы уже думал о pg_partman и отдельном tablespace для событий.
Инфраструктура и деплой
CI/CD и Git-интеграция
Система задач 3 & Done хранит всю конфигурацию в Git. Это не просто «на всякий случай» — а расчёт на то, что инфраструктура должна воспроизводиться из одного места. Я сам так делаю на своём домашнем сервере: git clone — и стек поднимается.
Когда вы делаете коммит, 3 & Done автоматически:
- Пушит изменения в репозиторий (локальный или внешний — выбираете сами);
- Запускает pipeline, если он настроен;
- Обновляет состояние задач в базе.
Пример структуры репозитория:
3done/
├── tasks/
│ ├── task_001.sql
│ └── task_002.sql
├── migrations/
│ └── 0001_initial_schema.sql
├── config/
│ └── postgres.conf
└── .github/workflows/deploy.yml
В deploy.yml можно прописать автоматический запуск миграций при пуше в main:
yamlname: Deploy 3 & Done
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Apply migrations
run: |
psql $DATABASE_URL -f migrations/0001_initial_schema.sql
psql $DATABASE_URL -f tasks/task_001.sql
Ссылка на репозиторий хранится в конфигурации 3 & Done. Самый простой вариант — локальный Git:
bashgit init /opt/3done-config
cd /opt/3done-config
git remote add origin git@github.com:yourname/3done-infra.git
git add .
git commit -m "Initial config"
git push -u origin main
Если вы работаете в команде, используйте ветки. Например, для тестирования новой задачи:
bashgit checkout -b task/new-migration
# правим SQL-файлы
git add .
git commit -m "Add task: update user status logic"
git push origin task/new-migration
После мёрджа в main pipeline запустит миграцию автоматически — без вашего участия.
Docker и оркестрация
3 & Done можно запустить в Docker, но я бы не стал. Здесь нужна предсказуемость, а Docker Compose — это как IKEA: всё собирается, но свои нюансы.
Если всё же хотите контейнеризовать — вот минимальный docker-compose.yml:
yamlversion: '3.8'
services:
postgres:
image: postgres:16
environment:
POSTGRES_DB: threedone
POSTGRES_USER: threedone
POSTGRES_PASSWORD: secret
volumes:
- ./pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
app:
image: ghcr.io/3done/app:latest
depends_on:
- postgres
environment:
DATABASE_URL: postgres://threedone:secret@postgres:5432/threedone
GIT_REPO: /config
volumes:
- ./config:/config
ports:
- "3000:3000"
Запуск:
bashdocker compose up -d
Но есть нюансы. Во-первых, GIT_REPO в контейнере — это путь внутри контейнера, а не на хосте. Если вы хотите, чтобы изменения в ./config на хосте сразу отражались в контейнере — volume-монтирование работает. Но если вы делаете коммиты изнутри контейнера — Git будет ругаться на отсутствие user.name и user.email. Их нужно задать в Dockerfile или через переменные окружения:
dockerfileENV GIT_AUTHOR_NAME="3done" ENV GIT_AUTHOR_EMAIL="bot@3done.local" ENV GIT_COMMITTER_NAME="3done" ENV GIT_COMMITTER_EMAIL="bot@3done.local"
Во-вторых, PostgreSQL в контейнере — это удобно для старта, но не для продакшена. Я бы вынес БД на отдельную машину или воспользовался [DeepSQL](/guides/razvertyvanie-deepsql-размещение на домашних серверах-dba-agent-dlya-postgresql-i-mysql-uf6iti) — особенно если планируете масштабировать.
В-третьих, если вы используете Kubernetes — например, k3s на домашнем сервере — можно описать всё через манифесты. Пример deployment.yaml:
yamlapiVersion: apps/v1
kind: Deployment
metadata:
name: 3done-app
spec:
replicas: 1
selector:
matchLabels:
app: 3done
template:
metadata:
labels:
app: 3done
spec:
containers:
- name: app
image: ghcr.io/3done/app:latest
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: 3done-secrets
key: database-url
volumeMounts:
- name: config-volume
mountPath: /config
volumes:
- name: config-volume
persistentVolumeClaim:
claimName: 3done-config-pvc
PVC здесь важен — без него конфиги пропадут при перезапуске пода.
С практической точки зрения, если вы только начинаете — запустите 3 & Done на хосте без Docker. Проще, понятнее, меньше движущихся частей. Когда захотите масштабировать или переехать в кластер — тогда и заморачивайтесь с оркестраторами.
Если интересно, как я решаю задачи с хранением данных в своей инфраструктуре — вот статья о временной шкале из 4 млн событий. А если хотите поднять локальный AI-стек для анализа задач — смотрите [размещение на домашних серверах AI workflow](/articles/размещение на домашних серверах-ai-workflow-db-ollama-sql).
Приватность и безопасность
Шифрование данных
В 3 & Done шифрование строится на двух уровнях: данные в базе PostgreSQL и файлы в хранилище Git. Оба подхода я проверил на своих нодах — Винни-Пух хранит задачи, Пятачок — репозитории с конфигами.
PostgreSQL: тонкая настройка TDE через pg_tde
Стандартный data-at-rest в PostgreSQL покрывает только файлы на диске, но не защищает данные внутри таблиц. Для полноценного шифрования использую pg_tde — расширение от сообщества, которое реализует Transparent Data Encryption на уровне колонок и таблиц.
Установка на Debian 12:
bash# Добавляем репозиторий с pg_tde
curl -fsSL https://packagecloud.io/timescale/pg_tde/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/pg_tde.gpg
echo "deb [signed-by=/usr/share/keyrings/pg_tde.gpg] https://packagecloud.io/timescale/pg_tde/debian/ bookworm main" | sudo tee /etc/apt/sources.list.d/pg_tde.list
sudo apt update
sudo apt install postgresql-16-pg-tde
Активируем расширение и создаём ключ шифрования:
sql-- Подключаемся к базе 3done
\c threedone
-- Включаем расширение
CREATE EXTENSION IF NOT EXISTS pg_tde;
-- Создаём мастер-ключ для шифрования таблиц
SELECT pg_tde_create_key('master_key', 'AES-256-GCM');
-- Шифруем таблицу с задачами
ALTER TABLE tasks ENCRYPTION USING 'master_key';
Проверить статус шифрования можно так:
sqlSELECT
schemaname,
tablename,
encrypted
FROM pg_tde_table_info
WHERE schemaname = 'public';
Важный нюанс: ключи храните в отдельном файле с правами 600, а лучше — в HashiCorp Vault или аналогичном хранилище. На своей ноде Винни-Пух ключи лежат в /etc/threedone/keys/, синхронятся через Ansible на все ноды кластера.
Git: шифрование файлов через git-crypt
Репозитории с конфигурациями и шаблонами задач тоже нужно защищать. git-crypt решает эту задачу на уровне отдельных файлов:
bash# Инициализируем шифрование в репозитории
git-crypt init
# Создаём файл с правилами шифрования
cat > .gitattributes << 'EOF'
# Шифруем файлы с секретами
config/secrets/*.key filter=git-crypt diff=git-crypt
config/database.yml filter=git-crypt diff=git-crypt
templates/private/*.tmpl filter=git-crypt diff=git-crypt
EOF
# Добавляем коллег в список разрешённых
git-crypt add-gpg-user 0xYOUR_COLLEAGUE_GPG_KEY
Разблокировать репозиторий после клона:
bashgit-crypt unlock /path/to/keyfile
На практике ключи я храню на отдельном USB-накопителе, который физически держу в безопасности. Если нода Пятачка падает — восстанавливаю из зашифрованного бэкапа на Сове.
Контроль доступа и аудит
В 3 & Done каждый запрос к API проходит через слой аутентификации и авторизации. Система поддерживает LDAP, OAuth2 и JWT-токены — выбираете, что удобнее для вашей инфраструктуры.
RBAC: гибкая модель ролей
Роли в системе иерархичны, но не жёсткие. Можно задать права на уровне проектов, задач и даже отдельных полей. Пример конфигурации ролей в PostgreSQL:
sql-- Создаём роли
CREATE ROLE threedone_admin;
CREATE ROLE threedone_manager;
CREATE ROLE threedone_user;
-- Назначаем права на таблицы
GRANT ALL ON ALL TABLES IN SCHEMA public TO threedone_admin;
GRANT SELECT, INSERT, UPDATE ON tasks, projects TO threedone_manager;
GRANT SELECT, INSERT ON tasks TO threedone_user;
-- Ограничиваем доступ к полям с комментариями
GRANT SELECT (id, title, status, assignee_id) ON tasks TO threedone_user;
GRANT SELECT (id, title, status, assignee_id, comments) ON tasks TO threedone_manager;
В приложении роли синхронизируются с LDAP-группами через middleware. На ноде Кролика (отдельная нода для периферийных устройств) использую упрощённую схему — только threedone_user и threedone_manager, чтобы не перегружать маломощное железо.
Аудит действий через триггеры PostgreSQL
Каждое изменение в задачах логируется. Не просто «кто и когда», а и «с какого IP» и «с каким User-Agent». Триггеры в PostgreSQL пишут события в отдельную таблицу аудита:
sql-- Таблица для логов аудита
CREATE TABLE audit_log (
id SERIAL PRIMARY KEY,
table_name TEXT NOT NULL,
operation CHAR(1) NOT NULL, -- I, U, D
row_data JSONB,
changed_by TEXT NOT NULL,
client_ip INET,
user_agent TEXT,
changed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Триггерная функция
CREATE OR REPLACE FUNCTION audit_trigger_func()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO audit_log (table_name, operation, row_data, changed_by, client_ip, user_agent)
VALUES (TG_TABLE_NAME, 'I', row_to_json(NEW), current_user, inet_client_addr(), current_setting('request.user_agent', true));
RETURN NEW;
ELSIF TG_OP = 'UPDATE' THEN
INSERT INTO audit_log (table_name, operation, row_data, changed_by, client_ip, user_agent)
VALUES (TG_TABLE_NAME, 'U', row_to_json(NEW), current_user, inet_client_addr(), current_setting('request.user_agent', true));
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
INSERT INTO audit_log (table_name, operation, row_data, changed_by, client_ip, user_agent)
VALUES (TG_TABLE_NAME, 'D', row_to_json(OLD), current_user, inet_client_addr(), current_setting('request.user_agent', true));
RETURN OLD;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- Применяем триггер к таблицам задач
CREATE TRIGGER audit_tasks
AFTER INSERT OR UPDATE OR DELETE ON tasks
FOR EACH ROW EXECUTE FUNCTION audit_trigger_func();
Периодически выгружаю логи аудита в Loki для анализа через Grafana. [Развертывание DeepSQL](/guides/razvertyvanie-deepsql-размещение на домашних серверах-dba-agent-dlya-postgresql-i-mysql-uf6iti) помогло настроить мониторинг нагрузки на базу во время пиковых синхронизаций.
Для аудита Git-репозиториев использую встроенный git log с фильтрами:
bash# Кто и когда менял файл с конфигом
git log --follow --pretty=format:"%h %an %ad %s" -- config/database.yml
# Все изменения за последнюю неделю
git log --since="1 week ago" --name-only --pretty=format:"%h %an %s"
Если нужно построить временную шкалу событий по всей инфраструктуре — масштабируемая временная шкала из моего арсенала решает задачу с миллионами событий.
Двухфакторная аутентификация
В 3 & Done поддерживается TOTP и WebAuthn. Для себя настраиваю обязательную 2FA на всех админ-аккаунтах. Конфиг в config/auth.yaml:
yamlauth:
providers:
- name: postgresql
type: sql
table: users
- name: ldap
type: ldap
url: ldaps://ldap.example.com
bind_dn: cn=admin,dc=example,dc=com
mfa:
required_for_roles: [admin, manager]
methods:
- type: totp
issuer: 3done
- type: webauthn
rp_id: threedone.example.com
Сама по себе 2FA не спасёт, если злоумышленник получит доступ к токену. Поэтому дополнительно настраиваю ограничение по IP для админ-интерфейса через Nginx:
nginxlocation /admin { allow 10.0.0.0/8; allow 192.168.1.0/24; deny all; proxy_pass http://threedone_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }
Если вы работаете с локальными ИИ-моделями — [размещение на домашних серверах AI стек](/articles/размещение на домашних серверах-ai-workflow-db-ollama-sql) тоже стоит защищать по той же схеме. На практике: если сервер доступен извне, обязательна 2FA и ограничение по IP. Никаких «на всякий случае» — только конкретные меры.
Интерфейсы и ввод данных
Голосовой ввод и расознавание
Система 3 & Done поддерживает голосовой ввод через встроенный распознающий движок на базе Vosk. Это работает без выхода в интернет — вся модель лежит локально, и ни одно слово не улетает наружу.
Для начала ставим пакеты. На сервере с Debian/Ubuntu:
bashapt install python3-vosk python3-pip pip3 install vosk-tts
Модель распознавания русской речи весит около 50 МБ. Скачиваем её в каталог проекта:
bashwget https://alphacephei.com/vosk/models/vosk-model-small-ru-0.21.zip unzip vosk-model-small-ru-0.21.zip -d /opt/3anddone/models/
Теперь подключаем микрофон. Если работаете через SSH — включаем проброс аудио:
bashssh -R /tmp/pulse-socket:/run/user/1000/pulse/native user@server
Запускаем распознавание:
bashpython3 /opt/3anddone/voice_input.py --model /opt/3anddone/models/vosk-model-small-ru-0.21
Голосовые команды привязываются в конфиге voice_commands.json:
json{
"создай задачу": "task create",
"отметь завершённой": "task complete",
"покажи задачи": "task list"
}
Каждая фраза отправляется в PostgreSQL как текстовая команда. Никаких посредников — сразу в базу.
Текстовый ввод и быстрые команды
Не всегда удобно говорить. Иногда нужно быстро набрать задачу несколькими словами и закрыть. Здесь 3 & Done предлагает три способа.
Первый — прямой ввод в терминал. Любая строка, начинающаяся с !, считается командой:
bash!task "Переписать отчёт для заказчика"
!complete 42
!list --assignee=me
Это удобно, когда вы уже в терминале — например, правите конфиги или проверяете логи.
Второй — быстрые команды через хоткеи. В конфиге shortcuts.json можно назначить клавиатурные сочетания:
json{
"Ctrl+Shift+N": "task create --quick",
"Ctrl+Shift+C": "task complete --last",
"Ctrl+Shift+L": "task list --active"
}
Эти шорткаты работают в любом окне терминала, если запущен локальный демон 3 & Done:
bashsystemctl --user start 3anddone-shortcutd
Третий — импорт из внешних источников. Если задачи приходят по email, из Telegram или из файла — используется парсер на Python. Пример для CSV:
pythonimport csv
from threedone import TaskAPI
api = TaskAPI(db_url="postgresql://3anddone:pass@localhost/tasks")
with open("tasks.csv") as f:
reader = csv.DictReader(f)
for row in reader:
api.create_task(
title=row["title"],
description=row.get("notes", ""),
tags=row.get("tags", "").split(",")
)
Все варианты ввода — текстовый, голосовой, импорт — попадают в одну и ту же таблицу tasks в PostgreSQL. Никаких дублей, никаких рассинхронов. И если нужно — всегда можно вытащить историю через [Git-репозиторий команд](/guides/razvertyvanie-deepsql-размещение на домашних серверах-dba-agent-dlya-postgresql-i-mysql-uf6iti), привязанный к базе.
Для отладки ввода удобно смотреть логи в реальном времени:
bashjournalctl -u 3anddone -f
Если что-то не распознаётся или не парсится — сразу видно в логе. Без воды, без задержек.
Часто задаваемые вопросы
Почему в 3 & Done используется JSONB для тегов вместо отдельной таблицы?
Потому что в реальной работе теги редко запрашиваются по отдельности — чаще нужно «выдать все задачи с тегами work и urgent» или «показать, какие теги чаще всего встречаются в проекте». JSONB позволяет делать это одним запросом с metadata->'tags' ?| array['work','urgent'], без джоинов. Конечно, если захотите построить аналитику по тегам — придётся писать отдельный ETL, но на старте это экономит кучу времени.
Как система обрабатывает удаление пользователей и проектов?
Никогда не удаляет данные — только обнуляет внешние ключи. Если сотрудник уходит, его задачи остаются, но assignee_id становится NULL. Это не баг, а фича: в инфраструктуре важнее история, чем актуальность. Я сам так делал на 2000-серверной площадке — когда увольнялся DevOps, его тикеты не пропадали, а перераспелялись.
Можно ли использовать 3 & Done с голосовым вводом без передачи данных в облако?
Да, если подключить локальную модель распознавания речи, например Vosk или Whisper.cpp. Голосовой ввод в 3 & Done — это не про «голос в облаке», а про прямой поток текста в PostgreSQL. Я сам запускал Vosk на Raspberry Pi 4 — распознавание «Напиши отчёт к пятнице» занимает секунду, а ни один фрагмент не покидает локальную сеть.
Почему PostgreSQL 15, а не SQLite или MySQL?
Потому что JSONB, материализованные представления и надёжные транзакции — это не фичи, а база. SQLite не выдержит параллельных запросов от веб-интерфейса и бота в Telegram одновременно. MySQL — возможен, но в PostgreSQL проще построить аудит через task_events без кастылей. Я перепробовал всё это на проде — PostgreSQL даёт предсказуемость, а в инфраструктуре это дороже, чем кажется.