From aacdd422b30bbf2d2fd55f5a58a6cd19a6977344 Mon Sep 17 00:00:00 2001 From: Andrey Date: Wed, 17 Jun 2026 18:14:16 +0300 Subject: [PATCH] Initialize Edevs Hub repository --- .dockerignore | 12 + .editorconfig | 15 + .env.example | 22 + .gitignore | 13 + .pre-commit-config.yaml | 14 + README.md | 29 + apps/backend/.gitkeep | 0 apps/backend/Dockerfile | 15 + apps/backend/hub_backend/__init__.py | 1 + apps/backend/hub_backend/asgi.py | 7 + apps/backend/hub_backend/settings.py | 134 ++ apps/backend/hub_backend/urls.py | 9 + apps/backend/hub_backend/wsgi.py | 7 + apps/backend/hub_platform/__init__.py | 1 + apps/backend/hub_platform/events/__init__.py | 1 + apps/backend/hub_platform/events/apps.py | 7 + apps/backend/hub_platform/events/context.py | 14 + apps/backend/hub_platform/events/logging.py | 9 + .../events/management/__init__.py | 1 + .../events/management/commands/__init__.py | 1 + .../management/commands/emit_test_event.py | 18 + .../events/management/commands/run_worker.py | 31 + .../backend/hub_platform/events/middleware.py | 19 + .../events/migrations/0001_initial.py | 71 + .../events/migrations/__init__.py | 1 + apps/backend/hub_platform/events/models.py | 62 + apps/backend/hub_platform/events/services.py | 53 + apps/backend/hub_platform/health/__init__.py | 1 + apps/backend/hub_platform/health/apps.py | 7 + apps/backend/hub_platform/health/tests.py | 9 + apps/backend/hub_platform/health/urls.py | 8 + apps/backend/hub_platform/health/views.py | 21 + apps/backend/hub_platform/http/__init__.py | 1 + apps/backend/hub_platform/http/middleware.py | 23 + apps/backend/manage.py | 14 + apps/backend/requirements.txt | 9 + apps/checkout/.gitkeep | 0 apps/checkout/Dockerfile | 16 + apps/checkout/index.html | 12 + apps/checkout/package.json | 24 + apps/checkout/src/App.tsx | 16 + apps/checkout/src/main.tsx | 11 + apps/checkout/src/styles.css | 14 + apps/checkout/tsconfig.json | 4 + apps/checkout/vite.config.ts | 9 + apps/internal-ui/.gitkeep | 0 apps/internal-ui/Dockerfile | 16 + apps/internal-ui/index.html | 12 + apps/internal-ui/package.json | 25 + apps/internal-ui/src/App.tsx | 52 + apps/internal-ui/src/main.tsx | 11 + apps/internal-ui/src/styles.css | 52 + apps/internal-ui/tsconfig.json | 4 + apps/internal-ui/vite.config.ts | 9 + apps/web-chat/.gitkeep | 0 apps/web-chat/Dockerfile | 16 + apps/web-chat/index.html | 12 + apps/web-chat/package.json | 24 + apps/web-chat/src/App.tsx | 18 + apps/web-chat/src/main.tsx | 11 + apps/web-chat/src/styles.css | 22 + apps/web-chat/tsconfig.json | 4 + apps/web-chat/vite.config.ts | 9 + compose.yaml | 133 ++ deploy/.gitkeep | 0 deploy/nginx/local.conf | 48 + design/checkout/.gitkeep | 0 design/internal/baseline/.thumbnail | Bin 0 -> 1404 bytes design/internal/baseline/AI-агенты.dc.html | 168 ++ design/internal/baseline/AUTH · Вход.dc.html | 88 + design/internal/baseline/MANIFEST.sha256 | 35 + .../baseline/ProductAIRelease.dc.html | 286 ++++ .../baseline/screenshots/01-emp-security.png | Bin 0 -> 27014 bytes .../baseline/screenshots/01-ov-lower.png | Bin 0 -> 19590 bytes .../baseline/screenshots/02-emp-security.png | Bin 0 -> 27014 bytes .../baseline/screenshots/02-ov-lower.png | Bin 0 -> 22935 bytes .../internal/baseline/screenshots/cc-01.png | Bin 0 -> 29143 bytes .../internal/baseline/screenshots/cc-full.png | Bin 0 -> 15496 bytes .../baseline/screenshots/dialogs-waiting.png | Bin 0 -> 34203 bytes .../baseline/screenshots/overview.png | Bin 0 -> 3715 bytes design/internal/baseline/support.js | 1513 +++++++++++++++++ .../ADR-HUB-0013-Internal-UI-Design-System.md | 163 ++ .../uploads/SPEC-HUB-0004-Internal-Hub-UI.md | 1284 ++++++++++++++ design/internal/baseline/Аудит.dc.html | 230 +++ design/internal/baseline/Интеграции.dc.html | 153 ++ .../baseline/Использование AI.dc.html | 265 +++ .../baseline/Карточка AI-агента.dc.html | 291 ++++ .../baseline/Карточка интеграции.dc.html | 289 ++++ .../baseline/Карточка продукта.dc.html | 313 ++++ .../baseline/Карточка сотрудника.dc.html | 203 +++ .../internal/baseline/Командный центр.dc.html | 451 +++++ design/internal/baseline/Настройки.dc.html | 246 +++ design/internal/baseline/Отделы.dc.html | 141 ++ .../baseline/Продажи · Диалоги.dc.html | 523 ++++++ .../Продажи · Карточка заказа.dc.html | 282 +++ .../Продажи · Карточка клиента.dc.html | 276 +++ .../baseline/Продажи · Клиенты.dc.html | 334 ++++ .../internal/baseline/Продажи · Обзор.dc.html | 371 ++++ .../baseline/Продажи · Продажи.dc.html | 364 ++++ design/internal/baseline/Продукты.dc.html | 213 +++ design/internal/baseline/Профиль.dc.html | 153 ++ design/internal/baseline/Сотрудники.dc.html | 223 +++ design/internal/baseline/Тестовый чат.dc.html | 218 +++ design/internal/screens/.gitkeep | 0 design/web-chat/.gitkeep | 0 package.json | 30 + packages/contracts/.gitkeep | 0 packages/contracts/package.json | 11 + packages/contracts/src/index.ts | 13 + packages/contracts/tsconfig.json | 4 + packages/shared/.gitkeep | 0 packages/shared/package.json | 11 + packages/shared/src/index.ts | 5 + packages/shared/tsconfig.json | 4 + packages/ui/.gitkeep | 0 packages/ui/package.json | 16 + packages/ui/src/index.ts | 1 + packages/ui/src/theme.ts | 27 + packages/ui/tsconfig.json | 4 + playwright.config.ts | 25 + pyproject.toml | 22 + scripts/.gitkeep | 0 scripts/check.ps1 | 6 + scripts/start.ps1 | 7 + tests/.gitkeep | 0 tests/e2e/apps.spec.ts | 6 + tsconfig.base.json | 19 + 127 files changed, 10531 insertions(+) create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 README.md create mode 100644 apps/backend/.gitkeep create mode 100644 apps/backend/Dockerfile create mode 100644 apps/backend/hub_backend/__init__.py create mode 100644 apps/backend/hub_backend/asgi.py create mode 100644 apps/backend/hub_backend/settings.py create mode 100644 apps/backend/hub_backend/urls.py create mode 100644 apps/backend/hub_backend/wsgi.py create mode 100644 apps/backend/hub_platform/__init__.py create mode 100644 apps/backend/hub_platform/events/__init__.py create mode 100644 apps/backend/hub_platform/events/apps.py create mode 100644 apps/backend/hub_platform/events/context.py create mode 100644 apps/backend/hub_platform/events/logging.py create mode 100644 apps/backend/hub_platform/events/management/__init__.py create mode 100644 apps/backend/hub_platform/events/management/commands/__init__.py create mode 100644 apps/backend/hub_platform/events/management/commands/emit_test_event.py create mode 100644 apps/backend/hub_platform/events/management/commands/run_worker.py create mode 100644 apps/backend/hub_platform/events/middleware.py create mode 100644 apps/backend/hub_platform/events/migrations/0001_initial.py create mode 100644 apps/backend/hub_platform/events/migrations/__init__.py create mode 100644 apps/backend/hub_platform/events/models.py create mode 100644 apps/backend/hub_platform/events/services.py create mode 100644 apps/backend/hub_platform/health/__init__.py create mode 100644 apps/backend/hub_platform/health/apps.py create mode 100644 apps/backend/hub_platform/health/tests.py create mode 100644 apps/backend/hub_platform/health/urls.py create mode 100644 apps/backend/hub_platform/health/views.py create mode 100644 apps/backend/hub_platform/http/__init__.py create mode 100644 apps/backend/hub_platform/http/middleware.py create mode 100644 apps/backend/manage.py create mode 100644 apps/backend/requirements.txt create mode 100644 apps/checkout/.gitkeep create mode 100644 apps/checkout/Dockerfile create mode 100644 apps/checkout/index.html create mode 100644 apps/checkout/package.json create mode 100644 apps/checkout/src/App.tsx create mode 100644 apps/checkout/src/main.tsx create mode 100644 apps/checkout/src/styles.css create mode 100644 apps/checkout/tsconfig.json create mode 100644 apps/checkout/vite.config.ts create mode 100644 apps/internal-ui/.gitkeep create mode 100644 apps/internal-ui/Dockerfile create mode 100644 apps/internal-ui/index.html create mode 100644 apps/internal-ui/package.json create mode 100644 apps/internal-ui/src/App.tsx create mode 100644 apps/internal-ui/src/main.tsx create mode 100644 apps/internal-ui/src/styles.css create mode 100644 apps/internal-ui/tsconfig.json create mode 100644 apps/internal-ui/vite.config.ts create mode 100644 apps/web-chat/.gitkeep create mode 100644 apps/web-chat/Dockerfile create mode 100644 apps/web-chat/index.html create mode 100644 apps/web-chat/package.json create mode 100644 apps/web-chat/src/App.tsx create mode 100644 apps/web-chat/src/main.tsx create mode 100644 apps/web-chat/src/styles.css create mode 100644 apps/web-chat/tsconfig.json create mode 100644 apps/web-chat/vite.config.ts create mode 100644 compose.yaml create mode 100644 deploy/.gitkeep create mode 100644 deploy/nginx/local.conf create mode 100644 design/checkout/.gitkeep create mode 100644 design/internal/baseline/.thumbnail create mode 100644 design/internal/baseline/AI-агенты.dc.html create mode 100644 design/internal/baseline/AUTH · Вход.dc.html create mode 100644 design/internal/baseline/MANIFEST.sha256 create mode 100644 design/internal/baseline/ProductAIRelease.dc.html create mode 100644 design/internal/baseline/screenshots/01-emp-security.png create mode 100644 design/internal/baseline/screenshots/01-ov-lower.png create mode 100644 design/internal/baseline/screenshots/02-emp-security.png create mode 100644 design/internal/baseline/screenshots/02-ov-lower.png create mode 100644 design/internal/baseline/screenshots/cc-01.png create mode 100644 design/internal/baseline/screenshots/cc-full.png create mode 100644 design/internal/baseline/screenshots/dialogs-waiting.png create mode 100644 design/internal/baseline/screenshots/overview.png create mode 100644 design/internal/baseline/support.js create mode 100644 design/internal/baseline/uploads/ADR-HUB-0013-Internal-UI-Design-System.md create mode 100644 design/internal/baseline/uploads/SPEC-HUB-0004-Internal-Hub-UI.md create mode 100644 design/internal/baseline/Аудит.dc.html create mode 100644 design/internal/baseline/Интеграции.dc.html create mode 100644 design/internal/baseline/Использование AI.dc.html create mode 100644 design/internal/baseline/Карточка AI-агента.dc.html create mode 100644 design/internal/baseline/Карточка интеграции.dc.html create mode 100644 design/internal/baseline/Карточка продукта.dc.html create mode 100644 design/internal/baseline/Карточка сотрудника.dc.html create mode 100644 design/internal/baseline/Командный центр.dc.html create mode 100644 design/internal/baseline/Настройки.dc.html create mode 100644 design/internal/baseline/Отделы.dc.html create mode 100644 design/internal/baseline/Продажи · Диалоги.dc.html create mode 100644 design/internal/baseline/Продажи · Карточка заказа.dc.html create mode 100644 design/internal/baseline/Продажи · Карточка клиента.dc.html create mode 100644 design/internal/baseline/Продажи · Клиенты.dc.html create mode 100644 design/internal/baseline/Продажи · Обзор.dc.html create mode 100644 design/internal/baseline/Продажи · Продажи.dc.html create mode 100644 design/internal/baseline/Продукты.dc.html create mode 100644 design/internal/baseline/Профиль.dc.html create mode 100644 design/internal/baseline/Сотрудники.dc.html create mode 100644 design/internal/baseline/Тестовый чат.dc.html create mode 100644 design/internal/screens/.gitkeep create mode 100644 design/web-chat/.gitkeep create mode 100644 package.json create mode 100644 packages/contracts/.gitkeep create mode 100644 packages/contracts/package.json create mode 100644 packages/contracts/src/index.ts create mode 100644 packages/contracts/tsconfig.json create mode 100644 packages/shared/.gitkeep create mode 100644 packages/shared/package.json create mode 100644 packages/shared/src/index.ts create mode 100644 packages/shared/tsconfig.json create mode 100644 packages/ui/.gitkeep create mode 100644 packages/ui/package.json create mode 100644 packages/ui/src/index.ts create mode 100644 packages/ui/src/theme.ts create mode 100644 packages/ui/tsconfig.json create mode 100644 playwright.config.ts create mode 100644 pyproject.toml create mode 100644 scripts/.gitkeep create mode 100644 scripts/check.ps1 create mode 100644 scripts/start.ps1 create mode 100644 tests/.gitkeep create mode 100644 tests/e2e/apps.spec.ts create mode 100644 tsconfig.base.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3db21ea --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.env +.git +.venv +__pycache__ +*.pyc +node_modules +dist +coverage +playwright-report +test-results +design +docs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2927fb2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.py] +indent_size = 4 + +[*.ps1] +end_of_line = crlf diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a105fd2 --- /dev/null +++ b/.env.example @@ -0,0 +1,22 @@ +COMPOSE_PROJECT_NAME=edevs_hub + +HUB_ENV=local +HUB_DEBUG=true +HUB_SECRET_KEY=change-me-only-for-local-development +HUB_ALLOWED_HOSTS=localhost,127.0.0.1,hub.localhost,pay.localhost,chat.localhost +HUB_CSRF_TRUSTED_ORIGINS=http://localhost,http://localhost:8000,http://localhost:5173,http://localhost:5174,http://localhost:5175 + +POSTGRES_DB=edevs_hub +POSTGRES_USER=edevs_hub +POSTGRES_PASSWORD=edevs_hub +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 + +REDIS_URL=redis://redis:6379/0 + +INTERNAL_UI_PORT=5173 +CHECKOUT_PORT=5174 +WEB_CHAT_PORT=5175 +BACKEND_PORT=8010 + +VITE_API_BASE_URL=http://localhost:8010/api/v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c30797 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.env +.venv/ +__pycache__/ +*.py[cod] +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +node_modules/ +dist/ +coverage/ +playwright-report/ +test-results/ +.DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..63cfddd --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.0 + hooks: + - id: ruff + args: ["--fix"] + - id: ruff-format + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-yaml + - id: check-json + - id: end-of-file-fixer + - id: trailing-whitespace diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef2f643 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Edevs Hub + +Canonical implementation workspace for `hub.edevs.tech`. + +## Local start + +```powershell +Copy-Item .env.example .env +.\scripts\start.ps1 +``` + +The local compose stack contains: + +- Django ASGI backend and background worker; +- PostgreSQL; +- Redis; +- Internal Hub UI; +- Checkout UI; +- Web Chat UI; +- local Nginx reverse proxy. + +No production secrets are stored in the repository. + +Default local URLs: + +- Internal Hub UI: `http://localhost:5173` +- Checkout: `http://localhost:5174` +- Web Chat: `http://localhost:5175` +- Backend API: `http://localhost:8010/api/v1` diff --git a/apps/backend/.gitkeep b/apps/backend/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/backend/Dockerfile b/apps/backend/Dockerfile new file mode 100644 index 0000000..153894a --- /dev/null +++ b/apps/backend/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +COPY apps/backend/requirements.txt /app/apps/backend/requirements.txt +RUN pip install --no-cache-dir -r /app/apps/backend/requirements.txt + +COPY apps/backend /app/apps/backend + +WORKDIR /app/apps/backend + +EXPOSE 8000 diff --git a/apps/backend/hub_backend/__init__.py b/apps/backend/hub_backend/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/apps/backend/hub_backend/__init__.py @@ -0,0 +1 @@ + diff --git a/apps/backend/hub_backend/asgi.py b/apps/backend/hub_backend/asgi.py new file mode 100644 index 0000000..48ee236 --- /dev/null +++ b/apps/backend/hub_backend/asgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hub_backend.settings") + +application = get_asgi_application() diff --git a/apps/backend/hub_backend/settings.py b/apps/backend/hub_backend/settings.py new file mode 100644 index 0000000..eb049a4 --- /dev/null +++ b/apps/backend/hub_backend/settings.py @@ -0,0 +1,134 @@ +import os +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent + + +def env_bool(name: str, default: bool = False) -> bool: + value = os.environ.get(name) + if value is None: + return default + return value.lower() in {"1", "true", "yes", "on"} + + +def env_list(name: str, default: list[str]) -> list[str]: + value = os.environ.get(name) + if value is None: + return default + return [item.strip() for item in value.split(",") if item.strip()] + + +SECRET_KEY = os.environ.get("HUB_SECRET_KEY", "local-development-only") +DEBUG = env_bool("HUB_DEBUG") +ALLOWED_HOSTS = env_list("HUB_ALLOWED_HOSTS", ["localhost", "127.0.0.1"]) +CSRF_TRUSTED_ORIGINS = env_list("HUB_CSRF_TRUSTED_ORIGINS", []) + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "rest_framework", + "hub_platform.health", + "hub_platform.events", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "hub_platform.http.middleware.LocalCorsMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "hub_platform.events.middleware.CorrelationIdMiddleware", +] + +ROOT_URLCONF = "hub_backend.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "hub_backend.wsgi.application" +ASGI_APPLICATION = "hub_backend.asgi.application" + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": os.environ.get("POSTGRES_DB", "edevs_hub"), + "USER": os.environ.get("POSTGRES_USER", "edevs_hub"), + "PASSWORD": os.environ.get("POSTGRES_PASSWORD", "edevs_hub"), + "HOST": os.environ.get("POSTGRES_HOST", "postgres"), + "PORT": os.environ.get("POSTGRES_PORT", "5432"), + "CONN_MAX_AGE": 60, + } +} + +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.redis.RedisCache", + "LOCATION": os.environ.get("REDIS_URL", "redis://redis:6379/0"), + } +} + +AUTH_PASSWORD_VALIDATORS = [ + {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, + {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, + {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, + {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, +] + +LANGUAGE_CODE = "ru-ru" +TIME_ZONE = "Europe/Moscow" +USE_I18N = True +USE_TZ = True + +STATIC_URL = "static/" +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +REST_FRAMEWORK = { + "DEFAULT_RENDERER_CLASSES": ["rest_framework.renderers.JSONRenderer"], + "DEFAULT_PARSER_CLASSES": ["rest_framework.parsers.JSONParser"], +} + +CORS_ALLOWED_ORIGINS = { + "http://localhost:5173", + "http://localhost:5174", + "http://localhost:5175", +} + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "structured": { + "format": "%(asctime)s %(levelname)s %(name)s %(message)s correlation_id=%(correlation_id)s" + } + }, + "filters": { + "correlation_id": {"()": "hub_platform.events.logging.CorrelationIdLogFilter"} + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "structured", + "filters": ["correlation_id"], + } + }, + "root": {"handlers": ["console"], "level": "INFO"}, +} diff --git a/apps/backend/hub_backend/urls.py b/apps/backend/hub_backend/urls.py new file mode 100644 index 0000000..983c55d --- /dev/null +++ b/apps/backend/hub_backend/urls.py @@ -0,0 +1,9 @@ +from django.contrib import admin +from django.urls import include, path +from rest_framework.schemas import get_schema_view + +urlpatterns = [ + path("admin/", admin.site.urls), + path("api/v1/schema/", get_schema_view(title="Edevs Hub API", version="0.1.0"), name="openapi-schema"), + path("api/v1/health/", include("hub_platform.health.urls")), +] diff --git a/apps/backend/hub_backend/wsgi.py b/apps/backend/hub_backend/wsgi.py new file mode 100644 index 0000000..4000b8b --- /dev/null +++ b/apps/backend/hub_backend/wsgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hub_backend.settings") + +application = get_wsgi_application() diff --git a/apps/backend/hub_platform/__init__.py b/apps/backend/hub_platform/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/apps/backend/hub_platform/__init__.py @@ -0,0 +1 @@ + diff --git a/apps/backend/hub_platform/events/__init__.py b/apps/backend/hub_platform/events/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/apps/backend/hub_platform/events/__init__.py @@ -0,0 +1 @@ + diff --git a/apps/backend/hub_platform/events/apps.py b/apps/backend/hub_platform/events/apps.py new file mode 100644 index 0000000..46835f3 --- /dev/null +++ b/apps/backend/hub_platform/events/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class EventsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "hub_platform.events" + verbose_name = "Domain events" diff --git a/apps/backend/hub_platform/events/context.py b/apps/backend/hub_platform/events/context.py new file mode 100644 index 0000000..7aa5565 --- /dev/null +++ b/apps/backend/hub_platform/events/context.py @@ -0,0 +1,14 @@ +from contextvars import ContextVar +from uuid import uuid4 + +correlation_id_var: ContextVar[str] = ContextVar("correlation_id", default="") + + +def ensure_correlation_id(value: str | None = None) -> str: + correlation_id = value or str(uuid4()) + correlation_id_var.set(correlation_id) + return correlation_id + + +def get_correlation_id() -> str: + return correlation_id_var.get() or ensure_correlation_id() diff --git a/apps/backend/hub_platform/events/logging.py b/apps/backend/hub_platform/events/logging.py new file mode 100644 index 0000000..5b33549 --- /dev/null +++ b/apps/backend/hub_platform/events/logging.py @@ -0,0 +1,9 @@ +import logging + +from hub_platform.events.context import get_correlation_id + + +class CorrelationIdLogFilter(logging.Filter): + def filter(self, record: logging.LogRecord) -> bool: + record.correlation_id = get_correlation_id() + return True diff --git a/apps/backend/hub_platform/events/management/__init__.py b/apps/backend/hub_platform/events/management/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/apps/backend/hub_platform/events/management/__init__.py @@ -0,0 +1 @@ + diff --git a/apps/backend/hub_platform/events/management/commands/__init__.py b/apps/backend/hub_platform/events/management/commands/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/apps/backend/hub_platform/events/management/commands/__init__.py @@ -0,0 +1 @@ + diff --git a/apps/backend/hub_platform/events/management/commands/emit_test_event.py b/apps/backend/hub_platform/events/management/commands/emit_test_event.py new file mode 100644 index 0000000..549a972 --- /dev/null +++ b/apps/backend/hub_platform/events/management/commands/emit_test_event.py @@ -0,0 +1,18 @@ +from django.core.management.base import BaseCommand + +from hub_platform.events.services import DomainEvent, enqueue_event + + +class Command(BaseCommand): + help = "Enqueues a deterministic local test event for E01 checks." + + def handle(self, *args: object, **options: object) -> None: + event = enqueue_event( + DomainEvent( + aggregate_type="local_check", + aggregate_id="e01", + event_type="local_check.requested", + payload={"source": "emit_test_event"}, + ) + ) + self.stdout.write(str(event.id)) diff --git a/apps/backend/hub_platform/events/management/commands/run_worker.py b/apps/backend/hub_platform/events/management/commands/run_worker.py new file mode 100644 index 0000000..42857ee --- /dev/null +++ b/apps/backend/hub_platform/events/management/commands/run_worker.py @@ -0,0 +1,31 @@ +import logging +import time + +from django.core.management.base import BaseCommand +from django.utils import timezone + +from hub_platform.events.models import OutboxStatus +from hub_platform.events.services import claim_next_outbox_event, mark_retry + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = "Runs the local domain event worker." + + def handle(self, *args: object, **options: object) -> None: + self.stdout.write("Hub worker started") + while True: + event = claim_next_outbox_event() + if event is None: + time.sleep(1) + continue + + try: + logger.info("Processing outbox event %s", event.id) + event.status = OutboxStatus.PROCESSED + event.processed_at = timezone.now() + event.save(update_fields=["status", "processed_at"]) + except Exception as exc: # pragma: no cover + logger.exception("Outbox event failed: %s", event.id) + mark_retry(event, str(exc)) diff --git a/apps/backend/hub_platform/events/middleware.py b/apps/backend/hub_platform/events/middleware.py new file mode 100644 index 0000000..3873d78 --- /dev/null +++ b/apps/backend/hub_platform/events/middleware.py @@ -0,0 +1,19 @@ +from collections.abc import Callable + +from django.http import HttpRequest, HttpResponse + +from hub_platform.events.context import ensure_correlation_id + + +class CorrelationIdMiddleware: + header_name = "HTTP_X_CORRELATION_ID" + response_header = "X-Correlation-Id" + + def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None: + self.get_response = get_response + + def __call__(self, request: HttpRequest) -> HttpResponse: + correlation_id = ensure_correlation_id(request.META.get(self.header_name)) + response = self.get_response(request) + response[self.response_header] = correlation_id + return response diff --git a/apps/backend/hub_platform/events/migrations/0001_initial.py b/apps/backend/hub_platform/events/migrations/0001_initial.py new file mode 100644 index 0000000..733f128 --- /dev/null +++ b/apps/backend/hub_platform/events/migrations/0001_initial.py @@ -0,0 +1,71 @@ +import uuid + +from django.db import migrations, models +from django.utils import timezone + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="InboxEvent", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ("source", models.CharField(max_length=128)), + ("external_event_id", models.CharField(max_length=256)), + ("payload_hash", models.CharField(max_length=128)), + ("received_at", models.DateTimeField(auto_now_add=True)), + ("processed_at", models.DateTimeField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name="OutboxEvent", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ("aggregate_type", models.CharField(max_length=128)), + ("aggregate_id", models.CharField(max_length=128)), + ("event_type", models.CharField(max_length=128)), + ("payload", models.JSONField(default=dict)), + ( + "status", + models.CharField( + choices=[ + ("PENDING", "Pending"), + ("PROCESSING", "Processing"), + ("PROCESSED", "Processed"), + ("FAILED", "Failed"), + ("DEAD_LETTER", "Dead letter"), + ], + db_index=True, + default="PENDING", + max_length=32, + ), + ), + ("attempts", models.PositiveIntegerField(default=0)), + ("next_attempt_at", models.DateTimeField(db_index=True, default=timezone.now)), + ("correlation_id", models.CharField(blank=True, max_length=128)), + ("last_error", models.TextField(blank=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("processed_at", models.DateTimeField(blank=True, null=True)), + ], + options={"ordering": ["created_at"]}, + ), + migrations.AddConstraint( + model_name="inboxevent", + constraint=models.UniqueConstraint( + fields=("source", "external_event_id"), + name="uniq_inbox_source_external_event_id", + ), + ), + migrations.AddIndex( + model_name="outboxevent", + index=models.Index(fields=["status", "next_attempt_at"], name="events_outb_status_881ef5_idx"), + ), + migrations.AddIndex( + model_name="outboxevent", + index=models.Index(fields=["aggregate_type", "aggregate_id"], name="events_outb_aggrega_a2b510_idx"), + ), + ] diff --git a/apps/backend/hub_platform/events/migrations/__init__.py b/apps/backend/hub_platform/events/migrations/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/apps/backend/hub_platform/events/migrations/__init__.py @@ -0,0 +1 @@ + diff --git a/apps/backend/hub_platform/events/models.py b/apps/backend/hub_platform/events/models.py new file mode 100644 index 0000000..7c21180 --- /dev/null +++ b/apps/backend/hub_platform/events/models.py @@ -0,0 +1,62 @@ +import uuid + +from django.db import models +from django.utils import timezone + + +class OutboxStatus(models.TextChoices): + PENDING = "PENDING", "Pending" + PROCESSING = "PROCESSING", "Processing" + PROCESSED = "PROCESSED", "Processed" + FAILED = "FAILED", "Failed" + DEAD_LETTER = "DEAD_LETTER", "Dead letter" + + +class OutboxEvent(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + aggregate_type = models.CharField(max_length=128) + aggregate_id = models.CharField(max_length=128) + event_type = models.CharField(max_length=128) + payload = models.JSONField(default=dict) + status = models.CharField( + max_length=32, + choices=OutboxStatus.choices, + default=OutboxStatus.PENDING, + db_index=True, + ) + attempts = models.PositiveIntegerField(default=0) + next_attempt_at = models.DateTimeField(default=timezone.now, db_index=True) + correlation_id = models.CharField(max_length=128, blank=True) + last_error = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + processed_at = models.DateTimeField(null=True, blank=True) + + class Meta: + ordering = ["created_at"] + indexes = [ + models.Index(fields=["status", "next_attempt_at"]), + models.Index(fields=["aggregate_type", "aggregate_id"]), + ] + + def __str__(self) -> str: + return f"{self.event_type}:{self.id}" + + +class InboxEvent(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + source = models.CharField(max_length=128) + external_event_id = models.CharField(max_length=256) + payload_hash = models.CharField(max_length=128) + received_at = models.DateTimeField(auto_now_add=True) + processed_at = models.DateTimeField(null=True, blank=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["source", "external_event_id"], + name="uniq_inbox_source_external_event_id", + ) + ] + + def __str__(self) -> str: + return f"{self.source}:{self.external_event_id}" diff --git a/apps/backend/hub_platform/events/services.py b/apps/backend/hub_platform/events/services.py new file mode 100644 index 0000000..a93673b --- /dev/null +++ b/apps/backend/hub_platform/events/services.py @@ -0,0 +1,53 @@ +from dataclasses import dataclass +from datetime import timedelta +from typing import Any + +from django.db import transaction +from django.utils import timezone + +from hub_platform.events.context import get_correlation_id +from hub_platform.events.models import OutboxEvent, OutboxStatus + + +@dataclass(frozen=True) +class DomainEvent: + aggregate_type: str + aggregate_id: str + event_type: str + payload: dict[str, Any] + + +def enqueue_event(event: DomainEvent) -> OutboxEvent: + return OutboxEvent.objects.create( + aggregate_type=event.aggregate_type, + aggregate_id=event.aggregate_id, + event_type=event.event_type, + payload=event.payload, + correlation_id=get_correlation_id(), + ) + + +def mark_retry(event: OutboxEvent, error: str, max_attempts: int = 5) -> None: + event.attempts += 1 + event.last_error = error + event.status = OutboxStatus.DEAD_LETTER if event.attempts >= max_attempts else OutboxStatus.FAILED + event.next_attempt_at = timezone.now() + timedelta(seconds=min(300, 2**event.attempts)) + event.save(update_fields=["attempts", "last_error", "status", "next_attempt_at"]) + + +def claim_next_outbox_event() -> OutboxEvent | None: + with transaction.atomic(): + event = ( + OutboxEvent.objects.select_for_update(skip_locked=True) + .filter( + status__in=[OutboxStatus.PENDING, OutboxStatus.FAILED], + next_attempt_at__lte=timezone.now(), + ) + .order_by("next_attempt_at", "created_at") + .first() + ) + if event is None: + return None + event.status = OutboxStatus.PROCESSING + event.save(update_fields=["status"]) + return event diff --git a/apps/backend/hub_platform/health/__init__.py b/apps/backend/hub_platform/health/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/apps/backend/hub_platform/health/__init__.py @@ -0,0 +1 @@ + diff --git a/apps/backend/hub_platform/health/apps.py b/apps/backend/hub_platform/health/apps.py new file mode 100644 index 0000000..febd549 --- /dev/null +++ b/apps/backend/hub_platform/health/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class HealthConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "hub_platform.health" + verbose_name = "Health" diff --git a/apps/backend/hub_platform/health/tests.py b/apps/backend/hub_platform/health/tests.py new file mode 100644 index 0000000..77b548b --- /dev/null +++ b/apps/backend/hub_platform/health/tests.py @@ -0,0 +1,9 @@ +from django.test import Client, TestCase + + +class HealthTests(TestCase): + def test_live_endpoint(self) -> None: + response = Client().get("/api/v1/health/live/") + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["status"], "ok") diff --git a/apps/backend/hub_platform/health/urls.py b/apps/backend/hub_platform/health/urls.py new file mode 100644 index 0000000..fe23326 --- /dev/null +++ b/apps/backend/hub_platform/health/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from hub_platform.health.views import live, ready + +urlpatterns = [ + path("live/", live, name="health-live"), + path("ready/", ready, name="health-ready"), +] diff --git a/apps/backend/hub_platform/health/views.py b/apps/backend/hub_platform/health/views.py new file mode 100644 index 0000000..c919d8a --- /dev/null +++ b/apps/backend/hub_platform/health/views.py @@ -0,0 +1,21 @@ +from django.core.cache import cache +from django.db import connection +from django.http import JsonResponse + + +def live(request): + return JsonResponse({"status": "ok", "service": "hub-backend"}) + + +def ready(request): + checks = {"database": False, "redis": False} + + with connection.cursor() as cursor: + cursor.execute("SELECT 1") + checks["database"] = cursor.fetchone()[0] == 1 + + cache.set("healthcheck", "ok", timeout=5) + checks["redis"] = cache.get("healthcheck") == "ok" + + status_code = 200 if all(checks.values()) else 503 + return JsonResponse({"status": "ok" if status_code == 200 else "degraded", "checks": checks}, status=status_code) diff --git a/apps/backend/hub_platform/http/__init__.py b/apps/backend/hub_platform/http/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/apps/backend/hub_platform/http/__init__.py @@ -0,0 +1 @@ + diff --git a/apps/backend/hub_platform/http/middleware.py b/apps/backend/hub_platform/http/middleware.py new file mode 100644 index 0000000..f4cb99a --- /dev/null +++ b/apps/backend/hub_platform/http/middleware.py @@ -0,0 +1,23 @@ +from collections.abc import Callable + +from django.conf import settings +from django.http import HttpRequest, HttpResponse + + +class LocalCorsMiddleware: + def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None: + self.get_response = get_response + + def __call__(self, request: HttpRequest) -> HttpResponse: + if request.method == "OPTIONS" and request.headers.get("Origin") in settings.CORS_ALLOWED_ORIGINS: + response = HttpResponse(status=204) + else: + response = self.get_response(request) + origin = request.headers.get("Origin") + if origin in settings.CORS_ALLOWED_ORIGINS: + response["Access-Control-Allow-Origin"] = origin + response["Access-Control-Allow-Credentials"] = "true" + response["Access-Control-Allow-Headers"] = "Content-Type, X-Correlation-Id, X-CSRFToken" + response["Access-Control-Allow-Methods"] = "GET, POST, PUT, PATCH, DELETE, OPTIONS" + response["Vary"] = "Origin" + return response diff --git a/apps/backend/manage.py b/apps/backend/manage.py new file mode 100644 index 0000000..59e4376 --- /dev/null +++ b/apps/backend/manage.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +import os +import sys + + +def main() -> None: + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hub_backend.settings") + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/apps/backend/requirements.txt b/apps/backend/requirements.txt new file mode 100644 index 0000000..62444f3 --- /dev/null +++ b/apps/backend/requirements.txt @@ -0,0 +1,9 @@ +Django>=5.2,<5.3 +djangorestframework>=3.16,<3.17 +psycopg[binary]>=3.2,<3.3 +redis>=5.2,<5.3 +uvicorn[standard]>=0.34,<0.35 +gunicorn>=23.0,<24.0 +pytest>=8.4,<8.5 +pytest-django>=4.11,<4.12 +ruff>=0.12,<0.13 diff --git a/apps/checkout/.gitkeep b/apps/checkout/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/checkout/Dockerfile b/apps/checkout/Dockerfile new file mode 100644 index 0000000..63621cd --- /dev/null +++ b/apps/checkout/Dockerfile @@ -0,0 +1,16 @@ +FROM node:22-alpine + +WORKDIR /app + +COPY package.json tsconfig.base.json ./ +COPY apps/checkout/package.json ./apps/checkout/package.json +COPY packages/ui/package.json ./packages/ui/package.json +COPY packages/contracts/package.json ./packages/contracts/package.json +COPY packages/shared/package.json ./packages/shared/package.json +RUN npm install + +COPY apps/checkout ./apps/checkout +COPY packages ./packages + +WORKDIR /app/apps/checkout +EXPOSE 5174 diff --git a/apps/checkout/index.html b/apps/checkout/index.html new file mode 100644 index 0000000..97f24e1 --- /dev/null +++ b/apps/checkout/index.html @@ -0,0 +1,12 @@ + + + + + + Оформление покупки | Edevs Hub + + +
+ + + diff --git a/apps/checkout/package.json b/apps/checkout/package.json new file mode 100644 index 0000000..ef9c911 --- /dev/null +++ b/apps/checkout/package.json @@ -0,0 +1,24 @@ +{ + "name": "@edevs/checkout", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -p tsconfig.json && vite build", + "typecheck": "tsc -p tsconfig.json", + "test": "vitest run --passWithNoTests" + }, + "dependencies": { + "@edevs/contracts": "0.1.0", + "@edevs/shared": "0.1.0", + "@edevs/ui": "0.1.0", + "antd": "^5.26.0", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0" + } +} diff --git a/apps/checkout/src/App.tsx b/apps/checkout/src/App.tsx new file mode 100644 index 0000000..39ba476 --- /dev/null +++ b/apps/checkout/src/App.tsx @@ -0,0 +1,16 @@ +import { ConfigProvider, Result } from "antd"; +import { edevsHubTheme } from "@edevs/ui"; + +export function App() { + return ( + +
+ +
+
+ ); +} diff --git a/apps/checkout/src/main.tsx b/apps/checkout/src/main.tsx new file mode 100644 index 0000000..181f392 --- /dev/null +++ b/apps/checkout/src/main.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; + +import { App } from "./App"; +import "./styles.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/apps/checkout/src/styles.css b/apps/checkout/src/styles.css new file mode 100644 index 0000000..fea7f74 --- /dev/null +++ b/apps/checkout/src/styles.css @@ -0,0 +1,14 @@ +html, +body, +#root { + min-height: 100%; + margin: 0; +} + +.checkout-shell { + min-height: 100vh; + display: grid; + place-items: center; + padding: 24px; + background: #f6f8fb; +} diff --git a/apps/checkout/tsconfig.json b/apps/checkout/tsconfig.json new file mode 100644 index 0000000..3d61827 --- /dev/null +++ b/apps/checkout/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src", "vite.config.ts"] +} diff --git a/apps/checkout/vite.config.ts b/apps/checkout/vite.config.ts new file mode 100644 index 0000000..a930ad5 --- /dev/null +++ b/apps/checkout/vite.config.ts @@ -0,0 +1,9 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5174, + }, +}); diff --git a/apps/internal-ui/.gitkeep b/apps/internal-ui/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/internal-ui/Dockerfile b/apps/internal-ui/Dockerfile new file mode 100644 index 0000000..1754477 --- /dev/null +++ b/apps/internal-ui/Dockerfile @@ -0,0 +1,16 @@ +FROM node:22-alpine + +WORKDIR /app + +COPY package.json tsconfig.base.json ./ +COPY apps/internal-ui/package.json ./apps/internal-ui/package.json +COPY packages/ui/package.json ./packages/ui/package.json +COPY packages/contracts/package.json ./packages/contracts/package.json +COPY packages/shared/package.json ./packages/shared/package.json +RUN npm install + +COPY apps/internal-ui ./apps/internal-ui +COPY packages ./packages + +WORKDIR /app/apps/internal-ui +EXPOSE 5173 diff --git a/apps/internal-ui/index.html b/apps/internal-ui/index.html new file mode 100644 index 0000000..430fa9a --- /dev/null +++ b/apps/internal-ui/index.html @@ -0,0 +1,12 @@ + + + + + + Edevs Hub + + +
+ + + diff --git a/apps/internal-ui/package.json b/apps/internal-ui/package.json new file mode 100644 index 0000000..756a028 --- /dev/null +++ b/apps/internal-ui/package.json @@ -0,0 +1,25 @@ +{ + "name": "@edevs/internal-ui", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -p tsconfig.json && vite build", + "typecheck": "tsc -p tsconfig.json", + "test": "vitest run --passWithNoTests" + }, + "dependencies": { + "@ant-design/icons": "^6.0.0", + "@edevs/contracts": "0.1.0", + "@edevs/shared": "0.1.0", + "@edevs/ui": "0.1.0", + "antd": "^5.26.0", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0" + } +} diff --git a/apps/internal-ui/src/App.tsx b/apps/internal-ui/src/App.tsx new file mode 100644 index 0000000..3503f5b --- /dev/null +++ b/apps/internal-ui/src/App.tsx @@ -0,0 +1,52 @@ +import { ApartmentOutlined, RobotOutlined, ShoppingOutlined, TeamOutlined } from "@ant-design/icons"; +import { ConfigProvider, Layout, Menu, Tag, Typography } from "antd"; +import { edevsHubTheme } from "@edevs/ui"; + +const { Header, Sider, Content } = Layout; + +export function App() { + return ( + + + +
Edevs Hub
+ , label: "Командный центр" }, + { type: "group", label: "Компания" }, + { key: "departments", icon: , label: "Отделы" }, + { key: "products", icon: , label: "Продукты" }, + { type: "group", label: "Платформа" }, + { key: "ai", icon: , label: "AI" }, + ]} + /> + + +
+ Edevs / Командный центр + Локальный контур +
+ +
+ Командный центр + + Первый рабочий каркас внутреннего Hub. Реальные домены подключаются на следующих этапах. + +
+
+
+
+ Продажи + NORMAL +
+

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

