# NODE.DC Launcher — техническое задание на фронтенд MVP и light-backend

**Версия:** 0.1  
**Назначение документа:** согласование архитектуры Launcher с backend-командой и постановка задачи на разработку frontend/light-backend в Codex.  
**Формат реализации:** отдельное одностраничное React-приложение.  
**Текущий этап:** без подключения Authentik к runtime, но с заранее заложенной структурой под будущую Authentik/OIDC-интеграцию.  

---

## 1. Краткое описание продукта

**NODE.DC Launcher** — отдельное веб-приложение, которое является единой точкой входа в экосистему NODE.DC.

Launcher не является частью NodeDC Agent Platform и не является частью Task Manager. Это отдельный верхнеуровневый слой, который отвечает за:

- вход пользователя в экосистему;
- отображение доступных пользователю сервисов;
- управление клиентами и пользователями;
- выдачу доступов к сервисам;
- подготовку структуры под Authentik/OIDC;
- запуск внешних приложений по SSO-ссылке;
- визуальную витрину сервисов NODE.DC.

В экосистему могут входить разные приложения:

- NodeDC Agent Platform;
- NODE.DC Task Manager;
- 1C Assistant;
- DM / Digital Modules;
- Tender Agent;
- Digital Twin Viewer;
- внутренние демо-сервисы;
- будущие сервисы, подключаемые через каталог Launcher.

Важно: **Launcher управляет доступом к приложениям/сервисам, но не управляет внутренними сущностями этих приложений**.

Например:

- проекты внутри Task Manager / Plane не относятся к Launcher;
- workflow внутри NodeDC не относятся к Launcher;
- доски, задачи, проектные роли и внутренние настройки Task Manager остаются внутри Task Manager;
- sharing workflow, агентные сценарии и runtime-права остаются внутри NodeDC.

Launcher даёт пользователю вход в приложение. Всё, что происходит внутри приложения после входа, является ответственностью самого приложения.

---

## 2. Цель текущей разработки

Создать первый полноценный frontend MVP Launcher, который можно развивать дальше как production-приложение.

На текущем этапе нужно сделать:

1. отдельное React-приложение;
2. одностраничную архитектуру без дополнительных публичных страниц;
3. пользовательский экран-витрину доступных сервисов;
4. root-admin режим для управления клиентами, пользователями, сервисами и доступами;
5. client-admin режим для управления пользователями и доступами внутри своей компании;
6. mock/light-backend API, который имитирует будущие данные Authentik и backend;
7. доменную модель, совместимую с будущей Authentik/OIDC-интеграцией;
8. визуальную систему в стиле NODE.DC glass-canon;
9. структуру проекта, пригодную для дальнейшей backend-интеграции.

На текущем этапе **не требуется**:

- подключать реальный Authentik;
- реализовывать реальный SSO;
- синхронизировать пользователей с Plane;
- синхронизировать пользователей с NodeDC;
- реализовывать реальные invite email;
- управлять проектами Plane;
- управлять workflow NodeDC;
- реализовывать billing;
- реализовывать production audit trail на backend.

Но все эти направления должны быть предусмотрены архитектурно.

---

## 3. Главная граница ответственности Launcher

Launcher — это **Control Plane экосистемы**, а не замена внутренним админкам сервисов.

### 3.1. Launcher отвечает за

- кто является клиентом;
- какие пользователи относятся к клиенту;
- какие группы есть внутри клиента;
- какие сервисы подключены клиенту;
- какие сервисы видит конкретный пользователь;
- кто является админом клиента;
- какие сервисы доступны всем, группе или отдельному участнику;
- какие индивидуальные исключения применяются;
- какие приложения есть в каталоге;
- какой URL, иконка, описание, статус и медиа-превью у приложения;
- какой статус синхронизации с Authentik / внешними приложениями.

### 3.2. Launcher не отвечает за

- проекты внутри Task Manager / Plane;
- доски внутри Task Manager;
- задачи внутри Task Manager;
- роли пользователей внутри конкретных проектов Task Manager;
- workflow внутри NodeDC;
- sharing конкретных workflow внутри NodeDC;
- настройку конкретных агентов NodeDC;
- бизнес-логику 1C Assistant;
- внутренние настройки подключённых сервисов.

### 3.3. Принцип разделения

```txt
Launcher
  управляет доступом к приложению

Task Manager
  управляет workspace, проектами, задачами, досками, внутренними ролями

NodeDC
  управляет workflow, агентами, sharing, runtime, execution traces

1C Assistant
  управляет своим набором данных, инструментов, сессий и прав
```

---

## 4. Общая архитектура экосистемы

```txt
Информационный сайт
        │
        ▼
Authentik / OIDC
        │
        ▼
NODE.DC Launcher
        │
        ├── NodeDC Agent Platform
        ├── NODE.DC Task Manager
        ├── 1C Assistant
        ├── Tender Agent
        ├── Digital Twin Viewer
        ├── DM / Digital Modules
        └── будущие сервисы
```

На текущем этапе Authentik не подключается физически, но приложение должно проектироваться так, чтобы позже подставить настоящий OIDC-профиль и claims без переписывания frontend-архитектуры.

---

## 5. Термины

| Термин | Описание |
|---|---|
| Клиент | Владелец доступа. Может быть компанией или частным лицом. |
| Компания | Клиент типа `company`. Например, ООО «Ромашка». |
| Частное лицо | Клиент типа `person`. Например, Иван Петров. |
| Участник | Пользователь внутри клиента. |
| Группа | Подгруппа участников внутри клиента. Например, «Бухгалтерия», «Менеджеры», «Руководство». |
| Сервис | Приложение, доступное через Launcher. Например, NodeDC, Task Manager, 1C Assistant. |
| Доступ | Правило, которое определяет, кому показывать и разрешать сервис. |
| Исключение | Индивидуальное правило, которое перекрывает общий доступ. Например, сервис включён всему клиенту, но конкретному пользователю отключён. |
| Root Admin | Главный администратор NODE.DC, который видит всех клиентов, пользователей и сервисы. |
| Client Admin | Администратор конкретного клиента. |
| Member | Обычный пользователь клиента. |
| Service Catalog | Каталог подключаемых сервисов. |
| Service Rail | Нижняя карусель/панель доступных пользователю сервисов. |
| Service Stage | Центральная визуальная зона выбранного сервиса. |

---

## 6. Роли и уровни прав

### 6.1. Глобальные роли Launcher

```ts
export type LauncherGlobalRole =
  | "root_admin"
  | "support_admin"
  | "client_owner"
  | "client_admin"
  | "member";
```

### 6.2. Root Admin NODE.DC

Root Admin — внутренний супер-администратор NODE.DC.

Он может:

- видеть всех клиентов;
- создавать клиентов;
- редактировать клиентов;
- блокировать клиентов;
- видеть всех пользователей;
- создавать пользователей;
- блокировать пользователей;
- создавать сервисы;
- редактировать каталог сервисов;
- выдавать доступы любому клиенту, группе или участнику;
- создавать исключения;
- видеть статусы синхронизации;
- повторять синхронизацию;
- смотреть детали ошибок;
- видеть скрытые и отключённые сервисы;
- управлять демо-доступами.

### 6.3. Support Admin

Опциональная внутренняя роль NODE.DC.

На MVP можно не реализовывать отдельно, но архитектурно предусмотреть.

Может использоваться для сотрудников поддержки, которым нужно видеть клиентов и помогать с настройкой, но не менять критические системные параметры.

