Реструктуризация: два глобальных раздела — Яша и Семен

Весь контент перенесён в content/yasha/ (7 подразделов).
Создан content/semen/ с подразделами: changelog, kb, diary.
Навигация — dropdown в навбаре, двухуровневое меню в hugo.toml.
Шаблоны, сайдбар, поиск обновлены под вложенные секции.
This commit is contained in:
Агальцов Антон
2026-04-26 19:44:52 +03:00
parent cc0b342208
commit dd67d2c2ec
249 changed files with 6900 additions and 9153 deletions
+4
View File
@@ -0,0 +1,4 @@
+++
title = "Яша"
draft = false
+++
+4
View File
@@ -0,0 +1,4 @@
+++
title = "Креды"
draft = false
+++
@@ -0,0 +1,46 @@
---
title: "docs.a2v.space Basic Auth"
date: 2026-03-29
tags: ["docs", "authentication", "nginx"]
description: "Учетные данные для доступа к docs.a2v.space"
---
## Basic Authentication
```bash
Username: agaltsovav
Password: Magamet787!
```
## Конфигурация Nginx
```nginx
server {
listen 8091;
server_name docs.a2v.space;
auth_basic "Restricted Access";
auth_basic_user_file /etc/nginx/.htpasswd;
}
```
## Доступ
- **URL**: https://docs.a2v.space (через reverse proxy)
- **Local**: http://localhost:8091 (для разработки)
- **Auth**: всегда требуется
## Мастер-пароль для шифрования
**Важно**: Пароль для расшифровки файлов в разделе `creds/`
```bash
Master Password: Popoloka16!
```
## Шифрование
- Алгоритм: AES-256-GCM
- Хранилище: client-side encryption
- Файлы шифруются при сохранении
- Расшифровываются при чтении
@@ -0,0 +1,37 @@
---
title: "Eversolo Companion PostgreSQL"
date: 2026-03-29
tags: ["eversolo", "companion", "postgresql", "docker"]
description: "Учетные данные для базы данных Eversolo Companion"
---
## Docker Compose креды (~/Projects/eversolo-companion/docker-compose.yml)
```yaml
services:
postgres:
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: eversolo
```
## Локальные креды (.env файл)
```bash
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password
POSTGRES_DB=eversolo
```
## Хостинг
- **Host**: localhost
- **Port**: 5433
- **Database**: eversolo
## Безопасность
- Используйте разные пароли для production
- Включите SSL для удаленного подключения
- Регулярно делайте бэкапы
+11
View File
@@ -0,0 +1,11 @@
---
title: "Mac Studio — leadera-api БД (docker-compose)"
description: "PostgreSQL (docker-compose) для Leadera"
service: "PostgreSQL"
date: 2026-03-29
tags: ["postgresql", "docker-compose", "leadera", "database"]
---
{{< encrypted-creds >}}
NYVK4PEGwmOHmAnGk8IAUc+lyFJtnEAvX6ZddBro/ILRWHOXBGYywgglFvQZ18BLDLb5DuA8GPAIdB/LwTHRXYO0KsmtlZQVWGEeeXlXAoEXpa5theoOQhebkW6LmiKyNGAVW2z9rK3nz6NnztYdn49LvGMjrHuR4IwCLbl/pYa96CPNRnxGDFlduFi8lMSspM0rBFi/bZhNGhzy36ViCl+aKr5P9cle0ZvM8E8av/6c5JjgDt88QFI3VYdTaTCE0+uHsXlnJN7Qgg/KH7kD
{{< /encrypted-creds >}}
+11
View File
@@ -0,0 +1,11 @@
---
title: "Mac Studio — leadera-api БД (локальная)"
description: "PostgreSQL (Docker) для Leadera"
service: "PostgreSQL"
date: 2026-03-29
tags: ["postgresql", "docker", "leadera", "database"]
---
{{< encrypted-creds >}}
p7Iw7Y3zbj1kLBcbOXGoO27oDwF85AhxBW7saT538hxi0ogJpyFeM49GDuLHzxuyknvdNC/rx3tb4AFnGkCeHSrrCmj+VK8Zxt7ujeujVJSRuOeeUlsqkHdlmaiXl2KQknrIsqVCi3Tnn0V5qcFxvo+1Ktc+surF6bBpWrTHzWV5mLtWVfD6C6kvvP/UFldFBD6v+FQs79yfoOe/j/bUrtpt6v5VndbbBmhUAQcy1BV2rpXxO373dTcLQ6coRWkt/RFo
{{< /encrypted-creds >}}
+11
View File
@@ -0,0 +1,11 @@
---
title: "VPN VPS"
description: "VPN сервер (Amnezia)"
service: "VPN"
date: 2026-03-29
tags: ["vpn", "amnezia", "infrastructure"]
---
{{< encrypted-creds >}}
YEI16NAJ78d3lmSa/lca8Zxczt+u6GyMUYHduX47VoFQdj0Ixozd8R6yXoB7eM+AjhCtNGkOxGlOj2s73ABOBjLEIVIhD4Z+xTyzdtah4VpyLoltYOeNYsnbIIFARdF/FBYq5COXohhXs33K5ozznrtDd/awbKMjl/sbQqjXAZ+A3ZSob6/5uU+fFKetXLR6v4ZLAYQ57iiD21ImSLSggwv+rJgK5XyPDKEPD4N5nOsiDrLgoukVBMvgJ54PqfPp2Hni+2tdKOlMnOG2npTuu4SkyALcfFar
{{< /encrypted-creds >}}
+11
View File
@@ -0,0 +1,11 @@
---
title: "VPS reg.ru"
description: "VPS хостинг (реверс-прокси)"
service: "VPS"
date: 2026-03-29
tags: ["vps", "reg.ru", "infrastructure"]
---
{{< encrypted-creds >}}
IpYXLMtgCFlGQ550DZCLjq37VsnNqL08Rn03iRS7kQ7ozBZNsiZySFzDAvYh4xIHytRDy0Sz0KGUXekr0S+cNiJsmtZ8kj0Pd0JhevhxTMZF2khFvs/LFWoTL5ZwAJ34y/qfLw2gaWX90VZDu0wO+KeAQeXhrWmol/2zUlfWX1AihyYqD2vRz+h2FcjD8ubYOXG35PyX7OlV1HPXgDv5X/bFNrxzPc867UOwB4aGxY9a
{{< /encrypted-creds >}}
+38
View File
@@ -0,0 +1,38 @@
---
title: "29 марта 2026"
date: 2026-03-29
tags: ["angular", "go", "docker", "eversolo", "hugo", "docs", "identity", "infrastructure", "companion"]
---
## Timeline
- **20:03** — Настроили Angular + Go проекты (leadera) через pm2, удалили proxy.conf, подняли PostgreSQL в Docker
- **20:31** — Изучили API Eversolo T8 — полный список REST-эндпоинтов для управления музыкой
- **20:49** — Обсудили важность записи контекста в файлы для сохранения памяти между сессиями
- **21:13** — Начали проект docs.a2v.space — персональная документация на Hugo
- **21:44** — Завершили настройку Identity (Братан, SOUL.md, USER.md, IDENTITY.md)
- **22:15** — Документировали инфраструктуру: VPS reverse proxy (claw.a2v.space, code.a2v.space, sand.a2v.space, docs.a2v.space)
- **22:31** — Настроили docs.a2v.space: Hugo сайт, 83 страницы, Bootstrap 5 тёмная тема, AES-256-GCM шифрование кредов (мастер-пароль: Popoloka16!)
- **22:47** — Добавили Basic auth: agaltsovav / Magamet787! для доступа к документации
- **23:02** — Создали cron задачу docs-a2v-daily-sync на ежедневное обновление в 03:00 МСК
- **23:18** — Полностью задокументировали Eversolo T8 API (воспроизведение, громкость, дисплей, питание, поиск и т.д.)
- **23:45** — Переписали TOOLS.md как полный справочник по инфраструктуре
## Eversolo Companion — статус проекта
**Backend (Go + Gin):**
- Репозиторий: ~/Projects/eversolo-companion
- Состояние: собран, 17+ REST API эндпоинтов, WebSocket, CORS, state poller
**Frontend (Angular 17):**
- Состояние: собран, тёмная тема (Spotify-like), 5 экранов, 5 сервисов
- Dev server: pm2 (eversolo-fe), порт 8096
**Инфраструктура:**
- PostgreSQL: Docker, порт 5433, миграции готовы
- Docker Compose: рабочий (postgres + api + frontend)
**Проблема:** нет доступа к Eversolo из OpenClaw sandbox (192.168.0.156)
- Решение: настройка tools.elevated.allowFrom.webchat или запуск через launchd
**Следующие шаги:** после решения сети — этапы 3-9 (библиотека sync, плейлисты, scrobbler, обложки)
+38
View File
@@ -0,0 +1,38 @@
---
title: "30 марта 2026"
date: 2026-03-30
tags: ["vikunja", "leadera", "tasktracker", "ollama", "qwen", "mtproxy", "infrastructure"]
---
## Timeline
- **Утро** — Установлен Vikunja (таск-трекер) на Mac Studio через Docker/Colima
- **День** — Создан проект «Leadera» в Vikunja (ID=2), добавлено 27 задач из аудита Leadera
- **День** — Начата реализация Leadera TaskTracker: миграции, модели, DTOs, repositories (бекенд), фронтенд (kanban, task-detail)
- **Вечер** — TaskTracker **откачён** по просьбе Антона — git checkout + git clean. Причина: Claude Code падал (SIGKILL), sub-agents не справлялись, Plane не запустился
- **Вечер** — Настроен MT Proxy на VPN VPS (185.234.64.52): порт 8443, секрет 9e40ae…
- **Вечер** — Qwen 3.5:9b подключён к OpenClaw через `openclaw models set ollama/qwen3.5:9b`, доступен для cron/spawn задач
- **Вечер** — Антон подключился к claw.a2v.space с мобильного, устройство одобрено
## Vikunja
- URL: http://192.168.0.37:8095
- Проект Leadera: ID=2
- API token: `tk_635fb7ac…`
- 27 задач из аудита, приоритеты P0–P3
## Leadera TaskTracker
- ТЗ: `leadera-tasktracker-spec.md`
- Реализация через sub-agents (GLM-5.1)
- **Откачено** — проекты возвращены к предыдущему коммиту
## MT Proxy
- Сервер: 185.234.64.52, порт 8443
- Docker контейнер: `mtproxy`
## Инфраструктура
- Qwen 3.5:9b доступен как дешёвая локальная модель для cron/spawn
- Мобильное подключение к claw.a2v.space подтверждено
+50
View File
@@ -0,0 +1,50 @@
---
title: "31 марта 2026"
date: 2026-03-31
tags: ["system-utils", "opencode", "leadera", "gantt", "canvas", "github", "roadmap"]
---
## Timeline
- **Утро** — Обсуждали аналоги htop: порекомендовал btm, btop, glances. Создан конфиг `~/.config/bottom/bottom.toml`
- **День** — Разбирались со скиллами OpenCode: как работают, автоматический подбор по description
- **День** — Созданы скиллы для Leadera проектов (Go: build-and-fix, restart-api; Angular: build-and-fix)
- **День** — Обсудили миграцию Ганта с DOM на Canvas 2D (Konva.js) — real-time коллаборация, экспорт PDF/PNG, 500+ задач
- **Вечер** — Установлен gh CLI, авторизован как agaltsovav
- **Вечер** — Созданы GitHub Projects: Leadera Roadmap (21 задача, P0P3), agaltsovav.ru Roadmap (7 задач)
## Системные утилиты
- **bottom** (btm) — выбранный мониторинг, конфиг в `~/.config/bottom/bottom.toml`
- Альтернативы: btop, glances
## OpenCode Skills
Созданы скиллы:
- `app-leadera-api/.opencode/skills/build-and-fix/` — сборка Go проекта + автофикс ошибок (до 10 итераций)
- `app-leadera-api/.opencode/skills/restart-api/` — swagger генерация + pm2 restart
- `app-leadera-fe/.opencode/skills/build-and-fix/` — сборка Angular + автофикс
## Gantt Canvas миграция
- Переход с DOM на Canvas 2D через **Konva.js**
- Цели: real-time коллаборация, экспорт PDF/PNG, 500+ задач
- Промт: `GANTT_CANVAS_MIGRATION_PROMPT.md` в app-leadera-fe
## GitHub
- CLI: `gh` авторизован (agaltsovav)
- Репо: app-leadera-fe, app-leadera-api, agaltsovav.ru
- Старые: leadera-fe/leadera-api (на VPS, leadera.ru)
- modece.ru — заброшен, планируется возвращение
## GitHub Projects — Roadmaps
### Leadera Roadmap (project #2) — 21 задача
- **P0:** JWT secret, CORS, bcrypt, ContextRole fallback, RegisterProtectedRoutes
- **P1:** Rate limiting, editor role check, MembersModal rewrite
- **P2:** HttpOnly cookies, CSRF, CSP, password policy, audit log, health-check, CI/CD, Canvas миграция, DnD
- **P3:** Code cleanup, тесты, email verification, поиск
### agaltsovav.ru Roadmap (project #1) — 7 задач
- Редизайн, мобильная версия, мультиязычность, блог, CI/CD, тёмная тема, форма обратной связи
+4
View File
@@ -0,0 +1,4 @@
+++
title = "Дневник"
draft = false
+++
+4
View File
@@ -0,0 +1,4 @@
+++
title = "Инструкции"
draft = false
+++
+75
View File
@@ -0,0 +1,75 @@
---
title: "Подключение claw.a2v.space (OpenClaw)"
date: 2026-03-29
tags: ["openclaw", "ssh", "nginx", "vps", "reverse-proxy"]
---
## Архитектура
```
Mac Studio (192.168.0.37:18789)
→ autossh SSH-туннель
→ VPS 95.163.232.9:8080
→ nginx HTTPS
→ https://claw.a2v.space
```
## Шаги настройки
### 1. DNS
A-запись `claw.a2v.space``95.163.232.9`
NS-серверы: `ns1.hosting.reg.ru`, `ns2.hosting.reg.ru`
### 2. Mac Studio — OpenClaw
- Порт: 18789
- Конфиг: `~/.openclaw/openclaw.json`
### 3. SSH-туннель (launchd)
Plist: `~/Library/LaunchAgents/com.openclaw.sshtunnel.plist`
Ключевой флаг: `-R 8080:localhost:18789`
### 4. VPS — nginx конфиг
```nginx
server {
listen 80;
server_name claw.a2v.space;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name claw.a2v.space;
ssl_certificate /etc/letsencrypt/live/claw.a2v.space/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/claw.a2v.space/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
### 5. SSL-сертификат
```bash
certbot certonly --nginx -d claw.a2v.space
```
### 6. OpenClaw config
```json
{
"allowedOrigins": ["https://claw.a2v.space"],
"trustedProxies": ["127.0.0.1"]
}
```
+59
View File
@@ -0,0 +1,59 @@
---
title: "Подключение code.a2v.space (OpenCode)"
date: 2026-03-29
tags: ["opencode", "ssh", "nginx", "vps", "reverse-proxy"]
---
## Архитектура
```
Mac Studio (localhost:4096)
→ autossh SSH-туннель (-R 8081:localhost:4096)
→ VPS 95.163.232.9:8081
→ nginx HTTPS
→ https://code.a2v.space
```
## Параметры
| Параметр | Значение |
|----------|----------|
| Локальный порт | 4096 |
| Туннель порт | 8081 |
| pm2 процесс | `opencode` |
| SSL сертификат | `/etc/letsencrypt/live/code.a2v.space/` |
## nginx конфиг
```nginx
server {
listen 80;
server_name code.a2v.space;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name code.a2v.space;
ssl_certificate /etc/letsencrypt/live/code.a2v.space/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/code.a2v.space/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
```
## SSL-сертификат
```bash
certbot certonly --nginx -d code.a2v.space
```
+73
View File
@@ -0,0 +1,73 @@
---
title: "Подключение sand.a2v.space (Leadera Project)"
date: 2026-03-29
tags: ["angular", "go", "docker", "nginx", "vps", "reverse-proxy"]
---
## Архитектура
```
Mac Studio
├─ Frontend (Angular) :4200 (pm2: leadera-fe)
├─ Backend (Go) :8080 (pm2: leadera-api)
└─ PostgreSQL 16 в Docker (Colima, container: app-leadera-db)
→ autossh SSH-туннель (-R 8082:localhost:4200 -R 8083:localhost:8080)
→ VPS 95.163.232.9
→ nginx HTTPS
→ https://sand.a2v.space
```
## Параметры
| Компонент | Локальный порт | Туннель порт | pm2 имя |
|-----------|----------------|--------------|---------|
| Frontend (Angular) | 4200 | 8082 | leadera-fe |
| Backend (Go) | 8080 | 8083 | leadera-api |
SSL сертификат: `/etc/letsencrypt/live/sand.a2v.space/`
## nginx конфиг
```nginx
server {
listen 80;
server_name sand.a2v.space;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name sand.a2v.space;
ssl_certificate /etc/letsencrypt/live/sand.a2v.space/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/sand.a2v.space/privkey.pem;
location /api {
proxy_pass http://127.0.0.1:8083;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://127.0.0.1:8082;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
## База данных
PostgreSQL 16 в Docker через Colima:
- Container: `app-leadera-db`
- Port: 5432
## SSL-сертификат
```bash
certbot certonly --nginx -d sand.a2v.space
```
+72
View File
@@ -0,0 +1,72 @@
---
title: "SSH туннели для VPS reverse proxy"
date: 2026-03-29
tags: ["ssh", "tunnel", "vps", "reverse-proxy"]
description: "Настройка SSH туннелей для доступа к сервисам на Mac Studio через VPS"
---
## Настройка SSH туннелей
### VPS reg.ru (95.163.232.9)
```bash
# На Mac Studio: создать туннель для каждого сервиса
ssh -N -R 8080:localhost:8080 root@95.163.232.9 # leadera-api
ssh -N -R 8081:localhost:4096 root@95.163.232.9 # opencode
ssh -N -R 8082:localhost:4200 root@95.163.232.9 # leadera-fe
ssh -N -R 8083:localhost:8080 root@95.163.232.9 # docs-a2v
```
### Настройка launchd plist
**Файл**: `~/Library/LaunchAgents/com.openclaw.sshtunnel.plist`
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.openclaw.sshtunnel</string>
<key>ProgramArguments</key>
<array>
<string>ssh</string>
<string>-N</string>
<string>-R</string>
<string>8080:localhost:8080</string>
<string>root@95.163.232.9</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
```
### Автозапуск
```bash
# Загрузить plist
launchctl load ~/Library/LaunchAgents/com.openclaw.sshtunnel.plist
# Проверить статус
launchctl list | grep sshtunnel
# Удалить (при необходимости)
launchctl unload ~/Library/LaunchAgents/com.openclaw.sshtunnel.plist
```
## Доступ к сервисам
Через reverse proxy на VPS:
- **code.a2v.space**: порт 8080 → Mac Studio:4096
- **claw.a2v.space**: порт 8081 → Mac Studio:8080 (leadera-api)
- **sand.a2v.space**: порт 8082 → Mac Studio:4200 (leadera-fe)
- **docs.a2v.space**: порт 8083 → Mac Studio:8080 (docs-a2v)
## SSL
Все сервисы используют Let's Encrypt сертификаты:
- Путь: `/etc/letsencrypt/live/{claw,code,sand}.a2v.space/`
- Автопродление через certbot
+102
View File
@@ -0,0 +1,102 @@
---
title: "Настройка VPS как реверс-прокси"
date: 2026-03-29
tags: ["vps", "nginx", "ssh", "reverse-proxy", "devops"]
---
## Общая схема
```
Mac Studio (локальная сеть)
→ autossh (SSH-туннели)
→ VPS 95.163.232.9 (Debian)
→ nginx (HTTPS reverse proxy)
→ интернет
```
## Что нужно на VPS
### 1. Установленные пакеты
```bash
apt update
apt install nginx certbot python3-certbot-nginx
```
### 2. SSH-доступ
- Авторизация только по ключам
- Публичный ключ с Mac Studio добавлен в `/root/.ssh/authorized_keys`
### 3. nginx
Для каждого домена создать server block:
- HTTP → редирект на HTTPS
- HTTPS с SSL-сертификатом Let's Encrypt
- `proxy_pass` на localhost-порт туннеля
Пример структуры:
```
/etc/nginx/sites-available/
claw.a2v.space.conf
code.a2v.space.conf
sand.a2v.space.conf
```
### 4. SSL-сертификаты
```bash
certbot certonly --nginx -d <domain>
certbot renew --dry-run # проверить автообновление
```
## Что нужно на Mac Studio
### autossh через launchd
Plist: `~/Library/LaunchAgents/com.openclaw.sshtunnel.plist`
Один plist с несколькими `-R` флагами:
```xml
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/autossh</string>
<string>-M</string>
<string>0</string>
<string>-R</string>
<string>8080:localhost:18789</string>
<string>-R</string>
<string>8081:localhost:4096</string>
<string>-R</string>
<string>8082:localhost:4200</string>
<string>-R</string>
<string>8083:localhost:8080</string>
<string>root@95.163.232.9</string>
<string>-N</string>
<string>-i</string>
<string>/Users/<user>/.ssh/id_rsa</string>
</array>
```
### Управление launchd
```bash
launchctl load ~/Library/LaunchAgents/com.openclaw.sshtunnel.plist
launchctl start com.openclaw.sshtunnel
launchctl stop com.openclaw.sshtunnel
```
## Проверка
```bash
# На VPS
ss -tlnp | grep -E '808[0-3]'
# Снаружи
curl -I https://claw.a2v.space
curl -I https://code.a2v.space
curl -I https://sand.a2v.space
```
+4
View File
@@ -0,0 +1,4 @@
+++
title = "База знаний"
draft = false
+++
+89
View File
@@ -0,0 +1,89 @@
---
title: "Eversolo T8 API"
date: 2026-03-29
tags: ["api", "eversolo", "music", "streamer"]
---
Eversolo T8 — музыкальный стример.
**IP:** 192.168.0.156
**Порт:** 9529
**Авторизация:** нет
Базовый URL: `http://192.168.0.156:9529`
## Воспроизведение
| Действие | URL |
|----------|-----|
| Play/Pause | `/ZidooMusicControl/v2/playOrPause` |
| Следующий трек | `/ZidooMusicControl/v2/playNext` |
| Предыдущий трек | `/ZidooMusicControl/v2/playLast` |
| Перемотка (ms) | `/ZidooMusicControl/v2/seekTo?time=<ms>` |
| Воспроизвести по ID | `/ZidooMusicControl/v2/playMusic?type=4&id=<id>` |
| Состояние | `/ZidooMusicControl/v2/getState` |
## Громкость
| Действие | URL |
|----------|-----|
| Громкость вверх | `/ZidooControlCenter/RemoteControl/sendkey?key=Key.VolumeUp` |
| Громкость вниз | `/ZidooControlCenter/RemoteControl/sendkey?key=Key.VolumeDown` |
| Точная громкость | `/ZidooMusicControl/v2/setDevicesVolume?volume=<0-200>` |
| Mute | `/ZidooMusicControl/v2/setMuteVolume?isMute=1` |
| Unmute | `/ZidooMusicControl/v2/setMuteVolume?isMute=0` |
## Входы/Выходы
| Действие | URL |
|----------|-----|
| Получить список | `/ZidooMusicControl/v2/getInputAndOutputList` |
| Установить вход | `/ZidooMusicControl/v2/setInputList?tag=<tag>&index=<n>` |
| Установить выход | `/ZidooMusicControl/v2/setOutInputList?tag=<tag>&index=<n>` |
Теги выхода: RCA, XLRRCA, XLR, HDMI, SPDIF, USB
## Дисплей
| Действие | URL |
|----------|-----|
| Яркость экрана (get) | `/SystemSettings/displaySettings/getScreenBrightness` |
| Яркость экрана (set) | `/SystemSettings/displaySettings/setScreenBrightness?index=<0-115>` |
| Яркость ручки (get) | `/SystemSettings/displaySettings/getKnobBrightness` |
| Яркость ручки (set) | `/SystemSettings/displaySettings/setKnobBrightness?index=<0-255>` |
| VU-режим (get) | `/SystemSettings/displaySettings/getVUModeList` |
| VU-режим (set) | `/SystemSettings/displaySettings/setVUMode?index=<n>` |
| Спектр (get) | `/SystemSettings/displaySettings/getSpPlayModeList` |
| Спектр (set) | `/SystemSettings/displaySettings/setSpPlayModeList?index=<n>` |
| Переключить VU/Спектр | `/ZidooMusicControl/v2/changVUDisplay?openType=<0|1>` |
## Питание
| Действие | URL |
|----------|-----|
| Параметры питания | `/ZidooMusicControl/v2/getPowerOption` |
| Перезагрузка | `/ZidooMusicControl/v2/setPowerOption?tag=reboot` |
| Выключение | `/ZidooMusicControl/v2/setPowerOption?tag=poweroff` |
| Переключить экран | `/ZidooMusicControl/v2/setPowerOption?tag=screen` |
| Включить экран | `/ZidooControlCenter/RemoteControl/sendkey?key=Key.Screen.ON` |
| Выключить экран | `/ZidooControlCenter/RemoteControl/sendkey?key=Key.Screen.OFF` |
## Поиск
| Поиск | URL |
|-------|-----|
| По трекам | `/ZidooMusicControl/v2/searchMusicV2?key=<text>&start=0&count=100` |
| По артистам | `/ZidooMusicControl/v2/searchArtistV2?key=<text>&start=0&count=100` |
| По альбомам | `/ZidooMusicControl/v2/searchAlbumV2?key=<text>&start=0&count=100` |
## Обложки и устройство
| Действие | URL |
|----------|-----|
| Обложка по ID | `/ZidooMusicControl/v2/getImage?id=<song_id>&target=16` |
| Модель устройства | `/ControlCenter/getModel` |
## Источники
- [Zidoo Forum](https://forum.zidoo.tv/index.php?threads/some-api.96983/)
- [Home Assistant Integration](https://github.com/hchris1/Eversolo/blob/main/custom_components/eversolo/api.py)
+11
View File
@@ -0,0 +1,11 @@
---
title: "Leadera"
weight: 1
---
# Leadera
SaaS-система управления IT-ландшафтом с диаграммами Ганта.
- **Прод:** [leadera.pro](https://leadera.pro)
- **Дев:** [sand.a2v.space](https://sand.a2v.space)
+294
View File
@@ -0,0 +1,294 @@
---
title: "Аудит Leadera — полный отчёт"
date: 2026-03-30
lastmod: 2026-03-30
tags: ["audit", "leadera", "security", "architecture"]
weight: 100
---
# Аудит проекта Leadera
**Дата:** 30.03.2026
**Версия:** pre-production, миграции 15
---
## 1. Обзор проекта
Leadera — SaaS-система управления IT-ландшафтом с диаграммами Ганта.
### Стек
| Компонент | Технологии |
|-----------|-----------|
| Backend | Go 1.21+, Gin, PostgreSQL 16, pgx/v5 + sqlx, JWT (HMAC-SHA256) |
| Frontend | Angular 21.1, Bootstrap 5, NgBootstrap, RxJS, Signals |
| Инфраструктура | Docker (Colima), pm2, nginx reverse proxy, VPS reg.ru |
### Доменная модель
```
User ──┬── Space (через space_members с role)
└── GanttChart (принадлежит Space)
├── GanttSection
└── GanttTask
```
**Роли:** admin, editor, viewer — назначаются **на уровне Space** через таблицу `space_members`.
### Что реализовано (endpoints)
**Auth (5):** register, login, refresh, change-password, logout
**Spaces (9):** CRUD + members CRUD
**Gantt Charts (9):** CRUD + archive + members (добавлен, но удалён в миграции 5)
**Gantt Sections (5):** CRUD + reorder
**Gantt Tasks (4):** CRUD
**Итого: ~32 endpoint**
---
## 2. Архитектура и качество кода
### 2.1. Backend — структура
```
cmd/server/main.go — точка входа
internal/
domain/ — модели (User, Space, GanttChart, etc.)
dto/ — request/response DTO (auth, space, gantt)
handlers/ — HTTP handlers (auth, space, gantt)
middleware/ — auth, cors, space, gantt, role, logging, recovery
repository/ — слой доступа к данным
service/ — бизнес-логика
config/ — конфигурация (.env)
utils/ — jwt, password, response, validation
migrations/ — SQL миграции (1-5)
```
**Оценка:** ✅ Чистая layered архитектура. Разделение ответственности чёткое.
### 2.2. Frontend — структура
```
src/app/
core/auth/ — auth service, interceptor
core/models/ — app.model, space.model
core/services/ — space.service, space-storage.service
pages/auth/ — sign-in, sign-up, reset-pw, etc.
pages/gantt/ — компоненты, модалки, сервисы, модели
pages/dashboards/ — spaces list, dashboard
layouts/ — navbar, topbar, main-layout
```
**Оценка:** ✅ Стандартная Angular-структура. Signals для state management.
### 2.3. Проблемы кода
| # | Проблема | Критичность | Описание |
|---|----------|-------------|----------|
| C1 | `\r` в service-файлах | Medium | gantt_service.go, gantt_section_service.go содержат CRLF |
| C2 | `main.go:238` — мёртвый код `getUserHandler` | Low | Неиспользуемая функция |
| C3 | Swagger не сгенерирован | Medium | Аннотации есть, но `api/` директории нет |
| C4 | `RegisterProtectedRoutes` не вызывается в main.go | **Critical** | GET/PUT/DELETE `/spaces/:id` не зарегистрированы |
---
## 3. Права доступа — главный вопрос
### 3.1. Текущая реализация
Права назначаются **исключительно через Space**:
1. Таблица `space_members` — связка user ↔ space с role (admin/editor/viewer)
2. `SpaceMiddleware.RequireSpace()` — проверяет членство и записывает `ContextSpaceRole` в context
3. `RoleMiddleware.RequireMinRole()` — проверяет иерархию ролей (admin > editor > viewer)
**Диаграммы Ганта НЕ имеют собственной системы ролей.** Таблица `gantt_chart_members` была создана (миграция 4) и тут же удалена (миграция 5). Все проверки доступа к диаграммам идут через space membership.
### 3.2. Как это работает
```
Запрос → AuthMiddleware (JWT → userID)
→ SpaceMiddleware.RequireSpace() (проверяет space_members → ContextSpaceRole)
→ GanttMiddleware.RequireGanttChart() (проверяет chart.space_id == spaceID)
→ RoleMiddleware.RequireMinRole("editor") (проверяет ContextSpaceRole)
```
**Это правильно.** Любой член Space с ролью editor+ может создавать/редактировать диаграммы. Viewers — только читать.
### 3.3. 🔴 Критическая проблема: Frontend не соответствует Backend
**Frontend `MembersModalComponent`** пытается управлять участниками **диаграммы**:
- `ganttService.addMember(chartId, dto)``/api/v1/spaces/{spaceId}/gantt-charts/{chartId}/members`
- `ganttService.updateMemberRole(chartId, userId, dto)``.../members/{userId}`
- `ganttService.removeMember(chartId, userId)``.../members/{userId}`
**Backend `GanttHandler.RegisterRoutes()` НЕ регистрирует маршруты для members диаграмм.** После удаления `gantt_chart_members` (миграция 5) эти endpoints не существуют.
**Результат:** модальное окно «Участники диаграммы» на фронтенде **не работает** — все вызовы вернут 404.
### 3.4. 🔴 Проблема: Role fallback
В `role_middleware.go:28-31`:
```go
role := c.GetString(ContextSpaceRole)
if role == "" {
role = c.GetString(ContextRole) // fallback на глобальную роль User
}
```
Если `ContextSpaceRole` не установлен, используется **глобальная роль пользователя** (из JWT). Это значит:
- Пользователь с `role=admin` в таблице `users` получит admin-доступ к **любому** space, даже если он не член этого space
- Но `SpaceMiddleware` проверяет членство раньше, так что в цепочке `RequireSpace() → RequireMinRole()` это не эксплуатируется
- Однако если `RequireMinRole()` используется **без** `RequireSpace()` — это дыра
**Рекомендация:** Удалить fallback на `ContextRole`. Если `ContextSpaceRole` пуст — отказывать.
### 3.5. Рекомендации по правам доступа
1. **Удалить модалку участников диаграммы** на фронтенде — она мертва
2. **Или** добавить раздел «Участники Space» с возможностью управления ролями через `/spaces/:space_id/members` (эти endpoints работают)
3. Удалить fallback на `ContextRole` в `role_middleware.go`
4. Добавить проверку: editor не может повысить кого-то до admin (сейчас может — `AddMember` разрешает editor+ назначать любую роль)
---
## 4. Безопасность
### 4.1. Критические
| # | Проблема | Файл | Описание |
|---|----------|------|----------|
| S1 | JWT Secret = test | `.env` | `JWT_SECRET=test_secret_key_change_in_production`**это прод-конфиг!** |
| S2 | CORS: `*` | `.env` | `CORS_ALLOWED_ORIGINS=*` — любой домен может обращаться к API |
| S3 | Пароль БД в .env | `.env` | `DB_PASSWORD=popoloka16` — creds в открытом виде |
| S4 | Refresh tokens: SHA256 | `utils/password.go` | SHA256 вместо bcrypt для refresh token hash — быстро брутфорсится |
### 4.2. Высокие
| # | Проблема | Описание |
|---|----------|----------|
| S5 | Нет rate limiting | Brute-force на `/auth/login` без ограничений |
| S6 | Нет CSRF protection | API принимает POST/PUT/DELETE без CSRF-токена |
| S7 | Нет CSP заголовков | X-Frame-Options, Content-Security-Policy не настроены |
| S8 | JWT в localStorage | XSS может украсть access + refresh tokens |
### 4.3. Средние
| # | Проблема | Описание |
|---|----------|----------|
| S9 | Нет password policy | Минимальные требования к паролю не enforced |
| S10 | Нет audit log | Действия с spaces/charts не логируются для аудита |
| S11 | Нет блокировки аккаунта | Неограниченные попытки входа |
---
## 5. Инфраструктура
### 5.1. Текущее состояние
| Компонент | Статус | Примечание |
|-----------|--------|------------|
| Go API (pm2: leadera-api) | ⚠️ | Порт 8080, хост 0.0.0.0 |
| Angular FE (pm2: leadera-fe) | ⚠️ | Порт 4200, хост 0.0.0.0 |
| PostgreSQL (Docker) | ✅ | Порт 5432, контейнер app-leadera-db |
| VPS reverse proxy | ⚠️ | sand.a2v.space → 8082/8083, но 502 если сервисы не запущены |
| SSL | ✅ | Let's Encrypt на VPS |
### 5.2. Проблемы
| # | Проблема | Описание |
|---|----------|----------|
| I1 | Docker для БД, нативно для API | Несогласованность — API и FE не в Docker |
| I2 | Нет health-check в pm2 | API может упасть без перезапуска |
| I3 | Нет CI/CD | Ручной деплой через `make run` + pm2 |
| I4 | Нет nginx на Mac Studio | FE и API торчат наружу напрямую |
---
## 6. Функциональная оценка
### 6.1. Что работает
| Функция | Backend | Frontend | Статус |
|---------|---------|----------|--------|
| Регистрация/логин | ✅ | ✅ | Работает |
| JWT auth + refresh | ✅ | ✅ | Работает |
| Spaces CRUD | ⚠️ | ✅ | `RegisterProtectedRoutes` не вызван (C4) |
| Space members | ✅ | ✅ | Работает |
| Gantt charts CRUD | ✅ | ✅ | Работает |
| Gantt sections | ✅ | ✅ | Работает |
| Gantt tasks | ✅ | ⚠️ | Нет UI для задач внутри диаграммы |
| Архивация диаграмм | ✅ | ✅ | Работает |
| Members диаграммы | ❌ | ✅ (мёртвый UI) | Backend endpoints не существуют |
### 6.2. Что НЕ работает или отсутствует
| Функция | Статус |
|---------|--------|
| Визуализация Ганта (таймлайн) | ❌ Нет компонента отрисовки |
| Drag & Drop задач | ❌ |
| Real-time collaboration | ❌ |
| Уведомления | ❌ |
| Поиск | ❌ |
| Экспорт (PDF/Excel) | ❌ |
| Email verification | ❌ |
| Password reset | Backend есть, UI есть — не проверено |
### 6.3. Оценка готовности
**Система на стадии каркаса.** Backend API функционален для CRUD операций, но:
- Визуализация Ганта — основная фича — не реализована (нет canvas/SVG рендеринга)
- Управление участниками только на уровне Space
- Нет CI/CD, тестов, мониторинга
---
## 7. Приоритизированный план исправлений
### Неделя 1 — Критическое (безопасность)
| Приоритет | Задача | Трудоёмкость |
|-----------|--------|--------------|
| P0 | Заменить JWT secret на криптостойкий | 1 час |
| P0 | Настроить CORS (только sand.a2v.space) | 1 час |
| P0 | bcrypt для refresh tokens вместо SHA256 | 2 часа |
| P0 | Удалить fallback на ContextRole в role_middleware | 30 мин |
| P0 | Исправить `RegisterProtectedRoutes` — зарегистрировать маршруты | 1 час |
| P1 | Rate limiting на auth endpoints | 4 часа |
| P1 | Editor не может назначать admin роль | 2 часа |
### Неделя 2 — Архитектура прав
| Приоритет | Задача | Трудоёмкость |
|-----------|--------|--------------|
| P1 | Удалить/переписать MembersModalComponent → Space members | 4 часа |
| P1 | Добавить страницу управления участниками Space | 8 часов |
| P2 | HttpOnly cookies для JWT (вместо localStorage) | 8 часов |
| P2 | CSRF protection | 4 часа |
### Неделя 3 — Функциональность
| Приоритет | Задача | Трудоёмкость |
|-----------|--------|--------------|
| P2 | Визуализация Ганта (canvas/d3) | 40+ часов |
| P2 | Drag & Drop для задач | 16 часов |
| P3 | Тесты (unit + integration) | 24 часа |
| P3 | CI/CD pipeline | 8 часов |
---
## 8. Резюме
### Ключевые выводы
1. **Права доступа** — архитектурно верно: роль назначается через Space, не через диаграмму. Но фронтенд ещё не обновлён после удаления `gantt_chart_members`.
2. **Безопасность** — критические уязвимости в конфигурации (JWT secret, CORS, bcrypt). Требуют немедленного исправления до любого публичного деплоя.
3. **Функциональность** — CRUD работает, но основная ценность (визуализация Ганта) не реализована.
4. **Код** — чистый, хорошая архитектура, но есть мёртвый код и незарегистрированные маршруты.
### Общая оценка: 🔴 Pre-alpha
Проект готов к внутренней демонстрации CRUD, но не готов к продакшену. Первоочередное — безопасность и исправление мёртвых endpoints.
@@ -0,0 +1,103 @@
---
title: "Leadera — Экспорт диаграммы Ганта в Excel"
date: 2026-04-09
lastmod: 2026-04-09
tags: ["leadera", "gantt", "excel", "export"]
weight: 120
---
# Экспорт диаграммы Ганта в Excel
**Дата:** 09.04.2026
**Статус:** Требования
**Проект:** Leadera
---
## Общее описание
Пользователь нажимает кнопку экспорта в интерфейсе диаграммы Ганта. Система запрашивает диапазон дат выгрузки (по умолчанию — текущий видимый диапазон из интерфейса). После указания диапазона скачивается Excel-файл с визуальным представлением диаграммы Ганта.
---
## Вводные данные
- **Источник:** текущая диаграмма Ганта проекта (один проект = одна диаграмма)
- **Диапазон дат:** пользователь указывает начальную и конечную дату. По умолчанию подставляется видимый в момент запроса диапазон из интерфейса
- **Масштаб таймлайна:** берётся тот, который выбран пользователем в интерфейсе в момент запроса (не сохранённый в системе, а текущий). Варианты: по ячейкам или по дням
---
## Структура Excel-файла
### Шапка (первые строки)
- Название диаграммы (название проекта)
- Диапазон дат, в рамках которого сделана выгрузка
### Лист — один
Вся диаграмма размещается на одном листе.
### Секции
- Секции отображаются как **визуальная группировка строк** (не отдельные листы)
- Название секции — строка-заголовок над группой задач
### Задачи
Каждая задача — отдельная строка. Данные задачи:
- **Название** (текстовая колонка слева)
- **Дата начала** (текстовая колонка слева)
- **Дата конца** (текстовая колонка слева)
- **Цвет** — ячейки таймлайна, соответствующие задаче, закрашены в цвет задачи
### Таймлайн
- Таймлайн представлен колонками дат в верхней части (горизонтальная ось времени)
- Масштаб колонок соответствует выбранному пользователем в момент экспорта (по ячейкам / по дням)
- Задачи отображаются как **merged cells** по соответствующим колонкам дат — визуальная полоса диаграммы Ганта
### Вехи
- Отображаются **отдельной строкой** (как задача, но с маркировкой вехи)
- Позиционируются на соответствующей дате в таймлайне
### Зависимости
- **Не отображаются** (стрелочки/связи между задачами не нужны)
---
## Пользовательский сценарий
1. Пользователь открывает диаграмму Ганта проекта
2. Настраивает масштаб таймлайна (по ячейкам / по дням)
3. Нажимает кнопку «Экспорт в Excel»
4. Система показывает диалог с выбором диапазона дат (предзаполнен текущим видимым диапазоном)
5. Пользователь подтверждает / корректирует диапазон
6. Система генерирует Excel-файл и инициирует скачивание
---
## Технические требования
### Backend (Go API)
- Новый endpoint для генерации Excel (например, `GET /api/v1/gantt/:projectId/export`)
- Query-параметры: `dateFrom`, `dateTo`, `scale` (cell|day)
- Ответ: binary Excel-файл (`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`)
- Авторизация: обязательна (только участники проекта)
- Библиотека для генерации Excel: `excelize` (Go)
### Frontend (Angular)
- Кнопка экспорта в тулбаре диаграммы Ганта
- Модальное окно выбора диапазона дат с datepicker'ами
- Предзаполнение диапазона из текущего видимого viewport'а
- Скачивание файла через `Blob` / `saveAs`
### Фильтрация данных
- Выгружаются все задачи и вехи, которые:
- Начинаются в диапазоне, ИЛИ
- Заканчиваются в диапазоне, ИЛИ
- Начинаются до диапазона и заканчиваются после (перекрывают целиком)
- Секции включаются, если в них есть хотя бы одна попавшая задача
---
## Открытые вопросы
_(Нет)_
@@ -0,0 +1,278 @@
---
title: "Leadera — Фильтры и реорганизация списка диаграмм Ганта"
date: 2026-04-07
lastmod: 2026-04-07
tags: ["leadera", "gantt", "api", "frontend", "requirements"]
weight: 120
---
# Фильтры и реорганизация списка диаграмм Ганта
**Дата:** 07.04.2026
**Статус:** Требования
**Проект:** Leadera (dev — sand.a2v.space)
---
## 1. Постановка задачи
Список диаграмм Ганта должен показывать **все доступные пользователю диаграммы** из всех пространств (spaces), с возможностью фильтрации и поиска. Сейчас список привязан к одному пространству — нужно убрать эту зависимость и добавить удобный toolbar.
---
## 2. Изменения API
### 2.1. Новый endpoint списка диаграмм
**Было:**
```
GET /api/v1/spaces/{spaceId}/gantt-charts?page=1&per_page=10
```
**Стало:**
```
GET /api/v1/gantt-charts?page=1&per_page=10
```
Пользователь видит все диаграммы из **всех пространств**, где он является подтверждённым участником (статус `accepted`).
### 2.2. Параметры фильтрации и поиска
| Параметр | Тип | Описание |
|----------|-----|----------|
| `page` | int | Номер страницы (default: 1) |
| `per_page` | int | Записей на странице (default: 10) |
| `space_id` | UUID (optional) | Фильтр по конкретному пространству |
| `created_by` | UUID (optional) | Фильтр по создателю диаграммы |
| `search` | string (optional) | Поиск по назвению и описанию (ILIKE, case-insensitive) |
| `sort` | string (optional) | Сортировка: `created_at`, `updated_at`, `title` (default: `updated_at`) |
| `order` | string (optional) | `asc` / `desc` (default: `desc`) |
**Примеры:**
```
GET /api/v1/gantt-charts?search=миграция
GET /api/v1/gantt-charts?space_id=0b334ded-...&created_by=a1b2c3d4-...
GET /api/v1/gantt-charts?page=2&per_page=20&sort=title&order=asc
```
### 2.3. Изменение структуры ответа
Добавить поле `space` в каждую диаграмму:
```json
{
"data": [
{
"id": "uuid",
"title": "Миграция backend",
"description": "...",
"created_by": "uuid",
"created_by_name": "Антон Агальцов",
"created_at": "2026-04-01T10:00:00Z",
"updated_at": "2026-04-07T09:00:00Z",
"space": {
"id": "uuid",
"name": "IT Ландшафт",
"color": "#c4623a"
},
"is_archived": false
}
],
"pagination": {
"page": 1,
"per_page": 10,
"total": 42,
"total_pages": 5
}
}
```
### 2.4. Endpoint для списка фильтров
Вспомогательный endpoint для заполнения dropdown-фильтров:
```
GET /api/v1/gantt-charts/filters
```
**Ответ:**
```json
{
"spaces": [
{ "id": "uuid", "name": "IT Ландшафт", "color": "#c4623a" },
{ "id": "uuid", "name": "DevOps", "color": "#27ae60" }
],
"creators": [
{ "id": "uuid", "display_name": "Антон Агальцов" },
{ "id": "uuid", "display_name": "Иван Петров" }
]
}
```
**Логика `creators`:** уникальные создатели всех диаграмм, доступных текущему пользователю. Получается через DISTINCT-запрос по `created_by` с JOIN на users.
### 2.5. Backend — изменения в Go
**Файлы для изменения:**
#### `internal/handlers/gantt_handler.go`
- Добавить новый handler `ListAllGanttCharts` — без привязки к space
- Зарегистрировать маршрут `GET /gantt-charts` (без `/spaces/:id/`)
- Парсить query-параметры: `space_id`, `created_by`, `search`, `sort`, `order`
- Старый маршрут `GET /spaces/:id/gantt-charts` — оставить для обратной совместимости (или удалить, если не используется)
#### `internal/service/gantt_service.go`
- Новый метод `ListAll(ctx, userId, filters)` — запрашивает все spaces пользователя (через `space_member` с `status=accepted`), затем выбирает все gantt_charts из этих spaces с фильтрами
#### `internal/repository/gantt_repository.go`
- Новый метод с динамическим SQL:
- Базовый JOIN: `gantt_charts``spaces``space_members`
- WHERE: `space_members.user_id = $1 AND space_members.status = 'accepted'`
- Опциональные фильтры: `AND space_id = $N`, `AND created_by = $N`, `AND (title ILIKE '%' || $N || '%' OR description ILIKE '%' || $N || '%')`
- Возвращать поле `space` (id, name, color) через JOIN
#### `internal/dto/gantt/`
- Новые DTO: `GanttChartListFilters`, `GanttChartWithSpaceResponse`, `GanttFiltersResponse`
**SQL (пример):**
```sql
SELECT gc.*, s.id as space_id, s.name as space_name, s.color as space_color,
u.display_name as created_by_name
FROM gantt_charts gc
JOIN spaces s ON s.id = gc.space_id
JOIN space_members sm ON sm.space_id = s.id
LEFT JOIN users u ON u.id = gc.created_by
WHERE sm.user_id = $1
AND sm.status = 'accepted'
AND ($2::uuid IS NULL OR gc.space_id = $2)
AND ($3::uuid IS NULL OR gc.created_by = $3)
AND ($4::text IS NULL OR gc.title ILIKE '%' || $4 || '%'
OR gc.description ILIKE '%' || $4 || '%')
ORDER BY
CASE WHEN $5 = 'title' THEN gc.title END,
CASE WHEN $5 = 'created_at' THEN gc.created_at::text END,
gc.updated_at DESC
LIMIT $6 OFFSET $7;
```
---
## 3. Изменения Frontend (Angular)
### 3.1. Toolbar фильтров
Добавить toolbar над списком диаграмм:
```
┌──────────────────────────────────────────────────────────────────┐
│ [🔍 Поиск... ] [▼ Пространство] │
│ [▼ Создатель] [▼ Сортировка] [Сбросить фильтры] │
└──────────────────────────────────────────────────────────────────┘
```
**Компоненты:**
- **Строка поиска** — input с debounce (300ms), поиск по назвению и описанию
- **Dropdown «Пространство»** — список из `/gantt-charts/filters`, с цветовой меткой
- **Dropdown «Создатель»** — список уникальных создателей
- **Dropdown «Сортировка»** — по дате обновления, дате создания, названию
- **Кнопка «Сбросить»** — очищает все фильтры
**Состояние фильтров в URL** (query params):
```
/gantt?search=миграция&space_id=xxx&sort=updated_at
```
Это позволяет делиться ссылкой с уже настроенными фильтрами.
### 3.2. Карточка диаграммы — бейдж пространства
На каждой карточке добавить бейдж с названием пространства:
```
┌─────────────────────────────────────┐
│ 🟠 IT Ландшафт │ ← бейдж с цветом пространства
│ Миграция backend │
│ Перевод PHP→Go, этап 2 │
│ Создал: Антон А. · 07.04.2026 │
└─────────────────────────────────────┘
```
### 3.3. Форма создания диаграммы — выбор пространства
В модальном окне создания:
- Dropdown со списком пространств, где пользователь имеет роль **editor** или выше
- Валидация: если пользователь не editor ни в одном пространстве — показать сообщение «Нет доступных пространств для создания»
**Запрос доступных пространств:** переиспользовать существующий `GET /api/v1/spaces` с фильтрацией на клиенте по `role ∈ [owner, admin, editor]`, либо добавить параметр `?min_role=editor` на backend.
### 3.4. Изменение routing
**Было:**
```
/gantt → список гантов текущего пространства
```
**Стало:**
```
/gantt → список всех доступных гантов (из всех пространств)
```
Убрать зависимость от `SpaceStorageService.getSelectedSpaceId()` в списке гантов.
### 3.5. Frontend — файлы для изменения
| Файл | Изменение |
|------|-----------|
| `pages/gantt/services/gantt.service.ts` | Новый метод `listAll(filters)`, `getFilters()` |
| `pages/gantt/gantt.component.ts` | Подключить toolbar, парсинг URL-параметров |
| `pages/gantt/gantt.component.html` | Добавить toolbar, бейджи на карточках |
| `pages/gantt/components/gantt-card/` | Добавить бейдж пространства |
| `pages/gantt/components/create-chart-modal/` | Dropdown выбора пространства |
| `core/models/` | Обновить модели: добавить `space` в GanttChart |
---
## 4. Обратная совместимость
| Endpoint | Статус |
|----------|--------|
| `GET /api/v1/spaces/{id}/gantt-charts` | Сохранить (для детального view внутри пространства) |
| `GET /api/v1/gantt-charts` | **Новый** — глобальный список |
| `GET /api/v1/gantt-charts/filters` | **Новый** — данные для dropdown'ов |
| `POST /api/v1/spaces/{id}/gantt-charts` | Сохранить (создание в контексте пространства) |
| Все CRUD для sections/tasks | Без изменений |
---
## 5. Критерии приёмки
- [ ] Endpoint `GET /api/v1/gantt-charts` возвращает все доступные диаграммы
- [ ] Фильтр по `space_id` — показывает только диаграммы конкретного пространства
- [ ] Фильтр по `created_by` — показывает только диаграммы выбранного создателя
- [ ] Поиск `search` — ищет по title и description (case-insensitive)
- [ ] Dropdown фильтров заполняется реальными данными из `/filters`
- [ ] Карточка диаграммы показывает бейдж пространства с цветом
- [ ] Форма создания предлагает только пространства с ролью ≥ editor
- [ ] Состояние фильтров сохраняется в URL (можно поделиться ссылкой)
- [ ] Старые endpoints (`/spaces/{id}/gantt-charts`) продолжают работать
- [ ] Swagger документация обновлена
---
## 6. Порядок реализации
1. Backend: DTO для новых запросов/ответов
2. Backend: Repository — SQL-запрос с фильтрами + JOIN на spaces
3. Backend: Service — `ListAll`, `GetFilters`
4. Backend: Handler — `ListAllGanttCharts`, `GetGanttFilters`, регистрация маршрутов
5. Backend: Swagger — обновить документацию
6. Frontend: Модели — обновить GanttChart, добавить Space в ответ
7. Frontend: Service — `listAll()`, `getFilters()`
8. Frontend: Toolbar компонент — поиск + dropdown'ы
9. Frontend: Карточка — бейдж пространства
10. Frontend: Модальное окно создания — dropdown пространства
11. Frontend: URL-параметры — синхронизация фильтров с router
12. Тестирование
@@ -0,0 +1,125 @@
---
title: "Leadera — Публичный доступ к диаграмме Ганта"
date: 2026-04-09
lastmod: 2026-04-09
tags: ["leadera", "gantt", "public-access", "sharing"]
weight: 130
---
# Публичный доступ к диаграмме Ганта (Share)
**Дата:** 09.04.2026
**Статус:** Требования
**Проект:** Leadera
---
## Общее описание
Пользователь может создать публичную ссылку на фрагмент диаграммы Ганта, доступную для просмотра без авторизации. Открывший ссылку видит диаграмму в read-only режиме с возможностью масштабирования таймлайна, но без каких-либо элементов управления (создание, редактирование, удаление задач/секций/вех).
---
## Пользовательский сценарий (создатель ссылки)
1. Пользователь открывает диаграмму Ганта проекта
2. Нажимает кнопку «Экспорт» / «Общий доступ»
3. Указывает диапазон дат, который должен отображаться на диаграмме
4. Система генерирует уникальную ссылку (UUID) и показывает её пользователю
5. Пользователь копирует ссылку и передаёт заинтересованным лицам
### Управление ссылками
- Вызывается через меню экспорта → модальное окно «Общий доступ»
- В модальном окне отображается список созданных ссылок с указанием диапазона дат
- Каждую ссылку можно **удалить** (полное удаление из БД, не soft delete)
- Ссылки **нельзя редактировать** — только создавать и удалять
- Ограничений на количество активных ссылок нет
- Статистика просмотров (сколько раз открывали) — **не нужна**
---
## Пользовательский сценарий (зритель по ссылке)
1. Зритель переходит по ссылке (без авторизации)
2. Открывается отдельная страница с диаграммой Ганта
3. Зритель видит:
- Задачи и вехи, попадающие в заданный диапазон
- Секции (заголовки группировок)
- Таймлайн с цветовой раскраской задач
4. Зритель может:
- Масштабировать таймлайн
- Переключать отображение «по ячейкам» / «по дням»
5. Зритель **не может**:
- Создавать, редактировать, удалять задачи / секции / вехи
- Видеть любые контролы управления (кнопки создания, контекстные меню, drag-and-drop и т.д.)
- Авторизоваться через эту страницу
---
## Данные и фильтрация
### Что попадает в представление
Задачи и вехи, у которых хотя бы одно из условий верно:
- Дата начала попадает в диапазон ссылки
- Дата конца попадает в диапазон ссылки
- Задача начинается до диапазона и заканчивается после (полностью перекрывает)
Секции включаются, если в них есть хотя бы одна попавшая задача.
### API возвращает
- Только задачи и вехи, попадающие в диапазон (не всю диаграмму)
- Информацию о секциях, содержащих попавшие задачи
- Минимальный набор полей, необходимый для отрисовки (без внутренних ID пользователей и т.п.)
---
## Технические требования
### Ссылки
- Формат: UUID v4 (каждый раз новый)
- Срок действия: бессрочно (пока не удалена)
- Удаление: **жёсткое** (hard delete из БД)
### Backend (Go API)
#### Таблица БД: `gantt_public_links`
- `id` (UUID, PK)
- `project_id` (FK → projects)
- `date_from` (date)
- `date_to` (date)
- `created_by` (FK → users)
- `created_at` (timestamp)
#### Endpoints
- `POST /api/v1/gantt/:projectId/public-links` — создать ссылку (авторизован, участник проекта)
- Body: `{ dateFrom, dateTo }`
- Response: `{ url: "https://leadera.pro/share/<uuid>" }`
- `GET /api/v1/gantt/:projectId/public-links` — список ссылок проекта (авторизован, участник)
- `DELETE /api/v1/gantt/public-links/:uuid` — удалить ссылку (авторизован, создатель)
- `GET /api/v1/public/gantt/:uuid` — получить данные диаграммы по ссылке (без авторизации)
- Response: задачи, вехи, секции в рамках диапазона
#### Rate limiting
- На публичный endpoint `GET /api/v1/public/gantt/:uuid` — rate limiting для защиты от перебора
- Рекомендация: 60 запросов в минуту на IP
### Frontend (Angular)
#### Модальное окно «Общий доступ»
- Вызывается из меню экспорта
- Содержит:
- Форму создания ссылки (диапазон дат + кнопка «Создать»)
- Список существующих ссылок (диапазон + URL + кнопка «Копировать» + кнопка «Удалить»)
#### Публичная страница `/share/:uuid`
- Отдельный route, **не требует авторизации**
- Отображает диаграмму Ганта в read-only режиме
- Доступны только: масштабирование таймлайна, переключение ячейки/дни
- Все контролы управления скрыты (удалены из шаблона, не просто disabled)
- Минимальный layout — без навигации проекта, без сайдбара
---
## Открытые вопросы
_(Нет)_
+329
View File
@@ -0,0 +1,329 @@
---
title: "Leadera: Вехи (Milestones) на диаграмме Ганта"
date: 2026-04-08
tags: ["leadera", "gantt", "angular", "go", "api", "ux"]
---
## Бизнес-требования
### BR-1: Сущность «Веха» (Milestone)
Веха — это маркер на таймлайне диаграммы Ганта, обозначающий важную дату в проекте. В отличие от задач, веха не имеет длительности и привязана к конкретной дате.
**Поля вехи:**
| Поле | Тип | Описание |
|------|-----|----------|
| id | UUID | Идентификатор |
| gantt_chart_id | UUID | Принадлежность к диаграмме (не к секции!) |
| title | string | Название вехи (обязательное, 1–255 символов) |
| date | date | Дата вехи (без времени, обязательная) |
| color | string | Цвет маркера (HEX, по умолчанию `#dc3545` — красный) |
| created_by | UUID | Кто создал (не отображается в UI, хранится для аудита) |
| updated_by | UUID | Кто обновил (не отображается в UI) |
| created_at | timestamp | Дата создания |
| updated_at | timestamp | Дата обновления |
| deleted_at | timestamp | Soft delete |
**Права доступа:** Создание, редактирование и удаление вех доступно участникам с правами выше «только чтение» (аналогично задачам).
**Особенности:**
- Вех может быть несколько в проекте
- Несколько вех могут иметь одну и ту же дату
- Вехи сортируются строго по дате (по возрастанию)
- Вехи не привязаны к секциям — они отображаются **сквозь все секции**
---
### BR-2: Управление вехами (модальное окно)
**Вызов:** Кнопка «Вехи» рядом с кнопкой «Создать секцию» на странице диаграммы.
**Модальное окно содержит:**
- Заголовок: «Вехи проекта»
- Список всех вех текущей диаграммы (сортировка по дате по возрастанию)
- Каждая веха в списке показывает: цвет (индикатор), название, дату, кнопки «Редактировать» и «Удалить»
- Кнопка «Добавить веху»
**Форма добавления/редактирования вехи:**
- Название — текстовый ввод (обязательное, 1–255 символов)
- Дата — datepicker (обязательное)
- Цвет — выбор цвета (по аналогии с палитрой цветов в задачах)
- По умолчанию цвет = красный (`#dc3545`)
**Удаление:** с подтверждением (confirm dialog).
---
### BR-3: Отображение вех на таймлайне
**Визуальное представление:**
- **Маркер-флажок** (flag marker) на шкале таймлайна
- От маркера вниз идёт **пунктирная вертикальная линия** через все секции (на всю высоту диаграммы)
- Цвет линии и маркера = цвет вехи
**Позиционирование:**
- Веха ставится ровно на свою дату на шкале времени
- Если дата вехи выходит за рамки текущего диапазона задач — таймлайн **расширяется** (аналогично задачам)
- При изменении масштаба (days/weeks/months/quarters) позиция вехи пересчитывается
**Tooltip при наведении:**
- При наведении на маркер/линию — показывается tooltip с названием вехи
- Если на одну дату приходится несколько вех — в tooltip показываются **все** вехи этой даты с их названиями (каждая с индикацией своего цвета)
---
## Технические требования
### База данных
#### Новая таблица: gantt_milestones
```sql
CREATE TABLE gantt_milestones (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
gantt_chart_id UUID NOT NULL REFERENCES gantt_charts(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
date DATE NOT NULL,
color VARCHAR(20) NOT NULL DEFAULT '#dc3545',
created_by UUID NOT NULL REFERENCES users(id),
updated_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
CREATE INDEX idx_gantt_milestones_chart ON gantt_milestones(gantt_chart_id);
CREATE INDEX idx_gantt_milestones_date ON gantt_milestones(date);
```
---
### Backend (Go API)
#### Этап 1: Миграция + Домен
**Промт для агента:**
> Добавь в проект новую таблицу `gantt_milestones`.
>
> 1. Создай миграцию в `cmd/migrate/`:
> - Новая таблица `gantt_milestones` с полями: id (UUID PK), gantt_chart_id (UUID FK → gantt_charts, ON DELETE CASCADE), title (VARCHAR 255 NOT NULL), date (DATE NOT NULL), color (VARCHAR 20 NOT NULL DEFAULT '#dc3545'), created_by (UUID FK → users), updated_by (UUID FK → users), created_at, updated_at, deleted_at. Индексы по gantt_chart_id и date.
>
> 2. Создай доменную модель `GanttMilestone` в `internal/domain/gantt.go`:
> ```go
> type GanttMilestone struct {
> ID UUID `json:"id"`
> GanttChartID UUID `json:"gantt_chart_id"`
> Title string `json:"title"`
> Date time.Time `json:"date"`
> Color string `json:"color"`
> CreatedBy UUID `json:"created_by"`
> UpdatedBy UUID `json:"updated_by"`
> CreatedAt time.Time `json:"created_at"`
> UpdatedAt time.Time `json:"updated_at"`
> DeletedAt *time.Time `json:"deleted_at,omitempty"`
> }
> ```
#### Этап 2: CRUD для GanttMilestones
**Промт для агента:**
> Реализуй полный CRUD для сущности `GanttMilestone` по аналогии с существующими handlers/services/repositories в проекте.
>
> **Роуты:**
> - `GET /api/gantt-charts/:chartId/milestones` — список вех диаграммы
> - `POST /api/gantt-charts/:chartId/milestones` — создать веху
> - `PUT /api/gantt-charts/:chartId/milestones/:milestoneId` — обновить веху
> - `DELETE /api/gantt-charts/:chartId/milestones/:milestoneId` — удалить веху (soft delete)
>
> **Repository:** `internal/repository/gantt_milestone_repository.go`
> - `GetByChartID(ctx, chartID) ([]domain.GanttMilestone, error)` — все неудалённые вехи диаграммы, отсортированные по date ASC
> - `GetByID(ctx, id) (*domain.GanttMilestone, error)`
> - `Create(ctx, milestone) error`
> - `Update(ctx, milestone) error`
> - `Delete(ctx, id) error` — soft delete (set deleted_at)
> - Все запросы — raw SQL через pgx, как в остальных репозиториях проекта
>
> **Service:** `internal/service/gantt_milestone_service.go`
> - Валидация: title (1–255 символов), date (обязательное), color (валидный HEX, default '#dc3545')
> - Проверка прав доступа: пользователь должен иметь права выше «только чтение» в пространстве (space) диаграммы
> - Проверка что chartId существует и не архивирован
>
> **Handler:** `internal/handler/gantt_milestone_handler.go`
> - Swagger-аннотации для всех эндпоинтов
> - Валидация входных данных
> - Ошибки в формате проекта
>
> **DTO:** `internal/dto/gantt/milestones_dto.go`
> ```go
> type CreateGanttMilestoneRequest struct {
> Title string `json:"title" binding:"required,min=1,max=255"`
> Date string `json:"date" binding:"required"`
> Color *string `json:"color" binding:"omitempty,max=20"`
> }
>
> type UpdateGanttMilestoneRequest struct {
> Title *string `json:"title" binding:"omitempty,min=1,max=255"`
> Date *string `json:"date" binding:"omitempty"`
> Color *string `json:"color" binding:"omitempty,max=20"`
> }
>
> type GanttMilestoneResponse struct {
> ID domain.UUID `json:"id"`
> GanttChartID domain.UUID `json:"gantt_chart_id"`
> Title string `json:"title"`
> Date string `json:"date"`
> Color string `json:"color"`
> CreatedByUser *CreatedByUpdatedUser `json:"created_by_user"`
> UpdatedByUser *CreatedByUpdatedUser `json:"updated_by_user"`
> CreatedAt time.Time `json:"created_at"`
> UpdatedAt time.Time `json:"updated_at"`
> }
>
> type GanttMilestonesListResponse struct {
> Milestones []GanttMilestoneResponse `json:"milestones"`
> }
> ```
>
> **Регистрация роутов:** Добавь группу milestone-роутов в роутер рядом с существующими роутами секций/задач. Проверка прав через существующий middleware.
#### Этап 3: Включение вех в ответ диаграммы
**Промт для агента:**
> При запросе данных диаграммы Ганта (полная загрузка) включай связанные вехи в ответ.
>
> 1. Добавь эндпоинт или расширь существующий `GET /api/gantt-charts/:chartId` — включить поле `milestones` с массивом `GanttMilestoneResponse`
> 2. Вехи с `deleted_at IS NOT NULL` не возвращаются
> 3. Вехи сортируются по `date` ASC
> 4. Вехи загружаются вместе с данными диаграммы (tasks, sections) — один запрос при открытии Ганта
---
### Frontend (Angular)
#### Этап FE-1: Модели + Сервис
**Промт для агента:**
> Добавь в проект поддержку вех (milestones) на диаграмме Ганта.
>
> 1. В `pages/gantt/models/` (или соответствующем файле моделей) добавь:
>
> ```typescript
> export interface GanttMilestone {
> id: string;
> gantt_chart_id: string;
> title: string;
> date: string;
> color: string;
> created_by_user?: { id: string; full_name: string };
> updated_by_user?: { id: string; full_name: string };
> created_at: string;
> updated_at: string;
> }
>
> export interface GanttMilestoneCreateRequest {
> title: string;
> date: string;
> color?: string;
> }
>
> export interface GanttMilestoneUpdateRequest {
> title?: string;
> date?: string;
> color?: string;
> }
> ```
>
> 2. Добавь `milestones: GanttMilestone[]` в интерфейс/модель полной загрузки данных диаграммы
>
> 3. Создай сервис `pages/gantt/services/gantt-milestone.service.ts`:
> - Inject HttpClient
> - Методы:
> - `getMilestones(chartId: string): Observable<GanttMilestone[]>`
> - `createMilestone(chartId: string, data: GanttMilestoneCreateRequest): Observable<GanttMilestone>`
> - `updateMilestone(chartId: string, milestoneId: string, data: GanttMilestoneUpdateRequest): Observable<GanttMilestone>`
> - `deleteMilestone(chartId: string, milestoneId: string): Observable<void>`
> - Все методы возвращают Observable с типизированными ответами (по аналогии с GanttChartService)
#### Этап FE-2: Модальное окно управления вехами
**Промт для агента:**
> Создай компонент модального окна для управления вехами диаграммы Ганта.
>
> 1. Создай компонент `pages/gantt/components/milestones-modal/`
>
> 2. **Кнопка вызова:** Добавь кнопку «Вехи» рядом с кнопкой «Создать секцию» в toolbar диаграммы. По клику — открытие модального окна через ng-bootstrap NgbModal.
>
> 3. **Модальное окно** (ng-bootstrap modal):
> - Заголовок: «Вехи проекта»
> - Список вех (сортировка по date ASC):
> - Каждый элемент: цветной кружок (color), название, дата (форматированная), кнопки ✏️ (редактировать) и 🗑️ (удалить с confirm)
> - Кнопка «Добавить веху» внизу списка
> - Если вех нет — сообщение «Вех пока нет. Добавьте первую!»
>
> 4. **Форма добавления/редактирования** (inline в модальном окне):
> - Название — текстовый ввод (обязательное)
> - Дата — ng-bootstrap datepicker (обязательное)
> - Цвет — палитра цветов (использовать тот же компонент/подход, что и для задач)
> - По умолчанию цвет = `#dc3545` (красный)
> - Кнопки «Сохранить» и «Отмена»
>
> 5. При сохранении — вызов соответствующего метода сервиса (create/update)
> 6. При удалении — confirm dialog → вызов deleteMilestone
> 7. После любого изменения — обновить список вех и обновить данные на канвасе (через сервис/GanttStore)
>
> 8. Инжектировать GanttMilestoneService и NgbModal
#### Этап FE-3: Отображение вех на таймлайне (Konva canvas)
**Промт для агента:**
> Отобрази вехи (milestones) на canvas-диаграмме Ганта.
>
> Проект использует Konva.js для рендеринга.
>
> 1. При загрузке данных диаграммы — извлечь массив `milestones`
>
> 2. Для каждой вехи нарисовать на canvas:
> - **Маркер-флажок** (flag marker) на позиции даты вехи на шкале времени
> - Стиль: маленький треугольник или флаг на верхней границе таймлайна
> - Цвет = milestone.color
> - **Пунктирная вертикальная линия** от маркера вниз через все секции (на всю высоту области секций)
> - Стиль: Konva.Line, dash: [5, 5], strokeWidth: 1.5, color = milestone.color, opacity: 0.7
>
> 3. **Позиционирование:**
> - Рассчитать X-позицию по дате вехи с использованием текущей шкалы времени (как для задач)
> - Если дата вехи выходит за текущий диапазон — расширить диапазон (влияет на общую ширину таймлайна)
> - При изменении масштаба (days/weeks/months/quarters) — пересчитать позиции всех вех
> - При скролле — вехи скроллятся вместе с задачами
>
> 4. **Tooltip при наведении:**
> - При наведении на маркер или пунктирную линию — показать Konva.Tooltip или HTML tooltip:
> - Если веха одна на эту дату — показать название
> - Если несколько вех на одну дату — показать список: цветной кружок + название для каждой
>
> 5. **Z-index:**
> - Линии вех рисуются **поверх** фона секций, но **под** задачами
> - Маркеры-флажки — поверх всего (самый высокий z-index)
>
> 6. **Реактивность:**
> - При добавлении/удалении/редактировании вехи — перерисовать только вехи (без полного ре рендера задач)
> - Подписаться на изменения массива milestones через сервис/store
---
## Порядок реализации
| # | Этап | Сторона | Зависимости |
|---|------|---------|-------------|
| 1 | Миграция + Домен | Backend | — |
| 2 | CRUD вех | Backend | Этап 1 |
| 3 | Включение вех в ответ диаграммы | Backend | Этап 2 |
| 4 | Модели + Сервис | Frontend | — (параллельно с backend) |
| 5 | Модальное окно управления вехами | Frontend | Этапы 4 + backend 2 |
| 6 | Отображение вех на таймлайне | Frontend | Этапы 4 + backend 3 |
@@ -0,0 +1,407 @@
---
title: "Leadera: Ограничения секций в диаграмме Ганта"
date: 2026-04-06
tags: ["leadera", "gantt", "angular", "go", "api", "ux"]
---
## Бизнес-требования
### BR-1: Лимит одновременных задач (Concurrency Limit)
У каждой секции появляется параметр **max_concurrent_tasks** (целое число ≥ 0, по умолчанию 0).
**Поведение:**
- Если `max_concurrent_tasks = 3` — в любой данный день в этой секции не может быть больше 3 активных задач одновременно
- Если `max_concurrent_tasks = 0` — ограничение отключено, лимита нет
- Пересечение считается по дате: задача занимает дни с `start_date` по `end_date` включительно. Две задачи «пересекаются», если у них есть хотя бы один общий день
- Ограничение работает **только в рамках одной секции**. Задачи из разных секций не влияют друг на друга
**Срабатывание при:**
1. Создании задачи — если добавление нарушает лимит → Alert
2. Перетаскивании задачи (drag) — если новая позиция нарушает лимит → Alert
3. Растягивании задачи (resize) — если новый размер нарушает лимит → Alert
4. Редактировании задачи (форма) — если изменённые даты нарушают лимит → Alert
**Важно:** Действие **НЕ отменяется**. Задача создаётся/перемещается в любом случае, но пользователь видит предупреждение.
**Текст Alert:** «В секции "[название секции]" на [дата/диапазон] превышен лимит одновременных задач (максимум: N)»
---
### BR-2: Блоки секций (Section Blocks)
У каждой секции появляется набор **блоков** — особый вид записей, отображаемых на таймлайне как заштрихованные области.
**Сущность GanttSectionBlock:**
| Поле | Тип | Описание |
|------|-----|----------|
| id | UUID | Идентификатор |
| section_id | UUID | Принадлежность к секции |
| title | string | Название (например: «Отпуск», «Командировка») |
| start_date | date | Начало интервала (минимум 1 день) |
| end_date | date | Конец интервала |
| block_concurrency | boolean | Запрещать ли пересечение обычных задач с данным блоком |
**Валидация:**
- `end_date` не может быть раньше `start_date`
- Все поля обязательны
- Блок может быть в прошлом
**Визуальное отображение на Ганте:**
- Заштрихованная область на **всю высоту секции** (включая все строки задач в секции)
- Название блока отображается **поверх штриховки с подложкой** (для читаемости)
**Поведение block_concurrency:**
- Если `block_concurrency = true` — при пересечении задачи с данным блоком показывается Alert (аналогично BR-1), но задача **всё равно создаётся/перемещается**
- Если `block_concurrency = false` — блок отображается визуально, но не влияет на задачи
**Срабатывание при:**
1. Создании задачи — пересекает блок с `block_concurrency = true` → Alert
2. Перетаскивании задачи (drag) → Alert
3. Растягивании задачи (resize) → Alert
4. Редактировании задачи (форма) → Alert
**Текст Alert:** «В секции "[название секции]" задача пересекает блок "[название блока]" ([дата] — [дата])»
---
### BR-3: Система Alert-ов
Все предупреждения (от BR-1 и BR-2) выводятся в **единую область** над диаграммой Ганта.
**Поведение:**
- Alert-ы — Bootstrap Alert (warning), **постоянные**
- Пользователь **НЕ может** закрыть Alert вручную
- Alert исчезает **автоматически**, когда текущая дата прошла дату конфликта (конфликт больше не актуален)
- Проверка конфликтов — **только на фронтенде** (на основе загруженных данных: задачи секции, блоки, max_concurrent_tasks)
- Если Alert-ов много — область **сворачиваемая** с счётчиком в бейдже (например: «⚠️ 3 предупреждения»)
- По умолчанию — развёрнуто
- Сворачивание/разворачивание — клик по заголовку области
---
### BR-4: Настройки секции
- Настройки совмещены с **существующим модальным окном редактирования секции** (не отдельное окно)
**Окно создания секции** — добавляется:
- Поле «Макс. одновременных задач» (числовой ввод, 0 = без лимита, по умолчанию 0)
**Окно редактирования секции** — добавляется:
- Поле «Макс. одновременных задач» (числовой ввод, 0 = без лимита)
- Список блоков (таблица/список с CRUD: добавить, редактировать, удалить)
- **Сортировка:** по `start_date` по убыванию (будущее сверху)
- Каждый блок — форма: title, start_date, end_date, block_concurrency (чекбокс: «Запретить пересечение задач»)
- Валидация: end_date >= start_date, все поля заполнены
---
### BR-5: Наследование
- При появлении функционала копирования/дублирования диаграммы — настройки секций (лимит + блоки) наследуются
---
## Технические требования
### База данных
#### Новая таблица: gantt_section_blocks
```sql
CREATE TABLE gantt_section_blocks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
section_id UUID NOT NULL REFERENCES gantt_sections(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
block_concurrency BOOLEAN NOT NULL DEFAULT false,
created_by UUID NOT NULL REFERENCES users(id),
updated_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE,
CONSTRAINT chk_block_dates CHECK (end_date >= start_date)
);
CREATE INDEX idx_gantt_section_blocks_section ON gantt_section_blocks(section_id);
CREATE INDEX idx_gantt_section_blocks_dates ON gantt_section_blocks(start_date, end_date);
```
#### Изменение таблицы: gantt_sections
```sql
ALTER TABLE gantt_sections ADD COLUMN max_concurrent_tasks INTEGER NOT NULL DEFAULT 0;
```
---
### Backend (Go API)
#### Этап 1: Миграция + Домен
**Промт для агента:**
> Добавь в проект новую таблицу `gantt_section_blocks` и колонку `max_concurrent_tasks` в `gantt_sections`.
>
> 1. Создай миграцию в `cmd/migrate/`:
> - Новая таблица `gantt_section_blocks` с полями: id (UUID PK), section_id (UUID FK → gantt_sections, ON DELETE CASCADE), title (VARCHAR 255 NOT NULL), start_date (DATE NOT NULL), end_date (DATE NOT NULL), block_concurrency (BOOLEAN DEFAULT false), created_by (UUID FK → users), updated_by (UUID FK → users), created_at, updated_at, deleted_at. CHECK constraint: end_date >= start_date. Индексы по section_id и (start_date, end_date).
> - ALTER TABLE gantt_sections ADD COLUMN max_concurrent_tasks INTEGER NOT NULL DEFAULT 0.
> 2. Создай доменную модель `GanttSectionBlock` в `internal/domain/gantt.go` со всеми полями таблицы.
> 3. Добавь поле `MaxConcurrentTasks int` в структуру `GanttSection`.
> 4. Обнови маппинг в relevant repository файлах, чтобы новое поле `max_concurrent_tasks` читалось/писалось.
#### Этап 2: CRUD для GanttSectionBlocks
**Промт для агента:**
> Реализуй полный CRUD для сущности `GanttSectionBlock` по аналогии с существующими handlers/services/repositories в проекте.
>
> **Роуты (внутри группы секции):**
> - `GET /api/gantt-charts/:chartId/sections/:sectionId/blocks` — список блоков секции
> - `POST /api/gantt-charts/:chartId/sections/:sectionId/blocks` — создать блок
> - `PUT /api/gantt-charts/:chartId/sections/:sectionId/blocks/:blockId` — обновить блок
> - `DELETE /api/gantt-charts/:chartId/sections/:sectionId/blocks/:blockId` — удалить блок (soft delete)
>
> **Repository:** `internal/repository/gantt_section_block_repository.go`
> - `GetBySectionID(ctx, sectionID) ([]domain.GanttSectionBlock, error)`
> - `GetByID(ctx, id) (*domain.GanttSectionBlock, error)`
> - `Create(ctx, block) error`
> - `Update(ctx, block) error`
> - `Delete(ctx, id) error` — soft delete (set deleted_at)
> - Все запросы — raw SQL через pgx, как в остальных репозиториях проекта
>
> **Service:** `internal/service/gantt_section_block_service.go`
> - Бизнес-логика: валидация dates (end >= start), проверка прав доступа к секции
> - Inject repository
>
> **Handler:** `internal/handler/gantt_section_block_handler.go`
> - Swagger-аннотации для всех эндпоинтов
> - Валидация входных данных
> - Ошибки в формате проекта (GanttErrorResponse)
>
> **Модели запросов:**
> - Create: title (required), start_date (required), end_date (required), block_concurrency (bool, default false)
> - Update: title, start_date, end_date, block_concurrency — все опциональны
#### Этап 3: Обновление настроек секции
**Промт для агента:**
> Обнови CRUD секций (gantt_sections) для поддержки нового поля `max_concurrent_tasks`.
>
> 1. Добавь `max_concurrent_tasks` в `GanttSectionCreateRequest` (опциональное, int ≥ 0, default 0)
> 2. Добавь `max_concurrent_tasks` в `GanttSectionUpdateRequest` (опциональное, int ≥ 0)
> 3. Обнови SQL-запросы в repository: INSERT и UPDATE должны включать `max_concurrent_tasks`
> 4. При GET секции (или списке секций) — возвращать `max_concurrent_tasks` в ответе
> 5. Валидация: значение должно быть ≥ 0
#### Этап 4: Включение блоков в ответ секций
**Промт для агента:**
> При запросе списка секций или одной секции включай связанные блоки в ответ.
>
> 1. Обнови `GET /api/gantt-charts/:chartId/sections/:sectionId` — добавить поле `blocks` с массивом GanttSectionBlock
> 2. Обнови `GET /api/gantt-charts/:chartId/sections` — добавить поле `blocks` для каждой секции
> 3. Блоки с `deleted_at IS NOT NULL` не возвращаются
> 4. Блоки сортируются по start_date по убыванию (будущее сверху)
---
### Frontend (Angular)
#### Этап FE-1: Модели + Сервис
**Промт для агента:**
> Добавь в проект поддержку ограничений секций Ганта.
>
> 1. В `pages/gantt/models/gantt-chart.model.ts` добавь:
>
> ```typescript
> export interface GanttSectionBlock {
> id: string;
> section_id: string;
> title: string;
> start_date: string;
> end_date: string;
> block_concurrency: boolean;
> created_by_user: UserInfo;
> updated_by_user: UserInfo;
> created_at: string;
> updated_at: string;
> }
>
> export interface GanttSectionBlockCreateRequest {
> title: string;
> start_date: string;
> end_date: string;
> block_concurrency?: boolean;
> }
>
> export interface GanttSectionBlockUpdateRequest {
> title?: string;
> start_date?: string;
> end_date?: string;
> block_concurrency?: boolean;
> }
>
> export interface ConflictCheck {
> has_conflicts: boolean;
> conflicts: ConflictItem[];
> }
>
> export interface ConflictItem {
> type: 'concurrency' | 'block';
> section_id: string;
> section_title: string;
> block_title?: string;
> start_date?: string;
> end_date?: string;
> message: string;
> dates?: string[];
> }
> ```
>
> 2. Добавь `max_concurrent_tasks: number` в интерфейс `GanttSection`
> 3. Добавь `blocks: GanttSectionBlock[]` в интерфейс `GanttSection`
> 4. Добавь `max_concurrent_tasks?: number` в `GanttSectionCreateRequest` и `GanttSectionUpdateRequest`
>
> 5. Создай сервис `pages/gantt/services/gantt-section-block.service.ts`:
> - Inject HttpClient
> - Методы: getBlocks(chartId, sectionId), createBlock(chartId, sectionId, data), updateBlock(chartId, sectionId, blockId, data), deleteBlock(chartId, sectionId, blockId)
> - Все методы возвращают Observable с типизированными ответами (по аналогии с GanttChartService)
#### Этап FE-2: Интеграция в модальные окна секции
**Промт для агента:**
> Интегрируй настройки ограничений в существующие модальные окна создания и редактирования секции Ганта.
>
> 1. Найди существующие компоненты модальных окон секции (создание/редактирование).
>
> **Окно создания секции** — добавить:
> - Числовой ввод: «Максимум одновременных задач»
> - Подсказка: «0 = без ограничений»
> - Валидация: число ≥ 0
> - Значение по умолчанию: 0
> - Отправляется в `GanttSectionCreateRequest` как `max_concurrent_tasks`
>
> **Окно редактирования секции** — добавить:
> - Числовой ввод: «Максимум одновременных задач» (аналогично созданию)
> - Секция «Блоки (запретные интервалы)»:
> - Таблица/список существующих блоков с колонками: Название, Начало, Конец, Блокировка (чекбокс/иконка), Действия (редактировать/удалить)
> - **Сортировка:** по `start_date` по убыванию (будущее сверху)
> - Кнопка «Добавить блок» — inline-форма:
> - title (текстовый ввод, обязательный)
> - start_date (datepicker, обязательный)
> - end_date (datepicker, обязательный)
> - block_concurrency (чекбокс: «Запретить пересечение задач»)
> - Валидация: end_date >= start_date, все поля заполнены
> - Редактирование — inline или через ту же форму
> - Удаление — с подтверждением
>
> 2. При открытии окна редактирования — загрузить текущие настройки секции + блоки
> 3. При сохранении — обновить max_concurrent_tasks и сохранить изменения блоков
> 4. Инжектировать GanttChartService и GanttSectionBlockService
#### Этап FE-3: Визуализация блоков на Ганте
**Промт для агента:**
> Отобрази блоки секций (GanttSectionBlock) на canvas-диаграмме Ганта.
>
> Проект использует Konva.js для рендеринга. Основной компонент: `gantt-chart-canvas`.
>
> 1. При загрузке данных диаграммы — извлечь блоки из каждой секции
> 2. В рендерере секций (canvas/timeline-renderer.ts или аналогичный) — для каждого блока:
> - Нарисовать заштрихованную область (diagonal hatch pattern) на **всю высоту секции** (все строки задач в этой секции)
> - Область ограничена по горизонтали: от start_date до end_date (в пикселях по шкале времени)
> - Цвет штриховки: полупрозрачный серый или configurable
> - Поверх штриховки — текст с названием блока (block.title) на подложке (прямоугольник с фоном, чтобы текст читался)
> 3. Блоки рендерятся **до** задач (z-index ниже), чтобы задачи были поверх
> 4. Блоки с `block_concurrency = true` и `false` визуально не отличаются (штриховка одинаковая)
> 5. При скролле/зуме — блоки пересчитываются как обычные элементы таймлайна
#### Этап FE-4: Система Alert-ов
**Промт для агента:**
> Создай компонент для отображения Alert-ов о конфликтах на диаграмме Ганта.
>
> 1. Создай компонент `pages/gantt/components/gantt-conflict-alerts/`
>
> 2. Компонент размещается **над** canvas-диаграммой Ганта
>
> 3. Входные данные: массив `ConflictItem[]` (из модели)
>
> 4. Визуал:
> - Bootstrap Alert (warning) для каждого конфликта
> - Каждый Alert показывает message из ConflictItem
> - **Без кнопки закрытия** — Alert нельзя закрыть вручную
> - Alert исчезает автоматически, когда текущая дата прошла дату конфликта
> - Область **сворачиваемая**: заголовок «⚠️ Предупреждения (N)» — клик сворачивает/разворачивает список
> - По умолчанию — развёрнуто
> - При 0 конфликтов — область скрыта
>
> 5. Состояние хранится в сервисе или в компоненте диаграммы:
> - Добавление нового конфликта → area auto-expands
> - Alert удаляется из массива **автоматически**, когда текущая дата прошла дату конфликта
> - Проверка актуальности конфликтов — при каждой отрисовке / изменении данных
> - Не использовать toast — Alert-ы постоянные
#### Этап FE-5: Проверка конфликтов при операциях с задачами
**Промт для агента:**
> Интегрируй проверку конфликтов во все операции с задачами на диаграмме Ганта.
>
> Проект использует interaction-handler.ts для drag/resize и формы для создания/редактирования.
>
> **Важно:** Проверка конфликтов — **только на фронтенде**, без вызова API. Используются загруженные данные секции (задачи, блоки, max_concurrent_tasks).
>
> 1. Вынеси логику проверки конфликтов `checkConflicts()` в отдельный метод в gantt-canvas.service.ts:
> - Принимает: section (GanttSection — содержит max_concurrent_tasks и blocks), tasks (GanttTask[] — задачи секции), task_id (nullable — исключить текущую задачу), start_date, end_date
> - Логика:
> a. Если section.max_concurrent_tasks > 0: посчитать кол-во задач (исключая task_id), пересекающихся с [start_date, end_date] на каждый день. Если в любой день count > max_concurrent_tasks — добавить конфликт concurrency
> b. Для каждого блока секции с block_concurrency = true, пересекающегося с [start_date, end_date] — добавить конфликт block
> - Возвращает ConflictCheck
>
> 2. После **каждой** успешной операции с задачей (создание, обновление, drag, resize):
> - Вызвать checkConflicts с параметрами задачи
> - Если has_conflicts = true — передать конфликты в GanttConflictAlertsComponent
> - Alert-ы показываются, действие **НЕ отменяется**
>
> 3. Точки интеграции:
> - **Создание задачи** (форма): после успешного POST → checkConflicts → show alerts
> - **Редактирование задачи** (форма): после успешного PUT → checkConflicts → show alerts
> - **Drag задачи** (interaction-handler.ts): после успешного обновления позиции → checkConflicts → show alerts
> - **Resize задачи** (interaction-handler.ts): после успешного обновления размера → checkConflicts → show alerts
>
> 4. При переключении диаграммы или секции — очищать Alert-ы
> 5. Alert-ы автоматически удаляются, когда текущая дата прошла дату конфликта — область скрывается, когда конфликтов не осталось
---
## Порядок реализации
| # | Этап | Сторона | Зависимости |
|---|------|---------|-------------|
| 1 | Миграция + Домен | Backend | — |
| 2 | CRUD блоков | Backend | Этап 1 |
| 3 | Обновление настроек секции | Backend | Этап 1 |
| 4 | Включение блоков в ответ секций | Backend | Этап 2 |
| 5 | Модели + Сервис | Frontend | — (можно параллельно с backend) |
| 6 | Интеграция в модальные окна | Frontend | Этапы 5 + backend 2,3 |
| 7 | Визуализация блоков на Ганте | Frontend | Этапы 5 + backend 4 |
| 8 | Система Alert-ов | Frontend | Этап 5 |
| 9 | Проверка конфликтов при операциях | Frontend | Этапы 5,7,8 |
@@ -0,0 +1,746 @@
---
title: "Leadera — WebSocket для диаграмм Ганта"
date: 2026-04-07
lastmod: 2026-04-07
tags: ["leadera", "websocket", "realtime", "gantt", "architecture"]
weight: 110
---
# WebSocket для диаграмм Ганта — realtime-совместное редактирование
**Дата:** 07.04.2026
**Статус:** Планирование
**Проект:** Leadera (dev — sand.a2v.space)
---
## 1. Постановка задачи
Диаграммы Ганта должны обновляться в реальном времени при совместной работе: если два или более пользователей редактируют один Гант, изменения каждого мгновенно отображаются у остальных — без перезагрузки страницы и ручного обновления.
---
## 2. Архитектура
```
Angular frontend ←── WebSocket ──→ Go backend (Hub) ──→ PostgreSQL
│ │
│ HTTP (мутации) │ Broadcast
└──→ REST API ──→ Service ──→ DB ──→ Hub ──→ все клиенты комнаты
```
**Ключевой принцип:** мутации данных идут через HTTP (REST), WebSocket используется **только для доставки уведомлений**. Сервер — источник истины, клиент не может обойти проверки через WS.
### Комнаты
Каждый открытый Гант — это «комната» с идентификатором `chartId`. При подключении клиент подписывается на комнату. Hub рассылает события всем подписчикам комнаты.
---
## 3. Backend (Go)
### 3.1. Зависимости
```bash
go get github.com/gorilla/websocket
```
### 3.2. Новые файлы — `internal/ws/`
#### `message.go` — типы сообщений
```go
package ws
import "time"
// EventType — тип события
type EventType string
const (
// Данные
EventSectionCreated EventType = "section_created"
EventSectionUpdated EventType = "section_updated"
EventSectionDeleted EventType = "section_deleted"
EventSectionsReordered EventType = "sections_reordered"
EventTaskCreated EventType = "task_created"
EventTaskUpdated EventType = "task_updated"
EventTaskDeleted EventType = "task_deleted"
// Присутствие
EventUserJoined EventType = "user_joined"
EventUserLeft EventType = "user_left"
// Безопасность / доступ
EventAccessRevoked EventType = "access_revoked"
EventChartArchived EventType = "chart_archived"
EventRoleChanged EventType = "role_changed"
EventTokenExpired EventType = "token_expired"
)
// Event — WS-событие, отправляемое клиентам
type Event struct {
Type EventType `json:"type"`
ChartID string `json:"chartId"`
UserID string `json:"userId,omitempty"`
Payload interface{} `json:"payload,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
// IncomingMessage — сообщение от клиента (для ping/pong)
type IncomingMessage struct {
Type string `json:"type"`
}
```
#### `hub.go` — центральный брокер
```go
package ws
import (
"sync"
)
// Hub управляет комнатами и рассылкой событий
type Hub struct {
mu sync.RWMutex
rooms map[string]map[*Client]bool // chartId → клиенты
}
func NewHub() *Hub {
return &Hub{
rooms: make(map[string]map[*Client]bool),
}
}
// Subscribe добавляет клиента в комнату
func (h *Hub) Subscribe(chartID string, client *Client) {
h.mu.Lock()
defer h.mu.Unlock()
if h.rooms[chartID] == nil {
h.rooms[chartID] = make(map[*Client]bool)
}
h.rooms[chartID][client] = true
// Уведомить остальных: user_joined
h.broadcastExcept(chartID, client, Event{
Type: EventUserJoined,
ChartID: chartID,
UserID: client.UserID,
Payload: map[string]string{"username": client.Username},
Timestamp: time.Now(),
})
}
// Unsubscribe удаляет клиента из комнаты
func (h *Hub) Unsubscribe(chartID string, client *Client) {
h.mu.Lock()
defer h.mu.Unlock()
if clients, ok := h.rooms[chartID]; ok {
delete(clients, client)
if len(clients) == 0 {
delete(h.rooms, chartID)
}
}
// Уведомить остальных: user_left
h.broadcastExcept(chartID, client, Event{
Type: EventUserLeft,
ChartID: chartID,
UserID: client.UserID,
Timestamp: time.Now(),
})
}
// Broadcast рассылает событие всем клиентам в комнате
func (h *Hub) Broadcast(chartID string, event Event) {
h.mu.RLock()
defer h.mu.RUnlock()
if clients, ok := h.rooms[chartID]; ok {
for client := range clients {
client.Send(event)
}
}
}
// SendToUser отправляет событие конкретному пользователю во всех его комнатах
func (h *Hub) SendToUser(userID string, event Event) {
h.mu.RLock()
defer h.mu.RUnlock()
for _, clients := range h.rooms {
for client := range clients {
if client.UserID == userID {
client.Send(event)
}
}
}
}
// broadcastExcept рассылает всем, кроме отправителя
func (h *Hub) broadcastExcept(chartID string, sender *Client, event Event) {
if clients, ok := h.rooms[chartID]; ok {
for client := range clients {
if client != sender {
client.Send(event)
}
}
}
}
```
#### `client.go` — обёртка WS-соединения
```go
package ws
import (
"encoding/json"
"sync"
"time"
"github.com/gorilla/websocket"
)
const (
writeWait = 10 * time.Second
pongWait = 60 * time.Second
pingPeriod = (pongWait * 9) / 10
maxMessageSize = 1024
)
type Client struct {
Hub *Hub
Conn *websocket.Conn
UserID string
Username string
ChartID string
SendCh chan Event
mu sync.Mutex
}
func NewClient(hub *Hub, conn *websocket.Conn, userID, username, chartID string) *Client {
return &Client{
Hub: hub,
Conn: conn,
UserID: userID,
Username: username,
ChartID: chartID,
SendCh: make(chan Event, 64),
}
}
// ReadPump читает входящие сообщения (ping/pong)
func (c *Client) ReadPump() {
defer func() {
c.Hub.Unsubscribe(c.ChartID, c)
c.Conn.Close()
}()
c.Conn.SetReadLimit(maxMessageSize)
c.Conn.SetReadDeadline(time.Now().Add(pongWait))
c.Conn.SetPongHandler(func(string) error {
c.Conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
for {
_, message, err := c.Conn.ReadMessage()
if err != nil {
break
}
// Клиент может слать только ping, данные игнорируем
_ = message
}
}
// WritePump отправляет события клиенту
func (c *Client) WritePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.Conn.Close()
}()
for {
select {
case event, ok := <-c.SendCh:
c.Conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
data, _ := json.Marshal(event)
c.mu.Lock()
c.Conn.WriteMessage(websocket.TextMessage, data)
c.mu.Unlock()
case <-ticker.C:
c.Conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
// Send ставит событие в очередь отправки
func (c *Client) Send(event Event) {
select {
case c.SendCh <- event:
default:
// Очередь полна — отключаем медленного клиента
close(c.SendCh)
}
}
```
### 3.3. WS endpoint — `internal/handlers/ws_handler.go`
```go
package handlers
import (
"net/http"
"time"
"app-leadera-api/internal/middleware"
"app-leadera-api/internal/ws"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true }, // nginx проксирует
}
type WSHandler struct {
hub *ws.Hub
ganttMW *middleware.GanttMiddleware
}
func NewWSHandler(hub *ws.Hub, ganttMW *middleware.GanttMiddleware) *WSHandler {
return &WSHandler{hub: hub, ganttMW: ganttMW}
}
func (h *WSHandler) HandleGanttWS(c *gin.Context) {
// 1. Получить пользователя из контекста (auth middleware уже отработал)
userID, exists := c.Get("userID")
if !exists {
c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
return
}
// 2. Проверить доступ к chartId (gantt middleware)
chartID := c.Param("chartId")
if _, err := uuid.Parse(chartID); err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": "invalid chart id"})
return
}
// TODO: проверить через ganttMW что userID имеет доступ к этому chart
// 3. Upgrade до WebSocket
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}
// 4. Создать клиент и подписать на комнату
client := ws.NewClient(
h.hub, conn,
userID.(string),
c.GetString("username"),
chartID,
)
h.hub.Subscribe(chartID, client)
// 5. Запустить pump'ы
go client.WritePump()
go client.ReadPump()
}
```
### 3.4. Регистрация маршрута
В `cmd/server/main.go` или где регистрируются routes:
```go
// После создания hub
wsHub := ws.NewHub()
// WS endpoint
wsHandler := handlers.NewWSHandler(wsHub, ganttMW)
api.GET("/ws/gantt/:chartId", authMW.RequireAuth(), wsHandler.HandleGanttWS)
// Передать hub в ganttHandler для broadcast'ов
ganttHandler := handlers.NewGanttHandler(ganttService, sectionService, taskService, blockService, wsHub)
```
### 3.5. Интеграция broadcast в handlers
В каждый write-handler — одна строка после успешной операции:
```go
// Пример: после успешного UpdateTask
h.hub.Broadcast(chartID, ws.Event{
Type: ws.EventTaskUpdated,
ChartID: chartID,
UserID: userID,
Payload: updatedTask,
Timestamp: time.Now(),
})
```
Точки интеграции в `gantt_handler.go`:
- `CreateGanttChart` → не нужен (пользователь только что создал, он один)
- `UpdateGanttChart` / `ArchiveGanttChart``chart_archived`
- `CreateSection``section_created`
- `UpdateSection``section_updated`
- `DeleteSection``section_deleted`
- `ReorderSections``sections_reordered`
- `CreateTask``task_created`
- `UpdateTask``task_updated`
- `DeleteTask``task_deleted`
### 3.6. Интеграция событий безопасности
В соответствующих service-методах:
```go
// При исключении из space (SpaceService.RemoveMember)
wsHub.SendToUser(removedUserID, ws.Event{
Type: ws.EventAccessRevoked,
Payload: map[string]string{"spaceId": spaceID},
})
// При архивации ганта
wsHub.Broadcast(chartID, ws.Event{
Type: ws.EventChartArchived,
ChartID: chartID,
})
// При изменении роли
wsHub.SendToUser(userID, ws.Event{
Type: ws.EventRoleChanged,
Payload: map[string]string{"role": newRole},
})
```
---
## 4. Frontend (Angular)
### 4.1. WebSocket сервис — `gantt-ws.service.ts`
```typescript
// src/app/pages/gantt/services/gantt-ws.service.ts
import { Injectable, inject, OnDestroy } from '@angular/core';
import { Subject, Observable } from 'rxjs';
import { environment } from '../../../environments/environment';
import { SpaceStorageService } from '../../../core/services/space-storage.service';
import { AuthService } from '../../../core/auth/services/auth.service';
export interface WSEvent {
type: string;
chartId?: string;
userId?: string;
payload?: any;
timestamp?: string;
}
@Injectable({ providedIn: 'root' })
export class GanttWSService implements OnDestroy {
private spaceStorage = inject(SpaceStorageService);
private auth = inject(AuthService);
private socket: WebSocket | null = null;
private eventSubject = new Subject<WSEvent>();
private reconnectTimer: any;
private chartId: string | null = null;
/** Observable для подписки на события */
get events$(): Observable<WSEvent> {
return this.eventSubject.asObservable();
}
/** Подключиться к комнате ганта */
connect(chartId: string): void {
this.disconnect();
this.chartId = chartId;
const token = this.auth.getAccessToken();
const wsUrl = this.buildWSUrl(chartId, token);
this.socket = new WebSocket(wsUrl);
this.socket.onopen = () => {
console.log(`[WS] Connected to gantt ${chartId}`);
};
this.socket.onmessage = (msg) => {
try {
const event: WSEvent = JSON.parse(msg.data);
this.eventSubject.next(event);
} catch (e) {
console.error('[WS] Parse error', e);
}
};
this.socket.onclose = (event) => {
console.log(`[WS] Closed: ${event.code}`);
if (event.code !== 1000) {
this.scheduleReconnect();
}
};
this.socket.onerror = () => {
this.socket?.close();
};
}
/** Отключиться */
disconnect(): void {
clearTimeout(this.reconnectTimer);
if (this.socket) {
this.socket.close(1000, 'manual');
this.socket = null;
}
this.chartId = null;
}
ngOnDestroy(): void {
this.disconnect();
}
private buildWSUrl(chartId: string, token: string): string {
const base = environment.wsUrl ||
location.protocol.replace('http', 'ws') + '//' + location.host;
return `${base}/api/v1/ws/gantt/${chartId}?token=${token}`;
}
private scheduleReconnect(): void {
if (!this.chartId) return;
this.reconnectTimer = setTimeout(() => {
console.log('[WS] Reconnecting...');
if (this.chartId) this.connect(this.chartId);
}, 3000);
}
}
```
### 4.2. Интеграция в gantt-chart-canvas компонент
```typescript
// В gantt-chart-canvas.component.ts
export class GanttChartCanvasComponent implements OnInit, OnDestroy {
private ws = inject(GanttWSService);
private onlineUsers: Set<string> = new Set();
private wsSubscription: Subscription | null = null;
ngOnInit(): void {
// Подключиться к WS при загрузке ганта
this.ws.connect(this.chartId);
this.wsSubscription = this.ws.events$.subscribe(event => {
this.handleWSEvent(event);
});
}
ngOnDestroy(): void {
this.wsSubscription?.unsubscribe();
this.ws.disconnect();
}
private handleWSEvent(event: WSEvent): void {
// Если это событие от текущего пользователя — пропускаем
// (мы уже обновили UI оптимистично или после HTTP-ответа)
if (event.userId === this.currentUserId) return;
switch (event.type) {
case 'section_created':
this.addSectionLocal(event.payload);
this.redrawCanvas();
break;
case 'section_updated':
this.updateSectionLocal(event.payload);
this.redrawCanvas();
break;
case 'section_deleted':
this.removeSectionLocal(event.payload.id);
this.redrawCanvas();
break;
case 'sections_reordered':
this.reorderSectionsLocal(event.payload.ordered_ids);
this.redrawCanvas();
break;
case 'task_created':
this.addTaskLocal(event.payload);
this.redrawCanvas();
break;
case 'task_updated':
this.updateTaskLocal(event.payload);
this.redrawCanvas();
break;
case 'task_deleted':
this.removeTaskLocal(event.payload.id);
this.redrawCanvas();
break;
case 'user_joined':
this.onlineUsers.add(event.userId!);
this.showOnlineIndicator();
break;
case 'user_left':
this.onlineUsers.delete(event.userId!);
this.showOnlineIndicator();
break;
// === Безопасность ===
case 'access_revoked':
this.router.navigate(['/gantt'], {
state: { alert: 'Доступ к пространству был отозван' }
});
break;
case 'chart_archived':
this.router.navigate(['/gantt'], {
state: { alert: 'Диаграмма была заархивирована' }
});
break;
case 'role_changed':
this.currentRole = event.payload.role;
this.updateEditableState();
break;
case 'token_expired':
this.router.navigate(['/auth/login']);
break;
}
}
private redrawCanvas(): void {
// Перерисовать Konva canvas с обновлёнными данными
}
}
```
### 4.3. Индикатор онлайн-пользователей
Добавить в шаблон canvas-компонента:
```html
<!-- Онлайн-индикатор -->
<div class="online-indicator" *ngIf="onlineUsers.size > 0">
<span class="pulse-dot"></span>
{{ onlineUsers.size }} онлайн
</div>
```
---
## 5. Безопасность
### 5.1. Авторизация при подключении
| Этап | Проверка |
|------|----------|
| WS Handshake | JWT в query-параметре `?token=...` |
| Подписка на комнату | Проверка membership в space |
| Каждое broadcast-событие | Hub знает userId каждого клиента |
### 5.2. Сценарии отзыва доступа
| Событие | Триггер | Действие на клиенте |
|---------|---------|---------------------|
| `access_revoked` | Исключение из space | Редирект на список гантов |
| `chart_archived` | Архивация ганта | Редирект на список гантов |
| `role_changed` | Смена роли (editor→viewer) | Отключить редактирование |
| `token_expired` | Токен протух | Редирект на логин |
### 5.3. Защита от злоупотреблений
| Угроза | Мера |
|--------|------|
| Спам WS-сообщений | Rate limit: 30 msg/min на клиента, `ReadLimit` в gorilla/websocket |
| Подключение без прав | Проверка space membership при handshake |
| Истёкший токен | Ping/pong + периодическая проверка JWT (раз в 5 мин) |
| XSS через WS | Payload — JSON, sanitize имён на frontend |
| Мутации через WS | WS **только для чтения** (уведомления). Мутации — через HTTP API с полной валидацией |
### 5.4. Nginx конфигурация
В конфиг `sand.a2v.space` добавить:
```nginx
# WebSocket проксирование
location /api/v1/ws/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400s; # 24h для long-lived WS
proxy_send_timeout 86400s;
}
```
---
## 6. Оценка трудозатрат
| Компонент | Строки кода | Время |
|-----------|-------------|-------|
| `internal/ws/` (hub, client, message) | ~200 | 2-3 часа |
| `ws_handler.go` (endpoint) | ~60 | 30 мин |
| Интеграция broadcast в handlers | ~20 (по строке на endpoint) | 30 мин |
| События безопасности в services | ~30 | 30 мин |
| `gantt-ws.service.ts` (Angular) | ~80 | 1 час |
| Интеграция в canvas-компонент | ~80 | 1-2 часа |
| Индикатор онлайн | ~30 | 30 мин |
| Nginx конфигурация | ~10 | 10 мин |
| Тестирование | — | 2-3 часа |
| **Итого** | **~510 строк** | **~1-2 дня** |
---
## 7. Порядок реализации
1. **Backend: `internal/ws/`** — hub, client, message types
2. **Backend: WS endpoint** — handler + route
3. **Backend: Broadcast в handlers** — интеграция в существующие CRUD-эндпоинты
4. **Backend: События безопасности** — access_revoked, chart_archived, role_changed
5. **Nginx: WS location** — проксирование WebSocket
6. **Frontend: WS service** — подключение, реконнект, парсинг событий
7. **Frontend: Интеграция в canvas** — обработка событий, обновление Konva
8. **Frontend: Индикатор онлайн** — визуальная обратная связь
9. **Тестирование** — многопользовательский сценарий, отзыв прав, обрывы связи
---
## 8. Дальнейшие улучшения (после MVP)
- **Cursor других пользователей** — показывать позицию курсора на таймлайне
- **Блокировка элемента** — «кто-то редактирует эту задачу» (optimistic locking через WS)
- **Чат в ганте** — комментарии к задачам прямо на канвасе
- **History/undo** — откат изменений с визуальной историей
- **Уведомления** — push-уведомления при @mention в комментариях к задачам
+6
View File
@@ -0,0 +1,6 @@
---
title: "Личный сайт (agaltsovav.ru)"
date: 2026-03-31
tags: ["personal-site", "hugo", "audit"]
weight: 90
---
@@ -0,0 +1,24 @@
---
title: "Бинарный файл в репозитории"
date: 2026-03-31
tags: ["audit", "personal-site"]
---
## Проблема
В корне репозитория лежат два файла, которые не должны быть в git:
- **`Шаблоны для сайта.key`** — Apple Keynote файл (бинарный)
- **`logo.psd`** — Photoshop файл (бинарный)
### Что не так
- Увеличивают размер репозитория
- Git неэффективно хранит бинарные файлы (каждая версия — полностью)
- `.key` файлы могут содержать конфиденциальные данные шаблонов
### Рекомендация
1. Перенести в отдельное хранилище (Google Drive, облако)
2. Удалить из git-истории через `git filter-repo`
3. Добавить в `.gitignore`: `*.key`, `*.psd`, `*.sketch`
@@ -0,0 +1,21 @@
---
title: "Copyright без 2026 года"
date: 2026-03-31
tags: ["audit", "personal-site"]
---
## Проблема
В `params.toml`:
```
copyRight = "Copyright (c) 2020-2025 Антон Агальцов"
```
Сейчас 2026 год — нужно обновить.
### Рекомендация
Обновить на `2020-2026`. Или сделать динамическим через Hugo template:
```
Copyright (c) 2020-{{ now.Year }} Антон Агальцов
```
@@ -0,0 +1,24 @@
---
title: "Digest не обновлялся с февраля 2025"
date: 2026-03-31
tags: ["audit", "personal-site"]
---
## Проблема
Последний дайджест — **февраль 2025**. С тех пор 12+ месяцев без новых выпусков.
### Что есть
- 27 файлов дайджестов (январь 2023 — февраль 2025)
- Автоматизация через `digest.js` — скрипт для генерации
- Но скрипт не запускается автоматически
### Риск
Low — контент устаревает, SEO-позиции падают для страниц дайджестов.
### Рекомендация
1. Настроить cron/CI для ежемесячной генерации дайджеста
2. Или интегрировать с OpenClaw — авто-сборка дайджеста на основе новых публикаций
@@ -0,0 +1,22 @@
---
title: "Устаревший Node.js 16 в Netlify"
date: 2026-03-31
tags: ["audit", "personal-site", "infrastructure"]
severity: high
status: open
---
# Устаревший Node.js 16 в Netlify
**Файл:** `netlify.toml:5`
```
NODE_VERSION = "16.16.0"
```
Node.js 16 вышел из поддержки (EOL) в сентябре 2023. Сборка на Netlify может начать падать или работать некорректно.
**Рекомендация:** Обновить до Node.js 20 LTS:
```toml
NODE_VERSION = "20"
```
@@ -0,0 +1,17 @@
---
title: "Yandex Metrika отключена"
date: 2026-03-31
tags: ["audit", "personal-site"]
---
## Проблема
В `params.toml`: `yandexMetrika = false`
Аналитика с сайта не собирается. Непонятно, есть ли вообще посетители и какие страницы популярны.
### Рекомендация
1. Включить Yandex Metrika или перейти на **Plausible** / **Umami** (privacy-friendly)
2. Если сайт не нуждается в аналитике — оставить как есть
3. Альтернатива: Netlify Analytics (уже встроен в хостинг)
@@ -0,0 +1,23 @@
---
title: "Отсутствуют node_modules — нужна установка"
date: 2026-03-31
tags: ["audit", "personal-site"]
---
## Проблема
`node_modules/` отсутствует — проект нельзя собрать без `npm install`.
### Риск
Low — стандартная ситуация. Но:
- `package-lock.json` привязан к Node 16, а на Mac Studio текущий Node v24
- Могут быть конфликты зависимостей (canvas, babel, postcss)
- Hugo 0.148.2 ставится через `hugo-installer` в npm — может не совпасть с глобальным Hugo
### Рекомендация
1. Убедиться что `npm install` проходит без ошибок на текущем Node
2. Если нет — обновить зависимости или использовать nvm для Node 16
3. Рассмотреть переход на глобальный Hugo вместо `exec-bin`
@@ -0,0 +1,24 @@
---
title: "Hugo Doks 0.5 — сильно устарел"
date: 2026-03-31
tags: ["audit", "personal-site"]
---
## Проблема
Проект использует тему **Hyas/Doks v0.5.0** (package.json). Актуальная версия Doks — **1.x** с полной переработкой архитектуры.
### Что не так
- Doks 0.5 использует устаревший Bootstrap 5.2.0-beta1
- Hugo 0.148.2 в зависимостях — новый, но интеграция с темой старая
- Конфигурация через `theme.toml` — в новых версиях перешли на другой формат
- `exec-bin` для вызова Hugo — устаревший подход, текущие версии Hugo ставятся глобально
### Риск
Medium — работает, но миграция на Doks 1.x потребует значительного рефакторинга layout-ов и конфигурации. Чем дольше откладывать — тем сложнее.
### Рекомендация
Запланировать миграцию на Doks 1.x или перейти на чистый Hugo + Bootstrap без зависимости от темы-фреймворка.
@@ -0,0 +1,26 @@
---
title: "Twitter handle не соответствует имени"
date: 2026-03-31
tags: ["audit", "personal-site"]
---
## Проблема
В `params.toml` указан Twitter: `@AntonTotoshkin`
Но владелец сайта — **Антон Агальцов**. Ники не совпадают.
### Два варианта
1. Это твой старый/другой аккаунт — тогда ок, но стоит указать актуальный
2. Это чужой аккаунт — тогда нужно убрать или исправить
### Затронутые места
- `twitterSite = "@AntonTotoshkin"`
- `schemaAuthorTwitter = "https://twitter.com/AntonTotoshkin"`
- `schemaTwitter = "https://twitter.com/AntonTotoshkin"`
### Рекомендация
Указать актуальный Twitter/X или убрать, если соцсеть не используется.
+4
View File
@@ -0,0 +1,4 @@
+++
title = "Планы"
draft = false
+++
@@ -0,0 +1,47 @@
---
title: "Завершить Eversolo Companion - этапы 3-9"
date: 2026-03-29
plan_date:
status: planned
done_date:
context_quote: "UI готов, API готов, БД готова — но API не может достучаться до Eversolo из-за ограничений OpenClaw sandbox"
tags: ["eversolo", "companion", "angular", "go", "docker"]
---
## Описание
Завершить разработку Eversolo Companion после решения проблемы с сетевым доступом.
## Что сделано
- **Backend (Go + Gin)**: собран, 17+ REST API эндпоинтов, WebSocket, CORS, state poller
- **Frontend (Angular 17)**: собран, тёмная тема (Spotify-like), 5 экранов, 5 сервисов
- **PostgreSQL**: Docker, порт 5433, миграции готовы
- **Docker Compose**: рабочий (postgres + api + frontend)
- **Спецификация**: полная в ~/Projects/eversolo-companion/SPEC.md
## Проблема
API не может достучаться до Eversolo T8 (192.168.0.156:9529) из-за ограничений OpenClaw sandbox.
## Возможные решения
1. Настроить `tools.elevated.allowFrom.webchat` для разрешения сетевых соединений
2. Запустить API через launchd plist (как SSH туннели)
3. Использовать отдельный контейнер без ограничений
## Этапы 3-9 (после решения сети)
1. **Библиотека синхронизации** — двухсторонний sync с Eversolo
2. **Плейлисты** — импорт/экспорт, редактирование
3. **Scrobbler** — Last.fm / ListenBrainz
4. **Обложки** — кэширование, отображение в UI
5. **Плеер** — улучшенный интерфейс управления
6. **Система уведомлений** — события от Eversolo
7. **Администрирование** — настройка подключений
## Связанные ресурсы
- [Спецификация проекта](/docs/eversolo-companion-spec/)
- [API документация](/kb/eversolo-api/)
- [Дневник разработки](/diary/2026-03-29/)
+27
View File
@@ -0,0 +1,27 @@
---
title: "Реализовать управление Eversolo из OpenClaw"
date: 2026-03-29
plan_date:
status: planned
done_date:
context_quote: "API изучен, записан в TOOLS.md. Проблема: нет доступа к LAN из sandbox OpenClaw."
tags: ["eversolo", "openclaw", "api"]
---
## Описание
Добавить инструменты управления Eversolo T8 в OpenClaw.
## Что сделано
- Изучен полный API Eversolo T8 (см. [Базу знаний](/kb/eversolo-api/))
## Проблема
Sandbox OpenClaw не имеет доступа к локальной сети, где находится Eversolo.
## Возможные решения
1. Настроить проброс портов
2. Запустить OpenClaw без sandbox-ограничений
3. Использовать промежуточный proxy-сервис
+21
View File
@@ -0,0 +1,21 @@
---
title: "Настроить интеграцию Ollama с OpenClaw"
date: 2026-03-29
plan_date:
status: planned
done_date:
context_quote: "Добавлен qwen3.5:9b в openclaw.json, но модель не появилась в openclaw models list. Отложено по решению Антона."
tags: ["ollama", "openclaw", "llm"]
---
## Описание
Добавить поддержку Ollama в OpenClaw для использования локальных LLM моделей.
## Проблема
Модель `qwen3.5:9b` добавлена в конфиг `openclaw.json`, но не отображается в `openclaw models list`.
## Статус
Отложено по решению Антона.