+
+
+
+
+ + + ); +} diff --git a/apps/internal-ui/src/main.tsx b/apps/internal-ui/src/main.tsx new file mode 100644 index 0000000..181f392 --- /dev/null +++ b/apps/internal-ui/src/main.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; + +import { App } from "./App"; +import "./styles.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/apps/internal-ui/src/styles.css b/apps/internal-ui/src/styles.css new file mode 100644 index 0000000..2e9717e --- /dev/null +++ b/apps/internal-ui/src/styles.css @@ -0,0 +1,52 @@ +html, +body, +#root { + min-width: 1024px; + min-height: 100%; + margin: 0; +} + +.hub-shell { + min-height: 100vh; +} + +.hub-sidebar { + border-right: 1px solid #e5e7eb; +} + +.hub-logo { + height: 56px; + padding: 18px 24px; + font-weight: 700; +} + +.hub-header { + display: flex; + align-items: center; + justify-content: space-between; + height: 56px; + padding: 0 24px; + border-bottom: 1px solid #e5e7eb; +} + +.hub-content { + padding: 24px; +} + +.page-header { + margin-bottom: 24px; +} + +.department-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); + gap: 16px; +} + +.department-card { + min-height: 180px; + padding: 20px; + background: #ffffff; + border: 1px solid #e5e7eb; + border-radius: 8px; +} diff --git a/apps/internal-ui/tsconfig.json b/apps/internal-ui/tsconfig.json new file mode 100644 index 0000000..3d61827 --- /dev/null +++ b/apps/internal-ui/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src", "vite.config.ts"] +} diff --git a/apps/internal-ui/vite.config.ts b/apps/internal-ui/vite.config.ts new file mode 100644 index 0000000..c9d4090 --- /dev/null +++ b/apps/internal-ui/vite.config.ts @@ -0,0 +1,9 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + }, +}); diff --git a/apps/web-chat/.gitkeep b/apps/web-chat/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/web-chat/Dockerfile b/apps/web-chat/Dockerfile new file mode 100644 index 0000000..3d7772f --- /dev/null +++ b/apps/web-chat/Dockerfile @@ -0,0 +1,16 @@ +FROM node:22-alpine + +WORKDIR /app + +COPY package.json tsconfig.base.json ./ +COPY apps/web-chat/package.json ./apps/web-chat/package.json +COPY packages/ui/package.json ./packages/ui/package.json +COPY packages/contracts/package.json ./packages/contracts/package.json +COPY packages/shared/package.json ./packages/shared/package.json +RUN npm install + +COPY apps/web-chat ./apps/web-chat +COPY packages ./packages + +WORKDIR /app/apps/web-chat +EXPOSE 5175 diff --git a/apps/web-chat/index.html b/apps/web-chat/index.html new file mode 100644 index 0000000..c313969 --- /dev/null +++ b/apps/web-chat/index.html @@ -0,0 +1,12 @@ + + + + + + Web Chat | Edevs Hub + + +
+ + + diff --git a/apps/web-chat/package.json b/apps/web-chat/package.json new file mode 100644 index 0000000..d56a89e --- /dev/null +++ b/apps/web-chat/package.json @@ -0,0 +1,24 @@ +{ + "name": "@edevs/web-chat", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -p tsconfig.json && vite build", + "typecheck": "tsc -p tsconfig.json", + "test": "vitest run --passWithNoTests" + }, + "dependencies": { + "@edevs/contracts": "0.1.0", + "@edevs/shared": "0.1.0", + "@edevs/ui": "0.1.0", + "antd": "^5.26.0", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0" + } +} diff --git a/apps/web-chat/src/App.tsx b/apps/web-chat/src/App.tsx new file mode 100644 index 0000000..e1ba812 --- /dev/null +++ b/apps/web-chat/src/App.tsx @@ -0,0 +1,18 @@ +import { ConfigProvider, FloatButton, Typography } from "antd"; +import { edevsHubTheme } from "@edevs/ui"; + +export function App() { + return ( + +
+
+ Web Chat + + Каркас публичного виджета готов. Детальная реализация UI ждёт прохождения DG-04. + +
+ +
+
+ ); +} diff --git a/apps/web-chat/src/main.tsx b/apps/web-chat/src/main.tsx new file mode 100644 index 0000000..181f392 --- /dev/null +++ b/apps/web-chat/src/main.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; + +import { App } from "./App"; +import "./styles.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/apps/web-chat/src/styles.css b/apps/web-chat/src/styles.css new file mode 100644 index 0000000..9444a86 --- /dev/null +++ b/apps/web-chat/src/styles.css @@ -0,0 +1,22 @@ +html, +body, +#root { + min-height: 100%; + margin: 0; +} + +.web-chat-demo { + min-height: 100vh; + padding: 24px; + background: #f6f8fb; +} + +.chat-panel { + width: min(420px, calc(100vw - 32px)); + margin-left: auto; + padding: 20px; + background: #ffffff; + border: 1px solid #e5e7eb; + border-radius: 8px; + box-shadow: 0 10px 30px rgb(15 23 42 / 10%); +} diff --git a/apps/web-chat/tsconfig.json b/apps/web-chat/tsconfig.json new file mode 100644 index 0000000..3d61827 --- /dev/null +++ b/apps/web-chat/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src", "vite.config.ts"] +} diff --git a/apps/web-chat/vite.config.ts b/apps/web-chat/vite.config.ts new file mode 100644 index 0000000..df7f6c9 --- /dev/null +++ b/apps/web-chat/vite.config.ts @@ -0,0 +1,9 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5175, + }, +}); diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..72a5b4a --- /dev/null +++ b/compose.yaml @@ -0,0 +1,133 @@ +x-hub-env: &hub-env + - path: .env.example + - path: .env + required: false + +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB:-edevs_hub} + POSTGRES_USER: ${POSTGRES_USER:-edevs_hub} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-edevs_hub} + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 10 + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 10 + + backend: + build: + context: . + dockerfile: apps/backend/Dockerfile + env_file: *hub-env + command: > + sh -c "python manage.py migrate && + uvicorn hub_backend.asgi:application --host 0.0.0.0 --port 8000" + ports: + - "${BACKEND_PORT:-8010}:8000" + volumes: + - ./apps/backend:/app/apps/backend + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: + [ + "CMD", + "python", + "-c", + "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/v1/health/live/', timeout=3)", + ] + interval: 10s + timeout: 5s + retries: 10 + + worker: + build: + context: . + dockerfile: apps/backend/Dockerfile + env_file: *hub-env + command: python manage.py run_worker + volumes: + - ./apps/backend:/app/apps/backend + depends_on: + backend: + condition: service_healthy + + internal-ui: + build: + context: . + dockerfile: apps/internal-ui/Dockerfile + env_file: *hub-env + command: npm run dev -- --host 0.0.0.0 --port 5173 + ports: + - "5173:5173" + volumes: + - ./apps/internal-ui:/app/apps/internal-ui + - ./packages:/app/packages + - /app/node_modules + - /app/apps/internal-ui/node_modules + + checkout: + build: + context: . + dockerfile: apps/checkout/Dockerfile + env_file: *hub-env + command: npm run dev -- --host 0.0.0.0 --port 5174 + ports: + - "5174:5174" + volumes: + - ./apps/checkout:/app/apps/checkout + - ./packages:/app/packages + - /app/node_modules + - /app/apps/checkout/node_modules + + web-chat: + build: + context: . + dockerfile: apps/web-chat/Dockerfile + env_file: *hub-env + command: npm run dev -- --host 0.0.0.0 --port 5175 + ports: + - "5175:5175" + volumes: + - ./apps/web-chat:/app/apps/web-chat + - ./packages:/app/packages + - /app/node_modules + - /app/apps/web-chat/node_modules + + proxy: + image: nginx:1.27-alpine + ports: + - "80:80" + volumes: + - ./deploy/nginx/local.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + backend: + condition: service_healthy + internal-ui: + condition: service_started + checkout: + condition: service_started + web-chat: + condition: service_started + +volumes: + postgres_data: diff --git a/deploy/.gitkeep b/deploy/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/deploy/nginx/local.conf b/deploy/nginx/local.conf new file mode 100644 index 0000000..cc621d0 --- /dev/null +++ b/deploy/nginx/local.conf @@ -0,0 +1,48 @@ +server { + listen 80; + server_name hub.localhost localhost; + + location /api/ { + proxy_pass http://backend:8000; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://internal-ui:5173; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +server { + listen 80; + server_name pay.localhost; + + location /api/ { + proxy_pass http://backend:8000; + proxy_set_header Host $host; + } + + location / { + proxy_pass http://checkout:5174; + proxy_set_header Host $host; + } +} + +server { + listen 80; + server_name chat.localhost; + + location /api/ { + proxy_pass http://backend:8000; + proxy_set_header Host $host; + } + + location / { + proxy_pass http://web-chat:5175; + proxy_set_header Host $host; + } +} diff --git a/design/checkout/.gitkeep b/design/checkout/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/design/internal/baseline/.thumbnail b/design/internal/baseline/.thumbnail new file mode 100644 index 0000000000000000000000000000000000000000..1434a0b88a0db1da993662dd362dbe070aa7453f GIT binary patch literal 1404 zcmWIYbaN|VWnc(*bqWXzu!!JdU|=u+VtYo0wG0fN&dvcR7=c^{#uFft1t^u9S5g$@ z?xYYA8KuDffPoPR!5}fYxB$Y!N&;1Vy9QBvSr@Ac{DPb*sl~}aB_DuTr3h#_&`lyh zwn#dJtpQ>uLD)7Rc6mub2~bS{h+PyCD76yiG=MiFvFeykZN(39n0wkHz(ij*%EoWfhjbvaDzQDl16%CdH5;)wZ0F;se zx+@$$8Fh~n6}pR z@)KK*i^ue`F2~wAU+9%ddbZ}}rzsAROCTa=A1F`Xq5j`l{`;}kTc1y&eTn@ni=$^FAMZWrF#BSD z(}Y-FCI*rAEI-?$3yZ^MJvor#w55ml@H^veD-}OpQeS^KL+Sv?f@NR->ZJ9o7eF`( z;mS)t828@1vUWtADm@J_&CBZR^>#_5$ycCmBaX=$eL#63DS zr&UYLa?Wf1Bbt`x>nHGKEO>BMcj@l#XDP>5pE}3nTV{UwU)!#0qQ`B^`}c`|=|7Up zvhtU>Tb51Pl!w-(o|<}97!`DvfxPKW&m ztJHZm+_gDfs8M#NqE9Dn91wTf3+k%q0y#k_q5JMImb6OnXfZiTKajl Y{qqRs-Kq+j%e*)m#B19?r7Qyj0M)$$;s5{u literal 0 HcmV?d00001 diff --git a/design/internal/baseline/AI-агенты.dc.html b/design/internal/baseline/AI-агенты.dc.html new file mode 100644 index 0000000..4302543 --- /dev/null +++ b/design/internal/baseline/AI-агенты.dc.html @@ -0,0 +1,168 @@ + + + + + + + + + + + + + +
+ + + + + +
+
+
Edevs/AI/AI-агенты
+
+ +
+
ИП
+
+
+ + + + +
+
+ + +
+
+ + +
+
+

AI-агенты

+

Sales-агенты продуктов · один агент на продукт

+
+
+ + +
+
Агентов активно
2 / 2
+
Диалогов на AI сейчас
35
+
Конверсия AI · сегодня
13,2%
+
Стоимость AI · сегодня
₽1 240
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
АГЕНТПРОДУКТМОДЕЛЬАКТИВНЫЙ RELEASEСТАТУСДИАЛОГИ СЕЙЧАСКОНВЕРСИЯ
+
+
+
{{ r.name }}
{{ r.id }}
+
+
{{ r.product }}{{ r.model }}{{ r.release }}Published{{ r.status }}{{ r.dialogs }}{{ r.conv }} +
+ Test chat + +
+ + + +
+
+
+
2 агента
+
В первой итерации — один sales-agent на продукт
+
+
+ +
+
+
+
+
+ + + diff --git a/design/internal/baseline/AUTH · Вход.dc.html b/design/internal/baseline/AUTH · Вход.dc.html new file mode 100644 index 0000000..c4406f8 --- /dev/null +++ b/design/internal/baseline/AUTH · Вход.dc.html @@ -0,0 +1,88 @@ + + + + + + + + + + + + + +
+
+ + +
+
+

Edevs Hub

+

Вход во внутренний кабинет

+
+ + +
+ + +
+ + Неверный email или пароль. Проверьте данные и попробуйте снова. +
+
+ + +
+ + +
+ + +
+ + + +
+ + +
+ +

Доступ только для сотрудников Edevs · защищённое соединение

+
+
+
+ + + diff --git a/design/internal/baseline/MANIFEST.sha256 b/design/internal/baseline/MANIFEST.sha256 new file mode 100644 index 0000000..a354b8b --- /dev/null +++ b/design/internal/baseline/MANIFEST.sha256 @@ -0,0 +1,35 @@ +d549527b8f9c7a42c08719c881d98733cda85d708f67b4f149cb9fa87bd302ae #U0410#U0443#U0434#U0438#U0442.dc.html +65f1b00ce2b529640284ad25c8e4d59a4e517ef5c1a9f086bac4e439193e131a #U0418#U043d#U0442#U0435#U0433#U0440#U0430#U0446#U0438#U0438.dc.html +01a7727f0b15e3b1e8bd866c624a8a31bb10a2da5e857b4a3b84c53dc55dc888 #U0418#U0441#U043f#U043e#U043b#U044c#U0437#U043e#U0432#U0430#U043d#U0438#U0435 AI.dc.html +9c777f9d4a1a01921f6d3a22b727f3b59e29e6be25a409cd712e9ae048ed360a #U041a#U0430#U0440#U0442#U043e#U0447#U043a#U0430 #U0438#U043d#U0442#U0435#U0433#U0440#U0430#U0446#U0438#U0438.dc.html +5d3d52d5b20c6e0292b697f8fadf3f24f6439dd199895e1452500ba9be73c199 #U041a#U0430#U0440#U0442#U043e#U0447#U043a#U0430 #U043f#U0440#U043e#U0434#U0443#U043a#U0442#U0430.dc.html +7057493435e132ecea34abcf71551ba89449e7ee74df81f617f322d9f3e629a3 #U041a#U0430#U0440#U0442#U043e#U0447#U043a#U0430 #U0441#U043e#U0442#U0440#U0443#U0434#U043d#U0438#U043a#U0430.dc.html +b815c76e76d52db851e0314bfd764c08915aa1670b273eda60f283e4b9d192ad #U041a#U0430#U0440#U0442#U043e#U0447#U043a#U0430 AI-#U0430#U0433#U0435#U043d#U0442#U0430.dc.html +653f41c7ac5a9fcee37f843f4453ff178f744ae43fa0d534988f6a3d882afb3e #U041a#U043e#U043c#U0430#U043d#U0434#U043d#U044b#U0439 #U0446#U0435#U043d#U0442#U0440.dc.html +c6d096b5c645e24d104684373f026239f5c1b7e04d6abd1c8f11c38a9dde3dc6 #U041d#U0430#U0441#U0442#U0440#U043e#U0439#U043a#U0438.dc.html +a003db70efc372ded0ff600000b33d78cd0d59e0f0351cd5f1ce68291afcc711 #U041e#U0442#U0434#U0435#U043b#U044b.dc.html +3a68bfc31de264c1d54466f69f8d1ac38eede69432b1e7dfe55191937c34b355 #U041f#U0440#U043e#U0434#U0430#U0436#U0438 #U00b7 #U0414#U0438#U0430#U043b#U043e#U0433#U0438.dc.html +26ce567d808363a7858fd248d22fa9c0e3548ab1f71783704ee419fd26051155 #U041f#U0440#U043e#U0434#U0430#U0436#U0438 #U00b7 #U041a#U0430#U0440#U0442#U043e#U0447#U043a#U0430 #U0437#U0430#U043a#U0430#U0437#U0430.dc.html +aae56acbc110e3bda8fbde2d3b9f817915e00837a524517c6721b0199d359560 #U041f#U0440#U043e#U0434#U0430#U0436#U0438 #U00b7 #U041a#U0430#U0440#U0442#U043e#U0447#U043a#U0430 #U043a#U043b#U0438#U0435#U043d#U0442#U0430.dc.html +abbbbc373be1ca2386fb1eda7790c77715e643ce06234258dde03a56145fee41 #U041f#U0440#U043e#U0434#U0430#U0436#U0438 #U00b7 #U041a#U043b#U0438#U0435#U043d#U0442#U044b.dc.html +e61ff1cdb6c4ab80463e5afd7fec41bdc5d407f17fbe64ed08aa49d830be349f #U041f#U0440#U043e#U0434#U0430#U0436#U0438 #U00b7 #U041e#U0431#U0437#U043e#U0440.dc.html +86ec3c171f78d2abd7625e3a039dda1e759566641a2329f113ee0be851c2df81 #U041f#U0440#U043e#U0434#U0430#U0436#U0438 #U00b7 #U041f#U0440#U043e#U0434#U0430#U0436#U0438.dc.html +b247932639fa3441bfaff801e733d26d6293cc93e40d67e52c3e7fa9d364c6a6 #U041f#U0440#U043e#U0434#U0443#U043a#U0442#U044b.dc.html +869ad944fdac1acbefd6e017fe59f20c43ac784deda063efd118271b72fece58 #U041f#U0440#U043e#U0444#U0438#U043b#U044c.dc.html +64c50854f460c471e52ab18af50c632a9dbbd4043442b252d8b3b13927141915 #U0421#U043e#U0442#U0440#U0443#U0434#U043d#U0438#U043a#U0438.dc.html +973deac2d4dc992a130e38cd1a792174ccfe0aedc2dc6e0a1628e747ff426d68 #U0422#U0435#U0441#U0442#U043e#U0432#U044b#U0439 #U0447#U0430#U0442.dc.html +94d0f43a0a80086e40ad9be57ca3e1f9460bbc1ac4655a15a8ebe7af8204d017 .thumbnail +6d89697f16bf17a1c2dc8757cd6ec987a34d7de6a16f0b8494d79c2d6148e062 AI-#U0430#U0433#U0435#U043d#U0442#U044b.dc.html +a6396ab0d26b278afa3bb6306e4667d43d2606f75306c9dd72aeed8eb6bccb8f AUTH #U00b7 #U0412#U0445#U043e#U0434.dc.html +80b1436c39c915a8eba25ada02e27d014e847a62b23d3fd635ba010395fbdeed ProductAIRelease.dc.html +cea9065b0892755a5ccb4bf7e1bf214bcd44520e0055f9c981e58bad0c58ecf4 screenshots/01-emp-security.png +e2c4d7a686d71f1d7464c1e121a6a49d813902becf08d3b6a44c77a1cb35e2cd screenshots/01-ov-lower.png +cea9065b0892755a5ccb4bf7e1bf214bcd44520e0055f9c981e58bad0c58ecf4 screenshots/02-emp-security.png +ae02004de7c6427ab3fdcc6b2124494013e2f2ba50b7c92545f81c96102d0bbc screenshots/02-ov-lower.png +d410897bf2fb7614c4cd49a2f5308348382b2cdd5253b2755dd3a16414c6183b screenshots/cc-01.png +e05ab592a28ee75975cfffe1b08def8679d333a29934cee4b79dcb32466a33db screenshots/cc-full.png +f68d5bccb5697155a7235ac38bfc99ce41e14f1f4b7f0f711ff0018f0cdb61bf screenshots/dialogs-waiting.png +cdb685c1642a8cb7f3cd0b80d6338b14c7ba8573cf2586ae56cd3d66747c893b screenshots/overview.png +505f94e08eeccbc87acc6fcc6a47dd9f51fcd74a8d28ab58eb7eb784f7b92efe support.js +212f0e10d99b36513d5eb424183f8d6e4d9ba6eae46495c9e0e27ce7a94b9f6b uploads/ADR-HUB-0013-Internal-UI-Design-System.md +e0067ea9a7cacbc71b0925e02f9277ef8e23c587f11cc23670009919b62acb7d uploads/SPEC-HUB-0004-Internal-Hub-UI.md diff --git a/design/internal/baseline/ProductAIRelease.dc.html b/design/internal/baseline/ProductAIRelease.dc.html new file mode 100644 index 0000000..86cc558 --- /dev/null +++ b/design/internal/baseline/ProductAIRelease.dc.html @@ -0,0 +1,286 @@ + + + + + + + + + + + + + +
+ + + + + +
+
+
AI/FirePage Sales/REL-FP-v5
+
+ +
+
ИП
+
+
+ + +
+
+
+
+

REL-FP-v5

{{ st.label }}
+
FirePage Sales · candidate создан сегодня 11:20 · автор Иван Петров
+
+
+
+ + Test chat + +
+
+ +
+
+ + +
+ {{ vb.icon }} +
{{ vb.title }}
{{ vb.note }}
+ +
+ + +
+ + +
+

Состав release

+ + +
+

Модель и параметры

+
+
Модель
gpt-4o-mini
+
Temperature
0.4
+
Max tokens
1024
+
Top-p
0.9
+
+
+ + +
+

Версии prompts

+ +
+
{{ p.name }}{{ p.ver }}
+ изменён + {{ p.updated }} +
+
+
+ + +
+

Обязательные знания

retrieval index · собран
+ +
+ + {{ k.title }} + {{ k.ver }} +
+
+
+ + +
+
+

Инструменты

+
+
create_checkout
+
get_offers
+
handoff_to_operator
+
+
+
+

Лимиты

+
Диалогов в день5 000
+
Бюджет в день₽5 000
+
Макс. сообщений / диалог40
+
+
+
+ + +
+ +
+

Проверки

{{ checksSummary }}
+ +
+ {{ c.icon }} +
{{ c.label }}
{{ c.detail }}
+
+
+
+ + +
+

Изменения

Относительно REL-FP-v4 (published)
+ +
+ {{ ch.sign }} +
{{ ch.text }}
+
+
+
+
+
+ +
+
+
+ + + +
+
+
+
+
+

Опубликовать REL-FP-v5?

Атомарная публикация конфигурации FirePage Sales
+
+
Новые ответы во всех активных диалогах сразу будут использовать этот release. Текущий REL-FP-v4 перейдёт в архив. Действие необратимо в части уже отправленных ответов.
+
+
+ Понимаю, что активные диалоги переключатся на новый release +
+
+
+ + +
+
+
+
+
+
+ + + diff --git a/design/internal/baseline/screenshots/01-emp-security.png b/design/internal/baseline/screenshots/01-emp-security.png new file mode 100644 index 0000000000000000000000000000000000000000..491fcf9ec73936be2115449b8587b697d21d2dda GIT binary patch literal 27014 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 literal 0 HcmV?d00001 diff --git a/design/internal/baseline/screenshots/01-ov-lower.png b/design/internal/baseline/screenshots/01-ov-lower.png new file mode 100644 index 0000000000000000000000000000000000000000..08fb05603b7453813e94fb53d6fb2808efe029bc GIT binary patch literal 19590 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 literal 0 HcmV?d00001 diff --git a/design/internal/baseline/screenshots/02-emp-security.png b/design/internal/baseline/screenshots/02-emp-security.png new file mode 100644 index 0000000000000000000000000000000000000000..491fcf9ec73936be2115449b8587b697d21d2dda GIT binary patch literal 27014 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 literal 0 HcmV?d00001 diff --git a/design/internal/baseline/screenshots/02-ov-lower.png b/design/internal/baseline/screenshots/02-ov-lower.png new file mode 100644 index 0000000000000000000000000000000000000000..54a49d88250fe87474d63bbbf0bb7929bbb8386b GIT binary patch literal 22935 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 literal 0 HcmV?d00001 diff --git a/design/internal/baseline/screenshots/dialogs-waiting.png b/design/internal/baseline/screenshots/dialogs-waiting.png new file mode 100644 index 0000000000000000000000000000000000000000..035ece284c37e0c7a537b28b7717a7b59718e821 GIT binary patch literal 34203 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 literal 0 HcmV?d00001 diff --git a/design/internal/baseline/screenshots/overview.png b/design/internal/baseline/screenshots/overview.png new file mode 100644 index 0000000000000000000000000000000000000000..15a077c212a8c636e687f69ae2166f59cc3ccf2a GIT binary patch literal 3715 zcmex=+ z$QDV5ur)yJBnaCE#4ax>C;_So0I`cgf}DZu6d+q89f_TU#7-_K0;%r-s5rZ)ISRxP=f? zS-`-+-@?GK?L0yZ5hewRMTubJSb!u`S{ehxr{xR`ypaqH!WS4AxT3*wKmv!`6o67P zKzHQ>VLC$&Ln?y;gC|2CLo$ONkY&VRz+lK=_Ww46GlK{pzYssKh>(DgxTuJPoROlO zjEtPEj-HBjKe5@ci+Wc+`GK@^#11N$=!DtAKhQMeDjE2By2#kinXb6mkz-S1J MhQL6C0OS9g0051Oc>n+a literal 0 HcmV?d00001 diff --git a/design/internal/baseline/support.js b/design/internal/baseline/support.js new file mode 100644 index 0000000..9b71e07 --- /dev/null +++ b/design/internal/baseline/support.js @@ -0,0 +1,1513 @@ +// GENERATED from dc-runtime/src/*.ts — do not edit. Rebuild with `cd dc-runtime && bun run build`. +"use strict"; +(() => { + var __defProp = Object.defineProperty; + var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; + var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + + // src/react.ts + function getReact() { + const R = window.React; + if (!R) throw new Error("dc-runtime: window.React is not available yet"); + return R; + } + function getReactDOM() { + const RD = window.ReactDOM; + if (!RD) throw new Error("dc-runtime: window.ReactDOM is not available yet"); + return RD; + } + var h = ((...args) => getReact().createElement( + ...args + )); + + // src/parse.ts + function parseDcDocument(doc) { + const dc = doc.querySelector("x-dc"); + if (!dc) return null; + const scriptEl = doc.querySelector("script[data-dc-script]"); + const { props, preview } = parseDataProps( + scriptEl?.getAttribute("data-props") ?? null + ); + return { + template: dc.innerHTML, + js: scriptEl ? scriptEl.textContent || "" : "", + props, + preview + }; + } + function parseDcText(src) { + const openMatch = /]*)?>/.exec(src); + if (!openMatch) return null; + const close = src.lastIndexOf(""); + if (close === -1 || close < openMatch.index) return null; + const template = src.slice(openMatch.index + openMatch[0].length, close); + const doc = new DOMParser().parseFromString(src, "text/html"); + const scriptEl = doc.querySelector("script[data-dc-script]"); + const { props, preview } = parseDataProps( + scriptEl?.getAttribute("data-props") ?? null + ); + return { + template, + js: scriptEl ? scriptEl.textContent || "" : "", + props, + preview + }; + } + function parseDataProps(raw) { + if (!raw) return { props: null, preview: null }; + let parsed; + try { + parsed = JSON.parse(raw); + } catch { + return { props: null, preview: null }; + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return { props: null, preview: null }; + } + const obj = parsed; + const preview = obj.$preview && typeof obj.$preview === "object" ? obj.$preview : null; + const rest = {}; + for (const k of Object.keys(obj)) { + if (k[0] !== "$") rest[k] = obj[k]; + } + return { props: Object.keys(rest).length ? rest : null, preview }; + } + function dcNameFromPath(pathname) { + let p = pathname || ""; + try { + p = decodeURIComponent(p); + } catch { + } + const base = p.split("/").pop() || "Root"; + return base.replace(/\.dc\.html$/, "").replace(/\.html?$/, "") || "Root"; + } + + // src/boot.ts + var BASE_CSS = ` + .sc-placeholder{background:rgba(255,255,255,.3);border:1px solid rgba(0,0,0,.5); + border-radius:2px;box-sizing:border-box;overflow:hidden} + @keyframes sc-shine{0%{background-position:100% 50%}100%{background-position:0% 50%}} + html.sc-dc-streaming .sc-placeholder, + html.sc-dc-streaming .sc-interp.sc-missing{position:relative; + background:color-mix(in srgb,currentColor 5%,transparent); + border-color:transparent} + html.sc-dc-streaming .sc-placeholder::before, + html.sc-dc-streaming .sc-interp.sc-missing::before{content:''; + position:absolute;inset:0;pointer-events:none; + background:linear-gradient(90deg,rgba(217,119,87,0) 25%,rgba(247,225,211,.95) 37%,rgba(217,119,87,0) 63%); + background-size:400% 100%;animation:sc-shine 1.4s ease infinite} + html.sc-dc-streaming .sc-placeholder:nth-child(n+9 of .sc-placeholder)::before, + html.sc-dc-streaming .sc-interp.sc-missing:nth-child(n+9 of .sc-interp.sc-missing)::before{animation:none; + background:color-mix(in srgb,currentColor 8%,transparent)} + .sc-placeholder-error{padding:4px 8px;font:11px/1.4 ui-monospace,monospace; + color:rgba(0,0,0,.7);word-break:break-word} + .sc-interp.sc-missing{display:inline-block;width:2em;height:1em;overflow:hidden; + vertical-align:text-bottom;background:rgba(255,255,255,.3);border:1px solid rgba(0,0,0,.5); + border-radius:2px;box-sizing:border-box;color:transparent; + user-select:none} + .sc-interp.sc-unresolved{font-family:ui-monospace,monospace;font-size:.85em; + color:rgba(0,0,0,.5);background:rgba(0,0,0,.05);border-radius:3px; + padding:0 3px} + .sc-host.sc-has-error{position:relative} + .sc-logic-error{position:absolute;top:8px;left:8px;z-index:2147483647;max-width:60ch; + padding:6px 10px;background:#b00020;color:#fff;font:12px/1.4 ui-monospace,monospace; + border-radius:4px;white-space:pre-wrap;pointer-events:none} + /* Mirrors PRINT_BASELINE_CSS in apps/web deck-stage-export.ts \u2014 keep both + in sync until dc-runtime regains a build step. */ + @media print { + @page { margin: 0.5cm; } + section, article, figure, table { break-inside: avoid; } + *, *::before, *::after { + print-color-adjust: exact; -webkit-print-color-adjust: exact; + backdrop-filter: none !important; -webkit-backdrop-filter: none !important; + animation-delay: -99s !important; animation-duration: .001s !important; + animation-iteration-count: 1 !important; animation-fill-mode: both !important; + animation-play-state: running !important; transition-duration: 0s !important; + } + } + `; + var FULL_PAGE_CSS = "html,body{height:100%;margin:0}#dc-root,#dc-root>.sc-host{height:100%}"; + function rootNameForDocument(doc, loc) { + let bootPath = loc.pathname || ""; + if (!/\.dc\.html?$/i.test(safeDecode(bootPath))) { + try { + bootPath = new URL(doc.baseURI || "/").pathname; + } catch { + } + } + return dcNameFromPath(bootPath); + } + function safeDecode(s) { + try { + return decodeURIComponent(s); + } catch { + return s; + } + } + function boot(runtime, doc = document) { + const parsed = parseDcDocument(doc); + if (!parsed) return null; + const React = getReact(); + const rootName = rootNameForDocument(doc, location); + runtime.markFetched(rootName); + runtime.adoptParsed(rootName, parsed); + fetch(location.href).then((res) => res.ok ? res.text() : "").then((t) => { + const raw = t ? parseDcText(t) : null; + if (raw?.template) runtime.updateHtml(rootName, raw.template); + }).catch(() => { + }); + const dc = doc.querySelector("x-dc"); + const hostEl = doc.createElement("div"); + hostEl.id = "dc-root"; + dc.replaceWith(hostEl); + if (!parsed.preview) { + const s = doc.createElement("style"); + s.textContent = FULL_PAGE_CSS; + doc.head.appendChild(s); + } + const Root = runtime.getDC(rootName); + const entry = runtime.registry.get(rootName); + function StandaloneRoot() { + const [, setTick] = React.useState(0); + React.useEffect(() => { + const sub = () => setTick((n) => n + 1); + entry.subs.add(sub); + return () => { + entry.subs.delete(sub); + }; + }, []); + return h(Root, entry.propOverrides || null); + } + const ReactDOM = getReactDOM(); + if (ReactDOM.createRoot) + ReactDOM.createRoot(hostEl).render(h(StandaloneRoot)); + else ReactDOM.render(h(StandaloneRoot), hostEl); + return rootName; + } + + // src/expr.ts + var IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*/; + var NUMBER_RE = /^-?\d+(\.\d+)?$/; + function resolve(vals, src) { + const expr = String(src).trim(); + if (!expr) return void 0; + if (expr[0] === "(" && expr[expr.length - 1] === ")" && parensWrapWhole(expr)) { + return resolve(vals, expr.slice(1, -1)); + } + const eq = findTopLevelEquality(expr); + if (eq) { + const lv = resolve(vals, expr.slice(0, eq.index)); + const rv = resolve(vals, expr.slice(eq.index + eq.op.length)); + switch (eq.op) { + case "===": + return lv === rv; + case "!==": + return lv !== rv; + case "==": + return lv == rv; + default: + return lv != rv; + } + } + if (expr[0] === "!") return !resolve(vals, expr.slice(1)); + if (expr === "true") return true; + if (expr === "false") return false; + if (expr === "null") return null; + if (expr === "undefined") return void 0; + if (NUMBER_RE.test(expr)) return Number(expr); + if (expr.length >= 2 && (expr[0] === '"' || expr[0] === "'") && expr[expr.length - 1] === expr[0]) { + return expr.slice(1, -1); + } + return resolvePath(vals, expr); + } + function parensWrapWhole(expr) { + let depth = 0; + for (let i = 0; i < expr.length - 1; i++) { + if (expr[i] === "(") depth++; + else if (expr[i] === ")") { + depth--; + if (depth === 0) return false; + } + } + return true; + } + function findTopLevelEquality(expr) { + let depth = 0; + for (let i = 0; i < expr.length; i++) { + const c = expr[i]; + if (c === "[" || c === "(") depth++; + else if (c === "]" || c === ")") depth--; + else if (depth === 0 && (c === "=" || c === "!") && expr[i + 1] === "=") { + if (i > 0 && (expr[i - 1] === "=" || expr[i - 1] === "!")) continue; + if (!expr.slice(0, i).trim()) continue; + const op = expr[i + 2] === "=" ? c + "==" : c + "="; + return { index: i, op }; + } + } + return null; + } + function resolvePath(vals, expr) { + const head = expr.match(IDENT_RE); + if (!head) return void 0; + let cur = vals == null ? void 0 : vals[head[0]]; + let i = head[0].length; + while (i < expr.length) { + if (expr[i] === ".") { + const m = expr.slice(i + 1).match(IDENT_RE) || expr.slice(i + 1).match(/^\d+/); + if (!m) return void 0; + cur = cur == null ? void 0 : cur[m[0]]; + i += 1 + m[0].length; + } else if (expr[i] === "[") { + let depth = 1; + let j = i + 1; + while (j < expr.length && depth > 0) { + if (expr[j] === "[") depth++; + else if (expr[j] === "]") { + depth--; + if (depth === 0) break; + } + j++; + } + if (depth !== 0) return void 0; + const key = resolve(vals, expr.slice(i + 1, j)); + cur = cur == null ? void 0 : cur[key]; + i = j + 1; + } else { + return void 0; + } + } + return cur; + } + + // src/encode.ts + var CAMEL_ATTR = "sc-camel-"; + var RAW_WRAP = { + select: "sc-raw-select", + table: "sc-raw-table", + tbody: "sc-raw-tbody", + thead: "sc-raw-thead", + tfoot: "sc-raw-tfoot", + tr: "sc-raw-tr", + td: "sc-raw-td", + th: "sc-raw-th", + caption: "sc-raw-caption" + }; + var RAW_UNWRAP = Object.fromEntries( + Object.entries(RAW_WRAP).map(([k, v]) => [v, k]) + ); + var EVENT_MAP = { + onclick: "onClick", + onchange: "onChange", + oninput: "onInput", + onsubmit: "onSubmit", + onkeydown: "onKeyDown", + onkeyup: "onKeyUp", + onkeypress: "onKeyPress", + onmousedown: "onMouseDown", + onmouseup: "onMouseUp", + onmouseenter: "onMouseEnter", + onmouseleave: "onMouseLeave", + onfocus: "onFocus", + onblur: "onBlur", + ondoubleclick: "onDoubleClick", + oncontextmenu: "onContextMenu" + }; + var ATTRS = `(?:[^>"']|"[^"]*"|'[^']*')*`; + var IMPORT_SELF_CLOSE_RE = new RegExp( + "<(x-import|dc-import)(" + ATTRS + ")/>", + "gi" + ); + var CAMEL_ATTR_RE = /(\s)([a-z]+[A-Z][A-Za-z0-9]*)(\s*=)/g; + function encodeCase(html) { + html = html.replace( + IMPORT_SELF_CLOSE_RE, + (_, t, a) => "<" + t + a + ">" + ); + html = html.replace(/)/gi, "/gi, ""); + html = html.replace( + CAMEL_ATTR_RE, + (_, sp, name, eq) => sp + CAMEL_ATTR + name.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()) + eq + ); + for (const [real, alias] of Object.entries(RAW_WRAP)) { + html = html.replace( + new RegExp("(])", "gi"), + "$1" + alias + ); + } + return html; + } + function kebabToCamel(s) { + return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); + } + function cssToObj(css) { + const o = {}; + for (const decl of css.split(";")) { + const i = decl.indexOf(":"); + if (i < 0) continue; + const prop = decl.slice(0, i).trim(); + o[prop.startsWith("--") ? prop : kebabToCamel(prop)] = decl.slice(i + 1).trim(); + } + return o; + } + function compileAttr(raw) { + const whole = raw.match(/^\s*\{\{([\s\S]+?)\}\}\s*$/); + if (whole) { + const path = whole[1]; + return (vals) => resolve(vals, path); + } + if (raw.includes("{{")) { + const parts = raw.split(/\{\{([\s\S]+?)\}\}/g); + return (vals) => parts.map((s, i) => i & 1 ? resolve(vals, s) ?? "" : s).join(""); + } + return () => raw; + } + + // src/compile.ts + function collectProps(node, isComponent, host) { + const propGetters = []; + const pseudoClasses = []; + let hintSize = null; + for (const { name, value } of [...node.attributes]) { + if (name === "sc-name" || name === "data-dc-tpl") continue; + let key = name; + if (key.startsWith(CAMEL_ATTR)) + key = kebabToCamel(key.slice(CAMEL_ATTR.length)); + if (key === "hint-size") { + hintSize = value; + continue; + } + if (key.startsWith("style-")) { + pseudoClasses.push(host.pseudoClass(key.slice(6), value)); + continue; + } + if (isComponent) { + if (key.includes("-")) key = kebabToCamel(key); + } else { + if (key === "class") key = "className"; + else if (key === "for") key = "htmlFor"; + else if (key.startsWith("on")) + key = EVENT_MAP[key] || "on" + key[2].toUpperCase() + key.slice(3); + } + propGetters.push([key, compileAttr(value)]); + } + return { propGetters, pseudoClasses, hintSize }; + } + var HOST_STYLE_PROPS = /* @__PURE__ */ new Set([ + "position", + "left", + "right", + "top", + "bottom", + "inset", + "width", + "height", + "z-index", + "transform" + ]); + function hostPositionStyle(style) { + const all = typeof style === "string" ? cssToObj(style) : style != null && typeof style === "object" ? style : null; + if (!all) return void 0; + const out = {}; + for (const [k, v] of Object.entries(all)) { + const kebab = k.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()); + if (HOST_STYLE_PROPS.has(kebab)) out[k] = v; + } + return Object.keys(out).length ? out : void 0; + } + function compileTemplate(html, host) { + const tpl = document.createElement("template"); + //! nosemgrep: direct-inner-html-assignment + tpl.innerHTML = encodeCase(html); + let tplN = 0; + (function stamp(node) { + if (node.nodeType === Node.ELEMENT_NODE) { + node.setAttribute("data-dc-tpl", String(tplN++)); + } + for (const c of node.childNodes) stamp(c); + })(tpl.content); + const builders = walkChildren(tpl.content, host); + const render = ((vals, ctx) => builders.map((b, i) => b(vals || {}, ctx, i))); + render.__annotated = tpl.innerHTML; + return render; + } + function walkChildren(node, host) { + return [...node.childNodes].map((c) => walk(c, host)).filter((b) => b != null); + } + function walk(node, host) { + if (node.nodeType === Node.TEXT_NODE) return walkText(node); + if (node.nodeType !== Node.ELEMENT_NODE) return null; + const el = node; + const tag = el.tagName.toLowerCase(); + if (tag === "sc-for") return walkFor(el, host); + if (tag === "sc-if") return walkIf(el, host); + if (tag === "x-import") return walkXImport(el, host); + if (tag === "sc-helmet") return host.helmet(el); + if (tag === "dc-import") return walkComponent(el, host); + return walkElement(el, host); + } + var warnedHoles = /* @__PURE__ */ new Set(); + function warnUnresolved(ctx, what) { + const key = (ctx?.__name || "?") + "\0" + what; + if (warnedHoles.has(key)) return; + warnedHoles.add(key); + console.warn("[dc-runtime] " + (ctx?.__name || "template") + ": " + what); + } + function walkText(node) { + const txt = node.nodeValue ?? ""; + if (!txt.includes("{{")) { + if (!txt.trim() && !txt.includes(" ")) return null; + return () => txt; + } + const parts = txt.split(/\{\{([\s\S]+?)\}\}/g); + return (vals, ctx, key) => h( + getReact().Fragment, + { key }, + ...parts.map((p, i) => { + if (!(i & 1)) return p; + const v = resolve(vals, p); + if (v === void 0) { + if (!ctx?.__streamingNow) { + if (document.body?.hasAttribute("data-dc-editor-on")) { + return h( + "span", + { key: i, className: "sc-interp sc-unresolved" }, + "{{ " + p.trim() + " }}" + ); + } + warnUnresolved( + ctx, + "{{ " + p.trim() + " }} never resolved \u2014 rendered as empty" + ); + return null; + } + return h( + "span", + { key: i, className: "sc-interp sc-missing" }, + p.trim() + ); + } + if (getReact().isValidElement(v) || Array.isArray(v)) { + return h(getReact().Fragment, { key: i }, v); + } + if (v === null || typeof v === "boolean") return null; + return h("span", { key: i, className: "sc-interp" }, String(v)); + }) + ); + } + function walkFor(el, host) { + const listGet = compileAttr(el.getAttribute("list") || ""); + const asName = el.getAttribute("as") || "item"; + const hintN = parseInt(el.getAttribute("hint-placeholder-count") || "0", 10); + const kids = walkChildren(el, host); + const listSrc = el.getAttribute("list") || ""; + return (vals, ctx, key) => { + let list = listGet(vals); + if (!Array.isArray(list)) { + if (!ctx?.__streamingNow) { + if (list !== void 0 && list !== null) { + warnUnresolved( + ctx, + 'sc-for list="' + listSrc + '" is not an array (' + typeof list + ")" + ); + } + list = []; + } else { + list = hintN > 0 ? Array(hintN).fill(void 0) : []; + } + } + return h( + getReact().Fragment, + { key }, + list.map((item, i) => { + const sub = { ...vals, [asName]: item, $index: i }; + return h( + getReact().Fragment, + { key: i }, + kids.map((b, j) => b(sub, ctx, j)) + ); + }) + ); + }; + } + function walkIf(el, host) { + const valGet = compileAttr(el.getAttribute("value") || ""); + const hintRaw = el.getAttribute("hint-placeholder-val"); + const hintGet = hintRaw != null ? compileAttr(hintRaw) : null; + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + let v = valGet(vals); + if (v === void 0 && hintGet && ctx?.__streamingNow) v = hintGet(vals); + return v ? h( + getReact().Fragment, + { key }, + kids.map((b, j) => b(vals, ctx, j)) + ) : null; + }; + } + function walkComponent(el, host) { + const name = el.getAttribute("name") || el.getAttribute("component") || ""; + el.removeAttribute("name"); + el.removeAttribute("component"); + const tplId = el.getAttribute("data-dc-tpl"); + const styleRaw = el.getAttribute("style"); + el.removeAttribute("style"); + const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; + const { propGetters, hintSize } = collectProps(el, true, host); + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + const props = { + key, + __hintSize: hintSize, + __tplId: tplId, + __hostStyle: styleGet ? hostPositionStyle(styleGet(vals)) : void 0 + }; + for (const [k, g] of propGetters) props[k] = g(vals); + if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); + return h(host.component(name), props); + }; + } + function walkXImport(el, host) { + const globalNameGet = compileAttr( + el.getAttribute("component-from-global-scope") || "" + ); + const exportNameGet = compileAttr( + el.getAttribute("component") || el.getAttribute("name") || "" + ); + const url = el.getAttribute("from") || el.getAttribute("src") || el.getAttribute("import") || ""; + const kind = /\.(jsx|tsx)(\?|#|$)/i.test(url) ? "jsx" : "js"; + const tplId = el.getAttribute("data-dc-tpl"); + const styleRaw = el.getAttribute("style"); + el.removeAttribute("style"); + const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; + const wrap = tplId != null || styleGet != null; + const { propGetters, hintSize } = collectProps(el, true, host); + const hasContent = el.children.length > 0 || !!(el.textContent || "").trim(); + const kids = hasContent ? walkChildren(el, host) : []; + const urlBindable = url.includes("{{"); + if (url && !urlBindable) host.loadExternal(kind, url); + const evalName = (g, vals) => { + const v = g(vals); + const s = v == null ? "" : String(v); + return s.includes("{{") ? "" : s; + }; + return (vals, ctx, key) => { + const globalName = evalName(globalNameGet, vals); + const name = globalName || evalName(exportNameGet, vals); + const C = !name || urlBindable ? null : globalName ? host.resolveExternalGlobal(url, globalName) : host.resolveExternal(url, name); + const hostStyle = styleGet ? hostPositionStyle(styleGet(vals)) : void 0; + const wrapper = wrap ? { + key, + className: "sc-host-x", + "data-dc-tpl": tplId, + style: hostStyle || { display: "contents" } + } : null; + if (!C) { + const error = urlBindable ? "x-import `from` cannot contain {{ \u2026 }} \u2014 module URLs are resolved at parse time; use a literal URL" : host.resolveExternalError(url, name); + const ph = host.placeholder({ + key: wrapper ? void 0 : key, + name, + hintSize, + error + }); + return wrapper ? h("div", wrapper, ph) : ph; + } + const props = wrapper ? {} : { key }; + let unresolvedHole = false; + for (const [k, g] of propGetters) { + if (k === "component" || k === "componentFromGlobalScope" || k === "name" || k === "from" || k === "src" || k === "import") { + continue; + } + const v = g(vals); + if (v === void 0) unresolvedHole = true; + props[k] = v; + } + if (unresolvedHole && ctx?.__htmlStreamingNow) { + const ph = host.placeholder({ + key: wrapper ? void 0 : key, + name, + hintSize, + error: null + }); + return wrapper ? h("div", wrapper, ph) : ph; + } + if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); + return wrapper ? h("div", wrapper, h(C, props)) : h(C, props); + }; + } + function walkElement(el, host) { + const realTag = RAW_UNWRAP[el.localName] || el.localName; + const tplId = el.getAttribute("data-dc-tpl"); + const { propGetters, pseudoClasses } = collectProps(el, false, host); + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + const props = { key, "data-dc-tpl": tplId }; + for (const [k, g] of propGetters) { + let v = g(vals); + if (k === "style" && typeof v === "string") v = cssToObj(v); + if ((k === "value" || k === "checked") && v === void 0) { + v = k === "checked" ? false : ""; + } + props[k] = v; + } + if (pseudoClasses.length) { + props.className = [props.className, ...pseudoClasses].filter(Boolean).join(" "); + } + return h(realTag, props, ...kids.map((b, j) => b(vals, ctx, j))); + }; + } + + // src/logic.ts + var StreamableLogic = class { + constructor(props) { + __publicField(this, "props"); + __publicField(this, "state", {}); + /** Back-pointer to the wrapper component, installed after construction. */ + __publicField(this, "__host"); + this.props = props || {}; + } + setState(update, cb) { + this.__host && this.__host.__setLogicState(update, cb); + } + forceUpdate() { + this.__host && this.__host.forceUpdate(); + } + componentDidMount() { + } + componentDidUpdate(_prevProps) { + } + componentWillUnmount() { + } + /** The flat object the template renders against (merged over props). */ + renderVals() { + return {}; + } + }; + function evalDcLogic(src) { + //! nosemgrep: eval-and-function-constructor + const fn = new Function( + "DCLogic", + "StreamableLogic", + "React", + src + '\n;return (typeof Component!=="undefined"&&Component)||undefined;' + ); + return fn(StreamableLogic, StreamableLogic, getReact()); + } + + // src/component.ts + function shallowEqual(a, b) { + if (!b) return false; + const ak = Object.keys(a).filter((k) => k !== "children"); + const bk = Object.keys(b).filter((k) => k !== "children"); + if (ak.length !== bk.length) return false; + for (const k of ak) if (a[k] !== b[k]) return false; + return true; + } + function Placeholder({ + name, + hintSize, + streaming, + error + }) { + const [w, hgt] = (hintSize || "100%,60px").split(","); + return h( + "div", + { + className: "sc-placeholder" + (streaming ? " sc-streaming" : ""), + style: { width: w.trim(), height: hgt && hgt.trim() }, + title: name + }, + error ? h( + "div", + { className: "sc-placeholder-error" }, + (name ? name + ": " : "") + error + ) : null + ); + } + function hintToMin(hint) { + if (!hint) return void 0; + const [w, hgt] = hint.split(","); + return { minWidth: w.trim(), minHeight: hgt && hgt.trim() }; + } + function createComponentFactory(registry, ensureFetched) { + const React = getReact(); + const AncestorContext = React.createContext([]); + class StreamableComponent extends React.Component { + constructor(props) { + super(props); + __publicField(this, "__name"); + __publicField(this, "__sub"); + __publicField(this, "__needsDidMount", false); + /** Snapshot of the registry's streaming flags taken at render time — + * builders read it off the RenderCtx (this) to pick placeholder vs + * render-nothing for unresolved values. */ + __publicField(this, "__streamingNow", false); + __publicField(this, "__htmlStreamingNow", false); + /** When a construct throws, remember the (class, registry.ver, props) + * triple so render-time reconcile doesn't re-attempt it on every parent + * re-render. A registry bump (new class, template, external module + * resolving via bumpAll) changes `ver` and breaks the memo so an + * env-dependent constructor can self-heal. */ + __publicField(this, "__failedLogic", null); + __publicField(this, "__failedUserProps", null); + __publicField(this, "__failedVer", -1); + /** Per-instance constructor error — kept here (not on the registry entry) + * so one instance's successful construct can't hide a sibling's failure, + * and a construct can never wipe an eval error `updateJs` recorded on + * `r.logicError`. */ + __publicField(this, "__ctorError", null); + __publicField(this, "logic"); + this.__name = props.__name; + this.state = { __v: 0, __err: null }; + this.__sub = () => { + if (this.state.__err) this.setState({ __err: null }); + this.forceUpdate(); + }; + this.__makeLogic(registry.get(this.__name).Logic, null); + ensureFetched(this.__name); + } + /** Error-boundary hook: a render crash anywhere in this DC's subtree + * (its own template, an x-import'd component, a child DC without its + * own deeper boundary) lands here instead of unmounting the page. */ + static getDerivedStateFromError(e) { + return { __err: e instanceof Error && e.message ? e.message : String(e) }; + } + componentDidCatch(e, info) { + console.error( + "[dc-runtime] render error in <" + this.__name + ">:", + e, + info?.componentStack || "" + ); + } + /** Instantiate the logic class (or the no-op base) and adopt `prevState` + * over its initial state — used both at mount and on hot-swap. */ + __makeLogic(Logic, prevState) { + const L = Logic || StreamableLogic; + try { + this.logic = new L(this.__userProps()); + this.__failedLogic = null; + this.__failedUserProps = null; + this.__ctorError = null; + } catch (e) { + console.error(e); + this.__failedLogic = Logic; + this.__failedUserProps = this.__userProps(); + this.__failedVer = registry.get(this.__name).ver; + this.__ctorError = this.__name + ": " + (e instanceof Error && e.message ? e.message : String(e)); + this.logic = new StreamableLogic( + this.__userProps() + ); + } + this.logic.__host = this; + if (prevState) + this.logic.state = { ...this.logic.state || {}, ...prevState }; + } + /** The props the author's logic + template see — internal __-prefixed + * wiring stripped. */ + __userProps() { + const { __name, __hintSize, __tplId, __hostStyle, ...rest } = this.props; + return rest; + } + __setLogicState(update, cb) { + const prev = this.logic.state; + const patch = typeof update === "function" ? update(prev) : update; + this.logic.state = { ...prev, ...patch }; + this.setState((s) => ({ __v: s.__v + 1 }), cb); + } + /** Swap the logic instance when the registry's Logic class changed + * (streaming completion, hot reload). State carries over; didMount + * re-fires after the swap commits so refs exist. */ + __reconcileLogic() { + const r = registry.get(this.__name); + const Next = r.Logic; + const Cur = this.logic.constructor; + if (Next === Cur || !Next && Cur === StreamableLogic || Next === this.__failedLogic && r.ver === this.__failedVer && shallowEqual(this.__userProps(), this.__failedUserProps)) { + return; + } + if (!this.__needsDidMount) { + try { + this.logic.componentWillUnmount(); + } catch (e) { + console.error(e); + } + } + this.__makeLogic(Next, this.logic.state); + this.__needsDidMount = true; + } + componentDidMount() { + registry.get(this.__name).subs.add(this.__sub); + try { + this.logic.componentDidMount(); + } catch (e) { + console.error(e); + } + } + componentDidUpdate(prevProps) { + this.logic.props = this.__userProps(); + if (this.__needsDidMount) { + if (this.state.__err || !registry.get(this.__name).tpl) return; + this.__needsDidMount = false; + try { + this.logic.componentDidMount(); + } catch (e) { + console.error(e); + } + } else { + try { + this.logic.componentDidUpdate(prevProps); + } catch (e) { + console.error(e); + } + } + } + componentWillUnmount() { + registry.get(this.__name).subs.delete(this.__sub); + if (!this.__needsDidMount) { + try { + this.logic.componentWillUnmount(); + } catch (e) { + console.error(e); + } + } + } + render() { + const r = registry.get(this.__name); + const cls = "sc-host" + (r.htmlStreaming ? " sc-streaming-html" : "") + (r.jsStreaming ? " sc-streaming-js" : ""); + const hintStyle = r.htmlStreaming ? hintToMin(this.props.__hintSize) : void 0; + const hostStyle = this.props.__hostStyle || hintStyle ? { ...hintStyle || {}, ...this.props.__hostStyle || {} } : void 0; + const hostBase = { + className: cls, + style: hostStyle, + "data-sc-name": this.__name, + "data-dc-tpl": this.props.__tplId + }; + const chain = Array.isArray(this.context) ? this.context : []; + if (chain.includes(this.__name)) { + const cycle = [ + ...chain.slice(chain.indexOf(this.__name)), + this.__name + ].join(" \u2192 "); + return h( + "div", + { ...hostBase, className: cls + " sc-has-error" }, + h(Placeholder, { + name: this.__name, + hintSize: this.props.__hintSize, + error: "circular import: " + cycle + }) + ); + } + if (this.state.__err) { + return h( + "div", + { ...hostBase, className: cls + " sc-has-error" }, + h( + "div", + { className: "sc-logic-error" }, + this.__name + ": " + this.state.__err + ), + h(Placeholder, { + name: this.__name, + hintSize: this.props.__hintSize, + error: this.state.__err + }) + ); + } + this.__reconcileLogic(); + if (!r.tpl) { + return h( + "div", + hostBase, + h(Placeholder, { name: this.__name, hintSize: this.props.__hintSize }) + ); + } + const userProps = this.__userProps(); + this.logic.props = userProps; + let vals = userProps; + let renderErr = r.logicError || this.__ctorError; + try { + vals = { ...userProps, ...this.logic.renderVals() || {} }; + } catch (e) { + console.error(e); + renderErr = this.__name + ".renderVals(): " + (e instanceof Error && e.message ? e.message : String(e)); + } + this.__streamingNow = !!(r.htmlStreaming || r.jsStreaming); + this.__htmlStreamingNow = !!r.htmlStreaming; + return h( + "div", + { ...hostBase, className: cls + (renderErr ? " sc-has-error" : "") }, + renderErr && h("div", { className: "sc-logic-error" }, renderErr), + h( + AncestorContext.Provider, + { value: [...chain, this.__name] }, + r.tpl(vals, this) + ) + ); + } + } + __publicField(StreamableComponent, "contextType", AncestorContext); + const named = /* @__PURE__ */ new Map(); + function getDC(name) { + const hit = named.get(name); + if (hit) return hit; + function Dispatcher(p) { + const [, setTick] = React.useState(0); + React.useEffect(() => { + const sub = () => setTick((n) => n + 1); + registry.get(name).subs.add(sub); + return () => { + registry.get(name).subs.delete(sub); + }; + }, []); + ensureFetched(name); + return h(StreamableComponent, { ...p, __name: name }); + } + Dispatcher.displayName = name; + named.set(name, Dispatcher); + return Dispatcher; + } + return { + getDC, + StreamableComponent + }; + } + + // src/external.ts + var isCustomElementName = (n) => !n.includes(".") && n.includes("-"); + function isRenderableType(g) { + if (typeof g === "function") return !isElementClass(g); + return typeof g === "object" && g !== null && typeof g.$$typeof === "symbol"; + } + function resolveDottedPath(root, name) { + let cur = root; + for (const seg of name.split(".")) { + if (cur == null) return void 0; + cur = cur[seg]; + } + return cur; + } + var BABEL_URL = "https://unpkg.com/@babel/standalone@7.26.4/babel.min.js"; + var GLOBAL_POLL_INTERVAL_MS = 50; + var GLOBAL_POLL_TIMEOUT_MS = 3e4; + function createExternalModules(onResolved) { + const cache = /* @__PURE__ */ new Map(); + let babelLoading = null; + const reportedMissing = /* @__PURE__ */ new Map(); + const polling = /* @__PURE__ */ new Set(); + function ensureBabel() { + if (window.Babel) return Promise.resolve(); + if (babelLoading) return babelLoading; + babelLoading = new Promise((res, rej) => { + const s = document.createElement("script"); + s.src = BABEL_URL; + s.crossOrigin = "anonymous"; + s.onload = () => res(); + s.onerror = rej; + document.head.appendChild(s); + }); + return babelLoading; + } + function load(kind, url) { + if (cache.has(url)) return; + cache.set(url, null); + console.info("[dc-runtime] x-import: loading", url, "(" + kind + ")"); + const ready = kind === "jsx" ? ensureBabel() : Promise.resolve(); + ready.then(() => fetch(url)).then((r) => { + if (!r.ok) throw new Error("HTTP " + r.status); + return r.text(); + }).then((src) => { + const code = kind === "jsx" ? window.Babel.transform(src, { + filename: url, + presets: ["react", "typescript"] + }).code : src; + const module = { exports: {} }; + const before = new Set(Object.keys(window)); + //! nosemgrep: eval-and-function-constructor + new Function("React", "module", "exports", "require", code)( + getReact(), + module, + module.exports, + () => ({}) + ); + const globals = {}; + for (const k of Object.keys(window)) { + if (!before.has(k) && typeof window[k] === "function") { + globals[k] = window[k]; + } + } + cache.set(url, { mod: module.exports, globals }); + console.info( + "[dc-runtime] x-import: loaded", + url, + "\u2014 exports:", + Object.keys(module.exports), + "window globals:", + Object.keys(globals) + ); + onResolved(); + }).catch((e) => { + cache.set(url, { + mod: {}, + globals: {}, + error: "failed to load: " + (e instanceof Error && e.message ? e.message : String(e)) + }); + console.error( + "[dc-runtime] x-import: FAILED to load", + url, + "(" + kind + ")", + e + ); + onResolved(); + }); + } + function resolve2(url, name) { + const entry = cache.get(url); + if (!entry) return null; + const { mod, globals } = entry; + const C = mod && mod[name] || globals && globals[name] || typeof window !== "undefined" && window[name] || mod && mod.default; + if (typeof C === "function") return C; + const key = url + "\0" + name; + if (!reportedMissing.has(key)) { + reportedMissing.set( + key, + entry.error || 'no export named "' + name + '" (has: ' + Object.keys(mod).join(", ") + ")" + ); + console.error( + "[dc-runtime] x-import: module", + url, + "loaded but has no component named", + JSON.stringify(name), + "\u2014 available exports:", + Object.keys(mod), + "window globals:", + Object.keys(globals), + ". The module must `module.exports = {" + name + "}` or set `window." + name + "`." + ); + } + return null; + } + function waitForGlobal(name) { + if (polling.has(name)) return; + polling.add(name); + const started = Date.now(); + const isCE = isCustomElementName(name); + const tick = () => { + const found = isCE ? customElements.get(name) : isRenderableType(resolveDottedPath(window, name)); + if (found) { + polling.delete(name); + onResolved(); + return; + } + if (Date.now() - started >= GLOBAL_POLL_TIMEOUT_MS) { + console.warn( + "[dc-runtime] x-import: global", + JSON.stringify(name), + "never appeared on window after " + GLOBAL_POLL_TIMEOUT_MS + "ms" + ); + return; + } + setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); + }; + setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); + } + function resolveGlobal(url, name) { + const isCE = isCustomElementName(name); + if (!url) { + if (isCE) { + if (customElements.get(name)) return name; + waitForGlobal(name); + return null; + } + const g2 = resolveDottedPath(window, name); + if (isRenderableType(g2)) return g2; + waitForGlobal(name); + return null; + } + const entry = cache.get(url); + if (!entry) return null; + if (isCE && customElements.get(name)) return name; + const g = entry.globals[name] ?? resolveDottedPath(window, name); + if (isRenderableType(g)) return g; + if (name.includes(".")) return null; + const key = url + "\0global\0" + name; + if (!reportedMissing.has(key)) { + reportedMissing.set(key, null); + if (isCE && !customElements.get(name)) { + console.warn( + "[dc-runtime] x-import:", + url, + "loaded but no custom element", + JSON.stringify(name), + "is registered and window." + name + " is not a function \u2014 rendering <" + name + "> as an unknown element." + ); + } + } + return name; + } + function getError(url, name) { + const entry = cache.get(url); + if (entry?.error) return entry.error; + return reportedMissing.get(url + "\0" + name) || null; + } + return { load, resolve: resolve2, resolveGlobal, getError }; + } + function isElementClass(g) { + try { + return typeof g === "function" && typeof HTMLElement !== "undefined" && g.prototype instanceof HTMLElement; + } catch { + return false; + } + } + + // src/helmet.ts + function createHelmetManager(doc, isStreaming) { + const mounted = /* @__PURE__ */ new Set(); + const live = /* @__PURE__ */ new Map(); + function compile(node) { + const raw = [...node.children]; + const helmetClosed = node.nextSibling != null || node.parentNode?.nextSibling != null; + return (_vals, ctx) => { + const name = ctx && ctx.__name || ""; + const streaming = !!(name && isStreaming(name)); + for (let i = 0; i < raw.length; i++) { + const child = raw[i]; + const tag = child.tagName; + const mayBePartial = streaming && !helmetClosed && i === raw.length - 1; + if (tag === "SCRIPT") { + if (mayBePartial) continue; + const key = "SCRIPT|" + (child.getAttribute("src") || child.textContent || ""); + if (mounted.has(key)) continue; + mounted.add(key); + const el = doc.createElement("script"); + for (const { name: an, value } of [...child.attributes]) + el.setAttribute(an, value); + if (child.textContent) el.textContent = child.textContent; + doc.head.appendChild(el); + } else if (tag === "LINK" || tag === "META") { + if (mayBePartial) continue; + const key = tag + "|" + (child.getAttribute("href") || child.getAttribute("src") || child.outerHTML); + if (mounted.has(key)) continue; + mounted.add(key); + doc.head.appendChild(child.cloneNode(true)); + } else { + const key = name + "|" + i; + let el = live.get(key); + if (!el || el.tagName !== tag) { + if (el) el.remove(); + el = doc.createElement(tag.toLowerCase()); + live.set(key, el); + doc.head.appendChild(el); + } + for (const { name: an, value } of [...child.attributes]) { + if (el.getAttribute(an) !== value) el.setAttribute(an, value); + } + if (el.textContent !== child.textContent) + el.textContent = child.textContent; + } + } + return null; + }; + } + return { compile }; + } + + // src/pseudo.ts + function createPseudoSheet(doc) { + let el = null; + const cache = /* @__PURE__ */ new Map(); + let n = 0; + return (pseudo, css) => { + const k = pseudo + "|" + css; + const hit = cache.get(k); + if (hit) return hit; + if (!el) { + el = doc.createElement("style"); + doc.head.appendChild(el); + } + const cls = "scp" + (n++).toString(36); + const sel = pseudo === "before" || pseudo === "after" ? "." + cls + "::" + pseudo : "." + cls + ":" + pseudo; + el.sheet.insertRule(sel + "{" + css + "}", el.sheet.cssRules.length); + cache.set(k, cls); + return cls; + }; + } + + // src/registry.ts + function createRegistry() { + const entries = /* @__PURE__ */ Object.create(null); + function get(name) { + return entries[name] || (entries[name] = { + html: "", + tpl: null, + Logic: null, + jsStreaming: false, + htmlStreaming: false, + ver: 0, + subs: /* @__PURE__ */ new Set(), + fetched: false + }); + } + function bump(name) { + const r = get(name); + r.ver++; + for (const fn of r.subs) fn(); + } + return { + entries, + get, + bump, + bumpAll() { + for (const n in entries) bump(n); + } + }; + } + + // src/runtime.ts + var COMPONENT_DIR = "."; + function createRuntime(doc = document) { + const registry = createRegistry(); + const pseudoClass = createPseudoSheet(doc); + const helmet = createHelmetManager( + doc, + (name) => registry.get(name).htmlStreaming + ); + const external = createExternalModules(() => registry.bumpAll()); + const factory = createComponentFactory(registry, ensureFetched); + const host = { + component: (name) => factory.getDC(name), + placeholder: (props) => h(Placeholder, props), + helmet: (node) => helmet.compile(node), + loadExternal: (kind, url) => external.load(kind, url), + resolveExternal: (url, name) => external.resolve(url, name), + resolveExternalGlobal: (url, name) => external.resolveGlobal(url, name), + resolveExternalError: (url, name) => external.getError(url, name), + pseudoClass + }; + function ensureFetched(name) { + const r = registry.get(name); + if (r.fetched) return; + r.fetched = true; + const url = COMPONENT_DIR + "/" + encodeURIComponent(name) + ".dc.html"; + fetch(url).then((res) => { + if (!res.ok) { + console.error( + "[dc-runtime] sibling fetch for <" + name + "/> failed:", + url, + "returned", + res.status, + "\u2014 the reference renders as an empty placeholder." + ); + return ""; + } + return res.text(); + }).then((t) => { + if (!t) return; + const parsed = parseDcText(t); + if (!parsed) { + console.error( + "[dc-runtime] sibling fetch for <" + name + "/>:", + url, + "has no block \u2014 not a Design Component." + ); + return; + } + if (parsed.props) r.propsMeta = parsed.props; + if (parsed.preview) r.preview = parsed.preview; + if (parsed.template && !r.html) updateHtml(name, parsed.template); + if (parsed.js && !r.Logic) updateJs(name, parsed.js); + }).catch( + (e) => console.error( + "[dc-runtime] sibling fetch for <" + name + "/> threw:", + url, + e + ) + ); + } + function updateHtml(name, html) { + const r = registry.get(name); + r.html = html; + try { + r.tpl = compileTemplate(html, host); + } catch (e) { + console.error("[dc-runtime] template compile FAILED for", name, e); + } + registry.bump(name); + } + function updateJs(name, src) { + const r = registry.get(name); + const seq = r.jsSeq = (r.jsSeq || 0) + 1; + try { + const Cls = evalDcLogic(src); + if (r.jsSeq !== seq) return; + if (typeof Cls !== "function") { + r.logicError = name + ".dc.html: + + + + + + + + + + + + diff --git a/design/internal/baseline/Интеграции.dc.html b/design/internal/baseline/Интеграции.dc.html new file mode 100644 index 0000000..32c344a --- /dev/null +++ b/design/internal/baseline/Интеграции.dc.html @@ -0,0 +1,153 @@ + + + + + + + + + + + + + +
+ + + + + +
+
+
Edevs/Интеграции
+
+ +
+
ИП
+
+
+ +
+
+ + +
+
+

Интеграции

+

Внешние сервисы платформы · {{ okCount }} в норме, {{ issueCount }} требуют внимания

+
+ +
+ + + +
+
+

{{ g.title }}

+ {{ g.note }} +
+
+ +
+
{{ i.mark }}
+
+
+ {{ i.name }} + {{ i.stLabel }} +
+
{{ i.desc }}
+
+
+
Последняя активность
+
{{ i.activity }}
+
+ {{ i.btnLabel }} +
+
+
+
+
+ +
+
+
+
+
+ + + diff --git a/design/internal/baseline/Использование AI.dc.html b/design/internal/baseline/Использование AI.dc.html new file mode 100644 index 0000000..9340a1c --- /dev/null +++ b/design/internal/baseline/Использование AI.dc.html @@ -0,0 +1,265 @@ + + + + + + + + + + + + + +
+ + + + + +
+
+
Edevs/AI/Использование AI
+
+ +
+
ИП
+
+
+ + + + +
+
+ + +
+
+

Использование AI

+

Стоимость, токены и эффективность агентов · {{ periodLabel }}

+
+
+ + + +
+
+ + +
+ +
+
{{ k.label }}
+
{{ k.value }}
+
{{ k.sub }}
+
+
+
+ + +
+
+ Приближение к бюджету · {{ periodLabel }} + {{ budgetSpent }} из {{ budgetTotal }} · {{ budgetPct }}% +
+
+
+ + +
+
+

Динамика стоимости AI

{{ periodLabel }} · {{ chartTotal }} суммарно
+
Стоимость
+
+
+ + + + + + + + + +
+ {{ x.label }} +
+
+
+ + +
+ + +
+

По продукту

+ + + + + + + +
ПРОДУКТДИАЛОГИСТОИМОСТЬ/ ДИАЛОГ
{{ r.name }}{{ r.dialogs }}{{ r.cost }}{{ r.per }}
+
+ + +
+

По модели

+ + + + + + + +
МОДЕЛЬTOKENS INTOKENS OUTСТОИМОСТЬ
{{ r.name }}{{ r.tin }}{{ r.tout }}{{ r.cost }}
+
+ + +
+

По агенту

+ + + + + + + +
АГЕНТДИАЛОГИHANDOFF/ ПРОДАЖА
{{ r.name }}{{ r.dialogs }}{{ r.handoff }}{{ r.perSale }}
+
+ + +
+

По каналу

+ + + + + + + +
КАНАЛДИАЛОГИСТОИМОСТЬДОЛЯ
{{ r.name }}{{ r.dialogs }}{{ r.cost }}{{ r.share }}
+
+ +
+
+
+
+
+
+ + + diff --git a/design/internal/baseline/Карточка AI-агента.dc.html b/design/internal/baseline/Карточка AI-агента.dc.html new file mode 100644 index 0000000..b0d083a --- /dev/null +++ b/design/internal/baseline/Карточка AI-агента.dc.html @@ -0,0 +1,291 @@ + + + + + + + + + + + + + +
+ + + + + +
+
+
Edevs/AI/AI-агенты/FirePage Sales
+
+ +
+
ИП
+
+
+ +
+
+ + +
+
+
+
+
+
+

FirePage Sales

+ {{ rt.label }} +
+
+
Продукт
FirePage
+
Модель
gpt-4o-mini
+
Текущий release
REL-FP-v4
+
Лимиты
5К диал./день · ₽5 000
+
+
+
+ +
+
+ + +
+ + + + + +
+ + + +
+
+
+

Назначение агента

+

Sales-агент консультирует клиентов по продукту FirePage, квалифицирует потребность, предлагает подходящий Offer и оформляет покупку. При выходе за рамки сценария передаёт диалог оператору.

+
+
+

Runtime сейчас

+
+
Активные диалоги
21
+
В очереди на ответ
2
+
Ср. время ответа
3,4с
+
+
+
+
+
+

Лимиты

+
Диалогов в день847 / 5 000
+
+
Бюджет в день₽1 240 / ₽5 000
+
+
+
+

Управление

+ + +
+
+
+
+ + + +
+
+ + Инструкции редактируются в составе release candidate. Активный release published и доступен только для чтения — изменения применяются после публикации новой версии. +
+ +
+
1

Системный prompt

+
Ты — менеджер по продажам FirePage, готовых нишевых сайтов. Отвечай дружелюбно и по делу, на русском. Помогай выбрать между коробкой и годовой поддержкой. Не обещай возможности, которых нет в базе знаний. При запросе человека или нестандартной ситуации — передавай диалог оператору.
+
+ +
+
2

Квалификация (qualification)

+
+
Уточни нишу/тематику сайта и нужен ли он под ключ.
+
Выясни, нужна ли годовая поддержка сразу или позже.
+
Определи срочность и бюджет, не дави на клиента.
+
+
+ +
+
3

Поведение в продаже (sales behavior)

+
+
Предлагай коробку как основное решение, поддержку — как опцию.
+
Оформляй checkout сразу, как клиент подтвердил выбор.
+
Используй только актуальные цены из активного release.
+
+
+ +
+

Ограничения

+
+
Не выдумывай функции и не обещай сроки вне базы знаний.
+
Не предоставляй скидки сверх правил Offer.
+
Не запрашивай данные карты в чате — оплата только через checkout.
+
+
+
+
+ + + +
+

Обязательные знания release

Retrieval index · FirePage KB · 6 материалов
+ +
+ +
{{ k.title }}
{{ k.meta }}
+ В индексе +
+
+
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
RELEASEСТАТУСДАТААВТОРДИАЛОГИКОНВЕРСИЯ
{{ r.id }}{{ r.status }}{{ r.date }}{{ r.author }}{{ r.dialogs }}{{ r.conv }}
+
+
+ + + +
+
+
Диалоги · 30 дней
1 342
+
Продажи · 30 дней
56
+
Конверсия
15,1%
+
Стоимость / продажа
₽67
+
+
+

Причины передачи оператору · 30 дней

+
+ +
+
{{ h.label }}{{ h.count }} · {{ h.pct }}%
+
+
+
+
+
+
+
+ +
+
+
+
+
+ + + diff --git a/design/internal/baseline/Карточка интеграции.dc.html b/design/internal/baseline/Карточка интеграции.dc.html new file mode 100644 index 0000000..d88b084 --- /dev/null +++ b/design/internal/baseline/Карточка интеграции.dc.html @@ -0,0 +1,289 @@ + + + + + + + + + + + + + +
+ + + + + +
+
+
Edevs/Интеграции/{{ name }}
+
+ +
+
ИП
+
+
+ +
+
+ + +
+
+
+
{{ mark }}
+
+
+

{{ name }}

+ {{ st.label }} +
+
{{ subtitle }}
+
+
+
+ +
+
+ +
+
Healthcheck
{{ hc.label }}
+
Аптайм · 30 дней
{{ uptime }}
+
Ответ (p95)
{{ latency }}
+
Последняя проверка
{{ lastCheck }}
+
+
+ + +
+ + +
+ + +
+

Параметры

+
Секреты не отображаются — только статус и метаданные
+ +
+ {{ p.label }} + {{ p.value }} +
+
+
+ + +
+

{{ specialTitle }}

+ + + +
Модель по умолчанию
gpt-4o-mini
+
Разрешённые модели
+
+ {{ m.name }} +
+
+ + + +
+
Merchant account
acc_ed···4021
+
JWT credential
Активен · до 30 июня
+
Webhook
Задержка ответа
+
Reconciliation
Авто · 02:00
+
+
Последние платёжные события
+
+ +
{{ e.name }}{{ e.id }}{{ e.time }}
+
+
+
+ + + +
+
Связанные продукты
FirePageFoxray
+
Идентификатор бота
@edevs_sales
+
Webhook
Активен
+
Сообщений · сегодня
1 284
+
+
+
+ + +
+

Журнал ошибок

за 7 дней
+ + +
+ +
{{ e.code }}
{{ e.message }}
+ {{ e.time }} +
+
+
+ +
Ошибок за период нет
+
+
+
+ + +
+ +
+

Подключение

+
Статус{{ st.label }}
+ +
+ + +
+

Секрет

+
{{ secretNote }}
+
+ + {{ secretMask }} + {{ secretAge }} +
+ +
+ + +
+

Последняя активность

+
+ +
{{ a.text }}
{{ a.time }}
+
+
+
+
+
+ +
+
+
+
+
+ + + diff --git a/design/internal/baseline/Карточка продукта.dc.html b/design/internal/baseline/Карточка продукта.dc.html new file mode 100644 index 0000000..4339184 --- /dev/null +++ b/design/internal/baseline/Карточка продукта.dc.html @@ -0,0 +1,313 @@ + + + + + + + + + + + + + +
+ + + + + +
+
+
Edevs/Продукты/{{ p.name }}
+
+ +
+
ИП
+
+
+ +
+
+ + +
+
+
+
+
+

{{ p.name }}

+ Активен + {{ p.kind }} +
+

{{ p.desc }}

+
+ +
+ +
+
Продажи · 30 дней
{{ p.sales30 }}
+
Выручка · 30 дней
{{ p.rev30 }}
+
{{ p.metric3Label }}
{{ p.metric3 }}
+
Конверсия диалогов
{{ p.conv }}
+
+
+ + +
+ + + + + +
+ + + +
+
+
+

Описание для продаж

+

{{ p.longDesc }}

+
+
+

Ключевые тезисы для AI

+
+ +
{{ b.text }}
+
+
+
+
+
+
+

Параметры

+
Тип{{ p.kind }}
+
ОтделПродажи
+
Sales-agent{{ p.agent }}
+
Создан12 мая 2026
+
+
+

Sales-agent

+
{{ p.agentNote }}
+ Открыть конфигурацию AI +
+
+
+
+ + + +
+
+
Активные Offer привязаны к версии цены. Изменение цены создаёт новую версию — прошлые заказы не затрагиваются.
+ +
+
+ +
+
{{ o.name }}{{ o.type }}
+
{{ o.price }}{{ o.unit }}
+
{{ o.secondary }}
+
{{ o.detail }}
+
{{ o.code }} · price {{ o.priceVer }}AI может предлагать
+
+
+
+ +
+

История версий цен

+ + + + + + + +
ВЕРСИЯOFFERЦЕНАДЕЙСТВУЕТ ССТАТУС
{{ h.ver }}{{ h.offer }}{{ h.price }}{{ h.from }}{{ h.status }}
+
+
+
+ + + +
+

База знаний продукта

Используется AI-агентом для ответов · {{ p.kbCount }} материалов
+ +
+ +
{{ k.title }}
{{ k.meta }}
+ {{ k.status }} +
+
+
+
+ + + +
+

Каналы продаж

Через какие каналы продукт доступен клиентам
+ +
+ +
{{ c.name }}
{{ c.meta }}
+ Активен +
+
+
+
+
+ + + +
+
+

Способ исполнения

{{ p.fulMethod }}
Подключено
+
+
Endpoint / handler
{{ p.fulEndpoint }}
+
Среднее время
{{ p.fulTime }}
+
Успешность · 30 дней
{{ p.fulRate }}
+
+
+
+

Последние операции

+ + + + + + + +
ЗАКАЗОПЕРАЦИЯСТАТУСВРЕМЯ
ORD-10519OP-55120Успешносегодня 14:03
ORD-10516OP-55117В процессесегодня 12:19
ORD-10470OP-55088Ошибкавчера 15:40
+
+
+
+ +
+
+
+
+
+ + + diff --git a/design/internal/baseline/Карточка сотрудника.dc.html b/design/internal/baseline/Карточка сотрудника.dc.html new file mode 100644 index 0000000..6521333 --- /dev/null +++ b/design/internal/baseline/Карточка сотрудника.dc.html @@ -0,0 +1,203 @@ + + + + + + + + + + + + + +
+ + + + + +
+
+
Edevs/Сотрудники/Анна Котова
+
+ +
+
ИП
+
+
+ +
+
+ + +
+
+
АК
+
+
+

Анна Котова

+ OPERATOR + {{ st.label }} +
+
a.kotova@edevs.tech · Отдел продаж
+
+
+ + +
+
+
+ + +
+ + +
+ +
+

Основные данные

+
+
+
+
+
+
+ + +
+

Роль и доступ

+
+
+
+
+ +
+ Диалоги + Клиенты + Заказы · чтение + Финансы · нет + Настройки · нет +
+
+ + +
+

Безопасность

+
+
Пароль
Последняя смена: 28 мая 2026
+ +
+
+
Двухфакторная аутентификация (TOTP)
{{ totpLabel }}
+
+
+
+
Активные сессии
2 устройства · Chrome (Москва), Safari (Москва)
+ +
+
+ + +
+

Опасная зона

+
+
{{ dangerNote }}
+ +
+
+
+ + +
+
+

Статус аккаунта

+
Статус{{ st.label }}
+
Создан12 мая 2026
+
Приглашение принято12 мая 2026
+
Последний вход5 мин назад
+
+ +
+

Текущая нагрузка

+
+
Активные диалоги
3
+
В очереди
0
+
Продажи · сегодня
5
+
Ср. ответ
1м 40с
+
+
+ +
+

Последние действия

+
+
Закрыла диалог · продажа Foxray
8 мин назад
+
Забрала диалог из очереди
22 мин назад
+
Вход в систему · Chrome
сегодня 09:02
+
+
+
+
+ +
+
+
+
+
+ + + diff --git a/design/internal/baseline/Командный центр.dc.html b/design/internal/baseline/Командный центр.dc.html new file mode 100644 index 0000000..8864bde --- /dev/null +++ b/design/internal/baseline/Командный центр.dc.html @@ -0,0 +1,451 @@ + + + + + + + + + + + + + +
+ + + + + +
+ + +
+
+ Edevs + / + Командный центр +
+
+
+ + {{ st.label }} +
+ +
+ +
+
+ + +
+
+ + +
+
+

Командный центр

+

Состояние компании одним взглядом · обновлено только что

+
+
+
+ + + +
+ +
+
+ + +
+
+ +
+
+
+ Компания: {{ st.label }} +
+

{{ compSummary }}

+
+
+
+
Отделы
+
1
+
+
+
Открытые диалоги
+
{{ m.open }}
+
+
+
Выручка · {{ periodLabel }}
+
{{ m.rev }}
+
+
+
+ + +
+ + +
+
+

Отделы

+ 1 активный +
+ + +
+
+
+ +
+
+
+

Продажи

+ + + {{ st.label }} + +
+
Ответственный: Анна Котова · 4 сотрудника · 1 AI-агент
+
+ +
+ +
{{ deptSummary }}
+ + +
ДИАЛОГИ — СЕЙЧАС
+
+
+
Открытые диалоги
+
{{ m.open }}
+
+
+
Активны за 15 мин
+
{{ m.active }}
+
+
+
На AI
+
{{ m.ai }}
+
+
+
На операторах
+
{{ m.op }}
+
+
+
Ожидают оператора
+
{{ m.wait }}
+
+
+ + +
КОММЕРЦИЯ — {{ periodLabelUpper }}
+
+
+
Незавершённые платежи
+
{{ m.pend }}
+
+
+
Ошибки fulfillment
+
{{ m.ferr }}
+
+
+
Продажи
+
{{ m.sales }}
+
+
+
Чистая выручка
+
{{ m.rev }}
+
+
+
+
+ + +
+ + +
+
+
+ + Требует внимания + {{ attentionCount }} +
+ Все +
+ +
+ + +
+
+
+ + Состояние интеграций +
+ {{ okCount }}/6 в норме +
+
+ +
+ + + {{ i.name }} + {{ i.group }} + + {{ i.statusLabel }} +
+
+
+
+ + +
+
+
+ + Расходы AI +
+ {{ periodLabel }} +
+
+ {{ aiSpendStr }} + из {{ budgetStr }} +
+
+
+
+
{{ aiPct }}% дневного бюджета
+
+
+
Токены
+
{{ aiTokens }}
+
+
+
Диалоги
+
{{ aiDialogs }}
+
+
+
Цена диалога
+
{{ costPerDialog }}
+
+
+
+ +
+
+ +
Обновлено: сегодня, 14:32 · детерминированная сводка
+ +
+
+
+
+
+ + + diff --git a/design/internal/baseline/Настройки.dc.html b/design/internal/baseline/Настройки.dc.html new file mode 100644 index 0000000..dcd5496 --- /dev/null +++ b/design/internal/baseline/Настройки.dc.html @@ -0,0 +1,246 @@ + + + + + + + + + + + + + +
+ + + + + +
+
+
Edevs/Настройки
+
+ +
+
ИП
+
+
+ +
+
+
+

Настройки

+

Конфигурация компании и платформы

+
+ +
+ +
+ + + +
+ + +
+ + + +
+
+

Реквизиты

+
+
+
+
+
+
+
+

Брендинг и локаль

+
Логотип Hub
PNG или SVG, до 1 МБ
+
+
+
+
+
+
+
+
+ + + +
+
+ + Не указан резервный канал доставки чеков (ОФД email). Конфигурация считается неполной — чеки уходят только основным способом. +
+
+

Текущий фискальный профиль

Активен · v2
+
+
Режим налогообложения
УСН «Доходы»
+
Ставка НДС
20%
+
Признак способа расчёта
Полный расчёт
+
Период действия
с 01 янв 2026
+
+
Изменение фискального профиля создаёт новую версию — прошлые чеки сохраняют профиль, действовавший на момент продажи.
+
+
+
+
+ + + +
+
+

Политика паролей

+
Минимальная длина
символов в пароле
+
Требовать спецсимволы и цифры
усложнённая сложность пароля
+
+
+

Доступ и сессии

+
TOTP владельца
включена
+
Длительность сессии
до повторного входа
+
Активные сессии
5 устройств у сотрудников
+
+
+
+ + + +
+
+

Категории уведомлений

+ +
{{ n.label }}
{{ n.desc }}
+
+
+
+

Для операторов

+
Звуковое уведомление о новом диалоге
+
Browser notifications
+
+
+
+ + + +
+
+

Сроки хранения (retention)

+
Диалоги и сообщения
после закрытия диалога
+
Вложения
файлы из диалогов
+
Автообезличивание неактивных контактов
через 24 месяца без активности
+
+
+ + Обязательные финансовые записи (заказы, платежи, чеки) хранятся бессрочно по требованиям законодательства и не удаляются автоматически. +
+
+
+ +
+
+
+
+
+
+
+ + + diff --git a/design/internal/baseline/Отделы.dc.html b/design/internal/baseline/Отделы.dc.html new file mode 100644 index 0000000..62466be --- /dev/null +++ b/design/internal/baseline/Отделы.dc.html @@ -0,0 +1,141 @@ + + + + + + + + + + + + + +
+ + + + + +
+
+
Edevs/Отделы
+
+ +
+ +
+
+ +
+
+ + +
+

Отделы

+

Существующие отделы компании · 1 активный

+
+ + +
+
+ +
+
+
+
+
+

Продажи

+ {{ st.label }} +
+

{{ summary }}

+
+
+
+ + +
+
+ Ответственный + АКАнна Котова +
+
+ Состав + 4 сотрудника · 1 AI-агент +
+
+ Связанные продукты + FirePageFoxray +
+
+ + +
+
Открытые диалоги
{{ open }}
+
Продажи · сегодня
{{ sales }}
+
Выручка
{{ rev }}
+
+ + + +
+
+ +

Новые отделы появятся здесь по мере их создания.

+ +
+
+
+
+
+ + + diff --git a/design/internal/baseline/Продажи · Диалоги.dc.html b/design/internal/baseline/Продажи · Диалоги.dc.html new file mode 100644 index 0000000..af550e2 --- /dev/null +++ b/design/internal/baseline/Продажи · Диалоги.dc.html @@ -0,0 +1,523 @@ + + + + + + + + + + + + + +
+ + + + + +
+ + +
+
+ Edevs + / + Продажи + / + Диалоги +
+
+ + + OWNER + + +
+
ИП
+
+
+ + +
+ + +
+
+
+

Диалоги

+ {{ totalCount }} всего +
+
+ + +
+
+ + +
+ + + + + +
+ + + +
+ + +
+ +
+
+
МС
+
+
+ Мария Соколова + + + {{ st.label }} + +
+
+ FirePage· + MAX + ·@maria.s +
+
+
+
+ + + + + + + +
+
+ + +
+
+ +
+
+ Сегодня +
+
+ +
Диалог начат · канал MAX · 14:02
+ + +
+
МС
+
+
Здравствуйте! Смотрю FirePage для лендингов. Есть тариф на команду?
+
14:02
+
+
+ + +
+
+
+
AI-агент FirePage
+
Да! Для команд есть тариф Business: до 10 пользователей, общие проекты и приоритетная поддержка. Помесячная оплата доступна.
+
14:03
+
+
+ + +
+
МС
+
+
Отлично. И ещё вопрос по интеграции с нашей CRM (amoCRM) — можно обсудить с менеджером?
+
14:08
+
+
+ + + +
AI передал диалог оператору · причина: запрос человека (CRM-интеграция)
+
Ожидает оператора · 4 мин
+
+ + + +
+
+
+ + + +
+
+
+ + + +
Иван Петров забрал диалог · 14:09
+
+
ИП
+
+
Иван · оператор
+
Здравствуйте, Мария! Я подключился. По amoCRM — у FirePage Business есть готовая двусторонняя интеграция, лиды и сделки синхронизируются автоматически. Расскажу подробнее?
+
14:10
+
+
+
+ +
+
+ + +
+
+ + + +
+
+
+
Диалог ждёт оператора
+
Возьмите диалог, чтобы ответить клиенту. AI поставлен на паузу.
+
+
+ + +
+
+
+ + + +
+
+
+
AI ведёт диалог
+
Перехватите, чтобы ответить вручную. Контроль перейдёт к вам.
+
+ +
+
+ + + +
+
+ + +
+ + +
+
+ + + +
+
+
+ +
+
+
+ + +
+ +
+ + + +
+ +
+ + + +
+
+
МС
+
Мария Соколова
+
Согласие получено
+
+ +
+
КАНАЛЫ И КОНТАКТЫ
+
+ +
MAX
@maria.s
+ основной +
+
+ +
m.sokolova@workmail.ru
Email · подтверждён
+
+
+ +
+7 ··· ·· 14
Телефон · скрыт
+
+
+ +
+
+ РАБОЧАЯ ЗАМЕТКА + +
+
Команда ~8 человек, интересует FirePage Business помесячно. Открытый вопрос — интеграция с их CRM (amoCRM).
+
+ +
+
СВОДКА
+
+
Заказы
0
+
Диалоги
2
+
Покупки
₽0
+
+
Первый контакт: сегодня, 14:02
+
+ +
+
ДЕЙСТВИЯ OWNER
+ + +
+
+
+ + + +
+
+
+
FirePage
Конструктор лендингов · активен
+
+
ПОДХОДЯЩИЕ OFFER
+
+
BusinessAI может предлагать
+
₽2 490/ мес · помесячно
+
До 10 пользователей · общие проекты · amoCRM-интеграция · приоритетная поддержка.
+
+
+
Proдля одного
+
₽990/ мес
+
1 пользователь · все шаблоны · базовая поддержка.
+
+
+
+ + + +
+
ПРЕДЫДУЩИЕ ДИАЛОГИ
+
+
+
Консультация по тарифам3 дня назад
+
FirePage · MAX · закрыт. Клиент уточнял лимиты тарифов, продажа не оформлена.
+
+
+
Текущий диалогсейчас
+
FirePage · MAX · ждёт оператора. Вопрос по amoCRM-интеграции для команды.
+
+
+
+
+ +
+
+ +
+
+
+
+ + + diff --git a/design/internal/baseline/Продажи · Карточка заказа.dc.html b/design/internal/baseline/Продажи · Карточка заказа.dc.html new file mode 100644 index 0000000..e173d09 --- /dev/null +++ b/design/internal/baseline/Продажи · Карточка заказа.dc.html @@ -0,0 +1,282 @@ + + + + + + + + + + + + + +
+ + + + + +
+
+
Edevs/Продажи/Продажи/ORD-10519
+
+ OWNER + +
+
ИП
+
+
+ +
+
+ + +
+
+ + +
+
+
+
+

ORD-10519

+ Выполнен · подписка активна +
+
+
Сумма
₽2 490 / мес
+ +
Продукт / Offer
FirePage · Business
+
Дата
16 июня 2026
+
Источник
MAX
+
Оформление
AI-агент
+
+
+ +
+
+ + +
+

Commerce timeline

+
+
Checkout
14:00
+
+
Order
14:01
+
+
Payment
14:02
+
+
Receipt
14:02
+
+
Fulfillment
14:03
+
+
Entitlement
активен
+
+
Refund
нет
+
+
+ + +
+ + + + + + +
+ + + +
+
+
+

Позиции заказа

Снимок на момент оформления · цена зафиксирована
+ + + + + + +
OFFERТИПЦЕНАКОЛ-ВОИТОГО
FirePage · Business
OFR-FP-BUS · price v4
Подписка · мес₽2 4901₽2 490
Итого к оплате₽2 490
+
+
+

Принятые документы

+
Публичная оферта v3.1принято 16 июня 14:00
+
Политика обработки персональных данныхпринято 16 июня 14:00
+
+
+
+
+

Данные покупателя

+
Татьяна Лебедева
+
t.lebedeva@agency.ru
+
ООО «Агентство Лебедева» · ИНН 7701234567
+
+
+

Источник оформления

+
Канал MAX · диалог с AI-агентом
+ checkout CHK-77120 → +
создан 16 июня 14:00
+
+
+
+
+ + + +
+
+

Платежи и попытки

+ + + +
ПЛАТЁЖПРОВАЙДЕРСУММАМЕТОДСТАТУССВЕРКА
PAY-88231Точка₽2 490Карта ···4242УспешноСверено
+
+
+

События провайдера

+
+
payment.authorizedevt_8831a14:02:03
+
payment.capturedevt_8831b14:02:05
+
payment.settledevt_8832c02:10
+
+
+
+
+ + + +
+

Фискальный чек

Отправлен в ОФД
+
+
Чек
RCP-44021
+
Тип
Приход
+
Фискальный признак (ФПД)
3920··· 7741
+
Сумма
₽2 490 · НДС 20%
+
ОФД
Принят · 16 июня 14:02
+
Отправлен клиенту
Email · доставлен
+
+
+
+ + + +
+

Product fulfillment

OP-55120 · FirePage
Исполнен
+
+
Попытки
1
+
Результат
Успешно
+
Обновлено
16 июня 14:03
+
+
PAYLOAD (SUMMARY)
+
{ "product": "firepage", "plan": "business", "seats": 10, "account": "agency-lebedeva", "activate_at": "2026-06-16T14:03Z" }
+
+
+ + + +
+

Подписка

SUB-2041 · FirePage Business
Активна
+
+
Текущий период
16 июн — 16 июл 2026
+
Следующее списание
16 июля 2026 · ₽2 490
+
Entitlement
10 рабочих мест
+
+
+ + +
+
+
+ + + +
+ + + + + + + + + +
ВРЕМЯДЕЙСТВИЕАКТОРРЕЗУЛЬТАТ
16 июня 14:03Fulfillment выполнен · OP-55120системауспешно
16 июня 14:02Чек отправлен в ОФД · RCP-44021системауспешно
16 июня 14:02Платёж получен · PAY-88231Точкауспешно
16 июня 14:01Заказ создан · ORD-10519AI-агентуспешно
16 июня 14:00Checkout оформлен · CHK-77120AI-агентуспешно
+
+
+ +
+
+
+
+
+ + + diff --git a/design/internal/baseline/Продажи · Карточка клиента.dc.html b/design/internal/baseline/Продажи · Карточка клиента.dc.html new file mode 100644 index 0000000..2f4851c --- /dev/null +++ b/design/internal/baseline/Продажи · Карточка клиента.dc.html @@ -0,0 +1,276 @@ + + + + + + + + + + + + + +
+ + + + + +
+
+
Edevs/Продажи/Клиенты/Елена Кузнецова
+
+ OWNER + +
+
ИП
+
+
+ +
+
+ + +
+
+ + +
+
+
ЕК
+
+
+

Елена Кузнецова

+ Согласие получено + CUS-4702 +
+
+ e.kuznetsova@corp.ru + +7 ··· ·· 88 + MAXWeb +
+
+ +
+
+ + +
+ + + + + + +
+ + + +
+
+ +
+
Заказы
3
+
Сумма покупок
₽12 470
+
Диалоги
4
+
Первый контакт
28 мая 2026
+
+ +
+

Потребность и заметка

+
Команда дизайна, 5 рабочих мест. Пользуется Foxray Team, рассматривает FirePage для лендингов. Платит по счёту, ЛПР — сама Елена.
+
+ +
+

Последняя активность

+
+
Создан заказ ORD-10512 · Foxray Team
сегодня 11:05
+
Оплата ₽6 500 · ORD-10455
02 июня 18:22
+
Диалог закрыт · продажа Foxray Pro
02 июня 14:00
+
Первый контакт · канал MAX
28 мая 2026
+
+
+
+ +
+ +
+

Текущий диалог

Оператор
+
Foxray · Web Chat · ведёт Иван Петров.
«Помогу с настройкой рабочих мест.»
+ Открыть диалог +
+ +
+

Связанные продукты

+
FoxrayTeam · активна
+
FirePagePro · разовая
+
+
+
+
+ + + + + + + + +
+ + + + + + + + + + + + + + +
ЗАКАЗДАТАПРОДУКТ / OFFERСУММАОПЛАТАИСПОЛНЕНИЕ
ORD-10512сегодня 11:05Foxray · Team₽4 980ОплаченИсполнен
ORD-1045502 июняFoxray · Pro₽6 500ОплаченИсполнен
ORD-1039028 маяFirePage · Pro₽990ОплаченИсполнен
+
+
+ + + +
+
MAX
@elena.kuz
основной · подтверждён
+
Web Chat
session 9f2a···
активна
+
Email
e.kuznetsova@corp.ru
подтверждён
+
Телефон
+7 ··· ·· 88 · скрыт
не подтверждён
+
+
+ + + +
+

История согласий

+
Согласие на обработку данных · получено
Канал MAX · подтверждено пользователем
04 июня 2026, 14:20
+
Согласие на маркетинговые сообщения · получено
Канал MAX
04 июня 2026, 14:20
+
Первичный контакт · согласие ещё не получено
Канал MAX · обработка по законному интересу
28 мая 2026
+
+
+ + + +
+ + + + + + + + + + + + + + +
ВРЕМЯДЕЙСТВИЕОБЪЕКТАКТОРРЕЗУЛЬТАТ
сегодня 11:05Создан заказORD-10512AI-агентуспешно
02 июня 18:22Объединение контактовCUS-4702 ← CUS-4810Иван Петровуспешно
02 июня 14:00Изменены контактные данныеemailИван Петровуспешно
28 мая 2026Контакт созданCUS-4702системауспешно
+
+
+ +
+
+
+
+
+ + + diff --git a/design/internal/baseline/Продажи · Клиенты.dc.html b/design/internal/baseline/Продажи · Клиенты.dc.html new file mode 100644 index 0000000..5add358 --- /dev/null +++ b/design/internal/baseline/Продажи · Клиенты.dc.html @@ -0,0 +1,334 @@ + + + + + + + + + + + + + +
+ + + + + +
+
+
Edevs/Продажи/Клиенты
+
+ OWNER + +
+
ИП
+
+
+ +
+
+ + +
+
+

Клиенты

+

Контакты отдела продаж · показано {{ shownCount }} из 248

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+ + +
+ + + +
+
+
+ + +
+ + +
+ + + +
+
+
+ + +
+ +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
КЛИЕНТКОНТАКТЫКАНАЛЫПРОДУКТЫПОСЛ. ДИАЛОГ {{ arrLast }}ОТКР. {{ arrOpen }}ЗАКАЗЫ {{ arrOrders }}СУММА ПОКУПОК {{ arrTotal }}
+
+
{{ r.initials }}
+
+ {{ r.name }} +
{{ r.cid }}
+
+
+
+
{{ r.email }}
+
{{ r.phone }}
+
+
+ + {{ ch.label }} + +
+
+
+ + {{ pr.name }} + +
+
+
{{ r.lastLabel }}
+
{{ r.openD }}{{ r.orders }}{{ r.total }} + + + + +
+
+ + + +
+
+
Клиенты не найдены
+
Измените условия фильтра или сбросьте их.
+
+
+ + +
+
Показано {{ shownCount }} из 248
+
+ + + + + … + + +
+ 10 / стр. +
+
+
+ +
+
+
+
+
+ + + diff --git a/design/internal/baseline/Продажи · Обзор.dc.html b/design/internal/baseline/Продажи · Обзор.dc.html new file mode 100644 index 0000000..c99f4bc --- /dev/null +++ b/design/internal/baseline/Продажи · Обзор.dc.html @@ -0,0 +1,371 @@ + + + + + + + + + + + + + +
+ + + + + +
+
+
+ Edevs/ + Продажи/ + Обзор +
+
+ OWNER + +
+
ИП
+
+
+ +
+
+ + +
+
+

Обзор отдела продаж

+

Операционное состояние и результаты · обновлено только что

+
+
+ + + +
+
+ + +
ОПЕРАЦИОННЫЕ · СЕЙЧАС
+
+ +
+
{{ k.label }}
+
{{ k.value }}
+
{{ k.sub }}
+
+
+
+ + +
РЕЗУЛЬТАТ · {{ periodUpper }}
+
+ +
+
{{ k.label }}
+
{{ k.value }}
+
+ + + {{ k.deltaText }} + + {{ k.sub }} +
+
+
+
+ + +
+
+
+

Динамика продаж

+
+ {{ chartTotal }} + {{ chartDelta }} + чистая выручка · {{ periodLabel }} +
+
+
Выручка
+
+
+ + + + + + + + + +
+ + {{ x.label }} + +
+
+
+ + +
+ + +
+
+

Продукты

+ Все продукты +
+ + + + + + + + + + + + + + + + + + + + + +
ПРОДУКТСТАТУСПРОДАЖИВЫРУЧКАКОНВ.ДИАЛОГИ
{{ p.name }}{{ p.status }}{{ p.sales }}{{ p.rev }}{{ p.conv }}{{ p.dlg }}
+
+ + +
+

Каналы

+ +
+
+ {{ c.name }} + {{ c.dlg }} диал. · {{ c.sales }} продаж +
+
+
+
+
+ + +
+

AI и операторы

+
+
+
AI-агент
+
+
Диалоги
{{ aiDlg }}
+
Продажи
{{ aiSales }}
+
Конверсия
{{ aiConv }}
+
Стоимость
{{ aiCost }}
+
+
+
+
Операторы
+
+
Диалоги
{{ opDlg }}
+
Продажи
{{ opSales }}
+
Конверсия
{{ opConv }}
+
Передано AI→
{{ handoff }}
+
+
+
+
+ + +
+
+ +

Проблемные диалоги

+
+ + + + {{ p.title }}{{ p.meta }} + {{ p.time }} + + +
+ + +
+
+ +

Платежи и fulfillment

+
+ + + + {{ p.title }}{{ p.meta }} + {{ p.time }} + + +
+ +
+
+
+
+
+
+ + + diff --git a/design/internal/baseline/Продажи · Продажи.dc.html b/design/internal/baseline/Продажи · Продажи.dc.html new file mode 100644 index 0000000..4cb4303 --- /dev/null +++ b/design/internal/baseline/Продажи · Продажи.dc.html @@ -0,0 +1,364 @@ + + + + + + + + + + + + + +
+ + + + + +
+
+
Edevs/Продажи/Продажи
+
+ OWNER + +
+
ИП
+
+
+ +
+
+ + +
+
+

Продажи

+

Заказы, платежи, возвраты, подписки и исполнение · основной объект — заказ

+
+
+
+ + +
+ +
+
+ + +
+ + + + + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ЗАКАЗДАТАКЛИЕНТПРОДУКТ / OFFERСУММАОПЛАТАИСПОЛНЕНИЕИСТОЧНИКПРОДАВЕЦ
{{ r.id }}{{ r.date }}{{ r.client }}{{ r.offer }}{{ r.amount }}{{ r.payLabel }}{{ r.fulLabel }}{{ r.srcLabel }}{{ r.seller }}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ПОДПИСКАКЛИЕНТOFFERСТАТУСПЕРИОДСЛЕД. СПИСАНИЕENTITLEMENT
{{ r.id }}{{ r.client }}{{ r.offer }}{{ r.stLabel }}{{ r.period }}{{ r.next }}{{ r.ent }}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ПЛАТЁЖЗАКАЗПРОВАЙДЕРСУММАСТАТУСМЕТОДДАТАСВЕРКА
{{ r.id }}{{ r.order }}{{ r.provider }}{{ r.amount }}{{ r.stLabel }}{{ r.method }}{{ r.date }}{{ r.rec }}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ВОЗВРАТЗАКАЗ / ПЛАТЁЖСУММАПРИЧИНАСТАТУСИНИЦИАТОРДАТА
{{ r.id }}{{ r.ref }}{{ r.amount }}{{ r.reason }}{{ r.stLabel }}{{ r.actor }}{{ r.date }}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ЗАКАЗПРОДУКТОПЕРАЦИЯСТАТУСПОПЫТКИПОСЛ. ОШИБКАОБНОВЛЕНОДЕЙСТВИЕ
{{ r.order }}{{ r.product }}{{ r.op }}{{ r.stLabel }}{{ r.attempts }}{{ r.error }}{{ r.updated }} + + + + + — + +
+
+ + + +
+
+
Ничего не найдено
+
Измените поисковый запрос.
+
+
+
+ + +
+
Показано {{ shownCount }} из {{ totalCount }}
+
+ + + + +
+ 20 / стр. +
+
+
+ +
+
+
+
+
+ + + diff --git a/design/internal/baseline/Продукты.dc.html b/design/internal/baseline/Продукты.dc.html new file mode 100644 index 0000000..b8213c9 --- /dev/null +++ b/design/internal/baseline/Продукты.dc.html @@ -0,0 +1,213 @@ + + + + + + + + + + + + + +
+ + + + + +
+
+
Edevs/Продукты
+
+ +
+
ИП
+
+
+ +
+
+ + +
+
+ + +
+
+

Продукты

+

2 активных продукта · цены и Offer управляются в карточке продукта

+
+
+
+ + + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
ПРОДУКТСТАТУСOFFER И ЦЕНЫSALES-AGENTКАНАЛЫFULFILLMENTПРОДАЖИ · {{ periodUpper }}
+
+
+
{{ r.name }}
{{ r.sub }}
+
+
Активен + +
+ {{ o.name }} · {{ o.type }} + {{ o.price }} +
+
+ +
{{ r.note }}
+
+
AI-агент
{{ r.agentRelease }}
+
+ + {{ c.label }} + +
+
Подключён
{{ r.fulfillment }}
{{ r.salesSum }}
{{ r.salesN }} продаж
+ + + + +
+
+
+
2 продукта
+
Активная цена не редактируется задним числом — создаётся новая версия
+
+
+ +
+
+
+
+
+ + + diff --git a/design/internal/baseline/Профиль.dc.html b/design/internal/baseline/Профиль.dc.html new file mode 100644 index 0000000..410a403 --- /dev/null +++ b/design/internal/baseline/Профиль.dc.html @@ -0,0 +1,153 @@ + + + + + + + + + + + + + +
+ + + + + +
+
+
Edevs/Профиль
+
+
ИП
+
+
+ +
+
+ + +
+
+
ИП
+
+

Иван Петров

OWNER
+
ivan@edevs.tech
+
+ +
+
+ +
+ +
+

Личные данные

+
+
+
+
+
+
+
+ + +
+

Смена пароля

+
+
+
+
+
+
+
+ + +
+
+

Двухфакторная аутентификация (TOTP)

{{ totpNote }}
+
+
+ +
Приложение-аутентификатор подключено · последний код принят 5 мин назад
+
+
+ + +
+

Активные сессии

+ +
+ {{ s.icon }} +
+
{{ s.device }}текущая
+
{{ s.meta }}
+
+ — + +
+
+
+
+ +
+
+
+
+
+ + + diff --git a/design/internal/baseline/Сотрудники.dc.html b/design/internal/baseline/Сотрудники.dc.html new file mode 100644 index 0000000..47bd3ff --- /dev/null +++ b/design/internal/baseline/Сотрудники.dc.html @@ -0,0 +1,223 @@ + + + + + + + + + + + + + +
+ + + + + +
+
+
Edevs/Сотрудники
+
+ +
+
ИП
+
+
+ +
+
+ + +
+
+ + +
+
+

Сотрудники

+

Доступ к Hub · показано {{ shownCount }} из 6

+
+ +
+ + +
+
+ + +
+
Роль +
+ + + +
+
+
Статус +
+ + + + +
+
+
+ +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
СОТРУДНИКРОЛЬОТДЕЛСТАТУСПОСЛЕДНИЙ ВХОДДИАЛОГИ
+
+
{{ r.initials }}
+
{{ r.name }}
{{ r.email }}
+
+
{{ r.role }}{{ r.dept }}{{ r.stLabel }}{{ r.lastLogin }}{{ r.dialogs }} + + + + +
+
+ + +
+
+
Сотрудники не найдены
+
Измените условия фильтра.
+
+
+ +
+
Показано {{ shownCount }} из 6
+
Сессии и пароли управляются в карточке сотрудника
+
+
+ +
+
+
+
+
+ + + diff --git a/design/internal/baseline/Тестовый чат.dc.html b/design/internal/baseline/Тестовый чат.dc.html new file mode 100644 index 0000000..f8a45de --- /dev/null +++ b/design/internal/baseline/Тестовый чат.dc.html @@ -0,0 +1,218 @@ + + + + + + + + + + + + + +
+ + + + + +
+
+
Edevs/AI/Тестовый чат
+
+ Тестовая песочница +
+
ИП
+
+
+ + +
+ + +
+
+
RELEASE CANDIDATE
+
+ + +
+
+ + FirePage Sales · gpt-4o-mini · temp 0.4 +
+ +
КАНАЛ СИМУЛЯЦИИ
+
+ + + +
+ +
ГОТОВЫЕ СЦЕНАРИИ
+
+ + + +
+
+
+ +
+
+ + +
+
+
+
+
Test conversation
Симуляция клиента · сообщений: 4
+
+ Не создаёт Contact, Order или Payment +
+ +
+
+
Сценарий: «Покупка коробки» · канал {{ channelLabel }}
+ + +
Т
Здравствуйте, интересует FirePage для сайта-визитки. Сколько стоит?
тестовый клиент
+ + +
AI · REL-FP-v5
Здравствуйте! FirePage — это готовый нишевой сайт под ключ. Коробка стоит ₽4 900 единоразово. Дополнительно можно подключить годовую поддержку за ₽1 470. Под визитку отлично подходит — оформить?
debug ниже →
+ + +
Т
Да, давайте оформим коробку.
+ + +
Отлично! Подготовил оформление коробки FirePage за ₽4 900. Вызываю checkout — в реальном диалоге клиент получил бы ссылку на оплату.
tool: create_checkout
+
+
+ + +
+
+
+ + +
+
+
+
+ + +
+

Диагностика

Последний ответ AI
+
+ + +
+
Tokens
1 284
in 940 · out 344
+
Cost
₽0,42
latency 3,1с
+
+ + +
ИСПОЛЬЗОВАННЫЕ CHUNKS
+
+ +
+
{{ c.source }}{{ c.score }}
+
{{ c.text }}
+
+
+
+ + +
ВЫЗВАННЫЕ TOOLS
+
+
get_offers
product=firepage → 2 offers
120мс
+
create_checkout
offer=OFR-FP-BOX · ₽4 900 (симуляция)
88мс
+
+ + +
HANDOFF
+
Передачи оператору не было — сценарий в рамках агента.
+ +
+
+ +
+
+
+
+ + + diff --git a/design/internal/screens/.gitkeep b/design/internal/screens/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/design/web-chat/.gitkeep b/design/web-chat/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json new file mode 100644 index 0000000..0230ed9 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "@edevs/hub-workspace", + "private": true, + "version": "0.1.0", + "packageManager": "npm@10.0.0", + "workspaces": [ + "apps/internal-ui", + "apps/checkout", + "apps/web-chat", + "packages/ui", + "packages/contracts", + "packages/shared" + ], + "scripts": { + "dev:internal-ui": "npm --workspace @edevs/internal-ui run dev", + "dev:checkout": "npm --workspace @edevs/checkout run dev", + "dev:web-chat": "npm --workspace @edevs/web-chat run dev", + "build": "npm run build --workspaces --if-present", + "typecheck": "npm run typecheck --workspaces --if-present", + "lint": "npm run lint --workspaces --if-present", + "test": "npm run test --workspaces --if-present" + }, + "devDependencies": { + "@playwright/test": "^1.54.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "^5.8.0", + "vite": "^7.0.0", + "vitest": "^3.2.0" + } +} diff --git a/packages/contracts/.gitkeep b/packages/contracts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/contracts/package.json b/packages/contracts/package.json new file mode 100644 index 0000000..1c84c4a --- /dev/null +++ b/packages/contracts/package.json @@ -0,0 +1,11 @@ +{ + "name": "@edevs/contracts", + "private": true, + "version": "0.1.0", + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "scripts": { + "typecheck": "tsc -p tsconfig.json" + } +} diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts new file mode 100644 index 0000000..3e4f3fb --- /dev/null +++ b/packages/contracts/src/index.ts @@ -0,0 +1,13 @@ +export interface HealthResponse { + status: "ok" | "degraded"; + service?: string; + checks?: Record; +} + +export interface DepartmentSummary { + departmentId: string; + generatedAt: string; + attentionStatus: "NORMAL" | "ATTENTION" | "CRITICAL"; + summaryText: string; + workspaceRoute: string; +} diff --git a/packages/contracts/tsconfig.json b/packages/contracts/tsconfig.json new file mode 100644 index 0000000..564a599 --- /dev/null +++ b/packages/contracts/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"] +} diff --git a/packages/shared/.gitkeep b/packages/shared/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..b4d479b --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,11 @@ +{ + "name": "@edevs/shared", + "private": true, + "version": "0.1.0", + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "scripts": { + "typecheck": "tsc -p tsconfig.json" + } +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..42fdc97 --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,5 @@ +export type AttentionStatus = "NORMAL" | "ATTENTION" | "CRITICAL"; + +export type HubApplication = "internal-ui" | "checkout" | "web-chat"; + +export const defaultApiBaseUrl = "http://localhost:8000/api/v1"; diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..564a599 --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"] +} diff --git a/packages/ui/.gitkeep b/packages/ui/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 0000000..fdee80c --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,16 @@ +{ + "name": "@edevs/ui", + "private": true, + "version": "0.1.0", + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "scripts": { + "typecheck": "tsc -p tsconfig.json" + }, + "peerDependencies": { + "antd": "^5.26.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts new file mode 100644 index 0000000..13689ef --- /dev/null +++ b/packages/ui/src/index.ts @@ -0,0 +1 @@ +export { edevsHubTheme } from "./theme"; diff --git a/packages/ui/src/theme.ts b/packages/ui/src/theme.ts new file mode 100644 index 0000000..53ef034 --- /dev/null +++ b/packages/ui/src/theme.ts @@ -0,0 +1,27 @@ +import type { ThemeConfig } from "antd"; + +export const edevsHubTheme: ThemeConfig = { + token: { + colorPrimary: "#2563eb", + colorSuccess: "#059669", + colorWarning: "#d97706", + colorError: "#dc2626", + colorInfo: "#2563eb", + borderRadius: 8, + fontFamily: + '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', + }, + components: { + Layout: { + headerBg: "#ffffff", + siderBg: "#ffffff", + bodyBg: "#f6f8fb", + }, + Card: { + borderRadiusLG: 8, + }, + Table: { + headerBg: "#f8fafc", + }, + }, +}; diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json new file mode 100644 index 0000000..564a599 --- /dev/null +++ b/packages/ui/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"] +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..fb09d48 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,25 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests/e2e", + timeout: 30_000, + fullyParallel: true, + reporter: [["list"], ["html", { open: "never" }]], + use: { + trace: "on-first-retry", + }, + projects: [ + { + name: "internal-ui", + use: { ...devices["Desktop Chrome"], baseURL: "http://localhost:5173" }, + }, + { + name: "checkout", + use: { ...devices["Desktop Chrome"], baseURL: "http://localhost:5174" }, + }, + { + name: "web-chat-mobile", + use: { ...devices["Pixel 7"], baseURL: "http://localhost:5175" }, + }, + ], +}); diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b90285c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[tool.ruff] +target-version = "py312" +line-length = 100 +src = ["apps/backend"] + +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "B", "DJ"] +ignore = ["DJ001"] + +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "hub_backend.settings" +pythonpath = ["apps/backend"] +testpaths = ["apps/backend"] +python_files = ["test_*.py", "*_tests.py"] + +[tool.mypy] +python_version = "3.12" +plugins = ["mypy_django_plugin.main"] +ignore_missing_imports = true + +[tool.django-stubs] +django_settings_module = "hub_backend.settings" diff --git a/scripts/.gitkeep b/scripts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/check.ps1 b/scripts/check.ps1 new file mode 100644 index 0000000..516cee0 --- /dev/null +++ b/scripts/check.ps1 @@ -0,0 +1,6 @@ +$ErrorActionPreference = "Stop" + +docker compose run --rm backend python manage.py test +docker compose run --rm internal-ui npm run typecheck +docker compose run --rm checkout npm run typecheck +docker compose run --rm web-chat npm run typecheck diff --git a/scripts/start.ps1 b/scripts/start.ps1 new file mode 100644 index 0000000..26b93c0 --- /dev/null +++ b/scripts/start.ps1 @@ -0,0 +1,7 @@ +$ErrorActionPreference = "Stop" + +if (-not (Test-Path ".env")) { + Copy-Item ".env.example" ".env" +} + +docker compose up --build diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/apps.spec.ts b/tests/e2e/apps.spec.ts new file mode 100644 index 0000000..b76ee93 --- /dev/null +++ b/tests/e2e/apps.spec.ts @@ -0,0 +1,6 @@ +import { expect, test } from "@playwright/test"; + +test("application shell responds", async ({ page, baseURL }) => { + await page.goto(baseURL ?? "/"); + await expect(page.locator("body")).toBeVisible(); +}); diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..dbec643 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + } +}
+ + + + + +
+
+
Edevs/Настройки/Аудит
+
+ +
+
ИП
+
+
+ +
+
+ +
+
+

