Files
2026-05-13 19:47:53 +05:00

61 KiB
Raw Permalink Blame History

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


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/:

    mkdir -p ~/projects && cd ~/projects
    
  • Step 2: Клонировать репо:

    git clone https://gitea.bigmadnekenny.ru/admin/bit-flight-deck.git
    cd bit-flight-deck
    
  • Step 3: Проверить что docs/, README.md, .gitignore на месте:

    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:

    -- Запускается ОДИН раз на 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: Сгенерировать пароль:

    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:

    docker exec -i pipeline_postgres psql -U <SUPERUSER из ~/infrastructure/.env> -d postgres < infra/init-bit-flight-deck-db.sql
    
  • Step 5: Verify — подключиться от имени нового пользователя:

    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:

    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=<set in .env>
    PG_DB=bit_flight_deck
    PG_HOST=pipeline_postgres
    PG_PORT=5432
    
    # BIT.RA HTTP-сервисы
    BITRA_BASE_URL=http://<host>/<dbname>/hs/IntegrationAPI/v1
    BITRA_USER=<api-user>
    BITRA_PASSWORD=<set in .env>
    
    # EVA Desk
    EVA_BASE_URL=https://firstbit.evateam.ru/api/
    EVA_ADMIN_TOKEN=<set in .env>
    
    # Bitrix24
    BITRIX_WEBHOOK_URL=https://vdst421.1cbit.ru/rest/91/<token>/
    BITRIX_WEBHOOK_SECRET=<set in .env, в URL для outbound webhooks>
    
    # Metabase
    METABASE_SITE_NAME="bit-flight-deck"
    METABASE_PORT=3001
    
    # NocoDB
    NOCODB_PORT=8090
    NOCODB_ADMIN_EMAIL=roachesnokov@gmail.com
    NOCODB_ADMIN_PASSWORD=<set in .env>
    
  • Step 2: Commit:

    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:

    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: Запустить:

    cd ~/projects/bit-flight-deck/
    docker compose up -d
    
  • Step 3: Verify Metabase:

    curl -s http://localhost:3001/api/health
    

    Ожидание: {"status":"ok"}. Дать ~1 минуту на старт.

  • Step 4: Verify NocoDB:

    curl -s http://localhost:8090/api/v1/health
    

    Ожидание: 200.

  • Step 5: Открыть Metabase в браузере (http://localhost:3001), пройти initial setup wizard. Подключение к БД настроится автоматически через env-переменные.

  • Step 6: Commit:

    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:

    sudo cat /etc/cloudflared/config.yml
    
  • Step 2: Добавить новый ingress перед service: http_status:404:

    - hostname: n8n.bigmadnekenny.ru
      service: http://localhost:5678
    
  • Step 3: Создать DNS-роут (через Cloudflare API или CLI):

    cloudflared tunnel route dns <tunnel-name> n8n.bigmadnekenny.ru
    

    Имя туннеля — посмотреть в cloudflared tunnel list.

  • Step 4: Перезапустить сервис:

    sudo systemctl restart cloudflared
    sudo systemctl status cloudflared
    

    Ожидание: active (running).

  • Step 5: Verify извне (например с телефона):

    curl -I https://n8n.bigmadnekenny.ru/
    

    Ожидание: HTTP 200 или 401 от N8N (зависит от настроек auth).

  • Step 6: Создать документацию infra/cloudflared-routes.md:

    # 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/<secret-token>
    
    Конфиг ingress в `/etc/cloudflared/config.yml`:
    ```yaml
    - hostname: n8n.bigmadnekenny.ru
      service: http://localhost:5678
    
    
    
  • Step 7: Commit:

    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:

    -- 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: Применить:

    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:

    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:

    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:

    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:

    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:

    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:

    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:

    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:

    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-снимков:

    -- 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: Создать миграцию-документ:

    -- 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 (14 значений из Enum):

    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:

    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:

    Функция СформироватьОтвет(Данные) Экспорт
        Запись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. Обработчик:

    Функция ПолучитьСотрудников(Запрос)
        МодифицированоПосле = Запрос.ПараметрыЗапроса.Получить("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:

    curl -u bit_flight_deck_api:<password> http://<host>/<dbname>/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:

    Запрос.Текст =
        "ВЫБРАТЬ
        |   Работы.Ссылка КАК ДокИдентификатор,
        |   Работы.Номер КАК Номер,
        |   Работы.Дата КАК Дата,
        |   Работы.Исполнитель КАК Исполнитель,
        |   Работы.Подразделение КАК Подразделение,
        |   Работы.Офис КАК Офис,
        |   Работы.Утвержден КАК Утвержден,
        |   Работы.Работы.(
        |       НомерСтроки КАК НомерСтроки,
        |       СодержаниеРабот КАК Содержание,
        |       КоличествоЧасов КАК Часы,
        |       ВидРаботы КАК ВидРаботы,
        |       Клиент КАК Клиент,
        |       Клиент.Наименование КАК КлиентИмя,
        |       Менеджер КАК Менеджер,
        |       Проект КАК Проект,
        |       Этап КАК Этап,
        |       НомерЗаявки КАК НомерЗаявки,
        |       ЛТ КАК ЛТ,
        |       РаботаВыполнена КАК РаботаВыполнена
        |   ) КАК Строки
        |ИЗ
        |   Документ.Работы КАК Работы
        |ГДЕ
        |   Работы.Дата >= &МодифицированоПосле";
    Запрос.УстановитьПараметр("МодифицированоПосле", ?(МодифицированоПосле = Неопределено, '00010101', Дата(МодифицированоПосле)));
    

    Сериализация в JSON — аналогично employees, с массивом строк ТЧ.

  • Step 2.5: Сериализация:

    // ... после выполнения запроса:
    Результат = Новый Массив;
    Выборка = Запрос.Выполнить().Выбрать();
    Пока Выборка.Следующий() Цикл
        ЗаписьДок = Новый Структура;
        ЗаписьДок.Вставить("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: Тест:

    curl -u bit_flight_deck_api:<password> "http://<host>/<dbname>/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.53.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:
      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:

    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.24.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.<table>.
  • Этот таймстамп подставлять в 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):
      {
        "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 >= <last_sync>. Получаем список 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/<secret-token> (token из .env).

  • Step 2: В настройках workflow добавить Production URLhttps://n8n.bigmadnekenny.ru/webhook/bitrix/<secret-token>.

  • 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=<deal_id> со всеми UF-полями.

  • Step 6: Postgres upsert в raw_bitrix.deals.

  • Step 7: Для пользователей: user.get.json?ID=<user_id> + upsert в raw_bitrix.users.

  • Step 8: В Bitrix создать outbound webhooks (в админ-панели → Разработчикам → Исходящие вебхуки → создать), указав URL: https://n8n.bigmadnekenny.ru/webhook/bitrix/<secret-token>. События: 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]=employeeraw_bitrix.users.

  • Step 5: department.get.jsonraw_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:

    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.27.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:

    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 + тест:

    SELECT core.merge_employee();
    SELECT count(*) FROM core.employee;
    
  • Step 3: Commit.

Task 7.87.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:

    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.147.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-нода:

    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:
    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.
  • 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-выполнение с чекпойнтами для ревью.

Какой подход?