### 6.4. Client Owner

Главный администратор клиента.

Может:

- видеть только свою компанию / клиента;
- приглашать пользователей;
- деактивировать пользователей внутри своей компании;
- создавать группы внутри клиента;
- добавлять пользователей в группы;
- назначать client_admin;
- выдавать пользователям доступ к тем сервисам, которые уже разрешены клиенту;
- смотреть итоговые доступы пользователей;
- управлять invite-ссылками;
- видеть статусы синхронизации по своей компании.

Не может:

- видеть других клиентов;
- создавать глобальные сервисы;
- менять URL сервисов;
- редактировать технические slug сервисов;
- выдавать доступ к сервису, который не подключен клиенту;
- управлять root-admin пользователями;
- напрямую менять Authentik-конфигурацию.

### 6.5. Client Admin

Администратор внутри клиента. Права похожи на Client Owner, но могут быть ограничены.

На MVP можно сделать Client Owner и Client Admin одинаковыми по интерфейсу, но в типах оставить разные роли.

### 6.6. Member

Обычный пользователь.

Может:

- войти в Launcher;
- видеть только доступные ему сервисы;
- открыть доступный сервис;
- открыть профиль;
- выйти из аккаунта;
- выбрать клиента, если пользователь состоит в нескольких клиентах.

Не может:

- открывать админку;
- видеть пользователей;
- видеть клиентов;
- выдавать доступы;
- видеть скрытые сервисы;
- редактировать каталог сервисов.

---

## 7. Матрица прав

| Действие | Root Admin | Client Owner | Client Admin | Member |
|---|---:|---:|---:|---:|
| Видеть все сервисы каталога | Да | Нет | Нет | Нет |
| Видеть доступные себе сервисы | Да | Да | Да | Да |
| Видеть скрытые сервисы | Да | Нет | Нет | Нет |
| Создать клиента | Да | Нет | Нет | Нет |
| Редактировать клиента | Да | Только свой профиль клиента | Только свой профиль клиента | Нет |
| Заблокировать клиента | Да | Нет | Нет | Нет |
| Видеть всех пользователей | Да | Нет | Нет | Нет |
| Видеть пользователей своего клиента | Да | Да | Да | Нет |
| Приглашать пользователей | Да | Да | Да | Нет |
| Деактивировать пользователя клиента | Да | Да | Да | Нет |
| Назначить Client Admin | Да | Да | Опционально | Нет |
| Создать группу внутри клиента | Да | Да | Да | Нет |
| Подключить сервис клиенту | Да | Нет | Нет | Нет |
| Подключить доступ к разрешённому сервису группе | Да | Да | Да | Нет |
| Подключить доступ к разрешённому сервису пользователю | Да | Да | Да | Нет |
| Создать deny-исключение | Да | Да | Да | Нет |
| Создать сервис в каталоге | Да | Нет | Нет | Нет |
| Редактировать сервис | Да | Нет | Нет | Нет |
| Открыть доступный сервис | Да | Да | Да | Да |
| Смотреть статусы синхронизации | Да | По своему клиенту | По своему клиенту | Нет |

---

## 8. Доменная модель

### 8.1. Client

```ts
export type ClientType = "company" | "person";
export type ClientStatus = "active" | "suspended" | "demo" | "expired";

export interface Client {
  id: string;
  type: ClientType;
  name: string;
  legalName?: string;
  status: ClientStatus;
  demoEndsAt?: string | null;
  contactName?: string | null;
  contactEmail?: string | null;
  notes?: string | null;
  createdAt: string;
  updatedAt: string;
}
```

### 8.2. LauncherUser

```ts
export type LauncherUserStatus = "invited" | "active" | "blocked";

export interface LauncherUser {
  id: string;
  authentikUserId?: string | null;
  email: string;
  name: string;
  avatarUrl?: string | null;
  globalStatus: LauncherUserStatus;
  createdAt: string;
  updatedAt: string;
}
```

На текущем этапе `authentikUserId` может быть mock-полем. После интеграции оно должно ссылаться на пользователя Authentik.

### 8.3. ClientMembership

```ts
export type ClientMembershipRole =
  | "client_owner"
  | "client_admin"
  | "member";

export type ClientMembershipStatus = "active" | "disabled";

export interface ClientMembership {
  id: string;
  clientId: string;
  userId: string;
  role: ClientMembershipRole;
  status: ClientMembershipStatus;
  createdAt: string;
  updatedAt: string;
}
```

Один пользователь потенциально может быть участником нескольких клиентов.

### 8.4. ClientGroup

```ts
export interface ClientGroup {
  id: string;
  clientId: string;
  name: string;
  description?: string | null;
  memberIds: string[];
  createdAt: string;
  updatedAt: string;
}
```

Группа всегда существует внутри клиента. Группа не является глобальной.

Примеры групп:

- Руководство;
- Менеджеры;
- Бухгалтерия;
- Демо-команда;
- IT-администраторы.

### 8.5. Service

```ts
export type ServiceStatus =
  | "active"
  | "maintenance"
  | "hidden"
  | "disabled";

export interface Service {
  id: string;
  slug: string;
  title: string;
  subtitle?: string | null;
  description: string;
  fullDescription?: string | null;
  url: string;
  launchUrl?: string | null;
  iconUrl?: string | null;
  coverImageUrl?: string | null;
  previewVideoUrl?: string | null;
  ambientVideoUrl?: string | null;
  accentColor?: string | null;
  status: ServiceStatus;
  order: number;
  authentikApplicationSlug?: string | null;
  authentikGroupName?: string | null;
  isAvailableForAllNewClients?: boolean;
  createdAt: string;
  updatedAt: string;
}
```

### 8.6. Service media

```ts
export type MediaKind = "image" | "video" | "gif" | "gradient";

export interface ServiceMedia {
  kind: MediaKind;
  url?: string;
  posterUrl?: string;
  fallbackGradient?: string;
}
```

Поддерживаемые форматы:

- png;
- jpg;
- jpeg;
- webp;
- gif;
- mp4;
- webm.

### 8.7. ServiceGrant

```ts
export type ServiceGrantTargetType = "client" | "group" | "user";
export type ServiceAppRole = "viewer" | "member" | "admin" | "owner";
export type ServiceGrantStatus = "active" | "disabled";

export interface ServiceGrant {
  id: string;
  serviceId: string;
  targetType: ServiceGrantTargetType;
  targetId: string;
  appRole: ServiceAppRole;
  status: ServiceGrantStatus;
  createdAt: string;
  updatedAt: string;
}
```

### 8.8. ServiceAccessException

```ts
export type ServiceAccessExceptionType = "deny" | "allow";

export interface ServiceAccessException {
  id: string;
  serviceId: string;
  userId: string;
  type: ServiceAccessExceptionType;
  reason?: string | null;
  createdAt: string;
  updatedAt: string;
}
```

Приоритет:

1. `deny` exception перекрывает любой общий доступ;
2. `allow` exception может точечно выдать доступ пользователю, если это разрешено бизнес-логикой;
3. direct user grant сильнее group/client grant по роли, но не сильнее `deny`;
4. если сервис hidden, обычный пользователь его не видит;
5. если сервис maintenance, пользователь может видеть карточку, но не может открыть сервис;
6. если клиент suspended/expired, сервисы не открываются.

### 8.9. Invite