Аудит

+

Журнал действий пользователей и системы · {{ shownCount }} записей

+
+ +
+ + +
+
+ + +
+
+
+
+
+
+ +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
ВРЕМЯАКТОРДЕЙСТВИЕОБЪЕКТРЕЗУЛЬТАТCORRELATION IDИСТОЧНИК
{{ r.time }}{{ r.actorMark }}{{ r.actor }}{{ r.action }}{{ r.object }}{{ r.result }}{{ r.cid }}{{ r.source }}
+
+ +
Записи не найдены
Измените фильтры или период.
+
+
+
Показано {{ shownCount }} из 1 204
+
Журнал доступен только для чтения
+
+
+ +
+
+
+ + + +
+
+
+
{{ d.action }}
{{ d.time }} · {{ d.cid }}
+ +
+
+
+
Актор
{{ d.actor }}
+
Результат
{{ d.result }}
+
Объект
{{ d.object }}
+
Источник / IP
{{ d.source }}
+
+ +
СТРУКТУРИРОВАННЫЕ ИЗМЕНЕНИЯ
+
+ +
+
{{ c.field }}
+
+ {{ c.from }} + + {{ c.to }} +
+
+
+
+ +
+ + Секреты и чувствительные payload не отображаются в журнале аудита. +
+
+
+
+