from datetime import date from hashlib import sha1 from django.db import transaction from plane.db.models import ( DEFAULT_STATES, Issue, IssueAssignee, IssueView, Project, ProjectMember, State, User, Workspace, ) SOURCE = "nodedc-platform-plan" WORKSPACE_SLUG = "nodedc" PROJECT_IDENTIFIER = "NDCPLATFORM" PROJECT_NAME = "NDC platform" CODEX_EMAIL = "codex@nodedc.local" PLATFORM_OWNER_EMAIL = "dcctouch@gmail.com" STATE_TEMPLATES = [ {"group": "backlog", "name": "В обсуждении", "color": "#60646C", "default": True}, {"group": "unstarted", "name": "К выполнению", "color": "#60646C", "default": False}, {"group": "started", "name": "В работе", "color": "#F59E0B", "default": False}, {"group": "completed", "name": "Готово", "color": "#46A758", "default": False}, {"group": "cancelled", "name": "Отложено", "color": "#9AA4BC", "default": False}, {"group": "triage", "name": "Триаж", "color": "#4E5355", "default": False}, ] def html(*paragraphs): return "".join(f"
{paragraph}
" for paragraph in paragraphs) def stable_id(*parts): value = "::".join(str(part) for part in parts) return sha1(value.encode("utf-8")).hexdigest()[:12] def text_block(slug, title, body): return { "id": f"{slug}-text-{stable_id(slug, title)}", "type": "text", "title": title, "body": body.strip(), } def checker(slug, title, items, checked=False): normalized_items = [] for index, item in enumerate(items): if isinstance(item, dict): item_text = item["text"] item_checked = item.get("checked", checked) else: item_text = item item_checked = checked normalized_items.append( { "id": f"{slug}-item-{index + 1}-{stable_id(slug, title, item_text)}", "text": item_text, "checked": item_checked, } ) return { "id": f"{slug}-checker-{stable_id(slug, title)}", "type": "checker", "title": title, "items": normalized_items, } CARDS = [ { "slug": "phase-0-discovery-platform-skeleton", "name": "Platform skeleton и discovery", "priority": "high", "state_group": "completed", "assignees": [CODEX_EMAIL], "description_html": html( "Нулевой этап платформы NODE.DC: зафиксировать текущую структуру Launcher и Task Manager, создать platform skeleton и перенести архитектурный план из ТЗ в рабочие документы.", "Граница scope: не переносить Launcher и Plane физически, не менять бизнес-логику приложений, не трогать Plane auth/users до отдельного backup и миграционного этапа.", "Критерий приемки: в NODEDC есть platform/docs, отдельный git remote NODEDC_PLATFORM, понятный discovery report, auth model, local deployment plan, security checklist и migration plan для существующего Plane user.", ), "blocks": [ text_block( "phase0", "Текущая архитектура", """ NODEDC сейчас используется как внешний workspace-корень. Launcher находится отдельным git-репозиторием в /Users/dcconstructions/Downloads/mnt/data/nodedc_launcher и является Vite/React GUI без backend слоя. Task Manager находится отдельным git-репозиторием в /Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER. Plane runtime лежит в plane-app, source fork лежит в plane-src, локальный стенд доступен на http://localhost:8090. Физический перенос репозиториев на нулевом этапе не нужен: он может сломать Plane runtime, env, volumes и backup. Платформенный слой создается рядом, а не вместо текущих приложений. Платформенный слой подключен как отдельный git-репозиторий: https://git.dcserve.ru/SILVER/NODEDC_PLATFORM.git. Локальная ветка: main. """, ), text_block( "phase0", "Этап 0. Discovery + platform skeleton", """ Статус: реализовано. Этап фиксирует baseline без изменения Launcher и Plane. Результат должен стать стартовой точкой для последующих infra/auth задач: вся архитектура и ограничения описаны в platform/docs, а будущая реализация разбита на крупные проектные карточки в Task Manager. """, ), checker( "phase0", "Чекер этапа 0. Discovery + platform skeleton", [ "Проверить базовые ТЗ в NODEDC/DOC/BASE.", "Найти текущие пути Launcher и Task Manager.", "Подтвердить, что Launcher и Task Manager являются отдельными git-репозиториями.", "Зафиксировать решение не переносить Plane внутрь Launcher.", "Создать platform skeleton в NODEDC/platform.", "Подключить NODEDC/platform к отдельной repo NODEDC_PLATFORM.", "Добавить ARCHITECTURE.md, AUTH_MODEL.md и DEPLOYMENT_LOCAL.md.", "Добавить SECURITY_CHECKLIST.md и MIGRATION_PLANE_USER.md.", "Добавить DISCOVERY_REPORT.md с текущими путями, рисками и следующим шагом.", "Создать проект NDC platform и крупные карточки roadmap в Task Manager.", ], checked=True, ), text_block( "phase0", "Реализация этапа 0", """ Создан platform skeleton в /Users/dcconstructions/Downloads/mnt/NODEDC/platform. Папка platform инициализирована как отдельный git-репозиторий на ветке main. Remote origin: https://git.dcserve.ru/SILVER/NODEDC_PLATFORM.git. Первый commit создан и отправлен в origin/main: 0f89c4d, "АРХ - NODEDC PLATFORM: каркас платформенного репозитория". Remote проверен через git smart HTTP endpoint: refs/heads/main указывает на 0f89c4d. Добавлены документы: README.md, docs/DISCOVERY_REPORT.md, docs/ARCHITECTURE.md, docs/AUTH_MODEL.md, docs/DEPLOYMENT_LOCAL.md, docs/SECURITY_CHECKLIST.md, docs/MIGRATION_PLANE_USER.md, infra/README.md, infra/.env.example, packages/auth-sdk/README.md, tasks/CODEX_PLATFORM_AUTH_TASK.md. Discovery подтвердил: Launcher сейчас Vite/React GUI без backend, Task Manager уже запущен как Plane CE self-host на localhost:8090, workspace nodedc существует, пользователь codex@nodedc.local доступен. """, ), ], }, { "slug": "infra-authentik-reverse-proxy", "name": "Platform infra: Authentik и proxy", "priority": "high", "state_group": "completed", "assignees": [CODEX_EMAIL], "description_html": html( "Инфраструктурный слой для локального Authentik и reverse proxy. Этот блок должен дать единые локальные домены, маршрутизацию приложений и внешний защитный слой перед Launcher и Task Manager.", "Критерий приемки: auth.local.nodedc, launcher.local.nodedc и task.local.nodedc открываются через proxy, Authentik работает за корректными forwarded headers, прямой пользовательский вход идет через домены, а не через внутренние порты.", ), "blocks": [ text_block( "infra", "Текущая архитектура", """ Сейчас Launcher и Task Manager живут как отдельные localhost-сервисы. Task Manager доступен через Plane proxy на http://localhost:8090. Единого Authentik, app domains и reverse proxy layer пока нет. Этот этап не меняет Plane auth flow. Он подготавливает внешний слой и Authentik bootstrap, к которому потом подключаются Launcher и Plane. """, ), text_block( "infra", "Этап 1. Local domains + reverse proxy", """ Статус: реализовано. Локальная схема доменов и proxy routing описаны в platform/infra. На первом проходе текущие Launcher и Task Manager проксируются как внешние localhost-upstream через host.docker.internal, без физического переноса репозиториев. Runtime launch закрыт через локальный Caddy-based image nodedc/plane-proxy:ru. Docker compose поднят, Authentik server/worker/Postgres healthy, reverse proxy слушает host port 80. Локальные домены прописаны в /etc/hosts. Прямые URL auth.local.nodedc, launcher.local.nodedc и task.local.nodedc проходят через host DNS без ручной подмены Host header. """, ), checker( "infra1", "Чекер этапа 1. Local domains + reverse proxy", [ {"text": "Согласовать список локальных доменов auth/launcher/task.", "checked": True}, {"text": "Подготовить /etc/hosts инструкцию.", "checked": True}, {"text": "Выбрать proxy: nginx, caddy или traefik.", "checked": True}, {"text": "Настроить routing auth.local.nodedc.", "checked": True}, {"text": "Настроить routing launcher.local.nodedc.", "checked": True}, {"text": "Настроить routing task.local.nodedc.", "checked": True}, {"text": "Прокинуть Host, X-Forwarded-Proto и X-Forwarded-For.", "checked": True}, {"text": "Проверить WebSocket headers для Authentik/Plane live.", "checked": True}, {"text": "Поднять docker compose и проверить auth/task через curl.", "checked": True}, {"text": "Прописать auth/launcher/task local domains в /etc/hosts для браузерного теста.", "checked": True}, ], ), text_block( "infra", "Этап 2. Authentik bootstrap", """ Статус: реализовано. Добавлен local compose для Authentik 2026.2 по актуальной официальной схеме: PostgreSQL 16, server и worker. Redis из раннего ТЗ не добавлен, потому что в текущем официальном compose Authentik 2026.2 Redis отсутствует. Создан временный локальный Authentik admin-пользователь под ручную проверку входа. Ручной login через http://auth.local.nodedc подтвержден пользователем 2026-05-04. Добавлен воспроизводимый local bootstrap для NODE.DC groups, Launcher/Task Manager OAuth2 providers, application tiles, group access bindings и OIDC client secrets. На текущем Authentik bootstrap выполнен. """, ), checker( "infra2", "Чекер этапа 2. Authentik bootstrap", [ {"text": "Добавить Authentik server, worker и postgres в local infra.", "checked": True}, {"text": "Зафиксировать, что Redis не входит в официальный compose Authentik 2026.2.", "checked": True}, {"text": "Добавить генератор infra/.env с локальными secrets и bootstrap credentials.", "checked": True}, {"text": "Создать локального Authentik admin-пользователя для ручного теста.", "checked": True}, {"text": "Создать группы nodedc:launcher:* и nodedc:taskmanager:*.", "checked": True}, {"text": "Создать Application/Provider для Launcher.", "checked": True}, {"text": "Создать Application/Provider для Task Manager.", "checked": True}, {"text": "Настроить app access policies по группам.", "checked": True}, {"text": "Описать bootstrap/export или blueprint strategy.", "checked": True}, {"text": "Проверить login/logout в Authentik за proxy.", "checked": True}, {"text": "Добавить безопасный NODE.DC branded login через Authentik Brand/CSS.", "checked": True}, {"text": "Зафиксировать запрет HTML-proxy/password facade в security RFC.", "checked": True}, ], ), text_block( "infra", "Реализация этапа 1", """ В platform repo добавлены infra/docker-compose.dev.yml, infra/reverse-proxy/Caddyfile, infra/authentik/README.md и infra/scripts/init-dev-env.sh. Compose публикует только reverse-proxy port 80. Authentik server, worker и PostgreSQL остаются внутри docker network. Caddy routes: auth.local.nodedc -> authentik-server:9000, launcher.local.nodedc -> host.docker.internal:5173, task.local.nodedc -> host.docker.internal:8090. Проверки: docker compose config проходит на infra/.env.example; init-dev-env.sh проходит sh -n и создает ignored infra/.env с правами 600. Docker Hub pull caddy:2-alpine зависал, поэтому compose переведен на локальный PLATFORM_PROXY_IMAGE=nodedc/plane-proxy:ru. Фактический docker compose up выполнен. Контейнеры nodedc-platform-authentik-server-1, nodedc-platform-authentik-worker-1, nodedc-platform-postgresql-authentik-1 healthy; nodedc-platform-reverse-proxy-1 слушает 0.0.0.0:80. Проверки через Host header: auth.local.nodedc возвращает 302 на Authentik authentication flow, launcher.local.nodedc возвращает 200 от Vite launcher, task.local.nodedc возвращает 200 от Plane. Для Vite launcher route зафиксирован отдельный upstream Host localhost:5173, иначе dev server отдавал 403 на внешний Host launcher.local.nodedc. Проверен bootstrap-пользователь Authentik: akadmin / admin@nodedc.local / internal / active. Пароль хранится только в ignored platform/infra/.env. Локальные домены прописаны в /etc/hosts. Прямые проверки без подмены Host header проходят: http://auth.local.nodedc/ -> 302, http://launcher.local.nodedc/ -> 200, http://task.local.nodedc/ -> 200. Plane live WebSocket upgrade через proxy проверен: запрос с Upgrade headers на http://task.local.nodedc/live/ возвращает 101 Switching Protocols. Caddy reverse_proxy сохраняет Upgrade/Connection headers для live-трафика. Изменения platform repo закоммичены и отправлены в origin/main: afa53d5, "АРХ - NODEDC PLATFORM: запуск локального proxy/Authentik стенда". Remote main обновлен с 55db22b до afa53d5. """, ), text_block( "infra", "Реализация этапа 2", """ В Authentik создан локальный internal user dcctouch@gmail.com с username dcctouch@gmail.com, активным статусом и usable password. Пользователь добавлен в встроенную группу authentik Admins, у которой is_superuser=True. Это дает доступ к админке Authentik для ручной проверки и дальнейшей настройки applications/providers. Пароль был задан из пользовательского ввода и не сохранялся в platform repo. Следующий открытый пункт: ручная проверка login/logout через http://auth.local.nodedc. Ручной login через proxy подтвержден пользователем 2026-05-04: dcctouch@gmail.com успешно вошел в Authentik, видит My applications и кнопку Admin interface. В platform repo добавлены infra/authentik/bootstrap-dev.py и infra/scripts/bootstrap-authentik-dev.sh. Скрипт idempotent, заполняет недостающие OIDC client secrets в ignored infra/.env и создает Authentik объекты через локальный ak shell. Созданы группы nodedc:superadmin, nodedc:launcher:admin, nodedc:launcher:user, nodedc:taskmanager:admin и nodedc:taskmanager:user. Пользователь dcctouch@gmail.com добавлен во все NODE.DC группы для локальной проверки. Созданы application tiles NODE.DC Launcher и NODE.DC Task Manager, OAuth2 providers NODE.DC Launcher OIDC и NODE.DC Task Manager OIDC. Provider discovery endpoints отвечают 200: /application/o/launcher/.well-known/openid-configuration и /application/o/task-manager/.well-known/openid-configuration. Application access задан через group bindings: Launcher доступен nodedc:superadmin, nodedc:launcher:admin, nodedc:launcher:user; Task Manager доступен nodedc:superadmin, nodedc:taskmanager:admin, nodedc:taskmanager:user. OIDC tokens получают стандартные openid/email/profile/offline_access scopes и custom groups scope. 2026-05-04 добавлен безопасный branded login prototype без proxy над Authentik. В platform/docs/AUTH_BRANDED_LOGIN_RFC.md зафиксирована threat model: запрещены HTML-rewrite proxy, password form в Launcher/BFF, ROPC/password grant и обход MFA/recovery/audit. Разрешенный путь — Authentik-native Brand/CSS/Flow customization. platform/infra/authentik/bootstrap-dev.py теперь idempotent настраивает Brand для auth.local.nodedc: title NODE.DC, default authentication flow default-authentication-flow, flow title "Работайте во всех измерениях.", layout stacked и branding_custom_css из /templates/branding/nodedc-login.css. CSS лежит в platform/infra/authentik/custom-templates/branding/nodedc-login.css и монтируется в Authentik существующим volume /templates. Runtime bootstrap выполнен: curl страницы auth.local.nodedc подтверждает title NODE.DC, matched_domain auth.local.nodedc и наличие brand-css со строкой "Работайте во всех измерениях". Authentik shell подтверждает default Brand auth.local.nodedc, branding_title NODE.DC и flow title "Работайте во всех измерениях.". Password/MFA/recovery mechanics не тронуты. Дополнение 2026-05-04: экран Authentik "My applications" признан не login flow, а пользовательским dashboard самого identity provider. Для обычного платформенного UX он исключен на proxy-уровне: auth.local.nodedc/ и auth.local.nodedc/if/user* возвращают 302 на launcher.local.nodedc. OIDC discovery/authorize/callback и /if/admin/ не редиректятся, чтобы не ломать SSO и служебную админку Authentik. Дополнение 2026-05-04: экран "You've logged out of NODE.DC Launcher" признан Authentik provider invalidation dashboard. OAuth2 providers переключены на штатный default-invalidation-flow с UserLogoutStage, чтобы global logout закрывал Authentik session и возвращал пользователя в NODE.DC route без показа Authentik application logout UI. Дополнение 2026-05-04: обнаружен второй logout gap — Authentik end-session может завершаться белой страницей "Logout successful" вместо возврата в продуктовый маршрут, а hidden fetch end-session не гарантирует закрытие SSO-cookie. Launcher logout handoff переведен на top-level IdP logout: после front-channel закрытия приложений браузер уходит в Authentik end-session с зарегистрированным post_logout_redirect_uri=http://launcher.local.nodedc/auth/logged-out, затем /auth/logged-out чистит platform cookies и переводит в /auth/login?prompt=login. Launcher frontend использует window.location.replace для logout и revalidates session on bfcache pageshow, чтобы browser Back не показывал старый залогиненный state после выхода. Дополнение 2026-05-04: authentication flow приближен к старому Plane login в рамках Authentik-native customization. Brand attributes выставлены в settings.locale=ru и settings.theme.base=dark. Flow title изменен на "Работайте во всех измерениях.". IdentificationStage теперь использует только email как идентификатор и привязан к штатному PasswordStage, поэтому email/password находятся в одном Authentik challenge без передачи пароля в Launcher. Отдельный PasswordStage binding удален из flow, MFA и UserLoginStage остаются штатными. Изменения platform repo закоммичены и отправлены в origin/main: 4a10726, "АРХ - NODEDC PLATFORM: bootstrap Authentik applications". Remote main обновлен с afa53d5 до 4a10726. """, ), ], }, { "slug": "launcher-control-plane-oidc", "name": "Launcher: backend, OIDC и app access", "priority": "high", "state_group": "started", "assignees": [CODEX_EMAIL], "description_html": html( "Launcher должен перестать быть только меню ссылок и стать control plane: входная точка платформы, клиенты/компании, пользователи, группы, инвайты, профиль, app registry, access matrix и audit log.", "Критерий приемки: пользователь входит через NODE.DC UI без публичного брендинга Authentik, Launcher хранит бизнес-модель доступа и профиля, а Authentik получает только техническую SSO/OIDC projection.", ), "blocks": [ text_block( "launcher", "Текущая архитектура", """ Launcher сейчас является Vite/React GUI в отдельном репозитории /Users/dcconstructions/Downloads/mnt/data/nodedc_launcher. Backend слоя не обнаружено. Значит OIDC callback, session handling, Authentik service token, app registry и audit log нужно добавлять отдельным backend/BFF слоем, а не держать во frontend. """, ), text_block( "launcher", "Этап 1. Backend/BFF shell", """ Статус: реализовано. Нужно добавить минимальный backend слой для безопасной auth-интеграции и будущего admin API. Выбран путь: BFF живет в Launcher repo и в dev режиме обслуживает Vite как middleware на том же порту 5173, чтобы существующий reverse proxy launcher.local.nodedc не требовал нового внешнего домена или порта. """, ), checker( "launcher1", "Чекер этапа 1. Backend/BFF shell", [ {"text": "Выбрать место backend слоя: Launcher repo или platform/services/launcher-api.", "checked": True}, {"text": "Добавить health endpoint.", "checked": True}, {"text": "Добавить server-side session storage.", "checked": True}, {"text": "Добавить env contract для OIDC и Authentik API.", "checked": True}, {"text": "Зафиксировать запрет service tokens во frontend.", "checked": True}, ], ), text_block( "launcher", "Этап 2. OIDC login/session", """ Статус: реализовано, ожидает ручной browser login/logout проверки. Launcher должен проходить OIDC Authorization Code Flow + PKCE через Authentik и получать нормализованного текущего пользователя. Важно: текущий hosted Authentik login является временным dev flow для проверки OIDC/JWKS/session/redirect. Целевой production login не должен показывать пользователю Authentik UI/brand. """, ), checker( "launcher2", "Чекер этапа 2. OIDC login/session", [ {"text": "Добавить login route.", "checked": True}, {"text": "Добавить callback route.", "checked": True}, {"text": "Проверить state/nonce/token.", "checked": True}, {"text": "Загрузить JWKS и валидировать JWT.", "checked": True}, {"text": "Нормализовать sub/email/name/groups.", "checked": True}, {"text": "Добавить logout flow.", "checked": True}, {"text": "Подтвердить browser login/logout через launcher.local.nodedc.", "checked": True}, ], ), text_block( "launcher", "Этап 3. App registry и фильтрация плиток", """ Статус: частично реализовано. Launcher показывает приложения не статическим списком, а через registry и access state. Источник истины по доступу — Launcher access model; Authentik groups/entitlements являются технической проекцией для SSO/enforcement. Продуктовое правило от 2026-05-04: Launcher не должен скрывать все недоступные плитки. Он показывает каталог приложений платформы, а отсутствие доступа отражает как disabled/Нет доступа на карточке. Это уже заложено в текущий frontend-декор и должно сохраняться при backend-интеграции. """, ), checker( "launcher3", "Чекер этапа 3. App registry и фильтрация плиток", [ {"text": "Спроектировать app_registry модель.", "checked": True}, {"text": "Добавить GET /api/me.", "checked": True}, {"text": "Добавить GET /api/apps.", "checked": True}, {"text": "Вычислять access state из runtime projection groups.", "checked": True}, {"text": "Не скрывать недоступные плитки, а показывать Нет доступа.", "checked": True}, {"text": "Проверить direct URL behavior через proxy.", "checked": True}, ], ), text_block( "launcher", "Целевой пользовательский flow", """ Production flow: пользователь открывает nodedc.ru, видит основной промо/маркетинговый сайт, нажимает "Войти на платформу", проходит окно логина/пароля и после успешного доступа попадает в NODE.DC Launcher. Пользовательский UI платформы не должен светить название identity provider. В текстах и кнопках используется нейтральное "Войти", "Вход на платформу", "Сессия NODE.DC". Authentik остается внутренним identity/session/OIDC слоем; продуктовая админка живет в Launcher. Прямой Authentik UI допустим только через отдельную служебную ссылку для системной настройки. Прямые ссылки на приложения остаются нормальным сценарием. Если пользователь открыл Task Manager напрямую без session, его нужно увести в login flow и вернуть к приложению после успешной авторизации. """, ), text_block( "launcher", "Этап 3.5. NODE.DC login facade", """ Статус: частично реализовано. Ручная проверка 2026-05-04 подтвердила: при входе пользователь видел стандартное окно Authentik. Это допустимо только для dev/OIDC bootstrap, но не соответствует целевой UX-модели NODE.DC. Выбран безопасный первый путь: полностью кастомизированный Authentik-native Brand/CSS/Flow, а не proxy и не password facade в Launcher. Это сохраняет Authentik как password/session/MFA/recovery/audit authority. Безопасностная граница: не делать frontend-only password form, не хранить IdP/service secrets в browser bundle, не ломать MFA/recovery/rate-limit/audit и не обходить Authentik как password/session authority. Если Brand/CSS не даст pixel-level дизайн Plane login, следующий допустимый уровень — template override внутри Authentik deployment с отдельным security review, но не HTML-rewrite proxy. """, ), checker( "launcher35", "Чекер этапа 3.5. NODE.DC login facade", [ {"text": "Выбрать безопасную стратегию: branded Authentik flow или Launcher/BFF facade.", "checked": True}, {"text": "Запретить reverse proxy HTML rewrite и password form в Launcher/BFF.", "checked": True}, {"text": "Добавить AUTH_BRANDED_LOGIN_RFC.md с threat model.", "checked": True}, {"text": "Настроить Authentik Brand/CSS для auth.local.nodedc.", "checked": True}, {"text": "Сверстать NODE.DC login по канону Task Manager/Launcher в пределах Brand/CSS.", "checked": True}, {"text": "Объединить email/password в одном Authentik challenge через IdentificationStage.password_stage.", "checked": True}, {"text": "Включить ru locale и dark theme на уровне Authentik Brand settings.", "checked": True}, {"text": "Скрыть Authentik brand в базовом authentication page title/brand.", "checked": True}, {"text": "Исключить Authentik My applications dashboard из пользовательского маршрута.", "checked": True}, {"text": "Исключить Authentik application logout dashboard из пользовательского маршрута.", "checked": True}, {"text": "Поддержать returnTo для прямых ссылок на приложения.", "checked": True}, {"text": "Поддержать forced login для диагностики.", "checked": True}, "Спроектировать recovery/enrollment/MFA без раскрытия Authentik UI.", {"text": "Проверить, что password/service tokens не попадают во frontend.", "checked": True}, ], ), text_block( "launcher", "Реализация этапов 1-3", """ В Launcher repo добавлен local BFF server/dev-server.mjs. npm run dev теперь запускает Node/Express BFF, который обслуживает Vite как middleware на том же порту 5173. Старый чистый Vite dev режим сохранен как npm run dev:vite. BFF автоматически подхватывает OIDC env из NODEDC/platform/infra/.env, не вшивая secrets в frontend или git. Добавлены routes: GET /healthz, GET /auth/login, GET /auth/callback, GET /auth/logout, GET /api/me, GET /api/apps, POST /api/storage/upload и POST /api/storage/data. OIDC flow реализован как Authorization Code + PKCE: state хранится server-side и в HttpOnly cookie, nonce проверяется по id_token, JWKS загружается из discovery endpoint, JWT валидируется по issuer/audience. Session хранится server-side in-memory для локального dev стенда. App registry возвращает полный каталог приложений из launcher storage и runtime access state: hasAccess, matchedGroups, accessReason. Frontend больше не скрывает недоступные плитки; он отключает переход и показывает "Нет доступа" через существующую механику карточек. Frontend Launcher подключен к /api/me и /api/apps. Без session Launcher больше не показывает промежуточное окно с кнопкой "Войти": frontend делает прямой replace на /auth/login, а сам /auth/login сразу отдает 302 в Authentik authorize flow. Если пользователь пришел на непустой launcher path, frontend добавляет returnTo к login URL, чтобы после callback вернуть его на исходный маршрут. После login пользователь нормализуется из OIDC claims, а плитки получают доступы из runtime app registry. Проверки 2026-05-04: npm run build проходит; http://launcher.local.nodedc/healthz возвращает oidcConfigured=true; http://launcher.local.nodedc/api/me без session возвращает 401 и loginUrl; http://launcher.local.nodedc/auth/login возвращает 302 на Authentik authorize endpoint; discovery endpoint Authentik для launcher возвращает issuer и authorization_endpoint. Ручная проверка 2026-05-04 выявила callback error {"error":"JSON Web Key Set malformed"}. Root cause: Authentik OAuth2 providers были созданы без signing_key, поэтому JWKS endpoint отдавал {}. Bootstrap исправлен: providers получают authentik Self-signed Certificate как signing key. После повторного bootstrap JWKS отдает RSA key. Ручной browser login/logout через http://launcher.local.nodedc подтвержден пользователем: после callback видна витрина Launcher, OPERATIONAL CORE открывает Task Manager, global logout закрывает Launcher и downstream session. Граница готовности на текущий момент: Launcher готов как базовый OIDC/BFF portal и local control-plane prototype, но не является финально готовым production control plane. Остаются mock/dev элементы, JSON-backed persistence, recovery/MFA UX и production storage/profile work. 2026-05-04 добавлен явный logout в профильное меню Launcher: кнопка "Выйти" вызывает /auth/logout и чистит local BFF session без ухода в Authentik UI/admin. Это нужно, чтобы пользователь оставался в NODE.DC UX после выхода. SSO-session у identity provider может оставаться активной, если не провести top-level logout через Authentik end-session. Поэтому для пользовательского выхода Launcher использует global logout handoff: чистит локальную BFF session, закрывает downstream app sessions, уводит браузер в Authentik end-session и возвращает через /auth/logged-out в /auth/login?prompt=login. Это убирает промежуточную Authentik "Logout successful" страницу и не дает прямому заходу в Task Manager молча перелогиниться через старую SSO session. 2026-05-04 login facade переведен на безопасную Authentik-native кастомизацию: Brand/CSS/template JS без proxy над password form. Окно приведено к NODE.DC/Plane визуальному канону, Authentik dashboard/logout application UI исключены из пользовательского маршрута, логотип синхронизирован с Launcher top bar по размеру и позиции. Текущий BFF/OIDC слой является переходной реализацией. Сейчас app access читается из OIDC groups как runtime projection, но целевая source-of-truth модель — Launcher backend: клиенты, членства, группы клиента, user grants, deny exceptions, профиль платформы и audit. Authentik должен получать из Launcher синхронизированную техническую проекцию для SSO/enforcement, а не быть ручной бизнес-админкой. """, ), text_block( "launcher", "Этап 4. Admin API и audit", """ Статус: частично реализовано. Launcher Admin API должен владеть бизнес-администрированием платформы: клиенты/компании, пользователи, членства, группы клиента, инвайты, grants/exceptions, профиль платформы и audit log. Пароли и SSO-session остаются в Authentik, но Authentik синхронизируется server-side и не показывается обычным администраторам как frontend/control-plane. Текущий проход реализует dev backend boundary поверх JSON-backed launcher-data.json. Это еще не production DB и не финальный Authentik sync, но уже выносит admin операции из frontend-only состояния в server-side BFF. """, ), checker( "launcher4", "Чекер этапа 4. Admin API и audit", [ {"text": "Добавить backend store для launcher-data.json.", "checked": True}, {"text": "Закрыть admin endpoints Launcher roles.", "checked": True}, {"text": "Добавить clients CRUD endpoints.", "checked": True}, {"text": "Добавить users list/profile update endpoints.", "checked": True}, {"text": "Добавить memberships update/delete endpoints.", "checked": True}, {"text": "Добавить client groups CRUD endpoints.", "checked": True}, {"text": "Добавить service catalog CRUD/reorder endpoints.", "checked": True}, {"text": "Добавить invite CRUD endpoints из Launcher.", "checked": True}, {"text": "Добавить access grants/exceptions/user-service endpoints.", "checked": True}, {"text": "Добавить Authentik sync dry-run plan endpoint.", "checked": True}, {"text": "Добавить admin_audit_log запись для backend mutations.", "checked": True}, {"text": "Подключить frontend admin overlay к новым admin endpoints.", "checked": True}, "Заменить JSON-backed store на production persistence.", {"text": "Реализовать фактический server-side sync в Authentik.", "checked": True}, ], ), text_block( "launcher", "Реализация этапа 4", """ В Launcher repo добавлен server/control-plane-store.mjs: dev control-plane repository поверх public/storage/launcher-data.json и dist/storage/launcher-data.json. Store нормализует доменные коллекции clients, users, memberships, groups, services, grants, exceptions, invites, syncStatuses и auditEvents. BFF server/dev-server.mjs получил admin guard по группам nodedc:superadmin/nodedc:launcher:admin и backend endpoints для текущего overlay: GET /api/admin/control-plane, clients CRUD, users/profile update, memberships update/delete, groups CRUD, services CRUD/reorder, invites CRUD, access grants/exceptions/user-service, sync retry и GET /api/admin/sync/authentik/plan. Backend mutations пишут auditEvents и помечают затронутые объекты как pending sync для target authentik. Endpoint /api/admin/sync/authentik/plan пока dry-run: он показывает users/groups/access projection, но не вызывает Authentik API. Frontend admin overlay подключен к server-side admin API через src/shared/api/adminApi.ts. Mutation handlers в src/app/LauncherApp.tsx больше не собирают mock id/audit локально для поддержанных admin операций, а вызывают BFF и принимают обновленный LauncherData из backend response. Проверки 2026-05-04: node --check server/dev-server.mjs и server/control-plane-store.mjs проходят; npm run build проходит; npm run test проходит; http://localhost:5173/healthz возвращает oidcConfigured=true; /api/admin/control-plane без session возвращает 401 через localhost и launcher.local.nodedc. Store mutations проверены на временной копии launcher-data.json: create client/group/invite и set user-service access обновляют counts и auditEvents. """, ), text_block( "launcher", "Этап 4.5. Live users seed и очистка demo-участников", """ Статус: частично реализовано. Декоративные MVP-пользователи root@nodedc.local, ivan@romashka.ru, vera@romashka.ru, vasya@romashka.ru, lena@romashka.ru, maria@example.ru и blocked demo-user удаляются из active Launcher seed. Их можно восстановить из backup, но рабочий dev-runtime должен проверяться на живых пользователях. Фактический runtime discovery 2026-05-04: - Authentik содержит dcctouch@gmail.com / DC Touch в группах nodedc:superadmin, nodedc:launcher:admin/user, nodedc:taskmanager:admin/user. - Plane содержит dcctouch@gmail.com / DC Touch как owner workspace nodedc. - Plane содержит silver_psih@yahoo.com / dcMASSACRE как member workspace nodedc и owner workspace silver-psih-mo493nm7. Перед реализацией нужно подтвердить канонические email/имена: пользователь подтвердил, что silver_psih@yahoo.com — правильная почта Silver Psy. Runtime Plane/Auth показывает dcctouch@gmail.com для текущего superadmin; этот email считается фактическим до отдельного указания заменить его на другой alias. Цель этапа: заменить demo-участников Launcher на живой минимальный seed, где супер-админ администрирует клиентов, группы, инвайты и доступы через Launcher UI, а не через прямые правки JSON/Auth/Plane. """, ), checker( "launcher45", "Чекер этапа 4.5. Live users seed и очистка demo-участников", [ {"text": "Подтвердить канонический email NODE.DC superadmin.", "checked": True}, {"text": "Подтвердить канонический email Silver Psy / менеджера.", "checked": True}, {"text": "Сделать backup public/storage/launcher-data.json.", "checked": True}, {"text": "Создать idempotent seed script для живых users/clients/groups/memberships.", "checked": True}, {"text": "Удалить декоративных MVP-участников из active Launcher seed.", "checked": True}, {"text": "Создать живой superadmin в Launcher profile model.", "checked": True}, {"text": "Создать живого Silver Psy/manager пользователя в Launcher profile model.", "checked": True}, {"text": "Синхронизировать недостающего пользователя в Authentik через server-side flow.", "checked": True}, {"text": "Не пересоздавать Plane users и не менять Plane workspace/task связи.", "checked": True}, {"text": "Показывать все видимые сервисы в Launcher даже без доступа к запуску.", "checked": True}, {"text": "Считать /api/apps из Launcher control-plane, а не из stale OIDC groups.", "checked": True}, {"text": "Синхронизировать affected users в Authentik при изменении memberships/groups/grants/exceptions.", "checked": True}, {"text": "Заменить polling открытых Launcher вкладок на event-driven runtime refresh.", "checked": True}, {"text": "Сделать atomic write launcher-data.json, чтобы reads не ловили полузаписанный JSON.", "checked": True}, {"text": "Добавить pending-state на ячейки access matrix, чтобы не было повторных кликов по in-flight mutation.", "checked": True}, {"text": "Отключить legacy frontend autosave launcher-data.json в authenticated runtime.", "checked": True}, {"text": "Добавить self-profile settings panel справа.", "checked": True}, {"text": "Добавить upload avatar в Launcher profile storage.", "checked": True}, {"text": "Добавить смену пароля через backend Authentik projection.", "checked": True}, "Проверить Launcher admin UI: клиенты, участники, группы, инвайты, доступы.", {"text": "Проверить Task Manager SSO для обоих живых пользователей.", "checked": True}, ], ), text_block( "launcher", "Реализация этапа 4.5", """ В Launcher repo создан scripts/seed-live-control-plane.mjs. Скрипт читает текущий public/storage/launcher-data.json, сохраняет существующий каталог services и заменяет active control-plane домен на один live-клиент DCTOUCH и двух пользователей: dcctouch@gmail.com / DC Touch и silver_psih@yahoo.com / Silver Psy. Перед применением создан backup public/storage/backups/launcher-data-live-seed-20260504-152728.json. Active storage после seed содержит 1 client, 2 users, 2 memberships, 2 groups, 3 grants, 0 invites. Silver Psy помещен в группу "Менеджеры" и получает доступ к OPERATIONAL CORE через group grant; DC Touch помещен в группу "Администраторы" и остается platform superadmin. Из src/shared/api/mockApi.ts убраны декоративные profileOptions для Ivan/Vera/Vasya/Lena/Maria. src/app/LauncherApp.tsx больше не мапит любого non-superadmin OIDC-пользователя на user_vasya: активный Launcher profile выбирается по Authentik sub/email, затем fallback на user_root для superadmin. Fallback src/shared/api/mockData.ts приведен к live seed, чтобы при отсутствии storage не всплывали старые demo-участники. Plane users/workspaces не пересоздавались и не менялись. Проверки 2026-05-04: node --check scripts/seed-live-control-plane.mjs, server/dev-server.mjs и server/control-plane-store.mjs проходят; npm run build проходит; npm run test проходит; http://localhost:5173/storage/launcher-data.json возвращает 1 client, 2 users и emails dcctouch@gmail.com/silver_psih@yahoo.com; http://launcher.local.nodedc/healthz возвращает ok и oidcConfigured=true. Дополнение 2026-05-04: реализован server-side Authentik provisioning flow. В Launcher repo добавлен server/authentik-sync.mjs: он работает только на backend, берет Authentik API token из server-side env, создает/обновляет пользователя, назначает группы и выставляет пароль только при явном provisioning/generatePassword сценарии. BFF server/dev-server.mjs получил POST /api/admin/users для создания пользователя в Launcher control-plane с optional Authentik provisioning и POST /api/admin/users/:userId/provision-authentik для повторной синхронизации существующего Launcher-пользователя. Healthcheck теперь показывает authentikApiConfigured без раскрытия token. server/control-plane-store.mjs получил createUser и markUserAuthentikProvisioned: Launcher остается source-of-truth, а Authentik UUID сохраняется в user.authentikUserId после успешной технической проекции. Frontend admin overlay получил форму "Создать участника" в разделе "Участники": email/name/role/group -> backend creates Launcher user -> Authentik projection. При генерации временного пароля пароль показывается только одноразово в UI и не пишется в storage/task card. Дополнительно исправлена проверка доступа к плитке AGENT CORE/NodeDC: service nodedc больше не считается доступным просто по nodedc:launcher:user. Для сервисных плиток используется service.authentikGroupName или специальная группа конкретного приложения; факт входа в Launcher не должен автоматически открывать все сервисы. Дополнение после ручной проверки Silver Psy: buildLauncherServices больше не фильтрует denied-сервисы из rail. Обычный пользователь видит весь видимый каталог приложений, но у сервисов без доступа кнопка запуска заблокирована и показывает "Нет доступа". Hidden-сервисы остаются скрытыми для non-root. Добавлен self-profile контур: /api/profile, PATCH /api/profile и POST /api/profile/password доступны любому authenticated пользователю для собственного профиля. Profile settings panel открывается из меню профиля справа налево, может работать одновременно с левым admin overlay, сохраняет name/email/phone/position/avatarUrl и умеет менять пароль через backend Authentik projection. Avatar upload использует существующий server-side storage upload и сохраняет ссылку в Launcher profile model. Runtime user в Launcher теперь предпочитает данные Launcher profile model поверх OIDC claims, чтобы изменения имени/аватарки отображались без ожидания следующего login. Фактически выполнено: silver_psih@yahoo.com создан в Authentik как active internal user, получил группы nodedc:launcher:user и nodedc:taskmanager:user. dcctouch@gmail.com также синхронизирован по Authentik UUID. public/storage/launcher-data.json теперь содержит Authentik UUID для обоих live-профилей, а syncStatuses для обоих users находятся в state synced. Проверки 2026-05-04: node --check server/authentik-sync.mjs, server/dev-server.mjs и server/control-plane-store.mjs проходят; npm run build проходит; npm run test проходит; Authentik API подтверждает наличие silver_psih@yahoo.com с 2 группами; /healthz возвращает authentikApiConfigured=true. Пользователь вручную подтвердил: Silver Psy входит в Launcher, не видит администрирование, открывает OPERATIONAL CORE и в Task Manager видит корректный доступный проект. Дополнение после проверки снятия доступа: /api/apps больше не берет app access из stale OIDC groups текущей BFF-сессии. Runtime-доступ рассчитывается из Launcher control-plane через resolveRequiredGroups, а OIDC groups остаются fallback только если пользователь еще не сопоставлен с Launcher profile model. Admin mutations теперь проталкивают affected users в Authentik сразу после изменения профиля, memberships, groups, access grants, deny/allow exceptions и user-service matrix. Это закрывает кейс, когда админ снял Silver Psy из доступа к OPERATIONAL CORE, а Authentik еще держал старую nodedc:taskmanager:user группу. Открытые Launcher вкладки authenticated пользователей больше не используют polling каждые 5 секунд. BFF держит /api/events через Server-Sent Events, а admin/profile/storage mutations публикуют событие control-plane.updated. Вкладки обновляют /api/me, /api/apps и storage/control-plane snapshot только по факту изменения control-plane состояния. Дополнение после ручной проверки access matrix: выявлен race записи public/storage/launcher-data.json. Пока writeFile перезаписывал файл напрямую, параллельные runtime запросы иногда читали пустой/полузаписанный JSON и падали с Unexpected end of JSON input; визуально это выглядело как применение access select только с нескольких попыток. server/control-plane-store.mjs переведен на atomic write: запись идет во временный файл в том же storage root, затем rename заменяет launcher-data.json. Это убирает окно, в котором reader может увидеть обнуленный файл. В access matrix добавлен pending-state на конкретную ячейку user/service: после выбора роли ячейка сразу показывает выбранное значение и "Сохраняем...", а повторный клик блокируется до завершения mutation. Это убирает многократные ручные попытки и race между несколькими одинаковыми запросами. Дополнение по итогам повторного теста: найден второй источник отката access matrix. Legacy frontend autosave сохранял весь LauncherData обратно в /api/storage/data после setData и мог перезаписать свежий backend-result старым состоянием из открытой вкладки. src/app/LauncherApp.tsx больше не вызывает persistLauncherData; authenticated runtime пишет control-plane только через admin/profile API mutations. /api/storage/data дополнительно закрыт requireLauncherAdmin и оставлен только как служебный dev endpoint. Текущий live-sync выполнен вручную после исправления: dcctouch@gmail.com имеет nodedc:launcher:user, nodedc:superadmin, nodedc:launcher:admin, nodedc:taskmanager:admin, nodedc:taskmanager:user; silver_psih@yahoo.com получает nodedc:launcher:user, nodedc:taskmanager:user и service-digital-twin согласно текущей access matrix. Дополнение 2026-05-04: Launcher BFF получил внутренний server-to-server endpoint POST /api/internal/access/check. Endpoint защищен bearer token, не доступен через пользовательскую session-модель и возвращает актуальный allow/deny по Launcher control-plane для конкретного serviceSlug, subject/email/userId. Ограничение по уже открытой downstream-сессии вынесено в Plane live enforcement этап. Launcher остается source-of-truth, а downstream-приложения должны либо дергать внутренний access check, либо использовать общий будущий NODE.DC auth SDK/gateway. Ограничение: email/password/profile update уже работает как dev-flow, но production UX для reset-password/invite-email и подтверждения смены email еще нужно вынести в отдельный полноценный этап. """, ), text_block( "launcher", "Актуализация 2026-05-09", """ За последние итерации Launcher фактически стал текущим control-plane для корпоративного контура NODE.DC. Через него уже проверяются: клиенты, участники, группы, инвайты, сервисный каталог, матрица доступов, Operational Core workspace binding, user profile, client avatar, fullscreen admin panels и service-specific role modal для Operational Core. Зафиксирован продуктовый принцип: enterprise-контур управляется через Launcher. Для таких пользователей и workspace Launcher является source of truth по приглашениям, сервисному доступу и базовым ролям; downstream-приложения получают техническую проекцию и не должны становиться вторым независимым источником прав. Отдельно вынесена новая большая карточка "Публичный контур пользователей". Она закрывает сценарий обычных внешних пользователей, которые приходят не через клиентскую компанию, а через запрос приглашения и дальше используют отдельные сервисы как standalone-продукты. """, ), checker( "launcher-actual-20260509", "Чекер актуализации 2026-05-09", [ {"text": "Зафиксировать Launcher как source of truth для enterprise-доступов.", "checked": True}, {"text": "Зафиксировать Operational Core role modal как текущую модель детальных назначений.", "checked": True}, {"text": "Отделить public/self-service контур в самостоятельную карточку.", "checked": True}, "Заменить JSON-backed store на production persistence.", "Описать recovery/MFA/email-change UX без раскрытия Authentik UI.", "Вынести billing/limits в отдельный RFC после стабилизации public-контура.", ], ), ], }, { "slug": "plane-oidc-user-migration", "name": "Plane: OIDC и миграция пользователя", "priority": "urgent", "state_group": "started", "assignees": [CODEX_EMAIL], "description_html": html( "Интеграция Plane fork с Authentik OIDC без потери текущего пользователя и связанных данных. Это самый рискованный блок, потому что нельзя пересоздать существующего Plane admin и потерять связи задач.", "Критерий приемки: старый Plane user связан с Authentik sub, после входа через NODE.DC/Authentik видит старые workspace, проекты, задачи, комментарии и назначения; signup закрыт; Plane остается вынимаемым standalone-продуктом с сохранением собственных auth/API механизмов.", ), "blocks": [ text_block( "plane", "Текущая архитектура", """ Task Manager работает как Plane CE self-host. Runtime лежит в plane-app, fork исходников в plane-src. В workspace nodedc уже есть данные, пользователи и проекты. Старые связи в Plane нельзя менять без отдельной проверенной миграции. Архитектурное правило от 2026-05-04: Plane подключается к NODE.DC как приложение/адаптер, но не превращается в невынимаемый модуль платформы. Его workspace/project/task/comment модели остаются внутри Plane, стандартные Plane auth/API механизмы сохраняются через env/конфигурацию, а Launcher не читает и не пишет Plane DB напрямую. """, ), text_block( "plane", "Этап 1. Backup и auth discovery", """ Статус: реализовано. Перед кодом нужен backup и точная карта Plane auth/users: модели пользователя, sessions, signup, login endpoints, env flags и все связи, которые нельзя трогать. """, ), checker( "plane1", "Чекер этапа 1. Backup и auth discovery", [ {"text": "Сделать dump plane_db.", "checked": True}, {"text": "Сохранить plane.env.", "checked": True}, {"text": "Сохранить uploads/MinIO volumes.", "checked": True}, {"text": "Найти User/Profile/WorkspaceMember/ProjectMember связи.", "checked": True}, {"text": "Найти текущие login/signup endpoints.", "checked": True}, {"text": "Найти env flags для signup/OAuth/password auth.", "checked": True}, {"text": "Составить список файлов для OIDC integration.", "checked": True}, ], ), text_block( "plane", "Реализация этапа 1", """ Создан backup перед изменением Plane auth слоя: plane DB dump, plane.env/api env snapshots, docker compose/config copies и архив uploads/MinIO volume. Backup лежит в /Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/plane-app/backup/nodedc-platform-oidc-20260504-122924. Discovery подтвердил критичный baseline: пользователь dcctouch@gmail.com уже существует в Plane как admin_nodedc, user id 844d7f18-285d-4671-8371-8ca9ca5ffa39, состоит owner в workspace nodedc и связанных проектах. Миграция не должна пересоздавать этого пользователя и не должна менять старые workspace/project/task связи. Точки интеграции найдены в plane-src/apps/api/plane/authentication/urls.py, plane-src/apps/api/plane/authentication/utils/login.py и plane-src/apps/api/plane/authentication/utils/redirection_path.py. Env flags Plane подтверждены: ENABLE_SIGNUP, ENABLE_EMAIL_PASSWORD, ENABLE_MAGIC_LINK_LOGIN. """, ), text_block( "plane", "Этап 2. External identity link", """ Статус: реализовано. Нужно добавить mapping Authentik identity на существующего Plane user. Это отдельный слой связи, а не массовое изменение старых owner/assignee/created_by. """, ), checker( "plane2", "Чекер этапа 2. External identity link", [ {"text": "Спроектировать модель external_identity_link.", "checked": True}, {"text": "Добавить миграцию модели.", "checked": True}, {"text": "Обеспечить unique provider+sub.", "checked": True}, {"text": "Обеспечить unique provider+plane_user_id.", "checked": True}, {"text": "Добавить last_login_at/status.", "checked": True}, {"text": "Проверить idempotent поведение.", "checked": True}, ], ), text_block( "plane", "Реализация этапа 2", """ В Plane source добавлена модель ExternalIdentityLink в plane-src/apps/api/plane/db/models/user.py и экспорт в plane-src/apps/api/plane/db/models/__init__.py. Применены миграции db.0137_external_identity_link и db.0138_external_identity_link_unique_user. Модель хранит provider, subject, user, email, groups, status и last_login_at. Уникальность закреплена по provider+subject и provider+user, чтобы один Authentik subject не связывался с несколькими Plane users и один Plane user не получал несколько Authentik identities одного provider. Dry-run команды link_authentik_user проверен на dcctouch@gmail.com без изменения данных. """, ), text_block( "plane", "Этап 3. OIDC login flow", """ Статус: частично реализовано. Plane должен принимать Authentik OIDC callback, валидировать token/state/nonce и логинить локального пользователя через mapping. """, ), checker( "plane3", "Чекер этапа 3. OIDC login flow", [ {"text": "Добавить NODE.DC/Authentik login entrypoint.", "checked": True}, {"text": "Добавить OIDC callback.", "checked": True}, {"text": "Валидировать issuer/audience/exp/sub.", "checked": True}, {"text": "Проверять nodedc:taskmanager:access.", "checked": True}, {"text": "Искать link по authentik_sub.", "checked": True}, {"text": "Логинить существующего plane_user_id.", "checked": True}, {"text": "Закрыть путь без mapping или app access.", "checked": True}, ], ), text_block( "plane", "Реализация этапа 3", """ Добавлены backend routes /auth/oidc/login/ и /auth/oidc/callback/ в Plane API. Login entrypoint генерирует Authorization Code + PKCE, state/nonce хранит в server-side session, callback обменивает code на tokens, проверяет id_token через JWKS и валидирует issuer/audience/nonce. Access check завязан на группы Authentik: nodedc:superadmin, nodedc:taskmanager:admin, nodedc:taskmanager:user. Для local-dev включен PLANE_OIDC_AUTO_LINK_EMAIL=1, чтобы первый успешный callback мог безопасно связать существующего Plane user по email без пересоздания пользователя. Проверено: /auth/oidc/login/?next_path=/nodedc стабильно возвращает 302 на Authentik authorize endpoint для client_id nodedc-task-manager. Ручной browser acceptance от 2026-05-04 закрыт: пользователь зашел в Launcher в incognito, прошел Authentik login, вернулся в Launcher, открыл Operational Core/Task Manager из плитки и получил доступ без Plane password. В Plane создан ExternalIdentityLink для dcctouch@gmail.com, status active, subject присутствует, linked Plane user остался старым user id 844d7f18-285d-4671-8371-8ca9ca5ffa39. После проверки добавлен route alias /auth/oidc/callback без trailing slash, потому что Authentik сначала обращался к callback без слэша и получал 404 перед успешным запросом на /auth/oidc/callback/. Группы в Plane и Launcher теперь дедуплицируются при нормализации claims. Закрыт базовый runtime-слой профильного контекста: Launcher читает avatarUrl из OIDC claims, а Plane синхронизирует display_name/first_name/last_name/avatar при OIDC login. Текущая Authentik mapping — временная dev-проекция до появления Launcher profile storage. Текущий Plane user dcctouch@gmail.com приведен к display name DC Touch, существующий Plane avatar сохранен. Полная avatar-синхронизация должна идти из Launcher как мастер-источника профиля, а не из ручной настройки Authentik. """, ), text_block( "plane", "Этап 4. Migration command и signup policy", """ Статус: частично реализовано. После модели и login flow нужна управляемая команда связывания старого пользователя и отключение обходных входов, которые ломают модель доступа. """, ), checker( "plane4", "Чекер этапа 4. Migration command и signup policy", [ {"text": "Добавить manage.py command link_authentik_user.", "checked": True}, {"text": "Поддержать dry-run.", "checked": True}, {"text": "Проверять конфликтующий mapping.", "checked": True}, {"text": "Не менять задачи/workspace/memberships.", "checked": True}, {"text": "Отключить публичный signup.", "checked": True}, {"text": "Закрыть лишние OAuth/magic-link обходы, если они нарушают invite/manual модель.", "checked": True}, {"text": "Проверить старый admin после OIDC login.", "checked": True}, ], ), text_block( "plane", "Реализация этапа 4", """ Добавлена management command plane-src/apps/api/plane/db/management/commands/link_authentik_user.py с параметрами --email, --sub и --dry-run. Команда проверяет существующего Plane user, конфликтующий provider+subject и конфликтующий provider+user mapping. plane-app/plane.env настроен для local runtime: WEB_URL=http://task.local.nodedc, CORS_ALLOWED_ORIGINS включает task.local.nodedc, ENABLE_SIGNUP=0, ENABLE_MAGIC_LINK_LOGIN=0, Plane OIDC env указывает на Authentik provider task-manager. ENABLE_EMAIL_PASSWORD временно оставлен включенным как fallback до ручного подтверждения OIDC входа, чтобы не потерять доступ к текущему Task Manager. """, ), text_block( "plane", "Этап 5. Единый профильный контекст", """ Статус: частично реализовано. Профиль пользователя должен быть одинаковым в Launcher и Task Manager. Источник истины — Launcher profile model; OIDC claims являются транспортной проекцией, через которую локальные приложения получают единый display name/avatar context. """, ), checker( "plane5", "Чекер этапа 5. Единый профильный контекст", [ {"text": "Добавить профильные claims в Authentik mapping.", "checked": True}, {"text": "Передавать avatarUrl в Launcher session.", "checked": True}, {"text": "Показывать avatarUrl в Launcher top bar/profile menu.", "checked": True}, {"text": "Синхронизировать Plane user profile при OIDC login.", "checked": True}, {"text": "Сохранить существующий Plane avatar, если Authentik не отдает picture.", "checked": True}, {"text": "Проверить dcctouch@gmail.com после profile sync.", "checked": True}, "Реализовать Launcher profile storage и avatar upload как мастер-источник.", ], ), text_block( "plane", "Реализация этапа 5", """ В platform/infra/authentik/bootstrap-dev.py добавлен NODE.DC OAuth Mapping: profile context. Он отдает name, given_name, family_name, preferred_username, picture и avatar_url из Authentik user.name и user.attributes. Это временная dev-проекция до появления Launcher backend profile storage. Launcher BFF нормализует picture/avatar_url/avatar в avatarUrl и отдает его через /api/me. Frontend прокидывает avatarUrl в runtime user и показывает изображение в top bar/profile menu, если claim присутствует. Plane OIDC callback получил PLANE_OIDC_SYNC_PROFILE=1 по умолчанию. При успешном OIDC login Plane обновляет display_name, first_name, last_name и avatar из claims. Если OIDC projection не отдает picture/avatar_url, существующий Plane avatar не очищается. Runtime проверен: dcctouch@gmail.com теперь display_name DC Touch, first_name DC, last_name Touch, avatar_url сохранен. Launcher готов отобразить avatarUrl, но фактический единый avatar должен появиться из будущего Launcher profile storage/avatar upload, а не из ручной настройки Authentik. """, ), text_block( "plane", "Этап 5.5. Live access enforcement", """ Статус: частично реализовано. Plane должен не только пускать пользователя при OIDC login, но и регулярно проверять актуальный доступ в Launcher control-plane. Если админ снимает доступ к OPERATIONAL CORE, уже открытая Plane session должна быть отозвана на следующем API/request без ожидания logout/session expiry. Интеграция должна быть отключаемой env-флагами, чтобы Plane можно было развернуть standalone без Launcher/Auth projection. """, ), checker( "plane55", "Чекер этапа 5.5. Live access enforcement", [ {"text": "Добавить внутренний Launcher access-check endpoint.", "checked": True}, {"text": "Защитить endpoint server-side bearer token.", "checked": True}, {"text": "Добавить Plane middleware после AuthenticationMiddleware.", "checked": True}, {"text": "Проверять Launcher access по Authentik subject/email.", "checked": True}, {"text": "Удалять Plane sessions при denied access.", "checked": True}, {"text": "Сделать enforcement отключаемым env-флагом.", "checked": True}, {"text": "Убрать legacy Plane email/password экран из NODE.DC direct flow.", "checked": True}, {"text": "Редиректить page denied/revoked в Launcher вместо старого login UI.", "checked": True}, {"text": "Редиректить 401/403 frontend requests в NODE.DC OIDC handoff.", "checked": True}, {"text": "Сделать Task Manager sign-out сквозным NODE.DC logout.", "checked": True}, {"text": "Добавить Task Manager front-channel logout endpoint /logout.", "checked": True}, {"text": "Закрывать app sessions из Launcher global logout перед IdP logout.", "checked": True}, {"text": "Проверить Plane API -> Launcher check из контейнера.", "checked": True}, {"text": "Провести ручной browser acceptance: снять доступ и увидеть отзыв уже открытой Plane-сессии.", "checked": True}, ], ), text_block( "plane", "Реализация этапа 5.5", """ Launcher repo: server/dev-server.mjs получил POST /api/internal/access/check. Endpoint принимает serviceSlug, subject/email/userId, находит live Launcher user, считает groups через resolveRequiredGroups и возвращает allowed/matchedGroups/user без раскрытия frontend secrets. Token берется из server-side env: NODEDC_INTERNAL_ACCESS_TOKEN / NODEDC_PLATFORM_SERVICE_TOKEN / fallback PLANE_OIDC_CLIENT_SECRET. Plane repo: добавлен plane-src/apps/api/plane/authentication/middleware/nodedc_access.py и подключение в plane-src/apps/api/plane/settings/common.py сразу после django.contrib.auth.middleware.AuthenticationMiddleware. Middleware активируется только при PLANE_NODEDC_ACCESS_ENFORCEMENT=1 и наличии PLANE_NODEDC_ACCESS_CHECK_URL + token. Standalone Plane без этих env не меняет поведение. Если Launcher возвращает denied, middleware удаляет sessions текущего Plane user через модель Session, вызывает logout(request) и возвращает 403 JSON для API или redirect на PLANE_NODEDC_ACCESS_DENIED_REDIRECT_URL для page request. В local runtime denied page redirect ведет в Launcher, а не в legacy Plane login. При временной недоступности Launcher check middleware возвращает 503, но не удаляет session, чтобы не делать destructive logout из-за сетевого сбоя. plane-app/docker-compose.yaml и plane-app/plane.env получили PLANE_NODEDC_ACCESS_* env, PLANE_NODEDC_ACCESS_DENIED_REDIRECT_URL и extra_hosts для launcher.local.nodedc. В local runtime API container пересоздан с новым env; patched middleware/settings скопированы в /code/plane и API container перезапущен. Plane web получил NODE.DC handoff для прямых заходов на task.local.nodedc: app/(home)/page.tsx показывает NodeDCAuthRedirect вместо AuthBase на NODE.DC-доменах; AuthenticationWrapper больше не возвращает unauthenticated users на /, а отправляет в /auth/oidc/login с сохранением next_path; api.service.ts при 401 и nodedc_access_revoked также уводит в OIDC handoff. Старый email/password экран Plane сохранен как standalone/fallback для не-NODE.DC доменов, но не должен появляться в нормальном local.notdc/task.local.nodedc сценарии. Дополнение по logout semantics: Launcher /api/me теперь отдает global logout URL /auth/logout?global=1&returnTo=/. Launcher /auth/logout?global=1 сначала закрывает app sessions через front-channel logout URLs, затем top-level уводит браузер в Authentik end-session с post_logout_redirect_uri=/auth/logged-out и финально возвращает пользователя в /auth/login?prompt=login, а не на Authentik "Logout successful". Для Task Manager добавлен GET/POST /logout, который чистит Plane session и возвращает короткий технический ответ для front-channel. Стандартный Plane POST /auth/sign-out/ теперь после локального logout редиректит в PLANE_NODEDC_GLOBAL_LOGOUT_URL, поэтому кнопка выхода внутри Task Manager больше не должна мгновенно заводить пользователя обратно через активную SSO session. plane-src/apps/proxy/Caddyfile.ce маршрутизирует /logout в API, а plane-app/docker-compose.yaml монтирует этот Caddyfile в local proxy runtime, чтобы route не терялся при restart контейнера. В plane-app/plane.env добавлен PLANE_NODEDC_GLOBAL_LOGOUT_URL=http://launcher.local.nodedc/auth/logout?global=1&returnTo=/. Проверки 2026-05-04: node --check server/dev-server.mjs проходит; python3 -m py_compile nodedc_access.py/settings.common проходит; docker compose --env-file plane.env config проходит; http://127.0.0.1:5173/healthz и launcher.local.nodedc/healthz возвращают internalAccessApiConfigured=true; task.local.nodedc/auth/oidc/login/ возвращает 302 на Authentik; из plane-app-api-1 POST на http://launcher.local.nodedc/api/internal/access/check для silver_psih@yahoo.com возвращает allowed=True и matchedGroups=['nodedc:taskmanager:user'], для missing-user возвращает allowed=False. Web image nodedc/plane-frontend:ru пересобран, plane-app-web-1 пересоздан, собранный home asset содержит NodeDCAuthRedirect и строку «Переходим в NODE.DC». Ограничение: полный browser acceptance именно на уже открытой вкладке после снятия доступа еще должен подтвердить пользователь. Для durable production deploy нужно пересобрать nodedc/plane-backend:local, потому что текущий локальный runtime применен через docker cp поверх контейнера. """, ), text_block( "plane", "Этап 6. Standalone compatibility и Plane API adapter", """ Статус: частично реализовано. Plane должен оставаться самостоятельным продуктом, который можно развернуть клиенту без остального NODE.DC stack. NODE.DC-интеграция должна быть включаемым слоем: OIDC provider, external identity link, app access projection и будущий adapter через Plane API/API tokens. Запрещено строить интеграцию через прямое владение Plane DB со стороны Launcher. Launcher хранит платформенную привязку client/user/access, но Plane продолжает владеть workspace/project/task/comment и собственными ролями внутри приложения. Частично закрыто в live enforcement: NODE.DC access middleware включается только через env PLANE_NODEDC_ACCESS_ENFORCEMENT и не активируется в standalone-профиле без Launcher access-check URL/token. """, ), checker( "plane6", "Чекер этапа 6. Standalone compatibility и Plane API adapter", [ {"text": "Зафиксировать env-флаг включения NODE.DC SSO слоя.", "checked": True}, "Описать standalone-профиль Plane без Launcher/Auth projection.", "Проверить сохранение Plane API-token механизма.", "Спроектировать Launcher -> Plane API adapter.", "Добавить mapping launcher_client_id -> plane_workspace_id.", "Добавить mapping launcher_user_id -> plane_user_id.", "Запретить прямые записи Launcher в Plane DB.", ], ), text_block( "plane", "Актуализация 2026-05-09", """ Текущая модель Operational Core разделена на два режима. Для workspace, которыми управляет Launcher, участники/инвайты/права должны быть скрыты или переведены в readonly внутри Task Manager, чтобы не получить конфликтующий source of truth. Для standalone/public workspace штатные механики Task Manager остаются включенными: пользователь может создавать workspace, приглашать участников и управлять проектами внутри продукта. Практически проверено: корпоративные назначения из Launcher доходят до Operational Core, public-пользователь может создать workspace после выдачи доступа, а прямой доступ к сервису проходит через NODE.DC SSO. Safari-only падение workspace зафиксировано как отдельный deferred debug, потому что Chrome/Chromium flow работает и проблема не должна блокировать платформенную обвязку. Открытая развилка: нужно формально добавить managedBy=launcher/managedBy=tasker или эквивалентный флаг в mapping workspace, чтобы интерфейс Task Manager понимал, когда скрывать собственное управление пользователями, а когда оставлять автономный SaaS-режим. """, ), checker( "plane-actual-20260509", "Чекер актуализации 2026-05-09", [ {"text": "Сохранить автономность Task Manager как standalone-продукта.", "checked": True}, {"text": "Зафиксировать managedBy=launcher для enterprise workspace.", "checked": True}, {"text": "Зафиксировать managedBy=tasker для standalone/public workspace.", "checked": True}, "Скрыть или readonly-заблокировать Task Manager users/invites для managedBy=launcher.", "Оставить Task Manager users/invites включенными для managedBy=tasker.", "Очистить оставшиеся demo users/seed data без удаления живых связей.", "Оформить Safari-only workspace crash как отдельный deferred debug.", ], ), ], }, { "slug": "design-guide-canon", "name": "NDC дизайн-гайд: единый UI-код платформы", "priority": "medium", "state_group": "cancelled", "assignees": [CODEX_EMAIL], "description_html": html( "Карточка сохранена как отложенная архитектурная ссылка: реальный дизайн-канон NODE.DC живет в HDESIGN-CODE.md, а не в теле канбан-карточки.", "Цель актуализации: убрать конфликт источников истины и зафиксировать, что дальнейшие UI-работы должны сверяться с документом дизайн-канона и существующими launcher/tasker компонентами.", "Статус: deferred. Карточка не является активной задачей разработки до решения выделять дизайн-систему в отдельный пакет.", ), "blocks": [ text_block( "design-guide", "Текущая архитектура", """ Единый визуальный канон NODE.DC зафиксирован в репозитории Operational Core в документе HDESIGN-CODE.md. Он описывает glass/card shell, top-bar, admin overlay, auth screens, spacing, typography, motion, token usage и правила интеграции в Launcher/Task Manager. Карточка NDCPLATFORM-7 больше не должна конкурировать с этим документом и не должна хранить длинный HTML-дизайн-гайд. Канбан-карточка остается навигационной отметкой: дизайн-канон существует, но вынос в отдельную design-system работу пока отложен. """, ), text_block( "design-guide", "Этап 1. Зафиксировать источник дизайн-канона", """ Статус: выполнено. HDESIGN-CODE.md является source of truth для текущего UI-кода NODE.DC. При разработке Launcher/admin overlay, auth screens и Task Manager NODE.DC-слоев сверяемся с ним, а не с историческим HTML-телом карточки. """, ), checker( "design-guide1", "Чекер этапа 1. Источник дизайн-канона", [ {"text": "Признать HDESIGN-CODE.md реальным дизайн-каноном.", "checked": True}, {"text": "Оставить NDCPLATFORM-7 в deferred/cancelled, без активной разработки.", "checked": True}, {"text": "Не использовать старое HTML-тело карточки как актуальный UI-спек.", "checked": True}, ], ), text_block( "design-guide", "Реализация этапа 1", """ Карточка приведена к короткой structured layout записи. Исторический смысл сохранен, но актуальный дизайн-канон вынесен в документ репозитория, чтобы UI-решения не расходились между карточкой, кодом и документацией. """, ), text_block( "design-guide", "Этап 2. Future design-system extraction", """ Статус: backlog/deferred. Если появится необходимость переиспользовать NODE.DC UI между несколькими приложениями как библиотеку, эту работу нужно открывать отдельным этапом внутри той же крупной карточки: tokens, primitives, surfaces, modals, cards, auth shell и cross-app visual QA. """, ), checker( "design-guide2", "Чекер этапа 2. Future design-system extraction", [ "Принять решение, нужен ли отдельный design-system package.", "Выделить переиспользуемые primitives без ломки Launcher/Tasker UI.", "Описать visual QA для Auth/Launcher/Tasker экранов.", ], ), ], }, { "slug": "tasker-provisioning-workspace-onboarding", "name": "Tasker provisioning и workspace onboarding", "priority": "high", "state_group": "started", "assignees": [CODEX_EMAIL], "description_html": html( "Архитектурный узел между Launcher control plane и Operational Core: пользователь должен попадать в Tasker через NODE.DC SSO без redirect-loop, 500-страниц и ручных Plane-инвайтов.", "Цель: разделить доступ к приложению, workspace onboarding и доменные роли Tasker. Launcher выдает сервисный доступ и enterprise-назначения, Tasker сохраняет собственные workspace/project/task модели и standalone-режим.", "Критерий приемки: пользователь с доступом к Operational Core получает предсказуемый исход: назначенный workspace/project, разрешенный self-service workspace или ожидание назначения; для managedBy=launcher нет второго источника прав внутри Tasker.", ), "blocks": [ text_block( "tasker-provisioning", "Текущая архитектура", """ Launcher уже является рабочим control plane для клиентов, пользователей, групп, инвайтов, service grants, Operational Core workspace binding и project-level назначений. Authentik получает техническую group projection из Launcher и остается внутренним IdP. Operational Core подключен как Plane fork: OIDC/handoff, ExternalIdentityLink, live access middleware, workspace policy hook и internal adapter endpoints живут в Tasker, а Launcher вызывает их через защищенный server-to-server API. Прямых записей Launcher в Plane DB нет. Фактически реализованы: auto-create/link локального Tasker user по verified OIDC/handoff claims, workspace membership bridge, project membership bridge, policy check на создание workspace, NODE.DC create-workspace UX и очистка stale assignees после снятия пользователей из workspace/project. Открытая архитектурная граница: нужно формально закрепить managedBy=launcher/managedBy=tasker для workspace, чтобы enterprise-workspace управлялись из Launcher, а standalone/public workspace сохраняли штатные Tasker users/invites/admin mechanics. """, ), text_block( "tasker-provisioning", "Этап 1. OIDC fail-safe и local user/link provisioning", """ Статус: реализовано. Этап закрывает патологию первого входа: Authentik подтверждает пользователя, но Tasker не знает локального User/ExternalIdentityLink. Сейчас Tasker умеет idempotently создать или связать локального пользователя по verified OIDC/handoff claims и не уводит пользователя в бесконечный login loop. """, ), checker( "tasker-provisioning1", "Чекер этапа 1. OIDC fail-safe и local user/link provisioning", [ {"text": "Развести ошибки: нет platform access, нет local user/link, нет workspace membership.", "checked": True}, {"text": "Добавить auto-link existing Plane user по email отдельным env-флагом.", "checked": True}, {"text": "Добавить auto-create Tasker user по verified OIDC claims отдельным env-флагом.", "checked": True}, {"text": "Создавать/обновлять ExternalIdentityLink provider=authentik + subject.", "checked": True}, {"text": "Синхронизировать display name/email/avatar без пересоздания старого Plane user.", "checked": True}, {"text": "Сохранить standalone Plane auth/API механизмы вне NODE.DC env-профиля.", "checked": True}, ], ), text_block( "tasker-provisioning", "Реализация этапа 1", """ Tasker OIDC слой живет в plane-src/apps/api/plane/authentication/views/app/oidc.py. Он проверяет state/nonce/JWKS/audience/issuer, принимает groups, резолвит или создает локального пользователя и обновляет ExternalIdentityLink. Launcher service handoff ведет в /auth/nodedc/handoff/ и потребляется через защищенный /api/internal/handoff/consume. Handoff передает normalized user, avatarUrl и groups без раскрытия пароля или Authentik service token. Профильная синхронизация идет из claims и Launcher avatar URL. Существующий Plane owner dcctouch@gmail.com не пересоздается: связь держится через ExternalIdentityLink и старый Plane user сохраняет workspace/project/task связи. """, ), text_block( "tasker-provisioning", "Этап 2. Launcher -> Tasker workspace adapter", """ Статус: реализовано. Этап добавляет управляемый bridge для enterprise workspace: Launcher может назначить или снять пользователя в Operational Core workspace через internal Tasker API, а Tasker idempotently меняет WorkspaceMember без Plane email-invite. """, ), checker( "tasker-provisioning2", "Чекер этапа 2. Launcher -> Tasker workspace adapter", [ {"text": "Добавить Tasker internal workspace catalog endpoint.", "checked": True}, {"text": "Добавить internal endpoint ensure workspace membership.", "checked": True}, {"text": "Добавить internal endpoint remove workspace membership.", "checked": True}, {"text": "Защитить endpoints внутренним token/secret.", "checked": True}, {"text": "Добавить Launcher admin routes для workspace membership.", "checked": True}, {"text": "Сохранять workspace membership projection в Launcher storage.", "checked": True}, {"text": "Не использовать Plane email-invite как основной enterprise flow.", "checked": True}, ], ), text_block( "tasker-provisioning", "Реализация этапа 2", """ Tasker adapter endpoints лежат в plane-src/apps/api/plane/authentication/views/nodedc_workspace_adapter.py и подключены в plane-src/apps/api/plane/urls.py. Они резолвят workspace/user по slug/id/email/subject, создают или деактивируют WorkspaceMember и возвращают normalized membership summary. Launcher BFF вызывает adapter из server/dev-server.mjs, проверяет права администратора на client/user, пишет projection через server/control-plane-store.mjs и обновляет UI через admin overlay. """, ), text_block( "tasker-provisioning", "Этап 3. Workspace onboarding policy", """ Статус: частично реализовано. Сервисный доступ к Operational Core и право создавать workspace разделены. Launcher уже возвращает workspacePolicy через internal access check, а Tasker спрашивает policy перед create-workspace flow. Полная трехрежимная модель client/user/group policy еще не оформлена как production data model. """, ), checker( "tasker-provisioning3", "Чекер этапа 3. Workspace onboarding policy", [ {"text": "Добавить Launcher setting taskManager.workspaceCreationPolicy.", "checked": True}, {"text": "Возвращать workspacePolicy из /api/internal/access/check.", "checked": True}, {"text": "Добавить Tasker backend workspace policy resolver.", "checked": True}, {"text": "Сохранить self-service create-workspace flow в NODE.DC дизайне.", "checked": True}, "Оформить admin_managed ожидание назначения без create workspace.", "Добавить production policy model на уровне client/service/user/group.", "Связать policy с managedBy=launcher/managedBy=tasker.", ], ), text_block( "tasker-provisioning", "Реализация этапа 3", """ Launcher хранит базовую настройку workspace creation policy в settings.taskManager. Tasker использует plane.authentication.nodedc_workspace_policy, чтобы получить решение из Launcher access-check endpoint и разрешить или запретить создание workspace. Create workspace UI в Tasker приведен к NODE.DC auth-card layout без удаления штатной Plane формы: вариант nodedc-auth сохраняет standalone совместимость и не ломает остальные вызовы. """, ), text_block( "tasker-provisioning", "Этап 4. Project-level доступы Operational Core из Launcher", """ Статус: реализовано локально, ожидает ручной acceptance. Этап закрывает следующий слой после workspace binding: Launcher может назначать роль пользователя внутри конкретного проекта Tasker. Это остается adapter-вызовом через internal API, а не прямой записью в Plane DB. """, ), checker( "tasker-provisioning4", "Чекер этапа 4. Project-level access bridge", [ {"text": "Расширить Tasker workspace catalog проектами внутри workspace.", "checked": True}, {"text": "Добавить internal endpoint ensure project membership.", "checked": True}, {"text": "Добавить internal endpoint remove project membership.", "checked": True}, {"text": "Сохранять project membership projection в Launcher storage.", "checked": True}, {"text": "Показать workspaces и projects в Operational Core modal.", "checked": True}, {"text": "При project role гарантировать минимальный workspace membership.", "checked": True}, "Провести ручной acceptance: назначение роли проекта, refresh, вход пользователем в Tasker.", ], ), text_block( "tasker-provisioning", "Реализация этапа 4", """ Tasker: добавлены endpoints /api/internal/nodedc/project-memberships/ensure/ и /api/internal/nodedc/project-memberships/remove/. Они защищены тем же internal token, резолвят workspace/project/user и idempotently создают или обновляют ProjectMember. Launcher: добавлены admin routes для project memberships, control-plane projection taskManagerProjectMemberships и UI в Operational Core modal. Роль проекта меняется кликом: прочерк, гость, участник, админ. Проверки этапа ранее проходили: npm run build в Launcher, node --check server/dev-server.mjs server/control-plane-store.mjs и python compile для Tasker adapter/routes. """, ), text_block( "tasker-provisioning", "Этап 5. Stale assignees cleanup после снятия пользователей", """ Статус: реализовано в рабочем дереве, ожидает финальную проверку и коммит после подтверждения. После удаления, блокировки или снятия пользователя из workspace/project Tasker не должен продолжать показывать его исполнителем в карточках и группировках. Последняя локальная правка удаляет IssueAssignee на membership remove и фильтрует assignee arrays только по активным workspace/project membership. """, ), checker( "tasker-provisioning5", "Чекер этапа 5. Stale assignees cleanup", [ {"text": "Удалять IssueAssignee при снятии workspace membership.", "checked": True}, {"text": "Удалять IssueAssignee при снятии project membership.", "checked": True}, {"text": "Покрыть admin/license/member remove paths.", "checked": True}, {"text": "Фильтровать backend assignee_ids по active workspace/project membership.", "checked": True}, {"text": "Добавить frontend guard в internal Kanban card.", "checked": True}, {"text": "Проверить текущую БД на реально stale IssueAssignee.", "checked": True}, "Прогнать целевой regression по issue list/kanban после restart backend/web.", ], ), text_block( "tasker-provisioning", "Реализация этапа 5", """ Затронуты Tasker файлы: plane-src/apps/api/plane/authentication/views/nodedc_workspace_adapter.py, workspace/project/member/license views, issue list/detail endpoints, common grouper utilities и internal-contour-card.tsx. Проверка БД 2026-05-09: запрос по активным IssueAssignee без active WorkspaceMember/ProjectMember вернул 0 записей. Значит текущий runtime не содержит реально stale assignee links после последней очистки. Рабочее дерево Task Manager остается dirty: этот этап еще не закоммичен и требует финальной проверки перед переводом карточного пункта в полностью закрытое состояние. """, ), text_block( "tasker-provisioning", "Этап 6. Source-of-truth split managedBy", """ Статус: следующий критический этап. Нужно формально закрепить источник управления для workspace. managedBy=launcher означает enterprise workspace: пользователи, инвайты и базовые роли идут из Launcher, а Tasker не должен давать конфликтующее управление. managedBy=tasker означает standalone/public workspace: штатные Tasker механизмы пользователей, инвайтов и ролей остаются включенными. """, ), checker( "tasker-provisioning6", "Чекер этапа 6. Source-of-truth split managedBy", [ "Добавить managedBy в Launcher Tasker workspace binding.", "Возвращать managedBy/workspacePolicy из Launcher internal access-check.", "Передавать managedBy в Tasker adapter responses или workspace policy resolver.", "Скрыть или readonly-заблокировать Tasker users/invites для managedBy=launcher.", "Оставить Tasker users/invites включенными для managedBy=tasker.", "Проверить enterprise client admin и public self-service user flows отдельно.", "Зафиксировать правила в NDCPLATFORM-4 и NDCPLATFORM-10 после реализации.", ], ), ], }, { "slug": "safari-workspace-crash-debug", "name": "Safari: workspace crash и storage/OIDC debug", "priority": "high", "state_group": "cancelled", "assignees": [CODEX_EMAIL], "description_html": html( "Отложенный debug Safari-only падения workspace в Task Manager. Проблема не блокирует текущую платформенную архитектуру, потому что Chrome/Chromium flow работает.", "Цель актуализации: сохранить симптом и границы диагностики, но не смешивать browser-specific bug с Launcher/Auth/Tasker source-of-truth работами.", "Статус: deferred. Возвращаться после закрытия критичного managedBy/source-of-truth этапа или при воспроизводимом Safari regression report.", ), "blocks": [ text_block( "safari-crash", "Текущая архитектура", """ Safari-only падение workspace относится к browser/runtime compatibility слою Task Manager. Оно не меняет целевую архитектуру: Authentik остается внутренним IdP, Launcher — source of truth для enterprise-доступов, Task Manager — standalone-capable Operational Core module. Пока Chrome/Chromium flow работает, этот debug не должен блокировать платформенную работу по access matrix, workspace onboarding и managedBy split. Карточка остается отложенной, чтобы не смешивать runtime browser bug с архитектурными задачами. """, ), text_block( "safari-crash", "Этап 1. Сохранить симптом и границы debug", """ Статус: зафиксировано как deferred. Нужно вернуться к карточке только при наличии воспроизводимого сценария: Safari version, URL, user, workspace slug, console trace, network trace, storage/cookie state и отличие от Chrome на том же аккаунте. """, ), checker( "safari1", "Чекер этапа 1. Deferred symptom", [ {"text": "Не считать Safari-only падение блокером Launcher/Auth/Tasker архитектуры.", "checked": True}, {"text": "Сохранить карточку как отдельный debug bucket.", "checked": True}, "Снять Safari console trace.", "Снять Safari network trace.", "Сравнить localStorage/sessionStorage/cookies Safari vs Chrome.", ], ), text_block( "safari-crash", "Этап 2. Future Safari diagnostics", """ Статус: deferred. Будущий debug должен идти от воспроизводимого crash-path: auth/session sync, workspace bootstrap API, storage hydration, frontend route boundary, browser-specific cookie policy или WebKit-only JS/runtime issue. """, ), checker( "safari2", "Чекер этапа 2. Safari diagnostics", [ "Воспроизвести падение на актуальном Safari.", "Отделить auth/session issue от frontend runtime crash.", "Проверить WebKit cookie/storage policy на task.local.nodedc.", "Сформулировать минимальный фикс без изменения общей архитектуры.", ], ), ], }, { "slug": "public-user-entry-and-service-access", "name": "Публичный контур пользователей", "priority": "high", "state_group": "backlog", "assignees": [PLATFORM_OWNER_EMAIL, CODEX_EMAIL], "description_html": html( "Отдельный public/open-access контур для внешних пользователей, которые приходят не как участники клиентской компании, а как самостоятельные пользователи сервисов NODE.DC.", "Цель: не плодить новый админский интерфейс, а расширить существующий Launcher control plane. Enterprise-компании остаются company-scoped, public users попадают в отдельный Public Access Pool и получают сервисные доступы вручную через root-admin flow.", "Критерий приемки: пользователь может нажать Запросить доступ в Auth/Login, заявка появляется в Launcher в разделе Инвайты как вкладка Заявки, root admin вручную апрувит/отклоняет, генерирует ссылку, а после входа пользователь видит только витрину и свои разрешенные сервисы.", ), "blocks": [ text_block( "public-users", "Текущая архитектура", """ Уже есть единый Authentik login, branded под NODE.DC, и Launcher как точка входа. Launcher умеет показывать витрину приложений, хранить клиентов, пользователей, инвайты, группы, матрицу доступов и привязку Operational Core workspace к клиенту. Enterprise-сценарий частично реализован: root admin создает компанию, добавляет туда client admin, выдает доступы, а client admin дальше управляет пользователями, группами, инвайтами и назначениями в рамках своей компании через Launcher. В Operational Core такие пользователи не должны видеть управление users/invites/workspace creation для launcher-managed workspace; они работают только в среде, которую им задал админ. Public/open-access контур должен быть отдельной группой пользователей, не привязанной к клиентской компании. Рабочее название: Public Access Pool / Свободный доступ. Это не новый enterprise-client и не хаотичный список отдельных компаний, а отдельный cohort в Launcher, видимый root admin. Public user не получает Launcher admin overlay, пока ему явно не выдали роль; он видит витрину и сервисы. Глобальная identity должна быть одна на email/Auth subject. Если пользователь сначала попал в Public Access Pool, а потом его надо перевести в enterprise-компанию, правильная операция — не удаление и повторная регистрация, а административный перевод: создать company membership, выбрать роль/группы, при необходимости убрать public-cohort marker и пересчитать доступы. Также появился standalone-сценарий Operational Core: public-пользователь после выдачи доступа к сервису может создать собственный workspace и управлять им через штатный Task Manager, но только для workspace managedBy=tasker. Создание NODE.DC identity и первичная выдача сервисного доступа остаются в Launcher. Еще не реализованы: кнопка Запросить доступ в login flow, модель access requests с обязательными полями, вкладка Заявки рядом с текущими Инвайтами, Public Access Pool в Launcher admin, перевод public user в компанию, правила приглашения других людей в public Tasker workspace без почтовой инфраструктуры. """, ), text_block( "public-users", "Этап 1. Разделение enterprise и Public Access Pool", """ Статус: backlog. Нужно формально разделить два режима: enterprise/direct-contract company scope и public/open-access pool. Enterprise-контур остается текущей реализацией: root admin создает компанию, назначает client admin, client admin управляет только своей компанией через Launcher. Public Access Pool — отдельный cohort для внешних самостоятельных пользователей. Он должен быть доступен root admin в существующем Launcher admin UI как отдельный режим/область рядом с компаниями, но не должен засорять список компаний сотнями псевдо-клиентов. Главное правило безопасности: у каждого Operational Core workspace должен быть ровно один источник управления правами. managedBy=launcher означает управление из Launcher и readonly/hidden member management в Tasker; managedBy=tasker означает штатное workspace management внутри Operational Core, но identity/service entitlement все равно выдаются через Launcher. """, ), checker( "public-users1", "Чекер этапа 1. Разделение enterprise и Public Access Pool", [ "Зафиксировать типы контуров: enterprise/direct-contract и public/open-access.", "Добавить модель Public Access Pool или эквивалентный user cohort без создания отдельной компании на каждого пользователя.", "Сделать global user уникальным по email/Auth subject независимо от public или enterprise происхождения.", "Показать Public Access Pool root admin в существующем Launcher admin selector/режиме.", "Скрыть Public Access Pool от client admin компаний.", "Описать managedBy=launcher для корпоративных workspace.", "Описать managedBy=tasker для публичных standalone workspace.", "Развести видимость админки Launcher для root admin, client admin и public user.", "Описать, как public-пользователь видит витрину сервисов без администрирования Launcher.", "Зафиксировать запрет прямого создания Auth/Launcher users из Tasker без Launcher approval.", ], ), text_block( "public-users", "Этап 2. Запрос приглашения из окна входа", """ Статус: backlog. В branded login нужно добавить безопасный сценарий запроса доступа. Это не регистрация с паролем и не обход Authentik: пользователь оставляет заявку, а учетная запись и invite создаются только после ручного решения root admin. Заявка должна попадать в Launcher admin, а не теряться в почте. Почтовое уведомление можно добавить позже как транспорт, но source of truth для обработки заявок должен быть внутри Launcher. UI-решение: не добавлять новое большое окно. Расширить существующий раздел Инвайты вкладками Заявки и Сгенерированные инвайты. Для root admin вкладка Заявки показывает public/open-access заявки. Для client admin текущие company-scoped инвайты остаются без доступа к global public queue. """, ), checker( "public-users2", "Чекер этапа 2. Запрос приглашения из окна входа", [ "Добавить вторичную кнопку Запросить доступ под кнопкой Войти.", "Добавить форму заявки с обязательными полями: email, имя, фамилия, отчество, телефон, компания.", "Оставить интересующий сервис/задачу и комментарий опциональными полями.", "Блокировать отправку, если любое обязательное поле пустое.", "Дублировать required validation server-side в Launcher public endpoint.", "Не запрашивать пароль на этапе заявки.", "Сохранять заявку server-side в Launcher storage/backend как accessRequest.", "Расширить текущий раздел Инвайты вкладками Заявки и Сгенерированные инвайты.", "Добавить статусы заявки: новая, в работе, принята, отклонена, архив.", "Добавить действие Принять: создать invite в Public Access Pool для указанного email.", "Добавить действие Отклонить без создания пользователя/Auth identity.", "Пока нет почты, показывать сгенерированную ссылку для ручной передачи админом.", ], ), text_block( "public-users", "Этап 3. Public user access flow", """ Статус: backlog. После апрува пользователь регистрируется по invite link, попадает в Launcher и видит витрину сервисов. Администрирования Launcher у него нет. Он находится в Public Access Pool, но не получает доступ к Operational Core или другим сервисам автоматически. Доступы к сервисам на первом этапе выдает root admin вручную через матрицу доступов. Когда public user получает Operational Core, он может создать собственный workspace в Tasker. Такой workspace должен быть managedBy=tasker: Tasker управляет проектами и workspace membership, Launcher управляет только identity, service entitlement и глобальной блокировкой пользователя. """, ), checker( "public-users3", "Чекер этапа 3. Public user access flow", [ "Проверить, что public user не видит администрирование Launcher.", "Проверить, что public user после invite попадает в Public Access Pool.", "Не выдавать Operational Core автоматически только по факту принятого invite.", "Показывать все сервисы витрины с состояниями доступен/нет доступа.", "Раздавать сервисный доступ public user через матрицу доступов Launcher.", "Открывать Operational Core без повторной авторизации после доступа.", "Разрешить создание workspace внутри Tasker для managedBy=tasker.", "Оставить настройки workspace/project внутри Tasker включенными для managedBy=tasker.", "Скрывать настройки участников/инвайтов Tasker только для managedBy=launcher.", ], ), text_block( "public-users", "Этап 4. Перевод Public user в enterprise-компанию", """ Статус: backlog. Нужна штатная операция root admin: перевести пользователя из Public Access Pool в конкретную компанию. Это безопаснее, чем удалять пользователя и заставлять его регистрироваться заново по enterprise-инвайту, потому что Authentik identity, session history, audit trail, accepted invites и будущие billing/usage links остаются консистентными. Целевая модель: Launcher хранит global user отдельно от company memberships. Public Access Pool — cohort/source/status, а не единственный контейнер identity. При переводе root admin выбирает компанию, роль, группы и политику доступа. Система создает или обновляет ClientMembership, может архивировать public pool membership/request, пересчитывает Authentik groups и service grants. Если enterprise admin позже выписывает invite на email уже существующего public user, accept flow должен merge by email в тот же global user, а не создавать дубль. Нужна также мягкая операция Убрать из Public Pool. Она не должна по умолчанию удалять global user/Auth identity: она архивирует public cohort и отзывает public-only grants. Полное удаление пользователя — отдельное destructive действие с явным cleanup-планом. """, ), checker( "public-users4", "Чекер этапа 4. Перевод Public user в компанию", [ "Добавить root-admin действие Перевести в компанию для пользователя из Public Access Pool.", "Выбирать target company, company role и группы при переводе.", "Создавать или обновлять ClientMembership без создания второго global user.", "Принимать enterprise invite на email существующего public user через merge by email.", "Архивировать или отключать public cohort marker после успешного перевода по выбору admin.", "Пересчитывать Authentik groups/service grants после перевода.", "Логировать перевод в audit с source pool, target company, actor и ролью.", "Добавить действие Убрать из Public Pool без удаления global Auth identity по умолчанию.", "Оставить полное удаление user отдельным destructive flow с подтверждением и cleanup.", ], ), text_block( "public-users", "Этап 5. Public workspace collaboration и invite boundary", """ Статус: backlog. Самый опасный участок — как public-пользователи приглашают других людей в свой Operational Core workspace без готовой почтовой инфраструктуры. Нельзя позволять Tasker напрямую создавать Authentik/Launcher users: это ломает единый identity perimeter. Правило для NODE.DC public mode: если приглашенный email уже принадлежит активному NODE.DC user с доступом к Operational Core, Tasker может добавить его в managedBy=tasker workspace по точному email/ID без глобального поиска людей. Если пользователя нет или у него нет entitlement на Operational Core, Tasker должен создать request в Launcher с контекстом workspace/requestedBy/targetEmail/role. Root admin вручную апрувит, генерирует invite link и после принятия доступ доводится до нужного workspace через Tasker adapter. Для managedBy=launcher workspace любые Tasker-side invites/users остаются скрытыми или readonly. Приглашения в enterprise-контуре идут через Launcher company admin/root admin, как в текущей реализации. """, ), checker( "public-users5", "Чекер этапа 5. Public workspace collaboration", [ "Запретить прямое создание Authentik/Launcher user из Tasker invite flow.", "Разрешить добавление существующего NODE.DC user в public managedBy=tasker workspace по точному email/ID.", "Проверять, что target user имеет Operational Core entitlement перед прямым добавлением.", "Если target user отсутствует или не имеет entitlement, создавать Launcher accessRequest с Tasker workspace context.", "Показывать такие заявки root admin во вкладке Заявки с источником Operational Core.", "После approval создавать invite link и связывать accepted user с исходным Tasker workspace.", "Оставить enterprise managedBy=launcher workspace без Tasker-side invites.", "Отложить автоматическую email-доставку до отдельного mailer/email этапа.", ], ), text_block( "public-users", "Этап 6. Billing-ready модель", """ Статус: backlog. Биллинг пока не реализуется, но модель публичного контура должна не закрыть путь к оплатам. Для enterprise-клиентов остаются прямые договоры и ручные даты договора/оплаты. Для public/self-service пользователей позже появятся подписки, тарифы, лимиты и модульные entitlements. """, ), checker( "public-users6", "Чекер этапа 6. Billing-ready модель", [ "Описать entitlement по каждому сервису отдельно.", "Описать статус подписки public user: trial, active, expired, blocked.", "Заложить лимиты Operational Core: workspace count, members count, storage.", "Заложить лимиты Voice Tasker: minutes, requests, model tier.", "Не внедрять платежный провайдер до отдельного billing RFC.", "Не смешивать прямые договоры enterprise-клиентов и public subscriptions.", ], ), ], }, { "slug": "auth-sdk-application-standards", "name": "Auth SDK и стандарты приложений", "priority": "medium", "state_group": "backlog", "assignees": [CODEX_EMAIL], "description_html": html( "Общий слой правил и SDK для будущих приложений NODE.DC, чтобы Tender, Agents, 1C, DM и новые сервисы не реализовывали auth каждый раз по-разному.", "Критерий приемки: есть typed AuthUser, JWKS/JWT validation helpers, requireAppAccess, env contract и документация claims для Node.js/Next.js сервисов; для Plane описан Python/Django эквивалент.", ), "blocks": [ text_block( "sdk", "Текущая архитектура", """ Единого auth-sdk пока нет. В platform/packages/auth-sdk создан только README и контракт будущего пакета. Пока не подключен Launcher backend, SDK должен оставаться спецификацией, чтобы не плодить преждевременные абстракции. """, ), text_block( "sdk", "Этап 1. Claims contract", """ Статус: не реализовано. Сначала нужно стабилизировать claims и naming групп, иначе SDK закрепит неправильный контракт. """, ), checker( "sdk1", "Чекер этапа 1. Claims contract", [ "Зафиксировать AuthUser type.", "Зафиксировать обязательные claims.", "Зафиксировать app access group naming.", "Зафиксировать error model для deny/unauthorized.", "Синхронизировать AUTH_MODEL.md с Authentik bootstrap.", ], ), text_block( "sdk", "Этап 2. TypeScript auth-sdk", """ Статус: backlog. После Launcher backend можно вынести повторяемую JWT/JWKS логику в маленький пакет без привязки к конкретному web framework. """, ), checker( "sdk2", "Чекер этапа 2. TypeScript auth-sdk", [ "Добавить JWKS loader/cache.", "Добавить JWT validation.", "Добавить normalizeAuthUser.", "Добавить requireAppAccess.", "Добавить unit tests.", "Подключить SDK в Launcher backend.", ], ), ], }, { "slug": "security-acceptance-staging", "name": "Security acceptance и staging path", "priority": "high", "state_group": "backlog", "assignees": [CODEX_EMAIL], "description_html": html( "Финальный слой проверки, что платформа не только запускается, но и закрывает реальные security-сценарии: прямые ссылки, отключение пользователей, отсутствие наружных внутренних портов, audit и staging path.", "Критерий приемки: security checklist закрыт проверками, staging compose или понятный staging plan готов, secrets не попали в frontend/git, Plane admin сохранил данные после OIDC migration.", ), "blocks": [ text_block( "security", "Текущая архитектура", """ Security checklist создан в platform/docs/SECURITY_CHECKLIST.md. Базовый local acceptance по happy path и части negative path уже пройден: Launcher без session уводит в OIDC login, недоступные плитки отображаются как disabled/Нет доступа, снятие доступа отзывает открытую Plane-сессию, старый Plane admin проходит через OIDC migration, frontend bundle не содержит service-token маркеров. Остаются destructive/edge checks: direct deny без group access, deactivated user, audit log и staging-hardening. """, ), text_block( "security", "Этап 1. Security acceptance tests", """ Статус: backlog. Нужно проверить не только happy path, но и отказы: нет логина, нет группы, прямой URL, deactivated user, отсутствие service token во frontend. """, ), checker( "security1", "Чекер этапа 1. Security acceptance tests", [ {"text": "Проверить redirect Launcher без логина.", "checked": True}, {"text": "Проверить скрытие Task Manager без group access.", "checked": True}, "Проверить deny на прямой task.local.nodedc без group access.", {"text": "Проверить отзыв уже открытой downstream-сессии после снятия доступа.", "checked": True}, {"text": "Проверить успешный вход пользователя с access.", "checked": True}, {"text": "Проверить старого Plane admin после OIDC migration.", "checked": True}, "Проверить deactivate user.", "Проверить audit log admin actions.", {"text": "Проверить отсутствие service tokens во frontend bundle.", "checked": True}, ], ), text_block( "security", "Этап 2. Staging deployment path", """ Статус: backlog. После локальной схемы нужен staging plan: HTTPS, secure cookies, реальные domains, secrets management, закрытые внутренние порты. """, ), checker( "security2", "Чекер этапа 2. Staging deployment path", [ "Описать staging domains.", "Подготовить docker-compose.staging.yml или эквивалентный deployment plan.", "Включить COOKIE_SECURE=true.", "Включить HTTPS/HSTS.", "Закрыть Postgres/Redis/MinIO наружу.", "Описать backup/restore runbook.", "Описать secrets rotation policy.", ], ), ], }, ] def ensure_project(workspace, owner, codex_user): project, _ = Project.objects.get_or_create( workspace=workspace, identifier=PROJECT_IDENTIFIER, defaults={ "name": PROJECT_NAME, "description": "Платформенный проект NODE.DC: Authentik, Launcher control plane, Plane OIDC и future app auth foundation.", "network": 0, "project_lead": owner, "created_by": codex_user, "updated_by": codex_user, }, ) project.name = PROJECT_NAME project.description = "Платформенный проект NODE.DC: Authentik, Launcher control plane, Plane OIDC и future app auth foundation." project.network = 0 project.project_lead = owner project.timezone = "Europe/Moscow" project.created_by = project.created_by or codex_user project.updated_by = codex_user project.save() for index, state_spec in enumerate(STATE_TEMPLATES): state = State.all_state_objects.filter(project=project, group=state_spec["group"]).first() if state is None: state = State(project=project, workspace=workspace) state.name = state_spec["name"] state.group = state_spec["group"] state.color = state_spec["color"] state.default = state_spec["default"] state.is_triage = state_spec["group"] == "triage" state.sequence = DEFAULT_STATES[index]["sequence"] state.created_by = state.created_by or codex_user state.updated_by = codex_user state.save(disable_auto_set_user=True) if state_spec["default"]: project.default_state = state project.save() for user, role in [(owner, 20), (codex_user, 20)]: member, _ = ProjectMember.objects.get_or_create( project=project, member=user, defaults={"workspace": workspace, "role": role, "created_by": codex_user, "updated_by": codex_user}, ) member.workspace = workspace member.role = role member.is_active = True member.updated_by = codex_user member.save() return project def ensure_issue(workspace, project, codex_user, spec): state = State.all_state_objects.get(project=project, group=spec["state_group"]) issue = Issue.objects.filter(project=project, workspace=workspace, external_source=SOURCE, external_id=spec["slug"]).first() if issue is None: issue = Issue.objects.filter(project=project, workspace=workspace, name=spec["name"]).first() if issue is None: issue = Issue(project=project, workspace=workspace, created_by=codex_user) issue.name = spec["name"] issue.description_html = spec["description_html"] issue.detail_layout = {"nodedc_structured_blocks": spec["blocks"]} issue.priority = spec["priority"] issue.state = state issue.start_date = date.today() if spec["state_group"] == "started" else None issue.target_date = None issue.external_source = SOURCE issue.external_id = spec["slug"] issue.created_by = issue.created_by or codex_user issue.updated_by = codex_user issue.save(disable_auto_set_user=True) IssueAssignee.objects.filter(issue=issue).delete() for email in spec["assignees"]: assignee = User.objects.get(email=email) IssueAssignee.objects.get_or_create( issue=issue, assignee=assignee, defaults={ "project": project, "workspace": workspace, "created_by": codex_user, "updated_by": codex_user, }, ) return issue def ensure_view(workspace, project, codex_user): view, _ = IssueView.objects.get_or_create( workspace=workspace, project=project, name="Roadmap NODE.DC platform", defaults={ "description": "Крупные архитектурные блоки платформенной auth/SSO работы", "filters": {}, "rich_filters": {}, "owned_by": codex_user, "access": 1, "logo_props": {}, }, ) view.description = "Крупные архитектурные блоки платформенной auth/SSO работы" view.filters = {} view.rich_filters = {} view.owned_by = codex_user view.access = 1 view.save(disable_auto_set_user=True) @transaction.atomic def main(): workspace = Workspace.objects.get(slug=WORKSPACE_SLUG) owner = workspace.owner codex_user = User.objects.get(email=CODEX_EMAIL) project = ensure_project(workspace, owner, codex_user) issues = [ensure_issue(workspace, project, codex_user, card) for card in CARDS] ensure_view(workspace, project, codex_user) summary = { "workspace": workspace.slug, "project": f"{project.identifier} / {project.name}", "issues": [f"{project.identifier}-{issue.sequence_id}: {issue.name}" for issue in issues], } print(summary) main()