```ts
export type InviteStatus =
  | "created"
  | "sent"
  | "accepted"
  | "expired"
  | "revoked";

export interface Invite {
  id: string;
  clientId: string;
  email: string;
  role: ClientMembershipRole;
  invitedByUserId: string;
  token: string;
  expiresAt: string;
  status: InviteStatus;
  createdAt: string;
  updatedAt: string;
}
```

На MVP можно не отправлять реальные email. Достаточно генерировать mock invite-ссылку и показывать её в интерфейсе.

### 8.10. SyncStatus

```ts
export type SyncTarget = "authentik" | "task_manager" | "nodedc" | "service";
export type SyncState = "synced" | "pending" | "error" | "disabled";

export interface SyncStatus {
  id: string;
  target: SyncTarget;
  entityType: "user" | "client" | "service" | "grant";
  entityId: string;
  status: SyncState;
  lastSyncAt?: string | null;
  errorMessage?: string | null;
  retryAvailable: boolean;
}
```

---

## 9. Логика вычисления итогового доступа

### 9.1. Общий принцип

Пользователь видит сервис, если:

```txt
клиент активен
AND пользователь активен
AND сервис активен или находится на техработах
AND у пользователя есть доступ через клиента, группу или персональное правило
AND нет deny-исключения
```

### 9.2. Алгоритм

```ts
export interface EffectiveAccessResult {
  serviceId: string;
  userId: string;
  allowed: boolean;
  visible: boolean;
  openEnabled: boolean;
  appRole?: ServiceAppRole;
  reason: string;
  source?: "client" | "group" | "user" | "exception";
  sourceId?: string;
}
```

```ts
export function computeEffectiveAccess(input: {
  client: Client;
  user: LauncherUser;
  membership: ClientMembership;
  userGroups: ClientGroup[];
  service: Service;
  grants: ServiceGrant[];
  exceptions: ServiceAccessException[];
}): EffectiveAccessResult {
  // 1. Клиент заблокирован или истёк
  if (input.client.status === "suspended" || input.client.status === "expired") {
    return {
      serviceId: input.service.id,
      userId: input.user.id,
      allowed: false,
      visible: false,
      openEnabled: false,
      reason: "Клиент приостановлен или срок доступа истёк",
    };
  }

  // 2. Пользователь заблокирован
  if (input.user.globalStatus === "blocked" || input.membership.status === "disabled") {
    return {
      serviceId: input.service.id,
      userId: input.user.id,
      allowed: false,
      visible: false,
      openEnabled: false,
      reason: "Пользователь заблокирован или отключён внутри клиента",
    };
  }

  // 3. Сервис отключён
  if (input.service.status === "disabled") {
    return {
      serviceId: input.service.id,
      userId: input.user.id,
      allowed: false,
      visible: false,
      openEnabled: false,
      reason: "Сервис отключён",
    };
  }

  // 4. Сервис скрыт
  if (input.service.status === "hidden") {
    return {
      serviceId: input.service.id,
      userId: input.user.id,
      allowed: false,
      visible: false,
      openEnabled: false,
      reason: "Сервис скрыт",
    };
  }

  // 5. Deny exception
  const deny = input.exceptions.find(
    (item) => item.serviceId === input.service.id && item.userId === input.user.id && item.type === "deny"
  );

  if (deny) {
    return {
      serviceId: input.service.id,
      userId: input.user.id,
      allowed: false,
      visible: false,
      openEnabled: false,
      source: "exception",
      sourceId: deny.id,
      reason: "Доступ отключён индивидуальным исключением",
    };
  }

  // 6. Direct user grant
  const userGrant = input.grants.find(
    (grant) =>
      grant.serviceId === input.service.id &&
      grant.targetType === "user" &&
      grant.targetId === input.user.id &&
      grant.status === "active"
  );

  if (userGrant) {
    return {
      serviceId: input.service.id,
      userId: input.user.id,
      allowed: true,
      visible: true,
      openEnabled: input.service.status === "active",
      appRole: userGrant.appRole,
      source: "user",
      sourceId: userGrant.id,
      reason: "Доступ выдан пользователю напрямую",
    };
  }

  // 7. Group grant
  const groupIds = input.userGroups.map((group) => group.id);
  const groupGrant = input.grants.find(
    (grant) =>
      grant.serviceId === input.service.id &&
      grant.targetType === "group" &&
      groupIds.includes(grant.targetId) &&
      grant.status === "active"
  );

  if (groupGrant) {
    return {
      serviceId: input.service.id,
      userId: input.user.id,
      allowed: true,
      visible: true,
      openEnabled: input.service.status === "active",
      appRole: groupGrant.appRole,
      source: "group",
      sourceId: groupGrant.id,
      reason: "Доступ выдан группе пользователя",
    };
  }

  // 8. Client grant
  const clientGrant = input.grants.find(
    (grant) =>
      grant.serviceId === input.service.id &&
      grant.targetType === "client" &&
      grant.targetId === input.client.id &&
      grant.status === "active"
  );

  if (clientGrant) {
    return {
      serviceId: input.service.id,
      userId: input.user.id,
      allowed: true,
      visible: true,
      openEnabled: input.service.status === "active",
      appRole: clientGrant.appRole,
      source: "client",
      sourceId: clientGrant.id,
      reason: "Доступ выдан всему клиенту",
    };
  }

  // 9. No access
  return {
    serviceId: input.service.id,
    userId: input.user.id,
    allowed: false,
    visible: false,
    openEnabled: false,
    reason: "Доступ к сервису не выдан",
  };
}
```

---

## 10. Связка с Authentik

### 10.1. Принцип

Authentik должен использоваться как identity provider и SSO-слой.

Launcher Backend должен оставаться владельцем бизнес-логики:

- клиенты;
- клиентские группы;
- каталог сервисов;
- доступы;
- исключения;
- демо-периоды;
- статусы клиентов;
- описание сервисов;
- порядок отображения сервисов.

Authentik должен отвечать за:

- пользователей;
- аутентификацию;
- OIDC;
- SSO;
- application bindings;
- группы/claims, необходимые приложениям;
- передачу identity в подключённые сервисы.

### 10.2. Почему бизнес-логику нельзя полностью отдавать в Authentik

В Launcher есть бизнес-сущности, которые неудобно и неправильно хранить только в Authentik:

- клиент как юридическая/коммерческая сущность;
- демо-доступ;
- контактное лицо;
- заметки администратора;
- медиа-превью сервиса;
- порядок отображения сервисов;
- статус сервиса в витрине;
- индивидуальные исключения;
- explanation итогового доступа;
- синхронизация с Task Manager и NodeDC.

Поэтому Authentik должен быть нижним identity-слоем, а Launcher — продуктовым control-plane.

### 10.3. Ожидаемые Authentik-группы

Примерная схема групп:

```txt
authentik
├── nodedc-root-admins
├── nodedc-support-admins
├── clients
│   ├── client-romashka
│   │   ├── client-romashka-admins
│   │   ├── client-romashka-users
│   │   ├── service-task-manager-romashka
│   │   └── service-nodedc-romashka
│   └── client-roga-kopyta
│       ├── client-roga-kopyta-admins
│       ├── client-roga-kopyta-users
│       ├── service-task-manager-roga-kopyta
│       └── service-nodedc-roga-kopyta
```

Это не финальная обязательная структура, но frontend и backend должны быть готовы к тому, что пользователь будет иметь:

- Authentik user id;
- email;
- имя;
- аватар;
- список групп;
- список claims;
- список доступных приложений/entitlements.

