sync: latest changes before Gitea migration
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
# AGENTS.md — docs.a2v.space
|
||||
|
||||
Hugo static site with custom theme. Russian-language personal documentation/knowledge base.
|
||||
|
||||
## Commands
|
||||
|
||||
- `hugo server` — dev server (live reload)
|
||||
- `hugo` — build to `public/`
|
||||
- No package manager, no build scripts, no tests, no linter.
|
||||
|
||||
## Structure
|
||||
|
||||
- `hugo.toml` — site config (TOML). Defines warm color palette params, menu, taxonomies (tags/categories/statuses), JSON output for search.
|
||||
- `content/` — 7 sections: `diary/`, `kb/`, `plans/`, `guides/`, `creds/`, `leadera/`, `personal-site/`. Front matter is TOML (`+++`).
|
||||
- `themes/a2v/` — custom theme (23 templates). All layout logic lives here.
|
||||
- `static/` — CSS and JS at project root, **not** in the theme directory.
|
||||
- `static/js/search.js` — Fuse.js client-side search against Hugo-generated `index.json`
|
||||
- `static/js/creds.js` — Web Crypto API (AES-256-GCM + PBKDF2). Decryption on page; encryption via `window.encryptCreds(data, password)` in browser console.
|
||||
- `static/css/custom.css` — all custom styles (434 lines, CSS variables from hugo.toml params)
|
||||
- `BRIEF.md` — original spec with seed content and structure reference.
|
||||
|
||||
## Key conventions
|
||||
|
||||
- Archetype sets `draft = true` by default. Undraft with `hugo --buildDrafts` or remove `draft = true` from front matter.
|
||||
- `relativeURLs = true` in config — all URLs are relative, no baseURL needed for local dev.
|
||||
- Bootstrap 5.3.2 and Fuse.js 7.0.0 loaded from CDN (no local copies).
|
||||
- Each section can have its own list/single templates under `themes/a2v/layouts/<section>/`.
|
||||
- The sidebar partial (`themes/a2v/layouts/partials/sidebar.html`) is context-aware: shows tags, statuses, or services depending on the current section.
|
||||
- The `source-path` partial renders the local filesystem path of each content file for easy editing.
|
||||
- `shortcodes/encrypted-creds.html` is a passthrough wrapper for encrypted credential payloads.
|
||||
|
||||
## Content front matter patterns
|
||||
|
||||
- **diary**: `date`, `title`, `tags`
|
||||
- **kb / guides**: `title`, `tags`, `draft`
|
||||
- **plans**: `title`, `date`, `plan_date`, `status` (planned/in_progress/done/cancelled), `done_date`, `context_quote`
|
||||
- **creds**: `title`, `description`, `service` — body contains base64-encoded encrypted payload
|
||||
- **leadera / personal-site**: `title`, `tags`
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Static assets must go in `static/` at project root, not `themes/a2v/static/` (the theme static dir is empty).
|
||||
- The search index is generated by `themes/a2v/layouts/_default/index.json` — if you add a new section, it will be included automatically, but the Russian section label mapping is hardcoded in `search.js`.
|
||||
- `hugo.toml` has `canonifyURLs = false` — do not set to true or relative URLs break.
|
||||
- Hugo version installed: v0.159.1+extended (Homebrew). The theme requires ≥ 0.92.0.
|
||||
- No `head.html` partial exists — the `<head>` block is inline in `baseof.html` with a `{{ block "head" }}` extension point.
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: "Leadera"
|
||||
weight: 1
|
||||
---
|
||||
|
||||
# Leadera
|
||||
|
||||
SaaS-система управления IT-ландшафтом с диаграммами Ганта.
|
||||
|
||||
- **Прод:** [leadera.pro](https://leadera.pro)
|
||||
- **Дев:** [sand.a2v.space](https://sand.a2v.space)
|
||||
@@ -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 — без навигации проекта, без сайдбара
|
||||
|
||||
---
|
||||
|
||||
## Открытые вопросы
|
||||
|
||||
_(Нет)_
|
||||
@@ -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 в комментариях к задачам
|
||||
@@ -30,18 +30,22 @@ canonifyURLs = false
|
||||
name = "Планы"
|
||||
url = "/plans/"
|
||||
weight = 3
|
||||
[[menu.main]]
|
||||
name = "Leadera"
|
||||
url = "/leadera/"
|
||||
weight = 4
|
||||
[[menu.main]]
|
||||
name = "Инструкции"
|
||||
url = "/guides/"
|
||||
weight = 4
|
||||
weight = 5
|
||||
[[menu.main]]
|
||||
name = "Креды"
|
||||
url = "/creds/"
|
||||
weight = 5
|
||||
weight = 6
|
||||
[[menu.main]]
|
||||
name = "Личный сайт"
|
||||
url = "/personal-site/"
|
||||
weight = 6
|
||||
weight = 7
|
||||
|
||||
[outputs]
|
||||
home = ["HTML", "JSON"]
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -135,6 +139,15 @@
|
||||
|
||||
<div class="article-content creds-card">
|
||||
<h1>docs.a2v.space Basic Auth</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/creds/docs-a2v-space-auth.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/creds\/docs-a2v-space-auth.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p>Учетные данные для доступа к docs.a2v.space</p>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -135,6 +139,15 @@
|
||||
|
||||
<div class="article-content creds-card">
|
||||
<h1>Eversolo Companion PostgreSQL</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/creds/eversolo-companion-postgres.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/creds\/eversolo-companion-postgres.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p>Учетные данные для базы данных Eversolo Companion</p>
|
||||
|
||||
+93
-89
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -142,97 +146,23 @@
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card creds-card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">docs.a2v.space Basic Auth</h3>
|
||||
<h3 class="card-title">VPS reg.ru</h3>
|
||||
|
||||
<p class="card-text">Учетные данные для доступа к docs.a2v.space</p>
|
||||
<p class="card-text">VPS хостинг (реверс-прокси)</p>
|
||||
|
||||
|
||||
<p class="text-muted mb-2"><strong>Сервис:</strong> VPS</p>
|
||||
|
||||
|
||||
<div class="encrypted-data" id="encrypted-docs-a2v-space-auth">
|
||||
QmFzaWMgQXV0aGVudGljYXRpb24gVXNlcm5hbWU6IGFnYWx0c292YXYgUGFzc3dvcmQ6IE1hZ2FtZXQ3ODchINCa0L7QvdGE0LjQs9GD0YDQsNGG0LjRjyBOZ2lueCBzZXJ2ZXIgeyBsaXN0ZW4gODA5MTsgc2VydmVyX25hbWUgZG9jcy5hMnYuc3BhY2U7IGF1dGhfYmFzaWMgJiMzNDtSZXN0cmljdGVkIEFjY2VzcyYjMzQ7OyBhdXRoX2Jhc2ljX3VzZXJfZmlsZSAvZXRjL25naW54Ly5odHBhc3N3ZDsgfSDQlNC+0YHRgtGD0L8gVVJMOiBodHRwczovL2RvY3MuYTJ2LnNwYWNlICjRh9C10YDQtdC3IHJldmVyc2UgcHJveHkpIExvY2FsOiBodHRwOi8vbG9jYWxob3N0OjgwOTEgKNC00LvRjyDRgNCw0LfRgNCw0LHQvtGC0LrQuCkgQXV0aDog0LLRgdC10LPQtNCwINGC0YDQtdCx0YPQtdGC0YHRjyDQnNCw0YHRgtC10YAt0L/QsNGA0L7Qu9GMINC00LvRjyDRiNC40YTRgNC+0LLQsNC90LjRjyDQktCw0LbQvdC+OiDQn9Cw0YDQvtC70Ywg0LTQu9GPINGA0LDRgdGI0LjRhNGA0L7QstC60Lgg0YTQsNC50LvQvtCyINCyINGA0LDQt9C00LXQu9C1IGNyZWRzLwpNYXN0ZXIgUGFzc3dvcmQ6IFBvcG9sb2thMTYhINCo0LjRhNGA0L7QstCw0L3QuNC1INCQ0LvQs9C+0YDQuNGC0Lw6IEFFUy0yNTYtR0NNINCl0YDQsNC90LjQu9C40YnQtTogY2xpZW50LXNpZGUgZW5jcnlwdGlvbiDQpNCw0LnQu9GLINGI0LjRhNGA0YPRjtGC0YHRjyDQv9GA0Lgg0YHQvtGF0YDQsNC90LXQvdC40Lgg0KDQsNGB0YjQuNGE0YDQvtCy0YvQstCw0Y7RgtGB0Y8g0L/RgNC4INGH0YLQtdC90LjQuCA=
|
||||
<div class="encrypted-data" id="encrypted-vps-reg-ru">
|
||||
CklwWVhMTXRnQ0ZsR1E1NTBEWkNManEzN1Zzbk5xTDA4Um4wM2lSUzdrUTdvekJaTnNpWnlTRnpEQXZZaDR4SUh5dFJEeTBTejBLR1VYZWtyMFMrY05pSnNtdFo4a2owUGQwSmhldmh4VE1aRjJraEZ2cy9MRldvVEw1WndBSjM0eS9xZkx3MmdhV1g5MFZaRHUwd08rS2VBUWVYaHJXbW9sLzJ6VWxmV1gxQWloeVlxRDJ2UnoraDJGY2pEOHViWU9YRzM1UHlYN09sVjFIUFhnRHY1WC9iRk5yeHpQYzg2N1VPd0I0YUd4WTlhCgoK
|
||||
</div>
|
||||
|
||||
<div class="decrypted-fields" id="decrypted-docs-a2v-space-auth" style="display: none;">
|
||||
<div class="decrypted-fields" id="decrypted-vps-reg-ru" style="display: none;">
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary mt-2 decrypt-btn" data-id="docs-a2v-space-auth">
|
||||
Расшифровать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card creds-card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Eversolo Companion PostgreSQL</h3>
|
||||
|
||||
<p class="card-text">Учетные данные для базы данных Eversolo Companion</p>
|
||||
|
||||
|
||||
|
||||
<div class="encrypted-data" id="encrypted-eversolo-companion-postgres">
|
||||
RG9ja2VyIENvbXBvc2Ug0LrRgNC10LTRiyAofi9Qcm9qZWN0cy9ldmVyc29sby1jb21wYW5pb24vZG9ja2VyLWNvbXBvc2UueW1sKSBzZXJ2aWNlczogcG9zdGdyZXM6IGVudmlyb25tZW50OiBQT1NUR1JFU19VU0VSOiBwb3N0Z3JlcyBQT1NUR1JFU19QQVNTV09SRDogcGFzc3dvcmQgUE9TVEdSRVNfREI6IGV2ZXJzb2xvINCb0L7QutCw0LvRjNC90YvQtSDQutGA0LXQtNGLICguZW52INGE0LDQudC7KSBQT1NUR1JFU19VU0VSPXBvc3RncmVzIFBPU1RHUkVTX1BBU1NXT1JEPXBhc3N3b3JkIFBPU1RHUkVTX0RCPWV2ZXJzb2xvINCl0L7RgdGC0LjQvdCzIEhvc3Q6IGxvY2FsaG9zdCBQb3J0OiA1NDMzIERhdGFiYXNlOiBldmVyc29sbyDQkdC10LfQvtC/0LDRgdC90L7RgdGC0Ywg0JjRgdC/0L7Qu9GM0LfRg9C50YLQtSDRgNCw0LfQvdGL0LUg0L/QsNGA0L7Qu9C4INC00LvRjyBwcm9kdWN0aW9uINCS0LrQu9GO0YfQuNGC0LUgU1NMINC00LvRjyDRg9C00LDQu9C10L3QvdC+0LPQviDQv9C+0LTQutC70Y7Rh9C10L3QuNGPINCg0LXQs9GD0LvRj9GA0L3QviDQtNC10LvQsNC50YLQtSDQsdGN0LrQsNC/0Ysg
|
||||
</div>
|
||||
|
||||
<div class="decrypted-fields" id="decrypted-eversolo-companion-postgres" style="display: none;">
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary mt-2 decrypt-btn" data-id="eversolo-companion-postgres">
|
||||
Расшифровать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card creds-card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Mac Studio — leadera-api БД (docker-compose)</h3>
|
||||
|
||||
<p class="card-text">PostgreSQL (docker-compose) для Leadera</p>
|
||||
|
||||
|
||||
<p class="text-muted mb-2"><strong>Сервис:</strong> PostgreSQL</p>
|
||||
|
||||
|
||||
<div class="encrypted-data" id="encrypted-leadera-db-docker">
|
||||
Ck5ZVks0UEVHd21PSG1BbkdrOElBVWMrbHlGSnRuRUF2WDZaZGRCcm8vSUxSV0hPWEJHWXl3Z2dsRnZRWjE4QkxETGI1RHVBOEdQQUlkQi9Md1RIUlhZTzBLc210bFpRVldHRWVlWGxYQW9FWHBhNXRoZW9PUWhlYmtXNkxtaUt5TkdBVlcyejlySzNuejZObnp0WWRuNDlMdkdNanJIdVI0SXdDTGJsL3BZYTk2Q1BOUm54R0RGbGR1Rmk4bE1Tc3BNMHJCRmkvYlpoTkdoenkzNlZpQ2wrYUtyNVA5Y2xlMFp2TThFOGF2LzZjNUpqZ0R0ODhRRkkzVllkVGFUQ0UwK3VIc1hsbkpON1FnZy9LSDdrRAoKCg==
|
||||
</div>
|
||||
|
||||
<div class="decrypted-fields" id="decrypted-leadera-db-docker" style="display: none;">
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary mt-2 decrypt-btn" data-id="leadera-db-docker">
|
||||
Расшифровать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card creds-card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Mac Studio — leadera-api БД (локальная)</h3>
|
||||
|
||||
<p class="card-text">PostgreSQL (Docker) для Leadera</p>
|
||||
|
||||
|
||||
<p class="text-muted mb-2"><strong>Сервис:</strong> PostgreSQL</p>
|
||||
|
||||
|
||||
<div class="encrypted-data" id="encrypted-leadera-db-local">
|
||||
CnA3SXc3WTN6Ymoxa0xCY2JPWEdvTzI3b0R3Rjg1QWh4Qlc3c2FUNTM4aHhpMG9nSnB5RmVNNDlHRHVMSHp4dXlrbnZkTkMvcngzdGI0QUZuR2tDZUhTcnJDbWorVks4Wnh0N3VqZXVqVkpTUnVPZWVVbHNxa0hkbG1haVhsMktRa25ySXNxVkNpM1RubjBWNXFjRnh2bysxS3RjK3N1ckY2YkJwV3JUSHpXVjVtTHRXVmZENkM2a3Z2UC9VRmxkRkJENnYrRlFzNzl5Zm9PZS9qL2JVcnRwdDZ2NVZuZGJiQm1oVUFRY3kxQlYycnBYeE8zNzNkVGNMUTZjb1JXa3QvUkZvCgoK
|
||||
</div>
|
||||
|
||||
<div class="decrypted-fields" id="decrypted-leadera-db-local" style="display: none;">
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary mt-2 decrypt-btn" data-id="leadera-db-local">
|
||||
<button class="btn btn-primary mt-2 decrypt-btn" data-id="vps-reg-ru">
|
||||
Расшифровать
|
||||
</button>
|
||||
</div>
|
||||
@@ -268,23 +198,97 @@
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card creds-card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">VPS reg.ru</h3>
|
||||
<h3 class="card-title">Mac Studio — leadera-api БД (локальная)</h3>
|
||||
|
||||
<p class="card-text">VPS хостинг (реверс-прокси)</p>
|
||||
<p class="card-text">PostgreSQL (Docker) для Leadera</p>
|
||||
|
||||
|
||||
<p class="text-muted mb-2"><strong>Сервис:</strong> VPS</p>
|
||||
<p class="text-muted mb-2"><strong>Сервис:</strong> PostgreSQL</p>
|
||||
|
||||
|
||||
<div class="encrypted-data" id="encrypted-vps-reg-ru">
|
||||
CklwWVhMTXRnQ0ZsR1E1NTBEWkNManEzN1Zzbk5xTDA4Um4wM2lSUzdrUTdvekJaTnNpWnlTRnpEQXZZaDR4SUh5dFJEeTBTejBLR1VYZWtyMFMrY05pSnNtdFo4a2owUGQwSmhldmh4VE1aRjJraEZ2cy9MRldvVEw1WndBSjM0eS9xZkx3MmdhV1g5MFZaRHUwd08rS2VBUWVYaHJXbW9sLzJ6VWxmV1gxQWloeVlxRDJ2UnoraDJGY2pEOHViWU9YRzM1UHlYN09sVjFIUFhnRHY1WC9iRk5yeHpQYzg2N1VPd0I0YUd4WTlhCgoK
|
||||
<div class="encrypted-data" id="encrypted-leadera-db-local">
|
||||
CnA3SXc3WTN6Ymoxa0xCY2JPWEdvTzI3b0R3Rjg1QWh4Qlc3c2FUNTM4aHhpMG9nSnB5RmVNNDlHRHVMSHp4dXlrbnZkTkMvcngzdGI0QUZuR2tDZUhTcnJDbWorVks4Wnh0N3VqZXVqVkpTUnVPZWVVbHNxa0hkbG1haVhsMktRa25ySXNxVkNpM1RubjBWNXFjRnh2bysxS3RjK3N1ckY2YkJwV3JUSHpXVjVtTHRXVmZENkM2a3Z2UC9VRmxkRkJENnYrRlFzNzl5Zm9PZS9qL2JVcnRwdDZ2NVZuZGJiQm1oVUFRY3kxQlYycnBYeE8zNzNkVGNMUTZjb1JXa3QvUkZvCgoK
|
||||
</div>
|
||||
|
||||
<div class="decrypted-fields" id="decrypted-vps-reg-ru" style="display: none;">
|
||||
<div class="decrypted-fields" id="decrypted-leadera-db-local" style="display: none;">
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary mt-2 decrypt-btn" data-id="vps-reg-ru">
|
||||
<button class="btn btn-primary mt-2 decrypt-btn" data-id="leadera-db-local">
|
||||
Расшифровать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card creds-card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Mac Studio — leadera-api БД (docker-compose)</h3>
|
||||
|
||||
<p class="card-text">PostgreSQL (docker-compose) для Leadera</p>
|
||||
|
||||
|
||||
<p class="text-muted mb-2"><strong>Сервис:</strong> PostgreSQL</p>
|
||||
|
||||
|
||||
<div class="encrypted-data" id="encrypted-leadera-db-docker">
|
||||
Ck5ZVks0UEVHd21PSG1BbkdrOElBVWMrbHlGSnRuRUF2WDZaZGRCcm8vSUxSV0hPWEJHWXl3Z2dsRnZRWjE4QkxETGI1RHVBOEdQQUlkQi9Md1RIUlhZTzBLc210bFpRVldHRWVlWGxYQW9FWHBhNXRoZW9PUWhlYmtXNkxtaUt5TkdBVlcyejlySzNuejZObnp0WWRuNDlMdkdNanJIdVI0SXdDTGJsL3BZYTk2Q1BOUm54R0RGbGR1Rmk4bE1Tc3BNMHJCRmkvYlpoTkdoenkzNlZpQ2wrYUtyNVA5Y2xlMFp2TThFOGF2LzZjNUpqZ0R0ODhRRkkzVllkVGFUQ0UwK3VIc1hsbkpON1FnZy9LSDdrRAoKCg==
|
||||
</div>
|
||||
|
||||
<div class="decrypted-fields" id="decrypted-leadera-db-docker" style="display: none;">
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary mt-2 decrypt-btn" data-id="leadera-db-docker">
|
||||
Расшифровать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card creds-card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Eversolo Companion PostgreSQL</h3>
|
||||
|
||||
<p class="card-text">Учетные данные для базы данных Eversolo Companion</p>
|
||||
|
||||
|
||||
|
||||
<div class="encrypted-data" id="encrypted-eversolo-companion-postgres">
|
||||
RG9ja2VyIENvbXBvc2Ug0LrRgNC10LTRiyAofi9Qcm9qZWN0cy9ldmVyc29sby1jb21wYW5pb24vZG9ja2VyLWNvbXBvc2UueW1sKSBzZXJ2aWNlczogcG9zdGdyZXM6IGVudmlyb25tZW50OiBQT1NUR1JFU19VU0VSOiBwb3N0Z3JlcyBQT1NUR1JFU19QQVNTV09SRDogcGFzc3dvcmQgUE9TVEdSRVNfREI6IGV2ZXJzb2xvINCb0L7QutCw0LvRjNC90YvQtSDQutGA0LXQtNGLICguZW52INGE0LDQudC7KSBQT1NUR1JFU19VU0VSPXBvc3RncmVzIFBPU1RHUkVTX1BBU1NXT1JEPXBhc3N3b3JkIFBPU1RHUkVTX0RCPWV2ZXJzb2xvINCl0L7RgdGC0LjQvdCzIEhvc3Q6IGxvY2FsaG9zdCBQb3J0OiA1NDMzIERhdGFiYXNlOiBldmVyc29sbyDQkdC10LfQvtC/0LDRgdC90L7RgdGC0Ywg0JjRgdC/0L7Qu9GM0LfRg9C50YLQtSDRgNCw0LfQvdGL0LUg0L/QsNGA0L7Qu9C4INC00LvRjyBwcm9kdWN0aW9uINCS0LrQu9GO0YfQuNGC0LUgU1NMINC00LvRjyDRg9C00LDQu9C10L3QvdC+0LPQviDQv9C+0LTQutC70Y7Rh9C10L3QuNGPINCg0LXQs9GD0LvRj9GA0L3QviDQtNC10LvQsNC50YLQtSDQsdGN0LrQsNC/0Ysg
|
||||
</div>
|
||||
|
||||
<div class="decrypted-fields" id="decrypted-eversolo-companion-postgres" style="display: none;">
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary mt-2 decrypt-btn" data-id="eversolo-companion-postgres">
|
||||
Расшифровать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card creds-card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">docs.a2v.space Basic Auth</h3>
|
||||
|
||||
<p class="card-text">Учетные данные для доступа к docs.a2v.space</p>
|
||||
|
||||
|
||||
|
||||
<div class="encrypted-data" id="encrypted-docs-a2v-space-auth">
|
||||
QmFzaWMgQXV0aGVudGljYXRpb24gVXNlcm5hbWU6IGFnYWx0c292YXYgUGFzc3dvcmQ6IE1hZ2FtZXQ3ODchINCa0L7QvdGE0LjQs9GD0YDQsNGG0LjRjyBOZ2lueCBzZXJ2ZXIgeyBsaXN0ZW4gODA5MTsgc2VydmVyX25hbWUgZG9jcy5hMnYuc3BhY2U7IGF1dGhfYmFzaWMgJiMzNDtSZXN0cmljdGVkIEFjY2VzcyYjMzQ7OyBhdXRoX2Jhc2ljX3VzZXJfZmlsZSAvZXRjL25naW54Ly5odHBhc3N3ZDsgfSDQlNC+0YHRgtGD0L8gVVJMOiBodHRwczovL2RvY3MuYTJ2LnNwYWNlICjRh9C10YDQtdC3IHJldmVyc2UgcHJveHkpIExvY2FsOiBodHRwOi8vbG9jYWxob3N0OjgwOTEgKNC00LvRjyDRgNCw0LfRgNCw0LHQvtGC0LrQuCkgQXV0aDog0LLRgdC10LPQtNCwINGC0YDQtdCx0YPQtdGC0YHRjyDQnNCw0YHRgtC10YAt0L/QsNGA0L7Qu9GMINC00LvRjyDRiNC40YTRgNC+0LLQsNC90LjRjyDQktCw0LbQvdC+OiDQn9Cw0YDQvtC70Ywg0LTQu9GPINGA0LDRgdGI0LjRhNGA0L7QstC60Lgg0YTQsNC50LvQvtCyINCyINGA0LDQt9C00LXQu9C1IGNyZWRzLwpNYXN0ZXIgUGFzc3dvcmQ6IFBvcG9sb2thMTYhINCo0LjRhNGA0L7QstCw0L3QuNC1INCQ0LvQs9C+0YDQuNGC0Lw6IEFFUy0yNTYtR0NNINCl0YDQsNC90LjQu9C40YnQtTogY2xpZW50LXNpZGUgZW5jcnlwdGlvbiDQpNCw0LnQu9GLINGI0LjRhNGA0YPRjtGC0YHRjyDQv9GA0Lgg0YHQvtGF0YDQsNC90LXQvdC40Lgg0KDQsNGB0YjQuNGE0YDQvtCy0YvQstCw0Y7RgtGB0Y8g0L/RgNC4INGH0YLQtdC90LjQuCA=
|
||||
</div>
|
||||
|
||||
<div class="decrypted-fields" id="decrypted-docs-a2v-space-auth" style="display: none;">
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary mt-2 decrypt-btn" data-id="docs-a2v-space-auth">
|
||||
Расшифровать
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -135,6 +139,15 @@
|
||||
|
||||
<div class="article-content creds-card">
|
||||
<h1>Mac Studio — leadera-api БД (docker-compose)</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/creds/leadera-db-docker.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/creds\/leadera-db-docker.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p>PostgreSQL (docker-compose) для Leadera</p>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -135,6 +139,15 @@
|
||||
|
||||
<div class="article-content creds-card">
|
||||
<h1>Mac Studio — leadera-api БД (локальная)</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/creds/leadera-db-local.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/creds\/leadera-db-local.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p>PostgreSQL (Docker) для Leadera</p>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -135,6 +139,15 @@
|
||||
|
||||
<div class="article-content creds-card">
|
||||
<h1>VPN VPS</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/creds/vpn-vps.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/creds\/vpn-vps.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p>VPN сервер (Amnezia)</p>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -135,6 +139,15 @@
|
||||
|
||||
<div class="article-content creds-card">
|
||||
<h1>VPS reg.ru</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/creds/vps-reg-ru.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/creds\/vps-reg-ru.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p>VPS хостинг (реверс-прокси)</p>
|
||||
|
||||
@@ -270,6 +270,27 @@ body {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Source path */
|
||||
.source-path {
|
||||
background-color: var(--warm-bg);
|
||||
border: 1px solid #e0d8cc;
|
||||
border-radius: 4px;
|
||||
padding: 0.4rem 0.75rem;
|
||||
display: inline-block;
|
||||
}
|
||||
.source-path code {
|
||||
font-size: 0.82em;
|
||||
color: var(--warm-gray);
|
||||
}
|
||||
.source-path .btn-outline-secondary {
|
||||
font-size: 0.75rem;
|
||||
border-color: #d0c8bc;
|
||||
}
|
||||
.source-path .btn-outline-secondary:hover {
|
||||
background-color: var(--warm-primary);
|
||||
border-color: var(--warm-primary);
|
||||
}
|
||||
|
||||
/* Article styles */
|
||||
.article-content {
|
||||
background-color: var(--warm-white);
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -205,6 +209,15 @@
|
||||
<div class="article-content">
|
||||
<h1>29 марта 2026</h1>
|
||||
<p class="diary-date">29.03.2026</p>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/diary/2026-03-29.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/diary\/2026-03-29.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
<h2 id="timeline">Timeline</h2>
|
||||
<ul>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -205,6 +209,15 @@
|
||||
<div class="article-content">
|
||||
<h1>30 марта 2026</h1>
|
||||
<p class="diary-date">30.03.2026</p>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/diary/2026-03-30.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/diary\/2026-03-30.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
<h2 id="timeline">Timeline</h2>
|
||||
<ul>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -205,6 +209,15 @@
|
||||
<div class="article-content">
|
||||
<h1>31 марта 2026</h1>
|
||||
<p class="diary-date">31.03.2026</p>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/diary/2026-03-31.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/diary\/2026-03-31.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
<h2 id="timeline">Timeline</h2>
|
||||
<ul>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -110,16 +114,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -166,14 +160,6 @@
|
||||
<h6 class="mt-3">Теги</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/audit/">audit</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/leadera/">leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/security/">security</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/architecture/">architecture</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ssh/">ssh</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/tunnel/">tunnel</a></li>
|
||||
@@ -209,6 +195,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Подключение claw.a2v.space (OpenClaw)</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/guides/claw-a2v-space.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/guides\/claw-a2v-space.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -110,16 +114,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -166,14 +160,6 @@
|
||||
<h6 class="mt-3">Теги</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/audit/">audit</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/leadera/">leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/security/">security</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/architecture/">architecture</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ssh/">ssh</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/tunnel/">tunnel</a></li>
|
||||
@@ -209,6 +195,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Подключение code.a2v.space (OpenCode)</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/guides/code-a2v-space.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/guides\/code-a2v-space.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
+187
-258
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -110,16 +114,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -166,14 +160,6 @@
|
||||
<h6 class="mt-3">Теги</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../tags/audit/">audit</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../tags/leadera/">leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../tags/security/">security</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../tags/architecture/">architecture</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../tags/ssh/">ssh</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../tags/tunnel/">tunnel</a></li>
|
||||
@@ -215,225 +201,82 @@
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../guides/leadera-audit/">Аудит Leadera — полный отчёт</a></h3>
|
||||
<p class="card-text"><h1 id="аудит-проекта-leadera">Аудит проекта Leadera</h1>
|
||||
<p><strong>Дата:</strong> 30.03.2026<br>
|
||||
<strong>Версия:</strong> pre-production, миграции 1–5</p>
|
||||
<hr>
|
||||
<h2 id="1-обзор-проекта">1. Обзор проекта</h2>
|
||||
<p>Leadera — SaaS-система управления IT-ландшафтом с диаграммами Ганта.</p>
|
||||
<h3 id="стек">Стек</h3>
|
||||
<h3 class="card-title"><a href="../guides/sand-a2v-space/">Подключение sand.a2v.space (Leadera Project)</a></h3>
|
||||
<p class="card-text"><h2 id="архитектура">Архитектура</h2>
|
||||
<pre tabindex="0"><code>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
|
||||
</code></pre><h2 id="параметры">Параметры</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Компонент</th>
|
||||
<th>Технологии</th>
|
||||
<th>Локальный порт</th>
|
||||
<th>Туннель порт</th>
|
||||
<th>pm2 имя</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Backend</td>
|
||||
<td>Go 1.21+, Gin, PostgreSQL 16, pgx/v5 + sqlx, JWT (HMAC-SHA256)</td>
|
||||
<td>Frontend (Angular)</td>
|
||||
<td>4200</td>
|
||||
<td>8082</td>
|
||||
<td>leadera-fe</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Frontend</td>
|
||||
<td>Angular 21.1, Bootstrap 5, NgBootstrap, RxJS, Signals</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Инфраструктура</td>
|
||||
<td>Docker (Colima), pm2, nginx reverse proxy, VPS reg.ru</td>
|
||||
<td>Backend (Go)</td>
|
||||
<td>8080</td>
|
||||
<td>8083</td>
|
||||
<td>leadera-api</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="доменная-модель">Доменная модель</h3>
|
||||
<pre tabindex="0"><code>User ──┬── Space (через space_members с role)
|
||||
│
|
||||
└── GanttChart (принадлежит Space)
|
||||
├── GanttSection
|
||||
└── GanttTask
|
||||
</code></pre><p><strong>Роли:</strong> admin, editor, viewer — назначаются <strong>на уровне Space</strong> через таблицу <code>space_members</code>.</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/audit/" class="tag">audit</a>
|
||||
|
||||
<a href="../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../tags/security/" class="tag">security</a>
|
||||
|
||||
<a href="../tags/architecture/" class="tag">architecture</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../guides/ssh-tunnels-vps/">SSH туннели для VPS reverse proxy</a></h3>
|
||||
<p class="card-text"><h2 id="настройка-ssh-туннелей">Настройка SSH туннелей</h2>
|
||||
<h3 id="vps-regru-951632329">VPS reg.ru (95.163.232.9)</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># На Mac Studio: создать туннель для каждого сервиса</span>
|
||||
</span></span><span style="display:flex;"><span>ssh -N -R 8080:localhost:8080 root@95.163.232.9 <span style="color:#75715e"># leadera-api</span>
|
||||
</span></span><span style="display:flex;"><span>ssh -N -R 8081:localhost:4096 root@95.163.232.9 <span style="color:#75715e"># opencode</span>
|
||||
</span></span><span style="display:flex;"><span>ssh -N -R 8082:localhost:4200 root@95.163.232.9 <span style="color:#75715e"># leadera-fe</span>
|
||||
</span></span><span style="display:flex;"><span>ssh -N -R 8083:localhost:8080 root@95.163.232.9 <span style="color:#75715e"># docs-a2v</span>
|
||||
</span></span></code></pre></div><h3 id="настройка-launchd-plist">Настройка launchd plist</h3>
|
||||
<p><strong>Файл</strong>: <code>~/Library/LaunchAgents/com.openclaw.sshtunnel.plist</code></p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#75715e"><?xml version="1.0" encoding="UTF-8"?></span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e"><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"></span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672"><plist</span> <span style="color:#a6e22e">version=</span><span style="color:#e6db74">"1.0"</span><span style="color:#f92672">></span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672"><dict></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><key></span>Label<span style="color:#f92672"></key></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>com.openclaw.sshtunnel<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><key></span>ProgramArguments<span style="color:#f92672"></key></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><array></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>ssh<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>-N<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>-R<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>8080:localhost:8080<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>root@95.163.232.9<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"></array></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><key></span>RunAtLoad<span style="color:#f92672"></key></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><true/></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><key></span>KeepAlive<span style="color:#f92672"></key></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><true/></span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672"></dict></span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672"></plist></span>
|
||||
</span></span></code></pre></div><h3 id="автозапуск">Автозапуск</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Загрузить plist</span>
|
||||
</span></span><span style="display:flex;"><span>launchctl load ~/Library/LaunchAgents/com.openclaw.sshtunnel.plist
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Проверить статус</span>
|
||||
</span></span><span style="display:flex;"><span>launchctl list | grep sshtunnel
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Удалить (при необходимости)</span>
|
||||
</span></span><span style="display:flex;"><span>launchctl unload ~/Library/LaunchAgents/com.openclaw.sshtunnel.plist
|
||||
</span></span></code></pre></div><h2 id="доступ-к-сервисам">Доступ к сервисам</h2>
|
||||
<p>Через reverse proxy на VPS:</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/ssh/" class="tag">ssh</a>
|
||||
|
||||
<a href="../tags/tunnel/" class="tag">tunnel</a>
|
||||
|
||||
<a href="../tags/vps/" class="tag">vps</a>
|
||||
|
||||
<a href="../tags/reverse-proxy/" class="tag">reverse-proxy</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../guides/vps-reverse-proxy/">Настройка VPS как реверс-прокси</a></h3>
|
||||
<p class="card-text"><h2 id="общая-схема">Общая схема</h2>
|
||||
<pre tabindex="0"><code>Mac Studio (локальная сеть)
|
||||
→ autossh (SSH-туннели)
|
||||
→ VPS 95.163.232.9 (Debian)
|
||||
→ nginx (HTTPS reverse proxy)
|
||||
→ интернет
|
||||
</code></pre><h2 id="что-нужно-на-vps">Что нужно на VPS</h2>
|
||||
<h3 id="1-установленные-пакеты">1. Установленные пакеты</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>apt update
|
||||
</span></span><span style="display:flex;"><span>apt install nginx certbot python3-certbot-nginx
|
||||
</span></span></code></pre></div><h3 id="2-ssh-доступ">2. SSH-доступ</h3>
|
||||
<ul>
|
||||
<li>Авторизация только по ключам</li>
|
||||
<li>Публичный ключ с Mac Studio добавлен в <code>/root/.ssh/authorized_keys</code></li>
|
||||
</ul>
|
||||
<h3 id="3-nginx">3. nginx</h3>
|
||||
<p>Для каждого домена создать server block:</p>
|
||||
<ul>
|
||||
<li>HTTP → редирект на HTTPS</li>
|
||||
<li>HTTPS с SSL-сертификатом Let’s Encrypt</li>
|
||||
<li><code>proxy_pass</code> на localhost-порт туннеля</li>
|
||||
</ul>
|
||||
<p>Пример структуры:</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/vps/" class="tag">vps</a>
|
||||
|
||||
<a href="../tags/nginx/" class="tag">nginx</a>
|
||||
|
||||
<a href="../tags/ssh/" class="tag">ssh</a>
|
||||
|
||||
<a href="../tags/reverse-proxy/" class="tag">reverse-proxy</a>
|
||||
|
||||
<a href="../tags/devops/" class="tag">devops</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../guides/claw-a2v-space/">Подключение claw.a2v.space (OpenClaw)</a></h3>
|
||||
<p class="card-text"><h2 id="архитектура">Архитектура</h2>
|
||||
<pre tabindex="0"><code>Mac Studio (192.168.0.37:18789)
|
||||
→ autossh SSH-туннель
|
||||
→ VPS 95.163.232.9:8080
|
||||
→ nginx HTTPS
|
||||
→ https://claw.a2v.space
|
||||
</code></pre><h2 id="шаги-настройки">Шаги настройки</h2>
|
||||
<h3 id="1-dns">1. DNS</h3>
|
||||
<p>A-запись <code>claw.a2v.space</code> → <code>95.163.232.9</code></p>
|
||||
<p>NS-серверы: <code>ns1.hosting.reg.ru</code>, <code>ns2.hosting.reg.ru</code></p>
|
||||
<h3 id="2-mac-studio--openclaw">2. Mac Studio — OpenClaw</h3>
|
||||
<ul>
|
||||
<li>Порт: 18789</li>
|
||||
<li>Конфиг: <code>~/.openclaw/openclaw.json</code></li>
|
||||
</ul>
|
||||
<h3 id="3-ssh-туннель-launchd">3. SSH-туннель (launchd)</h3>
|
||||
<p>Plist: <code>~/Library/LaunchAgents/com.openclaw.sshtunnel.plist</code></p>
|
||||
<p>Ключевой флаг: <code>-R 8080:localhost:18789</code></p>
|
||||
<h3 id="4-vps--nginx-конфиг">4. VPS — nginx конфиг</h3>
|
||||
<p>SSL сертификат: <code>/etc/letsencrypt/live/sand.a2v.space/</code></p>
|
||||
<h2 id="nginx-конфиг">nginx конфиг</h2>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">server_name</span> <span style="color:#e6db74">claw.a2v.space</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">server_name</span> <span style="color:#e6db74">sand.a2v.space</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">return</span> <span style="color:#ae81ff">301</span> <span style="color:#e6db74">https://</span>$server_name$request_uri;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">listen</span> <span style="color:#ae81ff">443</span> <span style="color:#e6db74">ssl</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">server_name</span> <span style="color:#e6db74">claw.a2v.space</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">server_name</span> <span style="color:#e6db74">sand.a2v.space</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ssl_certificate</span> <span style="color:#e6db74">/etc/letsencrypt/live/claw.a2v.space/fullchain.pem</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ssl_certificate_key</span> <span style="color:#e6db74">/etc/letsencrypt/live/claw.a2v.space/privkey.pem</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ssl_certificate</span> <span style="color:#e6db74">/etc/letsencrypt/live/sand.a2v.space/fullchain.pem</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ssl_certificate_key</span> <span style="color:#e6db74">/etc/letsencrypt/live/sand.a2v.space/privkey.pem</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">location</span> <span style="color:#e6db74">/api</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8083</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Real-IP</span> $remote_addr;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $proxy_add_x_forwarded_for;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> $scheme;
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8080</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8082</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Real-IP</span> $remote_addr;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $proxy_add_x_forwarded_for;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> $scheme;
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><h3 id="5-ssl-сертификат">5. SSL-сертификат</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>certbot certonly --nginx -d claw.a2v.space
|
||||
</span></span></code></pre></div><h3 id="6-openclaw-config">6. OpenClaw config</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"allowedOrigins"</span>: [<span style="color:#e6db74">"https://claw.a2v.space"</span>],
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"trustedProxies"</span>: [<span style="color:#e6db74">"127.0.0.1"</span>]
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div></p>
|
||||
</span></span></code></pre></div><h2 id="база-данных">База данных</h2>
|
||||
<p>PostgreSQL 16 в Docker через Colima:</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/openclaw/" class="tag">openclaw</a>
|
||||
<a href="../tags/angular/" class="tag">angular</a>
|
||||
|
||||
<a href="../tags/ssh/" class="tag">ssh</a>
|
||||
<a href="../tags/go/" class="tag">go</a>
|
||||
|
||||
<a href="../tags/docker/" class="tag">docker</a>
|
||||
|
||||
<a href="../tags/nginx/" class="tag">nginx</a>
|
||||
|
||||
@@ -534,82 +377,61 @@
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../guides/sand-a2v-space/">Подключение sand.a2v.space (Leadera Project)</a></h3>
|
||||
<h3 class="card-title"><a href="../guides/claw-a2v-space/">Подключение claw.a2v.space (OpenClaw)</a></h3>
|
||||
<p class="card-text"><h2 id="архитектура">Архитектура</h2>
|
||||
<pre tabindex="0"><code>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
|
||||
<pre tabindex="0"><code>Mac Studio (192.168.0.37:18789)
|
||||
→ autossh SSH-туннель
|
||||
→ VPS 95.163.232.9:8080
|
||||
→ nginx HTTPS
|
||||
→ https://sand.a2v.space
|
||||
</code></pre><h2 id="параметры">Параметры</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Компонент</th>
|
||||
<th>Локальный порт</th>
|
||||
<th>Туннель порт</th>
|
||||
<th>pm2 имя</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Frontend (Angular)</td>
|
||||
<td>4200</td>
|
||||
<td>8082</td>
|
||||
<td>leadera-fe</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Backend (Go)</td>
|
||||
<td>8080</td>
|
||||
<td>8083</td>
|
||||
<td>leadera-api</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>SSL сертификат: <code>/etc/letsencrypt/live/sand.a2v.space/</code></p>
|
||||
<h2 id="nginx-конфиг">nginx конфиг</h2>
|
||||
→ https://claw.a2v.space
|
||||
</code></pre><h2 id="шаги-настройки">Шаги настройки</h2>
|
||||
<h3 id="1-dns">1. DNS</h3>
|
||||
<p>A-запись <code>claw.a2v.space</code> → <code>95.163.232.9</code></p>
|
||||
<p>NS-серверы: <code>ns1.hosting.reg.ru</code>, <code>ns2.hosting.reg.ru</code></p>
|
||||
<h3 id="2-mac-studio--openclaw">2. Mac Studio — OpenClaw</h3>
|
||||
<ul>
|
||||
<li>Порт: 18789</li>
|
||||
<li>Конфиг: <code>~/.openclaw/openclaw.json</code></li>
|
||||
</ul>
|
||||
<h3 id="3-ssh-туннель-launchd">3. SSH-туннель (launchd)</h3>
|
||||
<p>Plist: <code>~/Library/LaunchAgents/com.openclaw.sshtunnel.plist</code></p>
|
||||
<p>Ключевой флаг: <code>-R 8080:localhost:18789</code></p>
|
||||
<h3 id="4-vps--nginx-конфиг">4. VPS — nginx конфиг</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">server_name</span> <span style="color:#e6db74">sand.a2v.space</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">server_name</span> <span style="color:#e6db74">claw.a2v.space</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">return</span> <span style="color:#ae81ff">301</span> <span style="color:#e6db74">https://</span>$server_name$request_uri;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">listen</span> <span style="color:#ae81ff">443</span> <span style="color:#e6db74">ssl</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">server_name</span> <span style="color:#e6db74">sand.a2v.space</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">server_name</span> <span style="color:#e6db74">claw.a2v.space</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ssl_certificate</span> <span style="color:#e6db74">/etc/letsencrypt/live/sand.a2v.space/fullchain.pem</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ssl_certificate_key</span> <span style="color:#e6db74">/etc/letsencrypt/live/sand.a2v.space/privkey.pem</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">location</span> <span style="color:#e6db74">/api</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8083</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Real-IP</span> $remote_addr;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $proxy_add_x_forwarded_for;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> $scheme;
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ssl_certificate</span> <span style="color:#e6db74">/etc/letsencrypt/live/claw.a2v.space/fullchain.pem</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ssl_certificate_key</span> <span style="color:#e6db74">/etc/letsencrypt/live/claw.a2v.space/privkey.pem</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8082</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8080</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Real-IP</span> $remote_addr;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $proxy_add_x_forwarded_for;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> $scheme;
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><h2 id="база-данных">База данных</h2>
|
||||
<p>PostgreSQL 16 в Docker через Colima:</p></p>
|
||||
</span></span></code></pre></div><h3 id="5-ssl-сертификат">5. SSL-сертификат</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>certbot certonly --nginx -d claw.a2v.space
|
||||
</span></span></code></pre></div><h3 id="6-openclaw-config">6. OpenClaw config</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"allowedOrigins"</span>: [<span style="color:#e6db74">"https://claw.a2v.space"</span>],
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"trustedProxies"</span>: [<span style="color:#e6db74">"127.0.0.1"</span>]
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/angular/" class="tag">angular</a>
|
||||
<a href="../tags/openclaw/" class="tag">openclaw</a>
|
||||
|
||||
<a href="../tags/go/" class="tag">go</a>
|
||||
|
||||
<a href="../tags/docker/" class="tag">docker</a>
|
||||
<a href="../tags/ssh/" class="tag">ssh</a>
|
||||
|
||||
<a href="../tags/nginx/" class="tag">nginx</a>
|
||||
|
||||
@@ -623,6 +445,113 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../guides/vps-reverse-proxy/">Настройка VPS как реверс-прокси</a></h3>
|
||||
<p class="card-text"><h2 id="общая-схема">Общая схема</h2>
|
||||
<pre tabindex="0"><code>Mac Studio (локальная сеть)
|
||||
→ autossh (SSH-туннели)
|
||||
→ VPS 95.163.232.9 (Debian)
|
||||
→ nginx (HTTPS reverse proxy)
|
||||
→ интернет
|
||||
</code></pre><h2 id="что-нужно-на-vps">Что нужно на VPS</h2>
|
||||
<h3 id="1-установленные-пакеты">1. Установленные пакеты</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>apt update
|
||||
</span></span><span style="display:flex;"><span>apt install nginx certbot python3-certbot-nginx
|
||||
</span></span></code></pre></div><h3 id="2-ssh-доступ">2. SSH-доступ</h3>
|
||||
<ul>
|
||||
<li>Авторизация только по ключам</li>
|
||||
<li>Публичный ключ с Mac Studio добавлен в <code>/root/.ssh/authorized_keys</code></li>
|
||||
</ul>
|
||||
<h3 id="3-nginx">3. nginx</h3>
|
||||
<p>Для каждого домена создать server block:</p>
|
||||
<ul>
|
||||
<li>HTTP → редирект на HTTPS</li>
|
||||
<li>HTTPS с SSL-сертификатом Let’s Encrypt</li>
|
||||
<li><code>proxy_pass</code> на localhost-порт туннеля</li>
|
||||
</ul>
|
||||
<p>Пример структуры:</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/vps/" class="tag">vps</a>
|
||||
|
||||
<a href="../tags/nginx/" class="tag">nginx</a>
|
||||
|
||||
<a href="../tags/ssh/" class="tag">ssh</a>
|
||||
|
||||
<a href="../tags/reverse-proxy/" class="tag">reverse-proxy</a>
|
||||
|
||||
<a href="../tags/devops/" class="tag">devops</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../guides/ssh-tunnels-vps/">SSH туннели для VPS reverse proxy</a></h3>
|
||||
<p class="card-text"><h2 id="настройка-ssh-туннелей">Настройка SSH туннелей</h2>
|
||||
<h3 id="vps-regru-951632329">VPS reg.ru (95.163.232.9)</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># На Mac Studio: создать туннель для каждого сервиса</span>
|
||||
</span></span><span style="display:flex;"><span>ssh -N -R 8080:localhost:8080 root@95.163.232.9 <span style="color:#75715e"># leadera-api</span>
|
||||
</span></span><span style="display:flex;"><span>ssh -N -R 8081:localhost:4096 root@95.163.232.9 <span style="color:#75715e"># opencode</span>
|
||||
</span></span><span style="display:flex;"><span>ssh -N -R 8082:localhost:4200 root@95.163.232.9 <span style="color:#75715e"># leadera-fe</span>
|
||||
</span></span><span style="display:flex;"><span>ssh -N -R 8083:localhost:8080 root@95.163.232.9 <span style="color:#75715e"># docs-a2v</span>
|
||||
</span></span></code></pre></div><h3 id="настройка-launchd-plist">Настройка launchd plist</h3>
|
||||
<p><strong>Файл</strong>: <code>~/Library/LaunchAgents/com.openclaw.sshtunnel.plist</code></p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#75715e"><?xml version="1.0" encoding="UTF-8"?></span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e"><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"></span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672"><plist</span> <span style="color:#a6e22e">version=</span><span style="color:#e6db74">"1.0"</span><span style="color:#f92672">></span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672"><dict></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><key></span>Label<span style="color:#f92672"></key></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>com.openclaw.sshtunnel<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><key></span>ProgramArguments<span style="color:#f92672"></key></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><array></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>ssh<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>-N<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>-R<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>8080:localhost:8080<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>root@95.163.232.9<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"></array></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><key></span>RunAtLoad<span style="color:#f92672"></key></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><true/></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><key></span>KeepAlive<span style="color:#f92672"></key></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><true/></span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672"></dict></span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672"></plist></span>
|
||||
</span></span></code></pre></div><h3 id="автозапуск">Автозапуск</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Загрузить plist</span>
|
||||
</span></span><span style="display:flex;"><span>launchctl load ~/Library/LaunchAgents/com.openclaw.sshtunnel.plist
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Проверить статус</span>
|
||||
</span></span><span style="display:flex;"><span>launchctl list | grep sshtunnel
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Удалить (при необходимости)</span>
|
||||
</span></span><span style="display:flex;"><span>launchctl unload ~/Library/LaunchAgents/com.openclaw.sshtunnel.plist
|
||||
</span></span></code></pre></div><h2 id="доступ-к-сервисам">Доступ к сервисам</h2>
|
||||
<p>Через reverse proxy на VPS:</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/ssh/" class="tag">ssh</a>
|
||||
|
||||
<a href="../tags/tunnel/" class="tag">tunnel</a>
|
||||
|
||||
<a href="../tags/vps/" class="tag">vps</a>
|
||||
|
||||
<a href="../tags/reverse-proxy/" class="tag">reverse-proxy</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -110,6 +110,58 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -174,6 +226,24 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/architecture/">architecture</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/websocket/">websocket</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/realtime/">realtime</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/gantt/">gantt</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/api/">api</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/frontend/">frontend</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/requirements/">requirements</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/angular/">angular</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/go/">go</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ux/">ux</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ssh/">ssh</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/tunnel/">tunnel</a></li>
|
||||
@@ -190,10 +260,6 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/opencode/">opencode</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/angular/">angular</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/go/">go</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/docker/">docker</a></li>
|
||||
|
||||
</ul>
|
||||
@@ -209,6 +275,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Аудит Leadera — полный отчёт</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/guides/leadera-audit.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/guides\/leadera-audit.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
@@ -0,0 +1,621 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Leadera — Фильтры и реорганизация списка диаграмм Ганта — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Фильтры</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Все инструкции</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h6 class="mt-3">Теги</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/audit/">audit</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/leadera/">leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/security/">security</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/architecture/">architecture</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/websocket/">websocket</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/realtime/">realtime</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/gantt/">gantt</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/api/">api</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/frontend/">frontend</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/requirements/">requirements</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/angular/">angular</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/go/">go</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ux/">ux</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ssh/">ssh</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/tunnel/">tunnel</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/vps/">vps</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/reverse-proxy/">reverse-proxy</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/nginx/">nginx</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/devops/">devops</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/openclaw/">openclaw</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/opencode/">opencode</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/docker/">docker</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Leadera — Фильтры и реорганизация списка диаграмм Ганта</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/guides/leadera-gantt-filters.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/guides\/leadera-gantt-filters.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
<a href="../../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../../tags/api/" class="tag">api</a>
|
||||
|
||||
<a href="../../tags/frontend/" class="tag">frontend</a>
|
||||
|
||||
<a href="../../tags/requirements/" class="tag">requirements</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h1 id="фильтры-и-реорганизация-списка-диаграмм-ганта">Фильтры и реорганизация списка диаграмм Ганта</h1>
|
||||
<p><strong>Дата:</strong> 07.04.2026
|
||||
<strong>Статус:</strong> Требования
|
||||
<strong>Проект:</strong> Leadera (dev — sand.a2v.space)</p>
|
||||
<hr>
|
||||
<h2 id="1-постановка-задачи">1. Постановка задачи</h2>
|
||||
<p>Список диаграмм Ганта должен показывать <strong>все доступные пользователю диаграммы</strong> из всех пространств (spaces), с возможностью фильтрации и поиска. Сейчас список привязан к одному пространству — нужно убрать эту зависимость и добавить удобный toolbar.</p>
|
||||
<hr>
|
||||
<h2 id="2-изменения-api">2. Изменения API</h2>
|
||||
<h3 id="21-новый-endpoint-списка-диаграмм">2.1. Новый endpoint списка диаграмм</h3>
|
||||
<p><strong>Было:</strong></p>
|
||||
<pre tabindex="0"><code>GET /api/v1/spaces/{spaceId}/gantt-charts?page=1&per_page=10
|
||||
</code></pre><p><strong>Стало:</strong></p>
|
||||
<pre tabindex="0"><code>GET /api/v1/gantt-charts?page=1&per_page=10
|
||||
</code></pre><p>Пользователь видит все диаграммы из <strong>всех пространств</strong>, где он является подтверждённым участником (статус <code>accepted</code>).</p>
|
||||
<h3 id="22-параметры-фильтрации-и-поиска">2.2. Параметры фильтрации и поиска</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Параметр</th>
|
||||
<th>Тип</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>page</code></td>
|
||||
<td>int</td>
|
||||
<td>Номер страницы (default: 1)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>per_page</code></td>
|
||||
<td>int</td>
|
||||
<td>Записей на странице (default: 10)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>space_id</code></td>
|
||||
<td>UUID (optional)</td>
|
||||
<td>Фильтр по конкретному пространству</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>created_by</code></td>
|
||||
<td>UUID (optional)</td>
|
||||
<td>Фильтр по создателю диаграммы</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>search</code></td>
|
||||
<td>string (optional)</td>
|
||||
<td>Поиск по назвению и описанию (ILIKE, case-insensitive)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>sort</code></td>
|
||||
<td>string (optional)</td>
|
||||
<td>Сортировка: <code>created_at</code>, <code>updated_at</code>, <code>title</code> (default: <code>updated_at</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>order</code></td>
|
||||
<td>string (optional)</td>
|
||||
<td><code>asc</code> / <code>desc</code> (default: <code>desc</code>)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Примеры:</strong></p>
|
||||
<pre tabindex="0"><code>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
|
||||
</code></pre><h3 id="23-изменение-структуры-ответа">2.3. Изменение структуры ответа</h3>
|
||||
<p>Добавить поле <code>space</code> в каждую диаграмму:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"data"</span>: [
|
||||
</span></span><span style="display:flex;"><span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"title"</span>: <span style="color:#e6db74">"Миграция backend"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"description"</span>: <span style="color:#e6db74">"..."</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"created_by"</span>: <span style="color:#e6db74">"uuid"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"created_by_name"</span>: <span style="color:#e6db74">"Антон Агальцов"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"created_at"</span>: <span style="color:#e6db74">"2026-04-01T10:00:00Z"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"updated_at"</span>: <span style="color:#e6db74">"2026-04-07T09:00:00Z"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"space"</span>: {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"name"</span>: <span style="color:#e6db74">"IT Ландшафт"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"color"</span>: <span style="color:#e6db74">"#c4623a"</span>
|
||||
</span></span><span style="display:flex;"><span> },
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"is_archived"</span>: <span style="color:#66d9ef">false</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> ],
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"pagination"</span>: {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"page"</span>: <span style="color:#ae81ff">1</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"per_page"</span>: <span style="color:#ae81ff">10</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"total"</span>: <span style="color:#ae81ff">42</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"total_pages"</span>: <span style="color:#ae81ff">5</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><h3 id="24-endpoint-для-списка-фильтров">2.4. Endpoint для списка фильтров</h3>
|
||||
<p>Вспомогательный endpoint для заполнения dropdown-фильтров:</p>
|
||||
<pre tabindex="0"><code>GET /api/v1/gantt-charts/filters
|
||||
</code></pre><p><strong>Ответ:</strong></p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"spaces"</span>: [
|
||||
</span></span><span style="display:flex;"><span> { <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>, <span style="color:#f92672">"name"</span>: <span style="color:#e6db74">"IT Ландшафт"</span>, <span style="color:#f92672">"color"</span>: <span style="color:#e6db74">"#c4623a"</span> },
|
||||
</span></span><span style="display:flex;"><span> { <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>, <span style="color:#f92672">"name"</span>: <span style="color:#e6db74">"DevOps"</span>, <span style="color:#f92672">"color"</span>: <span style="color:#e6db74">"#27ae60"</span> }
|
||||
</span></span><span style="display:flex;"><span> ],
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"creators"</span>: [
|
||||
</span></span><span style="display:flex;"><span> { <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>, <span style="color:#f92672">"display_name"</span>: <span style="color:#e6db74">"Антон Агальцов"</span> },
|
||||
</span></span><span style="display:flex;"><span> { <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>, <span style="color:#f92672">"display_name"</span>: <span style="color:#e6db74">"Иван Петров"</span> }
|
||||
</span></span><span style="display:flex;"><span> ]
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><p><strong>Логика <code>creators</code>:</strong> уникальные создатели всех диаграмм, доступных текущему пользователю. Получается через DISTINCT-запрос по <code>created_by</code> с JOIN на users.</p>
|
||||
<h3 id="25-backend--изменения-в-go">2.5. Backend — изменения в Go</h3>
|
||||
<p><strong>Файлы для изменения:</strong></p>
|
||||
<h4 id="internalhandlersgantt_handlergo"><code>internal/handlers/gantt_handler.go</code></h4>
|
||||
<ul>
|
||||
<li>Добавить новый handler <code>ListAllGanttCharts</code> — без привязки к space</li>
|
||||
<li>Зарегистрировать маршрут <code>GET /gantt-charts</code> (без <code>/spaces/:id/</code>)</li>
|
||||
<li>Парсить query-параметры: <code>space_id</code>, <code>created_by</code>, <code>search</code>, <code>sort</code>, <code>order</code></li>
|
||||
<li>Старый маршрут <code>GET /spaces/:id/gantt-charts</code> — оставить для обратной совместимости (или удалить, если не используется)</li>
|
||||
</ul>
|
||||
<h4 id="internalservicegantt_servicego"><code>internal/service/gantt_service.go</code></h4>
|
||||
<ul>
|
||||
<li>Новый метод <code>ListAll(ctx, userId, filters)</code> — запрашивает все spaces пользователя (через <code>space_member</code> с <code>status=accepted</code>), затем выбирает все gantt_charts из этих spaces с фильтрами</li>
|
||||
</ul>
|
||||
<h4 id="internalrepositorygantt_repositorygo"><code>internal/repository/gantt_repository.go</code></h4>
|
||||
<ul>
|
||||
<li>Новый метод с динамическим SQL:
|
||||
<ul>
|
||||
<li>Базовый JOIN: <code>gantt_charts</code> → <code>spaces</code> → <code>space_members</code></li>
|
||||
<li>WHERE: <code>space_members.user_id = $1 AND space_members.status = 'accepted'</code></li>
|
||||
<li>Опциональные фильтры: <code>AND space_id = $N</code>, <code>AND created_by = $N</code>, <code>AND (title ILIKE '%' || $N || '%' OR description ILIKE '%' || $N || '%')</code></li>
|
||||
<li>Возвращать поле <code>space</code> (id, name, color) через JOIN</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h4 id="internaldtogantt"><code>internal/dto/gantt/</code></h4>
|
||||
<ul>
|
||||
<li>Новые DTO: <code>GanttChartListFilters</code>, <code>GanttChartWithSpaceResponse</code>, <code>GanttFiltersResponse</code></li>
|
||||
</ul>
|
||||
<p><strong>SQL (пример):</strong></p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> gc.<span style="color:#f92672">*</span>, s.id <span style="color:#66d9ef">as</span> space_id, s.name <span style="color:#66d9ef">as</span> space_name, s.color <span style="color:#66d9ef">as</span> space_color,
|
||||
</span></span><span style="display:flex;"><span> u.display_name <span style="color:#66d9ef">as</span> created_by_name
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> gantt_charts gc
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">JOIN</span> spaces s <span style="color:#66d9ef">ON</span> s.id <span style="color:#f92672">=</span> gc.space_id
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">JOIN</span> space_members sm <span style="color:#66d9ef">ON</span> sm.space_id <span style="color:#f92672">=</span> s.id
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">LEFT</span> <span style="color:#66d9ef">JOIN</span> users u <span style="color:#66d9ef">ON</span> u.id <span style="color:#f92672">=</span> gc.created_by
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> sm.user_id <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">1</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">AND</span> sm.status <span style="color:#f92672">=</span> <span style="color:#e6db74">'accepted'</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">AND</span> (<span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">2</span>::uuid <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">OR</span> gc.space_id <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">2</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">AND</span> (<span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">3</span>::uuid <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">OR</span> gc.created_by <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">3</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">AND</span> (<span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">4</span>::text <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">OR</span> gc.title <span style="color:#66d9ef">ILIKE</span> <span style="color:#e6db74">'%'</span> <span style="color:#f92672">||</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">4</span> <span style="color:#f92672">||</span> <span style="color:#e6db74">'%'</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">OR</span> gc.description <span style="color:#66d9ef">ILIKE</span> <span style="color:#e6db74">'%'</span> <span style="color:#f92672">||</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">4</span> <span style="color:#f92672">||</span> <span style="color:#e6db74">'%'</span>)
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">CASE</span> <span style="color:#66d9ef">WHEN</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">5</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">'title'</span> <span style="color:#66d9ef">THEN</span> gc.title <span style="color:#66d9ef">END</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">CASE</span> <span style="color:#66d9ef">WHEN</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">5</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">'created_at'</span> <span style="color:#66d9ef">THEN</span> gc.created_at::text <span style="color:#66d9ef">END</span>,
|
||||
</span></span><span style="display:flex;"><span> gc.updated_at <span style="color:#66d9ef">DESC</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">LIMIT</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">6</span> <span style="color:#66d9ef">OFFSET</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">7</span>;
|
||||
</span></span></code></pre></div><hr>
|
||||
<h2 id="3-изменения-frontend-angular">3. Изменения Frontend (Angular)</h2>
|
||||
<h3 id="31-toolbar-фильтров">3.1. Toolbar фильтров</h3>
|
||||
<p>Добавить toolbar над списком диаграмм:</p>
|
||||
<pre tabindex="0"><code>┌──────────────────────────────────────────────────────────────────┐
|
||||
│ [🔍 Поиск... ] [▼ Пространство] │
|
||||
│ [▼ Создатель] [▼ Сортировка] [Сбросить фильтры] │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
</code></pre><p><strong>Компоненты:</strong></p>
|
||||
<ul>
|
||||
<li><strong>Строка поиска</strong> — input с debounce (300ms), поиск по назвению и описанию</li>
|
||||
<li><strong>Dropdown «Пространство»</strong> — список из <code>/gantt-charts/filters</code>, с цветовой меткой</li>
|
||||
<li><strong>Dropdown «Создатель»</strong> — список уникальных создателей</li>
|
||||
<li><strong>Dropdown «Сортировка»</strong> — по дате обновления, дате создания, названию</li>
|
||||
<li><strong>Кнопка «Сбросить»</strong> — очищает все фильтры</li>
|
||||
</ul>
|
||||
<p><strong>Состояние фильтров в URL</strong> (query params):</p>
|
||||
<pre tabindex="0"><code>/gantt?search=миграция&space_id=xxx&sort=updated_at
|
||||
</code></pre><p>Это позволяет делиться ссылкой с уже настроенными фильтрами.</p>
|
||||
<h3 id="32-карточка-диаграммы--бейдж-пространства">3.2. Карточка диаграммы — бейдж пространства</h3>
|
||||
<p>На каждой карточке добавить бейдж с названием пространства:</p>
|
||||
<pre tabindex="0"><code>┌─────────────────────────────────────┐
|
||||
│ 🟠 IT Ландшафт │ ← бейдж с цветом пространства
|
||||
│ Миграция backend │
|
||||
│ Перевод PHP→Go, этап 2 │
|
||||
│ Создал: Антон А. · 07.04.2026 │
|
||||
└─────────────────────────────────────┘
|
||||
</code></pre><h3 id="33-форма-создания-диаграммы--выбор-пространства">3.3. Форма создания диаграммы — выбор пространства</h3>
|
||||
<p>В модальном окне создания:</p>
|
||||
<ul>
|
||||
<li>Dropdown со списком пространств, где пользователь имеет роль <strong>editor</strong> или выше</li>
|
||||
<li>Валидация: если пользователь не editor ни в одном пространстве — показать сообщение «Нет доступных пространств для создания»</li>
|
||||
</ul>
|
||||
<p><strong>Запрос доступных пространств:</strong> переиспользовать существующий <code>GET /api/v1/spaces</code> с фильтрацией на клиенте по <code>role ∈ [owner, admin, editor]</code>, либо добавить параметр <code>?min_role=editor</code> на backend.</p>
|
||||
<h3 id="34-изменение-routing">3.4. Изменение routing</h3>
|
||||
<p><strong>Было:</strong></p>
|
||||
<pre tabindex="0"><code>/gantt → список гантов текущего пространства
|
||||
</code></pre><p><strong>Стало:</strong></p>
|
||||
<pre tabindex="0"><code>/gantt → список всех доступных гантов (из всех пространств)
|
||||
</code></pre><p>Убрать зависимость от <code>SpaceStorageService.getSelectedSpaceId()</code> в списке гантов.</p>
|
||||
<h3 id="35-frontend--файлы-для-изменения">3.5. Frontend — файлы для изменения</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Файл</th>
|
||||
<th>Изменение</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>pages/gantt/services/gantt.service.ts</code></td>
|
||||
<td>Новый метод <code>listAll(filters)</code>, <code>getFilters()</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pages/gantt/gantt.component.ts</code></td>
|
||||
<td>Подключить toolbar, парсинг URL-параметров</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pages/gantt/gantt.component.html</code></td>
|
||||
<td>Добавить toolbar, бейджи на карточках</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pages/gantt/components/gantt-card/</code></td>
|
||||
<td>Добавить бейдж пространства</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pages/gantt/components/create-chart-modal/</code></td>
|
||||
<td>Dropdown выбора пространства</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>core/models/</code></td>
|
||||
<td>Обновить модели: добавить <code>space</code> в GanttChart</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<h2 id="4-обратная-совместимость">4. Обратная совместимость</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Endpoint</th>
|
||||
<th>Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>GET /api/v1/spaces/{id}/gantt-charts</code></td>
|
||||
<td>Сохранить (для детального view внутри пространства)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>GET /api/v1/gantt-charts</code></td>
|
||||
<td><strong>Новый</strong> — глобальный список</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>GET /api/v1/gantt-charts/filters</code></td>
|
||||
<td><strong>Новый</strong> — данные для dropdown’ов</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>POST /api/v1/spaces/{id}/gantt-charts</code></td>
|
||||
<td>Сохранить (создание в контексте пространства)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Все CRUD для sections/tasks</td>
|
||||
<td>Без изменений</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<h2 id="5-критерии-приёмки">5. Критерии приёмки</h2>
|
||||
<ul>
|
||||
<li><input disabled="" type="checkbox"> Endpoint <code>GET /api/v1/gantt-charts</code> возвращает все доступные диаграммы</li>
|
||||
<li><input disabled="" type="checkbox"> Фильтр по <code>space_id</code> — показывает только диаграммы конкретного пространства</li>
|
||||
<li><input disabled="" type="checkbox"> Фильтр по <code>created_by</code> — показывает только диаграммы выбранного создателя</li>
|
||||
<li><input disabled="" type="checkbox"> Поиск <code>search</code> — ищет по title и description (case-insensitive)</li>
|
||||
<li><input disabled="" type="checkbox"> Dropdown фильтров заполняется реальными данными из <code>/filters</code></li>
|
||||
<li><input disabled="" type="checkbox"> Карточка диаграммы показывает бейдж пространства с цветом</li>
|
||||
<li><input disabled="" type="checkbox"> Форма создания предлагает только пространства с ролью ≥ editor</li>
|
||||
<li><input disabled="" type="checkbox"> Состояние фильтров сохраняется в URL (можно поделиться ссылкой)</li>
|
||||
<li><input disabled="" type="checkbox"> Старые endpoints (<code>/spaces/{id}/gantt-charts</code>) продолжают работать</li>
|
||||
<li><input disabled="" type="checkbox"> Swagger документация обновлена</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="6-порядок-реализации">6. Порядок реализации</h2>
|
||||
<ol>
|
||||
<li>Backend: DTO для новых запросов/ответов</li>
|
||||
<li>Backend: Repository — SQL-запрос с фильтрами + JOIN на spaces</li>
|
||||
<li>Backend: Service — <code>ListAll</code>, <code>GetFilters</code></li>
|
||||
<li>Backend: Handler — <code>ListAllGanttCharts</code>, <code>GetGanttFilters</code>, регистрация маршрутов</li>
|
||||
<li>Backend: Swagger — обновить документацию</li>
|
||||
<li>Frontend: Модели — обновить GanttChart, добавить Space в ответ</li>
|
||||
<li>Frontend: Service — <code>listAll()</code>, <code>getFilters()</code></li>
|
||||
<li>Frontend: Toolbar компонент — поиск + dropdown’ы</li>
|
||||
<li>Frontend: Карточка — бейдж пространства</li>
|
||||
<li>Frontend: Модальное окно создания — dropdown пространства</li>
|
||||
<li>Frontend: URL-параметры — синхронизация фильтров с router</li>
|
||||
<li>Тестирование</li>
|
||||
</ol>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,776 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Leadera: Вехи (Milestones) на диаграмме Ганта — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Фильтры</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Все инструкции</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h6 class="mt-3">Теги</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/audit/">audit</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/leadera/">leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/security/">security</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/architecture/">architecture</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/websocket/">websocket</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/realtime/">realtime</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/gantt/">gantt</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/api/">api</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/frontend/">frontend</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/requirements/">requirements</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/angular/">angular</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/go/">go</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ux/">ux</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ssh/">ssh</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/tunnel/">tunnel</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/vps/">vps</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/reverse-proxy/">reverse-proxy</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/nginx/">nginx</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/devops/">devops</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/openclaw/">openclaw</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/opencode/">opencode</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/docker/">docker</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Leadera: Вехи (Milestones) на диаграмме Ганта</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/guides/leadera-milestones.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/guides\/leadera-milestones.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
<a href="../../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../../tags/angular/" class="tag">angular</a>
|
||||
|
||||
<a href="../../tags/go/" class="tag">go</a>
|
||||
|
||||
<a href="../../tags/api/" class="tag">api</a>
|
||||
|
||||
<a href="../../tags/ux/" class="tag">ux</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h2 id="бизнес-требования">Бизнес-требования</h2>
|
||||
<h3 id="br-1-сущность-веха-milestone">BR-1: Сущность «Веха» (Milestone)</h3>
|
||||
<p>Веха — это маркер на таймлайне диаграммы Ганта, обозначающий важную дату в проекте. В отличие от задач, веха не имеет длительности и привязана к конкретной дате.</p>
|
||||
<p><strong>Поля вехи:</strong></p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Поле</th>
|
||||
<th>Тип</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>id</td>
|
||||
<td>UUID</td>
|
||||
<td>Идентификатор</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>gantt_chart_id</td>
|
||||
<td>UUID</td>
|
||||
<td>Принадлежность к диаграмме (не к секции!)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>title</td>
|
||||
<td>string</td>
|
||||
<td>Название вехи (обязательное, 1–255 символов)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>date</td>
|
||||
<td>date</td>
|
||||
<td>Дата вехи (без времени, обязательная)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>color</td>
|
||||
<td>string</td>
|
||||
<td>Цвет маркера (HEX, по умолчанию <code>#dc3545</code> — красный)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>created_by</td>
|
||||
<td>UUID</td>
|
||||
<td>Кто создал (не отображается в UI, хранится для аудита)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>updated_by</td>
|
||||
<td>UUID</td>
|
||||
<td>Кто обновил (не отображается в UI)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>created_at</td>
|
||||
<td>timestamp</td>
|
||||
<td>Дата создания</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>updated_at</td>
|
||||
<td>timestamp</td>
|
||||
<td>Дата обновления</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>deleted_at</td>
|
||||
<td>timestamp</td>
|
||||
<td>Soft delete</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Права доступа:</strong> Создание, редактирование и удаление вех доступно участникам с правами выше «только чтение» (аналогично задачам).</p>
|
||||
<p><strong>Особенности:</strong></p>
|
||||
<ul>
|
||||
<li>Вех может быть несколько в проекте</li>
|
||||
<li>Несколько вех могут иметь одну и ту же дату</li>
|
||||
<li>Вехи сортируются строго по дате (по возрастанию)</li>
|
||||
<li>Вехи не привязаны к секциям — они отображаются <strong>сквозь все секции</strong></li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3 id="br-2-управление-вехами-модальное-окно">BR-2: Управление вехами (модальное окно)</h3>
|
||||
<p><strong>Вызов:</strong> Кнопка «Вехи» рядом с кнопкой «Создать секцию» на странице диаграммы.</p>
|
||||
<p><strong>Модальное окно содержит:</strong></p>
|
||||
<ul>
|
||||
<li>Заголовок: «Вехи проекта»</li>
|
||||
<li>Список всех вех текущей диаграммы (сортировка по дате по возрастанию)</li>
|
||||
<li>Каждая веха в списке показывает: цвет (индикатор), название, дату, кнопки «Редактировать» и «Удалить»</li>
|
||||
<li>Кнопка «Добавить веху»</li>
|
||||
</ul>
|
||||
<p><strong>Форма добавления/редактирования вехи:</strong></p>
|
||||
<ul>
|
||||
<li>Название — текстовый ввод (обязательное, 1–255 символов)</li>
|
||||
<li>Дата — datepicker (обязательное)</li>
|
||||
<li>Цвет — выбор цвета (по аналогии с палитрой цветов в задачах)</li>
|
||||
<li>По умолчанию цвет = красный (<code>#dc3545</code>)</li>
|
||||
</ul>
|
||||
<p><strong>Удаление:</strong> с подтверждением (confirm dialog).</p>
|
||||
<hr>
|
||||
<h3 id="br-3-отображение-вех-на-таймлайне">BR-3: Отображение вех на таймлайне</h3>
|
||||
<p><strong>Визуальное представление:</strong></p>
|
||||
<ul>
|
||||
<li><strong>Маркер-флажок</strong> (flag marker) на шкале таймлайна</li>
|
||||
<li>От маркера вниз идёт <strong>пунктирная вертикальная линия</strong> через все секции (на всю высоту диаграммы)</li>
|
||||
<li>Цвет линии и маркера = цвет вехи</li>
|
||||
</ul>
|
||||
<p><strong>Позиционирование:</strong></p>
|
||||
<ul>
|
||||
<li>Веха ставится ровно на свою дату на шкале времени</li>
|
||||
<li>Если дата вехи выходит за рамки текущего диапазона задач — таймлайн <strong>расширяется</strong> (аналогично задачам)</li>
|
||||
<li>При изменении масштаба (days/weeks/months/quarters) позиция вехи пересчитывается</li>
|
||||
</ul>
|
||||
<p><strong>Tooltip при наведении:</strong></p>
|
||||
<ul>
|
||||
<li>При наведении на маркер/линию — показывается tooltip с названием вехи</li>
|
||||
<li>Если на одну дату приходится несколько вех — в tooltip показываются <strong>все</strong> вехи этой даты с их названиями (каждая с индикацией своего цвета)</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="технические-требования">Технические требования</h2>
|
||||
<h3 id="база-данных">База данных</h3>
|
||||
<h4 id="новая-таблица-gantt_milestones">Новая таблица: gantt_milestones</h4>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> gantt_milestones (
|
||||
</span></span><span style="display:flex;"><span> id UUID <span style="color:#66d9ef">PRIMARY</span> <span style="color:#66d9ef">KEY</span> <span style="color:#66d9ef">DEFAULT</span> gen_random_uuid(),
|
||||
</span></span><span style="display:flex;"><span> gantt_chart_id UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> gantt_charts(id) <span style="color:#66d9ef">ON</span> <span style="color:#66d9ef">DELETE</span> <span style="color:#66d9ef">CASCADE</span>,
|
||||
</span></span><span style="display:flex;"><span> title VARCHAR(<span style="color:#ae81ff">255</span>) <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||||
</span></span><span style="display:flex;"><span> date DATE <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||||
</span></span><span style="display:flex;"><span> color VARCHAR(<span style="color:#ae81ff">20</span>) <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">DEFAULT</span> <span style="color:#e6db74">'#dc3545'</span>,
|
||||
</span></span><span style="display:flex;"><span> created_by UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> users(id),
|
||||
</span></span><span style="display:flex;"><span> updated_by UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> users(id),
|
||||
</span></span><span style="display:flex;"><span> created_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span> <span style="color:#66d9ef">DEFAULT</span> NOW(),
|
||||
</span></span><span style="display:flex;"><span> updated_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span> <span style="color:#66d9ef">DEFAULT</span> NOW(),
|
||||
</span></span><span style="display:flex;"><span> deleted_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span>
|
||||
</span></span><span style="display:flex;"><span>);
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_gantt_milestones_chart <span style="color:#66d9ef">ON</span> gantt_milestones(gantt_chart_id);
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_gantt_milestones_date <span style="color:#66d9ef">ON</span> gantt_milestones(date);
|
||||
</span></span></code></pre></div><hr>
|
||||
<h3 id="backend-go-api">Backend (Go API)</h3>
|
||||
<h4 id="этап-1-миграция--домен">Этап 1: Миграция + Домен</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Добавь в проект новую таблицу <code>gantt_milestones</code>.</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Создай миграцию в <code>cmd/migrate/</code>:</p>
|
||||
<ul>
|
||||
<li>Новая таблица <code>gantt_milestones</code> с полями: 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.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Создай доменную модель <code>GanttMilestone</code> в <code>internal/domain/gantt.go</code>:</p>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">GanttMilestone</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ID</span> <span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"id"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">GanttChartID</span> <span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"gantt_chart_id"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Title</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"title"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Date</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"date"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Color</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"color"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CreatedBy</span> <span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"created_by"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UpdatedBy</span> <span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"updated_by"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CreatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"created_at"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UpdatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"updated_at"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">DeletedAt</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"deleted_at,omitempty"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div></blockquote>
|
||||
<h4 id="этап-2-crud-для-ganttmilestones">Этап 2: CRUD для GanttMilestones</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Реализуй полный CRUD для сущности <code>GanttMilestone</code> по аналогии с существующими handlers/services/repositories в проекте.</p>
|
||||
<p><strong>Роуты:</strong></p>
|
||||
<ul>
|
||||
<li><code>GET /api/gantt-charts/:chartId/milestones</code> — список вех диаграммы</li>
|
||||
<li><code>POST /api/gantt-charts/:chartId/milestones</code> — создать веху</li>
|
||||
<li><code>PUT /api/gantt-charts/:chartId/milestones/:milestoneId</code> — обновить веху</li>
|
||||
<li><code>DELETE /api/gantt-charts/:chartId/milestones/:milestoneId</code> — удалить веху (soft delete)</li>
|
||||
</ul>
|
||||
<p><strong>Repository:</strong> <code>internal/repository/gantt_milestone_repository.go</code></p>
|
||||
<ul>
|
||||
<li><code>GetByChartID(ctx, chartID) ([]domain.GanttMilestone, error)</code> — все неудалённые вехи диаграммы, отсортированные по date ASC</li>
|
||||
<li><code>GetByID(ctx, id) (*domain.GanttMilestone, error)</code></li>
|
||||
<li><code>Create(ctx, milestone) error</code></li>
|
||||
<li><code>Update(ctx, milestone) error</code></li>
|
||||
<li><code>Delete(ctx, id) error</code> — soft delete (set deleted_at)</li>
|
||||
<li>Все запросы — raw SQL через pgx, как в остальных репозиториях проекта</li>
|
||||
</ul>
|
||||
<p><strong>Service:</strong> <code>internal/service/gantt_milestone_service.go</code></p>
|
||||
<ul>
|
||||
<li>Валидация: title (1–255 символов), date (обязательное), color (валидный HEX, default ‘#dc3545’)</li>
|
||||
<li>Проверка прав доступа: пользователь должен иметь права выше «только чтение» в пространстве (space) диаграммы</li>
|
||||
<li>Проверка что chartId существует и не архивирован</li>
|
||||
</ul>
|
||||
<p><strong>Handler:</strong> <code>internal/handler/gantt_milestone_handler.go</code></p>
|
||||
<ul>
|
||||
<li>Swagger-аннотации для всех эндпоинтов</li>
|
||||
<li>Валидация входных данных</li>
|
||||
<li>Ошибки в формате проекта</li>
|
||||
</ul>
|
||||
<p><strong>DTO:</strong> <code>internal/dto/gantt/milestones_dto.go</code></p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">CreateGanttMilestoneRequest</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Title</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"title" binding:"required,min=1,max=255"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Date</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"date" binding:"required"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Color</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"color" binding:"omitempty,max=20"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">UpdateGanttMilestoneRequest</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Title</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"title" binding:"omitempty,min=1,max=255"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Date</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"date" binding:"omitempty"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Color</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"color" binding:"omitempty,max=20"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">GanttMilestoneResponse</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ID</span> <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"id"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">GanttChartID</span> <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"gantt_chart_id"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Title</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"title"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Date</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"date"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Color</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"color"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CreatedByUser</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">CreatedByUpdatedUser</span> <span style="color:#e6db74">`json:"created_by_user"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UpdatedByUser</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">CreatedByUpdatedUser</span> <span style="color:#e6db74">`json:"updated_by_user"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CreatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"created_at"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UpdatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"updated_at"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">GanttMilestonesListResponse</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Milestones</span> []<span style="color:#a6e22e">GanttMilestoneResponse</span> <span style="color:#e6db74">`json:"milestones"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><p><strong>Регистрация роутов:</strong> Добавь группу milestone-роутов в роутер рядом с существующими роутами секций/задач. Проверка прав через существующий middleware.</p>
|
||||
</blockquote>
|
||||
<h4 id="этап-3-включение-вех-в-ответ-диаграммы">Этап 3: Включение вех в ответ диаграммы</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>При запросе данных диаграммы Ганта (полная загрузка) включай связанные вехи в ответ.</p>
|
||||
<ol>
|
||||
<li>Добавь эндпоинт или расширь существующий <code>GET /api/gantt-charts/:chartId</code> — включить поле <code>milestones</code> с массивом <code>GanttMilestoneResponse</code></li>
|
||||
<li>Вехи с <code>deleted_at IS NOT NULL</code> не возвращаются</li>
|
||||
<li>Вехи сортируются по <code>date</code> ASC</li>
|
||||
<li>Вехи загружаются вместе с данными диаграммы (tasks, sections) — один запрос при открытии Ганта</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<hr>
|
||||
<h3 id="frontend-angular">Frontend (Angular)</h3>
|
||||
<h4 id="этап-fe-1-модели--сервис">Этап FE-1: Модели + Сервис</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Добавь в проект поддержку вех (milestones) на диаграмме Ганта.</p>
|
||||
<ol>
|
||||
<li>В <code>pages/gantt/models/</code> (или соответствующем файле моделей) добавь:</li>
|
||||
</ol>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttMilestone</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">gantt_chart_id</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">color</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">created_by_user</span><span style="color:#f92672">?:</span> { <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>; <span style="color:#a6e22e">full_name</span>: <span style="color:#66d9ef">string</span> };
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">updated_by_user</span><span style="color:#f92672">?:</span> { <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>; <span style="color:#a6e22e">full_name</span>: <span style="color:#66d9ef">string</span> };
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">created_at</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">updated_at</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttMilestoneCreateRequest</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">color?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttMilestoneUpdateRequest</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">date?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">color?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><ol start="2">
|
||||
<li>
|
||||
<p>Добавь <code>milestones: GanttMilestone[]</code> в интерфейс/модель полной загрузки данных диаграммы</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Создай сервис <code>pages/gantt/services/gantt-milestone.service.ts</code>:</p>
|
||||
</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>Inject HttpClient</li>
|
||||
<li>Методы:
|
||||
<ul>
|
||||
<li><code>getMilestones(chartId: string): Observable<GanttMilestone[]></code></li>
|
||||
<li><code>createMilestone(chartId: string, data: GanttMilestoneCreateRequest): Observable<GanttMilestone></code></li>
|
||||
<li><code>updateMilestone(chartId: string, milestoneId: string, data: GanttMilestoneUpdateRequest): Observable<GanttMilestone></code></li>
|
||||
<li><code>deleteMilestone(chartId: string, milestoneId: string): Observable<void></code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Все методы возвращают Observable с типизированными ответами (по аналогии с GanttChartService)</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-2-модальное-окно-управления-вехами">Этап FE-2: Модальное окно управления вехами</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Создай компонент модального окна для управления вехами диаграммы Ганта.</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Создай компонент <code>pages/gantt/components/milestones-modal/</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Кнопка вызова:</strong> Добавь кнопку «Вехи» рядом с кнопкой «Создать секцию» в toolbar диаграммы. По клику — открытие модального окна через ng-bootstrap NgbModal.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Модальное окно</strong> (ng-bootstrap modal):</p>
|
||||
<ul>
|
||||
<li>Заголовок: «Вехи проекта»</li>
|
||||
<li>Список вех (сортировка по date ASC):
|
||||
<ul>
|
||||
<li>Каждый элемент: цветной кружок (color), название, дата (форматированная), кнопки ✏️ (редактировать) и 🗑️ (удалить с confirm)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Кнопка «Добавить веху» внизу списка</li>
|
||||
<li>Если вех нет — сообщение «Вех пока нет. Добавьте первую!»</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Форма добавления/редактирования</strong> (inline в модальном окне):</p>
|
||||
<ul>
|
||||
<li>Название — текстовый ввод (обязательное)</li>
|
||||
<li>Дата — ng-bootstrap datepicker (обязательное)</li>
|
||||
<li>Цвет — палитра цветов (использовать тот же компонент/подход, что и для задач)</li>
|
||||
<li>По умолчанию цвет = <code>#dc3545</code> (красный)</li>
|
||||
<li>Кнопки «Сохранить» и «Отмена»</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>При сохранении — вызов соответствующего метода сервиса (create/update)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>При удалении — confirm dialog → вызов deleteMilestone</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>После любого изменения — обновить список вех и обновить данные на канвасе (через сервис/GanttStore)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Инжектировать GanttMilestoneService и NgbModal</p>
|
||||
</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-3-отображение-вех-на-таймлайне-konva-canvas">Этап FE-3: Отображение вех на таймлайне (Konva canvas)</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Отобрази вехи (milestones) на canvas-диаграмме Ганта.</p>
|
||||
<p>Проект использует Konva.js для рендеринга.</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>При загрузке данных диаграммы — извлечь массив <code>milestones</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Для каждой вехи нарисовать на canvas:</p>
|
||||
<ul>
|
||||
<li><strong>Маркер-флажок</strong> (flag marker) на позиции даты вехи на шкале времени
|
||||
<ul>
|
||||
<li>Стиль: маленький треугольник или флаг на верхней границе таймлайна</li>
|
||||
<li>Цвет = milestone.color</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Пунктирная вертикальная линия</strong> от маркера вниз через все секции (на всю высоту области секций)
|
||||
<ul>
|
||||
<li>Стиль: Konva.Line, dash: [5, 5], strokeWidth: 1.5, color = milestone.color, opacity: 0.7</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Позиционирование:</strong></p>
|
||||
<ul>
|
||||
<li>Рассчитать X-позицию по дате вехи с использованием текущей шкалы времени (как для задач)</li>
|
||||
<li>Если дата вехи выходит за текущий диапазон — расширить диапазон (влияет на общую ширину таймлайна)</li>
|
||||
<li>При изменении масштаба (days/weeks/months/quarters) — пересчитать позиции всех вех</li>
|
||||
<li>При скролле — вехи скроллятся вместе с задачами</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Tooltip при наведении:</strong></p>
|
||||
<ul>
|
||||
<li>При наведении на маркер или пунктирную линию — показать Konva.Tooltip или HTML tooltip:
|
||||
<ul>
|
||||
<li>Если веха одна на эту дату — показать название</li>
|
||||
<li>Если несколько вех на одну дату — показать список: цветной кружок + название для каждой</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Z-index:</strong></p>
|
||||
<ul>
|
||||
<li>Линии вех рисуются <strong>поверх</strong> фона секций, но <strong>под</strong> задачами</li>
|
||||
<li>Маркеры-флажки — поверх всего (самый высокий z-index)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Реактивность:</strong></p>
|
||||
<ul>
|
||||
<li>При добавлении/удалении/редактировании вехи — перерисовать только вехи (без полного ре рендера задач)</li>
|
||||
<li>Подписаться на изменения массива milestones через сервис/store</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<hr>
|
||||
<h2 id="порядок-реализации">Порядок реализации</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Этап</th>
|
||||
<th>Сторона</th>
|
||||
<th>Зависимости</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>Миграция + Домен</td>
|
||||
<td>Backend</td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>CRUD вех</td>
|
||||
<td>Backend</td>
|
||||
<td>Этап 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>Включение вех в ответ диаграммы</td>
|
||||
<td>Backend</td>
|
||||
<td>Этап 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>Модели + Сервис</td>
|
||||
<td>Frontend</td>
|
||||
<td>— (параллельно с backend)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>Модальное окно управления вехами</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этапы 4 + backend 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td>Отображение вех на таймлайне</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этапы 4 + backend 3</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,839 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Leadera: Ограничения секций в диаграмме Ганта — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Фильтры</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Все инструкции</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h6 class="mt-3">Теги</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/audit/">audit</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/leadera/">leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/security/">security</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/architecture/">architecture</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/websocket/">websocket</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/realtime/">realtime</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/gantt/">gantt</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/api/">api</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/frontend/">frontend</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/requirements/">requirements</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/angular/">angular</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/go/">go</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ux/">ux</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ssh/">ssh</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/tunnel/">tunnel</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/vps/">vps</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/reverse-proxy/">reverse-proxy</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/nginx/">nginx</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/devops/">devops</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/openclaw/">openclaw</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/opencode/">opencode</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/docker/">docker</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Leadera: Ограничения секций в диаграмме Ганта</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/guides/leadera-section-constraints.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/guides\/leadera-section-constraints.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
<a href="../../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../../tags/angular/" class="tag">angular</a>
|
||||
|
||||
<a href="../../tags/go/" class="tag">go</a>
|
||||
|
||||
<a href="../../tags/api/" class="tag">api</a>
|
||||
|
||||
<a href="../../tags/ux/" class="tag">ux</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h2 id="бизнес-требования">Бизнес-требования</h2>
|
||||
<h3 id="br-1-лимит-одновременных-задач-concurrency-limit">BR-1: Лимит одновременных задач (Concurrency Limit)</h3>
|
||||
<p>У каждой секции появляется параметр <strong>max_concurrent_tasks</strong> (целое число ≥ 0, по умолчанию 0).</p>
|
||||
<p><strong>Поведение:</strong></p>
|
||||
<ul>
|
||||
<li>Если <code>max_concurrent_tasks = 3</code> — в любой данный день в этой секции не может быть больше 3 активных задач одновременно</li>
|
||||
<li>Если <code>max_concurrent_tasks = 0</code> — ограничение отключено, лимита нет</li>
|
||||
<li>Пересечение считается по дате: задача занимает дни с <code>start_date</code> по <code>end_date</code> включительно. Две задачи «пересекаются», если у них есть хотя бы один общий день</li>
|
||||
<li>Ограничение работает <strong>только в рамках одной секции</strong>. Задачи из разных секций не влияют друг на друга</li>
|
||||
</ul>
|
||||
<p><strong>Срабатывание при:</strong></p>
|
||||
<ol>
|
||||
<li>Создании задачи — если добавление нарушает лимит → Alert</li>
|
||||
<li>Перетаскивании задачи (drag) — если новая позиция нарушает лимит → Alert</li>
|
||||
<li>Растягивании задачи (resize) — если новый размер нарушает лимит → Alert</li>
|
||||
<li>Редактировании задачи (форма) — если изменённые даты нарушают лимит → Alert</li>
|
||||
</ol>
|
||||
<p><strong>Важно:</strong> Действие <strong>НЕ отменяется</strong>. Задача создаётся/перемещается в любом случае, но пользователь видит предупреждение.</p>
|
||||
<p><strong>Текст Alert:</strong> «В секции “[название секции]” на [дата/диапазон] превышен лимит одновременных задач (максимум: N)»</p>
|
||||
<hr>
|
||||
<h3 id="br-2-блоки-секций-section-blocks">BR-2: Блоки секций (Section Blocks)</h3>
|
||||
<p>У каждой секции появляется набор <strong>блоков</strong> — особый вид записей, отображаемых на таймлайне как заштрихованные области.</p>
|
||||
<p><strong>Сущность GanttSectionBlock:</strong></p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Поле</th>
|
||||
<th>Тип</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>id</td>
|
||||
<td>UUID</td>
|
||||
<td>Идентификатор</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>section_id</td>
|
||||
<td>UUID</td>
|
||||
<td>Принадлежность к секции</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>title</td>
|
||||
<td>string</td>
|
||||
<td>Название (например: «Отпуск», «Командировка»)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>start_date</td>
|
||||
<td>date</td>
|
||||
<td>Начало интервала (минимум 1 день)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>end_date</td>
|
||||
<td>date</td>
|
||||
<td>Конец интервала</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>block_concurrency</td>
|
||||
<td>boolean</td>
|
||||
<td>Запрещать ли пересечение обычных задач с данным блоком</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Валидация:</strong></p>
|
||||
<ul>
|
||||
<li><code>end_date</code> не может быть раньше <code>start_date</code></li>
|
||||
<li>Все поля обязательны</li>
|
||||
<li>Блок может быть в прошлом</li>
|
||||
</ul>
|
||||
<p><strong>Визуальное отображение на Ганте:</strong></p>
|
||||
<ul>
|
||||
<li>Заштрихованная область на <strong>всю высоту секции</strong> (включая все строки задач в секции)</li>
|
||||
<li>Название блока отображается <strong>поверх штриховки с подложкой</strong> (для читаемости)</li>
|
||||
</ul>
|
||||
<p><strong>Поведение block_concurrency:</strong></p>
|
||||
<ul>
|
||||
<li>Если <code>block_concurrency = true</code> — при пересечении задачи с данным блоком показывается Alert (аналогично BR-1), но задача <strong>всё равно создаётся/перемещается</strong></li>
|
||||
<li>Если <code>block_concurrency = false</code> — блок отображается визуально, но не влияет на задачи</li>
|
||||
</ul>
|
||||
<p><strong>Срабатывание при:</strong></p>
|
||||
<ol>
|
||||
<li>Создании задачи — пересекает блок с <code>block_concurrency = true</code> → Alert</li>
|
||||
<li>Перетаскивании задачи (drag) → Alert</li>
|
||||
<li>Растягивании задачи (resize) → Alert</li>
|
||||
<li>Редактировании задачи (форма) → Alert</li>
|
||||
</ol>
|
||||
<p><strong>Текст Alert:</strong> «В секции “[название секции]” задача пересекает блок “[название блока]” ([дата] — [дата])»</p>
|
||||
<hr>
|
||||
<h3 id="br-3-система-alert-ов">BR-3: Система Alert-ов</h3>
|
||||
<p>Все предупреждения (от BR-1 и BR-2) выводятся в <strong>единую область</strong> над диаграммой Ганта.</p>
|
||||
<p><strong>Поведение:</strong></p>
|
||||
<ul>
|
||||
<li>Alert-ы — Bootstrap Alert (warning), <strong>постоянные</strong></li>
|
||||
<li>Пользователь <strong>НЕ может</strong> закрыть Alert вручную</li>
|
||||
<li>Alert исчезает <strong>автоматически</strong>, когда текущая дата прошла дату конфликта (конфликт больше не актуален)</li>
|
||||
<li>Проверка конфликтов — <strong>только на фронтенде</strong> (на основе загруженных данных: задачи секции, блоки, max_concurrent_tasks)</li>
|
||||
<li>Если Alert-ов много — область <strong>сворачиваемая</strong> с счётчиком в бейдже (например: «⚠️ 3 предупреждения»)</li>
|
||||
<li>По умолчанию — развёрнуто</li>
|
||||
<li>Сворачивание/разворачивание — клик по заголовку области</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3 id="br-4-настройки-секции">BR-4: Настройки секции</h3>
|
||||
<ul>
|
||||
<li>Настройки совмещены с <strong>существующим модальным окном редактирования секции</strong> (не отдельное окно)</li>
|
||||
</ul>
|
||||
<p><strong>Окно создания секции</strong> — добавляется:</p>
|
||||
<ul>
|
||||
<li>Поле «Макс. одновременных задач» (числовой ввод, 0 = без лимита, по умолчанию 0)</li>
|
||||
</ul>
|
||||
<p><strong>Окно редактирования секции</strong> — добавляется:</p>
|
||||
<ul>
|
||||
<li>Поле «Макс. одновременных задач» (числовой ввод, 0 = без лимита)</li>
|
||||
<li>Список блоков (таблица/список с CRUD: добавить, редактировать, удалить)
|
||||
<ul>
|
||||
<li><strong>Сортировка:</strong> по <code>start_date</code> по убыванию (будущее сверху)</li>
|
||||
<li>Каждый блок — форма: title, start_date, end_date, block_concurrency (чекбокс: «Запретить пересечение задач»)</li>
|
||||
<li>Валидация: end_date >= start_date, все поля заполнены</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3 id="br-5-наследование">BR-5: Наследование</h3>
|
||||
<ul>
|
||||
<li>При появлении функционала копирования/дублирования диаграммы — настройки секций (лимит + блоки) наследуются</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="технические-требования">Технические требования</h2>
|
||||
<h3 id="база-данных">База данных</h3>
|
||||
<h4 id="новая-таблица-gantt_section_blocks">Новая таблица: gantt_section_blocks</h4>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> gantt_section_blocks (
|
||||
</span></span><span style="display:flex;"><span> id UUID <span style="color:#66d9ef">PRIMARY</span> <span style="color:#66d9ef">KEY</span> <span style="color:#66d9ef">DEFAULT</span> gen_random_uuid(),
|
||||
</span></span><span style="display:flex;"><span> section_id UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> gantt_sections(id) <span style="color:#66d9ef">ON</span> <span style="color:#66d9ef">DELETE</span> <span style="color:#66d9ef">CASCADE</span>,
|
||||
</span></span><span style="display:flex;"><span> title VARCHAR(<span style="color:#ae81ff">255</span>) <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||||
</span></span><span style="display:flex;"><span> start_date DATE <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||||
</span></span><span style="display:flex;"><span> end_date DATE <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||||
</span></span><span style="display:flex;"><span> block_concurrency BOOLEAN <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">DEFAULT</span> <span style="color:#66d9ef">false</span>,
|
||||
</span></span><span style="display:flex;"><span> created_by UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> users(id),
|
||||
</span></span><span style="display:flex;"><span> updated_by UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> users(id),
|
||||
</span></span><span style="display:flex;"><span> created_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span> <span style="color:#66d9ef">DEFAULT</span> NOW(),
|
||||
</span></span><span style="display:flex;"><span> updated_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span> <span style="color:#66d9ef">DEFAULT</span> NOW(),
|
||||
</span></span><span style="display:flex;"><span> deleted_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">CONSTRAINT</span> chk_block_dates <span style="color:#66d9ef">CHECK</span> (end_date <span style="color:#f92672">>=</span> start_date)
|
||||
</span></span><span style="display:flex;"><span>);
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_gantt_section_blocks_section <span style="color:#66d9ef">ON</span> gantt_section_blocks(section_id);
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_gantt_section_blocks_dates <span style="color:#66d9ef">ON</span> gantt_section_blocks(start_date, end_date);
|
||||
</span></span></code></pre></div><h4 id="изменение-таблицы-gantt_sections">Изменение таблицы: gantt_sections</h4>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> gantt_sections <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">COLUMN</span> max_concurrent_tasks INTEGER <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">DEFAULT</span> <span style="color:#ae81ff">0</span>;
|
||||
</span></span></code></pre></div><hr>
|
||||
<h3 id="backend-go-api">Backend (Go API)</h3>
|
||||
<h4 id="этап-1-миграция--домен">Этап 1: Миграция + Домен</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Добавь в проект новую таблицу <code>gantt_section_blocks</code> и колонку <code>max_concurrent_tasks</code> в <code>gantt_sections</code>.</p>
|
||||
<ol>
|
||||
<li>Создай миграцию в <code>cmd/migrate/</code>:
|
||||
<ul>
|
||||
<li>Новая таблица <code>gantt_section_blocks</code> с полями: 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).</li>
|
||||
<li>ALTER TABLE gantt_sections ADD COLUMN max_concurrent_tasks INTEGER NOT NULL DEFAULT 0.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Создай доменную модель <code>GanttSectionBlock</code> в <code>internal/domain/gantt.go</code> со всеми полями таблицы.</li>
|
||||
<li>Добавь поле <code>MaxConcurrentTasks int</code> в структуру <code>GanttSection</code>.</li>
|
||||
<li>Обнови маппинг в relevant repository файлах, чтобы новое поле <code>max_concurrent_tasks</code> читалось/писалось.</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-2-crud-для-ganttsectionblocks">Этап 2: CRUD для GanttSectionBlocks</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Реализуй полный CRUD для сущности <code>GanttSectionBlock</code> по аналогии с существующими handlers/services/repositories в проекте.</p>
|
||||
<p><strong>Роуты (внутри группы секции):</strong></p>
|
||||
<ul>
|
||||
<li><code>GET /api/gantt-charts/:chartId/sections/:sectionId/blocks</code> — список блоков секции</li>
|
||||
<li><code>POST /api/gantt-charts/:chartId/sections/:sectionId/blocks</code> — создать блок</li>
|
||||
<li><code>PUT /api/gantt-charts/:chartId/sections/:sectionId/blocks/:blockId</code> — обновить блок</li>
|
||||
<li><code>DELETE /api/gantt-charts/:chartId/sections/:sectionId/blocks/:blockId</code> — удалить блок (soft delete)</li>
|
||||
</ul>
|
||||
<p><strong>Repository:</strong> <code>internal/repository/gantt_section_block_repository.go</code></p>
|
||||
<ul>
|
||||
<li><code>GetBySectionID(ctx, sectionID) ([]domain.GanttSectionBlock, error)</code></li>
|
||||
<li><code>GetByID(ctx, id) (*domain.GanttSectionBlock, error)</code></li>
|
||||
<li><code>Create(ctx, block) error</code></li>
|
||||
<li><code>Update(ctx, block) error</code></li>
|
||||
<li><code>Delete(ctx, id) error</code> — soft delete (set deleted_at)</li>
|
||||
<li>Все запросы — raw SQL через pgx, как в остальных репозиториях проекта</li>
|
||||
</ul>
|
||||
<p><strong>Service:</strong> <code>internal/service/gantt_section_block_service.go</code></p>
|
||||
<ul>
|
||||
<li>Бизнес-логика: валидация dates (end >= start), проверка прав доступа к секции</li>
|
||||
<li>Inject repository</li>
|
||||
</ul>
|
||||
<p><strong>Handler:</strong> <code>internal/handler/gantt_section_block_handler.go</code></p>
|
||||
<ul>
|
||||
<li>Swagger-аннотации для всех эндпоинтов</li>
|
||||
<li>Валидация входных данных</li>
|
||||
<li>Ошибки в формате проекта (GanttErrorResponse)</li>
|
||||
</ul>
|
||||
<p><strong>Модели запросов:</strong></p>
|
||||
<ul>
|
||||
<li>Create: title (required), start_date (required), end_date (required), block_concurrency (bool, default false)</li>
|
||||
<li>Update: title, start_date, end_date, block_concurrency — все опциональны</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
<h4 id="этап-3-обновление-настроек-секции">Этап 3: Обновление настроек секции</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Обнови CRUD секций (gantt_sections) для поддержки нового поля <code>max_concurrent_tasks</code>.</p>
|
||||
<ol>
|
||||
<li>Добавь <code>max_concurrent_tasks</code> в <code>GanttSectionCreateRequest</code> (опциональное, int ≥ 0, default 0)</li>
|
||||
<li>Добавь <code>max_concurrent_tasks</code> в <code>GanttSectionUpdateRequest</code> (опциональное, int ≥ 0)</li>
|
||||
<li>Обнови SQL-запросы в repository: INSERT и UPDATE должны включать <code>max_concurrent_tasks</code></li>
|
||||
<li>При GET секции (или списке секций) — возвращать <code>max_concurrent_tasks</code> в ответе</li>
|
||||
<li>Валидация: значение должно быть ≥ 0</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-4-включение-блоков-в-ответ-секций">Этап 4: Включение блоков в ответ секций</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>При запросе списка секций или одной секции включай связанные блоки в ответ.</p>
|
||||
<ol>
|
||||
<li>Обнови <code>GET /api/gantt-charts/:chartId/sections/:sectionId</code> — добавить поле <code>blocks</code> с массивом GanttSectionBlock</li>
|
||||
<li>Обнови <code>GET /api/gantt-charts/:chartId/sections</code> — добавить поле <code>blocks</code> для каждой секции</li>
|
||||
<li>Блоки с <code>deleted_at IS NOT NULL</code> не возвращаются</li>
|
||||
<li>Блоки сортируются по start_date по убыванию (будущее сверху)</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<hr>
|
||||
<h3 id="frontend-angular">Frontend (Angular)</h3>
|
||||
<h4 id="этап-fe-1-модели--сервис">Этап FE-1: Модели + Сервис</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Добавь в проект поддержку ограничений секций Ганта.</p>
|
||||
<ol>
|
||||
<li>В <code>pages/gantt/models/gantt-chart.model.ts</code> добавь:</li>
|
||||
</ol>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttSectionBlock</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">section_id</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">start_date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">end_date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">block_concurrency</span>: <span style="color:#66d9ef">boolean</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">created_by_user</span>: <span style="color:#66d9ef">UserInfo</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">updated_by_user</span>: <span style="color:#66d9ef">UserInfo</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">created_at</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">updated_at</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttSectionBlockCreateRequest</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">start_date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">end_date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">block_concurrency?</span>: <span style="color:#66d9ef">boolean</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttSectionBlockUpdateRequest</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">start_date?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">end_date?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">block_concurrency?</span>: <span style="color:#66d9ef">boolean</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ConflictCheck</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">has_conflicts</span>: <span style="color:#66d9ef">boolean</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">conflicts</span>: <span style="color:#66d9ef">ConflictItem</span>[];
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ConflictItem</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">type</span><span style="color:#f92672">:</span> <span style="color:#e6db74">'concurrency'</span> <span style="color:#f92672">|</span> <span style="color:#e6db74">'block'</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">section_id</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">section_title</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">block_title?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">start_date?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">end_date?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">message</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">dates?</span>: <span style="color:#66d9ef">string</span>[];
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><ol start="2">
|
||||
<li>
|
||||
<p>Добавь <code>max_concurrent_tasks: number</code> в интерфейс <code>GanttSection</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Добавь <code>blocks: GanttSectionBlock[]</code> в интерфейс <code>GanttSection</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Добавь <code>max_concurrent_tasks?: number</code> в <code>GanttSectionCreateRequest</code> и <code>GanttSectionUpdateRequest</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Создай сервис <code>pages/gantt/services/gantt-section-block.service.ts</code>:</p>
|
||||
</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>Inject HttpClient</li>
|
||||
<li>Методы: getBlocks(chartId, sectionId), createBlock(chartId, sectionId, data), updateBlock(chartId, sectionId, blockId, data), deleteBlock(chartId, sectionId, blockId)</li>
|
||||
<li>Все методы возвращают Observable с типизированными ответами (по аналогии с GanttChartService)</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-2-интеграция-в-модальные-окна-секции">Этап FE-2: Интеграция в модальные окна секции</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Интегрируй настройки ограничений в существующие модальные окна создания и редактирования секции Ганта.</p>
|
||||
<ol>
|
||||
<li>Найди существующие компоненты модальных окон секции (создание/редактирование).</li>
|
||||
</ol>
|
||||
<p><strong>Окно создания секции</strong> — добавить:</p>
|
||||
<ul>
|
||||
<li>Числовой ввод: «Максимум одновременных задач»</li>
|
||||
<li>Подсказка: «0 = без ограничений»</li>
|
||||
<li>Валидация: число ≥ 0</li>
|
||||
<li>Значение по умолчанию: 0</li>
|
||||
<li>Отправляется в <code>GanttSectionCreateRequest</code> как <code>max_concurrent_tasks</code></li>
|
||||
</ul>
|
||||
<p><strong>Окно редактирования секции</strong> — добавить:</p>
|
||||
<ul>
|
||||
<li>Числовой ввод: «Максимум одновременных задач» (аналогично созданию)</li>
|
||||
<li>Секция «Блоки (запретные интервалы)»:
|
||||
<ul>
|
||||
<li>Таблица/список существующих блоков с колонками: Название, Начало, Конец, Блокировка (чекбокс/иконка), Действия (редактировать/удалить)</li>
|
||||
<li><strong>Сортировка:</strong> по <code>start_date</code> по убыванию (будущее сверху)</li>
|
||||
<li>Кнопка «Добавить блок» — inline-форма:
|
||||
<ul>
|
||||
<li>title (текстовый ввод, обязательный)</li>
|
||||
<li>start_date (datepicker, обязательный)</li>
|
||||
<li>end_date (datepicker, обязательный)</li>
|
||||
<li>block_concurrency (чекбокс: «Запретить пересечение задач»)</li>
|
||||
<li>Валидация: end_date >= start_date, все поля заполнены</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Редактирование — inline или через ту же форму</li>
|
||||
<li>Удаление — с подтверждением</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<ol start="2">
|
||||
<li>При открытии окна редактирования — загрузить текущие настройки секции + блоки</li>
|
||||
<li>При сохранении — обновить max_concurrent_tasks и сохранить изменения блоков</li>
|
||||
<li>Инжектировать GanttChartService и GanttSectionBlockService</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-3-визуализация-блоков-на-ганте">Этап FE-3: Визуализация блоков на Ганте</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Отобрази блоки секций (GanttSectionBlock) на canvas-диаграмме Ганта.</p>
|
||||
<p>Проект использует Konva.js для рендеринга. Основной компонент: <code>gantt-chart-canvas</code>.</p>
|
||||
<ol>
|
||||
<li>При загрузке данных диаграммы — извлечь блоки из каждой секции</li>
|
||||
<li>В рендерере секций (canvas/timeline-renderer.ts или аналогичный) — для каждого блока:
|
||||
<ul>
|
||||
<li>Нарисовать заштрихованную область (diagonal hatch pattern) на <strong>всю высоту секции</strong> (все строки задач в этой секции)</li>
|
||||
<li>Область ограничена по горизонтали: от start_date до end_date (в пикселях по шкале времени)</li>
|
||||
<li>Цвет штриховки: полупрозрачный серый или configurable</li>
|
||||
<li>Поверх штриховки — текст с названием блока (block.title) на подложке (прямоугольник с фоном, чтобы текст читался)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Блоки рендерятся <strong>до</strong> задач (z-index ниже), чтобы задачи были поверх</li>
|
||||
<li>Блоки с <code>block_concurrency = true</code> и <code>false</code> визуально не отличаются (штриховка одинаковая)</li>
|
||||
<li>При скролле/зуме — блоки пересчитываются как обычные элементы таймлайна</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-4-система-alert-ов">Этап FE-4: Система Alert-ов</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Создай компонент для отображения Alert-ов о конфликтах на диаграмме Ганта.</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Создай компонент <code>pages/gantt/components/gantt-conflict-alerts/</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Компонент размещается <strong>над</strong> canvas-диаграммой Ганта</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Входные данные: массив <code>ConflictItem[]</code> (из модели)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Визуал:</p>
|
||||
<ul>
|
||||
<li>Bootstrap Alert (warning) для каждого конфликта</li>
|
||||
<li>Каждый Alert показывает message из ConflictItem</li>
|
||||
<li><strong>Без кнопки закрытия</strong> — Alert нельзя закрыть вручную</li>
|
||||
<li>Alert исчезает автоматически, когда текущая дата прошла дату конфликта</li>
|
||||
<li>Область <strong>сворачиваемая</strong>: заголовок «⚠️ Предупреждения (N)» — клик сворачивает/разворачивает список</li>
|
||||
<li>По умолчанию — развёрнуто</li>
|
||||
<li>При 0 конфликтов — область скрыта</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Состояние хранится в сервисе или в компоненте диаграммы:</p>
|
||||
<ul>
|
||||
<li>Добавление нового конфликта → area auto-expands</li>
|
||||
<li>Alert удаляется из массива <strong>автоматически</strong>, когда текущая дата прошла дату конфликта</li>
|
||||
<li>Проверка актуальности конфликтов — при каждой отрисовке / изменении данных</li>
|
||||
<li>Не использовать toast — Alert-ы постоянные</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-5-проверка-конфликтов-при-операциях-с-задачами">Этап FE-5: Проверка конфликтов при операциях с задачами</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Интегрируй проверку конфликтов во все операции с задачами на диаграмме Ганта.</p>
|
||||
<p>Проект использует interaction-handler.ts для drag/resize и формы для создания/редактирования.</p>
|
||||
<p><strong>Важно:</strong> Проверка конфликтов — <strong>только на фронтенде</strong>, без вызова API. Используются загруженные данные секции (задачи, блоки, max_concurrent_tasks).</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Вынеси логику проверки конфликтов <code>checkConflicts()</code> в отдельный метод в gantt-canvas.service.ts:</p>
|
||||
<ul>
|
||||
<li>Принимает: section (GanttSection — содержит max_concurrent_tasks и blocks), tasks (GanttTask[] — задачи секции), task_id (nullable — исключить текущую задачу), start_date, end_date</li>
|
||||
<li>Логика:
|
||||
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</li>
|
||||
<li>Возвращает ConflictCheck</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>После <strong>каждой</strong> успешной операции с задачей (создание, обновление, drag, resize):</p>
|
||||
<ul>
|
||||
<li>Вызвать checkConflicts с параметрами задачи</li>
|
||||
<li>Если has_conflicts = true — передать конфликты в GanttConflictAlertsComponent</li>
|
||||
<li>Alert-ы показываются, действие <strong>НЕ отменяется</strong></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Точки интеграции:</p>
|
||||
<ul>
|
||||
<li><strong>Создание задачи</strong> (форма): после успешного POST → checkConflicts → show alerts</li>
|
||||
<li><strong>Редактирование задачи</strong> (форма): после успешного PUT → checkConflicts → show alerts</li>
|
||||
<li><strong>Drag задачи</strong> (interaction-handler.ts): после успешного обновления позиции → checkConflicts → show alerts</li>
|
||||
<li><strong>Resize задачи</strong> (interaction-handler.ts): после успешного обновления размера → checkConflicts → show alerts</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>При переключении диаграммы или секции — очищать Alert-ы</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Alert-ы автоматически удаляются, когда текущая дата прошла дату конфликта — область скрывается, когда конфликтов не осталось</p>
|
||||
</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<hr>
|
||||
<h2 id="порядок-реализации">Порядок реализации</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Этап</th>
|
||||
<th>Сторона</th>
|
||||
<th>Зависимости</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>Миграция + Домен</td>
|
||||
<td>Backend</td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>CRUD блоков</td>
|
||||
<td>Backend</td>
|
||||
<td>Этап 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>Обновление настроек секции</td>
|
||||
<td>Backend</td>
|
||||
<td>Этап 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>Включение блоков в ответ секций</td>
|
||||
<td>Backend</td>
|
||||
<td>Этап 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>Модели + Сервис</td>
|
||||
<td>Frontend</td>
|
||||
<td>— (можно параллельно с backend)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td>Интеграция в модальные окна</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этапы 5 + backend 2,3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>7</td>
|
||||
<td>Визуализация блоков на Ганте</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этапы 5 + backend 4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>8</td>
|
||||
<td>Система Alert-ов</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этап 5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>9</td>
|
||||
<td>Проверка конфликтов при операциях</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этапы 5,7,8</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -110,16 +114,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -166,14 +160,6 @@
|
||||
<h6 class="mt-3">Теги</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/audit/">audit</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/leadera/">leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/security/">security</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/architecture/">architecture</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ssh/">ssh</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/tunnel/">tunnel</a></li>
|
||||
@@ -209,6 +195,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Подключение sand.a2v.space (Leadera Project)</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/guides/sand-a2v-space.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/guides\/sand-a2v-space.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -110,16 +114,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -166,14 +160,6 @@
|
||||
<h6 class="mt-3">Теги</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/audit/">audit</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/leadera/">leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/security/">security</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/architecture/">architecture</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ssh/">ssh</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/tunnel/">tunnel</a></li>
|
||||
@@ -209,6 +195,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>SSH туннели для VPS reverse proxy</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/guides/ssh-tunnels-vps.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/guides\/ssh-tunnels-vps.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -110,16 +114,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -166,14 +160,6 @@
|
||||
<h6 class="mt-3">Теги</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/audit/">audit</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/leadera/">leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/security/">security</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/architecture/">architecture</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ssh/">ssh</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/tunnel/">tunnel</a></li>
|
||||
@@ -209,6 +195,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Настройка VPS как реверс-прокси</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/guides/vps-reverse-proxy.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/guides\/vps-reverse-proxy.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
@@ -39,6 +39,10 @@
|
||||
<a class="nav-link" href="./plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="./leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="./guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -95,6 +99,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="./plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="./leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="./guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="./creds/">Креды</a></li>
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -127,6 +131,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Eversolo T8 API</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/kb/eversolo-api.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/kb\/eversolo-api.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
@@ -0,0 +1,536 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Leadera — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Leadera</h1>
|
||||
|
||||
<h1 id="leadera">Leadera</h1>
|
||||
<p>SaaS-система управления IT-ландшафтом с диаграммами Ганта.</p>
|
||||
<ul>
|
||||
<li><strong>Прод:</strong> <a href="https://leadera.pro">leadera.pro</a></li>
|
||||
<li><strong>Дев:</strong> <a href="https://sand.a2v.space">sand.a2v.space</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../leadera/leadera-gantt-public-access/">Leadera — Публичный доступ к диаграмме Ганта</a></h3>
|
||||
<p class="card-text"><h1 id="публичный-доступ-к-диаграмме-ганта-share">Публичный доступ к диаграмме Ганта (Share)</h1>
|
||||
<p><strong>Дата:</strong> 09.04.2026
|
||||
<strong>Статус:</strong> Требования
|
||||
<strong>Проект:</strong> Leadera</p>
|
||||
<hr>
|
||||
<h2 id="общее-описание">Общее описание</h2>
|
||||
<p>Пользователь может создать публичную ссылку на фрагмент диаграммы Ганта, доступную для просмотра без авторизации. Открывший ссылку видит диаграмму в read-only режиме с возможностью масштабирования таймлайна, но без каких-либо элементов управления (создание, редактирование, удаление задач/секций/вех).</p>
|
||||
<hr>
|
||||
<h2 id="пользовательский-сценарий-создатель-ссылки">Пользовательский сценарий (создатель ссылки)</h2>
|
||||
<ol>
|
||||
<li>Пользователь открывает диаграмму Ганта проекта</li>
|
||||
<li>Нажимает кнопку «Экспорт» / «Общий доступ»</li>
|
||||
<li>Указывает диапазон дат, который должен отображаться на диаграмме</li>
|
||||
<li>Система генерирует уникальную ссылку (UUID) и показывает её пользователю</li>
|
||||
<li>Пользователь копирует ссылку и передаёт заинтересованным лицам</li>
|
||||
</ol>
|
||||
<h3 id="управление-ссылками">Управление ссылками</h3>
|
||||
<ul>
|
||||
<li>Вызывается через меню экспорта → модальное окно «Общий доступ»</li>
|
||||
<li>В модальном окне отображается список созданных ссылок с указанием диапазона дат</li>
|
||||
<li>Каждую ссылку можно <strong>удалить</strong> (полное удаление из БД, не soft delete)</li>
|
||||
<li>Ссылки <strong>нельзя редактировать</strong> — только создавать и удалять</li>
|
||||
<li>Ограничений на количество активных ссылок нет</li>
|
||||
<li>Статистика просмотров (сколько раз открывали) — <strong>не нужна</strong></li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="пользовательский-сценарий-зритель-по-ссылке">Пользовательский сценарий (зритель по ссылке)</h2>
|
||||
<ol>
|
||||
<li>Зритель переходит по ссылке (без авторизации)</li>
|
||||
<li>Открывается отдельная страница с диаграммой Ганта</li>
|
||||
<li>Зритель видит:
|
||||
<ul>
|
||||
<li>Задачи и вехи, попадающие в заданный диапазон</li>
|
||||
<li>Секции (заголовки группировок)</li>
|
||||
<li>Таймлайн с цветовой раскраской задач</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Зритель может:
|
||||
<ul>
|
||||
<li>Масштабировать таймлайн</li>
|
||||
<li>Переключать отображение «по ячейкам» / «по дням»</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Зритель <strong>не может</strong>:
|
||||
<ul>
|
||||
<li>Создавать, редактировать, удалять задачи / секции / вехи</li>
|
||||
<li>Видеть любые контролы управления (кнопки создания, контекстные меню, drag-and-drop и т.д.)</li>
|
||||
<li>Авторизоваться через эту страницу</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<hr>
|
||||
<h2 id="данные-и-фильтрация">Данные и фильтрация</h2>
|
||||
<h3 id="что-попадает-в-представление">Что попадает в представление</h3>
|
||||
<p>Задачи и вехи, у которых хотя бы одно из условий верно:</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../tags/public-access/" class="tag">public-access</a>
|
||||
|
||||
<a href="../tags/sharing/" class="tag">sharing</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../leadera/leadera-gantt-excel-export/">Leadera — Экспорт диаграммы Ганта в Excel</a></h3>
|
||||
<p class="card-text"><h1 id="экспорт-диаграммы-ганта-в-excel">Экспорт диаграммы Ганта в Excel</h1>
|
||||
<p><strong>Дата:</strong> 09.04.2026
|
||||
<strong>Статус:</strong> Требования
|
||||
<strong>Проект:</strong> Leadera</p>
|
||||
<hr>
|
||||
<h2 id="общее-описание">Общее описание</h2>
|
||||
<p>Пользователь нажимает кнопку экспорта в интерфейсе диаграммы Ганта. Система запрашивает диапазон дат выгрузки (по умолчанию — текущий видимый диапазон из интерфейса). После указания диапазона скачивается Excel-файл с визуальным представлением диаграммы Ганта.</p>
|
||||
<hr>
|
||||
<h2 id="вводные-данные">Вводные данные</h2>
|
||||
<ul>
|
||||
<li><strong>Источник:</strong> текущая диаграмма Ганта проекта (один проект = одна диаграмма)</li>
|
||||
<li><strong>Диапазон дат:</strong> пользователь указывает начальную и конечную дату. По умолчанию подставляется видимый в момент запроса диапазон из интерфейса</li>
|
||||
<li><strong>Масштаб таймлайна:</strong> берётся тот, который выбран пользователем в интерфейсе в момент запроса (не сохранённый в системе, а текущий). Варианты: по ячейкам или по дням</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="структура-excel-файла">Структура Excel-файла</h2>
|
||||
<h3 id="шапка-первые-строки">Шапка (первые строки)</h3>
|
||||
<ul>
|
||||
<li>Название диаграммы (название проекта)</li>
|
||||
<li>Диапазон дат, в рамках которого сделана выгрузка</li>
|
||||
</ul>
|
||||
<h3 id="лист--один">Лист — один</h3>
|
||||
<p>Вся диаграмма размещается на одном листе.</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../tags/excel/" class="tag">excel</a>
|
||||
|
||||
<a href="../tags/export/" class="tag">export</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../leadera/leadera-milestones/">Leadera: Вехи (Milestones) на диаграмме Ганта</a></h3>
|
||||
<p class="card-text"><h2 id="бизнес-требования">Бизнес-требования</h2>
|
||||
<h3 id="br-1-сущность-веха-milestone">BR-1: Сущность «Веха» (Milestone)</h3>
|
||||
<p>Веха — это маркер на таймлайне диаграммы Ганта, обозначающий важную дату в проекте. В отличие от задач, веха не имеет длительности и привязана к конкретной дате.</p>
|
||||
<p><strong>Поля вехи:</strong></p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Поле</th>
|
||||
<th>Тип</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>id</td>
|
||||
<td>UUID</td>
|
||||
<td>Идентификатор</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>gantt_chart_id</td>
|
||||
<td>UUID</td>
|
||||
<td>Принадлежность к диаграмме (не к секции!)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>title</td>
|
||||
<td>string</td>
|
||||
<td>Название вехи (обязательное, 1–255 символов)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>date</td>
|
||||
<td>date</td>
|
||||
<td>Дата вехи (без времени, обязательная)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>color</td>
|
||||
<td>string</td>
|
||||
<td>Цвет маркера (HEX, по умолчанию <code>#dc3545</code> — красный)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>created_by</td>
|
||||
<td>UUID</td>
|
||||
<td>Кто создал (не отображается в UI, хранится для аудита)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>updated_by</td>
|
||||
<td>UUID</td>
|
||||
<td>Кто обновил (не отображается в UI)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>created_at</td>
|
||||
<td>timestamp</td>
|
||||
<td>Дата создания</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>updated_at</td>
|
||||
<td>timestamp</td>
|
||||
<td>Дата обновления</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>deleted_at</td>
|
||||
<td>timestamp</td>
|
||||
<td>Soft delete</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Права доступа:</strong> Создание, редактирование и удаление вех доступно участникам с правами выше «только чтение» (аналогично задачам).</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../tags/angular/" class="tag">angular</a>
|
||||
|
||||
<a href="../tags/go/" class="tag">go</a>
|
||||
|
||||
<a href="../tags/api/" class="tag">api</a>
|
||||
|
||||
<a href="../tags/ux/" class="tag">ux</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../leadera/leadera-gantt-filters/">Leadera — Фильтры и реорганизация списка диаграмм Ганта</a></h3>
|
||||
<p class="card-text"><h1 id="фильтры-и-реорганизация-списка-диаграмм-ганта">Фильтры и реорганизация списка диаграмм Ганта</h1>
|
||||
<p><strong>Дата:</strong> 07.04.2026
|
||||
<strong>Статус:</strong> Требования
|
||||
<strong>Проект:</strong> Leadera (dev — sand.a2v.space)</p>
|
||||
<hr>
|
||||
<h2 id="1-постановка-задачи">1. Постановка задачи</h2>
|
||||
<p>Список диаграмм Ганта должен показывать <strong>все доступные пользователю диаграммы</strong> из всех пространств (spaces), с возможностью фильтрации и поиска. Сейчас список привязан к одному пространству — нужно убрать эту зависимость и добавить удобный toolbar.</p>
|
||||
<hr>
|
||||
<h2 id="2-изменения-api">2. Изменения API</h2>
|
||||
<h3 id="21-новый-endpoint-списка-диаграмм">2.1. Новый endpoint списка диаграмм</h3>
|
||||
<p><strong>Было:</strong></p>
|
||||
<pre tabindex="0"><code>GET /api/v1/spaces/{spaceId}/gantt-charts?page=1&per_page=10
|
||||
</code></pre><p><strong>Стало:</strong></p>
|
||||
<pre tabindex="0"><code>GET /api/v1/gantt-charts?page=1&per_page=10
|
||||
</code></pre><p>Пользователь видит все диаграммы из <strong>всех пространств</strong>, где он является подтверждённым участником (статус <code>accepted</code>).</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../tags/api/" class="tag">api</a>
|
||||
|
||||
<a href="../tags/frontend/" class="tag">frontend</a>
|
||||
|
||||
<a href="../tags/requirements/" class="tag">requirements</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../leadera/leadera-websocket-gantt/">Leadera — WebSocket для диаграмм Ганта</a></h3>
|
||||
<p class="card-text"><h1 id="websocket-для-диаграмм-ганта--realtime-совместное-редактирование">WebSocket для диаграмм Ганта — realtime-совместное редактирование</h1>
|
||||
<p><strong>Дата:</strong> 07.04.2026
|
||||
<strong>Статус:</strong> Планирование
|
||||
<strong>Проект:</strong> Leadera (dev — sand.a2v.space)</p>
|
||||
<hr>
|
||||
<h2 id="1-постановка-задачи">1. Постановка задачи</h2>
|
||||
<p>Диаграммы Ганта должны обновляться в реальном времени при совместной работе: если два или более пользователей редактируют один Гант, изменения каждого мгновенно отображаются у остальных — без перезагрузки страницы и ручного обновления.</p>
|
||||
<hr>
|
||||
<h2 id="2-архитектура">2. Архитектура</h2>
|
||||
<pre tabindex="0"><code>Angular frontend ←── WebSocket ──→ Go backend (Hub) ──→ PostgreSQL
|
||||
│ │
|
||||
│ HTTP (мутации) │ Broadcast
|
||||
└──→ REST API ──→ Service ──→ DB ──→ Hub ──→ все клиенты комнаты
|
||||
</code></pre><p><strong>Ключевой принцип:</strong> мутации данных идут через HTTP (REST), WebSocket используется <strong>только для доставки уведомлений</strong>. Сервер — источник истины, клиент не может обойти проверки через WS.</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../tags/websocket/" class="tag">websocket</a>
|
||||
|
||||
<a href="../tags/realtime/" class="tag">realtime</a>
|
||||
|
||||
<a href="../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../tags/architecture/" class="tag">architecture</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../leadera/leadera-section-constraints/">Leadera: Ограничения секций в диаграмме Ганта</a></h3>
|
||||
<p class="card-text"><h2 id="бизнес-требования">Бизнес-требования</h2>
|
||||
<h3 id="br-1-лимит-одновременных-задач-concurrency-limit">BR-1: Лимит одновременных задач (Concurrency Limit)</h3>
|
||||
<p>У каждой секции появляется параметр <strong>max_concurrent_tasks</strong> (целое число ≥ 0, по умолчанию 0).</p>
|
||||
<p><strong>Поведение:</strong></p>
|
||||
<ul>
|
||||
<li>Если <code>max_concurrent_tasks = 3</code> — в любой данный день в этой секции не может быть больше 3 активных задач одновременно</li>
|
||||
<li>Если <code>max_concurrent_tasks = 0</code> — ограничение отключено, лимита нет</li>
|
||||
<li>Пересечение считается по дате: задача занимает дни с <code>start_date</code> по <code>end_date</code> включительно. Две задачи «пересекаются», если у них есть хотя бы один общий день</li>
|
||||
<li>Ограничение работает <strong>только в рамках одной секции</strong>. Задачи из разных секций не влияют друг на друга</li>
|
||||
</ul>
|
||||
<p><strong>Срабатывание при:</strong></p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../tags/angular/" class="tag">angular</a>
|
||||
|
||||
<a href="../tags/go/" class="tag">go</a>
|
||||
|
||||
<a href="../tags/api/" class="tag">api</a>
|
||||
|
||||
<a href="../tags/ux/" class="tag">ux</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../leadera/leadera-audit/">Аудит Leadera — полный отчёт</a></h3>
|
||||
<p class="card-text"><h1 id="аудит-проекта-leadera">Аудит проекта Leadera</h1>
|
||||
<p><strong>Дата:</strong> 30.03.2026<br>
|
||||
<strong>Версия:</strong> pre-production, миграции 1–5</p>
|
||||
<hr>
|
||||
<h2 id="1-обзор-проекта">1. Обзор проекта</h2>
|
||||
<p>Leadera — SaaS-система управления IT-ландшафтом с диаграммами Ганта.</p>
|
||||
<h3 id="стек">Стек</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Компонент</th>
|
||||
<th>Технологии</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Backend</td>
|
||||
<td>Go 1.21+, Gin, PostgreSQL 16, pgx/v5 + sqlx, JWT (HMAC-SHA256)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Frontend</td>
|
||||
<td>Angular 21.1, Bootstrap 5, NgBootstrap, RxJS, Signals</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Инфраструктура</td>
|
||||
<td>Docker (Colima), pm2, nginx reverse proxy, VPS reg.ru</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="доменная-модель">Доменная модель</h3>
|
||||
<pre tabindex="0"><code>User ──┬── Space (через space_members с role)
|
||||
│
|
||||
└── GanttChart (принадлежит Space)
|
||||
├── GanttSection
|
||||
└── GanttTask
|
||||
</code></pre><p><strong>Роли:</strong> admin, editor, viewer — назначаются <strong>на уровне Space</strong> через таблицу <code>space_members</code>.</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/audit/" class="tag">audit</a>
|
||||
|
||||
<a href="../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../tags/security/" class="tag">security</a>
|
||||
|
||||
<a href="../tags/architecture/" class="tag">architecture</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../js/search.js"></script>
|
||||
<script src="../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,732 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Аудит Leadera — полный отчёт — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Аудит Leadera — полный отчёт</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/leadera/leadera-audit.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/leadera\/leadera-audit.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
<a href="../../tags/audit/" class="tag">audit</a>
|
||||
|
||||
<a href="../../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../../tags/security/" class="tag">security</a>
|
||||
|
||||
<a href="../../tags/architecture/" class="tag">architecture</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h1 id="аудит-проекта-leadera">Аудит проекта Leadera</h1>
|
||||
<p><strong>Дата:</strong> 30.03.2026<br>
|
||||
<strong>Версия:</strong> pre-production, миграции 1–5</p>
|
||||
<hr>
|
||||
<h2 id="1-обзор-проекта">1. Обзор проекта</h2>
|
||||
<p>Leadera — SaaS-система управления IT-ландшафтом с диаграммами Ганта.</p>
|
||||
<h3 id="стек">Стек</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Компонент</th>
|
||||
<th>Технологии</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Backend</td>
|
||||
<td>Go 1.21+, Gin, PostgreSQL 16, pgx/v5 + sqlx, JWT (HMAC-SHA256)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Frontend</td>
|
||||
<td>Angular 21.1, Bootstrap 5, NgBootstrap, RxJS, Signals</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Инфраструктура</td>
|
||||
<td>Docker (Colima), pm2, nginx reverse proxy, VPS reg.ru</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="доменная-модель">Доменная модель</h3>
|
||||
<pre tabindex="0"><code>User ──┬── Space (через space_members с role)
|
||||
│
|
||||
└── GanttChart (принадлежит Space)
|
||||
├── GanttSection
|
||||
└── GanttTask
|
||||
</code></pre><p><strong>Роли:</strong> admin, editor, viewer — назначаются <strong>на уровне Space</strong> через таблицу <code>space_members</code>.</p>
|
||||
<h3 id="что-реализовано-endpoints">Что реализовано (endpoints)</h3>
|
||||
<p><strong>Auth (5):</strong> register, login, refresh, change-password, logout<br>
|
||||
<strong>Spaces (9):</strong> CRUD + members CRUD<br>
|
||||
<strong>Gantt Charts (9):</strong> CRUD + archive + members (добавлен, но удалён в миграции 5)<br>
|
||||
<strong>Gantt Sections (5):</strong> CRUD + reorder<br>
|
||||
<strong>Gantt Tasks (4):</strong> CRUD</p>
|
||||
<p><strong>Итого: ~32 endpoint</strong></p>
|
||||
<hr>
|
||||
<h2 id="2-архитектура-и-качество-кода">2. Архитектура и качество кода</h2>
|
||||
<h3 id="21-backend--структура">2.1. Backend — структура</h3>
|
||||
<pre tabindex="0"><code>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)
|
||||
</code></pre><p><strong>Оценка:</strong> ✅ Чистая layered архитектура. Разделение ответственности чёткое.</p>
|
||||
<h3 id="22-frontend--структура">2.2. Frontend — структура</h3>
|
||||
<pre tabindex="0"><code>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
|
||||
</code></pre><p><strong>Оценка:</strong> ✅ Стандартная Angular-структура. Signals для state management.</p>
|
||||
<h3 id="23-проблемы-кода">2.3. Проблемы кода</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Проблема</th>
|
||||
<th>Критичность</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>C1</td>
|
||||
<td><code>\r</code> в service-файлах</td>
|
||||
<td>Medium</td>
|
||||
<td>gantt_service.go, gantt_section_service.go содержат CRLF</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>C2</td>
|
||||
<td><code>main.go:238</code> — мёртвый код <code>getUserHandler</code></td>
|
||||
<td>Low</td>
|
||||
<td>Неиспользуемая функция</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>C3</td>
|
||||
<td>Swagger не сгенерирован</td>
|
||||
<td>Medium</td>
|
||||
<td>Аннотации есть, но <code>api/</code> директории нет</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>C4</td>
|
||||
<td><code>RegisterProtectedRoutes</code> не вызывается в main.go</td>
|
||||
<td><strong>Critical</strong></td>
|
||||
<td>GET/PUT/DELETE <code>/spaces/:id</code> не зарегистрированы</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<h2 id="3-права-доступа--главный-вопрос">3. Права доступа — главный вопрос</h2>
|
||||
<h3 id="31-текущая-реализация">3.1. Текущая реализация</h3>
|
||||
<p>Права назначаются <strong>исключительно через Space</strong>:</p>
|
||||
<ol>
|
||||
<li>Таблица <code>space_members</code> — связка user ↔ space с role (admin/editor/viewer)</li>
|
||||
<li><code>SpaceMiddleware.RequireSpace()</code> — проверяет членство и записывает <code>ContextSpaceRole</code> в context</li>
|
||||
<li><code>RoleMiddleware.RequireMinRole()</code> — проверяет иерархию ролей (admin > editor > viewer)</li>
|
||||
</ol>
|
||||
<p><strong>Диаграммы Ганта НЕ имеют собственной системы ролей.</strong> Таблица <code>gantt_chart_members</code> была создана (миграция 4) и тут же удалена (миграция 5). Все проверки доступа к диаграммам идут через space membership.</p>
|
||||
<h3 id="32-как-это-работает">3.2. Как это работает</h3>
|
||||
<pre tabindex="0"><code>Запрос → AuthMiddleware (JWT → userID)
|
||||
→ SpaceMiddleware.RequireSpace() (проверяет space_members → ContextSpaceRole)
|
||||
→ GanttMiddleware.RequireGanttChart() (проверяет chart.space_id == spaceID)
|
||||
→ RoleMiddleware.RequireMinRole("editor") (проверяет ContextSpaceRole)
|
||||
</code></pre><p><strong>Это правильно.</strong> Любой член Space с ролью editor+ может создавать/редактировать диаграммы. Viewers — только читать.</p>
|
||||
<h3 id="33--критическая-проблема-frontend-не-соответствует-backend">3.3. 🔴 Критическая проблема: Frontend не соответствует Backend</h3>
|
||||
<p><strong>Frontend <code>MembersModalComponent</code></strong> пытается управлять участниками <strong>диаграммы</strong>:</p>
|
||||
<ul>
|
||||
<li><code>ganttService.addMember(chartId, dto)</code> → <code>/api/v1/spaces/{spaceId}/gantt-charts/{chartId}/members</code></li>
|
||||
<li><code>ganttService.updateMemberRole(chartId, userId, dto)</code> → <code>.../members/{userId}</code></li>
|
||||
<li><code>ganttService.removeMember(chartId, userId)</code> → <code>.../members/{userId}</code></li>
|
||||
</ul>
|
||||
<p><strong>Backend <code>GanttHandler.RegisterRoutes()</code> НЕ регистрирует маршруты для members диаграмм.</strong> После удаления <code>gantt_chart_members</code> (миграция 5) эти endpoints не существуют.</p>
|
||||
<p><strong>Результат:</strong> модальное окно «Участники диаграммы» на фронтенде <strong>не работает</strong> — все вызовы вернут 404.</p>
|
||||
<h3 id="34--проблема-role-fallback">3.4. 🔴 Проблема: Role fallback</h3>
|
||||
<p>В <code>role_middleware.go:28-31</code>:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">role</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">GetString</span>(<span style="color:#a6e22e">ContextSpaceRole</span>)
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#a6e22e">role</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">""</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">role</span> = <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">GetString</span>(<span style="color:#a6e22e">ContextRole</span>) <span style="color:#75715e">// fallback на глобальную роль User</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><p>Если <code>ContextSpaceRole</code> не установлен, используется <strong>глобальная роль пользователя</strong> (из JWT). Это значит:</p>
|
||||
<ul>
|
||||
<li>Пользователь с <code>role=admin</code> в таблице <code>users</code> получит admin-доступ к <strong>любому</strong> space, даже если он не член этого space</li>
|
||||
<li>Но <code>SpaceMiddleware</code> проверяет членство раньше, так что в цепочке <code>RequireSpace() → RequireMinRole()</code> это не эксплуатируется</li>
|
||||
<li>Однако если <code>RequireMinRole()</code> используется <strong>без</strong> <code>RequireSpace()</code> — это дыра</li>
|
||||
</ul>
|
||||
<p><strong>Рекомендация:</strong> Удалить fallback на <code>ContextRole</code>. Если <code>ContextSpaceRole</code> пуст — отказывать.</p>
|
||||
<h3 id="35-рекомендации-по-правам-доступа">3.5. Рекомендации по правам доступа</h3>
|
||||
<ol>
|
||||
<li><strong>Удалить модалку участников диаграммы</strong> на фронтенде — она мертва</li>
|
||||
<li><strong>Или</strong> добавить раздел «Участники Space» с возможностью управления ролями через <code>/spaces/:space_id/members</code> (эти endpoints работают)</li>
|
||||
<li>Удалить fallback на <code>ContextRole</code> в <code>role_middleware.go</code></li>
|
||||
<li>Добавить проверку: editor не может повысить кого-то до admin (сейчас может — <code>AddMember</code> разрешает editor+ назначать любую роль)</li>
|
||||
</ol>
|
||||
<hr>
|
||||
<h2 id="4-безопасность">4. Безопасность</h2>
|
||||
<h3 id="41-критические">4.1. Критические</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Проблема</th>
|
||||
<th>Файл</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>S1</td>
|
||||
<td>JWT Secret = test</td>
|
||||
<td><code>.env</code></td>
|
||||
<td><code>JWT_SECRET=test_secret_key_change_in_production</code> — <strong>это прод-конфиг!</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>S2</td>
|
||||
<td>CORS: <code>*</code></td>
|
||||
<td><code>.env</code></td>
|
||||
<td><code>CORS_ALLOWED_ORIGINS=*</code> — любой домен может обращаться к API</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>S3</td>
|
||||
<td>Пароль БД в .env</td>
|
||||
<td><code>.env</code></td>
|
||||
<td><code>DB_PASSWORD=popoloka16</code> — creds в открытом виде</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>S4</td>
|
||||
<td>Refresh tokens: SHA256</td>
|
||||
<td><code>utils/password.go</code></td>
|
||||
<td>SHA256 вместо bcrypt для refresh token hash — быстро брутфорсится</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="42-высокие">4.2. Высокие</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Проблема</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>S5</td>
|
||||
<td>Нет rate limiting</td>
|
||||
<td>Brute-force на <code>/auth/login</code> без ограничений</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>S6</td>
|
||||
<td>Нет CSRF protection</td>
|
||||
<td>API принимает POST/PUT/DELETE без CSRF-токена</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>S7</td>
|
||||
<td>Нет CSP заголовков</td>
|
||||
<td>X-Frame-Options, Content-Security-Policy не настроены</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>S8</td>
|
||||
<td>JWT в localStorage</td>
|
||||
<td>XSS может украсть access + refresh tokens</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="43-средние">4.3. Средние</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Проблема</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>S9</td>
|
||||
<td>Нет password policy</td>
|
||||
<td>Минимальные требования к паролю не enforced</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>S10</td>
|
||||
<td>Нет audit log</td>
|
||||
<td>Действия с spaces/charts не логируются для аудита</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>S11</td>
|
||||
<td>Нет блокировки аккаунта</td>
|
||||
<td>Неограниченные попытки входа</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<h2 id="5-инфраструктура">5. Инфраструктура</h2>
|
||||
<h3 id="51-текущее-состояние">5.1. Текущее состояние</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Компонент</th>
|
||||
<th>Статус</th>
|
||||
<th>Примечание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Go API (pm2: leadera-api)</td>
|
||||
<td>⚠️</td>
|
||||
<td>Порт 8080, хост 0.0.0.0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Angular FE (pm2: leadera-fe)</td>
|
||||
<td>⚠️</td>
|
||||
<td>Порт 4200, хост 0.0.0.0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PostgreSQL (Docker)</td>
|
||||
<td>✅</td>
|
||||
<td>Порт 5432, контейнер app-leadera-db</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VPS reverse proxy</td>
|
||||
<td>⚠️</td>
|
||||
<td>sand.a2v.space → 8082/8083, но 502 если сервисы не запущены</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SSL</td>
|
||||
<td>✅</td>
|
||||
<td>Let’s Encrypt на VPS</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="52-проблемы">5.2. Проблемы</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Проблема</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>I1</td>
|
||||
<td>Docker для БД, нативно для API</td>
|
||||
<td>Несогласованность — API и FE не в Docker</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>I2</td>
|
||||
<td>Нет health-check в pm2</td>
|
||||
<td>API может упасть без перезапуска</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>I3</td>
|
||||
<td>Нет CI/CD</td>
|
||||
<td>Ручной деплой через <code>make run</code> + pm2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>I4</td>
|
||||
<td>Нет nginx на Mac Studio</td>
|
||||
<td>FE и API торчат наружу напрямую</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<h2 id="6-функциональная-оценка">6. Функциональная оценка</h2>
|
||||
<h3 id="61-что-работает">6.1. Что работает</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Функция</th>
|
||||
<th>Backend</th>
|
||||
<th>Frontend</th>
|
||||
<th>Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Регистрация/логин</td>
|
||||
<td>✅</td>
|
||||
<td>✅</td>
|
||||
<td>Работает</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>JWT auth + refresh</td>
|
||||
<td>✅</td>
|
||||
<td>✅</td>
|
||||
<td>Работает</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Spaces CRUD</td>
|
||||
<td>⚠️</td>
|
||||
<td>✅</td>
|
||||
<td><code>RegisterProtectedRoutes</code> не вызван (C4)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Space members</td>
|
||||
<td>✅</td>
|
||||
<td>✅</td>
|
||||
<td>Работает</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gantt charts CRUD</td>
|
||||
<td>✅</td>
|
||||
<td>✅</td>
|
||||
<td>Работает</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gantt sections</td>
|
||||
<td>✅</td>
|
||||
<td>✅</td>
|
||||
<td>Работает</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gantt tasks</td>
|
||||
<td>✅</td>
|
||||
<td>⚠️</td>
|
||||
<td>Нет UI для задач внутри диаграммы</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Архивация диаграмм</td>
|
||||
<td>✅</td>
|
||||
<td>✅</td>
|
||||
<td>Работает</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Members диаграммы</td>
|
||||
<td>❌</td>
|
||||
<td>✅ (мёртвый UI)</td>
|
||||
<td>Backend endpoints не существуют</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="62-что-не-работает-или-отсутствует">6.2. Что НЕ работает или отсутствует</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Функция</th>
|
||||
<th>Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Визуализация Ганта (таймлайн)</td>
|
||||
<td>❌ Нет компонента отрисовки</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Drag & Drop задач</td>
|
||||
<td>❌</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Real-time collaboration</td>
|
||||
<td>❌</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Уведомления</td>
|
||||
<td>❌</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Поиск</td>
|
||||
<td>❌</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Экспорт (PDF/Excel)</td>
|
||||
<td>❌</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Email verification</td>
|
||||
<td>❌</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Password reset</td>
|
||||
<td>Backend есть, UI есть — не проверено</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="63-оценка-готовности">6.3. Оценка готовности</h3>
|
||||
<p><strong>Система на стадии каркаса.</strong> Backend API функционален для CRUD операций, но:</p>
|
||||
<ul>
|
||||
<li>Визуализация Ганта — основная фича — не реализована (нет canvas/SVG рендеринга)</li>
|
||||
<li>Управление участниками только на уровне Space</li>
|
||||
<li>Нет CI/CD, тестов, мониторинга</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="7-приоритизированный-план-исправлений">7. Приоритизированный план исправлений</h2>
|
||||
<h3 id="неделя-1--критическое-безопасность">Неделя 1 — Критическое (безопасность)</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Приоритет</th>
|
||||
<th>Задача</th>
|
||||
<th>Трудоёмкость</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>P0</td>
|
||||
<td>Заменить JWT secret на криптостойкий</td>
|
||||
<td>1 час</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P0</td>
|
||||
<td>Настроить CORS (только sand.a2v.space)</td>
|
||||
<td>1 час</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P0</td>
|
||||
<td>bcrypt для refresh tokens вместо SHA256</td>
|
||||
<td>2 часа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P0</td>
|
||||
<td>Удалить fallback на ContextRole в role_middleware</td>
|
||||
<td>30 мин</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P0</td>
|
||||
<td>Исправить <code>RegisterProtectedRoutes</code> — зарегистрировать маршруты</td>
|
||||
<td>1 час</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P1</td>
|
||||
<td>Rate limiting на auth endpoints</td>
|
||||
<td>4 часа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P1</td>
|
||||
<td>Editor не может назначать admin роль</td>
|
||||
<td>2 часа</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="неделя-2--архитектура-прав">Неделя 2 — Архитектура прав</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Приоритет</th>
|
||||
<th>Задача</th>
|
||||
<th>Трудоёмкость</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>P1</td>
|
||||
<td>Удалить/переписать MembersModalComponent → Space members</td>
|
||||
<td>4 часа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P1</td>
|
||||
<td>Добавить страницу управления участниками Space</td>
|
||||
<td>8 часов</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P2</td>
|
||||
<td>HttpOnly cookies для JWT (вместо localStorage)</td>
|
||||
<td>8 часов</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P2</td>
|
||||
<td>CSRF protection</td>
|
||||
<td>4 часа</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="неделя-3--функциональность">Неделя 3 — Функциональность</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Приоритет</th>
|
||||
<th>Задача</th>
|
||||
<th>Трудоёмкость</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>P2</td>
|
||||
<td>Визуализация Ганта (canvas/d3)</td>
|
||||
<td>40+ часов</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P2</td>
|
||||
<td>Drag & Drop для задач</td>
|
||||
<td>16 часов</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P3</td>
|
||||
<td>Тесты (unit + integration)</td>
|
||||
<td>24 часа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P3</td>
|
||||
<td>CI/CD pipeline</td>
|
||||
<td>8 часов</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<h2 id="8-резюме">8. Резюме</h2>
|
||||
<h3 id="ключевые-выводы">Ключевые выводы</h3>
|
||||
<ol>
|
||||
<li><strong>Права доступа</strong> — архитектурно верно: роль назначается через Space, не через диаграмму. Но фронтенд ещё не обновлён после удаления <code>gantt_chart_members</code>.</li>
|
||||
<li><strong>Безопасность</strong> — критические уязвимости в конфигурации (JWT secret, CORS, bcrypt). Требуют немедленного исправления до любого публичного деплоя.</li>
|
||||
<li><strong>Функциональность</strong> — CRUD работает, но основная ценность (визуализация Ганта) не реализована.</li>
|
||||
<li><strong>Код</strong> — чистый, хорошая архитектура, но есть мёртвый код и незарегистрированные маршруты.</li>
|
||||
</ol>
|
||||
<h3 id="общая-оценка--pre-alpha">Общая оценка: 🔴 Pre-alpha</h3>
|
||||
<p>Проект готов к внутренней демонстрации CRUD, но не готов к продакшену. Первоочередное — безопасность и исправление мёртвых endpoints.</p>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,262 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Leadera — Экспорт диаграммы Ганта в Excel — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Leadera — Экспорт диаграммы Ганта в Excel</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/leadera/leadera-gantt-excel-export.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/leadera\/leadera-gantt-excel-export.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
<a href="../../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../../tags/excel/" class="tag">excel</a>
|
||||
|
||||
<a href="../../tags/export/" class="tag">export</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h1 id="экспорт-диаграммы-ганта-в-excel">Экспорт диаграммы Ганта в Excel</h1>
|
||||
<p><strong>Дата:</strong> 09.04.2026
|
||||
<strong>Статус:</strong> Требования
|
||||
<strong>Проект:</strong> Leadera</p>
|
||||
<hr>
|
||||
<h2 id="общее-описание">Общее описание</h2>
|
||||
<p>Пользователь нажимает кнопку экспорта в интерфейсе диаграммы Ганта. Система запрашивает диапазон дат выгрузки (по умолчанию — текущий видимый диапазон из интерфейса). После указания диапазона скачивается Excel-файл с визуальным представлением диаграммы Ганта.</p>
|
||||
<hr>
|
||||
<h2 id="вводные-данные">Вводные данные</h2>
|
||||
<ul>
|
||||
<li><strong>Источник:</strong> текущая диаграмма Ганта проекта (один проект = одна диаграмма)</li>
|
||||
<li><strong>Диапазон дат:</strong> пользователь указывает начальную и конечную дату. По умолчанию подставляется видимый в момент запроса диапазон из интерфейса</li>
|
||||
<li><strong>Масштаб таймлайна:</strong> берётся тот, который выбран пользователем в интерфейсе в момент запроса (не сохранённый в системе, а текущий). Варианты: по ячейкам или по дням</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="структура-excel-файла">Структура Excel-файла</h2>
|
||||
<h3 id="шапка-первые-строки">Шапка (первые строки)</h3>
|
||||
<ul>
|
||||
<li>Название диаграммы (название проекта)</li>
|
||||
<li>Диапазон дат, в рамках которого сделана выгрузка</li>
|
||||
</ul>
|
||||
<h3 id="лист--один">Лист — один</h3>
|
||||
<p>Вся диаграмма размещается на одном листе.</p>
|
||||
<h3 id="секции">Секции</h3>
|
||||
<ul>
|
||||
<li>Секции отображаются как <strong>визуальная группировка строк</strong> (не отдельные листы)</li>
|
||||
<li>Название секции — строка-заголовок над группой задач</li>
|
||||
</ul>
|
||||
<h3 id="задачи">Задачи</h3>
|
||||
<p>Каждая задача — отдельная строка. Данные задачи:</p>
|
||||
<ul>
|
||||
<li><strong>Название</strong> (текстовая колонка слева)</li>
|
||||
<li><strong>Дата начала</strong> (текстовая колонка слева)</li>
|
||||
<li><strong>Дата конца</strong> (текстовая колонка слева)</li>
|
||||
<li><strong>Цвет</strong> — ячейки таймлайна, соответствующие задаче, закрашены в цвет задачи</li>
|
||||
</ul>
|
||||
<h3 id="таймлайн">Таймлайн</h3>
|
||||
<ul>
|
||||
<li>Таймлайн представлен колонками дат в верхней части (горизонтальная ось времени)</li>
|
||||
<li>Масштаб колонок соответствует выбранному пользователем в момент экспорта (по ячейкам / по дням)</li>
|
||||
<li>Задачи отображаются как <strong>merged cells</strong> по соответствующим колонкам дат — визуальная полоса диаграммы Ганта</li>
|
||||
</ul>
|
||||
<h3 id="вехи">Вехи</h3>
|
||||
<ul>
|
||||
<li>Отображаются <strong>отдельной строкой</strong> (как задача, но с маркировкой вехи)</li>
|
||||
<li>Позиционируются на соответствующей дате в таймлайне</li>
|
||||
</ul>
|
||||
<h3 id="зависимости">Зависимости</h3>
|
||||
<ul>
|
||||
<li><strong>Не отображаются</strong> (стрелочки/связи между задачами не нужны)</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="пользовательский-сценарий">Пользовательский сценарий</h2>
|
||||
<ol>
|
||||
<li>Пользователь открывает диаграмму Ганта проекта</li>
|
||||
<li>Настраивает масштаб таймлайна (по ячейкам / по дням)</li>
|
||||
<li>Нажимает кнопку «Экспорт в Excel»</li>
|
||||
<li>Система показывает диалог с выбором диапазона дат (предзаполнен текущим видимым диапазоном)</li>
|
||||
<li>Пользователь подтверждает / корректирует диапазон</li>
|
||||
<li>Система генерирует Excel-файл и инициирует скачивание</li>
|
||||
</ol>
|
||||
<hr>
|
||||
<h2 id="технические-требования">Технические требования</h2>
|
||||
<h3 id="backend-go-api">Backend (Go API)</h3>
|
||||
<ul>
|
||||
<li>Новый endpoint для генерации Excel (например, <code>GET /api/v1/gantt/:projectId/export</code>)</li>
|
||||
<li>Query-параметры: <code>dateFrom</code>, <code>dateTo</code>, <code>scale</code> (cell|day)</li>
|
||||
<li>Ответ: binary Excel-файл (<code>application/vnd.openxmlformats-officedocument.spreadsheetml.sheet</code>)</li>
|
||||
<li>Авторизация: обязательна (только участники проекта)</li>
|
||||
<li>Библиотека для генерации Excel: <code>excelize</code> (Go)</li>
|
||||
</ul>
|
||||
<h3 id="frontend-angular">Frontend (Angular)</h3>
|
||||
<ul>
|
||||
<li>Кнопка экспорта в тулбаре диаграммы Ганта</li>
|
||||
<li>Модальное окно выбора диапазона дат с datepicker’ами</li>
|
||||
<li>Предзаполнение диапазона из текущего видимого viewport’а</li>
|
||||
<li>Скачивание файла через <code>Blob</code> / <code>saveAs</code></li>
|
||||
</ul>
|
||||
<h3 id="фильтрация-данных">Фильтрация данных</h3>
|
||||
<ul>
|
||||
<li>Выгружаются все задачи и вехи, которые:
|
||||
<ul>
|
||||
<li>Начинаются в диапазоне, ИЛИ</li>
|
||||
<li>Заканчиваются в диапазоне, ИЛИ</li>
|
||||
<li>Начинаются до диапазона и заканчиваются после (перекрывают целиком)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Секции включаются, если в них есть хотя бы одна попавшая задача</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="открытые-вопросы">Открытые вопросы</h2>
|
||||
<p><em>(Нет)</em></p>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,463 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Leadera — Фильтры и реорганизация списка диаграмм Ганта — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Leadera — Фильтры и реорганизация списка диаграмм Ганта</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/leadera/leadera-gantt-filters.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/leadera\/leadera-gantt-filters.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
<a href="../../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../../tags/api/" class="tag">api</a>
|
||||
|
||||
<a href="../../tags/frontend/" class="tag">frontend</a>
|
||||
|
||||
<a href="../../tags/requirements/" class="tag">requirements</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h1 id="фильтры-и-реорганизация-списка-диаграмм-ганта">Фильтры и реорганизация списка диаграмм Ганта</h1>
|
||||
<p><strong>Дата:</strong> 07.04.2026
|
||||
<strong>Статус:</strong> Требования
|
||||
<strong>Проект:</strong> Leadera (dev — sand.a2v.space)</p>
|
||||
<hr>
|
||||
<h2 id="1-постановка-задачи">1. Постановка задачи</h2>
|
||||
<p>Список диаграмм Ганта должен показывать <strong>все доступные пользователю диаграммы</strong> из всех пространств (spaces), с возможностью фильтрации и поиска. Сейчас список привязан к одному пространству — нужно убрать эту зависимость и добавить удобный toolbar.</p>
|
||||
<hr>
|
||||
<h2 id="2-изменения-api">2. Изменения API</h2>
|
||||
<h3 id="21-новый-endpoint-списка-диаграмм">2.1. Новый endpoint списка диаграмм</h3>
|
||||
<p><strong>Было:</strong></p>
|
||||
<pre tabindex="0"><code>GET /api/v1/spaces/{spaceId}/gantt-charts?page=1&per_page=10
|
||||
</code></pre><p><strong>Стало:</strong></p>
|
||||
<pre tabindex="0"><code>GET /api/v1/gantt-charts?page=1&per_page=10
|
||||
</code></pre><p>Пользователь видит все диаграммы из <strong>всех пространств</strong>, где он является подтверждённым участником (статус <code>accepted</code>).</p>
|
||||
<h3 id="22-параметры-фильтрации-и-поиска">2.2. Параметры фильтрации и поиска</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Параметр</th>
|
||||
<th>Тип</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>page</code></td>
|
||||
<td>int</td>
|
||||
<td>Номер страницы (default: 1)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>per_page</code></td>
|
||||
<td>int</td>
|
||||
<td>Записей на странице (default: 10)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>space_id</code></td>
|
||||
<td>UUID (optional)</td>
|
||||
<td>Фильтр по конкретному пространству</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>created_by</code></td>
|
||||
<td>UUID (optional)</td>
|
||||
<td>Фильтр по создателю диаграммы</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>search</code></td>
|
||||
<td>string (optional)</td>
|
||||
<td>Поиск по назвению и описанию (ILIKE, case-insensitive)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>sort</code></td>
|
||||
<td>string (optional)</td>
|
||||
<td>Сортировка: <code>created_at</code>, <code>updated_at</code>, <code>title</code> (default: <code>updated_at</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>order</code></td>
|
||||
<td>string (optional)</td>
|
||||
<td><code>asc</code> / <code>desc</code> (default: <code>desc</code>)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Примеры:</strong></p>
|
||||
<pre tabindex="0"><code>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
|
||||
</code></pre><h3 id="23-изменение-структуры-ответа">2.3. Изменение структуры ответа</h3>
|
||||
<p>Добавить поле <code>space</code> в каждую диаграмму:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"data"</span>: [
|
||||
</span></span><span style="display:flex;"><span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"title"</span>: <span style="color:#e6db74">"Миграция backend"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"description"</span>: <span style="color:#e6db74">"..."</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"created_by"</span>: <span style="color:#e6db74">"uuid"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"created_by_name"</span>: <span style="color:#e6db74">"Антон Агальцов"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"created_at"</span>: <span style="color:#e6db74">"2026-04-01T10:00:00Z"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"updated_at"</span>: <span style="color:#e6db74">"2026-04-07T09:00:00Z"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"space"</span>: {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"name"</span>: <span style="color:#e6db74">"IT Ландшафт"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"color"</span>: <span style="color:#e6db74">"#c4623a"</span>
|
||||
</span></span><span style="display:flex;"><span> },
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"is_archived"</span>: <span style="color:#66d9ef">false</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> ],
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"pagination"</span>: {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"page"</span>: <span style="color:#ae81ff">1</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"per_page"</span>: <span style="color:#ae81ff">10</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"total"</span>: <span style="color:#ae81ff">42</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"total_pages"</span>: <span style="color:#ae81ff">5</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><h3 id="24-endpoint-для-списка-фильтров">2.4. Endpoint для списка фильтров</h3>
|
||||
<p>Вспомогательный endpoint для заполнения dropdown-фильтров:</p>
|
||||
<pre tabindex="0"><code>GET /api/v1/gantt-charts/filters
|
||||
</code></pre><p><strong>Ответ:</strong></p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"spaces"</span>: [
|
||||
</span></span><span style="display:flex;"><span> { <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>, <span style="color:#f92672">"name"</span>: <span style="color:#e6db74">"IT Ландшафт"</span>, <span style="color:#f92672">"color"</span>: <span style="color:#e6db74">"#c4623a"</span> },
|
||||
</span></span><span style="display:flex;"><span> { <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>, <span style="color:#f92672">"name"</span>: <span style="color:#e6db74">"DevOps"</span>, <span style="color:#f92672">"color"</span>: <span style="color:#e6db74">"#27ae60"</span> }
|
||||
</span></span><span style="display:flex;"><span> ],
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"creators"</span>: [
|
||||
</span></span><span style="display:flex;"><span> { <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>, <span style="color:#f92672">"display_name"</span>: <span style="color:#e6db74">"Антон Агальцов"</span> },
|
||||
</span></span><span style="display:flex;"><span> { <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>, <span style="color:#f92672">"display_name"</span>: <span style="color:#e6db74">"Иван Петров"</span> }
|
||||
</span></span><span style="display:flex;"><span> ]
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><p><strong>Логика <code>creators</code>:</strong> уникальные создатели всех диаграмм, доступных текущему пользователю. Получается через DISTINCT-запрос по <code>created_by</code> с JOIN на users.</p>
|
||||
<h3 id="25-backend--изменения-в-go">2.5. Backend — изменения в Go</h3>
|
||||
<p><strong>Файлы для изменения:</strong></p>
|
||||
<h4 id="internalhandlersgantt_handlergo"><code>internal/handlers/gantt_handler.go</code></h4>
|
||||
<ul>
|
||||
<li>Добавить новый handler <code>ListAllGanttCharts</code> — без привязки к space</li>
|
||||
<li>Зарегистрировать маршрут <code>GET /gantt-charts</code> (без <code>/spaces/:id/</code>)</li>
|
||||
<li>Парсить query-параметры: <code>space_id</code>, <code>created_by</code>, <code>search</code>, <code>sort</code>, <code>order</code></li>
|
||||
<li>Старый маршрут <code>GET /spaces/:id/gantt-charts</code> — оставить для обратной совместимости (или удалить, если не используется)</li>
|
||||
</ul>
|
||||
<h4 id="internalservicegantt_servicego"><code>internal/service/gantt_service.go</code></h4>
|
||||
<ul>
|
||||
<li>Новый метод <code>ListAll(ctx, userId, filters)</code> — запрашивает все spaces пользователя (через <code>space_member</code> с <code>status=accepted</code>), затем выбирает все gantt_charts из этих spaces с фильтрами</li>
|
||||
</ul>
|
||||
<h4 id="internalrepositorygantt_repositorygo"><code>internal/repository/gantt_repository.go</code></h4>
|
||||
<ul>
|
||||
<li>Новый метод с динамическим SQL:
|
||||
<ul>
|
||||
<li>Базовый JOIN: <code>gantt_charts</code> → <code>spaces</code> → <code>space_members</code></li>
|
||||
<li>WHERE: <code>space_members.user_id = $1 AND space_members.status = 'accepted'</code></li>
|
||||
<li>Опциональные фильтры: <code>AND space_id = $N</code>, <code>AND created_by = $N</code>, <code>AND (title ILIKE '%' || $N || '%' OR description ILIKE '%' || $N || '%')</code></li>
|
||||
<li>Возвращать поле <code>space</code> (id, name, color) через JOIN</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h4 id="internaldtogantt"><code>internal/dto/gantt/</code></h4>
|
||||
<ul>
|
||||
<li>Новые DTO: <code>GanttChartListFilters</code>, <code>GanttChartWithSpaceResponse</code>, <code>GanttFiltersResponse</code></li>
|
||||
</ul>
|
||||
<p><strong>SQL (пример):</strong></p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> gc.<span style="color:#f92672">*</span>, s.id <span style="color:#66d9ef">as</span> space_id, s.name <span style="color:#66d9ef">as</span> space_name, s.color <span style="color:#66d9ef">as</span> space_color,
|
||||
</span></span><span style="display:flex;"><span> u.display_name <span style="color:#66d9ef">as</span> created_by_name
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> gantt_charts gc
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">JOIN</span> spaces s <span style="color:#66d9ef">ON</span> s.id <span style="color:#f92672">=</span> gc.space_id
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">JOIN</span> space_members sm <span style="color:#66d9ef">ON</span> sm.space_id <span style="color:#f92672">=</span> s.id
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">LEFT</span> <span style="color:#66d9ef">JOIN</span> users u <span style="color:#66d9ef">ON</span> u.id <span style="color:#f92672">=</span> gc.created_by
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> sm.user_id <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">1</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">AND</span> sm.status <span style="color:#f92672">=</span> <span style="color:#e6db74">'accepted'</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">AND</span> (<span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">2</span>::uuid <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">OR</span> gc.space_id <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">2</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">AND</span> (<span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">3</span>::uuid <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">OR</span> gc.created_by <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">3</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">AND</span> (<span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">4</span>::text <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">OR</span> gc.title <span style="color:#66d9ef">ILIKE</span> <span style="color:#e6db74">'%'</span> <span style="color:#f92672">||</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">4</span> <span style="color:#f92672">||</span> <span style="color:#e6db74">'%'</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">OR</span> gc.description <span style="color:#66d9ef">ILIKE</span> <span style="color:#e6db74">'%'</span> <span style="color:#f92672">||</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">4</span> <span style="color:#f92672">||</span> <span style="color:#e6db74">'%'</span>)
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">CASE</span> <span style="color:#66d9ef">WHEN</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">5</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">'title'</span> <span style="color:#66d9ef">THEN</span> gc.title <span style="color:#66d9ef">END</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">CASE</span> <span style="color:#66d9ef">WHEN</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">5</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">'created_at'</span> <span style="color:#66d9ef">THEN</span> gc.created_at::text <span style="color:#66d9ef">END</span>,
|
||||
</span></span><span style="display:flex;"><span> gc.updated_at <span style="color:#66d9ef">DESC</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">LIMIT</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">6</span> <span style="color:#66d9ef">OFFSET</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">7</span>;
|
||||
</span></span></code></pre></div><hr>
|
||||
<h2 id="3-изменения-frontend-angular">3. Изменения Frontend (Angular)</h2>
|
||||
<h3 id="31-toolbar-фильтров">3.1. Toolbar фильтров</h3>
|
||||
<p>Добавить toolbar над списком диаграмм:</p>
|
||||
<pre tabindex="0"><code>┌──────────────────────────────────────────────────────────────────┐
|
||||
│ [🔍 Поиск... ] [▼ Пространство] │
|
||||
│ [▼ Создатель] [▼ Сортировка] [Сбросить фильтры] │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
</code></pre><p><strong>Компоненты:</strong></p>
|
||||
<ul>
|
||||
<li><strong>Строка поиска</strong> — input с debounce (300ms), поиск по назвению и описанию</li>
|
||||
<li><strong>Dropdown «Пространство»</strong> — список из <code>/gantt-charts/filters</code>, с цветовой меткой</li>
|
||||
<li><strong>Dropdown «Создатель»</strong> — список уникальных создателей</li>
|
||||
<li><strong>Dropdown «Сортировка»</strong> — по дате обновления, дате создания, названию</li>
|
||||
<li><strong>Кнопка «Сбросить»</strong> — очищает все фильтры</li>
|
||||
</ul>
|
||||
<p><strong>Состояние фильтров в URL</strong> (query params):</p>
|
||||
<pre tabindex="0"><code>/gantt?search=миграция&space_id=xxx&sort=updated_at
|
||||
</code></pre><p>Это позволяет делиться ссылкой с уже настроенными фильтрами.</p>
|
||||
<h3 id="32-карточка-диаграммы--бейдж-пространства">3.2. Карточка диаграммы — бейдж пространства</h3>
|
||||
<p>На каждой карточке добавить бейдж с названием пространства:</p>
|
||||
<pre tabindex="0"><code>┌─────────────────────────────────────┐
|
||||
│ 🟠 IT Ландшафт │ ← бейдж с цветом пространства
|
||||
│ Миграция backend │
|
||||
│ Перевод PHP→Go, этап 2 │
|
||||
│ Создал: Антон А. · 07.04.2026 │
|
||||
└─────────────────────────────────────┘
|
||||
</code></pre><h3 id="33-форма-создания-диаграммы--выбор-пространства">3.3. Форма создания диаграммы — выбор пространства</h3>
|
||||
<p>В модальном окне создания:</p>
|
||||
<ul>
|
||||
<li>Dropdown со списком пространств, где пользователь имеет роль <strong>editor</strong> или выше</li>
|
||||
<li>Валидация: если пользователь не editor ни в одном пространстве — показать сообщение «Нет доступных пространств для создания»</li>
|
||||
</ul>
|
||||
<p><strong>Запрос доступных пространств:</strong> переиспользовать существующий <code>GET /api/v1/spaces</code> с фильтрацией на клиенте по <code>role ∈ [owner, admin, editor]</code>, либо добавить параметр <code>?min_role=editor</code> на backend.</p>
|
||||
<h3 id="34-изменение-routing">3.4. Изменение routing</h3>
|
||||
<p><strong>Было:</strong></p>
|
||||
<pre tabindex="0"><code>/gantt → список гантов текущего пространства
|
||||
</code></pre><p><strong>Стало:</strong></p>
|
||||
<pre tabindex="0"><code>/gantt → список всех доступных гантов (из всех пространств)
|
||||
</code></pre><p>Убрать зависимость от <code>SpaceStorageService.getSelectedSpaceId()</code> в списке гантов.</p>
|
||||
<h3 id="35-frontend--файлы-для-изменения">3.5. Frontend — файлы для изменения</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Файл</th>
|
||||
<th>Изменение</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>pages/gantt/services/gantt.service.ts</code></td>
|
||||
<td>Новый метод <code>listAll(filters)</code>, <code>getFilters()</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pages/gantt/gantt.component.ts</code></td>
|
||||
<td>Подключить toolbar, парсинг URL-параметров</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pages/gantt/gantt.component.html</code></td>
|
||||
<td>Добавить toolbar, бейджи на карточках</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pages/gantt/components/gantt-card/</code></td>
|
||||
<td>Добавить бейдж пространства</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pages/gantt/components/create-chart-modal/</code></td>
|
||||
<td>Dropdown выбора пространства</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>core/models/</code></td>
|
||||
<td>Обновить модели: добавить <code>space</code> в GanttChart</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<h2 id="4-обратная-совместимость">4. Обратная совместимость</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Endpoint</th>
|
||||
<th>Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>GET /api/v1/spaces/{id}/gantt-charts</code></td>
|
||||
<td>Сохранить (для детального view внутри пространства)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>GET /api/v1/gantt-charts</code></td>
|
||||
<td><strong>Новый</strong> — глобальный список</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>GET /api/v1/gantt-charts/filters</code></td>
|
||||
<td><strong>Новый</strong> — данные для dropdown’ов</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>POST /api/v1/spaces/{id}/gantt-charts</code></td>
|
||||
<td>Сохранить (создание в контексте пространства)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Все CRUD для sections/tasks</td>
|
||||
<td>Без изменений</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<h2 id="5-критерии-приёмки">5. Критерии приёмки</h2>
|
||||
<ul>
|
||||
<li><input disabled="" type="checkbox"> Endpoint <code>GET /api/v1/gantt-charts</code> возвращает все доступные диаграммы</li>
|
||||
<li><input disabled="" type="checkbox"> Фильтр по <code>space_id</code> — показывает только диаграммы конкретного пространства</li>
|
||||
<li><input disabled="" type="checkbox"> Фильтр по <code>created_by</code> — показывает только диаграммы выбранного создателя</li>
|
||||
<li><input disabled="" type="checkbox"> Поиск <code>search</code> — ищет по title и description (case-insensitive)</li>
|
||||
<li><input disabled="" type="checkbox"> Dropdown фильтров заполняется реальными данными из <code>/filters</code></li>
|
||||
<li><input disabled="" type="checkbox"> Карточка диаграммы показывает бейдж пространства с цветом</li>
|
||||
<li><input disabled="" type="checkbox"> Форма создания предлагает только пространства с ролью ≥ editor</li>
|
||||
<li><input disabled="" type="checkbox"> Состояние фильтров сохраняется в URL (можно поделиться ссылкой)</li>
|
||||
<li><input disabled="" type="checkbox"> Старые endpoints (<code>/spaces/{id}/gantt-charts</code>) продолжают работать</li>
|
||||
<li><input disabled="" type="checkbox"> Swagger документация обновлена</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="6-порядок-реализации">6. Порядок реализации</h2>
|
||||
<ol>
|
||||
<li>Backend: DTO для новых запросов/ответов</li>
|
||||
<li>Backend: Repository — SQL-запрос с фильтрами + JOIN на spaces</li>
|
||||
<li>Backend: Service — <code>ListAll</code>, <code>GetFilters</code></li>
|
||||
<li>Backend: Handler — <code>ListAllGanttCharts</code>, <code>GetGanttFilters</code>, регистрация маршрутов</li>
|
||||
<li>Backend: Swagger — обновить документацию</li>
|
||||
<li>Frontend: Модели — обновить GanttChart, добавить Space в ответ</li>
|
||||
<li>Frontend: Service — <code>listAll()</code>, <code>getFilters()</code></li>
|
||||
<li>Frontend: Toolbar компонент — поиск + dropdown’ы</li>
|
||||
<li>Frontend: Карточка — бейдж пространства</li>
|
||||
<li>Frontend: Модальное окно создания — dropdown пространства</li>
|
||||
<li>Frontend: URL-параметры — синхронизация фильтров с router</li>
|
||||
<li>Тестирование</li>
|
||||
</ol>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,298 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Leadera — Публичный доступ к диаграмме Ганта — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Leadera — Публичный доступ к диаграмме Ганта</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/leadera/leadera-gantt-public-access.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/leadera\/leadera-gantt-public-access.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
<a href="../../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../../tags/public-access/" class="tag">public-access</a>
|
||||
|
||||
<a href="../../tags/sharing/" class="tag">sharing</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h1 id="публичный-доступ-к-диаграмме-ганта-share">Публичный доступ к диаграмме Ганта (Share)</h1>
|
||||
<p><strong>Дата:</strong> 09.04.2026
|
||||
<strong>Статус:</strong> Требования
|
||||
<strong>Проект:</strong> Leadera</p>
|
||||
<hr>
|
||||
<h2 id="общее-описание">Общее описание</h2>
|
||||
<p>Пользователь может создать публичную ссылку на фрагмент диаграммы Ганта, доступную для просмотра без авторизации. Открывший ссылку видит диаграмму в read-only режиме с возможностью масштабирования таймлайна, но без каких-либо элементов управления (создание, редактирование, удаление задач/секций/вех).</p>
|
||||
<hr>
|
||||
<h2 id="пользовательский-сценарий-создатель-ссылки">Пользовательский сценарий (создатель ссылки)</h2>
|
||||
<ol>
|
||||
<li>Пользователь открывает диаграмму Ганта проекта</li>
|
||||
<li>Нажимает кнопку «Экспорт» / «Общий доступ»</li>
|
||||
<li>Указывает диапазон дат, который должен отображаться на диаграмме</li>
|
||||
<li>Система генерирует уникальную ссылку (UUID) и показывает её пользователю</li>
|
||||
<li>Пользователь копирует ссылку и передаёт заинтересованным лицам</li>
|
||||
</ol>
|
||||
<h3 id="управление-ссылками">Управление ссылками</h3>
|
||||
<ul>
|
||||
<li>Вызывается через меню экспорта → модальное окно «Общий доступ»</li>
|
||||
<li>В модальном окне отображается список созданных ссылок с указанием диапазона дат</li>
|
||||
<li>Каждую ссылку можно <strong>удалить</strong> (полное удаление из БД, не soft delete)</li>
|
||||
<li>Ссылки <strong>нельзя редактировать</strong> — только создавать и удалять</li>
|
||||
<li>Ограничений на количество активных ссылок нет</li>
|
||||
<li>Статистика просмотров (сколько раз открывали) — <strong>не нужна</strong></li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="пользовательский-сценарий-зритель-по-ссылке">Пользовательский сценарий (зритель по ссылке)</h2>
|
||||
<ol>
|
||||
<li>Зритель переходит по ссылке (без авторизации)</li>
|
||||
<li>Открывается отдельная страница с диаграммой Ганта</li>
|
||||
<li>Зритель видит:
|
||||
<ul>
|
||||
<li>Задачи и вехи, попадающие в заданный диапазон</li>
|
||||
<li>Секции (заголовки группировок)</li>
|
||||
<li>Таймлайн с цветовой раскраской задач</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Зритель может:
|
||||
<ul>
|
||||
<li>Масштабировать таймлайн</li>
|
||||
<li>Переключать отображение «по ячейкам» / «по дням»</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Зритель <strong>не может</strong>:
|
||||
<ul>
|
||||
<li>Создавать, редактировать, удалять задачи / секции / вехи</li>
|
||||
<li>Видеть любые контролы управления (кнопки создания, контекстные меню, drag-and-drop и т.д.)</li>
|
||||
<li>Авторизоваться через эту страницу</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<hr>
|
||||
<h2 id="данные-и-фильтрация">Данные и фильтрация</h2>
|
||||
<h3 id="что-попадает-в-представление">Что попадает в представление</h3>
|
||||
<p>Задачи и вехи, у которых хотя бы одно из условий верно:</p>
|
||||
<ul>
|
||||
<li>Дата начала попадает в диапазон ссылки</li>
|
||||
<li>Дата конца попадает в диапазон ссылки</li>
|
||||
<li>Задача начинается до диапазона и заканчивается после (полностью перекрывает)</li>
|
||||
</ul>
|
||||
<p>Секции включаются, если в них есть хотя бы одна попавшая задача.</p>
|
||||
<h3 id="api-возвращает">API возвращает</h3>
|
||||
<ul>
|
||||
<li>Только задачи и вехи, попадающие в диапазон (не всю диаграмму)</li>
|
||||
<li>Информацию о секциях, содержащих попавшие задачи</li>
|
||||
<li>Минимальный набор полей, необходимый для отрисовки (без внутренних ID пользователей и т.п.)</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="технические-требования">Технические требования</h2>
|
||||
<h3 id="ссылки">Ссылки</h3>
|
||||
<ul>
|
||||
<li>Формат: UUID v4 (каждый раз новый)</li>
|
||||
<li>Срок действия: бессрочно (пока не удалена)</li>
|
||||
<li>Удаление: <strong>жёсткое</strong> (hard delete из БД)</li>
|
||||
</ul>
|
||||
<h3 id="backend-go-api">Backend (Go API)</h3>
|
||||
<h4 id="таблица-бд-gantt_public_links">Таблица БД: <code>gantt_public_links</code></h4>
|
||||
<ul>
|
||||
<li><code>id</code> (UUID, PK)</li>
|
||||
<li><code>project_id</code> (FK → projects)</li>
|
||||
<li><code>date_from</code> (date)</li>
|
||||
<li><code>date_to</code> (date)</li>
|
||||
<li><code>created_by</code> (FK → users)</li>
|
||||
<li><code>created_at</code> (timestamp)</li>
|
||||
</ul>
|
||||
<h4 id="endpoints">Endpoints</h4>
|
||||
<ul>
|
||||
<li><code>POST /api/v1/gantt/:projectId/public-links</code> — создать ссылку (авторизован, участник проекта)
|
||||
<ul>
|
||||
<li>Body: <code>{ dateFrom, dateTo }</code></li>
|
||||
<li>Response: <code>{ url: "https://leadera.pro/share/<uuid>" }</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><code>GET /api/v1/gantt/:projectId/public-links</code> — список ссылок проекта (авторизован, участник)</li>
|
||||
<li><code>DELETE /api/v1/gantt/public-links/:uuid</code> — удалить ссылку (авторизован, создатель)</li>
|
||||
<li><code>GET /api/v1/public/gantt/:uuid</code> — получить данные диаграммы по ссылке (без авторизации)
|
||||
<ul>
|
||||
<li>Response: задачи, вехи, секции в рамках диапазона</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h4 id="rate-limiting">Rate limiting</h4>
|
||||
<ul>
|
||||
<li>На публичный endpoint <code>GET /api/v1/public/gantt/:uuid</code> — rate limiting для защиты от перебора</li>
|
||||
<li>Рекомендация: 60 запросов в минуту на IP</li>
|
||||
</ul>
|
||||
<h3 id="frontend-angular">Frontend (Angular)</h3>
|
||||
<h4 id="модальное-окно-общий-доступ">Модальное окно «Общий доступ»</h4>
|
||||
<ul>
|
||||
<li>Вызывается из меню экспорта</li>
|
||||
<li>Содержит:
|
||||
<ul>
|
||||
<li>Форму создания ссылки (диапазон дат + кнопка «Создать»)</li>
|
||||
<li>Список существующих ссылок (диапазон + URL + кнопка «Копировать» + кнопка «Удалить»)</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h4 id="публичная-страница-shareuuid">Публичная страница <code>/share/:uuid</code></h4>
|
||||
<ul>
|
||||
<li>Отдельный route, <strong>не требует авторизации</strong></li>
|
||||
<li>Отображает диаграмму Ганта в read-only режиме</li>
|
||||
<li>Доступны только: масштабирование таймлайна, переключение ячейки/дни</li>
|
||||
<li>Все контролы управления скрыты (удалены из шаблона, не просто disabled)</li>
|
||||
<li>Минимальный layout — без навигации проекта, без сайдбара</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="открытые-вопросы">Открытые вопросы</h2>
|
||||
<p><em>(Нет)</em></p>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,618 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Leadera: Вехи (Milestones) на диаграмме Ганта — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Leadera: Вехи (Milestones) на диаграмме Ганта</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/leadera/leadera-milestones.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/leadera\/leadera-milestones.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
<a href="../../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../../tags/angular/" class="tag">angular</a>
|
||||
|
||||
<a href="../../tags/go/" class="tag">go</a>
|
||||
|
||||
<a href="../../tags/api/" class="tag">api</a>
|
||||
|
||||
<a href="../../tags/ux/" class="tag">ux</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h2 id="бизнес-требования">Бизнес-требования</h2>
|
||||
<h3 id="br-1-сущность-веха-milestone">BR-1: Сущность «Веха» (Milestone)</h3>
|
||||
<p>Веха — это маркер на таймлайне диаграммы Ганта, обозначающий важную дату в проекте. В отличие от задач, веха не имеет длительности и привязана к конкретной дате.</p>
|
||||
<p><strong>Поля вехи:</strong></p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Поле</th>
|
||||
<th>Тип</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>id</td>
|
||||
<td>UUID</td>
|
||||
<td>Идентификатор</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>gantt_chart_id</td>
|
||||
<td>UUID</td>
|
||||
<td>Принадлежность к диаграмме (не к секции!)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>title</td>
|
||||
<td>string</td>
|
||||
<td>Название вехи (обязательное, 1–255 символов)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>date</td>
|
||||
<td>date</td>
|
||||
<td>Дата вехи (без времени, обязательная)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>color</td>
|
||||
<td>string</td>
|
||||
<td>Цвет маркера (HEX, по умолчанию <code>#dc3545</code> — красный)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>created_by</td>
|
||||
<td>UUID</td>
|
||||
<td>Кто создал (не отображается в UI, хранится для аудита)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>updated_by</td>
|
||||
<td>UUID</td>
|
||||
<td>Кто обновил (не отображается в UI)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>created_at</td>
|
||||
<td>timestamp</td>
|
||||
<td>Дата создания</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>updated_at</td>
|
||||
<td>timestamp</td>
|
||||
<td>Дата обновления</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>deleted_at</td>
|
||||
<td>timestamp</td>
|
||||
<td>Soft delete</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Права доступа:</strong> Создание, редактирование и удаление вех доступно участникам с правами выше «только чтение» (аналогично задачам).</p>
|
||||
<p><strong>Особенности:</strong></p>
|
||||
<ul>
|
||||
<li>Вех может быть несколько в проекте</li>
|
||||
<li>Несколько вех могут иметь одну и ту же дату</li>
|
||||
<li>Вехи сортируются строго по дате (по возрастанию)</li>
|
||||
<li>Вехи не привязаны к секциям — они отображаются <strong>сквозь все секции</strong></li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3 id="br-2-управление-вехами-модальное-окно">BR-2: Управление вехами (модальное окно)</h3>
|
||||
<p><strong>Вызов:</strong> Кнопка «Вехи» рядом с кнопкой «Создать секцию» на странице диаграммы.</p>
|
||||
<p><strong>Модальное окно содержит:</strong></p>
|
||||
<ul>
|
||||
<li>Заголовок: «Вехи проекта»</li>
|
||||
<li>Список всех вех текущей диаграммы (сортировка по дате по возрастанию)</li>
|
||||
<li>Каждая веха в списке показывает: цвет (индикатор), название, дату, кнопки «Редактировать» и «Удалить»</li>
|
||||
<li>Кнопка «Добавить веху»</li>
|
||||
</ul>
|
||||
<p><strong>Форма добавления/редактирования вехи:</strong></p>
|
||||
<ul>
|
||||
<li>Название — текстовый ввод (обязательное, 1–255 символов)</li>
|
||||
<li>Дата — datepicker (обязательное)</li>
|
||||
<li>Цвет — выбор цвета (по аналогии с палитрой цветов в задачах)</li>
|
||||
<li>По умолчанию цвет = красный (<code>#dc3545</code>)</li>
|
||||
</ul>
|
||||
<p><strong>Удаление:</strong> с подтверждением (confirm dialog).</p>
|
||||
<hr>
|
||||
<h3 id="br-3-отображение-вех-на-таймлайне">BR-3: Отображение вех на таймлайне</h3>
|
||||
<p><strong>Визуальное представление:</strong></p>
|
||||
<ul>
|
||||
<li><strong>Маркер-флажок</strong> (flag marker) на шкале таймлайна</li>
|
||||
<li>От маркера вниз идёт <strong>пунктирная вертикальная линия</strong> через все секции (на всю высоту диаграммы)</li>
|
||||
<li>Цвет линии и маркера = цвет вехи</li>
|
||||
</ul>
|
||||
<p><strong>Позиционирование:</strong></p>
|
||||
<ul>
|
||||
<li>Веха ставится ровно на свою дату на шкале времени</li>
|
||||
<li>Если дата вехи выходит за рамки текущего диапазона задач — таймлайн <strong>расширяется</strong> (аналогично задачам)</li>
|
||||
<li>При изменении масштаба (days/weeks/months/quarters) позиция вехи пересчитывается</li>
|
||||
</ul>
|
||||
<p><strong>Tooltip при наведении:</strong></p>
|
||||
<ul>
|
||||
<li>При наведении на маркер/линию — показывается tooltip с названием вехи</li>
|
||||
<li>Если на одну дату приходится несколько вех — в tooltip показываются <strong>все</strong> вехи этой даты с их названиями (каждая с индикацией своего цвета)</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="технические-требования">Технические требования</h2>
|
||||
<h3 id="база-данных">База данных</h3>
|
||||
<h4 id="новая-таблица-gantt_milestones">Новая таблица: gantt_milestones</h4>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> gantt_milestones (
|
||||
</span></span><span style="display:flex;"><span> id UUID <span style="color:#66d9ef">PRIMARY</span> <span style="color:#66d9ef">KEY</span> <span style="color:#66d9ef">DEFAULT</span> gen_random_uuid(),
|
||||
</span></span><span style="display:flex;"><span> gantt_chart_id UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> gantt_charts(id) <span style="color:#66d9ef">ON</span> <span style="color:#66d9ef">DELETE</span> <span style="color:#66d9ef">CASCADE</span>,
|
||||
</span></span><span style="display:flex;"><span> title VARCHAR(<span style="color:#ae81ff">255</span>) <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||||
</span></span><span style="display:flex;"><span> date DATE <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||||
</span></span><span style="display:flex;"><span> color VARCHAR(<span style="color:#ae81ff">20</span>) <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">DEFAULT</span> <span style="color:#e6db74">'#dc3545'</span>,
|
||||
</span></span><span style="display:flex;"><span> created_by UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> users(id),
|
||||
</span></span><span style="display:flex;"><span> updated_by UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> users(id),
|
||||
</span></span><span style="display:flex;"><span> created_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span> <span style="color:#66d9ef">DEFAULT</span> NOW(),
|
||||
</span></span><span style="display:flex;"><span> updated_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span> <span style="color:#66d9ef">DEFAULT</span> NOW(),
|
||||
</span></span><span style="display:flex;"><span> deleted_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span>
|
||||
</span></span><span style="display:flex;"><span>);
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_gantt_milestones_chart <span style="color:#66d9ef">ON</span> gantt_milestones(gantt_chart_id);
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_gantt_milestones_date <span style="color:#66d9ef">ON</span> gantt_milestones(date);
|
||||
</span></span></code></pre></div><hr>
|
||||
<h3 id="backend-go-api">Backend (Go API)</h3>
|
||||
<h4 id="этап-1-миграция--домен">Этап 1: Миграция + Домен</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Добавь в проект новую таблицу <code>gantt_milestones</code>.</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Создай миграцию в <code>cmd/migrate/</code>:</p>
|
||||
<ul>
|
||||
<li>Новая таблица <code>gantt_milestones</code> с полями: 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.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Создай доменную модель <code>GanttMilestone</code> в <code>internal/domain/gantt.go</code>:</p>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">GanttMilestone</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ID</span> <span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"id"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">GanttChartID</span> <span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"gantt_chart_id"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Title</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"title"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Date</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"date"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Color</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"color"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CreatedBy</span> <span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"created_by"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UpdatedBy</span> <span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"updated_by"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CreatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"created_at"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UpdatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"updated_at"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">DeletedAt</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"deleted_at,omitempty"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div></blockquote>
|
||||
<h4 id="этап-2-crud-для-ganttmilestones">Этап 2: CRUD для GanttMilestones</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Реализуй полный CRUD для сущности <code>GanttMilestone</code> по аналогии с существующими handlers/services/repositories в проекте.</p>
|
||||
<p><strong>Роуты:</strong></p>
|
||||
<ul>
|
||||
<li><code>GET /api/gantt-charts/:chartId/milestones</code> — список вех диаграммы</li>
|
||||
<li><code>POST /api/gantt-charts/:chartId/milestones</code> — создать веху</li>
|
||||
<li><code>PUT /api/gantt-charts/:chartId/milestones/:milestoneId</code> — обновить веху</li>
|
||||
<li><code>DELETE /api/gantt-charts/:chartId/milestones/:milestoneId</code> — удалить веху (soft delete)</li>
|
||||
</ul>
|
||||
<p><strong>Repository:</strong> <code>internal/repository/gantt_milestone_repository.go</code></p>
|
||||
<ul>
|
||||
<li><code>GetByChartID(ctx, chartID) ([]domain.GanttMilestone, error)</code> — все неудалённые вехи диаграммы, отсортированные по date ASC</li>
|
||||
<li><code>GetByID(ctx, id) (*domain.GanttMilestone, error)</code></li>
|
||||
<li><code>Create(ctx, milestone) error</code></li>
|
||||
<li><code>Update(ctx, milestone) error</code></li>
|
||||
<li><code>Delete(ctx, id) error</code> — soft delete (set deleted_at)</li>
|
||||
<li>Все запросы — raw SQL через pgx, как в остальных репозиториях проекта</li>
|
||||
</ul>
|
||||
<p><strong>Service:</strong> <code>internal/service/gantt_milestone_service.go</code></p>
|
||||
<ul>
|
||||
<li>Валидация: title (1–255 символов), date (обязательное), color (валидный HEX, default ‘#dc3545’)</li>
|
||||
<li>Проверка прав доступа: пользователь должен иметь права выше «только чтение» в пространстве (space) диаграммы</li>
|
||||
<li>Проверка что chartId существует и не архивирован</li>
|
||||
</ul>
|
||||
<p><strong>Handler:</strong> <code>internal/handler/gantt_milestone_handler.go</code></p>
|
||||
<ul>
|
||||
<li>Swagger-аннотации для всех эндпоинтов</li>
|
||||
<li>Валидация входных данных</li>
|
||||
<li>Ошибки в формате проекта</li>
|
||||
</ul>
|
||||
<p><strong>DTO:</strong> <code>internal/dto/gantt/milestones_dto.go</code></p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">CreateGanttMilestoneRequest</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Title</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"title" binding:"required,min=1,max=255"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Date</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"date" binding:"required"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Color</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"color" binding:"omitempty,max=20"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">UpdateGanttMilestoneRequest</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Title</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"title" binding:"omitempty,min=1,max=255"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Date</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"date" binding:"omitempty"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Color</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"color" binding:"omitempty,max=20"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">GanttMilestoneResponse</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ID</span> <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"id"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">GanttChartID</span> <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"gantt_chart_id"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Title</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"title"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Date</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"date"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Color</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"color"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CreatedByUser</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">CreatedByUpdatedUser</span> <span style="color:#e6db74">`json:"created_by_user"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UpdatedByUser</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">CreatedByUpdatedUser</span> <span style="color:#e6db74">`json:"updated_by_user"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CreatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"created_at"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UpdatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"updated_at"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">GanttMilestonesListResponse</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Milestones</span> []<span style="color:#a6e22e">GanttMilestoneResponse</span> <span style="color:#e6db74">`json:"milestones"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><p><strong>Регистрация роутов:</strong> Добавь группу milestone-роутов в роутер рядом с существующими роутами секций/задач. Проверка прав через существующий middleware.</p>
|
||||
</blockquote>
|
||||
<h4 id="этап-3-включение-вех-в-ответ-диаграммы">Этап 3: Включение вех в ответ диаграммы</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>При запросе данных диаграммы Ганта (полная загрузка) включай связанные вехи в ответ.</p>
|
||||
<ol>
|
||||
<li>Добавь эндпоинт или расширь существующий <code>GET /api/gantt-charts/:chartId</code> — включить поле <code>milestones</code> с массивом <code>GanttMilestoneResponse</code></li>
|
||||
<li>Вехи с <code>deleted_at IS NOT NULL</code> не возвращаются</li>
|
||||
<li>Вехи сортируются по <code>date</code> ASC</li>
|
||||
<li>Вехи загружаются вместе с данными диаграммы (tasks, sections) — один запрос при открытии Ганта</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<hr>
|
||||
<h3 id="frontend-angular">Frontend (Angular)</h3>
|
||||
<h4 id="этап-fe-1-модели--сервис">Этап FE-1: Модели + Сервис</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Добавь в проект поддержку вех (milestones) на диаграмме Ганта.</p>
|
||||
<ol>
|
||||
<li>В <code>pages/gantt/models/</code> (или соответствующем файле моделей) добавь:</li>
|
||||
</ol>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttMilestone</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">gantt_chart_id</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">color</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">created_by_user</span><span style="color:#f92672">?:</span> { <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>; <span style="color:#a6e22e">full_name</span>: <span style="color:#66d9ef">string</span> };
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">updated_by_user</span><span style="color:#f92672">?:</span> { <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>; <span style="color:#a6e22e">full_name</span>: <span style="color:#66d9ef">string</span> };
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">created_at</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">updated_at</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttMilestoneCreateRequest</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">color?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttMilestoneUpdateRequest</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">date?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">color?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><ol start="2">
|
||||
<li>
|
||||
<p>Добавь <code>milestones: GanttMilestone[]</code> в интерфейс/модель полной загрузки данных диаграммы</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Создай сервис <code>pages/gantt/services/gantt-milestone.service.ts</code>:</p>
|
||||
</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>Inject HttpClient</li>
|
||||
<li>Методы:
|
||||
<ul>
|
||||
<li><code>getMilestones(chartId: string): Observable<GanttMilestone[]></code></li>
|
||||
<li><code>createMilestone(chartId: string, data: GanttMilestoneCreateRequest): Observable<GanttMilestone></code></li>
|
||||
<li><code>updateMilestone(chartId: string, milestoneId: string, data: GanttMilestoneUpdateRequest): Observable<GanttMilestone></code></li>
|
||||
<li><code>deleteMilestone(chartId: string, milestoneId: string): Observable<void></code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Все методы возвращают Observable с типизированными ответами (по аналогии с GanttChartService)</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-2-модальное-окно-управления-вехами">Этап FE-2: Модальное окно управления вехами</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Создай компонент модального окна для управления вехами диаграммы Ганта.</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Создай компонент <code>pages/gantt/components/milestones-modal/</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Кнопка вызова:</strong> Добавь кнопку «Вехи» рядом с кнопкой «Создать секцию» в toolbar диаграммы. По клику — открытие модального окна через ng-bootstrap NgbModal.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Модальное окно</strong> (ng-bootstrap modal):</p>
|
||||
<ul>
|
||||
<li>Заголовок: «Вехи проекта»</li>
|
||||
<li>Список вех (сортировка по date ASC):
|
||||
<ul>
|
||||
<li>Каждый элемент: цветной кружок (color), название, дата (форматированная), кнопки ✏️ (редактировать) и 🗑️ (удалить с confirm)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Кнопка «Добавить веху» внизу списка</li>
|
||||
<li>Если вех нет — сообщение «Вех пока нет. Добавьте первую!»</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Форма добавления/редактирования</strong> (inline в модальном окне):</p>
|
||||
<ul>
|
||||
<li>Название — текстовый ввод (обязательное)</li>
|
||||
<li>Дата — ng-bootstrap datepicker (обязательное)</li>
|
||||
<li>Цвет — палитра цветов (использовать тот же компонент/подход, что и для задач)</li>
|
||||
<li>По умолчанию цвет = <code>#dc3545</code> (красный)</li>
|
||||
<li>Кнопки «Сохранить» и «Отмена»</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>При сохранении — вызов соответствующего метода сервиса (create/update)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>При удалении — confirm dialog → вызов deleteMilestone</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>После любого изменения — обновить список вех и обновить данные на канвасе (через сервис/GanttStore)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Инжектировать GanttMilestoneService и NgbModal</p>
|
||||
</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-3-отображение-вех-на-таймлайне-konva-canvas">Этап FE-3: Отображение вех на таймлайне (Konva canvas)</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Отобрази вехи (milestones) на canvas-диаграмме Ганта.</p>
|
||||
<p>Проект использует Konva.js для рендеринга.</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>При загрузке данных диаграммы — извлечь массив <code>milestones</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Для каждой вехи нарисовать на canvas:</p>
|
||||
<ul>
|
||||
<li><strong>Маркер-флажок</strong> (flag marker) на позиции даты вехи на шкале времени
|
||||
<ul>
|
||||
<li>Стиль: маленький треугольник или флаг на верхней границе таймлайна</li>
|
||||
<li>Цвет = milestone.color</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Пунктирная вертикальная линия</strong> от маркера вниз через все секции (на всю высоту области секций)
|
||||
<ul>
|
||||
<li>Стиль: Konva.Line, dash: [5, 5], strokeWidth: 1.5, color = milestone.color, opacity: 0.7</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Позиционирование:</strong></p>
|
||||
<ul>
|
||||
<li>Рассчитать X-позицию по дате вехи с использованием текущей шкалы времени (как для задач)</li>
|
||||
<li>Если дата вехи выходит за текущий диапазон — расширить диапазон (влияет на общую ширину таймлайна)</li>
|
||||
<li>При изменении масштаба (days/weeks/months/quarters) — пересчитать позиции всех вех</li>
|
||||
<li>При скролле — вехи скроллятся вместе с задачами</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Tooltip при наведении:</strong></p>
|
||||
<ul>
|
||||
<li>При наведении на маркер или пунктирную линию — показать Konva.Tooltip или HTML tooltip:
|
||||
<ul>
|
||||
<li>Если веха одна на эту дату — показать название</li>
|
||||
<li>Если несколько вех на одну дату — показать список: цветной кружок + название для каждой</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Z-index:</strong></p>
|
||||
<ul>
|
||||
<li>Линии вех рисуются <strong>поверх</strong> фона секций, но <strong>под</strong> задачами</li>
|
||||
<li>Маркеры-флажки — поверх всего (самый высокий z-index)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Реактивность:</strong></p>
|
||||
<ul>
|
||||
<li>При добавлении/удалении/редактировании вехи — перерисовать только вехи (без полного ре рендера задач)</li>
|
||||
<li>Подписаться на изменения массива milestones через сервис/store</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<hr>
|
||||
<h2 id="порядок-реализации">Порядок реализации</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Этап</th>
|
||||
<th>Сторона</th>
|
||||
<th>Зависимости</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>Миграция + Домен</td>
|
||||
<td>Backend</td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>CRUD вех</td>
|
||||
<td>Backend</td>
|
||||
<td>Этап 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>Включение вех в ответ диаграммы</td>
|
||||
<td>Backend</td>
|
||||
<td>Этап 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>Модели + Сервис</td>
|
||||
<td>Frontend</td>
|
||||
<td>— (параллельно с backend)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>Модальное окно управления вехами</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этапы 4 + backend 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td>Отображение вех на таймлайне</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этапы 4 + backend 3</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,681 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Leadera: Ограничения секций в диаграмме Ганта — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Leadera: Ограничения секций в диаграмме Ганта</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/leadera/leadera-section-constraints.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/leadera\/leadera-section-constraints.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
<a href="../../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../../tags/angular/" class="tag">angular</a>
|
||||
|
||||
<a href="../../tags/go/" class="tag">go</a>
|
||||
|
||||
<a href="../../tags/api/" class="tag">api</a>
|
||||
|
||||
<a href="../../tags/ux/" class="tag">ux</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h2 id="бизнес-требования">Бизнес-требования</h2>
|
||||
<h3 id="br-1-лимит-одновременных-задач-concurrency-limit">BR-1: Лимит одновременных задач (Concurrency Limit)</h3>
|
||||
<p>У каждой секции появляется параметр <strong>max_concurrent_tasks</strong> (целое число ≥ 0, по умолчанию 0).</p>
|
||||
<p><strong>Поведение:</strong></p>
|
||||
<ul>
|
||||
<li>Если <code>max_concurrent_tasks = 3</code> — в любой данный день в этой секции не может быть больше 3 активных задач одновременно</li>
|
||||
<li>Если <code>max_concurrent_tasks = 0</code> — ограничение отключено, лимита нет</li>
|
||||
<li>Пересечение считается по дате: задача занимает дни с <code>start_date</code> по <code>end_date</code> включительно. Две задачи «пересекаются», если у них есть хотя бы один общий день</li>
|
||||
<li>Ограничение работает <strong>только в рамках одной секции</strong>. Задачи из разных секций не влияют друг на друга</li>
|
||||
</ul>
|
||||
<p><strong>Срабатывание при:</strong></p>
|
||||
<ol>
|
||||
<li>Создании задачи — если добавление нарушает лимит → Alert</li>
|
||||
<li>Перетаскивании задачи (drag) — если новая позиция нарушает лимит → Alert</li>
|
||||
<li>Растягивании задачи (resize) — если новый размер нарушает лимит → Alert</li>
|
||||
<li>Редактировании задачи (форма) — если изменённые даты нарушают лимит → Alert</li>
|
||||
</ol>
|
||||
<p><strong>Важно:</strong> Действие <strong>НЕ отменяется</strong>. Задача создаётся/перемещается в любом случае, но пользователь видит предупреждение.</p>
|
||||
<p><strong>Текст Alert:</strong> «В секции “[название секции]” на [дата/диапазон] превышен лимит одновременных задач (максимум: N)»</p>
|
||||
<hr>
|
||||
<h3 id="br-2-блоки-секций-section-blocks">BR-2: Блоки секций (Section Blocks)</h3>
|
||||
<p>У каждой секции появляется набор <strong>блоков</strong> — особый вид записей, отображаемых на таймлайне как заштрихованные области.</p>
|
||||
<p><strong>Сущность GanttSectionBlock:</strong></p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Поле</th>
|
||||
<th>Тип</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>id</td>
|
||||
<td>UUID</td>
|
||||
<td>Идентификатор</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>section_id</td>
|
||||
<td>UUID</td>
|
||||
<td>Принадлежность к секции</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>title</td>
|
||||
<td>string</td>
|
||||
<td>Название (например: «Отпуск», «Командировка»)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>start_date</td>
|
||||
<td>date</td>
|
||||
<td>Начало интервала (минимум 1 день)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>end_date</td>
|
||||
<td>date</td>
|
||||
<td>Конец интервала</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>block_concurrency</td>
|
||||
<td>boolean</td>
|
||||
<td>Запрещать ли пересечение обычных задач с данным блоком</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Валидация:</strong></p>
|
||||
<ul>
|
||||
<li><code>end_date</code> не может быть раньше <code>start_date</code></li>
|
||||
<li>Все поля обязательны</li>
|
||||
<li>Блок может быть в прошлом</li>
|
||||
</ul>
|
||||
<p><strong>Визуальное отображение на Ганте:</strong></p>
|
||||
<ul>
|
||||
<li>Заштрихованная область на <strong>всю высоту секции</strong> (включая все строки задач в секции)</li>
|
||||
<li>Название блока отображается <strong>поверх штриховки с подложкой</strong> (для читаемости)</li>
|
||||
</ul>
|
||||
<p><strong>Поведение block_concurrency:</strong></p>
|
||||
<ul>
|
||||
<li>Если <code>block_concurrency = true</code> — при пересечении задачи с данным блоком показывается Alert (аналогично BR-1), но задача <strong>всё равно создаётся/перемещается</strong></li>
|
||||
<li>Если <code>block_concurrency = false</code> — блок отображается визуально, но не влияет на задачи</li>
|
||||
</ul>
|
||||
<p><strong>Срабатывание при:</strong></p>
|
||||
<ol>
|
||||
<li>Создании задачи — пересекает блок с <code>block_concurrency = true</code> → Alert</li>
|
||||
<li>Перетаскивании задачи (drag) → Alert</li>
|
||||
<li>Растягивании задачи (resize) → Alert</li>
|
||||
<li>Редактировании задачи (форма) → Alert</li>
|
||||
</ol>
|
||||
<p><strong>Текст Alert:</strong> «В секции “[название секции]” задача пересекает блок “[название блока]” ([дата] — [дата])»</p>
|
||||
<hr>
|
||||
<h3 id="br-3-система-alert-ов">BR-3: Система Alert-ов</h3>
|
||||
<p>Все предупреждения (от BR-1 и BR-2) выводятся в <strong>единую область</strong> над диаграммой Ганта.</p>
|
||||
<p><strong>Поведение:</strong></p>
|
||||
<ul>
|
||||
<li>Alert-ы — Bootstrap Alert (warning), <strong>постоянные</strong></li>
|
||||
<li>Пользователь <strong>НЕ может</strong> закрыть Alert вручную</li>
|
||||
<li>Alert исчезает <strong>автоматически</strong>, когда текущая дата прошла дату конфликта (конфликт больше не актуален)</li>
|
||||
<li>Проверка конфликтов — <strong>только на фронтенде</strong> (на основе загруженных данных: задачи секции, блоки, max_concurrent_tasks)</li>
|
||||
<li>Если Alert-ов много — область <strong>сворачиваемая</strong> с счётчиком в бейдже (например: «⚠️ 3 предупреждения»)</li>
|
||||
<li>По умолчанию — развёрнуто</li>
|
||||
<li>Сворачивание/разворачивание — клик по заголовку области</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3 id="br-4-настройки-секции">BR-4: Настройки секции</h3>
|
||||
<ul>
|
||||
<li>Настройки совмещены с <strong>существующим модальным окном редактирования секции</strong> (не отдельное окно)</li>
|
||||
</ul>
|
||||
<p><strong>Окно создания секции</strong> — добавляется:</p>
|
||||
<ul>
|
||||
<li>Поле «Макс. одновременных задач» (числовой ввод, 0 = без лимита, по умолчанию 0)</li>
|
||||
</ul>
|
||||
<p><strong>Окно редактирования секции</strong> — добавляется:</p>
|
||||
<ul>
|
||||
<li>Поле «Макс. одновременных задач» (числовой ввод, 0 = без лимита)</li>
|
||||
<li>Список блоков (таблица/список с CRUD: добавить, редактировать, удалить)
|
||||
<ul>
|
||||
<li><strong>Сортировка:</strong> по <code>start_date</code> по убыванию (будущее сверху)</li>
|
||||
<li>Каждый блок — форма: title, start_date, end_date, block_concurrency (чекбокс: «Запретить пересечение задач»)</li>
|
||||
<li>Валидация: end_date >= start_date, все поля заполнены</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3 id="br-5-наследование">BR-5: Наследование</h3>
|
||||
<ul>
|
||||
<li>При появлении функционала копирования/дублирования диаграммы — настройки секций (лимит + блоки) наследуются</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="технические-требования">Технические требования</h2>
|
||||
<h3 id="база-данных">База данных</h3>
|
||||
<h4 id="новая-таблица-gantt_section_blocks">Новая таблица: gantt_section_blocks</h4>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> gantt_section_blocks (
|
||||
</span></span><span style="display:flex;"><span> id UUID <span style="color:#66d9ef">PRIMARY</span> <span style="color:#66d9ef">KEY</span> <span style="color:#66d9ef">DEFAULT</span> gen_random_uuid(),
|
||||
</span></span><span style="display:flex;"><span> section_id UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> gantt_sections(id) <span style="color:#66d9ef">ON</span> <span style="color:#66d9ef">DELETE</span> <span style="color:#66d9ef">CASCADE</span>,
|
||||
</span></span><span style="display:flex;"><span> title VARCHAR(<span style="color:#ae81ff">255</span>) <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||||
</span></span><span style="display:flex;"><span> start_date DATE <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||||
</span></span><span style="display:flex;"><span> end_date DATE <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||||
</span></span><span style="display:flex;"><span> block_concurrency BOOLEAN <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">DEFAULT</span> <span style="color:#66d9ef">false</span>,
|
||||
</span></span><span style="display:flex;"><span> created_by UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> users(id),
|
||||
</span></span><span style="display:flex;"><span> updated_by UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> users(id),
|
||||
</span></span><span style="display:flex;"><span> created_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span> <span style="color:#66d9ef">DEFAULT</span> NOW(),
|
||||
</span></span><span style="display:flex;"><span> updated_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span> <span style="color:#66d9ef">DEFAULT</span> NOW(),
|
||||
</span></span><span style="display:flex;"><span> deleted_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">CONSTRAINT</span> chk_block_dates <span style="color:#66d9ef">CHECK</span> (end_date <span style="color:#f92672">>=</span> start_date)
|
||||
</span></span><span style="display:flex;"><span>);
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_gantt_section_blocks_section <span style="color:#66d9ef">ON</span> gantt_section_blocks(section_id);
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_gantt_section_blocks_dates <span style="color:#66d9ef">ON</span> gantt_section_blocks(start_date, end_date);
|
||||
</span></span></code></pre></div><h4 id="изменение-таблицы-gantt_sections">Изменение таблицы: gantt_sections</h4>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> gantt_sections <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">COLUMN</span> max_concurrent_tasks INTEGER <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">DEFAULT</span> <span style="color:#ae81ff">0</span>;
|
||||
</span></span></code></pre></div><hr>
|
||||
<h3 id="backend-go-api">Backend (Go API)</h3>
|
||||
<h4 id="этап-1-миграция--домен">Этап 1: Миграция + Домен</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Добавь в проект новую таблицу <code>gantt_section_blocks</code> и колонку <code>max_concurrent_tasks</code> в <code>gantt_sections</code>.</p>
|
||||
<ol>
|
||||
<li>Создай миграцию в <code>cmd/migrate/</code>:
|
||||
<ul>
|
||||
<li>Новая таблица <code>gantt_section_blocks</code> с полями: 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).</li>
|
||||
<li>ALTER TABLE gantt_sections ADD COLUMN max_concurrent_tasks INTEGER NOT NULL DEFAULT 0.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Создай доменную модель <code>GanttSectionBlock</code> в <code>internal/domain/gantt.go</code> со всеми полями таблицы.</li>
|
||||
<li>Добавь поле <code>MaxConcurrentTasks int</code> в структуру <code>GanttSection</code>.</li>
|
||||
<li>Обнови маппинг в relevant repository файлах, чтобы новое поле <code>max_concurrent_tasks</code> читалось/писалось.</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-2-crud-для-ganttsectionblocks">Этап 2: CRUD для GanttSectionBlocks</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Реализуй полный CRUD для сущности <code>GanttSectionBlock</code> по аналогии с существующими handlers/services/repositories в проекте.</p>
|
||||
<p><strong>Роуты (внутри группы секции):</strong></p>
|
||||
<ul>
|
||||
<li><code>GET /api/gantt-charts/:chartId/sections/:sectionId/blocks</code> — список блоков секции</li>
|
||||
<li><code>POST /api/gantt-charts/:chartId/sections/:sectionId/blocks</code> — создать блок</li>
|
||||
<li><code>PUT /api/gantt-charts/:chartId/sections/:sectionId/blocks/:blockId</code> — обновить блок</li>
|
||||
<li><code>DELETE /api/gantt-charts/:chartId/sections/:sectionId/blocks/:blockId</code> — удалить блок (soft delete)</li>
|
||||
</ul>
|
||||
<p><strong>Repository:</strong> <code>internal/repository/gantt_section_block_repository.go</code></p>
|
||||
<ul>
|
||||
<li><code>GetBySectionID(ctx, sectionID) ([]domain.GanttSectionBlock, error)</code></li>
|
||||
<li><code>GetByID(ctx, id) (*domain.GanttSectionBlock, error)</code></li>
|
||||
<li><code>Create(ctx, block) error</code></li>
|
||||
<li><code>Update(ctx, block) error</code></li>
|
||||
<li><code>Delete(ctx, id) error</code> — soft delete (set deleted_at)</li>
|
||||
<li>Все запросы — raw SQL через pgx, как в остальных репозиториях проекта</li>
|
||||
</ul>
|
||||
<p><strong>Service:</strong> <code>internal/service/gantt_section_block_service.go</code></p>
|
||||
<ul>
|
||||
<li>Бизнес-логика: валидация dates (end >= start), проверка прав доступа к секции</li>
|
||||
<li>Inject repository</li>
|
||||
</ul>
|
||||
<p><strong>Handler:</strong> <code>internal/handler/gantt_section_block_handler.go</code></p>
|
||||
<ul>
|
||||
<li>Swagger-аннотации для всех эндпоинтов</li>
|
||||
<li>Валидация входных данных</li>
|
||||
<li>Ошибки в формате проекта (GanttErrorResponse)</li>
|
||||
</ul>
|
||||
<p><strong>Модели запросов:</strong></p>
|
||||
<ul>
|
||||
<li>Create: title (required), start_date (required), end_date (required), block_concurrency (bool, default false)</li>
|
||||
<li>Update: title, start_date, end_date, block_concurrency — все опциональны</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
<h4 id="этап-3-обновление-настроек-секции">Этап 3: Обновление настроек секции</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Обнови CRUD секций (gantt_sections) для поддержки нового поля <code>max_concurrent_tasks</code>.</p>
|
||||
<ol>
|
||||
<li>Добавь <code>max_concurrent_tasks</code> в <code>GanttSectionCreateRequest</code> (опциональное, int ≥ 0, default 0)</li>
|
||||
<li>Добавь <code>max_concurrent_tasks</code> в <code>GanttSectionUpdateRequest</code> (опциональное, int ≥ 0)</li>
|
||||
<li>Обнови SQL-запросы в repository: INSERT и UPDATE должны включать <code>max_concurrent_tasks</code></li>
|
||||
<li>При GET секции (или списке секций) — возвращать <code>max_concurrent_tasks</code> в ответе</li>
|
||||
<li>Валидация: значение должно быть ≥ 0</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-4-включение-блоков-в-ответ-секций">Этап 4: Включение блоков в ответ секций</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>При запросе списка секций или одной секции включай связанные блоки в ответ.</p>
|
||||
<ol>
|
||||
<li>Обнови <code>GET /api/gantt-charts/:chartId/sections/:sectionId</code> — добавить поле <code>blocks</code> с массивом GanttSectionBlock</li>
|
||||
<li>Обнови <code>GET /api/gantt-charts/:chartId/sections</code> — добавить поле <code>blocks</code> для каждой секции</li>
|
||||
<li>Блоки с <code>deleted_at IS NOT NULL</code> не возвращаются</li>
|
||||
<li>Блоки сортируются по start_date по убыванию (будущее сверху)</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<hr>
|
||||
<h3 id="frontend-angular">Frontend (Angular)</h3>
|
||||
<h4 id="этап-fe-1-модели--сервис">Этап FE-1: Модели + Сервис</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Добавь в проект поддержку ограничений секций Ганта.</p>
|
||||
<ol>
|
||||
<li>В <code>pages/gantt/models/gantt-chart.model.ts</code> добавь:</li>
|
||||
</ol>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttSectionBlock</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">section_id</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">start_date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">end_date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">block_concurrency</span>: <span style="color:#66d9ef">boolean</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">created_by_user</span>: <span style="color:#66d9ef">UserInfo</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">updated_by_user</span>: <span style="color:#66d9ef">UserInfo</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">created_at</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">updated_at</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttSectionBlockCreateRequest</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">start_date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">end_date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">block_concurrency?</span>: <span style="color:#66d9ef">boolean</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttSectionBlockUpdateRequest</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">start_date?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">end_date?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">block_concurrency?</span>: <span style="color:#66d9ef">boolean</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ConflictCheck</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">has_conflicts</span>: <span style="color:#66d9ef">boolean</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">conflicts</span>: <span style="color:#66d9ef">ConflictItem</span>[];
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ConflictItem</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">type</span><span style="color:#f92672">:</span> <span style="color:#e6db74">'concurrency'</span> <span style="color:#f92672">|</span> <span style="color:#e6db74">'block'</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">section_id</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">section_title</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">block_title?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">start_date?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">end_date?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">message</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">dates?</span>: <span style="color:#66d9ef">string</span>[];
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><ol start="2">
|
||||
<li>
|
||||
<p>Добавь <code>max_concurrent_tasks: number</code> в интерфейс <code>GanttSection</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Добавь <code>blocks: GanttSectionBlock[]</code> в интерфейс <code>GanttSection</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Добавь <code>max_concurrent_tasks?: number</code> в <code>GanttSectionCreateRequest</code> и <code>GanttSectionUpdateRequest</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Создай сервис <code>pages/gantt/services/gantt-section-block.service.ts</code>:</p>
|
||||
</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>Inject HttpClient</li>
|
||||
<li>Методы: getBlocks(chartId, sectionId), createBlock(chartId, sectionId, data), updateBlock(chartId, sectionId, blockId, data), deleteBlock(chartId, sectionId, blockId)</li>
|
||||
<li>Все методы возвращают Observable с типизированными ответами (по аналогии с GanttChartService)</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-2-интеграция-в-модальные-окна-секции">Этап FE-2: Интеграция в модальные окна секции</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Интегрируй настройки ограничений в существующие модальные окна создания и редактирования секции Ганта.</p>
|
||||
<ol>
|
||||
<li>Найди существующие компоненты модальных окон секции (создание/редактирование).</li>
|
||||
</ol>
|
||||
<p><strong>Окно создания секции</strong> — добавить:</p>
|
||||
<ul>
|
||||
<li>Числовой ввод: «Максимум одновременных задач»</li>
|
||||
<li>Подсказка: «0 = без ограничений»</li>
|
||||
<li>Валидация: число ≥ 0</li>
|
||||
<li>Значение по умолчанию: 0</li>
|
||||
<li>Отправляется в <code>GanttSectionCreateRequest</code> как <code>max_concurrent_tasks</code></li>
|
||||
</ul>
|
||||
<p><strong>Окно редактирования секции</strong> — добавить:</p>
|
||||
<ul>
|
||||
<li>Числовой ввод: «Максимум одновременных задач» (аналогично созданию)</li>
|
||||
<li>Секция «Блоки (запретные интервалы)»:
|
||||
<ul>
|
||||
<li>Таблица/список существующих блоков с колонками: Название, Начало, Конец, Блокировка (чекбокс/иконка), Действия (редактировать/удалить)</li>
|
||||
<li><strong>Сортировка:</strong> по <code>start_date</code> по убыванию (будущее сверху)</li>
|
||||
<li>Кнопка «Добавить блок» — inline-форма:
|
||||
<ul>
|
||||
<li>title (текстовый ввод, обязательный)</li>
|
||||
<li>start_date (datepicker, обязательный)</li>
|
||||
<li>end_date (datepicker, обязательный)</li>
|
||||
<li>block_concurrency (чекбокс: «Запретить пересечение задач»)</li>
|
||||
<li>Валидация: end_date >= start_date, все поля заполнены</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Редактирование — inline или через ту же форму</li>
|
||||
<li>Удаление — с подтверждением</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<ol start="2">
|
||||
<li>При открытии окна редактирования — загрузить текущие настройки секции + блоки</li>
|
||||
<li>При сохранении — обновить max_concurrent_tasks и сохранить изменения блоков</li>
|
||||
<li>Инжектировать GanttChartService и GanttSectionBlockService</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-3-визуализация-блоков-на-ганте">Этап FE-3: Визуализация блоков на Ганте</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Отобрази блоки секций (GanttSectionBlock) на canvas-диаграмме Ганта.</p>
|
||||
<p>Проект использует Konva.js для рендеринга. Основной компонент: <code>gantt-chart-canvas</code>.</p>
|
||||
<ol>
|
||||
<li>При загрузке данных диаграммы — извлечь блоки из каждой секции</li>
|
||||
<li>В рендерере секций (canvas/timeline-renderer.ts или аналогичный) — для каждого блока:
|
||||
<ul>
|
||||
<li>Нарисовать заштрихованную область (diagonal hatch pattern) на <strong>всю высоту секции</strong> (все строки задач в этой секции)</li>
|
||||
<li>Область ограничена по горизонтали: от start_date до end_date (в пикселях по шкале времени)</li>
|
||||
<li>Цвет штриховки: полупрозрачный серый или configurable</li>
|
||||
<li>Поверх штриховки — текст с названием блока (block.title) на подложке (прямоугольник с фоном, чтобы текст читался)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Блоки рендерятся <strong>до</strong> задач (z-index ниже), чтобы задачи были поверх</li>
|
||||
<li>Блоки с <code>block_concurrency = true</code> и <code>false</code> визуально не отличаются (штриховка одинаковая)</li>
|
||||
<li>При скролле/зуме — блоки пересчитываются как обычные элементы таймлайна</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-4-система-alert-ов">Этап FE-4: Система Alert-ов</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Создай компонент для отображения Alert-ов о конфликтах на диаграмме Ганта.</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Создай компонент <code>pages/gantt/components/gantt-conflict-alerts/</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Компонент размещается <strong>над</strong> canvas-диаграммой Ганта</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Входные данные: массив <code>ConflictItem[]</code> (из модели)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Визуал:</p>
|
||||
<ul>
|
||||
<li>Bootstrap Alert (warning) для каждого конфликта</li>
|
||||
<li>Каждый Alert показывает message из ConflictItem</li>
|
||||
<li><strong>Без кнопки закрытия</strong> — Alert нельзя закрыть вручную</li>
|
||||
<li>Alert исчезает автоматически, когда текущая дата прошла дату конфликта</li>
|
||||
<li>Область <strong>сворачиваемая</strong>: заголовок «⚠️ Предупреждения (N)» — клик сворачивает/разворачивает список</li>
|
||||
<li>По умолчанию — развёрнуто</li>
|
||||
<li>При 0 конфликтов — область скрыта</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Состояние хранится в сервисе или в компоненте диаграммы:</p>
|
||||
<ul>
|
||||
<li>Добавление нового конфликта → area auto-expands</li>
|
||||
<li>Alert удаляется из массива <strong>автоматически</strong>, когда текущая дата прошла дату конфликта</li>
|
||||
<li>Проверка актуальности конфликтов — при каждой отрисовке / изменении данных</li>
|
||||
<li>Не использовать toast — Alert-ы постоянные</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-5-проверка-конфликтов-при-операциях-с-задачами">Этап FE-5: Проверка конфликтов при операциях с задачами</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Интегрируй проверку конфликтов во все операции с задачами на диаграмме Ганта.</p>
|
||||
<p>Проект использует interaction-handler.ts для drag/resize и формы для создания/редактирования.</p>
|
||||
<p><strong>Важно:</strong> Проверка конфликтов — <strong>только на фронтенде</strong>, без вызова API. Используются загруженные данные секции (задачи, блоки, max_concurrent_tasks).</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Вынеси логику проверки конфликтов <code>checkConflicts()</code> в отдельный метод в gantt-canvas.service.ts:</p>
|
||||
<ul>
|
||||
<li>Принимает: section (GanttSection — содержит max_concurrent_tasks и blocks), tasks (GanttTask[] — задачи секции), task_id (nullable — исключить текущую задачу), start_date, end_date</li>
|
||||
<li>Логика:
|
||||
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</li>
|
||||
<li>Возвращает ConflictCheck</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>После <strong>каждой</strong> успешной операции с задачей (создание, обновление, drag, resize):</p>
|
||||
<ul>
|
||||
<li>Вызвать checkConflicts с параметрами задачи</li>
|
||||
<li>Если has_conflicts = true — передать конфликты в GanttConflictAlertsComponent</li>
|
||||
<li>Alert-ы показываются, действие <strong>НЕ отменяется</strong></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Точки интеграции:</p>
|
||||
<ul>
|
||||
<li><strong>Создание задачи</strong> (форма): после успешного POST → checkConflicts → show alerts</li>
|
||||
<li><strong>Редактирование задачи</strong> (форма): после успешного PUT → checkConflicts → show alerts</li>
|
||||
<li><strong>Drag задачи</strong> (interaction-handler.ts): после успешного обновления позиции → checkConflicts → show alerts</li>
|
||||
<li><strong>Resize задачи</strong> (interaction-handler.ts): после успешного обновления размера → checkConflicts → show alerts</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>При переключении диаграммы или секции — очищать Alert-ы</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Alert-ы автоматически удаляются, когда текущая дата прошла дату конфликта — область скрывается, когда конфликтов не осталось</p>
|
||||
</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<hr>
|
||||
<h2 id="порядок-реализации">Порядок реализации</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Этап</th>
|
||||
<th>Сторона</th>
|
||||
<th>Зависимости</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>Миграция + Домен</td>
|
||||
<td>Backend</td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>CRUD блоков</td>
|
||||
<td>Backend</td>
|
||||
<td>Этап 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>Обновление настроек секции</td>
|
||||
<td>Backend</td>
|
||||
<td>Этап 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>Включение блоков в ответ секций</td>
|
||||
<td>Backend</td>
|
||||
<td>Этап 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>Модели + Сервис</td>
|
||||
<td>Frontend</td>
|
||||
<td>— (можно параллельно с backend)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td>Интеграция в модальные окна</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этапы 5 + backend 2,3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>7</td>
|
||||
<td>Визуализация блоков на Ганте</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этапы 5 + backend 4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>8</td>
|
||||
<td>Система Alert-ов</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этап 5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>9</td>
|
||||
<td>Проверка конфликтов при операциях</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этапы 5,7,8</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,942 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Leadera — WebSocket для диаграмм Ганта — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Leadera — WebSocket для диаграмм Ганта</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/leadera/leadera-websocket-gantt.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/leadera\/leadera-websocket-gantt.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
<a href="../../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../../tags/websocket/" class="tag">websocket</a>
|
||||
|
||||
<a href="../../tags/realtime/" class="tag">realtime</a>
|
||||
|
||||
<a href="../../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../../tags/architecture/" class="tag">architecture</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h1 id="websocket-для-диаграмм-ганта--realtime-совместное-редактирование">WebSocket для диаграмм Ганта — realtime-совместное редактирование</h1>
|
||||
<p><strong>Дата:</strong> 07.04.2026
|
||||
<strong>Статус:</strong> Планирование
|
||||
<strong>Проект:</strong> Leadera (dev — sand.a2v.space)</p>
|
||||
<hr>
|
||||
<h2 id="1-постановка-задачи">1. Постановка задачи</h2>
|
||||
<p>Диаграммы Ганта должны обновляться в реальном времени при совместной работе: если два или более пользователей редактируют один Гант, изменения каждого мгновенно отображаются у остальных — без перезагрузки страницы и ручного обновления.</p>
|
||||
<hr>
|
||||
<h2 id="2-архитектура">2. Архитектура</h2>
|
||||
<pre tabindex="0"><code>Angular frontend ←── WebSocket ──→ Go backend (Hub) ──→ PostgreSQL
|
||||
│ │
|
||||
│ HTTP (мутации) │ Broadcast
|
||||
└──→ REST API ──→ Service ──→ DB ──→ Hub ──→ все клиенты комнаты
|
||||
</code></pre><p><strong>Ключевой принцип:</strong> мутации данных идут через HTTP (REST), WebSocket используется <strong>только для доставки уведомлений</strong>. Сервер — источник истины, клиент не может обойти проверки через WS.</p>
|
||||
<h3 id="комнаты">Комнаты</h3>
|
||||
<p>Каждый открытый Гант — это «комната» с идентификатором <code>chartId</code>. При подключении клиент подписывается на комнату. Hub рассылает события всем подписчикам комнаты.</p>
|
||||
<hr>
|
||||
<h2 id="3-backend-go">3. Backend (Go)</h2>
|
||||
<h3 id="31-зависимости">3.1. Зависимости</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>go get github.com/gorilla/websocket
|
||||
</span></span></code></pre></div><h3 id="32-новые-файлы--internalws">3.2. Новые файлы — <code>internal/ws/</code></h3>
|
||||
<h4 id="messagego--типы-сообщений"><code>message.go</code> — типы сообщений</h4>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">ws</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> <span style="color:#e6db74">"time"</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// EventType — тип события</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">EventType</span> <span style="color:#66d9ef">string</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> (
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Данные</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventSectionCreated</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"section_created"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventSectionUpdated</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"section_updated"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventSectionDeleted</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"section_deleted"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventSectionsReordered</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"sections_reordered"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventTaskCreated</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"task_created"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventTaskUpdated</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"task_updated"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventTaskDeleted</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"task_deleted"</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Присутствие</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventUserJoined</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"user_joined"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventUserLeft</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"user_left"</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Безопасность / доступ</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventAccessRevoked</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"access_revoked"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventChartArchived</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"chart_archived"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventRoleChanged</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"role_changed"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventTokenExpired</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"token_expired"</span>
|
||||
</span></span><span style="display:flex;"><span>)
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Event — WS-событие, отправляемое клиентам</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Event</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span> <span style="color:#a6e22e">EventType</span> <span style="color:#e6db74">`json:"type"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ChartID</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"chartId"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UserID</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"userId,omitempty"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Payload</span> <span style="color:#66d9ef">interface</span>{} <span style="color:#e6db74">`json:"payload,omitempty"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Timestamp</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"timestamp"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// IncomingMessage — сообщение от клиента (для ping/pong)</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">IncomingMessage</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"type"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><h4 id="hubgo--центральный-брокер"><code>hub.go</code> — центральный брокер</h4>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">ws</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"sync"</span>
|
||||
</span></span><span style="display:flex;"><span>)
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Hub управляет комнатами и рассылкой событий</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Hub</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">mu</span> <span style="color:#a6e22e">sync</span>.<span style="color:#a6e22e">RWMutex</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">rooms</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">map</span>[<span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>]<span style="color:#66d9ef">bool</span> <span style="color:#75715e">// chartId → клиенты</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewHub</span>() <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#f92672">&</span><span style="color:#a6e22e">Hub</span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">rooms</span>: make(<span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">map</span>[<span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>]<span style="color:#66d9ef">bool</span>),
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Subscribe добавляет клиента в комнату</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">h</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span>) <span style="color:#a6e22e">Subscribe</span>(<span style="color:#a6e22e">chartID</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">client</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Lock</span>()
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Unlock</span>()
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span>[<span style="color:#a6e22e">chartID</span>] <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span>[<span style="color:#a6e22e">chartID</span>] = make(<span style="color:#66d9ef">map</span>[<span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>]<span style="color:#66d9ef">bool</span>)
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span>[<span style="color:#a6e22e">chartID</span>][<span style="color:#a6e22e">client</span>] = <span style="color:#66d9ef">true</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Уведомить остальных: user_joined</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">broadcastExcept</span>(<span style="color:#a6e22e">chartID</span>, <span style="color:#a6e22e">client</span>, <span style="color:#a6e22e">Event</span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span>: <span style="color:#a6e22e">EventUserJoined</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ChartID</span>: <span style="color:#a6e22e">chartID</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UserID</span>: <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">UserID</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Payload</span>: <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">"username"</span>: <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Username</span>},
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Timestamp</span>: <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>(),
|
||||
</span></span><span style="display:flex;"><span> })
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Unsubscribe удаляет клиента из комнаты</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">h</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span>) <span style="color:#a6e22e">Unsubscribe</span>(<span style="color:#a6e22e">chartID</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">client</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Lock</span>()
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Unlock</span>()
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">clients</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span>[<span style="color:#a6e22e">chartID</span>]; <span style="color:#a6e22e">ok</span> {
|
||||
</span></span><span style="display:flex;"><span> delete(<span style="color:#a6e22e">clients</span>, <span style="color:#a6e22e">client</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">clients</span>) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
|
||||
</span></span><span style="display:flex;"><span> delete(<span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span>, <span style="color:#a6e22e">chartID</span>)
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Уведомить остальных: user_left</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">broadcastExcept</span>(<span style="color:#a6e22e">chartID</span>, <span style="color:#a6e22e">client</span>, <span style="color:#a6e22e">Event</span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span>: <span style="color:#a6e22e">EventUserLeft</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ChartID</span>: <span style="color:#a6e22e">chartID</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UserID</span>: <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">UserID</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Timestamp</span>: <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>(),
|
||||
</span></span><span style="display:flex;"><span> })
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Broadcast рассылает событие всем клиентам в комнате</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">h</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span>) <span style="color:#a6e22e">Broadcast</span>(<span style="color:#a6e22e">chartID</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">event</span> <span style="color:#a6e22e">Event</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">RLock</span>()
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">RUnlock</span>()
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">clients</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span>[<span style="color:#a6e22e">chartID</span>]; <span style="color:#a6e22e">ok</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">clients</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Send</span>(<span style="color:#a6e22e">event</span>)
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// SendToUser отправляет событие конкретному пользователю во всех его комнатах</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">h</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span>) <span style="color:#a6e22e">SendToUser</span>(<span style="color:#a6e22e">userID</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">event</span> <span style="color:#a6e22e">Event</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">RLock</span>()
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">RUnlock</span>()
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">clients</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">clients</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">UserID</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">userID</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Send</span>(<span style="color:#a6e22e">event</span>)
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// broadcastExcept рассылает всем, кроме отправителя</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">h</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span>) <span style="color:#a6e22e">broadcastExcept</span>(<span style="color:#a6e22e">chartID</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">sender</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>, <span style="color:#a6e22e">event</span> <span style="color:#a6e22e">Event</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">clients</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span>[<span style="color:#a6e22e">chartID</span>]; <span style="color:#a6e22e">ok</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">clients</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">client</span> <span style="color:#f92672">!=</span> <span style="color:#a6e22e">sender</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Send</span>(<span style="color:#a6e22e">event</span>)
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><h4 id="clientgo--обёртка-ws-соединения"><code>client.go</code> — обёртка WS-соединения</h4>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">ws</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"encoding/json"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"sync"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"time"</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"github.com/gorilla/websocket"</span>
|
||||
</span></span><span style="display:flex;"><span>)
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> (
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">writeWait</span> = <span style="color:#ae81ff">10</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">pongWait</span> = <span style="color:#ae81ff">60</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">pingPeriod</span> = (<span style="color:#a6e22e">pongWait</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">9</span>) <span style="color:#f92672">/</span> <span style="color:#ae81ff">10</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">maxMessageSize</span> = <span style="color:#ae81ff">1024</span>
|
||||
</span></span><span style="display:flex;"><span>)
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Client</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Hub</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Conn</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">Conn</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UserID</span> <span style="color:#66d9ef">string</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Username</span> <span style="color:#66d9ef">string</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ChartID</span> <span style="color:#66d9ef">string</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">SendCh</span> <span style="color:#66d9ef">chan</span> <span style="color:#a6e22e">Event</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">mu</span> <span style="color:#a6e22e">sync</span>.<span style="color:#a6e22e">Mutex</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewClient</span>(<span style="color:#a6e22e">hub</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span>, <span style="color:#a6e22e">conn</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">Conn</span>, <span style="color:#a6e22e">userID</span>, <span style="color:#a6e22e">username</span>, <span style="color:#a6e22e">chartID</span> <span style="color:#66d9ef">string</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#f92672">&</span><span style="color:#a6e22e">Client</span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Hub</span>: <span style="color:#a6e22e">hub</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Conn</span>: <span style="color:#a6e22e">conn</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UserID</span>: <span style="color:#a6e22e">userID</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Username</span>: <span style="color:#a6e22e">username</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ChartID</span>: <span style="color:#a6e22e">chartID</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">SendCh</span>: make(<span style="color:#66d9ef">chan</span> <span style="color:#a6e22e">Event</span>, <span style="color:#ae81ff">64</span>),
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// ReadPump читает входящие сообщения (ping/pong)</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>) <span style="color:#a6e22e">ReadPump</span>() {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">defer</span> <span style="color:#66d9ef">func</span>() {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Hub</span>.<span style="color:#a6e22e">Unsubscribe</span>(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">ChartID</span>, <span style="color:#a6e22e">c</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">Close</span>()
|
||||
</span></span><span style="display:flex;"><span> }()
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">SetReadLimit</span>(<span style="color:#a6e22e">maxMessageSize</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">SetReadDeadline</span>(<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>().<span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">pongWait</span>))
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">SetPongHandler</span>(<span style="color:#66d9ef">func</span>(<span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">error</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">SetReadDeadline</span>(<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>().<span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">pongWait</span>))
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
|
||||
</span></span><span style="display:flex;"><span> })
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">message</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">ReadMessage</span>()
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Клиент может слать только ping, данные игнорируем</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">_</span> = <span style="color:#a6e22e">message</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// WritePump отправляет события клиенту</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>) <span style="color:#a6e22e">WritePump</span>() {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ticker</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">NewTicker</span>(<span style="color:#a6e22e">pingPeriod</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">defer</span> <span style="color:#66d9ef">func</span>() {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ticker</span>.<span style="color:#a6e22e">Stop</span>()
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">Close</span>()
|
||||
</span></span><span style="display:flex;"><span> }()
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">select</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">event</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#f92672"><-</span><span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">SendCh</span>:
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">SetWriteDeadline</span>(<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>().<span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">writeWait</span>))
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">WriteMessage</span>(<span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">CloseMessage</span>, []<span style="color:#66d9ef">byte</span>{})
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">data</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">Marshal</span>(<span style="color:#a6e22e">event</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Lock</span>()
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">WriteMessage</span>(<span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">TextMessage</span>, <span style="color:#a6e22e">data</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Unlock</span>()
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#f92672"><-</span><span style="color:#a6e22e">ticker</span>.<span style="color:#a6e22e">C</span>:
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">SetWriteDeadline</span>(<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>().<span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">writeWait</span>))
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">WriteMessage</span>(<span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">PingMessage</span>, <span style="color:#66d9ef">nil</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Send ставит событие в очередь отправки</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>) <span style="color:#a6e22e">Send</span>(<span style="color:#a6e22e">event</span> <span style="color:#a6e22e">Event</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">select</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">SendCh</span> <span style="color:#f92672"><-</span> <span style="color:#a6e22e">event</span>:
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">default</span>:
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Очередь полна — отключаем медленного клиента</span>
|
||||
</span></span><span style="display:flex;"><span> close(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">SendCh</span>)
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><h3 id="33-ws-endpoint--internalhandlersws_handlergo">3.3. WS endpoint — <code>internal/handlers/ws_handler.go</code></h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">handlers</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"net/http"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"time"</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"app-leadera-api/internal/middleware"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"app-leadera-api/internal/ws"</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"github.com/gin-gonic/gin"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"github.com/google/uuid"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"github.com/gorilla/websocket"</span>
|
||||
</span></span><span style="display:flex;"><span>)
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">upgrader</span> = <span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">Upgrader</span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ReadBufferSize</span>: <span style="color:#ae81ff">1024</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">WriteBufferSize</span>: <span style="color:#ae81ff">1024</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CheckOrigin</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) <span style="color:#66d9ef">bool</span> { <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span> }, <span style="color:#75715e">// nginx проксирует</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">WSHandler</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">hub</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">Hub</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ganttMW</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">middleware</span>.<span style="color:#a6e22e">GanttMiddleware</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewWSHandler</span>(<span style="color:#a6e22e">hub</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">Hub</span>, <span style="color:#a6e22e">ganttMW</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">middleware</span>.<span style="color:#a6e22e">GanttMiddleware</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">WSHandler</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#f92672">&</span><span style="color:#a6e22e">WSHandler</span>{<span style="color:#a6e22e">hub</span>: <span style="color:#a6e22e">hub</span>, <span style="color:#a6e22e">ganttMW</span>: <span style="color:#a6e22e">ganttMW</span>}
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">h</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">WSHandler</span>) <span style="color:#a6e22e">HandleGanttWS</span>(<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">gin</span>.<span style="color:#a6e22e">Context</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// 1. Получить пользователя из контекста (auth middleware уже отработал)</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">userID</span>, <span style="color:#a6e22e">exists</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">"userID"</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">exists</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AbortWithStatusJSON</span>(<span style="color:#ae81ff">401</span>, <span style="color:#a6e22e">gin</span>.<span style="color:#a6e22e">H</span>{<span style="color:#e6db74">"error"</span>: <span style="color:#e6db74">"unauthorized"</span>})
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// 2. Проверить доступ к chartId (gantt middleware)</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">chartID</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Param</span>(<span style="color:#e6db74">"chartId"</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">uuid</span>.<span style="color:#a6e22e">Parse</span>(<span style="color:#a6e22e">chartID</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AbortWithStatusJSON</span>(<span style="color:#ae81ff">400</span>, <span style="color:#a6e22e">gin</span>.<span style="color:#a6e22e">H</span>{<span style="color:#e6db74">"error"</span>: <span style="color:#e6db74">"invalid chart id"</span>})
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// TODO: проверить через ganttMW что userID имеет доступ к этому chart</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// 3. Upgrade до WebSocket</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">conn</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">upgrader</span>.<span style="color:#a6e22e">Upgrade</span>(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Writer</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Request</span>, <span style="color:#66d9ef">nil</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// 4. Создать клиент и подписать на комнату</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">NewClient</span>(
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">hub</span>, <span style="color:#a6e22e">conn</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">userID</span>.(<span style="color:#66d9ef">string</span>),
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">GetString</span>(<span style="color:#e6db74">"username"</span>),
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">chartID</span>,
|
||||
</span></span><span style="display:flex;"><span> )
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">hub</span>.<span style="color:#a6e22e">Subscribe</span>(<span style="color:#a6e22e">chartID</span>, <span style="color:#a6e22e">client</span>)
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// 5. Запустить pump'ы</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">go</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">WritePump</span>()
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">go</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">ReadPump</span>()
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><h3 id="34-регистрация-маршрута">3.4. Регистрация маршрута</h3>
|
||||
<p>В <code>cmd/server/main.go</code> или где регистрируются routes:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// После создания hub</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wsHub</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">NewHub</span>()
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// WS endpoint</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wsHandler</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">handlers</span>.<span style="color:#a6e22e">NewWSHandler</span>(<span style="color:#a6e22e">wsHub</span>, <span style="color:#a6e22e">ganttMW</span>)
|
||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">api</span>.<span style="color:#a6e22e">GET</span>(<span style="color:#e6db74">"/ws/gantt/:chartId"</span>, <span style="color:#a6e22e">authMW</span>.<span style="color:#a6e22e">RequireAuth</span>(), <span style="color:#a6e22e">wsHandler</span>.<span style="color:#a6e22e">HandleGanttWS</span>)
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Передать hub в ganttHandler для broadcast'ов</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">ganttHandler</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">handlers</span>.<span style="color:#a6e22e">NewGanttHandler</span>(<span style="color:#a6e22e">ganttService</span>, <span style="color:#a6e22e">sectionService</span>, <span style="color:#a6e22e">taskService</span>, <span style="color:#a6e22e">blockService</span>, <span style="color:#a6e22e">wsHub</span>)
|
||||
</span></span></code></pre></div><h3 id="35-интеграция-broadcast-в-handlers">3.5. Интеграция broadcast в handlers</h3>
|
||||
<p>В каждый write-handler — одна строка после успешной операции:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// Пример: после успешного UpdateTask</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">hub</span>.<span style="color:#a6e22e">Broadcast</span>(<span style="color:#a6e22e">chartID</span>, <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">Event</span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span>: <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">EventTaskUpdated</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ChartID</span>: <span style="color:#a6e22e">chartID</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UserID</span>: <span style="color:#a6e22e">userID</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Payload</span>: <span style="color:#a6e22e">updatedTask</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Timestamp</span>: <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>(),
|
||||
</span></span><span style="display:flex;"><span>})
|
||||
</span></span></code></pre></div><p>Точки интеграции в <code>gantt_handler.go</code>:</p>
|
||||
<ul>
|
||||
<li><code>CreateGanttChart</code> → не нужен (пользователь только что создал, он один)</li>
|
||||
<li><code>UpdateGanttChart</code> / <code>ArchiveGanttChart</code> → <code>chart_archived</code></li>
|
||||
<li><code>CreateSection</code> → <code>section_created</code></li>
|
||||
<li><code>UpdateSection</code> → <code>section_updated</code></li>
|
||||
<li><code>DeleteSection</code> → <code>section_deleted</code></li>
|
||||
<li><code>ReorderSections</code> → <code>sections_reordered</code></li>
|
||||
<li><code>CreateTask</code> → <code>task_created</code></li>
|
||||
<li><code>UpdateTask</code> → <code>task_updated</code></li>
|
||||
<li><code>DeleteTask</code> → <code>task_deleted</code></li>
|
||||
</ul>
|
||||
<h3 id="36-интеграция-событий-безопасности">3.6. Интеграция событий безопасности</h3>
|
||||
<p>В соответствующих service-методах:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// При исключении из space (SpaceService.RemoveMember)</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wsHub</span>.<span style="color:#a6e22e">SendToUser</span>(<span style="color:#a6e22e">removedUserID</span>, <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">Event</span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span>: <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">EventAccessRevoked</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Payload</span>: <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">"spaceId"</span>: <span style="color:#a6e22e">spaceID</span>},
|
||||
</span></span><span style="display:flex;"><span>})
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// При архивации ганта</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wsHub</span>.<span style="color:#a6e22e">Broadcast</span>(<span style="color:#a6e22e">chartID</span>, <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">Event</span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span>: <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">EventChartArchived</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ChartID</span>: <span style="color:#a6e22e">chartID</span>,
|
||||
</span></span><span style="display:flex;"><span>})
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// При изменении роли</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wsHub</span>.<span style="color:#a6e22e">SendToUser</span>(<span style="color:#a6e22e">userID</span>, <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">Event</span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span>: <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">EventRoleChanged</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Payload</span>: <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">"role"</span>: <span style="color:#a6e22e">newRole</span>},
|
||||
</span></span><span style="display:flex;"><span>})
|
||||
</span></span></code></pre></div><hr>
|
||||
<h2 id="4-frontend-angular">4. Frontend (Angular)</h2>
|
||||
<h3 id="41-websocket-сервис--gantt-wsservicets">4.1. WebSocket сервис — <code>gantt-ws.service.ts</code></h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#75715e">// src/app/pages/gantt/services/gantt-ws.service.ts
|
||||
</span></span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">Injectable</span>, <span style="color:#a6e22e">inject</span>, <span style="color:#a6e22e">OnDestroy</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">'@angular/core'</span>;
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">Subject</span>, <span style="color:#a6e22e">Observable</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">'rxjs'</span>;
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">environment</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">'../../../environments/environment'</span>;
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">SpaceStorageService</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">'../../../core/services/space-storage.service'</span>;
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">AuthService</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">'../../../core/auth/services/auth.service'</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">WSEvent</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">type</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">chartId?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">userId?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">payload?</span>: <span style="color:#66d9ef">any</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">timestamp?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">@Injectable</span>({ <span style="color:#a6e22e">providedIn</span><span style="color:#f92672">:</span> <span style="color:#e6db74">'root'</span> })
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">GanttWSService</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">OnDestroy</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">spaceStorage</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">inject</span>(<span style="color:#a6e22e">SpaceStorageService</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">auth</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">inject</span>(<span style="color:#a6e22e">AuthService</span>);
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">socket</span>: <span style="color:#66d9ef">WebSocket</span> <span style="color:#f92672">|</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">eventSubject</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Subject</span><<span style="color:#f92672">WSEvent</span>>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">reconnectTimer</span>: <span style="color:#66d9ef">any</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">chartId</span>: <span style="color:#66d9ef">string</span> <span style="color:#f92672">|</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">/** Observable для подписки на события */</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">get</span> <span style="color:#a6e22e">events$</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">Observable</span><<span style="color:#f92672">WSEvent</span>> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">eventSubject</span>.<span style="color:#a6e22e">asObservable</span>();
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">/** Подключиться к комнате ганта */</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">connect</span>(<span style="color:#a6e22e">chartId</span>: <span style="color:#66d9ef">string</span>)<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">disconnect</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">chartId</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">chartId</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">token</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">auth</span>.<span style="color:#a6e22e">getAccessToken</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">wsUrl</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">buildWSUrl</span>(<span style="color:#a6e22e">chartId</span>, <span style="color:#a6e22e">token</span>);
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">WebSocket</span>(<span style="color:#a6e22e">wsUrl</span>);
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span>.<span style="color:#a6e22e">onopen</span> <span style="color:#f92672">=</span> () <span style="color:#f92672">=></span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">`[WS] Connected to gantt </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">chartId</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>);
|
||||
</span></span><span style="display:flex;"><span> };
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span>.<span style="color:#a6e22e">onmessage</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">msg</span>) <span style="color:#f92672">=></span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">try</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">event</span>: <span style="color:#66d9ef">WSEvent</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">JSON</span>.<span style="color:#a6e22e">parse</span>(<span style="color:#a6e22e">msg</span>.<span style="color:#a6e22e">data</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">eventSubject</span>.<span style="color:#a6e22e">next</span>(<span style="color:#a6e22e">event</span>);
|
||||
</span></span><span style="display:flex;"><span> } <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">e</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">error</span>(<span style="color:#e6db74">'[WS] Parse error'</span>, <span style="color:#a6e22e">e</span>);
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> };
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span>.<span style="color:#a6e22e">onclose</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">event</span>) <span style="color:#f92672">=></span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">`[WS] Closed: </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">code</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">code</span> <span style="color:#f92672">!==</span> <span style="color:#ae81ff">1000</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">scheduleReconnect</span>();
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> };
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span>.<span style="color:#a6e22e">onerror</span> <span style="color:#f92672">=</span> () <span style="color:#f92672">=></span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span><span style="color:#f92672">?</span>.<span style="color:#a6e22e">close</span>();
|
||||
</span></span><span style="display:flex;"><span> };
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">/** Отключиться */</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">disconnect</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">clearTimeout</span>(<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">reconnectTimer</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span>.<span style="color:#a6e22e">close</span>(<span style="color:#ae81ff">1000</span>, <span style="color:#e6db74">'manual'</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">chartId</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ngOnDestroy</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">disconnect</span>();
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">buildWSUrl</span>(<span style="color:#a6e22e">chartId</span>: <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">token</span>: <span style="color:#66d9ef">string</span>)<span style="color:#f92672">:</span> <span style="color:#66d9ef">string</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">base</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">environment</span>.<span style="color:#a6e22e">wsUrl</span> <span style="color:#f92672">||</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">location</span>.<span style="color:#a6e22e">protocol</span>.<span style="color:#a6e22e">replace</span>(<span style="color:#e6db74">'http'</span>, <span style="color:#e6db74">'ws'</span>) <span style="color:#f92672">+</span> <span style="color:#e6db74">'//'</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">location</span>.<span style="color:#a6e22e">host</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#e6db74">`</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">base</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/api/v1/ws/gantt/</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">chartId</span><span style="color:#e6db74">}</span><span style="color:#e6db74">?token=</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">token</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>;
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">scheduleReconnect</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">chartId</span>) <span style="color:#66d9ef">return</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">reconnectTimer</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">setTimeout</span>(() <span style="color:#f92672">=></span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">'[WS] Reconnecting...'</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">chartId</span>) <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">connect</span>(<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">chartId</span>);
|
||||
</span></span><span style="display:flex;"><span> }, <span style="color:#ae81ff">3000</span>);
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><h3 id="42-интеграция-в-gantt-chart-canvas-компонент">4.2. Интеграция в gantt-chart-canvas компонент</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#75715e">// В gantt-chart-canvas.component.ts
|
||||
</span></span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">GanttChartCanvasComponent</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">OnInit</span>, <span style="color:#a6e22e">OnDestroy</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">ws</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">inject</span>(<span style="color:#a6e22e">GanttWSService</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">onlineUsers</span>: <span style="color:#66d9ef">Set</span><<span style="color:#f92672">string</span>> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Set</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">wsSubscription</span>: <span style="color:#66d9ef">Subscription</span> <span style="color:#f92672">|</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ngOnInit</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Подключиться к WS при загрузке ганта
|
||||
</span></span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">connect</span>(<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">chartId</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">wsSubscription</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">events$</span>.<span style="color:#a6e22e">subscribe</span>(<span style="color:#a6e22e">event</span> <span style="color:#f92672">=></span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">handleWSEvent</span>(<span style="color:#a6e22e">event</span>);
|
||||
</span></span><span style="display:flex;"><span> });
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ngOnDestroy</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">wsSubscription</span><span style="color:#f92672">?</span>.<span style="color:#a6e22e">unsubscribe</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">disconnect</span>();
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">handleWSEvent</span>(<span style="color:#a6e22e">event</span>: <span style="color:#66d9ef">WSEvent</span>)<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Если это событие от текущего пользователя — пропускаем
|
||||
</span></span></span><span style="display:flex;"><span> <span style="color:#75715e">// (мы уже обновили UI оптимистично или после HTTP-ответа)
|
||||
</span></span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">userId</span> <span style="color:#f92672">===</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">currentUserId</span>) <span style="color:#66d9ef">return</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">switch</span> (<span style="color:#a6e22e">event</span>.<span style="color:#66d9ef">type</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'section_created'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">addSectionLocal</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">redrawCanvas</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'section_updated'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">updateSectionLocal</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">redrawCanvas</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'section_deleted'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">removeSectionLocal</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>.<span style="color:#a6e22e">id</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">redrawCanvas</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'sections_reordered'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">reorderSectionsLocal</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>.<span style="color:#a6e22e">ordered_ids</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">redrawCanvas</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'task_created'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">addTaskLocal</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">redrawCanvas</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'task_updated'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">updateTaskLocal</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">redrawCanvas</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'task_deleted'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">removeTaskLocal</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>.<span style="color:#a6e22e">id</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">redrawCanvas</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'user_joined'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">onlineUsers</span>.<span style="color:#a6e22e">add</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">userId</span><span style="color:#f92672">!</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">showOnlineIndicator</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'user_left'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">onlineUsers</span>.<span style="color:#66d9ef">delete</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">userId</span><span style="color:#f92672">!</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">showOnlineIndicator</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// === Безопасность ===
|
||||
</span></span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'access_revoked'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">router</span>.<span style="color:#a6e22e">navigate</span>([<span style="color:#e6db74">'/gantt'</span>], {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">state</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">alert</span><span style="color:#f92672">:</span> <span style="color:#e6db74">'Доступ к пространству был отозван'</span> }
|
||||
</span></span><span style="display:flex;"><span> });
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'chart_archived'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">router</span>.<span style="color:#a6e22e">navigate</span>([<span style="color:#e6db74">'/gantt'</span>], {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">state</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">alert</span><span style="color:#f92672">:</span> <span style="color:#e6db74">'Диаграмма была заархивирована'</span> }
|
||||
</span></span><span style="display:flex;"><span> });
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'role_changed'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">currentRole</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>.<span style="color:#a6e22e">role</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">updateEditableState</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'token_expired'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">router</span>.<span style="color:#a6e22e">navigate</span>([<span style="color:#e6db74">'/auth/login'</span>]);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">redrawCanvas</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Перерисовать Konva canvas с обновлёнными данными
|
||||
</span></span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><h3 id="43-индикатор-онлайн-пользователей">4.3. Индикатор онлайн-пользователей</h3>
|
||||
<p>Добавить в шаблон canvas-компонента:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-html" data-lang="html"><span style="display:flex;"><span><span style="color:#75715e"><!-- Онлайн-индикатор --></span>
|
||||
</span></span><span style="display:flex;"><span><<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">"online-indicator"</span> <span style="color:#960050;background-color:#1e0010">*</span><span style="color:#a6e22e">ngIf</span><span style="color:#f92672">=</span><span style="color:#e6db74">"onlineUsers.size > 0"</span>>
|
||||
</span></span><span style="display:flex;"><span> <<span style="color:#f92672">span</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">"pulse-dot"</span>></<span style="color:#f92672">span</span>>
|
||||
</span></span><span style="display:flex;"><span> {{ onlineUsers.size }} онлайн
|
||||
</span></span><span style="display:flex;"><span></<span style="color:#f92672">div</span>>
|
||||
</span></span></code></pre></div><hr>
|
||||
<h2 id="5-безопасность">5. Безопасность</h2>
|
||||
<h3 id="51-авторизация-при-подключении">5.1. Авторизация при подключении</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Этап</th>
|
||||
<th>Проверка</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>WS Handshake</td>
|
||||
<td>JWT в query-параметре <code>?token=...</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Подписка на комнату</td>
|
||||
<td>Проверка membership в space</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Каждое broadcast-событие</td>
|
||||
<td>Hub знает userId каждого клиента</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="52-сценарии-отзыва-доступа">5.2. Сценарии отзыва доступа</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Событие</th>
|
||||
<th>Триггер</th>
|
||||
<th>Действие на клиенте</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>access_revoked</code></td>
|
||||
<td>Исключение из space</td>
|
||||
<td>Редирект на список гантов</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>chart_archived</code></td>
|
||||
<td>Архивация ганта</td>
|
||||
<td>Редирект на список гантов</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>role_changed</code></td>
|
||||
<td>Смена роли (editor→viewer)</td>
|
||||
<td>Отключить редактирование</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>token_expired</code></td>
|
||||
<td>Токен протух</td>
|
||||
<td>Редирект на логин</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="53-защита-от-злоупотреблений">5.3. Защита от злоупотреблений</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Угроза</th>
|
||||
<th>Мера</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Спам WS-сообщений</td>
|
||||
<td>Rate limit: 30 msg/min на клиента, <code>ReadLimit</code> в gorilla/websocket</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Подключение без прав</td>
|
||||
<td>Проверка space membership при handshake</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Истёкший токен</td>
|
||||
<td>Ping/pong + периодическая проверка JWT (раз в 5 мин)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>XSS через WS</td>
|
||||
<td>Payload — JSON, sanitize имён на frontend</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Мутации через WS</td>
|
||||
<td>WS <strong>только для чтения</strong> (уведомления). Мутации — через HTTP API с полной валидацией</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="54-nginx-конфигурация">5.4. Nginx конфигурация</h3>
|
||||
<p>В конфиг <code>sand.a2v.space</code> добавить:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#75715e"># WebSocket проксирование
|
||||
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">location</span> <span style="color:#e6db74">/api/v1/ws/</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8080</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_http_version</span> <span style="color:#ae81ff">1</span><span style="color:#e6db74">.1</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Upgrade</span> $http_upgrade;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Connection</span> <span style="color:#e6db74">"upgrade"</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Real-IP</span> $remote_addr;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $proxy_add_x_forwarded_for;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> $scheme;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_read_timeout</span> <span style="color:#e6db74">86400s</span>; <span style="color:#75715e"># 24h для long-lived WS
|
||||
</span></span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_send_timeout</span> <span style="color:#e6db74">86400s</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><hr>
|
||||
<h2 id="6-оценка-трудозатрат">6. Оценка трудозатрат</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Компонент</th>
|
||||
<th>Строки кода</th>
|
||||
<th>Время</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>internal/ws/</code> (hub, client, message)</td>
|
||||
<td>~200</td>
|
||||
<td>2-3 часа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ws_handler.go</code> (endpoint)</td>
|
||||
<td>~60</td>
|
||||
<td>30 мин</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Интеграция broadcast в handlers</td>
|
||||
<td>~20 (по строке на endpoint)</td>
|
||||
<td>30 мин</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>События безопасности в services</td>
|
||||
<td>~30</td>
|
||||
<td>30 мин</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>gantt-ws.service.ts</code> (Angular)</td>
|
||||
<td>~80</td>
|
||||
<td>1 час</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Интеграция в canvas-компонент</td>
|
||||
<td>~80</td>
|
||||
<td>1-2 часа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Индикатор онлайн</td>
|
||||
<td>~30</td>
|
||||
<td>30 мин</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Nginx конфигурация</td>
|
||||
<td>~10</td>
|
||||
<td>10 мин</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Тестирование</td>
|
||||
<td>—</td>
|
||||
<td>2-3 часа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Итого</strong></td>
|
||||
<td><strong>~510 строк</strong></td>
|
||||
<td><strong>~1-2 дня</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<h2 id="7-порядок-реализации">7. Порядок реализации</h2>
|
||||
<ol>
|
||||
<li><strong>Backend: <code>internal/ws/</code></strong> — hub, client, message types</li>
|
||||
<li><strong>Backend: WS endpoint</strong> — handler + route</li>
|
||||
<li><strong>Backend: Broadcast в handlers</strong> — интеграция в существующие CRUD-эндпоинты</li>
|
||||
<li><strong>Backend: События безопасности</strong> — access_revoked, chart_archived, role_changed</li>
|
||||
<li><strong>Nginx: WS location</strong> — проксирование WebSocket</li>
|
||||
<li><strong>Frontend: WS service</strong> — подключение, реконнект, парсинг событий</li>
|
||||
<li><strong>Frontend: Интеграция в canvas</strong> — обработка событий, обновление Konva</li>
|
||||
<li><strong>Frontend: Индикатор онлайн</strong> — визуальная обратная связь</li>
|
||||
<li><strong>Тестирование</strong> — многопользовательский сценарий, отзыв прав, обрывы связи</li>
|
||||
</ol>
|
||||
<hr>
|
||||
<h2 id="8-дальнейшие-улучшения-после-mvp">8. Дальнейшие улучшения (после MVP)</h2>
|
||||
<ul>
|
||||
<li><strong>Cursor других пользователей</strong> — показывать позицию курсора на таймлайне</li>
|
||||
<li><strong>Блокировка элемента</strong> — «кто-то редактирует эту задачу» (optimistic locking через WS)</li>
|
||||
<li><strong>Чат в ганте</strong> — комментарии к задачам прямо на канвасе</li>
|
||||
<li><strong>History/undo</strong> — откат изменений с визуальной историей</li>
|
||||
<li><strong>Уведомления</strong> — push-уведомления при @mention в комментариях к задачам</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -111,6 +117,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Бинарный файл в репозитории</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/personal-site/binary-files-in-repo.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/personal-site\/binary-files-in-repo.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<h2 id="проблема">Проблема</h2>
|
||||
<p>В корне репозитория лежат два файла, которые не должны быть в git:</p>
|
||||
<ul>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -111,6 +117,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Copyright без 2026 года</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/personal-site/copyright-outdated.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/personal-site\/copyright-outdated.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<h2 id="проблема">Проблема</h2>
|
||||
<p>В <code>params.toml</code>:</p>
|
||||
<pre tabindex="0"><code>copyRight = "Copyright (c) 2020-2025 Антон Агальцов"
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -111,6 +117,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Digest не обновлялся с февраля 2025</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/personal-site/digest-stale.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/personal-site\/digest-stale.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<h2 id="проблема">Проблема</h2>
|
||||
<p>Последний дайджест — <strong>февраль 2025</strong>. С тех пор 12+ месяцев без новых выпусков.</p>
|
||||
<h3 id="что-есть">Что есть</h3>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../creds/">Креды</a></li>
|
||||
@@ -115,21 +121,21 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../personal-site/copyright-outdated/">Copyright без 2026 года</a></li>
|
||||
|
||||
<li><a href="../personal-site/digest-stale/">Digest не обновлялся с февраля 2025</a></li>
|
||||
|
||||
<li><a href="../personal-site/outdated-doks-theme/">Hugo Doks 0.5 — сильно устарел</a></li>
|
||||
|
||||
<li><a href="../personal-site/twitter-handle-mismatch/">Twitter handle не соответствует имени</a></li>
|
||||
|
||||
<li><a href="../personal-site/no-analytics/">Yandex Metrika отключена</a></li>
|
||||
|
||||
<li><a href="../personal-site/binary-files-in-repo/">Бинарный файл в репозитории</a></li>
|
||||
<li><a href="../personal-site/netlify-node-version/">Устаревший Node.js 16 в Netlify</a></li>
|
||||
|
||||
<li><a href="../personal-site/no-node-modules/">Отсутствуют node_modules — нужна установка</a></li>
|
||||
|
||||
<li><a href="../personal-site/netlify-node-version/">Устаревший Node.js 16 в Netlify</a></li>
|
||||
<li><a href="../personal-site/binary-files-in-repo/">Бинарный файл в репозитории</a></li>
|
||||
|
||||
<li><a href="../personal-site/no-analytics/">Yandex Metrika отключена</a></li>
|
||||
|
||||
<li><a href="../personal-site/twitter-handle-mismatch/">Twitter handle не соответствует имени</a></li>
|
||||
|
||||
<li><a href="../personal-site/outdated-doks-theme/">Hugo Doks 0.5 — сильно устарел</a></li>
|
||||
|
||||
<li><a href="../personal-site/digest-stale/">Digest не обновлялся с февраля 2025</a></li>
|
||||
|
||||
<li><a href="../personal-site/copyright-outdated/">Copyright без 2026 года</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -111,6 +117,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Устаревший Node.js 16 в Netlify</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/personal-site/netlify-node-version.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/personal-site\/netlify-node-version.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<h1 id="устаревший-nodejs-16-в-netlify">Устаревший Node.js 16 в Netlify</h1>
|
||||
<p><strong>Файл:</strong> <code>netlify.toml:5</code></p>
|
||||
<pre tabindex="0"><code>NODE_VERSION = "16.16.0"
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -111,6 +117,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Yandex Metrika отключена</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/personal-site/no-analytics.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/personal-site\/no-analytics.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<h2 id="проблема">Проблема</h2>
|
||||
<p>В <code>params.toml</code>: <code>yandexMetrika = false</code></p>
|
||||
<p>Аналитика с сайта не собирается. Непонятно, есть ли вообще посетители и какие страницы популярны.</p>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -111,6 +117,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Отсутствуют node_modules — нужна установка</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/personal-site/no-node-modules.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/personal-site\/no-node-modules.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<h2 id="проблема">Проблема</h2>
|
||||
<p><code>node_modules/</code> отсутствует — проект нельзя собрать без <code>npm install</code>.</p>
|
||||
<h3 id="риск">Риск</h3>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -111,6 +117,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Hugo Doks 0.5 — сильно устарел</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/personal-site/outdated-doks-theme.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/personal-site\/outdated-doks-theme.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<h2 id="проблема">Проблема</h2>
|
||||
<p>Проект использует тему <strong>Hyas/Doks v0.5.0</strong> (package.json). Актуальная версия Doks — <strong>1.x</strong> с полной переработкой архитектуры.</p>
|
||||
<h3 id="что-не-так">Что не так</h3>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -111,6 +117,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Twitter handle не соответствует имени</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/personal-site/twitter-handle-mismatch.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/personal-site\/twitter-handle-mismatch.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<h2 id="проблема">Проблема</h2>
|
||||
<p>В <code>params.toml</code> указан Twitter: <code>@AntonTotoshkin</code></p>
|
||||
<p>Но владелец сайта — <strong>Антон Агальцов</strong>. Ники не совпадают.</p>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -103,6 +107,17 @@
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Завершить Eversolo Companion - этапы 3-9</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/plans/eversolo-companion-phases-3-9.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/plans\/eversolo-companion-phases-3-9.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<h1>Завершить Eversolo Companion - этапы 3-9</h1>
|
||||
<span class="status-badge h5">
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -103,6 +107,17 @@
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Реализовать управление Eversolo из OpenClaw</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/plans/eversolo-control.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/plans\/eversolo-control.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<h1>Реализовать управление Eversolo из OpenClaw</h1>
|
||||
<span class="status-badge h5">
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -103,6 +107,17 @@
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Настроить интеграцию Ollama с OpenClaw</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/plans/ollama-openclaw.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/plans\/ollama-openclaw.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<h1>Настроить интеграцию Ollama с OpenClaw</h1>
|
||||
<span class="status-badge h5">
|
||||
|
||||
+74
-26
@@ -2,11 +2,83 @@
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
||||
<url>
|
||||
<loc>/leadera/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/personal-site/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/guides/leadera-audit/</loc>
|
||||
<loc>/leadera/leadera-audit/</loc>
|
||||
<lastmod>2026-03-30T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/leadera/leadera-websocket-gantt/</loc>
|
||||
<lastmod>2026-04-07T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/leadera/leadera-gantt-excel-export/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/leadera/leadera-gantt-filters/</loc>
|
||||
<lastmod>2026-04-07T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/leadera/leadera-gantt-public-access/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/excel/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/export/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/gantt/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/leadera/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/public-access/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/sharing/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/angular/</loc>
|
||||
<lastmod>2026-04-08T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/api/</loc>
|
||||
<lastmod>2026-04-08T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/go/</loc>
|
||||
<lastmod>2026-04-08T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/leadera/leadera-milestones/</loc>
|
||||
<lastmod>2026-04-08T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/ux/</loc>
|
||||
<lastmod>2026-04-08T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/architecture/</loc>
|
||||
<lastmod>2026-04-07T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/frontend/</loc>
|
||||
<lastmod>2026-04-07T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/realtime/</loc>
|
||||
<lastmod>2026-04-07T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/requirements/</loc>
|
||||
<lastmod>2026-04-07T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/websocket/</loc>
|
||||
<lastmod>2026-04-07T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/leadera/leadera-section-constraints/</loc>
|
||||
<lastmod>2026-04-06T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/diary/2026-03-31/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
@@ -25,12 +97,6 @@
|
||||
</url><url>
|
||||
<loc>/personal-site/digest-stale/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/gantt/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/github/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
@@ -43,9 +109,6 @@
|
||||
</url><url>
|
||||
<loc>/tags/infrastructure/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/leadera/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/opencode/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
@@ -58,9 +121,6 @@
|
||||
</url><url>
|
||||
<loc>/tags/system-utils/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/personal-site/twitter-handle-mismatch/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
@@ -79,12 +139,6 @@
|
||||
</url><url>
|
||||
<loc>/diary/2026-03-30/</loc>
|
||||
<lastmod>2026-03-30T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/architecture/</loc>
|
||||
<lastmod>2026-03-30T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/guides/</loc>
|
||||
<lastmod>2026-03-30T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/mtproxy/</loc>
|
||||
<lastmod>2026-03-30T00:00:00+00:00</lastmod>
|
||||
@@ -109,12 +163,6 @@
|
||||
</url><url>
|
||||
<loc>/tags/amnezia/</loc>
|
||||
<lastmod>2026-03-29T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/angular/</loc>
|
||||
<lastmod>2026-03-29T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/api/</loc>
|
||||
<lastmod>2026-03-29T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/authentication/</loc>
|
||||
<lastmod>2026-03-29T00:00:00+00:00</lastmod>
|
||||
@@ -152,7 +200,7 @@
|
||||
<loc>/kb/eversolo-api/</loc>
|
||||
<lastmod>2026-03-29T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/go/</loc>
|
||||
<loc>/guides/</loc>
|
||||
<lastmod>2026-03-29T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/identity/</loc>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,11 +121,15 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
<li><a href="../../leadera/leadera-milestones/">Leadera: Вехи (Milestones) на диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-section-constraints/">Leadera: Ограничения секций в диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../guides/sand-a2v-space/">Подключение sand.a2v.space (Leadera Project)</a></li>
|
||||
|
||||
<li><a href="../../plans/eversolo-companion-phases-3-9/">Завершить Eversolo Companion - этапы 3-9</a></li>
|
||||
|
||||
<li><a href="../../guides/sand-a2v-space/">Подключение sand.a2v.space (Leadera Project)</a></li>
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -6,8 +6,22 @@
|
||||
<description>Recent content in Angular on docs.a2v.space</description>
|
||||
<generator>Hugo</generator>
|
||||
<language>ru-ru</language>
|
||||
<lastBuildDate>Sun, 29 Mar 2026 00:00:00 +0000</lastBuildDate>
|
||||
<lastBuildDate>Wed, 08 Apr 2026 00:00:00 +0000</lastBuildDate>
|
||||
<atom:link href="/tags/angular/index.xml" rel="self" type="application/rss+xml" />
|
||||
<item>
|
||||
<title>Leadera: Вехи (Milestones) на диаграмме Ганта</title>
|
||||
<link>/leadera/leadera-milestones/</link>
|
||||
<pubDate>Wed, 08 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-milestones/</guid>
|
||||
<description><h2 id="бизнес-требования">Бизнес-требования</h2>
<h3 id="br-1-сущность-веха-milestone">BR-1: Сущность «Веха» (Milestone)</h3>
<p>Веха — это маркер на таймлайне диаграммы Ганта, обозначающий важную дату в проекте. В отличие от задач, веха не имеет длительности и привязана к конкретной дате.</p>
<p><strong>Поля вехи:</strong></p>
<table>
 <thead>
 <tr>
 <th>Поле</th>
 <th>Тип</th>
 <th>Описание</th>
 </tr>
 </thead>
 <tbody>
 <tr>
 <td>id</td>
 <td>UUID</td>
 <td>Идентификатор</td>
 </tr>
 <tr>
 <td>gantt_chart_id</td>
 <td>UUID</td>
 <td>Принадлежность к диаграмме (не к секции!)</td>
 </tr>
 <tr>
 <td>title</td>
 <td>string</td>
 <td>Название вехи (обязательное, 1–255 символов)</td>
 </tr>
 <tr>
 <td>date</td>
 <td>date</td>
 <td>Дата вехи (без времени, обязательная)</td>
 </tr>
 <tr>
 <td>color</td>
 <td>string</td>
 <td>Цвет маркера (HEX, по умолчанию <code>#dc3545</code> — красный)</td>
 </tr>
 <tr>
 <td>created_by</td>
 <td>UUID</td>
 <td>Кто создал (не отображается в UI, хранится для аудита)</td>
 </tr>
 <tr>
 <td>updated_by</td>
 <td>UUID</td>
 <td>Кто обновил (не отображается в UI)</td>
 </tr>
 <tr>
 <td>created_at</td>
 <td>timestamp</td>
 <td>Дата создания</td>
 </tr>
 <tr>
 <td>updated_at</td>
 <td>timestamp</td>
 <td>Дата обновления</td>
 </tr>
 <tr>
 <td>deleted_at</td>
 <td>timestamp</td>
 <td>Soft delete</td>
 </tr>
 </tbody>
</table>
<p><strong>Права доступа:</strong> Создание, редактирование и удаление вех доступно участникам с правами выше «только чтение» (аналогично задачам).</p></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Leadera: Ограничения секций в диаграмме Ганта</title>
|
||||
<link>/leadera/leadera-section-constraints/</link>
|
||||
<pubDate>Mon, 06 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-section-constraints/</guid>
|
||||
<description><h2 id="бизнес-требования">Бизнес-требования</h2>
<h3 id="br-1-лимит-одновременных-задач-concurrency-limit">BR-1: Лимит одновременных задач (Concurrency Limit)</h3>
<p>У каждой секции появляется параметр <strong>max_concurrent_tasks</strong> (целое число ≥ 0, по умолчанию 0).</p>
<p><strong>Поведение:</strong></p>
<ul>
<li>Если <code>max_concurrent_tasks = 3</code> — в любой данный день в этой секции не может быть больше 3 активных задач одновременно</li>
<li>Если <code>max_concurrent_tasks = 0</code> — ограничение отключено, лимита нет</li>
<li>Пересечение считается по дате: задача занимает дни с <code>start_date</code> по <code>end_date</code> включительно. Две задачи «пересекаются», если у них есть хотя бы один общий день</li>
<li>Ограничение работает <strong>только в рамках одной секции</strong>. Задачи из разных секций не влияют друг на друга</li>
</ul>
<p><strong>Срабатывание при:</strong></p></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>29 марта 2026</title>
|
||||
<link>/diary/2026-03-29/</link>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,10 +121,16 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../kb/eversolo-api/">Eversolo T8 API</a></li>
|
||||
<li><a href="../../leadera/leadera-milestones/">Leadera: Вехи (Milestones) на диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-gantt-filters/">Leadera — Фильтры и реорганизация списка диаграмм Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-section-constraints/">Leadera: Ограничения секций в диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../plans/eversolo-control/">Реализовать управление Eversolo из OpenClaw</a></li>
|
||||
|
||||
<li><a href="../../kb/eversolo-api/">Eversolo T8 API</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,8 +6,29 @@
|
||||
<description>Recent content in Api on docs.a2v.space</description>
|
||||
<generator>Hugo</generator>
|
||||
<language>ru-ru</language>
|
||||
<lastBuildDate>Sun, 29 Mar 2026 00:00:00 +0000</lastBuildDate>
|
||||
<lastBuildDate>Wed, 08 Apr 2026 00:00:00 +0000</lastBuildDate>
|
||||
<atom:link href="/tags/api/index.xml" rel="self" type="application/rss+xml" />
|
||||
<item>
|
||||
<title>Leadera — Фильтры и реорганизация списка диаграмм Ганта</title>
|
||||
<link>/leadera/leadera-gantt-filters/</link>
|
||||
<pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-gantt-filters/</guid>
|
||||
<description><h1 id="фильтры-и-реорганизация-списка-диаграмм-ганта">Фильтры и реорганизация списка диаграмм Ганта</h1>
<p><strong>Дата:</strong> 07.04.2026
<strong>Статус:</strong> Требования
<strong>Проект:</strong> Leadera (dev — sand.a2v.space)</p>
<hr>
<h2 id="1-постановка-задачи">1. Постановка задачи</h2>
<p>Список диаграмм Ганта должен показывать <strong>все доступные пользователю диаграммы</strong> из всех пространств (spaces), с возможностью фильтрации и поиска. Сейчас список привязан к одному пространству — нужно убрать эту зависимость и добавить удобный toolbar.</p>
<hr>
<h2 id="2-изменения-api">2. Изменения API</h2>
<h3 id="21-новый-endpoint-списка-диаграмм">2.1. Новый endpoint списка диаграмм</h3>
<p><strong>Было:</strong></p>
<pre tabindex="0"><code>GET /api/v1/spaces/{spaceId}/gantt-charts?page=1&amp;per_page=10
</code></pre><p><strong>Стало:</strong></p>
<pre tabindex="0"><code>GET /api/v1/gantt-charts?page=1&amp;per_page=10
</code></pre><p>Пользователь видит все диаграммы из <strong>всех пространств</strong>, где он является подтверждённым участником (статус <code>accepted</code>).</p></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Leadera: Вехи (Milestones) на диаграмме Ганта</title>
|
||||
<link>/leadera/leadera-milestones/</link>
|
||||
<pubDate>Wed, 08 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-milestones/</guid>
|
||||
<description><h2 id="бизнес-требования">Бизнес-требования</h2>
<h3 id="br-1-сущность-веха-milestone">BR-1: Сущность «Веха» (Milestone)</h3>
<p>Веха — это маркер на таймлайне диаграммы Ганта, обозначающий важную дату в проекте. В отличие от задач, веха не имеет длительности и привязана к конкретной дате.</p>
<p><strong>Поля вехи:</strong></p>
<table>
 <thead>
 <tr>
 <th>Поле</th>
 <th>Тип</th>
 <th>Описание</th>
 </tr>
 </thead>
 <tbody>
 <tr>
 <td>id</td>
 <td>UUID</td>
 <td>Идентификатор</td>
 </tr>
 <tr>
 <td>gantt_chart_id</td>
 <td>UUID</td>
 <td>Принадлежность к диаграмме (не к секции!)</td>
 </tr>
 <tr>
 <td>title</td>
 <td>string</td>
 <td>Название вехи (обязательное, 1–255 символов)</td>
 </tr>
 <tr>
 <td>date</td>
 <td>date</td>
 <td>Дата вехи (без времени, обязательная)</td>
 </tr>
 <tr>
 <td>color</td>
 <td>string</td>
 <td>Цвет маркера (HEX, по умолчанию <code>#dc3545</code> — красный)</td>
 </tr>
 <tr>
 <td>created_by</td>
 <td>UUID</td>
 <td>Кто создал (не отображается в UI, хранится для аудита)</td>
 </tr>
 <tr>
 <td>updated_by</td>
 <td>UUID</td>
 <td>Кто обновил (не отображается в UI)</td>
 </tr>
 <tr>
 <td>created_at</td>
 <td>timestamp</td>
 <td>Дата создания</td>
 </tr>
 <tr>
 <td>updated_at</td>
 <td>timestamp</td>
 <td>Дата обновления</td>
 </tr>
 <tr>
 <td>deleted_at</td>
 <td>timestamp</td>
 <td>Soft delete</td>
 </tr>
 </tbody>
</table>
<p><strong>Права доступа:</strong> Создание, редактирование и удаление вех доступно участникам с правами выше «только чтение» (аналогично задачам).</p></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Leadera: Ограничения секций в диаграмме Ганта</title>
|
||||
<link>/leadera/leadera-section-constraints/</link>
|
||||
<pubDate>Mon, 06 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-section-constraints/</guid>
|
||||
<description><h2 id="бизнес-требования">Бизнес-требования</h2>
<h3 id="br-1-лимит-одновременных-задач-concurrency-limit">BR-1: Лимит одновременных задач (Concurrency Limit)</h3>
<p>У каждой секции появляется параметр <strong>max_concurrent_tasks</strong> (целое число ≥ 0, по умолчанию 0).</p>
<p><strong>Поведение:</strong></p>
<ul>
<li>Если <code>max_concurrent_tasks = 3</code> — в любой данный день в этой секции не может быть больше 3 активных задач одновременно</li>
<li>Если <code>max_concurrent_tasks = 0</code> — ограничение отключено, лимита нет</li>
<li>Пересечение считается по дате: задача занимает дни с <code>start_date</code> по <code>end_date</code> включительно. Две задачи «пересекаются», если у них есть хотя бы один общий день</li>
<li>Ограничение работает <strong>только в рамках одной секции</strong>. Задачи из разных секций не влияют друг на друга</li>
</ul>
<p><strong>Срабатывание при:</strong></p></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Eversolo T8 API</title>
|
||||
<link>/kb/eversolo-api/</link>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,7 +121,9 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../guides/leadera-audit/">Аудит Leadera — полный отчёт</a></li>
|
||||
<li><a href="../../leadera/leadera-websocket-gantt/">Leadera — WebSocket для диаграмм Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-audit/">Аудит Leadera — полный отчёт</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -6,14 +6,21 @@
|
||||
<description>Recent content in Architecture on docs.a2v.space</description>
|
||||
<generator>Hugo</generator>
|
||||
<language>ru-ru</language>
|
||||
<lastBuildDate>Mon, 30 Mar 2026 00:00:00 +0000</lastBuildDate>
|
||||
<lastBuildDate>Tue, 07 Apr 2026 00:00:00 +0000</lastBuildDate>
|
||||
<atom:link href="/tags/architecture/index.xml" rel="self" type="application/rss+xml" />
|
||||
<item>
|
||||
<title>Аудит Leadera — полный отчёт</title>
|
||||
<link>/guides/leadera-audit/</link>
|
||||
<link>/leadera/leadera-audit/</link>
|
||||
<pubDate>Mon, 30 Mar 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/guides/leadera-audit/</guid>
|
||||
<guid>/leadera/leadera-audit/</guid>
|
||||
<description><h1 id="аудит-проекта-leadera">Аудит проекта Leadera</h1>
<p><strong>Дата:</strong> 30.03.2026<br>
<strong>Версия:</strong> pre-production, миграции 1–5</p>
<hr>
<h2 id="1-обзор-проекта">1. Обзор проекта</h2>
<p>Leadera — SaaS-система управления IT-ландшафтом с диаграммами Ганта.</p>
<h3 id="стек">Стек</h3>
<table>
 <thead>
 <tr>
 <th>Компонент</th>
 <th>Технологии</th>
 </tr>
 </thead>
 <tbody>
 <tr>
 <td>Backend</td>
 <td>Go 1.21+, Gin, PostgreSQL 16, pgx/v5 + sqlx, JWT (HMAC-SHA256)</td>
 </tr>
 <tr>
 <td>Frontend</td>
 <td>Angular 21.1, Bootstrap 5, NgBootstrap, RxJS, Signals</td>
 </tr>
 <tr>
 <td>Инфраструктура</td>
 <td>Docker (Colima), pm2, nginx reverse proxy, VPS reg.ru</td>
 </tr>
 </tbody>
</table>
<h3 id="доменная-модель">Доменная модель</h3>
<pre tabindex="0"><code>User ──┬── Space (через space_members с role)
 │
 └── GanttChart (принадлежит Space)
 ├── GanttSection
 └── GanttTask
</code></pre><p><strong>Роли:</strong> admin, editor, viewer — назначаются <strong>на уровне Space</strong> через таблицу <code>space_members</code>.</p></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Leadera — WebSocket для диаграмм Ганта</title>
|
||||
<link>/leadera/leadera-websocket-gantt/</link>
|
||||
<pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-websocket-gantt/</guid>
|
||||
<description><h1 id="websocket-для-диаграмм-ганта--realtime-совместное-редактирование">WebSocket для диаграмм Ганта — realtime-совместное редактирование</h1>
<p><strong>Дата:</strong> 07.04.2026
<strong>Статус:</strong> Планирование
<strong>Проект:</strong> Leadera (dev — sand.a2v.space)</p>
<hr>
<h2 id="1-постановка-задачи">1. Постановка задачи</h2>
<p>Диаграммы Ганта должны обновляться в реальном времени при совместной работе: если два или более пользователей редактируют один Гант, изменения каждого мгновенно отображаются у остальных — без перезагрузки страницы и ручного обновления.</p>
<hr>
<h2 id="2-архитектура">2. Архитектура</h2>
<pre tabindex="0"><code>Angular frontend ←── WebSocket ──→ Go backend (Hub) ──→ PostgreSQL
 │ │
 │ HTTP (мутации) │ Broadcast
 └──→ REST API ──→ Service ──→ DB ──→ Hub ──→ все клиенты комнаты
</code></pre><p><strong>Ключевой принцип:</strong> мутации данных идут через HTTP (REST), WebSocket используется <strong>только для доставки уведомлений</strong>. Сервер — источник истины, клиент не может обойти проверки через WS.</p></description>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,25 +121,25 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../personal-site/">Личный сайт (agaltsovav.ru)</a></li>
|
||||
|
||||
<li><a href="../../guides/leadera-audit/">Аудит Leadera — полный отчёт</a></li>
|
||||
|
||||
<li><a href="../../personal-site/copyright-outdated/">Copyright без 2026 года</a></li>
|
||||
|
||||
<li><a href="../../personal-site/digest-stale/">Digest не обновлялся с февраля 2025</a></li>
|
||||
|
||||
<li><a href="../../personal-site/outdated-doks-theme/">Hugo Doks 0.5 — сильно устарел</a></li>
|
||||
|
||||
<li><a href="../../personal-site/twitter-handle-mismatch/">Twitter handle не соответствует имени</a></li>
|
||||
|
||||
<li><a href="../../personal-site/no-analytics/">Yandex Metrika отключена</a></li>
|
||||
|
||||
<li><a href="../../personal-site/binary-files-in-repo/">Бинарный файл в репозитории</a></li>
|
||||
<li><a href="../../personal-site/netlify-node-version/">Устаревший Node.js 16 в Netlify</a></li>
|
||||
|
||||
<li><a href="../../personal-site/no-node-modules/">Отсутствуют node_modules — нужна установка</a></li>
|
||||
|
||||
<li><a href="../../personal-site/netlify-node-version/">Устаревший Node.js 16 в Netlify</a></li>
|
||||
<li><a href="../../personal-site/binary-files-in-repo/">Бинарный файл в репозитории</a></li>
|
||||
|
||||
<li><a href="../../personal-site/no-analytics/">Yandex Metrika отключена</a></li>
|
||||
|
||||
<li><a href="../../personal-site/twitter-handle-mismatch/">Twitter handle не соответствует имени</a></li>
|
||||
|
||||
<li><a href="../../personal-site/outdated-doks-theme/">Hugo Doks 0.5 — сильно устарел</a></li>
|
||||
|
||||
<li><a href="../../personal-site/digest-stale/">Digest не обновлялся с февраля 2025</a></li>
|
||||
|
||||
<li><a href="../../personal-site/copyright-outdated/">Copyright без 2026 года</a></li>
|
||||
|
||||
<li><a href="../../personal-site/">Личный сайт (agaltsovav.ru)</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-audit/">Аудит Leadera — полный отчёт</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
</item>
|
||||
<item>
|
||||
<title>Аудит Leadera — полный отчёт</title>
|
||||
<link>/guides/leadera-audit/</link>
|
||||
<link>/leadera/leadera-audit/</link>
|
||||
<pubDate>Mon, 30 Mar 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/guides/leadera-audit/</guid>
|
||||
<guid>/leadera/leadera-audit/</guid>
|
||||
<description><h1 id="аудит-проекта-leadera">Аудит проекта Leadera</h1>
<p><strong>Дата:</strong> 30.03.2026<br>
<strong>Версия:</strong> pre-production, миграции 1–5</p>
<hr>
<h2 id="1-обзор-проекта">1. Обзор проекта</h2>
<p>Leadera — SaaS-система управления IT-ландшафтом с диаграммами Ганта.</p>
<h3 id="стек">Стек</h3>
<table>
 <thead>
 <tr>
 <th>Компонент</th>
 <th>Технологии</th>
 </tr>
 </thead>
 <tbody>
 <tr>
 <td>Backend</td>
 <td>Go 1.21+, Gin, PostgreSQL 16, pgx/v5 + sqlx, JWT (HMAC-SHA256)</td>
 </tr>
 <tr>
 <td>Frontend</td>
 <td>Angular 21.1, Bootstrap 5, NgBootstrap, RxJS, Signals</td>
 </tr>
 <tr>
 <td>Инфраструктура</td>
 <td>Docker (Colima), pm2, nginx reverse proxy, VPS reg.ru</td>
 </tr>
 </tbody>
</table>
<h3 id="доменная-модель">Доменная модель</h3>
<pre tabindex="0"><code>User ──┬── Space (через space_members с role)
 │
 └── GanttChart (принадлежит Space)
 ├── GanttSection
 └── GanttTask
</code></pre><p><strong>Роли:</strong> admin, editor, viewer — назначаются <strong>на уровне Space</strong> через таблицу <code>space_members</code>.</p></description>
|
||||
</item>
|
||||
<item>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,11 +121,11 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
<li><a href="../../plans/eversolo-companion-phases-3-9/">Завершить Eversolo Companion - этапы 3-9</a></li>
|
||||
|
||||
<li><a href="../../creds/eversolo-companion-postgres/">Eversolo Companion PostgreSQL</a></li>
|
||||
|
||||
<li><a href="../../plans/eversolo-companion-phases-3-9/">Завершить Eversolo Companion - этапы 3-9</a></li>
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,10 +121,10 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../creds/leadera-db-docker/">Mac Studio — leadera-api БД (docker-compose)</a></li>
|
||||
|
||||
<li><a href="../../creds/leadera-db-local/">Mac Studio — leadera-api БД (локальная)</a></li>
|
||||
|
||||
<li><a href="../../creds/leadera-db-docker/">Mac Studio — leadera-api БД (docker-compose)</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,15 +121,15 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
|
||||
<li><a href="../../creds/eversolo-companion-postgres/">Eversolo Companion PostgreSQL</a></li>
|
||||
|
||||
<li><a href="../../creds/leadera-db-local/">Mac Studio — leadera-api БД (локальная)</a></li>
|
||||
<li><a href="../../guides/sand-a2v-space/">Подключение sand.a2v.space (Leadera Project)</a></li>
|
||||
|
||||
<li><a href="../../plans/eversolo-companion-phases-3-9/">Завершить Eversolo Companion - этапы 3-9</a></li>
|
||||
|
||||
<li><a href="../../guides/sand-a2v-space/">Подключение sand.a2v.space (Leadera Project)</a></li>
|
||||
<li><a href="../../creds/leadera-db-local/">Mac Studio — leadera-api БД (локальная)</a></li>
|
||||
|
||||
<li><a href="../../creds/eversolo-companion-postgres/">Eversolo Companion PostgreSQL</a></li>
|
||||
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,10 +121,10 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
|
||||
<li><a href="../../creds/docs-a2v-space-auth/">docs.a2v.space Basic Auth</a></li>
|
||||
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,15 +121,15 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
|
||||
<li><a href="../../creds/eversolo-companion-postgres/">Eversolo Companion PostgreSQL</a></li>
|
||||
|
||||
<li><a href="../../kb/eversolo-api/">Eversolo T8 API</a></li>
|
||||
<li><a href="../../plans/eversolo-control/">Реализовать управление Eversolo из OpenClaw</a></li>
|
||||
|
||||
<li><a href="../../plans/eversolo-companion-phases-3-9/">Завершить Eversolo Companion - этапы 3-9</a></li>
|
||||
|
||||
<li><a href="../../plans/eversolo-control/">Реализовать управление Eversolo из OpenClaw</a></li>
|
||||
<li><a href="../../kb/eversolo-api/">Eversolo T8 API</a></li>
|
||||
|
||||
<li><a href="../../creds/eversolo-companion-postgres/">Eversolo Companion PostgreSQL</a></li>
|
||||
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Excel — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Excel</h1>
|
||||
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../leadera/leadera-gantt-excel-export/">Leadera — Экспорт диаграммы Ганта в Excel</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>Excel on docs.a2v.space</title>
|
||||
<link>/tags/excel/</link>
|
||||
<description>Recent content in Excel on docs.a2v.space</description>
|
||||
<generator>Hugo</generator>
|
||||
<language>ru-ru</language>
|
||||
<lastBuildDate>Thu, 09 Apr 2026 00:00:00 +0000</lastBuildDate>
|
||||
<atom:link href="/tags/excel/index.xml" rel="self" type="application/rss+xml" />
|
||||
<item>
|
||||
<title>Leadera — Экспорт диаграммы Ганта в Excel</title>
|
||||
<link>/leadera/leadera-gantt-excel-export/</link>
|
||||
<pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-gantt-excel-export/</guid>
|
||||
<description><h1 id="экспорт-диаграммы-ганта-в-excel">Экспорт диаграммы Ганта в Excel</h1>
<p><strong>Дата:</strong> 09.04.2026
<strong>Статус:</strong> Требования
<strong>Проект:</strong> Leadera</p>
<hr>
<h2 id="общее-описание">Общее описание</h2>
<p>Пользователь нажимает кнопку экспорта в интерфейсе диаграммы Ганта. Система запрашивает диапазон дат выгрузки (по умолчанию — текущий видимый диапазон из интерфейса). После указания диапазона скачивается Excel-файл с визуальным представлением диаграммы Ганта.</p>
<hr>
<h2 id="вводные-данные">Вводные данные</h2>
<ul>
<li><strong>Источник:</strong> текущая диаграмма Ганта проекта (один проект = одна диаграмма)</li>
<li><strong>Диапазон дат:</strong> пользователь указывает начальную и конечную дату. По умолчанию подставляется видимый в момент запроса диапазон из интерфейса</li>
<li><strong>Масштаб таймлайна:</strong> берётся тот, который выбран пользователем в интерфейсе в момент запроса (не сохранённый в системе, а текущий). Варианты: по ячейкам или по дням</li>
</ul>
<hr>
<h2 id="структура-excel-файла">Структура Excel-файла</h2>
<h3 id="шапка-первые-строки">Шапка (первые строки)</h3>
<ul>
<li>Название диаграммы (название проекта)</li>
<li>Диапазон дат, в рамках которого сделана выгрузка</li>
</ul>
<h3 id="лист--один">Лист — один</h3>
<p>Вся диаграмма размещается на одном листе.</p></description>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Export — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Export</h1>
|
||||
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../leadera/leadera-gantt-excel-export/">Leadera — Экспорт диаграммы Ганта в Excel</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>Export on docs.a2v.space</title>
|
||||
<link>/tags/export/</link>
|
||||
<description>Recent content in Export on docs.a2v.space</description>
|
||||
<generator>Hugo</generator>
|
||||
<language>ru-ru</language>
|
||||
<lastBuildDate>Thu, 09 Apr 2026 00:00:00 +0000</lastBuildDate>
|
||||
<atom:link href="/tags/export/index.xml" rel="self" type="application/rss+xml" />
|
||||
<item>
|
||||
<title>Leadera — Экспорт диаграммы Ганта в Excel</title>
|
||||
<link>/leadera/leadera-gantt-excel-export/</link>
|
||||
<pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-gantt-excel-export/</guid>
|
||||
<description><h1 id="экспорт-диаграммы-ганта-в-excel">Экспорт диаграммы Ганта в Excel</h1>
<p><strong>Дата:</strong> 09.04.2026
<strong>Статус:</strong> Требования
<strong>Проект:</strong> Leadera</p>
<hr>
<h2 id="общее-описание">Общее описание</h2>
<p>Пользователь нажимает кнопку экспорта в интерфейсе диаграммы Ганта. Система запрашивает диапазон дат выгрузки (по умолчанию — текущий видимый диапазон из интерфейса). После указания диапазона скачивается Excel-файл с визуальным представлением диаграммы Ганта.</p>
<hr>
<h2 id="вводные-данные">Вводные данные</h2>
<ul>
<li><strong>Источник:</strong> текущая диаграмма Ганта проекта (один проект = одна диаграмма)</li>
<li><strong>Диапазон дат:</strong> пользователь указывает начальную и конечную дату. По умолчанию подставляется видимый в момент запроса диапазон из интерфейса</li>
<li><strong>Масштаб таймлайна:</strong> берётся тот, который выбран пользователем в интерфейсе в момент запроса (не сохранённый в системе, а текущий). Варианты: по ячейкам или по дням</li>
</ul>
<hr>
<h2 id="структура-excel-файла">Структура Excel-файла</h2>
<h3 id="шапка-первые-строки">Шапка (первые строки)</h3>
<ul>
<li>Название диаграммы (название проекта)</li>
<li>Диапазон дат, в рамках которого сделана выгрузка</li>
</ul>
<h3 id="лист--один">Лист — один</h3>
<p>Вся диаграмма размещается на одном листе.</p></description>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Frontend — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Frontend</h1>
|
||||
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../leadera/leadera-gantt-filters/">Leadera — Фильтры и реорганизация списка диаграмм Ганта</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>Frontend on docs.a2v.space</title>
|
||||
<link>/tags/frontend/</link>
|
||||
<description>Recent content in Frontend on docs.a2v.space</description>
|
||||
<generator>Hugo</generator>
|
||||
<language>ru-ru</language>
|
||||
<lastBuildDate>Tue, 07 Apr 2026 00:00:00 +0000</lastBuildDate>
|
||||
<atom:link href="/tags/frontend/index.xml" rel="self" type="application/rss+xml" />
|
||||
<item>
|
||||
<title>Leadera — Фильтры и реорганизация списка диаграмм Ганта</title>
|
||||
<link>/leadera/leadera-gantt-filters/</link>
|
||||
<pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-gantt-filters/</guid>
|
||||
<description><h1 id="фильтры-и-реорганизация-списка-диаграмм-ганта">Фильтры и реорганизация списка диаграмм Ганта</h1>
<p><strong>Дата:</strong> 07.04.2026
<strong>Статус:</strong> Требования
<strong>Проект:</strong> Leadera (dev — sand.a2v.space)</p>
<hr>
<h2 id="1-постановка-задачи">1. Постановка задачи</h2>
<p>Список диаграмм Ганта должен показывать <strong>все доступные пользователю диаграммы</strong> из всех пространств (spaces), с возможностью фильтрации и поиска. Сейчас список привязан к одному пространству — нужно убрать эту зависимость и добавить удобный toolbar.</p>
<hr>
<h2 id="2-изменения-api">2. Изменения API</h2>
<h3 id="21-новый-endpoint-списка-диаграмм">2.1. Новый endpoint списка диаграмм</h3>
<p><strong>Было:</strong></p>
<pre tabindex="0"><code>GET /api/v1/spaces/{spaceId}/gantt-charts?page=1&amp;per_page=10
</code></pre><p><strong>Стало:</strong></p>
<pre tabindex="0"><code>GET /api/v1/gantt-charts?page=1&amp;per_page=10
</code></pre><p>Пользователь видит все диаграммы из <strong>всех пространств</strong>, где он является подтверждённым участником (статус <code>accepted</code>).</p></description>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,6 +121,18 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../leadera/leadera-gantt-public-access/">Leadera — Публичный доступ к диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-gantt-excel-export/">Leadera — Экспорт диаграммы Ганта в Excel</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-milestones/">Leadera: Вехи (Milestones) на диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-gantt-filters/">Leadera — Фильтры и реорганизация списка диаграмм Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-websocket-gantt/">Leadera — WebSocket для диаграмм Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-section-constraints/">Leadera: Ограничения секций в диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../diary/2026-03-31/">31 марта 2026</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,11 +121,15 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
<li><a href="../../leadera/leadera-milestones/">Leadera: Вехи (Milestones) на диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-section-constraints/">Leadera: Ограничения секций в диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../guides/sand-a2v-space/">Подключение sand.a2v.space (Leadera Project)</a></li>
|
||||
|
||||
<li><a href="../../plans/eversolo-companion-phases-3-9/">Завершить Eversolo Companion - этапы 3-9</a></li>
|
||||
|
||||
<li><a href="../../guides/sand-a2v-space/">Подключение sand.a2v.space (Leadera Project)</a></li>
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -6,8 +6,22 @@
|
||||
<description>Recent content in Go on docs.a2v.space</description>
|
||||
<generator>Hugo</generator>
|
||||
<language>ru-ru</language>
|
||||
<lastBuildDate>Sun, 29 Mar 2026 00:00:00 +0000</lastBuildDate>
|
||||
<lastBuildDate>Wed, 08 Apr 2026 00:00:00 +0000</lastBuildDate>
|
||||
<atom:link href="/tags/go/index.xml" rel="self" type="application/rss+xml" />
|
||||
<item>
|
||||
<title>Leadera: Вехи (Milestones) на диаграмме Ганта</title>
|
||||
<link>/leadera/leadera-milestones/</link>
|
||||
<pubDate>Wed, 08 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-milestones/</guid>
|
||||
<description><h2 id="бизнес-требования">Бизнес-требования</h2>
<h3 id="br-1-сущность-веха-milestone">BR-1: Сущность «Веха» (Milestone)</h3>
<p>Веха — это маркер на таймлайне диаграммы Ганта, обозначающий важную дату в проекте. В отличие от задач, веха не имеет длительности и привязана к конкретной дате.</p>
<p><strong>Поля вехи:</strong></p>
<table>
 <thead>
 <tr>
 <th>Поле</th>
 <th>Тип</th>
 <th>Описание</th>
 </tr>
 </thead>
 <tbody>
 <tr>
 <td>id</td>
 <td>UUID</td>
 <td>Идентификатор</td>
 </tr>
 <tr>
 <td>gantt_chart_id</td>
 <td>UUID</td>
 <td>Принадлежность к диаграмме (не к секции!)</td>
 </tr>
 <tr>
 <td>title</td>
 <td>string</td>
 <td>Название вехи (обязательное, 1–255 символов)</td>
 </tr>
 <tr>
 <td>date</td>
 <td>date</td>
 <td>Дата вехи (без времени, обязательная)</td>
 </tr>
 <tr>
 <td>color</td>
 <td>string</td>
 <td>Цвет маркера (HEX, по умолчанию <code>#dc3545</code> — красный)</td>
 </tr>
 <tr>
 <td>created_by</td>
 <td>UUID</td>
 <td>Кто создал (не отображается в UI, хранится для аудита)</td>
 </tr>
 <tr>
 <td>updated_by</td>
 <td>UUID</td>
 <td>Кто обновил (не отображается в UI)</td>
 </tr>
 <tr>
 <td>created_at</td>
 <td>timestamp</td>
 <td>Дата создания</td>
 </tr>
 <tr>
 <td>updated_at</td>
 <td>timestamp</td>
 <td>Дата обновления</td>
 </tr>
 <tr>
 <td>deleted_at</td>
 <td>timestamp</td>
 <td>Soft delete</td>
 </tr>
 </tbody>
</table>
<p><strong>Права доступа:</strong> Создание, редактирование и удаление вех доступно участникам с правами выше «только чтение» (аналогично задачам).</p></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Leadera: Ограничения секций в диаграмме Ганта</title>
|
||||
<link>/leadera/leadera-section-constraints/</link>
|
||||
<pubDate>Mon, 06 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-section-constraints/</guid>
|
||||
<description><h2 id="бизнес-требования">Бизнес-требования</h2>
<h3 id="br-1-лимит-одновременных-задач-concurrency-limit">BR-1: Лимит одновременных задач (Concurrency Limit)</h3>
<p>У каждой секции появляется параметр <strong>max_concurrent_tasks</strong> (целое число ≥ 0, по умолчанию 0).</p>
<p><strong>Поведение:</strong></p>
<ul>
<li>Если <code>max_concurrent_tasks = 3</code> — в любой данный день в этой секции не может быть больше 3 активных задач одновременно</li>
<li>Если <code>max_concurrent_tasks = 0</code> — ограничение отключено, лимита нет</li>
<li>Пересечение считается по дате: задача занимает дни с <code>start_date</code> по <code>end_date</code> включительно. Две задачи «пересекаются», если у них есть хотя бы один общий день</li>
<li>Ограничение работает <strong>только в рамках одной секции</strong>. Задачи из разных секций не влияют друг на друга</li>
</ul>
<p><strong>Срабатывание при:</strong></p></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>29 марта 2026</title>
|
||||
<link>/diary/2026-03-29/</link>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
+86
-62
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../creds/">Креды</a></li>
|
||||
@@ -115,91 +121,109 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../tags/audit/">Audit</a></li>
|
||||
<li><a href="../tags/sharing/">Sharing</a></li>
|
||||
|
||||
<li><a href="../tags/canvas/">Canvas</a></li>
|
||||
|
||||
<li><a href="../tags/gantt/">Gantt</a></li>
|
||||
|
||||
<li><a href="../tags/github/">Github</a></li>
|
||||
|
||||
<li><a href="../tags/hugo/">Hugo</a></li>
|
||||
|
||||
<li><a href="../tags/infrastructure/">Infrastructure</a></li>
|
||||
<li><a href="../tags/public-access/">Public-Access</a></li>
|
||||
|
||||
<li><a href="../tags/leadera/">Leadera</a></li>
|
||||
|
||||
<li><a href="../tags/opencode/">Opencode</a></li>
|
||||
<li><a href="../tags/gantt/">Gantt</a></li>
|
||||
|
||||
<li><a href="../tags/personal-site/">Personal-Site</a></li>
|
||||
<li><a href="../tags/export/">Export</a></li>
|
||||
|
||||
<li><a href="../tags/roadmap/">Roadmap</a></li>
|
||||
<li><a href="../tags/excel/">Excel</a></li>
|
||||
|
||||
<li><a href="../tags/system-utils/">System-Utils</a></li>
|
||||
|
||||
<li><a href="../tags/architecture/">Architecture</a></li>
|
||||
|
||||
<li><a href="../tags/mtproxy/">Mtproxy</a></li>
|
||||
|
||||
<li><a href="../tags/ollama/">Ollama</a></li>
|
||||
|
||||
<li><a href="../tags/qwen/">Qwen</a></li>
|
||||
|
||||
<li><a href="../tags/security/">Security</a></li>
|
||||
|
||||
<li><a href="../tags/tasktracker/">Tasktracker</a></li>
|
||||
|
||||
<li><a href="../tags/vikunja/">Vikunja</a></li>
|
||||
|
||||
<li><a href="../tags/amnezia/">Amnezia</a></li>
|
||||
|
||||
<li><a href="../tags/angular/">Angular</a></li>
|
||||
|
||||
<li><a href="../tags/api/">Api</a></li>
|
||||
|
||||
<li><a href="../tags/authentication/">Authentication</a></li>
|
||||
|
||||
<li><a href="../tags/companion/">Companion</a></li>
|
||||
|
||||
<li><a href="../tags/database/">Database</a></li>
|
||||
|
||||
<li><a href="../tags/devops/">Devops</a></li>
|
||||
|
||||
<li><a href="../tags/docker/">Docker</a></li>
|
||||
|
||||
<li><a href="../tags/docker-compose/">Docker-Compose</a></li>
|
||||
|
||||
<li><a href="../tags/docs/">Docs</a></li>
|
||||
|
||||
<li><a href="../tags/eversolo/">Eversolo</a></li>
|
||||
<li><a href="../tags/ux/">Ux</a></li>
|
||||
|
||||
<li><a href="../tags/go/">Go</a></li>
|
||||
|
||||
<li><a href="../tags/identity/">Identity</a></li>
|
||||
<li><a href="../tags/api/">Api</a></li>
|
||||
|
||||
<li><a href="../tags/llm/">Llm</a></li>
|
||||
<li><a href="../tags/angular/">Angular</a></li>
|
||||
|
||||
<li><a href="../tags/music/">Music</a></li>
|
||||
<li><a href="../tags/websocket/">Websocket</a></li>
|
||||
|
||||
<li><a href="../tags/nginx/">Nginx</a></li>
|
||||
<li><a href="../tags/requirements/">Requirements</a></li>
|
||||
|
||||
<li><a href="../tags/openclaw/">Openclaw</a></li>
|
||||
<li><a href="../tags/realtime/">Realtime</a></li>
|
||||
|
||||
<li><a href="../tags/postgresql/">Postgresql</a></li>
|
||||
<li><a href="../tags/frontend/">Frontend</a></li>
|
||||
|
||||
<li><a href="../tags/reg.ru/">Reg.ru</a></li>
|
||||
<li><a href="../tags/architecture/">Architecture</a></li>
|
||||
|
||||
<li><a href="../tags/reverse-proxy/">Reverse-Proxy</a></li>
|
||||
<li><a href="../tags/system-utils/">System-Utils</a></li>
|
||||
|
||||
<li><a href="../tags/ssh/">Ssh</a></li>
|
||||
<li><a href="../tags/roadmap/">Roadmap</a></li>
|
||||
|
||||
<li><a href="../tags/streamer/">Streamer</a></li>
|
||||
<li><a href="../tags/personal-site/">Personal-Site</a></li>
|
||||
|
||||
<li><a href="../tags/tunnel/">Tunnel</a></li>
|
||||
<li><a href="../tags/opencode/">Opencode</a></li>
|
||||
|
||||
<li><a href="../tags/infrastructure/">Infrastructure</a></li>
|
||||
|
||||
<li><a href="../tags/hugo/">Hugo</a></li>
|
||||
|
||||
<li><a href="../tags/github/">Github</a></li>
|
||||
|
||||
<li><a href="../tags/canvas/">Canvas</a></li>
|
||||
|
||||
<li><a href="../tags/audit/">Audit</a></li>
|
||||
|
||||
<li><a href="../tags/vikunja/">Vikunja</a></li>
|
||||
|
||||
<li><a href="../tags/tasktracker/">Tasktracker</a></li>
|
||||
|
||||
<li><a href="../tags/security/">Security</a></li>
|
||||
|
||||
<li><a href="../tags/qwen/">Qwen</a></li>
|
||||
|
||||
<li><a href="../tags/ollama/">Ollama</a></li>
|
||||
|
||||
<li><a href="../tags/mtproxy/">Mtproxy</a></li>
|
||||
|
||||
<li><a href="../tags/vps/">Vps</a></li>
|
||||
|
||||
<li><a href="../tags/vpn/">Vpn</a></li>
|
||||
|
||||
<li><a href="../tags/vps/">Vps</a></li>
|
||||
<li><a href="../tags/tunnel/">Tunnel</a></li>
|
||||
|
||||
<li><a href="../tags/streamer/">Streamer</a></li>
|
||||
|
||||
<li><a href="../tags/ssh/">Ssh</a></li>
|
||||
|
||||
<li><a href="../tags/reverse-proxy/">Reverse-Proxy</a></li>
|
||||
|
||||
<li><a href="../tags/reg.ru/">Reg.ru</a></li>
|
||||
|
||||
<li><a href="../tags/postgresql/">Postgresql</a></li>
|
||||
|
||||
<li><a href="../tags/openclaw/">Openclaw</a></li>
|
||||
|
||||
<li><a href="../tags/nginx/">Nginx</a></li>
|
||||
|
||||
<li><a href="../tags/music/">Music</a></li>
|
||||
|
||||
<li><a href="../tags/llm/">Llm</a></li>
|
||||
|
||||
<li><a href="../tags/identity/">Identity</a></li>
|
||||
|
||||
<li><a href="../tags/eversolo/">Eversolo</a></li>
|
||||
|
||||
<li><a href="../tags/docs/">Docs</a></li>
|
||||
|
||||
<li><a href="../tags/docker-compose/">Docker-Compose</a></li>
|
||||
|
||||
<li><a href="../tags/docker/">Docker</a></li>
|
||||
|
||||
<li><a href="../tags/devops/">Devops</a></li>
|
||||
|
||||
<li><a href="../tags/database/">Database</a></li>
|
||||
|
||||
<li><a href="../tags/companion/">Companion</a></li>
|
||||
|
||||
<li><a href="../tags/authentication/">Authentication</a></li>
|
||||
|
||||
<li><a href="../tags/amnezia/">Amnezia</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
+106
-43
@@ -6,8 +6,113 @@
|
||||
<description>Recent content in Tags on docs.a2v.space</description>
|
||||
<generator>Hugo</generator>
|
||||
<language>ru-ru</language>
|
||||
<lastBuildDate>Tue, 31 Mar 2026 00:00:00 +0000</lastBuildDate>
|
||||
<lastBuildDate>Thu, 09 Apr 2026 00:00:00 +0000</lastBuildDate>
|
||||
<atom:link href="/tags/index.xml" rel="self" type="application/rss+xml" />
|
||||
<item>
|
||||
<title>Excel</title>
|
||||
<link>/tags/excel/</link>
|
||||
<pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/excel/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Export</title>
|
||||
<link>/tags/export/</link>
|
||||
<pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/export/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Gantt</title>
|
||||
<link>/tags/gantt/</link>
|
||||
<pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/gantt/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Leadera</title>
|
||||
<link>/tags/leadera/</link>
|
||||
<pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/leadera/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Public-Access</title>
|
||||
<link>/tags/public-access/</link>
|
||||
<pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/public-access/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Sharing</title>
|
||||
<link>/tags/sharing/</link>
|
||||
<pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/sharing/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Angular</title>
|
||||
<link>/tags/angular/</link>
|
||||
<pubDate>Wed, 08 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/angular/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Api</title>
|
||||
<link>/tags/api/</link>
|
||||
<pubDate>Wed, 08 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/api/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Go</title>
|
||||
<link>/tags/go/</link>
|
||||
<pubDate>Wed, 08 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/go/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Ux</title>
|
||||
<link>/tags/ux/</link>
|
||||
<pubDate>Wed, 08 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/ux/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Architecture</title>
|
||||
<link>/tags/architecture/</link>
|
||||
<pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/architecture/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Frontend</title>
|
||||
<link>/tags/frontend/</link>
|
||||
<pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/frontend/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Realtime</title>
|
||||
<link>/tags/realtime/</link>
|
||||
<pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/realtime/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Requirements</title>
|
||||
<link>/tags/requirements/</link>
|
||||
<pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/requirements/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Websocket</title>
|
||||
<link>/tags/websocket/</link>
|
||||
<pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/websocket/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Audit</title>
|
||||
<link>/tags/audit/</link>
|
||||
@@ -22,13 +127,6 @@
|
||||
<guid>/tags/canvas/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Gantt</title>
|
||||
<link>/tags/gantt/</link>
|
||||
<pubDate>Tue, 31 Mar 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/gantt/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Github</title>
|
||||
<link>/tags/github/</link>
|
||||
@@ -50,13 +148,6 @@
|
||||
<guid>/tags/infrastructure/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Leadera</title>
|
||||
<link>/tags/leadera/</link>
|
||||
<pubDate>Tue, 31 Mar 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/leadera/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Opencode</title>
|
||||
<link>/tags/opencode/</link>
|
||||
@@ -85,13 +176,6 @@
|
||||
<guid>/tags/system-utils/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Architecture</title>
|
||||
<link>/tags/architecture/</link>
|
||||
<pubDate>Mon, 30 Mar 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/architecture/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Mtproxy</title>
|
||||
<link>/tags/mtproxy/</link>
|
||||
@@ -141,20 +225,6 @@
|
||||
<guid>/tags/amnezia/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Angular</title>
|
||||
<link>/tags/angular/</link>
|
||||
<pubDate>Sun, 29 Mar 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/angular/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Api</title>
|
||||
<link>/tags/api/</link>
|
||||
<pubDate>Sun, 29 Mar 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/api/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Authentication</title>
|
||||
<link>/tags/authentication/</link>
|
||||
@@ -211,13 +281,6 @@
|
||||
<guid>/tags/eversolo/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Go</title>
|
||||
<link>/tags/go/</link>
|
||||
<pubDate>Sun, 29 Mar 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/go/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Identity</title>
|
||||
<link>/tags/identity/</link>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -119,11 +125,11 @@
|
||||
|
||||
<li><a href="../../diary/2026-03-30/">30 марта 2026</a></li>
|
||||
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
<li><a href="../../creds/vps-reg-ru/">VPS reg.ru</a></li>
|
||||
|
||||
<li><a href="../../creds/vpn-vps/">VPN VPS</a></li>
|
||||
|
||||
<li><a href="../../creds/vps-reg-ru/">VPS reg.ru</a></li>
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,16 +121,28 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../guides/leadera-audit/">Аудит Leadera — полный отчёт</a></li>
|
||||
<li><a href="../../leadera/leadera-gantt-public-access/">Leadera — Публичный доступ к диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-gantt-excel-export/">Leadera — Экспорт диаграммы Ганта в Excel</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-milestones/">Leadera: Вехи (Milestones) на диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-gantt-filters/">Leadera — Фильтры и реорганизация списка диаграмм Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-websocket-gantt/">Leadera — WebSocket для диаграмм Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-section-constraints/">Leadera: Ограничения секций в диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../diary/2026-03-31/">31 марта 2026</a></li>
|
||||
|
||||
<li><a href="../../diary/2026-03-30/">30 марта 2026</a></li>
|
||||
|
||||
<li><a href="../../creds/leadera-db-docker/">Mac Studio — leadera-api БД (docker-compose)</a></li>
|
||||
<li><a href="../../leadera/leadera-audit/">Аудит Leadera — полный отчёт</a></li>
|
||||
|
||||
<li><a href="../../creds/leadera-db-local/">Mac Studio — leadera-api БД (локальная)</a></li>
|
||||
|
||||
<li><a href="../../creds/leadera-db-docker/">Mac Studio — leadera-api БД (docker-compose)</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user