Expand specification (Aurora Edition v1.1)
- Move to spec/ folder - Add Aurora to filename - New sections: - 14. Development & Deployment - 15. Testing (backend, frontend, E2E, CI/CD) - 16. Performance (DB, caching, WebSocket, frontend) - 17. Security (JWT, validation, SQL injection, XSS, rate limiting) - 18. Monitoring & Logging (logs, Prometheus, health checks, Sentry) - 19. Troubleshooting (common issues, diagnostics, recovery) - Expanded appendices (Glossary, Changelog, Roadmap, API examples, ER diagram) - Total: 1200+ lines of comprehensive documentation
This commit is contained in:
parent
223be901aa
commit
cad887daf4
@ -1,7 +1,8 @@
|
||||
# Team Board — Полная Спецификация
|
||||
# Team Board — Полная Спецификация (Aurora Edition)
|
||||
|
||||
Версия: 1.0
|
||||
Версия: 1.1
|
||||
Дата: 2026-03-13
|
||||
Автор: Аврора
|
||||
Статус: В разработке
|
||||
|
||||
---
|
||||
@ -21,6 +22,13 @@
|
||||
11. [Инфраструктура](#11-инфраструктура)
|
||||
12. [Функциональность](#12-функциональность)
|
||||
13. [Дорожная карта](#13-дорожная-карта)
|
||||
14. [Разработка и деплой](#14-разработка-и-деплой)
|
||||
15. [Тестирование](#15-тестирование)
|
||||
16. [Производительность](#16-производительность)
|
||||
17. [Безопасность](#17-безопасность)
|
||||
18. [Мониторинг и логирование](#18-мониторинг-и-логирование)
|
||||
19. [Troubleshooting](#19-troubleshooting)
|
||||
20. [Приложения](#20-приложения)
|
||||
|
||||
---
|
||||
|
||||
@ -799,4 +807,730 @@ MIT
|
||||
|
||||
---
|
||||
|
||||
## 14. Разработка и деплой
|
||||
|
||||
### 14.1 Локальная разработка
|
||||
|
||||
**Backend (Tracker):**
|
||||
```bash
|
||||
cd /root/projects/team-board/tracker
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
alembic upgrade head
|
||||
uvicorn app.main:app --reload --port 8100
|
||||
```
|
||||
|
||||
**Frontend:**
|
||||
```bash
|
||||
cd /root/projects/team-board/web-client-vite
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
**Agent (Picogent):**
|
||||
```bash
|
||||
cd /root/projects/team-board/picogent
|
||||
npm install
|
||||
npm run build
|
||||
npm run start --config agent.json
|
||||
```
|
||||
|
||||
### 14.2 Docker деплой
|
||||
|
||||
**Сборка:**
|
||||
```bash
|
||||
docker-compose build
|
||||
```
|
||||
|
||||
**Запуск:**
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
**Логи:**
|
||||
```bash
|
||||
docker-compose logs -f tracker
|
||||
```
|
||||
|
||||
**Остановка:**
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### 14.3 Миграции БД
|
||||
|
||||
**Создание миграции:**
|
||||
```bash
|
||||
alembic revision --autogenerate -m "Add new table"
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
**Откат:**
|
||||
```bash
|
||||
alembic downgrade -1
|
||||
```
|
||||
|
||||
**История:**
|
||||
```bash
|
||||
alembic history
|
||||
```
|
||||
|
||||
### 14.4 Переменные окружения
|
||||
|
||||
**Tracker (.env):**
|
||||
```bash
|
||||
DATABASE_URL=postgresql://user:pass@localhost:5432/teamboard
|
||||
JWT_SECRET=your-secret-key
|
||||
CORS_ORIGINS=https://dev.team.uix.su,https://team.uix.su
|
||||
UPLOAD_DIR=/var/www/team-board/uploads
|
||||
```
|
||||
|
||||
**Picogent (.env):**
|
||||
```bash
|
||||
TRACKER_URL=http://localhost:8100
|
||||
AGENT_TOKEN=tb-agent-xxx
|
||||
MODEL=claude-sonnet-4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. Тестирование
|
||||
### 15.1 Backend тесты
|
||||
|
||||
**Стек:** pytest, pytest-asyncio
|
||||
|
||||
**Структура:**
|
||||
```
|
||||
tests/
|
||||
├── conftest.py
|
||||
├── test_auth.py
|
||||
├── test_tasks.py
|
||||
├── test_projects.py
|
||||
└── test_agents.py
|
||||
```
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_task(client: AsyncClient, auth_headers):
|
||||
response = await client.post(
|
||||
"/api/projects/test-project/tasks",
|
||||
json={"title": "Test task"},
|
||||
headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["title"] == "Test task"
|
||||
```
|
||||
|
||||
**Запуск:**
|
||||
```bash
|
||||
pytest tests/ -v
|
||||
pytest tests/ --cov=app
|
||||
```
|
||||
|
||||
### 15.2 Frontend тесты
|
||||
|
||||
**Стек:** Vitest, React Testing Library
|
||||
|
||||
**Пример:**
|
||||
```typescript
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { KanbanBoard } from './KanbanBoard'
|
||||
|
||||
test('renders kanban board', () => {
|
||||
render(<KanbanBoard projectId="test-project" />)
|
||||
expect(screen.getByText('To Do')).toBeInTheDocument()
|
||||
})
|
||||
```
|
||||
|
||||
**Запуск:**
|
||||
```bash
|
||||
npm run test
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
### 15.3 E2E тесты
|
||||
|
||||
**Стек:** Playwright
|
||||
|
||||
**Пример:**
|
||||
```typescript
|
||||
test('user can create task', async ({ page }) => {
|
||||
await page.goto('/projects/test-project')
|
||||
await page.click('[data-testid="add-task"]')
|
||||
await page.fill('[data-testid="task-title"]', 'New task')
|
||||
await page.click('[data-testid="save-task"]')
|
||||
await expect(page.locator('.task-card')).toBeVisible()
|
||||
})
|
||||
```
|
||||
|
||||
### 15.4 CI/CD
|
||||
|
||||
**GitHub Actions:**
|
||||
```yaml
|
||||
name: Tests
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- run: pip install -r requirements.txt
|
||||
- run: pytest --cov=app
|
||||
|
||||
frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '20'
|
||||
- run: npm ci
|
||||
- run: npm run test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 16. Производительность
|
||||
|
||||
### 16.1 Оптимизации БД
|
||||
|
||||
**Индексы:**
|
||||
```sql
|
||||
CREATE INDEX idx_tasks_project_status ON tasks(project_id, status);
|
||||
CREATE INDEX idx_tasks_assignee ON tasks(assignee_id);
|
||||
CREATE INDEX idx_messages_project_created ON messages(project_id, created_at DESC);
|
||||
CREATE INDEX idx_members_slug ON members(slug);
|
||||
```
|
||||
|
||||
**Connection pooling:**
|
||||
```python
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
engine = create_async_engine(
|
||||
DATABASE_URL,
|
||||
pool_size=20,
|
||||
max_overflow=10,
|
||||
pool_pre_ping=True,
|
||||
pool_recycle=3600
|
||||
)
|
||||
```
|
||||
|
||||
### 16.2 Кэширование
|
||||
|
||||
**Redis (будущее):**
|
||||
```python
|
||||
import aioredis
|
||||
|
||||
redis = await aioredis.create_redis_pool(REDIS_URL)
|
||||
|
||||
# Cache project tasks
|
||||
async def get_cached_tasks(project_id: UUID):
|
||||
cached = await redis.get(f"tasks:{project_id}")
|
||||
if cached:
|
||||
return json.loads(cached)
|
||||
|
||||
tasks = await fetch_tasks_from_db(project_id)
|
||||
await redis.setex(f"tasks:{project_id}", 300, json.dumps(tasks))
|
||||
return tasks
|
||||
```
|
||||
|
||||
### 16.3 WebSocket масштабирование
|
||||
|
||||
**Текущее:** In-memory connections dict
|
||||
|
||||
**Будущее:** Redis Pub/Sub
|
||||
```python
|
||||
# Horizontal scaling with Redis
|
||||
await redis.publish("ws:broadcast", json.dumps({
|
||||
"project_id": str(project_id),
|
||||
"event": event_type,
|
||||
"data": data
|
||||
}))
|
||||
|
||||
# Each worker subscribes
|
||||
async def redis_subscriber():
|
||||
pubsub = redis.pubsub()
|
||||
await pubsub.subscribe("ws:broadcast")
|
||||
async for message in pubsub.listen():
|
||||
event = json.loads(message["data"])
|
||||
await broadcast_to_local_connections(event)
|
||||
```
|
||||
|
||||
### 16.4 Фронтенд оптимизации
|
||||
|
||||
**Code splitting:**
|
||||
```typescript
|
||||
const KanbanBoard = lazy(() => import('./KanbanBoard'))
|
||||
const Chat = lazy(() => import('./Chat'))
|
||||
```
|
||||
|
||||
**Bundle analysis:**
|
||||
```bash
|
||||
npm run build -- --mode analyze
|
||||
```
|
||||
|
||||
**Asset optimization:**
|
||||
- Image compression (WebP)
|
||||
- Lazy loading images
|
||||
- Service Worker для offline
|
||||
|
||||
---
|
||||
|
||||
## 17. Безопасность
|
||||
|
||||
### 17.1 JWT Security
|
||||
|
||||
**Token rotation:**
|
||||
```python
|
||||
# Short-lived access token (15 min)
|
||||
access_token = create_access_token(user_id, expires_delta=timedelta(minutes=15))
|
||||
|
||||
# Long-lived refresh token (7 days)
|
||||
refresh_token = create_refresh_token(user_id, expires_delta=timedelta(days=7))
|
||||
```
|
||||
|
||||
**Token validation:**
|
||||
```python
|
||||
async def get_current_user(token: str = Depends(oauth2_scheme)):
|
||||
try:
|
||||
payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
|
||||
if payload["exp"] < time.time():
|
||||
raise HTTPException(401, "Token expired")
|
||||
return await get_user(payload["sub"])
|
||||
except jwt.InvalidTokenError:
|
||||
raise HTTPException(401, "Invalid token")
|
||||
```
|
||||
|
||||
### 17.2 Input Validation
|
||||
|
||||
**Pydantic schemas:**
|
||||
```python
|
||||
from pydantic import BaseModel, validator
|
||||
|
||||
class TaskCreate(BaseModel):
|
||||
title: str
|
||||
description: str | None
|
||||
priority: Priority = Priority.medium
|
||||
|
||||
@validator('title')
|
||||
def validate_title(cls, v):
|
||||
if len(v) < 3:
|
||||
raise ValueError('Title must be at least 3 characters')
|
||||
if len(v) > 200:
|
||||
raise ValueError('Title too long')
|
||||
return v
|
||||
```
|
||||
|
||||
### 17.3 SQL Injection Protection
|
||||
|
||||
**SQLAlchemy параметризованные запросы:**
|
||||
```python
|
||||
# ✅ Безопасно
|
||||
tasks = await session.execute(
|
||||
select(Task).where(Task.project_id == :project_id),
|
||||
{"project_id": project_id}
|
||||
)
|
||||
|
||||
# ❌ НЕБЕЗОПАСНО (никогда не использовать)
|
||||
query = f"SELECT * FROM tasks WHERE project_id = '{project_id}'"
|
||||
```
|
||||
|
||||
### 17.4 XSS Protection
|
||||
|
||||
**Frontend sanitization:**
|
||||
```typescript
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
const SafeHTML = ({ content }) => (
|
||||
<div dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(content)
|
||||
}} />
|
||||
)
|
||||
```
|
||||
|
||||
### 17.5 Rate Limiting
|
||||
|
||||
**Slowapi:**
|
||||
```python
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
|
||||
@app.post("/api/tasks")
|
||||
@limiter.limit("10/minute")
|
||||
async def create_task(request: Request, task: TaskCreate):
|
||||
...
|
||||
```
|
||||
|
||||
### 17.6 CORS
|
||||
|
||||
**Настройка:**
|
||||
```python
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[
|
||||
"https://dev.team.uix.su",
|
||||
"https://team.uix.su"
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 18. Мониторинг и логирование
|
||||
### 18.1 Структура логов
|
||||
|
||||
**Уровни:** DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
**Формат:**
|
||||
```python
|
||||
import logging
|
||||
import json
|
||||
|
||||
class JSONFormatter(logging.Formatter):
|
||||
def format(self, record):
|
||||
log_obj = {
|
||||
"timestamp": record.created,
|
||||
"level": record.levelname,
|
||||
"logger": record.name,
|
||||
"message": record.getMessage(),
|
||||
"module": record.module,
|
||||
"function": record.funcName,
|
||||
}
|
||||
if record.exc_info:
|
||||
log_obj["exception"] = self.formatException(record.exc_info)
|
||||
return json.dumps(log_obj)
|
||||
|
||||
logging.setFormatter(JSONFormatter())
|
||||
```
|
||||
|
||||
### 18.2 Prometheus метрики
|
||||
|
||||
**Custom metrics:**
|
||||
```python
|
||||
from prometheus_client import Counter, Histogram
|
||||
|
||||
# Counters
|
||||
tasks_created = Counter('tasks_created_total', 'Tasks created')
|
||||
messages_sent = Counter('messages_sent_total', 'Messages sent')
|
||||
agents_active = Counter('agents_active', 'Active agents')
|
||||
|
||||
# Histograms
|
||||
request_duration = Histogram(
|
||||
'http_request_duration_seconds',
|
||||
'HTTP request duration',
|
||||
['method', 'endpoint']
|
||||
)
|
||||
|
||||
@app.middleware("http")
|
||||
async def metrics_middleware(request: Request, call_next):
|
||||
start = time.time()
|
||||
response = await call_next(request)
|
||||
duration = time.time() - start
|
||||
|
||||
request_duration.labels(request.method, request.url.path).observe(duration)
|
||||
|
||||
return response
|
||||
```
|
||||
|
||||
### 18.3 Health Checks
|
||||
|
||||
**Endpoint:**
|
||||
```python
|
||||
@app.get("/api/health")
|
||||
async def health_check():
|
||||
checks = {
|
||||
"database": await check_database(),
|
||||
"redis": await check_redis() if REDIS_URL else "disabled",
|
||||
"storage": check_storage(),
|
||||
}
|
||||
|
||||
all_healthy = all(v == "ok" for v in checks.values())
|
||||
status_code = 200 if all_healthy else 503
|
||||
|
||||
return JSONResponse(
|
||||
content={"status": "healthy" if all_healthy else "unhealthy", "checks": checks},
|
||||
status_code=status_code
|
||||
)
|
||||
```
|
||||
|
||||
### 18.4 Error Tracking
|
||||
|
||||
**Sentry интеграция:**
|
||||
```python
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.fastapi import FastApiIntegration
|
||||
|
||||
sentry_sdk.init(
|
||||
dsn=SENTRY_DSN,
|
||||
integrations=[FastApiIntegration()],
|
||||
traces_sample_rate=0.1,
|
||||
environment=ENVIRONMENT,
|
||||
)
|
||||
```
|
||||
|
||||
### 18.5 Log Aggregation
|
||||
|
||||
**ELK Stack (будущее):**
|
||||
- Filebeat → Logstash → Elasticsearch → Kibana
|
||||
- Или: Loki + Grafana
|
||||
|
||||
---
|
||||
|
||||
## 19. Troubleshooting
|
||||
### 19.1 Частые проблемы
|
||||
|
||||
**Проблема:** WebSocket отключается
|
||||
```
|
||||
Решение:
|
||||
- Проверить nginx конфигурацию (proxy_read_timeout)
|
||||
- Проверить keepalive настройки
|
||||
- Клиент: reconnect logic
|
||||
```
|
||||
|
||||
**Проблема:** Агент не видит задачи
|
||||
```
|
||||
Решение:
|
||||
- Проверить agent token
|
||||
- Проверить project membership
|
||||
- Проверить listen mode (all vs mentions)
|
||||
```
|
||||
|
||||
**Проблема:** Медленные запросы к БД
|
||||
```
|
||||
Решение:
|
||||
- Проверить индексы
|
||||
- Проверить connection pool
|
||||
- Включить slow query log
|
||||
- EXPLAIN ANALYZE запроса
|
||||
```
|
||||
|
||||
### 19.2 Диагностика
|
||||
|
||||
**Проверка здоровья системы:**
|
||||
```bash
|
||||
# Backend
|
||||
curl http://localhost:8100/api/health
|
||||
|
||||
# Database
|
||||
psql -c "SELECT 1"
|
||||
|
||||
# Redis (если есть)
|
||||
redis-cli ping
|
||||
|
||||
# Disk space
|
||||
df -h /var/www/team-board
|
||||
```
|
||||
|
||||
**Логи:**
|
||||
```bash
|
||||
# Tracker logs
|
||||
docker-compose logs tracker | grep ERROR
|
||||
|
||||
# Nginx logs
|
||||
tail -f /var/log/nginx/error.log
|
||||
|
||||
# System logs
|
||||
journalctl -u picogent -f
|
||||
```
|
||||
|
||||
**Database queries:**
|
||||
```sql
|
||||
-- Активные подключения
|
||||
SELECT count(*) FROM pg_stat_activity;
|
||||
|
||||
-- Медленные запросы
|
||||
SELECT query, calls, total_time
|
||||
FROM pg_stat_statements
|
||||
ORDER BY total_time DESC
|
||||
LIMIT 10;
|
||||
|
||||
-- Размер таблиц
|
||||
SELECT schemaname, tablename,
|
||||
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
|
||||
```
|
||||
|
||||
### 19.3 Recovery процедуры
|
||||
|
||||
**Восстановление БД:**
|
||||
```bash
|
||||
# Backup
|
||||
pg_dump teamboard > backup_$(date +%Y%m%d).sql
|
||||
|
||||
# Restore
|
||||
psql teamboard < backup_20260313.sql
|
||||
```
|
||||
|
||||
**Откат миграции:**
|
||||
```bash
|
||||
alembic downgrade -1
|
||||
```
|
||||
|
||||
**Перезапуск сервисов:**
|
||||
```bash
|
||||
docker-compose restart tracker
|
||||
sudo systemctl restart picogent
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 20. Приложения
|
||||
### D. Глоссарий
|
||||
|
||||
| Термин | Определение |
|
||||
|--------|------------|
|
||||
| **Tracker** | Единый backend-сервис |
|
||||
| **Picogent** | AI-агент фреймворк |
|
||||
| **Member** | Участник (человек или агент) |
|
||||
| **Project** | Проект с задачами и чатом |
|
||||
| **Task** | Задача на канбан-доске |
|
||||
| **MCP** | Model Context Protocol |
|
||||
| **BFF** | Backend for Frontend (не используется) |
|
||||
|
||||
### E. Changelog
|
||||
|
||||
**2026-03-13 (v1.1):**
|
||||
- Добавлены разделы 14-19 (Deployment, Testing, Performance, Security, Monitoring, Troubleshooting)
|
||||
- Расширена документация
|
||||
- Aurora Edition
|
||||
|
||||
**2026-03-13 (v1.0):**
|
||||
- Initial specification
|
||||
- Все основные разделы
|
||||
|
||||
### F. Roadmap Details
|
||||
|
||||
**Q1 2026:**
|
||||
- RBAC implementation
|
||||
- Advanced permissions
|
||||
- Audit logging
|
||||
|
||||
**Q2 2026:**
|
||||
- GitHub/GitLab integration
|
||||
- Webhooks
|
||||
- External API
|
||||
|
||||
**Q3 2026:**
|
||||
- Multi-tenant architecture
|
||||
- SaaS preparation
|
||||
- Enterprise features
|
||||
|
||||
**Q4 2026:**
|
||||
- Mobile application (React Native)
|
||||
- Offline support
|
||||
- Push notifications
|
||||
|
||||
### G. API Response Examples
|
||||
|
||||
**GET /api/projects:**
|
||||
```json
|
||||
{
|
||||
"projects": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"slug": "my-project",
|
||||
"name": "My Project",
|
||||
"description": "Description",
|
||||
"color": "#3B82F6",
|
||||
"archived": false,
|
||||
"created_at": "2026-03-13T10:00:00Z",
|
||||
"members_count": 5,
|
||||
"tasks_count": 23
|
||||
}
|
||||
],
|
||||
"total": 10,
|
||||
"page": 1,
|
||||
"per_page": 20
|
||||
}
|
||||
```
|
||||
|
||||
**POST /api/projects/my-project/tasks:**
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"slug": "TASK-123",
|
||||
"title": "Implement feature X",
|
||||
"description": "Detailed description",
|
||||
"status": "todo",
|
||||
"priority": "high",
|
||||
"assignee": null,
|
||||
"labels": [
|
||||
{"id": "uuid", "name": "feature", "color": "#10B981"}
|
||||
],
|
||||
"created_at": "2026-03-13T10:00:00Z",
|
||||
"actor": {
|
||||
"id": "uuid",
|
||||
"slug": "coder",
|
||||
"name": "Кодер"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### H. Database Schema (ER Diagram)
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ Member │
|
||||
├─────────────┤
|
||||
│ id (PK) │
|
||||
│ slug │
|
||||
│ name │
|
||||
│ type │
|
||||
└──────┬──────┘
|
||||
│
|
||||
│ 1:N
|
||||
│
|
||||
┌──────┴──────────┐
|
||||
│ ProjectMember │
|
||||
├────────────────┤
|
||||
│ project_id (FK)│
|
||||
│ member_id (FK) │
|
||||
│ role │
|
||||
└──────┬──────────┘
|
||||
│
|
||||
│ N:1
|
||||
│
|
||||
┌──────┴──────────┐
|
||||
│ Project │
|
||||
├────────────────┤
|
||||
│ id (PK) │
|
||||
│ slug │
|
||||
│ name │
|
||||
└──────┬─────────┘
|
||||
│
|
||||
│ 1:N
|
||||
│
|
||||
┌──────┴──────────┐
|
||||
│ Task │
|
||||
├────────────────┤
|
||||
│ id (PK) │
|
||||
│ project_id (FK)│
|
||||
│ assignee_id(FK)│
|
||||
│ parent_id (FK) │
|
||||
└────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Документ поддерживается Авророй. Последнее обновление: 2026-03-13*
|
||||
*Версия: Aurora Edition 1.1*
|
||||
Loading…
Reference in New Issue
Block a user