### 10.4. Ожидаемые OIDC claims

```ts
export interface AuthentikClaimsMock {
  sub: string;
  email: string;
  name: string;
  preferred_username?: string;
  picture?: string;
  groups?: string[];
  entitlements?: string[];
}
```

На MVP эти claims должны имитироваться mock API.

### 10.5. Важное правило для приложений Authentik

Для production-интеграции важно не оставлять приложения без явных bindings, потому что приложение без bindings может быть доступно шире, чем требуется. Поэтому service access должен синхронизироваться с Authentik через явные группы/application bindings.

### 10.6. Статусы синхронизации

В Launcher UI должны быть предусмотрены статусы:

- синхронизировано;
- ожидает синхронизации;
- ошибка синхронизации;
- отключено.

Для ошибки должны быть действия:

- повторить;
- открыть детали ошибки.

---

## 11. Связка с Task Manager

### 11.1. Главное правило

Launcher управляет только доступом к приложению Task Manager.

Launcher **не управляет**:

- проектами внутри Task Manager;
- досками;
- задачами;
- проектными ролями;
- внутренним разделением на контуры;
- внутренней логикой Plane.

### 11.2. Пользовательский сценарий

```txt
1. Root Admin создаёт клиента ООО «Ромашка».
2. Root Admin подключает клиенту сервис Task Manager.
3. Client Owner приглашает пользователей.
4. Client Owner выдаёт нужным пользователям доступ к Task Manager.
5. Пользователь входит в Launcher.
6. Пользователь видит карточку Task Manager.
7. Пользователь нажимает «Открыть».
8. Пользователь попадает в Task Manager под тем же аккаунтом.
9. Всё дальнейшее управление проектами, досками и задачами происходит внутри Task Manager.
```

### 11.3. Возможная backend-интеграция в будущем

В будущем backend может автоматически:

- создавать workspace клиента в Task Manager;
- добавлять пользователей клиента в workspace;
- деактивировать пользователей;
- передавать роль admin/member;
- удалять доступ при отключении сервиса.

Но это не должно быть frontend-зависимостью MVP.

---

## 12. Связка с NodeDC Agent Platform

### 12.1. Главное правило

Launcher управляет только доступом к приложению NodeDC.

Launcher **не управляет**:

- workflow;
- agent nodes;
- sharing конкретных workflow;
- execution traces;
- runtime запуском;
- настройками нод;
- внутренними credential checks;
- агентной бизнес-логикой.

### 12.2. Пользовательский сценарий

```txt
1. Root Admin подключает клиенту сервис NodeDC.
2. Client Owner выдаёт пользователю доступ к NodeDC.
3. Пользователь видит NodeDC в Launcher.
4. Пользователь открывает NodeDC.
5. Внутри NodeDC он видит только те workflow/ресурсы, которые ему выданы уже механизмами NodeDC.
```

### 12.3. Возможная backend-интеграция в будущем

Backend может передавать в NodeDC:

- user id;
- client id;
- app role;
- группы пользователя;
- признак client admin;
- service entitlement.

---

## 13. UX-концепция пользовательского Launcher

### 13.1. Общая композиция

Пользовательский экран — это не таблица и не обычная админка, а визуальная витрина сервисов.

Композиция:

```txt
┌──────────────────────────────────────────────────────────┐
│ Logo / NODE.DC                 Client / Profile / Admin  │
│                                                          │
│                                                          │
│                 Service Stage / Ambient Media            │
│                                                          │
│                 ┌────────────────────────┐               │
│                 │ Glass Service Detail   │               │
│                 │ Название               │               │
│                 │ Описание               │               │
│                 │ Статус                 │               │
│                 │ [Открыть]              │               │
│                 └────────────────────────┘               │
│                                                          │
│                                                          │
│        [NodeDC] [Task Manager] [1C] [DM] [Tender]         │
└──────────────────────────────────────────────────────────┘
```

### 13.2. Основные зоны

1. **Top Bar**  
   Верхняя панель с логотипом, клиентом, профилем, кнопкой администрирования.

2. **Service Stage**  
   Центральная зона выбранного сервиса. Может показывать видео, картинку, gif или градиент.

3. **Service Detail Glass Card**  
   Стеклянная карточка с описанием выбранного сервиса.

4. **Service Rail**  
   Нижняя карусель доступных сервисов.

5. **Profile Menu**  
   Меню пользователя.

6. **Admin Overlay**  
   Панель администрирования, если у пользователя есть права.

### 13.3. Service Rail

Service Rail — нижняя карусель приложений.

Важно: это не проекты Plane и не контуры внутри Task Manager. Это верхнеуровневые приложения экосистемы.

Примеры карточек в rail:

- NodeDC;
- Task Manager;
- 1C;
- DM;
- Tender Agent;
- Digital Twin;
- Demo Gallery.

### 13.4. Карточка сервиса в rail

Карточка должна содержать:

- иконку или thumbnail;
- название;
- короткий subtitle;
- статус;
- состояние active/selected;
- disabled-состояние для техработ;
- soft glass hover.

### 13.5. Поведение выбора сервиса

При выборе сервиса:

1. карточка становится активной;
2. центральный фон меняется на media выбранного сервиса;
3. detail-card обновляет название и описание;
4. кнопка «Открыть» обновляет URL;
5. если сервис на техработах, кнопка disabled;
6. если у пользователя нет доступа, сервис не показывается;
7. root-admin может видеть скрытые/disabled сервисы с бейджами.

### 13.6. Кнопка открытия

Текст кнопки:

- `Открыть` — для активного сервиса;
- `На техработах` — для maintenance;
- `Нет доступа` — если карточка показывается администратору в диагностическом режиме;
- `Скрыт` — для hidden-сервиса в root-admin preview.

---

## 14. Админка Launcher

### 14.1. Формат

Админка должна быть реализована как overlay внутри одностраничного приложения.

Не нужно делать отдельную публичную страницу. Допустимо внутреннее состояние `mode = "admin"`, но приложение остаётся SPA.

Вариант UX:

```txt
Launcher View
    ↓ click «Администрирование»
Full-screen Admin Overlay
    ↓ close
Launcher View
```

### 14.2. Разделы root-admin

```txt
Администрирование NODE.DC
├── Обзор
├── Клиенты
├── Участники
├── Группы
├── Каталог сервисов
├── Доступы
├── Инвайты
├── Синхронизация
└── Аудит
```

### 14.3. Разделы client-admin

```txt
Администрирование компании
├── Обзор
├── Участники
├── Группы
├── Доступы к сервисам
├── Инвайты
└── Профиль компании
```

### 14.4. Раздел «Обзор»

Для root-admin:

- количество клиентов;
- количество активных клиентов;
- количество пользователей;
- количество сервисов;
- ошибки синхронизации;
- клиенты с истекающим demo;
- последние действия.

Для client-admin:

- количество пользователей в компании;
- доступные сервисы компании;
- активные приглашения;
- ошибки синхронизации по клиенту.

---

## 15. Раздел «Клиенты»

Доступен root-admin.

### 15.1. Список клиентов

Поля таблицы:

- название;
- тип;
- статус;
- количество участников;
- количество подключённых сервисов;
- срок demo;
- контакт;
- последнее обновление;
- действия.

### 15.2. Карточка клиента

Поля:

- название;
- юридическое название;
- тип клиента;
- статус;
- срок demo;
- контактное лицо;
- email контакта;
- заметки;
- участники;
- группы;
- подключённые сервисы;
- исключения;
- sync status.

