# MVP-1 «Загрузка сотрудников» Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Развернуть рабочий read-only аналитический слой над BIT.RA + EVA + Bitrix24 с дашбордом «Загрузка сотрудников» (4 слоя: факт / текущая / плановая-как-«осталось» / прогноз) в Metabase для команды ~20 сотрудников. **Architecture:** PostgreSQL (отдельная БД `bit_flight_deck` в существующем `pipeline_postgres`), N8N для оркестрации (cron + Bitrix webhooks через CF Tunnel), HTTP-сервисы 1С в BIT.RA, JSON-RPC pull из EVA, REST + webhooks из Битрикса. SQL views/procedures для трансформаций raw → stg → core → mart. Metabase для дашбордов, NocoDB для управления identity_map. **Tech Stack:** PostgreSQL 15, N8N (existing), Docker Compose, Metabase, NocoDB, Cloudflare Tunnel (existing), 1С:Предприятие 8.3, JSON-RPC, REST. **Spec:** [docs/superpowers/specs/2026-05-13-mvp1-workload-design.md](../specs/2026-05-13-mvp1-workload-design.md) --- ## File Structure ``` ~/projects/bit-flight-deck/ ← локальная папка проекта (WSL) ├── docker-compose.yml Metabase + NocoDB, подключение к pipeline_net ├── .env секреты (не в git) ├── .env.example шаблон ├── README.md (уже создан) ├── .gitignore (уже создан) ├── infra/ │ ├── init-bit-flight-deck-db.sql создание БД + пользователя + схем │ ├── cloudflared-routes.md документация по поддоменам tunnel │ └── pg_backup.sh ручной cron-скрипт бэкапа ├── sql/ │ ├── migrations/ │ │ ├── 001_schemas.sql CREATE SCHEMA для raw/stg/core/mart │ │ ├── 002_core_employee.sql core.employee + core.department + history │ │ ├── 003_core_project.sql core.project + core.stage │ │ ├── 004_core_work_log.sql core.work_log + core.work_type │ │ ├── 005_core_task.sql core.task │ │ ├── 006_core_deal.sql core.deal + core.deal_team_member │ │ ├── 007_core_identity_map.sql core.identity_map │ │ ├── 008_raw_schemas.sql raw_bitra.* / raw_eva.* / raw_bitrix.* таблицы │ │ └── 009_stg_schemas.sql stg_* таблицы (плоские, нормализованные из raw) │ ├── views/ │ │ ├── stg_bitra_employee.sql VIEW из raw_bitra.employee → stg_bitra.employee │ │ ├── stg_bitra_works.sql ... (по одной view на сущность каждого источника) │ │ ├── stg_eva_person.sql │ │ ├── stg_eva_task.sql │ │ ├── stg_bitrix_deal.sql │ │ ├── stg_bitrix_user.sql │ │ ├── mart_workload_actual.sql VIEW для дашборда — факт │ │ ├── mart_workload_current.sql текущая │ │ ├── mart_workload_planned.sql плановая (как «осталось») │ │ ├── mart_workload_forecast.sql прогноз │ │ ├── mart_workload_summary.sql composite │ │ └── mart_tasks_orphan.sql метрика качества │ ├── procedures/ │ │ ├── core_merge_employee.sql SP «merge stg_*.employee → core.employee + identity_map» │ │ ├── core_merge_project.sql │ │ ├── core_merge_work_log.sql │ │ ├── core_merge_task.sql │ │ ├── core_merge_deal.sql │ │ └── mart_refresh_all.sql пересоздание materialized views (если будут) │ └── seed/ │ ├── work_types.sql 14 значений Enum.ВидыРабот с категориями (commercial/internal/free/ignored) │ ├── sd_projects_whitelist.sql 3 ID SD-проектов EVA │ └── bitrix_stages_forecast.sql список стадий CAT=16 для прогноза ├── n8n/ │ └── workflows/ JSON-экспорты workflows │ ├── 01-pull-bitra-employees.json │ ├── 02-pull-bitra-works.json │ ├── 03-pull-bitra-projects-dict.json │ ├── 04-pull-eva-incremental.json │ ├── 05-pull-eva-nightly-full.json │ ├── 06-bitrix-webhook-handler.json │ ├── 07-pull-bitrix-nightly-reconcile.json │ ├── 08-trigger-transforms.json │ └── 99-health-check.json └── docs/superpowers/ ├── specs/... (уже есть) └── plans/2026-05-13-mvp1-workload.md этот файл В BIT.RA (1С-разработка, отдельная история): └── Расширение или общий модуль IntegrationAPI с HTTP-сервисами: GET /api/employees GET /api/works GET /api/projects GET /api/dictionaries GET /api/work_types GET /api/dept_history ``` --- ## Phase 0: Project Bootstrap ### Task 0.1: Создать локальный проект в WSL **Files:** - Create: `~/projects/bit-flight-deck/` (если ещё нет) - Clone: `https://gitea.bigmadnekenny.ru/admin/bit-flight-deck.git` - [ ] **Step 1:** В WSL перейти в `~/projects/`: ```bash mkdir -p ~/projects && cd ~/projects ``` - [ ] **Step 2:** Клонировать репо: ```bash git clone https://gitea.bigmadnekenny.ru/admin/bit-flight-deck.git cd bit-flight-deck ``` - [ ] **Step 3:** Проверить что docs/, README.md, .gitignore на месте: ```bash ls -la cat docs/superpowers/specs/2026-05-13-mvp1-workload-design.md | head ``` Ожидание: видим спеку и наброски MVP-2/3/4. ### Task 0.2: Создать БД bit_flight_deck и пользователя **Files:** - Create: `infra/init-bit-flight-deck-db.sql` - [ ] **Step 1:** Создать файл `infra/init-bit-flight-deck-db.sql`: ```sql -- Запускается ОДИН раз на pipeline_postgres от имени суперпользователя. CREATE DATABASE bit_flight_deck; CREATE USER bit_flight_deck_user WITH PASSWORD '<СГЕНЕРИРОВАТЬ_СИЛЬНЫЙ_PASSWORD>'; GRANT ALL PRIVILEGES ON DATABASE bit_flight_deck TO bit_flight_deck_user; ALTER DATABASE bit_flight_deck OWNER TO bit_flight_deck_user; ``` - [ ] **Step 2:** Сгенерировать пароль: ```bash openssl rand -hex 16 ``` Подставить в .sql вместо `<СГЕНЕРИРОВАТЬ_СИЛЬНЫЙ_PASSWORD>`. - [ ] **Step 3:** Записать пароль в `.env` (создать файл, в git не коммитим): ``` PG_USER=bit_flight_deck_user PG_PASSWORD=<сгенерированный пароль> PG_DB=bit_flight_deck PG_HOST=pipeline_postgres PG_PORT=5432 ``` - [ ] **Step 4:** Применить SQL к pipeline_postgres: ```bash docker exec -i pipeline_postgres psql -U -d postgres < infra/init-bit-flight-deck-db.sql ``` - [ ] **Step 5:** Verify — подключиться от имени нового пользователя: ```bash docker exec -it pipeline_postgres psql -U bit_flight_deck_user -d bit_flight_deck -c "SELECT current_database(), current_user;" ``` Ожидание: возвращает строку `bit_flight_deck | bit_flight_deck_user`. - [ ] **Step 6:** Commit: ```bash git add infra/init-bit-flight-deck-db.sql .env.example git commit -m "infra: add init script for bit_flight_deck DB and user" ``` (Файл `.env` НЕ коммитим — только `.env.example` с шаблоном.) ### Task 0.3: Создать `.env.example` **Files:** Create: `.env.example` - [ ] **Step 1:** Создать `.env.example` со всеми переменными которые понадобятся: ``` # PostgreSQL PG_USER=bit_flight_deck_user PG_PASSWORD= PG_DB=bit_flight_deck PG_HOST=pipeline_postgres PG_PORT=5432 # BIT.RA HTTP-сервисы BITRA_BASE_URL=http:////hs/IntegrationAPI/v1 BITRA_USER= BITRA_PASSWORD= # EVA Desk EVA_BASE_URL=https://firstbit.evateam.ru/api/ EVA_ADMIN_TOKEN= # Bitrix24 BITRIX_WEBHOOK_URL=https://vdst421.1cbit.ru/rest/91// BITRIX_WEBHOOK_SECRET= # Metabase METABASE_SITE_NAME="bit-flight-deck" METABASE_PORT=3001 # NocoDB NOCODB_PORT=8090 NOCODB_ADMIN_EMAIL=roachesnokov@gmail.com NOCODB_ADMIN_PASSWORD= ``` - [ ] **Step 2:** Commit: ```bash git add .env.example git commit -m "infra: add .env.example template" ``` ### Task 0.4: Создать docker-compose с Metabase + NocoDB **Files:** Create: `docker-compose.yml` - [ ] **Step 1:** Создать `docker-compose.yml`: ```yaml services: metabase: image: metabase/metabase:latest container_name: bfd_metabase restart: unless-stopped ports: - "${METABASE_PORT}:3000" environment: MB_DB_TYPE: postgres MB_DB_DBNAME: ${PG_DB} MB_DB_PORT: 5432 MB_DB_USER: ${PG_USER} MB_DB_PASS: ${PG_PASSWORD} MB_DB_HOST: ${PG_HOST} MB_SITE_NAME: ${METABASE_SITE_NAME} networks: - pipeline_net nocodb: image: nocodb/nocodb:latest container_name: bfd_nocodb restart: unless-stopped ports: - "${NOCODB_PORT}:8080" environment: NC_DB: pg://${PG_HOST}:5432?u=${PG_USER}&p=${PG_PASSWORD}&d=${PG_DB} NC_ADMIN_EMAIL: ${NOCODB_ADMIN_EMAIL} NC_ADMIN_PASSWORD: ${NOCODB_ADMIN_PASSWORD} networks: - pipeline_net networks: pipeline_net: external: true ``` - [ ] **Step 2:** Запустить: ```bash cd ~/projects/bit-flight-deck/ docker compose up -d ``` - [ ] **Step 3:** Verify Metabase: ```bash curl -s http://localhost:3001/api/health ``` Ожидание: `{"status":"ok"}`. Дать ~1 минуту на старт. - [ ] **Step 4:** Verify NocoDB: ```bash curl -s http://localhost:8090/api/v1/health ``` Ожидание: 200. - [ ] **Step 5:** Открыть Metabase в браузере (http://localhost:3001), пройти initial setup wizard. Подключение к БД настроится автоматически через env-переменные. - [ ] **Step 6:** Commit: ```bash git add docker-compose.yml git commit -m "infra: add Metabase + NocoDB docker-compose" ``` --- ## Phase 1: Cloudflare Tunnel — поддомен n8n.bigmadnekenny.ru ### Task 1.1: Добавить ingress-route в cloudflared **Files:** - Modify: `/etc/cloudflared/config.yml` (на хосте, не в проекте) - Create: `infra/cloudflared-routes.md` (документация) - [ ] **Step 1:** Открыть существующий config cloudflared: ```bash sudo cat /etc/cloudflared/config.yml ``` - [ ] **Step 2:** Добавить новый ingress перед `service: http_status:404`: ```yaml - hostname: n8n.bigmadnekenny.ru service: http://localhost:5678 ``` - [ ] **Step 3:** Создать DNS-роут (через Cloudflare API или CLI): ```bash cloudflared tunnel route dns n8n.bigmadnekenny.ru ``` Имя туннеля — посмотреть в `cloudflared tunnel list`. - [ ] **Step 4:** Перезапустить сервис: ```bash sudo systemctl restart cloudflared sudo systemctl status cloudflared ``` Ожидание: `active (running)`. - [ ] **Step 5:** Verify извне (например с телефона): ```bash curl -I https://n8n.bigmadnekenny.ru/ ``` Ожидание: HTTP 200 или 401 от N8N (зависит от настроек auth). - [ ] **Step 6:** Создать документацию `infra/cloudflared-routes.md`: ```markdown # Cloudflare Tunnel routes for bit-flight-deck ## n8n.bigmadnekenny.ru → localhost:5678 (N8N) Назначение: приём outbound webhooks от Bitrix24 на N8N webhook-trigger. URL для webhook'а Bitrix: https://n8n.bigmadnekenny.ru/webhook/bitrix/ Конфиг ingress в `/etc/cloudflared/config.yml`: ```yaml - hostname: n8n.bigmadnekenny.ru service: http://localhost:5678 ``` ``` - [ ] **Step 7:** Commit: ```bash git add infra/cloudflared-routes.md git commit -m "infra: document n8n.bigmadnekenny.ru cloudflared route" ``` --- ## Phase 2: Database schemas ### Task 2.1: Создать схемы PostgreSQL **Files:** Create: `sql/migrations/001_schemas.sql` - [ ] **Step 1:** Создать файл с DDL: ```sql -- 001_schemas.sql — базовые схемы для слоёв DWH CREATE SCHEMA IF NOT EXISTS raw_bitra; CREATE SCHEMA IF NOT EXISTS raw_eva; CREATE SCHEMA IF NOT EXISTS raw_bitrix; CREATE SCHEMA IF NOT EXISTS stg_bitra; CREATE SCHEMA IF NOT EXISTS stg_eva; CREATE SCHEMA IF NOT EXISTS stg_bitrix; CREATE SCHEMA IF NOT EXISTS core; CREATE SCHEMA IF NOT EXISTS mart; -- Журнал применённых миграций CREATE TABLE IF NOT EXISTS public.migrations ( filename text PRIMARY KEY, applied_at timestamptz DEFAULT now() ); ``` - [ ] **Step 2:** Применить: ```bash docker exec -i pipeline_postgres psql -U bit_flight_deck_user -d bit_flight_deck < sql/migrations/001_schemas.sql docker exec -i pipeline_postgres psql -U bit_flight_deck_user -d bit_flight_deck -c "INSERT INTO public.migrations (filename) VALUES ('001_schemas.sql');" ``` - [ ] **Step 3:** Verify: ```bash docker exec -i pipeline_postgres psql -U bit_flight_deck_user -d bit_flight_deck -c "SELECT schema_name FROM information_schema.schemata WHERE schema_name LIKE 'raw_%' OR schema_name LIKE 'stg_%' OR schema_name IN ('core','mart');" ``` Ожидание: 8 строк. - [ ] **Step 4:** Commit. ### Task 2.2: core.employee + department + history **Files:** Create: `sql/migrations/002_core_employee.sql` - [ ] **Step 1:** DDL: ```sql CREATE TABLE core.office ( id bigserial PRIMARY KEY, name text NOT NULL, bitra_id text UNIQUE ); CREATE TABLE core.department ( id bigserial PRIMARY KEY, name text NOT NULL, bitra_id text UNIQUE, bitrix_id bigint UNIQUE, parent_id bigint REFERENCES core.department, source text NOT NULL DEFAULT 'bitra' -- bitra | bitrix ); CREATE TABLE core.employee ( id bigserial PRIMARY KEY, email text UNIQUE NOT NULL, full_name text, first_name text, last_name text, bitra_user_id text UNIQUE, eva_person_id text UNIQUE, bitrix_user_id bigint UNIQUE, rate decimal(10,2), office_id bigint REFERENCES core.office, department_id bigint REFERENCES core.department, is_active boolean DEFAULT true, is_target_for_mvp1 boolean DEFAULT false, last_synced timestamptz ); CREATE INDEX idx_employee_email ON core.employee (lower(email)); CREATE INDEX idx_employee_bitra_id ON core.employee (bitra_user_id); CREATE INDEX idx_employee_eva_id ON core.employee (eva_person_id); CREATE INDEX idx_employee_bitrix_id ON core.employee (bitrix_user_id); CREATE TABLE core.department_history ( employee_id bigint REFERENCES core.employee, valid_from date NOT NULL, valid_to date, department_id bigint REFERENCES core.department, source text NOT NULL DEFAULT 'bitra' ); ``` - [ ] **Step 2:** Применить + записать в migrations. - [ ] **Step 3:** Verify: ```bash docker exec -i pipeline_postgres psql -U bit_flight_deck_user -d bit_flight_deck -c "\dt core.*" ``` Ожидание: 4 таблицы. - [ ] **Step 4:** Commit. ### Task 2.3: core.project + stage **Files:** Create: `sql/migrations/003_core_project.sql` - [ ] **Step 1:** DDL: ```sql CREATE TABLE core.project ( id bigserial PRIMARY KEY, name text NOT NULL, bitra_id text UNIQUE, eva_id text UNIQUE, bitra_code text, eva_code text, is_sd boolean DEFAULT false, -- SD-проект (из явного списка) status text, cache_status_type text, -- из EVA: OPEN/IN_PROGRESS/CLOSED project_manager_id bigint REFERENCES core.employee, client_id bigint, -- ID Битрикс-компании, без маппинга на BIT.RA-клиента bitra_client_id text, bitra_client_name text, -- для отображения deadline date, budget decimal(15,2) ); CREATE INDEX idx_project_bitra ON core.project (bitra_id); CREATE INDEX idx_project_eva ON core.project (eva_id); CREATE TABLE core.stage ( id bigserial PRIMARY KEY, name text NOT NULL, bitra_id text UNIQUE, project_id bigint REFERENCES core.project, plan_start_date date, plan_end_date date, is_completed boolean DEFAULT false, is_acted boolean DEFAULT false ); CREATE INDEX idx_stage_project ON core.stage (project_id); ``` - [ ] **Step 2:** Apply + verify (\dt core.* — теперь 6 таблиц) + commit. ### Task 2.4: core.work_type + work_log **Files:** Create: `sql/migrations/004_core_work_log.sql` - [ ] **Step 1:** DDL: ```sql CREATE TABLE core.work_type ( code text PRIMARY KEY, -- ЛУРВ, ЛТ, Демо, ИТС, ИТСПлатныеРаботы, Сертификация, ... label text NOT NULL, category text NOT NULL CHECK (category IN ('commercial','presale','internal','free','ignored')), is_billable boolean NOT NULL DEFAULT false ); CREATE TABLE core.work_log ( id bigserial PRIMARY KEY, employee_id bigint NOT NULL REFERENCES core.employee, work_date date NOT NULL, work_type_code text NOT NULL REFERENCES core.work_type, project_id bigint REFERENCES core.project, stage_id bigint REFERENCES core.stage, bitra_client_id text, -- ID клиента BIT.RA, без core.client bitra_client_name text, hours decimal(10,2) NOT NULL, description text, bitra_doc_id text NOT NULL, -- ссылка на Document.Работы bitra_row_index int NOT NULL, UNIQUE (bitra_doc_id, bitra_row_index) ); CREATE INDEX idx_work_log_employee_date ON core.work_log (employee_id, work_date DESC); CREATE INDEX idx_work_log_project ON core.work_log (project_id); CREATE INDEX idx_work_log_work_type ON core.work_log (work_type_code); ``` - [ ] **Step 2:** Apply + verify + commit. ### Task 2.5: core.task **Files:** Create: `sql/migrations/005_core_task.sql` - [ ] **Step 1:** DDL: ```sql CREATE TABLE core.task ( id bigserial PRIMARY KEY, eva_id text UNIQUE NOT NULL, -- CmfTask:UUID code text, -- PBSD-12582 name text, project_id bigint REFERENCES core.project, responsible_id bigint REFERENCES core.employee, cache_status_type text NOT NULL CHECK (cache_status_type IN ('OPEN','IN_PROGRESS','IN_REVIEW','CLOSED')), eva_status_id text, eva_status_name text, cmf_created_at timestamptz, cmf_modified_at timestamptz, status_in_progress_start timestamptz, deadline timestamptz, last_synced timestamptz ); CREATE INDEX idx_task_responsible ON core.task (responsible_id); CREATE INDEX idx_task_status ON core.task (cache_status_type); CREATE INDEX idx_task_project ON core.task (project_id); ``` - [ ] **Step 2:** Apply + commit. ### Task 2.6: core.deal + deal_team_member **Files:** Create: `sql/migrations/006_core_deal.sql` - [ ] **Step 1:** DDL: ```sql CREATE TABLE core.deal ( id bigserial PRIMARY KEY, bitrix_id bigint UNIQUE NOT NULL, title text, category_id int, -- 16 stage_id text, -- C16:FINAL_INVOICE stage_semantic_id char(1), -- P | S | F opportunity decimal(15,2), begindate date, closedate date, assigned_to_id bigint REFERENCES core.employee, bitrix_company_id bigint, bitrix_company_name text, project_manager_id bigint REFERENCES core.employee, is_in_forecast boolean DEFAULT false, -- попадает ли в mart.workload_forecast last_synced timestamptz ); CREATE INDEX idx_deal_stage ON core.deal (stage_id); CREATE INDEX idx_deal_forecast ON core.deal (is_in_forecast) WHERE is_in_forecast = true; CREATE TABLE core.deal_team_member ( deal_id bigint REFERENCES core.deal ON DELETE CASCADE, employee_id bigint REFERENCES core.employee, weight decimal(5,2) NOT NULL DEFAULT 1.0, is_manual_override boolean DEFAULT false, PRIMARY KEY (deal_id, employee_id) ); CREATE INDEX idx_deal_team_employee ON core.deal_team_member (employee_id); ``` - [ ] **Step 2:** Apply + commit. ### Task 2.7: core.identity_map **Files:** Create: `sql/migrations/007_core_identity_map.sql` - [ ] **Step 1:** DDL: ```sql CREATE TABLE core.identity_map ( id bigserial PRIMARY KEY, entity_type text NOT NULL CHECK (entity_type IN ('employee','project','client')), core_id bigint, bitra_id text, eva_id text, bitrix_id bigint, confidence text NOT NULL CHECK (confidence IN ('auto','confirmed','manual')), match_key text, -- например email или EVA_ID confirmed_by text, confirmed_at timestamptz, created_at timestamptz DEFAULT now(), UNIQUE (entity_type, bitra_id, eva_id, bitrix_id) ); CREATE INDEX idx_identity_map_core ON core.identity_map (entity_type, core_id); CREATE INDEX idx_identity_map_confidence ON core.identity_map (confidence) WHERE confidence = 'manual'; ``` - [ ] **Step 2:** Apply + commit. ### Task 2.8: raw schemas **Files:** Create: `sql/migrations/008_raw_schemas.sql` - [ ] **Step 1:** DDL — таблицы-сейфы для JSONB-снимков: ```sql -- BIT.RA raw CREATE TABLE raw_bitra.employees ( bitra_id text PRIMARY KEY, payload jsonb NOT NULL, synced_at timestamptz DEFAULT now() ); CREATE TABLE raw_bitra.works ( bitra_doc_id text PRIMARY KEY, payload jsonb NOT NULL, synced_at timestamptz DEFAULT now() ); CREATE TABLE raw_bitra.projects ( bitra_id text PRIMARY KEY, payload jsonb NOT NULL, synced_at timestamptz DEFAULT now() ); CREATE TABLE raw_bitra.dictionaries ( kind text NOT NULL, -- 'office' | 'department' | 'manager' | 'stage' | 'config' | 'contract' bitra_id text NOT NULL, payload jsonb NOT NULL, synced_at timestamptz DEFAULT now(), PRIMARY KEY (kind, bitra_id) ); CREATE TABLE raw_bitra.work_types ( bitra_id text PRIMARY KEY, payload jsonb NOT NULL, synced_at timestamptz DEFAULT now() ); CREATE TABLE raw_bitra.dept_history ( bitra_employee_id text NOT NULL, period date NOT NULL, payload jsonb NOT NULL, synced_at timestamptz DEFAULT now(), PRIMARY KEY (bitra_employee_id, period) ); -- EVA raw CREATE TABLE raw_eva.persons ( eva_id text PRIMARY KEY, payload jsonb NOT NULL, synced_at timestamptz DEFAULT now() ); CREATE TABLE raw_eva.projects ( eva_id text PRIMARY KEY, payload jsonb NOT NULL, synced_at timestamptz DEFAULT now() ); CREATE TABLE raw_eva.tasks ( eva_id text PRIMARY KEY, payload jsonb NOT NULL, synced_at timestamptz DEFAULT now() ); -- Bitrix raw CREATE TABLE raw_bitrix.deals ( bitrix_id bigint PRIMARY KEY, payload jsonb NOT NULL, synced_at timestamptz DEFAULT now() ); CREATE TABLE raw_bitrix.users ( bitrix_id bigint PRIMARY KEY, payload jsonb NOT NULL, synced_at timestamptz DEFAULT now() ); CREATE TABLE raw_bitrix.departments ( bitrix_id bigint PRIMARY KEY, payload jsonb NOT NULL, synced_at timestamptz DEFAULT now() ); -- Журнал синхронизаций CREATE TABLE raw_bitra.sync_log ( source text NOT NULL, entity text NOT NULL, last_sync_ts timestamptz, records_count int, status text, error_message text, synced_at timestamptz DEFAULT now() ); CREATE INDEX idx_sync_log_source_entity ON raw_bitra.sync_log (source, entity, synced_at DESC); ``` - [ ] **Step 2:** Apply + verify через `\dt raw_*.*` + commit. ### Task 2.9: stg schemas (плоские нормализованные) **Files:** Create: `sql/migrations/009_stg_schemas.sql` stg-слой — это **views** на raw. Они материализуются как обычные SELECT-запросы. Никаких CREATE TABLE — только VIEW. Это значит структура определяется в `sql/views/`, а в миграции — только пустышка-плейсхолдер для документации. - [ ] **Step 1:** Создать миграцию-документ: ```sql -- 009_stg_schemas.sql -- stg-слой реализован как VIEW в sql/views/stg_*.sql. -- Эта миграция — placeholder для трекинга порядка применения. SELECT 1; ``` - [ ] **Step 2:** Apply + commit. ### Task 2.10: Seed work_types **Files:** Create: `sql/seed/work_types.sql` - [ ] **Step 1:** Полный seed по [reference-bitra-work-types](../../memory/reference_bitra_work_types.md) (14 значений из Enum): ```sql INSERT INTO core.work_type (code, label, category, is_billable) VALUES ('ЛУРВ', 'ЛУРВ (платно)', 'commercial', true), ('ЛТ', 'ЛТ (платно)', 'commercial', true), ('Демо', 'Демо (Пресейл)', 'presale', false), ('ИТС', 'ИТС (договор)', 'commercial', true), ('ИТСПлатныеРаботы', 'ИТС (доп. услуги)', 'commercial', true), ('Сертификация', 'Сертификация', 'internal', false), ('Внутреннее', 'Обучение (кроме сертификации)', 'internal', false), ('НеОпл', 'Бесплатные часы в счёт ПП', 'free', false), ('Установка', 'Установка в счёт ПП', 'free', false), ('Гарантия', 'Гарантия (бесплатно)', 'free', false), ('Управленка', 'Работа руководителя', 'internal', false), ('ВнутренниеРаботы', 'Внутренние работы', 'internal', false), ('Коробка', 'Коробка (рудимент)', 'ignored', false), ('Отложено', 'Отложено (рудимент)', 'ignored', false) ON CONFLICT (code) DO UPDATE SET label = EXCLUDED.label, category = EXCLUDED.category, is_billable = EXCLUDED.is_billable; ``` - [ ] **Step 2:** Apply + verify: ```bash docker exec -i pipeline_postgres psql -U bit_flight_deck_user -d bit_flight_deck -c "SELECT category, count(*) FROM core.work_type GROUP BY category;" ``` Ожидание: 5 строк (commercial=4, presale=1, internal=4, free=3, ignored=2). - [ ] **Step 3:** Commit. --- ## Phase 3: BIT.RA HTTP-сервисы > **ВАЖНО:** эта фаза — 1С-разработка внутри BIT.RA. Делается через **EDT** или **Конфигуратор 1С**. Тестирование — через curl/Postman. Версионирование кода 1С — отдельная задача (например через `db-dump-xml` skill). ### Task 3.1: Создать пользователя API в BIT.RA **Files:** (внутри 1С) - [ ] **Step 1:** В BIT.RA через Конфигуратор создать пользователя `bit_flight_deck_api` с минимальными правами (только чтение релевантных объектов из спецификации). - [ ] **Step 2:** Назначить роль (создать новую) `bit_flight_deck_API_Чтение` с правами: - Чтение для `Catalog.Пользователи`, `Catalog.Подразделение`, `Catalog.Офис`, `Catalog.Менеджеры`, `Catalog.Клиенты`, `Catalog.Проекты`, `Catalog.ЭтапыПроектов`, `Catalog.Конфигурации`, `Catalog.Договоры`, `Catalog.СценарииПланирования`. - Чтение для `Document.Работы`. - Чтение для `AccumulationRegister.Работы`, `InformationRegister.ПодразделениеСотрудников`. - Чтение для `Enum.ВидыРабот`. - [ ] **Step 3:** Установить пароль, записать в `.env` как `BITRA_PASSWORD`. ### Task 3.2: Создать общий модуль IntegrationAPI **Files:** в BIT.RA — `CommonModule.IntegrationAPI` - [ ] **Step 1:** В Конфигураторе создать `Общий модуль → IntegrationAPI`. Установить флаги: «Сервер», «Вызов сервера», «Внешнее соединение». - [ ] **Step 2:** Создать процедуру-хелпер для возврата JSON: ```bsl Функция СформироватьОтвет(Данные) Экспорт ЗаписьJSON = Новый ЗаписьJSON; ПараметрыJSON = Новый ПараметрыЗаписиJSON(ПереносСтрокJSON.БезПереносов); ЗаписьJSON.УстановитьСтроку(ПараметрыJSON); ЗаписатьJSON(ЗаписьJSON, Данные); Ответ = Новый HTTPСервисОтвет(200); Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8"); Ответ.УстановитьТелоИзСтроки(ЗаписьJSON.Закрыть()); Возврат Ответ; КонецФункции ``` ### Task 3.3: HTTP-сервис /api/employees **Files:** в BIT.RA — `HTTPService.IntegrationAPI` (или модифицировать существующий) - [ ] **Step 1:** Создать HTTP-сервис `IntegrationAPI` с корневым URL `/v1`. - [ ] **Step 2:** Шаблон URL `/employees` с методом GET. Обработчик: ```bsl Функция ПолучитьСотрудников(Запрос) МодифицированоПосле = Запрос.ПараметрыЗапроса.Получить("modified_since"); Запрос = Новый Запрос; Запрос.Текст = "ВЫБРАТЬ | Пользователи.Ссылка КАК Идентификатор, | Пользователи.Наименование КАК ФИО, | Пользователи.EVA_ID КАК EvaID, | Пользователи.EVA_Токен КАК EvaТокен, | Пользователи.Офис.Наименование КАК Офис, | Пользователи.Подразделение КАК Подразделение, | Пользователи.Ставка КАК Ставка, | Пользователи.Недействителен КАК Недействителен, | Пользователи.ДолженЗаполнятьОтчет КАК ДолженЗаполнятьОтчет, | Пользователи.ОтчетПродажиПроектов КАК ОтчетПродажиПроектов, | Пользователи.КонтактнаяИнформация.( | Вид.Наименование КАК ВидКИ, | Представление КАК Значение | ) КАК КонтактнаяИнформация |ИЗ | Справочник.Пользователи КАК Пользователи"; // TODO: фильтр по modified_since если потребуется (для пользователей редко) Результат = Новый Массив; Выборка = Запрос.Выполнить().Выбрать(); Пока Выборка.Следующий() Цикл Email = ""; КИВыборка = Выборка.КонтактнаяИнформация.Выбрать(); Пока КИВыборка.Следующий() Цикл Если КИВыборка.ВидКИ = "Email" Или СтрНайти(КИВыборка.ВидКИ, "Email") > 0 Тогда Email = КИВыборка.Значение; Прервать; КонецЕсли; КонецЦикла; Запись = Новый Структура; Запись.Вставить("id", Строка(Выборка.Идентификатор.УникальныйИдентификатор())); Запись.Вставить("full_name", Выборка.ФИО); Запись.Вставить("email", Email); Запись.Вставить("eva_id", Выборка.EvaID); Запись.Вставить("office", Выборка.Офис); Запись.Вставить("department", Строка(Выборка.Подразделение)); Запись.Вставить("rate", Выборка.Ставка); Запись.Вставить("is_active", НЕ Выборка.Недействителен); Запись.Вставить("should_fill_report", Выборка.ДолженЗаполнятьОтчет); Результат.Добавить(Запись); КонецЦикла; Возврат IntegrationAPI.СформироватьОтвет(Результат); КонецФункции ``` - [ ] **Step 3:** Опубликовать на веб-сервере (Apache). Использовать существующий механизм публикации BIT.RA (см. cf-1c-skills `web-publish` если нужно). - [ ] **Step 4:** Тест curl: ```bash curl -u bit_flight_deck_api: http:////hs/IntegrationAPI/v1/employees | jq '.[0]' ``` Ожидание: JSON с полями id, full_name, email и др. - [ ] **Step 5:** Закоммитить spec и описание в репозиторий — добавить в `docs/superpowers/specs/2026-05-13-mvp1-workload-design.md` (или отдельным файлом) конкретные имена эндпоинтов и форматы ответов. ### Task 3.4: HTTP-сервис /api/works **Files:** в BIT.RA — расширить тот же HTTPService - [ ] **Step 1:** Шаблон URL `/works` с GET. Поддержка query param `modified_since` (timestamp ISO 8601). - [ ] **Step 2:** Запрос — `Document.Работы` с табличной частью «Работы», фильтрация по `Дата >= modified_since`: ```bsl Запрос.Текст = "ВЫБРАТЬ | Работы.Ссылка КАК ДокИдентификатор, | Работы.Номер КАК Номер, | Работы.Дата КАК Дата, | Работы.Исполнитель КАК Исполнитель, | Работы.Подразделение КАК Подразделение, | Работы.Офис КАК Офис, | Работы.Утвержден КАК Утвержден, | Работы.Работы.( | НомерСтроки КАК НомерСтроки, | СодержаниеРабот КАК Содержание, | КоличествоЧасов КАК Часы, | ВидРаботы КАК ВидРаботы, | Клиент КАК Клиент, | Клиент.Наименование КАК КлиентИмя, | Менеджер КАК Менеджер, | Проект КАК Проект, | Этап КАК Этап, | НомерЗаявки КАК НомерЗаявки, | ЛТ КАК ЛТ, | РаботаВыполнена КАК РаботаВыполнена | ) КАК Строки |ИЗ | Документ.Работы КАК Работы |ГДЕ | Работы.Дата >= &МодифицированоПосле"; Запрос.УстановитьПараметр("МодифицированоПосле", ?(МодифицированоПосле = Неопределено, '00010101', Дата(МодифицированоПосле))); ``` Сериализация в JSON — аналогично employees, с массивом строк ТЧ. - [ ] **Step 2.5:** Сериализация: ```bsl // ... после выполнения запроса: Результат = Новый Массив; Выборка = Запрос.Выполнить().Выбрать(); Пока Выборка.Следующий() Цикл ЗаписьДок = Новый Структура; ЗаписьДок.Вставить("id", Строка(Выборка.ДокИдентификатор.УникальныйИдентификатор())); ЗаписьДок.Вставить("number", Выборка.Номер); ЗаписьДок.Вставить("date", Формат(Выборка.Дата, "ДФ=yyyy-MM-dd")); ЗаписьДок.Вставить("employee_id", Строка(Выборка.Исполнитель.УникальныйИдентификатор())); ЗаписьДок.Вставить("department", Строка(Выборка.Подразделение)); ЗаписьДок.Вставить("office", Строка(Выборка.Офис)); ЗаписьДок.Вставить("approved", Выборка.Утвержден); Строки = Новый Массив; ВыборкаСтрок = Выборка.Строки.Выбрать(); Пока ВыборкаСтрок.Следующий() Цикл Строка = Новый Структура; Строка.Вставить("row_index", ВыборкаСтрок.НомерСтроки); Строка.Вставить("description", ВыборкаСтрок.Содержание); Строка.Вставить("hours", ВыборкаСтрок.Часы); Строка.Вставить("work_type", Строка(ВыборкаСтрок.ВидРаботы)); Строка.Вставить("client_id", ?(ЗначениеЗаполнено(ВыборкаСтрок.Клиент), Строка(ВыборкаСтрок.Клиент.УникальныйИдентификатор()), "")); Строка.Вставить("client_name", ВыборкаСтрок.КлиентИмя); Строка.Вставить("project_id", ?(ЗначениеЗаполнено(ВыборкаСтрок.Проект), Строка(ВыборкаСтрок.Проект.УникальныйИдентификатор()), "")); Строка.Вставить("stage_id", ?(ЗначениеЗаполнено(ВыборкаСтрок.Этап), Строка(ВыборкаСтрок.Этап.УникальныйИдентификатор()), "")); Строка.Вставить("request_number", ВыборкаСтрок.НомерЗаявки); Строка.Вставить("lt_id", ?(ЗначениеЗаполнено(ВыборкаСтрок.ЛТ), Строка(ВыборкаСтрок.ЛТ.УникальныйИдентификатор()), "")); Строка.Вставить("work_done", ВыборкаСтрок.РаботаВыполнена); Строки.Добавить(Строка); КонецЦикла; ЗаписьДок.Вставить("rows", Строки); Результат.Добавить(ЗаписьДок); КонецЦикла; Возврат IntegrationAPI.СформироватьОтвет(Результат); ``` - [ ] **Step 3:** Тест: ```bash curl -u bit_flight_deck_api: "http:////hs/IntegrationAPI/v1/works?modified_since=2026-05-01" | jq 'length, .[0].rows | length' ``` Ожидание: число документов и число строк. - [ ] **Step 4:** Закоммитить в `n8n/workflows/01-pull-bitra-works.json` ожидаемый формат (потом). ### Task 3.5–3.8: HTTP-сервисы /api/projects, /api/dictionaries, /api/work_types, /api/dept_history (структура аналогична Task 3.3-3.4: запрос, сериализация JSON, тест curl). Минимальные требования: - `/api/projects` — Catalog.Проекты с реквизитами (Конфигурация, РуководительПроекта, МенеджерПроекта, Клиент, Договор, ДатаСтарта, ДатаФиниш, Бюджет, EVA_ID, СтатусПроекта). - `/api/dictionaries` — JSON-объект с ключами `office`, `department`, `manager`, `stage`, `config`, `contract` — каждый массив объектов из соответствующего справочника. - `/api/work_types` — Enum.ВидыРабот: `[{"code":"ЛУРВ","label":"ЛУРВ (платно)","order":...}, ...]`. - `/api/dept_history` — записи `InformationRegister.ПодразделениеСотрудников` с фильтром по `Период >= modified_since`. Для каждого — отдельный коммит. --- ## Phase 4: N8N pull workflows для BIT.RA > **Workflows создаются в UI N8N (http://localhost:5678), экспортируются как JSON и коммитятся в `n8n/workflows/`.** ### Task 4.1: Workflow «Pull BIT.RA employees» **Files:** Create: `n8n/workflows/01-pull-bitra-employees.json` (после экспорта) - [ ] **Step 1:** В N8N создать новый workflow. Добавить ноду **Schedule Trigger** (cron `0 */6 * * *` — раз в 6 часов). - [ ] **Step 2:** Добавить ноду **HTTP Request** (GET): - URL: `{{$env.BITRA_BASE_URL}}/employees` - Authentication: Basic Auth (`BITRA_USER` / `BITRA_PASSWORD`). - Response Format: JSON. - [ ] **Step 3:** Добавить ноду **Postgres** с операцией Insert/Update: - Credentials: `pipeline_postgres` (использовать DB `bit_flight_deck` + `bit_flight_deck_user`). - SQL: ```sql INSERT INTO raw_bitra.employees (bitra_id, payload, synced_at) SELECT elem->>'id' AS bitra_id, elem AS payload, now() AS synced_at FROM json_array_elements($1::json) AS elem ON CONFLICT (bitra_id) DO UPDATE SET payload = EXCLUDED.payload, synced_at = now(); ``` - Параметр $1 — JSON-массив от HTTP-узла. - [ ] **Step 4:** Тестовый запуск (Execute Workflow). Verify: ```bash docker exec -i pipeline_postgres psql -U bit_flight_deck_user -d bit_flight_deck -c "SELECT count(*) FROM raw_bitra.employees;" ``` Ожидание: > 0. - [ ] **Step 5:** Активировать workflow. - [ ] **Step 6:** Экспорт workflow в JSON (Settings → Download). Сохранить как `n8n/workflows/01-pull-bitra-employees.json`. Commit. ### Task 4.2–4.6: Workflows для BIT.RA works, projects, dictionaries, work_types, dept_history Аналогично 4.1. Для works/dept_history дополнительно передавать `modified_since` — последний `max(synced_at)` из соответствующей таблицы: - Перед HTTP-нодой добавить **Postgres-узел** SELECT: `SELECT coalesce(max(synced_at), '2020-01-01') FROM raw_bitra.`. - Этот таймстамп подставлять в URL HTTP-узла. Каждый workflow: - Schedule trigger (`*/30 * * * *` для works — каждые 30 мин; раз в сутки для редко меняющихся). - HTTP request с auth. - Postgres upsert в соответствующую raw-таблицу. - Запись в `raw_bitra.sync_log`. --- ## Phase 5: N8N pull EVA ### Task 5.1: Workflow «Pull EVA nightly full» **Files:** Create: `n8n/workflows/05-pull-eva-nightly-full.json` - [ ] **Step 1:** Schedule trigger — `0 2 * * *` (раз в сутки в 02:00). - [ ] **Step 2:** Для каждой сущности (CmfPerson, CmfProject, CmfTask, CmfStatus, CmfStatusHistory) — HTTP Request POST: - URL: `https://firstbit.evateam.ru/api/` - Headers: `Authorization: Bearer {{$env.EVA_ADMIN_TOKEN}}`, `Content-Type: application/json` - Body (JSON): ```json { "jsonrpc": "2.2", "method": "CmfPerson.list", "callid": "{{$workflow.id}}-{{$execution.id}}", "kwargs": { "fields": ["id","login","email","name","first_name","last_name","does_not_work","cmf_archived","work_position","primary_role_id","calendar_id"], "slice": [0, 1000] } } ``` - [ ] **Step 3:** Postgres upsert в `raw_eva.persons` (аналогично BIT.RA). - [ ] **Step 4:** Повторить для projects, tasks, status_history. Параметризовать через split-into-batches если задач много (27k). - [ ] **Step 5:** Verify count в raw_eva.* > 0. Export workflow. Commit. ### Task 5.2: Workflow «Pull EVA incremental via CmfAudit» **Files:** Create: `n8n/workflows/04-pull-eva-incremental.json` - [ ] **Step 1:** Schedule — `*/30 * * * *` (каждые 30 минут). - [ ] **Step 2:** Postgres SELECT — `SELECT coalesce(max(audit_at), now() - interval '1 day') FROM raw_eva.sync_log WHERE source='eva' AND entity='audit'`. - [ ] **Step 3:** HTTP POST на `CmfAudit.list` с фильтром `cmf_created_at >= `. Получаем список ID + class_name. - [ ] **Step 4:** Split по class_name. Для каждого — соответствующий `.get` по id. - [ ] **Step 5:** Запись в raw_eva.{persons,projects,tasks} в зависимости от class_name. - [ ] **Step 6:** Update sync_log. Export. Commit. --- ## Phase 6: N8N Bitrix24 — webhooks + reconcile ### Task 6.1: Workflow «Bitrix webhook handler» **Files:** Create: `n8n/workflows/06-bitrix-webhook-handler.json` - [ ] **Step 1:** Добавить **Webhook trigger** ноду. Method POST. Path: `/webhook/bitrix/` (token из `.env`). - [ ] **Step 2:** В настройках workflow добавить **Production URL** — `https://n8n.bigmadnekenny.ru/webhook/bitrix/`. - [ ] **Step 3:** Body распарсить — Bitrix передаёт `event`, `data[FIELDS][ID]`. Извлечь deal_id или user_id. - [ ] **Step 4:** Switch-нода по event: - `ONCRMDEALADD|UPDATE|DELETE` → следующая ветка для сделок. - `ONUSERADD|UPDATE|DELETE` → ветка для пользователей. - [ ] **Step 5:** Для сделок: HTTP GET `{{$env.BITRIX_WEBHOOK_URL}}crm.deal.get.json?id=` со всеми UF-полями. - [ ] **Step 6:** Postgres upsert в `raw_bitrix.deals`. - [ ] **Step 7:** Для пользователей: `user.get.json?ID=` + upsert в `raw_bitrix.users`. - [ ] **Step 8:** В Bitrix создать outbound webhooks (в админ-панели → Разработчикам → Исходящие вебхуки → создать), указав URL: `https://n8n.bigmadnekenny.ru/webhook/bitrix/`. События: `ONCRMDEALADD/UPDATE/DELETE`, `ONUSERADD/UPDATE/DELETE`. - [ ] **Step 9:** Verify — изменить тестовую сделку в Битриксе → через 1-2 минуты в raw_bitrix.deals должна появиться запись. - [ ] **Step 10:** Export. Commit. ### Task 6.2: Workflow «Bitrix nightly reconcile» **Files:** Create: `n8n/workflows/07-pull-bitrix-nightly-reconcile.json` - [ ] **Step 1:** Schedule `0 3 * * *` (03:00). - [ ] **Step 2:** HTTP POST на `{{$env.BITRIX_WEBHOOK_URL}}crm.deal.list.json` с filter `[CATEGORY_ID]=16` + select `[*, UF_*]` + пагинация (`start`). - [ ] **Step 3:** Postgres upsert в `raw_bitrix.deals`. - [ ] **Step 4:** Параллельно — `user.get.json?FILTER[ACTIVE]=Y&FILTER[USER_TYPE]=employee` → `raw_bitrix.users`. - [ ] **Step 5:** `department.get.json` → `raw_bitrix.departments`. - [ ] **Step 6:** Export. Commit. --- ## Phase 7: SQL Transformations ### Task 7.1: Views stg_bitra.* **Files:** Create: `sql/views/stg_bitra_employee.sql` - [ ] **Step 1:** Создать view: ```sql CREATE OR REPLACE VIEW stg_bitra.employee AS SELECT payload->>'id' AS bitra_id, payload->>'full_name' AS full_name, lower(nullif(payload->>'email', '')) AS email, nullif(payload->>'eva_id', '') AS eva_id, payload->>'office' AS office, payload->>'department' AS department, (payload->>'rate')::decimal(10,2) AS rate, (payload->>'is_active')::boolean AS is_active, (payload->>'should_fill_report')::boolean AS should_fill_report, synced_at FROM raw_bitra.employees; ``` - [ ] **Step 2:** Apply (psql) + verify `SELECT count(*) FROM stg_bitra.employee;`. - [ ] **Step 3:** Commit. ### Task 7.2–7.6: Аналогичные views для stg_bitra.works, stg_eva.person, stg_eva.task, stg_eva.project, stg_bitrix.deal, stg_bitrix.user Каждая — SQL-view, извлекающая поля из JSONB payload в плоские колонки. ### Task 7.7: Stored procedure `core.merge_employee()` **Files:** Create: `sql/procedures/core_merge_employee.sql` - [ ] **Step 1:** ```sql CREATE OR REPLACE FUNCTION core.merge_employee() RETURNS void AS $$ BEGIN -- 1. Upsert по bitra_id, identity_map auto на email INSERT INTO core.employee ( email, full_name, bitra_user_id, rate, is_active, last_synced ) SELECT DISTINCT ON (lower(b.email)) b.email, b.full_name, b.bitra_id, b.rate, b.is_active, now() FROM stg_bitra.employee b WHERE b.email IS NOT NULL ON CONFLICT (email) DO UPDATE SET full_name = EXCLUDED.full_name, bitra_user_id = EXCLUDED.bitra_user_id, rate = EXCLUDED.rate, is_active = EXCLUDED.is_active, last_synced = now(); -- 2. Дополнить EVA-id для тех у кого есть email-mapping UPDATE core.employee ce SET eva_person_id = e.eva_id FROM stg_eva.person e WHERE lower(ce.email) = lower(e.login) AND ce.eva_person_id IS DISTINCT FROM e.eva_id; -- 3. Дополнить Bitrix-id UPDATE core.employee ce SET bitrix_user_id = u.bitrix_id FROM stg_bitrix.user u WHERE lower(ce.email) = lower(u.email) AND ce.bitrix_user_id IS DISTINCT FROM u.bitrix_id; -- 4. identity_map записи для unmatched сотрудников (auto-confidence) -- (упрощено для MVP: ручное разрешение через NocoDB) END; $$ LANGUAGE plpgsql; ``` - [ ] **Step 2:** Apply + тест: ```sql SELECT core.merge_employee(); SELECT count(*) FROM core.employee; ``` - [ ] **Step 3:** Commit. ### Task 7.8–7.12: Аналогичные procedures для project, work_log, task, deal Каждая — SP, которая делает merge из stg в core. ### Task 7.13: Mart view `mart.workload_actual` **Files:** Create: `sql/views/mart_workload_actual.sql` - [ ] **Step 1:** ```sql CREATE OR REPLACE VIEW mart.workload_actual AS WITH last30 AS ( SELECT w.employee_id, sum(w.hours) FILTER (WHERE wt.category = 'commercial') AS hours_commercial, sum(w.hours) AS hours_total, sum(w.hours) FILTER (WHERE wt.category = 'commercial')::decimal / nullif(sum(w.hours), 0) * 100 AS pct_commercial FROM core.work_log w JOIN core.work_type wt ON wt.code = w.work_type_code WHERE w.work_date >= current_date - interval '30 days' AND wt.category != 'ignored' GROUP BY w.employee_id ) SELECT e.id AS employee_id, e.full_name, e.email, d.name AS department, o.name AS office, coalesce(l.hours_commercial, 0) AS hours_commercial_30d, coalesce(l.hours_total, 0) AS hours_total_30d, coalesce(l.pct_commercial, 0) AS pct_commercial_30d FROM core.employee e LEFT JOIN core.department d ON d.id = e.department_id LEFT JOIN core.office o ON o.id = e.office_id LEFT JOIN last30 l ON l.employee_id = e.id WHERE e.is_active = true AND e.is_target_for_mvp1 = true; ``` - [ ] **Step 2:** Apply + verify count > 0 (после первой синхронизации) + commit. ### Task 7.14–7.16: Mart views для current/planned/forecast - `mart.workload_current` — задачи EVA в IN_PROGRESS с responsible_id. - `mart.workload_planned` — задачи EVA в OPEN + IN_PROGRESS с responsible_id («осталось»). - `mart.workload_forecast` — сделки Bitrix CAT=16 в стадиях forecast (см. seed) с распределением через deal_team_member.weight. ### Task 7.17: N8N workflow «Trigger transforms» **Files:** Create: `n8n/workflows/08-trigger-transforms.json` - [ ] **Step 1:** Schedule `*/10 * * * *` (каждые 10 мин). - [ ] **Step 2:** Postgres-нода: ```sql SELECT core.merge_employee(); SELECT core.merge_project(); SELECT core.merge_work_log(); SELECT core.merge_task(); SELECT core.merge_deal(); ``` - [ ] **Step 3:** Export. Commit. --- ## Phase 8: Metabase dashboard ### Task 8.1: Подключить БД к Metabase - [ ] **Step 1:** Открыть http://localhost:3001 → Admin → Databases → Add. - [ ] **Step 2:** Host: `pipeline_postgres`, Port: 5432, DB: `bit_flight_deck`, User: `bit_flight_deck_user`. - [ ] **Step 3:** Тест connection — должно быть OK. ### Task 8.2: Создать дашборд «Загрузка сотрудников MVP-1» - [ ] **Step 1:** Создать новый Dashboard «Загрузка сотрудников MVP-1». - [ ] **Step 2:** Добавить виджеты: 1. **Plate**: «Средний % коммерческой загрузки за 30д» — SQL: `SELECT avg(pct_commercial_30d) FROM mart.workload_actual;` 2. **Plate**: «N безхозных задач EVA» — SQL: `SELECT count(*) FROM mart.tasks_orphan;` 3. **Table**: «Загрузка по сотрудникам» — SELECT по composite view `mart.workload_summary` со всеми 4 слоями. 4. **Line chart**: «Динамика % коммерческой загрузки» за 90 дней по неделям. 5. **Bar chart**: «Топ свободных» и «Топ перегруженных». - [ ] **Step 3:** Применить условное форматирование (красный/жёлтый/зелёный по pct_commercial). - [ ] **Step 4:** Сохранить дашборд, экспортировать в JSON (Metabase serialization API или вручную через UI) → `n8n/workflows/metabase-dashboard.json` или отдельная папка `metabase/`. - [ ] **Step 5:** Commit. --- ## Phase 9: NocoDB Admin UI ### Task 9.1: Подключить NocoDB к БД - [ ] **Step 1:** Открыть http://localhost:8090, login. - [ ] **Step 2:** Создать Base → External Database → PostgreSQL → connection params как у Metabase. ### Task 9.2: Создать представления - [ ] **Step 1:** Создать таблицу `core.identity_map` в NocoDB как Linked Table. - [ ] **Step 2:** Filter view «Identity issues» — `confidence = 'manual'`. - [ ] **Step 3:** Создать таблицу `core.deal_team_member` с editable `weight` и `is_manual_override`. - [ ] **Step 4:** Сохранить view configs. --- ## Phase 10: Bootstrap identity и acceptance ### Task 10.1: Запустить initial sync - [ ] **Step 1:** Запустить вручную все workflows BIT.RA в N8N (Execute Workflow). - [ ] **Step 2:** Запустить EVA nightly full. - [ ] **Step 3:** Запустить Bitrix nightly reconcile. - [ ] **Step 4:** Запустить трансформации (`core.merge_*`). ### Task 10.2: Промаркировать ~20 сотрудников MVP-1 - [ ] **Step 1:** Получить от пользователя список email или department-id. - [ ] **Step 2:** SQL update: ```sql UPDATE core.employee SET is_target_for_mvp1 = true WHERE lower(email) IN ('user1@1cbit.ru', 'user2@1cbit.ru', ...); ``` - [ ] **Step 3:** Verify через дашборд — таблица должна показать ~20 строк. ### Task 10.3: Acceptance — все 8 критериев из спеки - [ ] **Step 1:** Проверить каждое acceptance criterion из секции 11 [spec](../specs/2026-05-13-mvp1-workload-design.md). - [ ] **Step 2:** Зафиксировать результаты в `docs/superpowers/specs/2026-05-13-mvp1-workload-design.md` (раздел Status → Live). ### Task 10.4: Финальный коммит и тэг - [ ] **Step 1:** `git tag -a mvp1-v1.0 -m "MVP-1 workload — first production release"` - [ ] **Step 2:** `git push --tags` --- ## Self-Review ### Spec coverage - ✅ Section 1 (Контекст) → не требует имплементации, документ. - ✅ Section 2 (Цели/не-цели) → ограничено scope; Phase 0-10 покрывает цели. - ✅ Section 3 (Бизнес-вопросы и витрины) → Phase 7 mart views + Phase 8 дашборд. - ✅ Section 4 (Архитектура) → Phase 0 (Metabase/NocoDB), Phase 1 (CF Tunnel), Phase 2 (PG), Phase 3 (BIT.RA), Phase 4-6 (N8N). - ✅ Section 5 (Транспорт) → Phase 3-6. - ✅ Section 6 (Data Model) → Phase 2 (core DDL), Phase 7 (mart views). - ✅ Section 7 (Identity-resolution) → Phase 7.7 (core.merge_employee), Phase 9 (NocoDB UI). - ✅ Section 8 (SQL-трансформации) → Phase 7. - ✅ Section 9 (Дашборд) → Phase 8. - ✅ Section 10 (Operational) → задеваем; backup упоминаем как ручной cron. - ✅ Section 11 (Acceptance) → Phase 10.3. - ✅ Section 12 (Параллельные орг-задачи) → задача пользователя, не код. - ✅ Section 13 (Open Questions) → задачи пользователя (whitelist сотрудников, SD-проекты, стадии Битрикса). - ✅ Section 14 (Backlog) → не в этом плане, по дизайну. ### Placeholder scan - В Task 3.5-3.8 написано «структура аналогична Task 3.3-3.4» — это допустимая отсылка к ОБРАЗЦУ кода, но для атомарной задачи стоит дублировать. Помечаю как known shortcut — engineer должен следовать pattern из 3.3 и 3.4. - В Task 4.2-4.6, 5.2, 7.2-7.6 — «аналогично» отсылки. Это компромисс между читаемостью и DRY. ### Type consistency - `core.employee.id` (bigserial) — везде bigint. ✅ - `core.task.cache_status_type` — везде text с CHECK на 4 значения. ✅ - `core.deal.bitrix_id` (bigint) — везде bigint. ✅ - Имя `core.merge_employee()` — везде с подчёркиванием. ✅ --- ## Execution Handoff **Plan complete and saved to `docs/superpowers/plans/2026-05-13-mvp1-workload.md`. Two execution options:** **1. Subagent-Driven (recommended)** — я диспетчирую свежего субагента на каждую задачу, ревью между задачами, быстрая итерация. **2. Inline Execution** — выполняем задачи в текущей сессии через executing-plans, batch-выполнение с чекпойнтами для ревью. **Какой подход?**