### 15.3. Действия

- создать клиента;
- редактировать клиента;
- приостановить клиента;
- активировать клиента;
- продлить demo;
- открыть доступы клиента;
- открыть участников клиента;
- открыть sync details.

---

## 16. Раздел «Участники»

### 16.1. Для root-admin

Показывает всех пользователей всех клиентов.

Поля:

- имя;
- email;
- клиент/клиенты;
- статус;
- роль;
- доступные сервисы;
- дата создания;
- действия.

### 16.2. Для client-admin

Показывает только пользователей своего клиента.

Поля:

- имя;
- email;
- роль внутри клиента;
- группы;
- доступные сервисы;
- статус;
- действия.

### 16.3. Действия

- добавить участника;
- отправить invite;
- деактивировать;
- активировать;
- назначить client_admin;
- убрать роль client_admin;
- посмотреть доступы;
- открыть профиль участника.

---

## 17. Раздел «Группы»

Группы всегда создаются внутри клиента.

### 17.1. Поля группы

- название;
- описание;
- количество участников;
- подключённые сервисы;
- дата создания.

### 17.2. Действия

- создать группу;
- переименовать группу;
- удалить группу;
- добавить участников;
- удалить участников;
- подключить группе сервис;
- отключить группе сервис.

### 17.3. Пример

```txt
Клиент: ООО «Ромашка»

Группы:
- Руководство
- Менеджеры
- Бухгалтерия
- Демо-команда
```

---

## 18. Раздел «Каталог сервисов»

Доступен root-admin.

### 18.1. Список сервисов

Поля:

- название;
- slug;
- статус;
- URL;
- Authentik application slug;
- Authentik group name;
- порядок отображения;
- количество клиентов с доступом;
- действия.

### 18.2. Форма сервиса

Поля:

```txt
Название
Subtitle
Slug
Краткое описание
Полное описание
URL
Launch URL
Статус
Иконка
Thumbnail
Cover image
Preview video
Ambient video
Fallback gradient
Accent color
Порядок отображения
Authentik application slug
Authentik group name
Доступен всем новым клиентам
```

### 18.3. Статусы сервиса

| Статус | Поведение |
|---|---|
| active | Показывается пользователям с доступом, можно открыть. |
| maintenance | Показывается пользователям с доступом, открыть нельзя, кнопка disabled. |
| hidden | Обычным пользователям не показывается. Root-admin видит в режиме администрирования. |
| disabled | Сервис отключён полностью, не открывается. |

### 18.4. Медиа

Сервис может иметь несколько типов медиа:

- иконка для карточки;
- thumbnail для rail;
- cover image для central stage;
- preview video для карточки;
- ambient video для фоновой анимации;
- fallback gradient, если медиа нет.

Frontend должен иметь fallback-логику:

```txt
ambientVideoUrl
  ↓ если нет
previewVideoUrl
  ↓ если нет
coverImageUrl
  ↓ если нет
fallbackGradient
  ↓ если нет
системный абстрактный background
```

---

## 19. Раздел «Доступы»

### 19.1. Главная идея

Доступы должны отображаться не только как CRUD-список, а как понятная матрица.

Администратор должен видеть:

- кому выдан сервис;
- почему пользователь имеет доступ;
- откуда пришёл доступ: клиент, группа, пользователь;
- есть ли исключение;
- какая итоговая роль в приложении.

### 19.2. Матрица доступа

```txt
Клиент: ООО «Ромашка»

Участник       Task Manager       NodeDC          1C Assistant
-----------------------------------------------------------------
Иван           Admin / client     Admin / user    —
Вася           Member / client    —               —
Лена           Deny exception     Member / group  Member / group
Бухгалтерия    Member             —               Member
```

### 19.3. Explanation panel

При выборе ячейки показывать объяснение:

```txt
Вася / Task Manager
Есть доступ.
Причина: сервис подключён всему клиенту.
Роль: member.
Источник: client grant.
```

```txt
Лена / Task Manager
Нет доступа.
Причина: индивидуальное deny-исключение перекрывает доступ клиента.
```

### 19.4. Действия

- выдать сервис всему клиенту;
- выдать сервис группе;
- выдать сервис пользователю;
- убрать доступ;
- создать deny-исключение;
- удалить deny-исключение;
- изменить appRole;
- открыть sync details.

---

## 20. Раздел «Инвайты»

### 20.1. MVP flow

```txt
1. Client Admin открывает раздел «Инвайты».
2. Вводит email пользователя.
3. Выбирает роль: member / client_admin.
4. Создаёт invite.
5. Система генерирует одноразовую ссылку.
6. Админ копирует ссылку.
7. Пользователь переходит по ссылке.
8. В production Authentik создаёт/активирует пользователя.
9. Launcher связывает пользователя с клиентом.
```

### 20.2. На MVP

Можно не делать реальную регистрацию. Достаточно:

- создать invite в mock-store;
- показать ссылку;
- показать статус invite;
- дать revoke action;
- дать expire simulation.

---

## 21. Раздел «Синхронизация»

### 21.1. Цель

Показывать, что данные Launcher должны быть синхронизированы с внешними системами.

На MVP это mock-status, но UI должен быть production-like.

### 21.2. Цели синхронизации

- Authentik;
- Task Manager;
- NodeDC;
- сервисы будущих модулей.

### 21.3. UI

Поля:

- объект;
- тип объекта;
- целевая система;
- статус;
- последнее обновление;
- ошибка;
- действия.

Действия:

- повторить;
- открыть детали;
- скопировать ошибку.

---

## 22. Раздел «Аудит»

На MVP можно сделать mock/read-only.

Поля:

- дата;
- пользователь;
- действие;
- объект;
- клиент;
- результат;
- details.

Примеры событий:

- создан клиент;
- пользователь приглашён;
- сервис подключён клиенту;
- сервис отключён пользователю;
- создано deny-исключение;
- ошибка синхронизации;
- повторная синхронизация запущена.

---

## 23. Frontend API contracts

### 23.1. GET /api/me

```ts
export interface MeResponse {
  user: {
    id: string;
    authentikUserId?: string | null;
    name: string;
    email: string;
    avatarUrl?: string | null;
  };
  launcherRole: LauncherGlobalRole;
  memberships: Array<{
    clientId: string;
    clientName: string;
    role: ClientMembershipRole;
    status: ClientMembershipStatus;
  }>;
  activeClientId: string;
  permissions: {
    canOpenAdmin: boolean;
    canManageClients: boolean;
    canManageOwnClient: boolean;
    canManageServiceCatalog: boolean;
    canInviteUsers: boolean;
    canManageAccess: boolean;
    canViewSync: boolean;
  };
  mockAuthentikClaims?: AuthentikClaimsMock;
}
```

### 23.2. GET /api/launcher/services

Возвращает только сервисы, доступные пользователю в рамках активного клиента.

```ts
export interface LauncherServiceView {
  id: string;
  slug: string;
  title: string;
  subtitle?: string | null;
  description: string;
  status: ServiceStatus;
  userAccess: "allowed" | "denied";
  appRole?: ServiceAppRole;
  openUrl?: string | null;
  media: {
    icon?: string | null;
    thumbnail?: string | null;
    coverImage?: string | null;
    previewVideo?: string | null;
    ambientVideo?: string | null;
    fallbackGradient?: string | null;
  };
  effectiveAccess: EffectiveAccessResult;
}
```

### 23.3. GET /api/admin/clients

Root-admin only.

```ts
export interface AdminClientsResponse {
  clients: Array<Client & {
    membersCount: number;
    servicesCount: number;
    activeSyncErrorsCount: number;
  }>;
}
```

### 23.4. GET /api/admin/clients/:clientId

```ts
export interface AdminClientDetailsResponse {
  client: Client;
  memberships: Array<ClientMembership & { user: LauncherUser }>;
  groups: ClientGroup[];
  serviceGrants: ServiceGrant[];
  exceptions: ServiceAccessException[];
  syncStatuses: SyncStatus[];
}
```

### 23.5. GET /api/admin/services

```ts
export interface AdminServicesResponse {
  services: Service[];
}
```

### 23.6. GET /api/admin/access-matrix?clientId=...

```ts
export interface AccessMatrixResponse {
  client: Client;
  users: LauncherUser[];
  groups: ClientGroup[];
  services: Service[];
  cells: Array<{
    userId: string;
    serviceId: string;
    effectiveAccess: EffectiveAccessResult;
  }>;
}
```

### 23.7. POST /api/admin/service-grants

```ts
export interface CreateServiceGrantRequest {
  serviceId: string;
  targetType: ServiceGrantTargetType;
  targetId: string;
  appRole: ServiceAppRole;
}
```

### 23.8. POST /api/admin/service-exceptions

```ts
export interface CreateServiceExceptionRequest {
  serviceId: string;
  userId: string;
  type: ServiceAccessExceptionType;
  reason?: string;
}
```

### 23.9. POST /api/admin/invites

```ts
export interface CreateInviteRequest {
  clientId: string;
  email: string;
  role: ClientMembershipRole;
}
```

---

## 24. Frontend state model

### 24.1. Launcher state

```ts
export interface LauncherState {
  mode: "user" | "admin";
  activeClientId: string;
  selectedServiceId?: string;
  services: LauncherServiceView[];
  me?: MeResponse;
  isAdminOverlayOpen: boolean;
}
```

### 24.2. Admin state

```ts
export type AdminSection =
  | "overview"
  | "clients"
  | "users"
  | "groups"
  | "services"
  | "access"
  | "invites"
  | "sync"
  | "audit";

export interface AdminState {
  activeSection: AdminSection;
  selectedClientId?: string;
  selectedUserId?: string;
  selectedServiceId?: string;
  selectedAccessCell?: {
    userId: string;
    serviceId: string;
  };
}
```

### 24.3. Рекомендуемые библиотеки

- React;
- TypeScript;
- Tailwind CSS;
- Zustand для локального UI-state;
- TanStack Query для API-state;
- React Router опционально, но без обязательных страниц;
- Framer Motion для мягких переходов;
- Radix UI или Headless UI для accessibility-основы, если уже принято в проекте;
- собственные shared components для glass-canon.

---

## 25. UI component map

### 25.1. App shell

```txt
LauncherApp
├── LauncherProvider
├── AppBackground
├── TopBar
├── ServiceStage
├── ServiceDetailCard
├── ServiceRail
├── ProfileMenu
└── AdminOverlay
```

### 25.2. User components

```txt
TopBar
ClientSwitcher
ProfileAvatar
ProfileMenu
ServiceRail
ServiceTile
ServiceStatusBadge
ServiceStage
ServiceMediaRenderer
ServiceDetailGlassCard
LaunchServiceButton
EmptyServicesState
MaintenanceState
```

### 25.3. Admin components

```txt
AdminOverlay
AdminSidebar
AdminSectionHeader
AdminOverview
ClientsTable
ClientDetailsPanel
UsersTable
UserDetailsPanel
GroupsTable
GroupEditor
ServicesTable
ServiceEditor
AccessMatrix
AccessCell
AccessExplanationPanel
InviteList
InviteCreateModal
SyncStatusList
SyncErrorDetailsModal
AuditLogTable
```

### 25.4. Shared components

```txt
GlassSurface
GlassCard
GlassModal
GlassDrawer
AccentButton
SecondaryButton
DangerButton
RoundIconButton
Checker
Chip
StatusBadge
PortalDropdown
SearchField
SegmentedControl
MediaUploadField
MediaPreview
ConfirmDialog
```

---

## 26. Design system требования

### 26.1. Общий стиль

Launcher должен продолжать визуальный язык NODE.DC:

- тёмный интерфейс;
- matte glass;
- blur/backdrop-filter;
- мягкие стеклянные границы;
- крупные радиусы;
- без жёстких outline;
- без синих browser outline;
- акцентные круглые элементы;
- мягкие hover/focus surface;
- русскоязычные подписи;
- единые shared-компоненты.

### 26.2. Радиусы

```css
:root {
  --launcher-radius-modal: 1.75rem;
  --launcher-radius-card: 1.35rem;
  --launcher-radius-control: 1.25rem;
  --launcher-radius-circle: 999px;
}
```

### 26.3. Цвета

Использовать CSS variables, совместимые с текущим дизайн-кодом:

```css
:root {
  --nodedc-accent-rgb: 181 255 90;
  --nodedc-card-passive-rgb: 24 27 31;
  --nodedc-card-active-rgb: 181 255 90;
  --nodedc-on-accent-rgb: 11 17 23;
}
```

### 26.4. Glass surface

```css
.launcher-glass-surface {
  background: rgba(10, 12, 16, 0.62);
  backdrop-filter: blur(28px);
  -webkit-backdrop-filter: blur(28px);
  border: 1px solid rgba(255, 255, 255, 0.12);
  box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
  border-radius: var(--launcher-radius-card);
}
```

### 26.5. Primary CTA

```css
.launcher-primary-button {
  border-radius: var(--launcher-radius-control);
  background: rgb(var(--nodedc-accent-rgb));
  color: rgb(var(--nodedc-on-accent-rgb));
  border: none;
  outline: none;
}
```

Текст на светлом акценте должен быть тёмным.

### 26.6. Dropdown / popup

Все dropdown/popup, которые открываются внутри карточек, таблиц, scroll-контейнеров, sticky header или detail panel, должны рендериться через portal.

Inline popup внутри ограниченного контейнера считается дефектом.

### 26.7. Кнопки

- без жёсткого outline;
- без случайных синих browser рамок;
- primary CTA — акцентная заливка;
- secondary — тёмная glass-поверхность;
- danger — мягкая danger surface, без кислотно-красного свечения;
- icon actions — круглые.

### 26.8. Тексты

UI должен быть на русском.

Не оставлять смешанные подписи:

- Created at;
- Updated at;
- Label;
- State;
- Workspace;
- Project;
- Members.

Допускаются технические поля в dev/debug блоках, но production UI должен быть русифицирован.

---

## 27. Mock data для первого этапа

### 27.1. Клиенты

```ts
export const mockClients: Client[] = [
  {
    id: "client_romashka",
    type: "company",
    name: "ООО Ромашка",
    legalName: "ООО Ромашка",
    status: "active",
    demoEndsAt: null,
    contactName: "Иван Петров",
    contactEmail: "ivan@romashka.ru",
    notes: "Основной demo-клиент для проверки Task Manager и NodeDC.",
    createdAt: "2026-04-01T10:00:00Z",
    updatedAt: "2026-04-30T10:00:00Z",
  },
  {
    id: "client_roga_kopyta",
    type: "company",
    name: "ООО Рога и Копыта",
    legalName: "ООО Рога и Копыта",
    status: "demo",
    demoEndsAt: "2026-06-01T00:00:00Z",
    contactName: "Мария Иванова",
    contactEmail: "maria@example.ru",
    notes: "Клиент на демо-доступе.",
    createdAt: "2026-04-10T10:00:00Z",
    updatedAt: "2026-04-30T10:00:00Z",
  },
];
```

### 27.2. Сервисы

```ts
export const mockServices: Service[] = [
  {
    id: "service_nodedc",
    slug: "nodedc",
    title: "NodeDC",
    subtitle: "Агентная платформа",
    description: "Сборка, запуск и мониторинг агентных workflow.",
    fullDescription: "NodeDC используется для настройки агентных процессов, визуальной оркестрации, интеграций и runtime-мониторинга.",
    url: "https://dev.handhdc.ru",
    launchUrl: "https://dev.handhdc.ru/sso/launch",
    iconUrl: "/mock/services/nodedc/icon.svg",
    coverImageUrl: "/mock/services/nodedc/cover.webp",
    previewVideoUrl: "/mock/services/nodedc/preview.mp4",
    ambientVideoUrl: "/mock/services/nodedc/ambient.webm",
    accentColor: "#B5FF5A",
    status: "active",
    order: 10,
    authentikApplicationSlug: "nodedc",
    authentikGroupName: "service-nodedc",
    isAvailableForAllNewClients: false,
    createdAt: "2026-04-01T10:00:00Z",
    updatedAt: "2026-04-30T10:00:00Z",
  },
  {
    id: "service_task_manager",
    slug: "task-manager",
    title: "Task Manager",
    subtitle: "Операционный слой",
    description: "Задачи, контуры предприятия, рабочие процессы и AI-функции поверх задачника.",
    fullDescription: "Task Manager основан на архитектуре Plane и расширен AI-функциями NODE.DC.",
    url: "https://tasks.handhdc.ru",
    launchUrl: "https://tasks.handhdc.ru/sso/launch",
    iconUrl: "/mock/services/task-manager/icon.svg",
    coverImageUrl: "/mock/services/task-manager/cover.webp",
    previewVideoUrl: "/mock/services/task-manager/preview.mp4",
    ambientVideoUrl: "/mock/services/task-manager/ambient.webm",
    accentColor: "#D7C8FF",
    status: "active",
    order: 20,
    authentikApplicationSlug: "task-manager",
    authentikGroupName: "service-task-manager",
    isAvailableForAllNewClients: false,
    createdAt: "2026-04-01T10:00:00Z",
    updatedAt: "2026-04-30T10:00:00Z",
  },
  {
    id: "service_1c",
    slug: "1c-assistant",
    title: "1C Assistant",
    subtitle: "Бухгалтерский ассистент",
    description: "Вопросы к 1С, точные выборки, доказательная навигация по данным.",
    fullDescription: "Ассистент для бухгалтерских запросов, анализа операций, остатков, задолженностей и документов.",
    url: "https://1c.handhdc.ru",
    launchUrl: "https://1c.handhdc.ru/sso/launch",
    iconUrl: "/mock/services/1c/icon.svg",
    coverImageUrl: "/mock/services/1c/cover.webp",
    previewVideoUrl: null,
    ambientVideoUrl: null,
    accentColor: "#8FD7FF",
    status: "maintenance",
    order: 30,
    authentikApplicationSlug: "1c-assistant",
    authentikGroupName: "service-1c-assistant",
    isAvailableForAllNewClients: false,
    createdAt: "2026-04-01T10:00:00Z",
    updatedAt: "2026-04-30T10:00:00Z",
  },
];
```

---

## 28. Рекомендуемая структура проекта

```txt
src/
├── app/
│   ├── LauncherApp.tsx
│   ├── providers/
│   │   ├── QueryProvider.tsx
│   │   └── LauncherProvider.tsx
│   └── routes/
│       └── AppRouter.tsx
│
├── entities/
│   ├── client/
│   │   ├── types.ts
│   │   ├── mock.ts
│   │   └── api.ts
│   ├── user/
│   │   ├── types.ts
│   │   ├── mock.ts
│   │   └── api.ts
│   ├── service/
│   │   ├── types.ts
│   │   ├── mock.ts
│   │   └── api.ts
│   └── access/
│       ├── types.ts
│       ├── computeEffectiveAccess.ts
│       ├── mock.ts
│       └── api.ts
│
├── features/
│   ├── service-launch/
│   ├── client-switcher/
│   ├── profile-menu/
│   ├── admin-clients/
│   ├── admin-users/
│   ├── admin-groups/
│   ├── admin-services/
│   ├── admin-access-matrix/
│   ├── admin-invites/
│   └── admin-sync/
│
├── widgets/
│   ├── top-bar/
│   ├── service-stage/
│   ├── service-rail/
│   ├── service-detail-card/
│   └── admin-overlay/
│
├── shared/
│   ├── api/
│   │   ├── client.ts
│   │   └── mockAdapter.ts
│   ├── ui/
│   │   ├── GlassSurface.tsx
│   │   ├── GlassCard.tsx
│   │   ├── Button.tsx
│   │   ├── RoundIconButton.tsx
│   │   ├── PortalDropdown.tsx
│   │   ├── StatusBadge.tsx
│   │   └── MediaRenderer.tsx
│   ├── config/
│   │   └── designTokens.ts
│   └── lib/
│       ├── permissions.ts
│       └── cn.ts
│
└── styles/
    ├── globals.css
    ├── launcher-theme.css
    └── glass.css
```

---

## 29. Сценарии проверки MVP

### 29.1. Member видит только доступные сервисы

Дано:

- пользователь Вася;
- клиент ООО «Ромашка»;
- клиенту подключён Task Manager;
- NodeDC не подключён.

Ожидаемо:

- Вася видит Task Manager;
- Вася не видит NodeDC;
- кнопка «Открыть» ведёт в Task Manager.

### 29.2. Сервис на техработах

Дано:

- 1C Assistant подключён пользователю;
- статус сервиса `maintenance`.

Ожидаемо:

- карточка сервиса видна;
- есть бейдж «Техработы»;
- кнопка открытия disabled;
- detail-card объясняет, что сервис временно недоступен.

### 29.3. Deny exception

Дано:

- Task Manager подключён всему клиенту;
- пользователю Лена создано deny-исключение.

Ожидаемо:

- Лена не видит Task Manager;
- access matrix показывает deny exception;
- explanation panel объясняет, что индивидуальное исключение перекрыло доступ клиента.

### 29.4. Client Admin не видит других клиентов

Дано:

- пользователь Иван является client_admin ООО «Ромашка».

Ожидаемо:

- в админке он видит только ООО «Ромашка»;
- он не видит ООО «Рога и Копыта»;
- он не может создать глобальный сервис;
- он может приглашать пользователей в ООО «Ромашка».

### 29.5. Root Admin видит скрытый сервис

Дано:

- сервис DM имеет статус `hidden`.

Ожидаемо:

- обычный member не видит DM;
- root_admin видит DM в каталоге сервисов;
- root_admin видит бейдж `Скрыт`.

### 29.6. Смена активного клиента

Дано:

- пользователь состоит в двух клиентах.

Ожидаемо:

- в top bar доступен client switcher;
- при смене клиента обновляется service rail;
- selectedServiceId сбрасывается на первый доступный сервис нового клиента;
- админские права пересчитываются для выбранного клиента.

---

## 30. Acceptance Criteria

### 30.1. Архитектура

- Приложение создано как отдельный React frontend.
- Нет зависимости от реального Authentik на MVP.
- Есть mock API слой.
- Типы данных совместимы с будущим Authentik/OIDC.
- Доступы считаются через computeEffectiveAccess.
- Внутренние проекты Plane не упоминаются как сущности Launcher.
- Внутренние workflow NodeDC не управляются через Launcher.

### 30.2. Пользовательский UI

- Есть fullscreen user launcher view.
- Есть top bar.
- Есть service rail.
- Есть service stage.
- Есть glass detail card.
- Можно выбирать сервис.
- Можно открыть активный сервис.
- Maintenance-сервис нельзя открыть.
- Hidden-сервис не виден обычному пользователю.
- Member видит только доступные сервисы.

### 30.3. Админка

- Root admin может открыть admin overlay.
- Root admin видит клиентов, пользователей, сервисы и доступы.
- Client admin видит только свою компанию.
- Есть матрица доступов.
- Есть explanation итогового доступа.
- Есть CRUD mock для клиентов/сервисов/групп/доступов.
- Есть invite mock-flow.
- Есть sync-status mock-flow.

### 30.4. Design

- UI соответствует glass-canon.
- Нет жёстких browser outline.
- Dropdown рендерятся через portal.
- Кнопки имеют единый стиль.
- Primary CTA использует акцентную заливку и контрастный текст.
- UI русифицирован.
- Нет локальной стилизации «на глаз», используются shared-компоненты.

### 30.5. Code quality

- Все доменные типы вынесены в отдельные файлы.
- Mock data отделена от UI.
- API adapter отделён от components.
- Access logic покрыта unit-тестами.
- UI components не содержат бизнес-логику доступа.
- Состояния loading/error/empty предусмотрены.

---

## 31. Unit tests для access engine

Минимальный набор тестов:

```txt
computeEffectiveAccess
├── returns false when client is suspended
├── returns false when user is blocked
├── returns false when service is disabled
├── hides hidden service from normal user
├── returns false when no grant exists
├── returns true from client grant
├── returns true from group grant
├── returns true from user grant
├── deny exception overrides client grant
├── deny exception overrides group grant
├── deny exception overrides user grant
├── maintenance service is visible but openEnabled=false
└── returns correct explanation reason
```

---

## 32. Этапы разработки

### Этап 1. База приложения

- создать проект;
- подключить TypeScript;
- настроить Tailwind;
- создать app shell;
- создать mock API;
- создать доменные типы;
- создать design tokens.

### Этап 2. Пользовательский Launcher

- top bar;
- service rail;
- service stage;
- service detail card;
- profile menu;
- client switcher;
- service open flow;
- media renderer.

### Этап 3. Access engine

- типы grants/exceptions;
- computeEffectiveAccess;
- unit tests;
- интеграция с `/api/launcher/services`;
- фильтрация сервисов пользователя.

### Этап 4. Root Admin Overlay

- layout admin overlay;
- admin sidebar;
- overview;
- clients table;
- users table;
- services table;
- basic edit forms.

### Этап 5. Access Matrix

- access matrix;
- explanation panel;
- create grant;
- remove grant;
- create deny exception;
- remove exception.

### Этап 6. Client Admin

- ограничение видимости по клиенту;
- участники клиента;
- группы клиента;
- доступы только к разрешённым сервисам;
- invite mock-flow.

### Этап 7. Sync и audit mock

- sync status list;
- retry mock;
- error details;
- audit log mock.

### Этап 8. Подготовка к backend

- заменить mock adapter на fetch adapter;
- описать API DTO;
- добавить env config;
- добавить error boundary;
- добавить loading skeletons;
- подготовить Authentik claims adapter.

---

## 33. Что отправить backend-команде на согласование

Backend-команде нужно согласовать:

1. доменные сущности;
2. названия ролей;
3. схему клиента и членства;
4. схему service grants;
5. схему deny exceptions;
6. expected `/api/me`;
7. expected `/api/launcher/services`;
8. expected admin endpoints;
9. формат Authentik claims;
10. схему будущей синхронизации с Authentik;
11. правило, что бизнес-логика доступа живёт в Launcher Backend;
12. правило, что Authentik остаётся identity/SSO-слоем.

---

## 34. Что отправить Codex на разработку

Codex нужно передать:

1. этот документ;
2. текущий дизайн-код NODE.DC;
3. референс изображения Launcher;
4. список сервисов для mock;
5. требование не делать реальный Authentik;
6. требование сделать mock API-compatible architecture;
7. требование не смешивать проекты Plane с сервисами Launcher;
8. требование реализовать access engine;
9. требование использовать shared glass components;
10. требование подготовить приложение к backend-интеграции.

---

## 35. Короткое резюме для команды

Launcher — отдельный control-plane экосистемы NODE.DC.

Он показывает пользователю доступные приложения и даёт администраторам управлять клиентами, пользователями, сервисами и доступами.

На MVP не подключается настоящий Authentik, но структура данных сразу проектируется под OIDC/Authentik.

Главный принцип: Launcher управляет входом и доступом к сервисам, но не управляет внутренними сущностями подключённых приложений.

Task Manager сам управляет своими проектами и задачами. NodeDC сам управляет workflow и агентами. Launcher только определяет, кто имеет право открыть соответствующий сервис.

---

## 36. Открытые вопросы

Эти вопросы не блокируют MVP, но их нужно закрыть перед production-интеграцией:

1. Будет ли один пользователь иметь доступ к нескольким клиентам?
2. Нужно ли разделять Client Owner и Client Admin на MVP?
3. Нужно ли root-admin видеть пользовательскую витрину от лица выбранного клиента?
4. Какой финальный naming для доменов сервисов?
5. Какой exact формат Authentik group naming?
6. Кто является владельцем provisioning в Task Manager: Launcher Backend или отдельный интеграционный сервис?
7. Кто является владельцем provisioning в NodeDC?
8. Нужны ли платные тарифы/billing в Launcher или это внешний контур?
9. Нужно ли делать публичную self-registration в будущем?
10. Нужен ли approval flow для подключения сервисов клиенту?

---

## 37. Приложение: минимальный Codex prompt

```md
Нужно реализовать отдельное React-приложение NODE.DC Launcher.

Launcher — это SPA/control-plane для экосистемы NODE.DC. Он показывает пользователю доступные сервисы и даёт администраторам управлять клиентами, пользователями, группами, сервисами и доступами.

На текущем этапе не подключать настоящий Authentik. Сделать mock/light-backend слой, но типы и API-контракты заложить так, чтобы позже подключить Authentik/OIDC.

Главное: Launcher управляет доступом к приложениям, но не управляет внутренними сущностями приложений. Проекты внутри Task Manager/Plane не относятся к Launcher. Workflow внутри NodeDC не относятся к Launcher.

Реализовать:
- fullscreen user launcher view;
- top bar;
- service rail;
- service stage;
- glass service detail card;
- profile menu;
- admin overlay;
- root-admin sections: clients, users, groups, services, access, invites, sync, audit;
- client-admin ограничение по своему клиенту;
- service catalog;
- access matrix;
- computeEffectiveAccess;
- deny exceptions;
- mock data;
- mock API adapter;
- glass-canon shared components.

UI на русском. Дизайн — matte glass, blur, большие радиусы, без browser outline, portal dropdown, shared components.
```

