From 68c30298355d5f0482911d8a9170d2b734c49c3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BA=D0=B0=D1=8D=D0=BB=20=D0=9E=D0=B3=D0=B0?= =?UTF-8?q?=D0=BD=D0=B5=D1=81=D1=8F=D0=BD?= Date: Wed, 8 Apr 2026 02:10:17 +0300 Subject: [PATCH] Main logic --- .env.example | 11 + .gitignore | 3 + Makefile | 40 + angular.json | 19 +- docs/doc_v1.json | 96 ++ package-lock.json | 1136 ++++++++++++++++- package.json | 16 +- proxy.conf.cjs | 15 + public/KB_USA-standard.svg | 398 ++++++ public/fonts/TinkoffSans-Bold.ttf | Bin 0 -> 70888 bytes public/fonts/TinkoffSans-Medium.ttf | Bin 0 -> 71272 bytes public/fonts/TinkoffSans-Regular.ttf | Bin 0 -> 70240 bytes scripts/sync-env.cjs | 33 + src/app/app.config.ts | 15 +- src/app/app.css | 40 + src/app/app.html | 356 +----- src/app/app.routes.ts | 16 +- src/app/app.spec.ts | 20 +- src/app/app.ts | 12 +- src/app/core/config/api.tokens.ts | 34 + src/app/core/devtools/dev-log.service.ts | 52 + src/app/core/http/api-base-url.interceptor.ts | 17 + src/app/core/http/dev-log.interceptor.ts | 117 ++ .../core/http/error-classification.util.ts | 67 + src/app/core/http/http-error.util.ts | 56 + .../core/keyboard/keyboard-key-name-map.ts | 71 ++ .../core/keyboard/keyboard-payload.util.ts | 88 ++ .../keyboard-svg-highlight.service.ts | 54 + .../core/keyboard/vk-to-keyboard-svg-id.ts | 115 ++ src/app/core/models/api.types.ts | 71 ++ .../user-error-messages.config.ts | 34 + .../user-error-notify.service.ts | 40 + src/app/core/services/sessions-api.service.ts | 80 ++ .../session-status-chip-classes.pipe.ts | 24 + .../sessions/session-status-labels.config.ts | 11 + src/app/core/sessions/session-status.pipe.ts | 33 + .../dev-console/dev-console.component.ts | 89 ++ .../devtools/dev-console/dev-console.css | 144 +++ .../devtools/dev-console/dev-console.html | 79 ++ .../hls-player/hls-player.component.ts | 48 + .../sessions/hls-player/hls-player.css | 11 + .../sessions/hls-player/hls-player.html | 1 + .../session-detail.component.ts | 325 +++++ .../session-detail/session-detail.css | 258 ++++ .../session-detail/session-detail.html | 271 ++++ .../sessions-list/sessions-list.component.ts | 106 ++ .../sessions/sessions-list/sessions-list.css | 119 ++ .../sessions/sessions-list/sessions-list.html | 82 ++ .../telemetry-event-detail.component.ts | 58 + .../telemetry-event-detail.css | 88 ++ .../telemetry-event-detail.html | 44 + src/app/shared/utils/date-time.util.ts | 23 + src/environments/environment.prod.ts | 6 + src/environments/environment.ts | 6 + src/styles.css | 69 +- src/styles/color-tokens.css | 80 ++ src/styles/session-status-chips.css | 18 + src/styles/sg-input-fields.css | 110 ++ 58 files changed, 4807 insertions(+), 418 deletions(-) create mode 100644 .env.example create mode 100644 Makefile create mode 100644 proxy.conf.cjs create mode 100644 public/KB_USA-standard.svg create mode 100644 public/fonts/TinkoffSans-Bold.ttf create mode 100644 public/fonts/TinkoffSans-Medium.ttf create mode 100644 public/fonts/TinkoffSans-Regular.ttf create mode 100644 scripts/sync-env.cjs create mode 100644 src/app/core/config/api.tokens.ts create mode 100644 src/app/core/devtools/dev-log.service.ts create mode 100644 src/app/core/http/api-base-url.interceptor.ts create mode 100644 src/app/core/http/dev-log.interceptor.ts create mode 100644 src/app/core/http/error-classification.util.ts create mode 100644 src/app/core/http/http-error.util.ts create mode 100644 src/app/core/keyboard/keyboard-key-name-map.ts create mode 100644 src/app/core/keyboard/keyboard-payload.util.ts create mode 100644 src/app/core/keyboard/keyboard-svg-highlight.service.ts create mode 100644 src/app/core/keyboard/vk-to-keyboard-svg-id.ts create mode 100644 src/app/core/models/api.types.ts create mode 100644 src/app/core/notifications/user-error-messages.config.ts create mode 100644 src/app/core/notifications/user-error-notify.service.ts create mode 100644 src/app/core/services/sessions-api.service.ts create mode 100644 src/app/core/sessions/session-status-chip-classes.pipe.ts create mode 100644 src/app/core/sessions/session-status-labels.config.ts create mode 100644 src/app/core/sessions/session-status.pipe.ts create mode 100644 src/app/features/devtools/dev-console/dev-console.component.ts create mode 100644 src/app/features/devtools/dev-console/dev-console.css create mode 100644 src/app/features/devtools/dev-console/dev-console.html create mode 100644 src/app/features/sessions/hls-player/hls-player.component.ts create mode 100644 src/app/features/sessions/hls-player/hls-player.css create mode 100644 src/app/features/sessions/hls-player/hls-player.html create mode 100644 src/app/features/sessions/session-detail/session-detail.component.ts create mode 100644 src/app/features/sessions/session-detail/session-detail.css create mode 100644 src/app/features/sessions/session-detail/session-detail.html create mode 100644 src/app/features/sessions/sessions-list/sessions-list.component.ts create mode 100644 src/app/features/sessions/sessions-list/sessions-list.css create mode 100644 src/app/features/sessions/sessions-list/sessions-list.html create mode 100644 src/app/features/sessions/telemetry-event-detail/telemetry-event-detail.component.ts create mode 100644 src/app/features/sessions/telemetry-event-detail/telemetry-event-detail.css create mode 100644 src/app/features/sessions/telemetry-event-detail/telemetry-event-detail.html create mode 100644 src/app/shared/utils/date-time.util.ts create mode 100644 src/environments/environment.prod.ts create mode 100644 src/environments/environment.ts create mode 100644 src/styles/color-tokens.css create mode 100644 src/styles/session-status-chips.css create mode 100644 src/styles/sg-input-fields.css diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2b99525 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# Скопируйте в `.env` и при необходимости измените. +# Клиентский бандл: после правок выполните `npm run env:sync`. + +# База API (путь относительно текущего origin в браузере, для file:// — склеивается с SG_API_FALLBACK_ORIGIN) +SG_API_BASE_PATH=/api/v1 + +# Origin для разрешения относительных URL (HLS playlist и т.д.) при открытии приложения как file:// или вне основного хоста +SG_API_FALLBACK_ORIGIN=https://sparkguardian.ru + +# Только dev-сервер (`ng serve`): куда проксировать `/api/**` +SG_DEV_PROXY_TARGET=http://sparkguardian.ru:8080 diff --git a/.gitignore b/.gitignore index 854acd5..8e27dd1 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,9 @@ yarn-error.log !.vscode/mcp.json .history/* +# Local env (see .env.example) +.env + # Miscellaneous /.angular/cache .sass-cache/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f762f44 --- /dev/null +++ b/Makefile @@ -0,0 +1,40 @@ +.DEFAULT_GOAL := help + +.PHONY: help install env-sync start serve build build-dev watch test clean + +help: + @echo "SparkGuardian" + @echo " make install Установить зависимости (npm install)" + @echo " make env-sync Сгенерировать src/environments/environment.ts из .env" + @echo " make start Dev-сервер (ng serve + прокси, перед стартом — env:sync)" + @echo " make serve то же, что start" + @echo " make build Production-сборка (перед сборкой — env:sync)" + @echo " make build-dev Сборка в режиме development" + @echo " make watch ng build --watch (development)" + @echo " make test Unit-тесты (ng test)" + @echo " make clean Удалить dist, out-tsc, кэш Angular" + +install: + npm install + +env-sync: + npm run env:sync + +start serve: + npm start + +build: + npm run build + +build-dev: + npm run env:sync + npx ng build --configuration development + +watch: + npm run watch + +test: + npm test + +clean: + rm -rf dist out-tsc .angular/cache diff --git a/angular.json b/angular.json index 1627544..cf454a8 100644 --- a/angular.json +++ b/angular.json @@ -22,14 +22,27 @@ { "glob": "**/*", "input": "public" + }, + { + "glob": "**/*", + "input": "node_modules/@taiga-ui/icons/src", + "output": "assets/taiga-ui/icons" } ], "styles": [ + "node_modules/@taiga-ui/styles/taiga-ui-theme.less", + "node_modules/@taiga-ui/styles/taiga-ui-fonts.less", "src/styles.css" ] }, "configurations": { "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], "budgets": [ { "type": "initial", @@ -54,12 +67,16 @@ }, "serve": { "builder": "@angular/build:dev-server", + "options": { + "proxyConfig": "proxy.conf.cjs" + }, "configurations": { "production": { "buildTarget": "sparkguardian:build:production" }, "development": { - "buildTarget": "sparkguardian:build:development" + "buildTarget": "sparkguardian:build:development", + "proxyConfig": "proxy.conf.cjs" } }, "defaultConfiguration": "development" diff --git a/docs/doc_v1.json b/docs/doc_v1.json index 9faa529..9793aa7 100644 --- a/docs/doc_v1.json +++ b/docs/doc_v1.json @@ -162,6 +162,64 @@ } } }, + "/sessions/{id}/events": { + "get": { + "security": [ + { + "ForwardAuth": [] + } + ], + "description": "Downloads encrypted .bin chunks from S3, decrypts, and returns parsed events with optional time range filtering.", + "produces": [ + "application/json" + ], + "tags": [ + "telemetry" + ], + "summary": "Get parsed telemetry events", + "parameters": [ + { + "type": "integer", + "description": "Session ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Start timestamp (Unix ms)", + "name": "from", + "in": "query" + }, + { + "type": "integer", + "description": "End timestamp (Unix ms)", + "name": "to", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.ParsedEventsResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + } + } + } + }, "/sessions/{id}/playlist/{stream_type}.m3u8": { "get": { "security": [ @@ -448,6 +506,44 @@ } } }, + "handler.ParsedEvent": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "integer" + } + }, + "event_type": { + "type": "string", + "example": "keyboard" + }, + "timestamp": { + "type": "integer", + "example": 1711360200000 + } + } + }, + "handler.ParsedEventsResponse": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "example": 42 + }, + "events": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.ParsedEvent" + } + }, + "session_id": { + "type": "integer", + "example": 1 + } + } + }, "handler.SessionDetailResponse": { "type": "object", "properties": { diff --git a/package-lock.json b/package-lock.json index 23a6a6e..14b32bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,12 @@ "@angular/forms": "^21.2.0", "@angular/platform-browser": "^21.2.0", "@angular/router": "^21.2.0", + "@taiga-ui/cdk": "^5.1.0", + "@taiga-ui/core": "^5.1.0", + "@taiga-ui/icons": "^5.1.0", + "@taiga-ui/kit": "^5.1.0", + "@taiga-ui/styles": "^5.1.0", + "hls.js": "^1.6.15", "rxjs": "~7.8.0", "tslib": "^2.3.0" }, @@ -21,7 +27,9 @@ "@angular/build": "^21.2.6", "@angular/cli": "^21.2.6", "@angular/compiler-cli": "^21.2.0", + "dotenv": "^16.4.7", "jsdom": "^28.0.0", + "less": "^4.6.4", "prettier": "^3.8.1", "typescript": "~5.9.2", "vitest": "^4.0.8" @@ -280,8 +288,9 @@ "version": "21.2.6", "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.6.tgz", "integrity": "sha512-u5gPTAY7MC02uACQE39xxiFcm1hslF+ih/f2borMWnhER0JNTpHjLiLRXFkq7or7+VVHU30zfhK4XNAuO4WTIg==", - "dev": true, + "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "ajv": "8.18.0", "ajv-formats": "3.0.1", @@ -308,8 +317,9 @@ "version": "21.2.6", "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.6.tgz", "integrity": "sha512-hk2duJlPJyiMaI9MVWA5XpmlpD9C4n8qgquV/MJ7/n+ZRSwW3w1ndL5qUmA1ki+4Da54v/Rc8Wt5tUS955+93w==", - "dev": true, + "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@angular-devkit/core": "21.2.6", "jsonc-parser": "3.3.1", @@ -423,6 +433,23 @@ } } }, + "node_modules/@angular/cdk": { + "version": "21.2.5", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.2.5.tgz", + "integrity": "sha512-F1sVqMAGYoiJNYYaR2cerqTo7IqpxQ3ZtMDxR3rtB0rSSd5UPOIQoqpsfSd6uH8FVnuzKaBII8Mg6YrjClFsng==", + "license": "MIT", + "peer": true, + "dependencies": { + "parse5": "^8.0.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^21.0.0 || ^22.0.0", + "@angular/core": "^21.0.0 || ^22.0.0", + "@angular/platform-browser": "^21.0.0 || ^22.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/@angular/cli": { "version": "21.2.6", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.6.tgz", @@ -553,6 +580,7 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.7.tgz", "integrity": "sha512-YD/h07cdEeAUs41ysTk6820T0lG/XiQmFiq02d3IsiHYI5Vaj2pg9Ti1wWZYEBM//hVAPTzV0dwdV7Q1Gxju1w==", "license": "MIT", + "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "tslib": "^2.3.0" @@ -595,6 +623,7 @@ "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.7.tgz", "integrity": "sha512-Ina6XgtpvXT1OsLAomURHJGQDOkIVGrguWAOZ7+gOjsJEjUfpxTktFter+/K59KMC2yv6yneLvYSn3AswTYx7A==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -686,6 +715,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1103,31 +1133,6 @@ "node": ">=20.19.0" } }, - "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -2040,7 +2045,7 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -2169,6 +2174,50 @@ "win32" ] }, + "node_modules/@maskito/angular": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@maskito/angular/-/angular-5.2.2.tgz", + "integrity": "sha512-89J1JQE2vdmv49M/8Eow4eNZbRE6io8teM9TXplsHKurU7qnFNp8tZPY7MGsk+xbZRaL0iil102ieYJENu36Pg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "2.8.1" + }, + "peerDependencies": { + "@angular/core": ">=19.0.0", + "@angular/forms": ">=19.0.0", + "@maskito/core": "^5.2.2" + } + }, + "node_modules/@maskito/core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@maskito/core/-/core-5.2.2.tgz", + "integrity": "sha512-OFLK9NMQrz4eCgNgJ74S4x6IklqGQ71pcQToNGRzkIYnlEZbGpO+pzLREKLN/iKh4oBz8PI/9JnD3HtkN5Rl+w==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@maskito/kit": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@maskito/kit/-/kit-5.2.2.tgz", + "integrity": "sha512-ACuhzZ0EmMPBqi+RMbAqVPTMpQuhYVSBTLgBlyQppha1DurC4fN1fXL8MzHqbJbKiCbG2WELpl4mgyTgMcACIg==", + "license": "Apache-2.0", + "peer": true, + "peerDependencies": { + "@maskito/core": "^5.2.2" + } + }, + "node_modules/@maskito/phone": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@maskito/phone/-/phone-5.2.2.tgz", + "integrity": "sha512-OSldrjrK8nIb+8XfHVJ4KqhjInzw/mk8w/5YpIrroF9o85Usqyo22XlIwYmh/Sr9NWIuuKg2FwG/E2HAw2M7GA==", + "license": "Apache-2.0", + "peer": true, + "peerDependencies": { + "@maskito/core": "^5.2.2", + "@maskito/kit": "^5.2.2", + "libphonenumber-js": ">=1.0.0" + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.26.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", @@ -2636,6 +2685,127 @@ "@emnapi/runtime": "^1.7.1" } }, + "node_modules/@ng-web-apis/common": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@ng-web-apis/common/-/common-5.2.0.tgz", + "integrity": "sha512-vB6axKncJP33izmcC34EjWjzqfmGv2/PEbQLdVWU88jVCE3WWO9kOfyqVSMNCaLxu3ZC6qZRzZdIgmg4xe316A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=19.0.0", + "@angular/core": ">=19.0.0", + "@types/dom-speech-recognition": "^0.0.8", + "rxjs": ">=7.0.0" + } + }, + "node_modules/@ng-web-apis/intersection-observer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@ng-web-apis/intersection-observer/-/intersection-observer-5.2.0.tgz", + "integrity": "sha512-Y03IirbIVvUli5v41WlnFJEARF5svAkvo7hmVtVlCiK3yQdVlxiYRc/Ix0BooGTuhgTBv6L58ehInveLKyzLqg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=19.0.0", + "@ng-web-apis/common": "^5.2.0" + } + }, + "node_modules/@ng-web-apis/mutation-observer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@ng-web-apis/mutation-observer/-/mutation-observer-5.2.0.tgz", + "integrity": "sha512-J82Q2/+RYwBZQU9JhbAbKE7p6MmWLQr/DPM59drYfyfMH0TvfMfxKtiYc+Reru3ByCTqqNNAIQ/w3c+11gDYcA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=19.0.0", + "@ng-web-apis/common": "^5.2.0" + } + }, + "node_modules/@ng-web-apis/platform": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@ng-web-apis/platform/-/platform-5.2.0.tgz", + "integrity": "sha512-OMK9trN5Sc9IM8/5wnyNbFsjj0OPt3uC3Vsw4+tAiia7/FPDihJQWkd8VB9R+F/Xe7fOzqWCDC+ZnYADMp3vNw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + } + }, + "node_modules/@ng-web-apis/resize-observer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@ng-web-apis/resize-observer/-/resize-observer-5.2.0.tgz", + "integrity": "sha512-lRX6vIRGjQ5+wIT8voOiIhEck+4YSe8dhsRkEHLLrqjNQS8uQXcuDyG4kSGMLcHoExirYbRM0ATv6k1F6MXLOA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=19.0.0", + "@ng-web-apis/common": "^5.2.0" + } + }, + "node_modules/@ng-web-apis/screen-orientation": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@ng-web-apis/screen-orientation/-/screen-orientation-5.2.0.tgz", + "integrity": "sha512-a7x7QFVQL8zESG47YRmwAHCaNrurXqPbNwsxoSL05VREi+u4p3/Sfe0WlzC7SwUkHJLqZf4RzzJuCuRa4QuI4g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=19.0.0", + "@ng-web-apis/common": "^5.2.0", + "rxjs": ">=7.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "optional": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@npmcli/agent": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", @@ -3754,7 +3924,7 @@ "version": "21.2.6", "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.6.tgz", "integrity": "sha512-KpLD8R2S762jbLdNEepE+b7KjhVOKPFHHdgNqhPv0NiGLdsvXSOx1e63JvFacoCZdmP7n3/gwmyT/utcVvnsag==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@angular-devkit/core": "21.2.6", @@ -3853,6 +4023,245 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, + "node_modules/@taiga-ui/cdk": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-5.1.0.tgz", + "integrity": "sha512-wzf988cc5f6MTBsiKcLPZA2c8XwtEOAV9pBHjCN8tykjBMC600jVdczZHZdWY2MiqVHVfySnomlYkZM62IQmQg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.8.1" + }, + "optionalDependencies": { + "@angular-devkit/core": ">=19.0.0", + "@angular-devkit/schematics": ">=19.0.0", + "@schematics/angular": ">=19.0.0", + "ng-morph": "^4.8.4", + "parse5": "^7.3.0" + }, + "peerDependencies": { + "@angular/cdk": ">=19.0.0", + "@angular/common": ">=19.0.0", + "@angular/core": ">=19.0.0", + "@angular/forms": ">=19.0.0", + "@ng-web-apis/common": "^5.2.0", + "@ng-web-apis/mutation-observer": "^5.2.0", + "@ng-web-apis/platform": "^5.2.0", + "@ng-web-apis/resize-observer": "^5.2.0", + "@ng-web-apis/screen-orientation": "^5.2.0", + "@taiga-ui/event-plugins": "^5.0.0", + "@taiga-ui/font-watcher": "~0.5.0", + "@taiga-ui/polymorpheus": "^5.0.0", + "rxjs": ">=7.0.0" + } + }, + "node_modules/@taiga-ui/cdk/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@taiga-ui/cdk/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "optional": true, + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/@taiga-ui/core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-5.1.0.tgz", + "integrity": "sha512-thDWcJuDsgO9eItpa0KIplDSofmNnKSpSWflpzRst3DhIH1O+wfYBeebHk2keugOKU+bF+beX9Um4q38I89HvA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": ">=2.8.1" + }, + "peerDependencies": { + "@angular/common": ">=19.0.0", + "@angular/core": ">=19.0.0", + "@angular/forms": ">=19.0.0", + "@angular/platform-browser": ">=19.0.0", + "@angular/router": ">=19.0.0", + "@ng-web-apis/common": "^5.2.0", + "@ng-web-apis/mutation-observer": "^5.2.0", + "@ng-web-apis/platform": "^5.2.0", + "@taiga-ui/cdk": "5.1.0", + "@taiga-ui/event-plugins": "^5.0.0", + "@taiga-ui/i18n": "5.1.0", + "@taiga-ui/polymorpheus": "^5.0.0", + "@taiga-ui/styles": "5.1.0", + "rxjs": ">=7.0.0" + } + }, + "node_modules/@taiga-ui/design-tokens": { + "version": "0.291.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/design-tokens/-/design-tokens-0.291.0.tgz", + "integrity": "sha512-oGLjOh/UXgJRRnz65Wp6yeUNl0ox+IWMdjKDJ6GKUKVr4TnJILDqN+hsGKJcQ+cctjRa+34QxGZO7AUtbAY1oA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + } + }, + "node_modules/@taiga-ui/event-plugins": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/event-plugins/-/event-plugins-5.0.0.tgz", + "integrity": "sha512-8AxIUn/lX4kwuDlIFmhElgBNtGDgQVC9/F+3O2A29IcMSt9693KRi8qKwToe9p3UuUsT9nnj5YeE11BtJMG0wA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=16.0.0", + "@angular/platform-browser": ">=16.0.0", + "rxjs": ">=7.0.0" + } + }, + "node_modules/@taiga-ui/font-watcher": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/font-watcher/-/font-watcher-0.5.0.tgz", + "integrity": "sha512-9QBUh3XT6KOS+pKVy4ggr0CxgiZtbDYlterUNcfXw8cswkIkiUl5VGDQdvmta60iPHqOrOd/Z/7aAUom35vX/g==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@taiga-ui/i18n": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-5.1.0.tgz", + "integrity": "sha512-NqCo1fK95w6aXHkvIZ3aqZOA2z+CnvD/eEEXZjbCs/Ik6QfWmGP8GbS5uwb7SzqNZz9QiVaJi5RNZtoInHf/tQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": ">=2.8.1" + }, + "peerDependencies": { + "@angular/core": ">=19.0.0", + "@ng-web-apis/common": "^5.2.0", + "rxjs": ">=7.0.0" + } + }, + "node_modules/@taiga-ui/icons": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-5.1.0.tgz", + "integrity": "sha512-iRWYdYjt5PRneUSfUgPms9IHG0WbXNU9kNi/JnvMFukV/qhkBTuuvZaOv43LnBom9u/Wdsqfmi6WzUVjaxYKJw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.3.0" + } + }, + "node_modules/@taiga-ui/kit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-5.1.0.tgz", + "integrity": "sha512-3k1asuOgEjDLAtkK/snO8anctfo4vmMsyr16NHcNVDRqvUeRDrWf7sdRD1uXloO0hTrJvVbKR3WnDWxnuTKm8Q==", + "license": "Apache-2.0", + "dependencies": { + "tslib": ">=2.8.1" + }, + "peerDependencies": { + "@angular/common": ">=19.0.0", + "@angular/core": ">=19.0.0", + "@angular/forms": ">=19.0.0", + "@angular/router": ">=19.0.0", + "@maskito/angular": "^5.2.2", + "@maskito/core": "^5.2.2", + "@maskito/kit": "^5.2.2", + "@maskito/phone": "^5.2.2", + "@ng-web-apis/common": "^5.2.0", + "@ng-web-apis/intersection-observer": "^5.2.0", + "@ng-web-apis/mutation-observer": "^5.2.0", + "@ng-web-apis/platform": "^5.2.0", + "@ng-web-apis/resize-observer": "^5.2.0", + "@taiga-ui/cdk": "5.1.0", + "@taiga-ui/core": "5.1.0", + "@taiga-ui/i18n": "5.1.0", + "@taiga-ui/polymorpheus": "^5.0.0", + "@taiga-ui/styles": "5.1.0", + "rxjs": ">=7.0.0" + } + }, + "node_modules/@taiga-ui/polymorpheus": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/polymorpheus/-/polymorpheus-5.0.0.tgz", + "integrity": "sha512-FRus7OgxYyRuETB17g1/YXYs8PAeF4x8+K4ZDfD3Ede8Vxv/XslAYZvf/Ro0wag6Uiy0kAP4bvSaIGPS1Y8Fyg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.8.1" + }, + "peerDependencies": { + "@angular/core": ">=19.0.0", + "@angular/platform-browser": ">=19.0.0" + } + }, + "node_modules/@taiga-ui/styles": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/styles/-/styles-5.1.0.tgz", + "integrity": "sha512-wWAcSm87aA0tBRcn6BUt7nVShMZBJNsOFG7QE8T7mIhG4Ss+7FCBSbbDNksWym8l69/LMIZGeAU0tc+rRSFXnw==", + "license": "Apache-2.0", + "peer": true, + "peerDependencies": { + "@taiga-ui/design-tokens": "~0.291.0" + } + }, + "node_modules/@ts-morph/common": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.24.0.tgz", + "integrity": "sha512-c1xMmNHWpNselmpIqursHeOHHBTIsJLbB+NuovbTTRCNiTLEr/U9dbJ8qy0jd/O2x5pc3seWuOUN5R2IoOTp8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "fast-glob": "^3.3.2", + "minimatch": "^9.0.4", + "mkdirp": "^3.0.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@ts-morph/common/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT", + "optional": true + }, + "node_modules/@ts-morph/common/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@ts-morph/common/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@tufjs/canonical-json": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", @@ -3906,6 +4315,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/dom-speech-recognition": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.8.tgz", + "integrity": "sha512-JgpjlGQWGc+OjjTNfyJ8Ic2w2+NOdrmEpmdVAkg+sYblx8I5ZZTeA+cfxvgUMtXXT7/WzYYR2Rrp3ZDg+l/3vA==", + "license": "MIT", + "peer": true + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3913,6 +4329,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "license": "MIT", + "optional": true + }, "node_modules/@vitejs/plugin-basic-ssl": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz", @@ -4091,7 +4514,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -4108,7 +4531,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -4168,7 +4591,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -4190,6 +4613,36 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -4299,6 +4752,19 @@ "node": "18 || 20 || >=22" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "optional": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", @@ -4449,7 +4915,7 @@ "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -4496,7 +4962,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "restore-cursor": "^5.0.0" @@ -4512,7 +4978,7 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18.20" @@ -4599,6 +5065,13 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/code-block-writer": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "license": "MIT", + "optional": true + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4626,6 +5099,13 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT", + "optional": true + }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -4677,6 +5157,22 @@ "node": ">=6.6.0" } }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -4733,7 +5229,6 @@ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" @@ -4900,6 +5395,19 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4989,6 +5497,20 @@ "dev": true, "license": "MIT" }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -5223,14 +5745,31 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -5243,6 +5782,16 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "optional": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -5261,6 +5810,19 @@ } } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "optional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -5365,7 +5927,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -5431,6 +5993,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "optional": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -5484,6 +6059,12 @@ "node": ">= 0.4" } }, + "node_modules/hls.js": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz", + "integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==", + "license": "Apache-2.0" + }, "node_modules/hono": { "version": "4.12.11", "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.11.tgz", @@ -5650,6 +6231,20 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/immutable": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", @@ -5698,7 +6293,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -5725,7 +6319,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -5739,7 +6332,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -5748,6 +6341,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -5766,7 +6369,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -5775,6 +6378,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5895,7 +6511,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/json-schema-typed": { @@ -5922,7 +6538,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/jsonparse": { @@ -5935,12 +6551,58 @@ ], "license": "MIT" }, + "node_modules/less": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/less/-/less-4.6.4.tgz", + "integrity": "sha512-OJmO5+HxZLLw0RLzkqaNHzcgEAQG7C0y3aMbwtCzIUFZsLMNNq/1IdAdHEycQ58CwUO3jPTHmoN+tE5I7FQxNg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "copy-anything": "^3.0.5", + "parse-node-version": "^1.0.1" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.12.41", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.41.tgz", + "integrity": "sha512-lsmMmGXBxXIK/VMLEj0kL6MtUs1kBGj1nTCzi6zgQoG1DEwqwt2DQyHxcLykceIxAnfE3hya7NuIh6PpC6S3fA==", + "license": "MIT", + "peer": true + }, "node_modules/listr2": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", @@ -6022,7 +6684,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "is-unicode-supported": "^2.0.0", @@ -6122,12 +6784,38 @@ "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/make-fetch-happen": { "version": "15.0.5", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.5.tgz", @@ -6192,6 +6880,57 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "optional": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -6223,7 +6962,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -6381,6 +7120,22 @@ "node": ">= 18" } }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -6432,6 +7187,57 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" } }, + "node_modules/multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/multimatch/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT", + "optional": true + }, + "node_modules/multimatch/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/multimatch/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/mute-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", @@ -6461,6 +7267,38 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/needle": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.5.0.tgz", + "integrity": "sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -6471,6 +7309,57 @@ "node": ">= 0.6" } }, + "node_modules/ng-morph": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/ng-morph/-/ng-morph-4.8.4.tgz", + "integrity": "sha512-XwL53wCOhyaAxvoekN74ONbWUK30huzp+GpZYyC01RfaG2AX9l7YlC1mGG/l7Rx7YXtFAk85VFnNJqn2e46K8g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "jsonc-parser": "3.3.1", + "minimatch": "10.0.1", + "multimatch": "5.0.0", + "ts-morph": "23.0.0" + }, + "peerDependencies": { + "@angular-devkit/core": ">=16.0.0", + "@angular-devkit/schematics": ">=16.0.0", + "tslib": "^2.7.0" + } + }, + "node_modules/ng-morph/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT", + "optional": true + }, + "node_modules/ng-morph/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ng-morph/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/node-addon-api": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", @@ -6745,7 +7634,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "mimic-function": "^5.0.0" @@ -6761,7 +7650,7 @@ "version": "9.3.0", "resolved": "https://registry.npmjs.org/ora/-/ora-9.3.0.tgz", "integrity": "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "chalk": "^5.6.2", @@ -6833,11 +7722,20 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/parse5": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", - "dev": true, "license": "MIT", "dependencies": { "entities": "^6.0.0" @@ -6891,7 +7789,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -6910,6 +7807,13 @@ "node": ">= 0.8" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT", + "optional": true + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -6976,7 +7880,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -6985,6 +7889,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/piscina": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.1.4.tgz", @@ -7126,6 +8041,14 @@ "node": ">= 0.10" } }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -7152,6 +8075,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -7203,7 +8147,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7213,7 +8157,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "onetime": "^7.0.0", @@ -7236,6 +8180,17 @@ "node": ">= 4" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "optional": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -7337,6 +8292,30 @@ "node": ">= 18" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -7405,6 +8384,17 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -7595,7 +8585,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, + "devOptional": true, "license": "ISC", "engines": { "node": ">=14" @@ -7684,7 +8674,7 @@ "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">= 12" @@ -7787,7 +8777,7 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.1.tgz", "integrity": "sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -7800,7 +8790,7 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "get-east-asian-width": "^1.5.0", @@ -7817,7 +8807,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.2.2" @@ -7927,6 +8917,19 @@ "dev": true, "license": "MIT" }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -7963,6 +8966,17 @@ "node": ">=20" } }, + "node_modules/ts-morph": { + "version": "23.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-23.0.0.tgz", + "integrity": "sha512-FcvFx7a9E8TUe6T3ShihXJLiJOiqyafzFKUO4aqIHDUCIvADdGNShcbc2W5PMr3LerXRv7mafvFZ9lRENxJmug==", + "license": "MIT", + "optional": true, + "dependencies": { + "@ts-morph/common": "~0.24.0", + "code-block-writer": "^13.0.1" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -8525,7 +9539,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" diff --git a/package.json b/package.json index 8d1669f..0109c7b 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,12 @@ "version": "0.0.0", "scripts": { "ng": "ng", - "start": "ng serve", + "env:sync": "node scripts/sync-env.cjs", + "prestart": "npm run env:sync", + "prebuild": "npm run env:sync", + "start": "ng serve --proxy-config proxy.conf.cjs", "build": "ng build", + "prewatch": "npm run env:sync", "watch": "ng build --watch --configuration development", "test": "ng test" }, @@ -17,16 +21,24 @@ "@angular/forms": "^21.2.0", "@angular/platform-browser": "^21.2.0", "@angular/router": "^21.2.0", + "@taiga-ui/cdk": "^5.1.0", + "@taiga-ui/core": "^5.1.0", + "@taiga-ui/icons": "^5.1.0", + "@taiga-ui/kit": "^5.1.0", + "@taiga-ui/styles": "^5.1.0", + "hls.js": "^1.6.15", "rxjs": "~7.8.0", "tslib": "^2.3.0" }, "devDependencies": { + "dotenv": "^16.4.7", "@angular/build": "^21.2.6", "@angular/cli": "^21.2.6", "@angular/compiler-cli": "^21.2.0", "jsdom": "^28.0.0", + "less": "^4.6.4", "prettier": "^3.8.1", "typescript": "~5.9.2", "vitest": "^4.0.8" } -} \ No newline at end of file +} diff --git a/proxy.conf.cjs b/proxy.conf.cjs new file mode 100644 index 0000000..dc28275 --- /dev/null +++ b/proxy.conf.cjs @@ -0,0 +1,15 @@ +/** + * Прокси для `ng serve`. Целевой backend задаётся в `.env` (SG_DEV_PROXY_TARGET). + */ +const path = require('path'); +require('dotenv').config({ path: path.join(__dirname, '.env') }); + +const target = process.env.SG_DEV_PROXY_TARGET || 'http://sparkguardian.ru:8080'; + +module.exports = { + '/api/**': { + target, + secure: false, + changeOrigin: true, + }, +}; diff --git a/public/KB_USA-standard.svg b/public/KB_USA-standard.svg new file mode 100644 index 0000000..0523350 --- /dev/null +++ b/public/KB_USA-standard.svg @@ -0,0 +1,398 @@ + + + + + + + + + + + image/svg+xml + + + Meta (⌘) and Menu: Lucide icons (https://lucide.dev), ISC License, © Lucide Contributors. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Q + W + E + R + T + Y + U + I + O + P + + A + S + D + F + G + H + J + K + L + + Z + X + C + V + B + N + M + + + + + ` + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 + - + = + + [ + ] + \ + + ; + ' + + , + . + / + + + + ~ + ! + @ + # + $ + % + ^ + & + + ( + ) + _ + + + + { + } + | + + : + " + + < + > + ? + + + + Ctrl + Alt + Alt + Ctrl + + + + + + delete + + tab + + return + + caps lock + + shift + + shift + + + + diff --git a/public/fonts/TinkoffSans-Bold.ttf b/public/fonts/TinkoffSans-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e8eb528eb7fb44b61182fec3880a97e4b8069519 GIT binary patch literal 70888 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 literal 0 HcmV?d00001 diff --git a/public/fonts/TinkoffSans-Regular.ttf b/public/fonts/TinkoffSans-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..13963b9d89c6f5addc015df6e88127ba3a5d48aa GIT binary patch literal 70240 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 RH$MIQ+q%v0SNDeJ{}+t)C$#_o literal 0 HcmV?d00001 diff --git a/scripts/sync-env.cjs b/scripts/sync-env.cjs new file mode 100644 index 0000000..c0da5a2 --- /dev/null +++ b/scripts/sync-env.cjs @@ -0,0 +1,33 @@ +/** + * Генерирует `src/environments/environment.ts` из переменных окружения (.env). + * Значения по умолчанию совпадают с .env.example. + */ +const fs = require('fs'); +const path = require('path'); + +require('dotenv').config({ path: path.join(__dirname, '..', '.env') }); + +const defaults = { + SG_API_FALLBACK_ORIGIN: 'https://sparkguardian.ru', + SG_API_BASE_PATH: '/api/v1', +}; + +function val(key) { + const v = process.env[key]; + if (v !== undefined && v !== '') { + return v; + } + return defaults[key]; +} + +const content = `// Сгенерировано npm run env:sync — правьте .env и снова запустите sync +export const environment = { + production: false, + apiFallbackOrigin: ${JSON.stringify(val('SG_API_FALLBACK_ORIGIN'))}, + apiBasePath: ${JSON.stringify(val('SG_API_BASE_PATH'))}, +} as const; +`; + +const outPath = path.join(__dirname, '..', 'src', 'environments', 'environment.ts'); +fs.writeFileSync(outPath, content, 'utf8'); +console.log('env:sync →', outPath); diff --git a/src/app/app.config.ts b/src/app/app.config.ts index cb1270e..c73f80d 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,11 +1,22 @@ +import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; import { provideRouter } from '@angular/router'; +import { provideTaiga } from '@taiga-ui/core'; +import { tuiNotificationOptionsProvider } from '@taiga-ui/core/components/notification'; +import { apiBaseUrlInterceptor } from './core/http/api-base-url.interceptor'; +import { devLogInterceptor } from './core/http/dev-log.interceptor'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ provideBrowserGlobalErrorListeners(), - provideRouter(routes) - ] + provideRouter(routes), + provideTaiga(), + tuiNotificationOptionsProvider(() => ({ + block: 'start', + inline: 'end', + })), + provideHttpClient(withInterceptors([apiBaseUrlInterceptor, devLogInterceptor])), + ], }; diff --git a/src/app/app.css b/src/app/app.css index e69de29..788b826 100644 --- a/src/app/app.css +++ b/src/app/app.css @@ -0,0 +1,40 @@ +:host { + display: block; + min-height: 100%; +} + +.shell { + min-height: 100dvh; + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +.shell-header { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 0.75rem; + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--tui-border-normal); + background: var(--tui-background-elevation-1); +} + +.brand { + font: var(--tui-font-heading-5); + color: var(--tui-text-primary); + text-decoration: none; +} + +.brand:hover { + color: var(--tui-text-action); +} + +.shell-sub { + font: var(--tui-font-text-s); + color: var(--sg-color-subtitle); +} + +.shell-main { + flex: 1; +} diff --git a/src/app/app.html b/src/app/app.html index a1c4296..f789a01 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -1,344 +1,14 @@ - - - - - - - - - - - -
-
- - - - - - - - - - - + @if (isDev) { + + } + diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index dc39edb..2c3d309 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,3 +1,17 @@ import { Routes } from '@angular/router'; -export const routes: Routes = []; +export const routes: Routes = [ + { + path: '', + loadComponent: () => + import('./features/sessions/sessions-list/sessions-list.component').then((m) => m.SessionsListComponent), + }, + { + path: 'sessions/:id', + loadComponent: () => + import('./features/sessions/session-detail/session-detail.component').then( + (m) => m.SessionDetailComponent, + ), + }, + { path: '**', redirectTo: '' }, +]; diff --git a/src/app/app.spec.ts b/src/app/app.spec.ts index 5de2561..5e1837f 100644 --- a/src/app/app.spec.ts +++ b/src/app/app.spec.ts @@ -1,10 +1,26 @@ import { TestBed } from '@angular/core/testing'; import { App } from './app'; +import { appConfig } from './app.config'; describe('App', () => { beforeEach(async () => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: (query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: () => {}, + removeListener: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false, + }), + }); + await TestBed.configureTestingModule({ imports: [App], + providers: [...appConfig.providers], }).compileComponents(); }); @@ -14,10 +30,10 @@ describe('App', () => { expect(app).toBeTruthy(); }); - it('should render title', async () => { + it('should show app name in header', async () => { const fixture = TestBed.createComponent(App); await fixture.whenStable(); const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('h1')?.textContent).toContain('Hello, sparkguardian'); + expect(compiled.querySelector('.brand')?.textContent).toContain('SparkGuardian'); }); }); diff --git a/src/app/app.ts b/src/app/app.ts index 1188fce..91a460a 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,12 +1,14 @@ -import { Component, signal } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; +import { TuiRoot } from '@taiga-ui/core'; +import { Component, isDevMode } from '@angular/core'; +import { RouterLink, RouterOutlet } from '@angular/router'; +import { DevConsoleComponent } from './features/devtools/dev-console/dev-console.component'; @Component({ selector: 'app-root', - imports: [RouterOutlet], + imports: [RouterLink, RouterOutlet, TuiRoot, DevConsoleComponent], templateUrl: './app.html', - styleUrl: './app.css' + styleUrl: './app.css', }) export class App { - protected readonly title = signal('sparkguardian'); + protected readonly isDev = isDevMode(); } diff --git a/src/app/core/config/api.tokens.ts b/src/app/core/config/api.tokens.ts new file mode 100644 index 0000000..0ec98b5 --- /dev/null +++ b/src/app/core/config/api.tokens.ts @@ -0,0 +1,34 @@ +import { DOCUMENT } from '@angular/common'; +import { inject, InjectionToken } from '@angular/core'; + +import { environment } from '../../../environments/environment'; + +/** + * Origin для разрешения относительных `playlist_url` (HLS). + * При `ng serve` — origin dev-сервера; API проксируется (см. `proxy.conf.cjs`, `SG_DEV_PROXY_TARGET` в `.env`). + */ +export const API_ORIGIN = new InjectionToken('API_ORIGIN', { + factory: () => { + const doc = inject(DOCUMENT); + const loc = doc.defaultView?.location; + if (!loc || loc.protocol === 'file:') { + return environment.apiFallbackOrigin; + } + return loc.origin; + }, +}); + +/** + * Базовый путь API. Относительный путь работает в dev (прокси) и при деплое на тот же домен. + * Для `file://` — абсолютный URL из `environment`. + */ +export const API_BASE_URL = new InjectionToken('API_BASE_URL', { + factory: () => { + const doc = inject(DOCUMENT); + const loc = doc.defaultView?.location; + if (loc?.protocol === 'file:') { + return `${environment.apiFallbackOrigin}${environment.apiBasePath}`; + } + return environment.apiBasePath; + }, +}); diff --git a/src/app/core/devtools/dev-log.service.ts b/src/app/core/devtools/dev-log.service.ts new file mode 100644 index 0000000..9642894 --- /dev/null +++ b/src/app/core/devtools/dev-log.service.ts @@ -0,0 +1,52 @@ +import { Injectable, signal } from '@angular/core'; + +export type DevLogLevel = 'info' | 'warn' | 'error'; +export type DevLogStatus = 'pending' | 'ok' | 'error'; + +export interface DevHttpLogDetails { + method: string; + url: string; + requestHeaders?: Record; + requestBody?: unknown; + statusCode?: number; + durationMs?: number; + responseHeaders?: Record; + responseBody?: unknown; + error?: string; +} + +export interface DevLogEntry { + id: number; + time: string; + level: DevLogLevel; + source: 'http' | 'system'; + message: string; + status?: DevLogStatus; + details?: DevHttpLogDetails; +} + +@Injectable({ providedIn: 'root' }) +export class DevLogService { + private seq = 0; + private readonly max = 300; + readonly entries = signal([]); + + add(entry: Omit): void { + const withMeta: DevLogEntry = { + id: ++this.seq, + time: new Date().toISOString(), + ...entry, + }; + this.entries.update((curr) => [...curr.slice(-(this.max - 1)), withMeta]); + } + + update(id: number, patch: Partial>): void { + this.entries.update((curr) => + curr.map((item) => (item.id === id ? { ...item, ...patch } : item)), + ); + } + + clear(): void { + this.entries.set([]); + } +} diff --git a/src/app/core/http/api-base-url.interceptor.ts b/src/app/core/http/api-base-url.interceptor.ts new file mode 100644 index 0000000..7e5aff3 --- /dev/null +++ b/src/app/core/http/api-base-url.interceptor.ts @@ -0,0 +1,17 @@ +import { HttpInterceptorFn } from '@angular/common/http'; +import { inject } from '@angular/core'; + +import { API_BASE_URL } from '../config/api.tokens'; + +/** + * Подставляет базовый URL ко всем относительным запросам. + * Абсолютные URL (`http…`) не трогаем — как в Axios с `baseURL`, только через перехватчик. + */ +export const apiBaseUrlInterceptor: HttpInterceptorFn = (req, next) => { + const base = inject(API_BASE_URL); + if (/^https?:\/\//i.test(req.url)) { + return next(req); + } + const path = req.url.startsWith('/') ? req.url : `/${req.url}`; + return next(req.clone({ url: `${base}${path}` })); +}; diff --git a/src/app/core/http/dev-log.interceptor.ts b/src/app/core/http/dev-log.interceptor.ts new file mode 100644 index 0000000..f84a22b --- /dev/null +++ b/src/app/core/http/dev-log.interceptor.ts @@ -0,0 +1,117 @@ +import { + HttpErrorResponse, + HttpEvent, + HttpHeaders, + HttpInterceptorFn, + HttpResponse, +} from '@angular/common/http'; +import { inject, isDevMode } from '@angular/core'; +import { tap } from 'rxjs'; + +import { httpErrorMessage } from './http-error.util'; +import { DevLogService } from '../devtools/dev-log.service'; + +function headersToObject(headers: HttpHeaders): Record { + const out: Record = {}; + for (const key of headers.keys()) { + out[key] = headers.get(key); + } + return out; +} + +function normalizeBody(value: unknown): unknown { + if (value === undefined) { + return null; + } + if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return value; + } + if (value instanceof Blob) { + return `[Blob ${value.type || 'unknown'} ${value.size} bytes]`; + } + if (value instanceof FormData) { + const fields: Record = {}; + value.forEach((v, k) => { + fields[k] = typeof v === 'string' ? v : `[File ${(v as File).name}]`; + }); + return fields; + } + if (value instanceof ArrayBuffer) { + return `[ArrayBuffer ${value.byteLength} bytes]`; + } + return value; +} + +export const devLogInterceptor: HttpInterceptorFn = (req, next) => { + if (!isDevMode()) { + return next(req); + } + + const logs = inject(DevLogService); + const started = performance.now(); + logs.add({ + level: 'info', + source: 'http', + status: 'pending', + message: `→ ${req.method} ${req.urlWithParams} (pending)`, + details: { + method: req.method, + url: req.urlWithParams, + requestHeaders: headersToObject(req.headers), + requestBody: normalizeBody(req.body), + }, + }); + const entryId = logs.entries().at(-1)?.id; + + return next(req).pipe( + tap({ + next: (event: HttpEvent) => { + if (!(event instanceof HttpResponse)) { + return; + } + const ms = Math.round(performance.now() - started); + if (entryId) { + logs.update(entryId, { + level: 'info', + status: 'ok', + message: `✓ ${req.method} ${req.urlWithParams} [${event.status}] ${ms}ms`, + details: { + method: req.method, + url: req.urlWithParams, + requestHeaders: headersToObject(req.headers), + requestBody: normalizeBody(req.body), + statusCode: event.status, + durationMs: ms, + responseHeaders: headersToObject(event.headers), + responseBody: normalizeBody(event.body), + }, + }); + } + }, + error: (error: unknown) => { + const ms = Math.round(performance.now() - started); + const status = error instanceof HttpErrorResponse ? error.status : undefined; + if (entryId) { + logs.update(entryId, { + level: 'error', + status: 'error', + message: `✕ ${req.method} ${req.urlWithParams}${status ? ` [${status}]` : ''} ${ms}ms: ${httpErrorMessage(error)}`, + details: { + method: req.method, + url: req.urlWithParams, + requestHeaders: headersToObject(req.headers), + requestBody: normalizeBody(req.body), + statusCode: status, + durationMs: ms, + responseHeaders: + error instanceof HttpErrorResponse ? headersToObject(error.headers) : undefined, + responseBody: + error instanceof HttpErrorResponse ? normalizeBody(error.error) : undefined, + error: httpErrorMessage(error), + }, + }); + } + }, + }), + ); +}; diff --git a/src/app/core/http/error-classification.util.ts b/src/app/core/http/error-classification.util.ts new file mode 100644 index 0000000..726d016 --- /dev/null +++ b/src/app/core/http/error-classification.util.ts @@ -0,0 +1,67 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { TimeoutError } from 'rxjs'; + +import type { UserErrorKind } from '../notifications/user-error-messages.config'; + +function isParseFailureMessage(message: string | undefined): boolean { + return !!message?.includes('Http failure during parsing'); +} + +/** + * Определяет категорию ошибки для пользовательского сообщения (без утечки технических деталей). + */ +export function classifyUserError(err: unknown): UserErrorKind { + if (err instanceof TimeoutError) { + return 'timeout'; + } + + if (err instanceof HttpErrorResponse) { + if (isParseFailureMessage(err.message)) { + return 'parse_error'; + } + const s = err.status; + if (s === 0) { + return 'network'; + } + if (s === 408) { + return 'timeout'; + } + if (s === 401) { + return 'unauthorized'; + } + if (s === 403) { + return 'forbidden'; + } + if (s === 404) { + return 'not_found'; + } + if (s === 400) { + return 'bad_request'; + } + if (s >= 500) { + return 'server_error'; + } + if (s >= 400 && s < 500) { + return 'client_error'; + } + return 'unknown'; + } + + if (err instanceof Error) { + const m = err.message; + if (isParseFailureMessage(m)) { + return 'parse_error'; + } + if (/идентификатор|некорректн/i.test(m)) { + return 'invalid_input'; + } + if (/timeout|timed out/i.test(m)) { + return 'timeout'; + } + if (/network|failed to fetch|load failed|net::/i.test(m)) { + return 'network'; + } + } + + return 'unknown'; +} diff --git a/src/app/core/http/http-error.util.ts b/src/app/core/http/http-error.util.ts new file mode 100644 index 0000000..3f0fb23 --- /dev/null +++ b/src/app/core/http/http-error.util.ts @@ -0,0 +1,56 @@ +import { HttpErrorResponse } from '@angular/common/http'; + +import type { ApiErrorBody } from '../models/api.types'; + +/** Безопасная подстановка текста в разметку с innerHTML (например, Taiga notification). */ +export function escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function summarizeHtmlError(text: string): string | null { + const t = text.trim(); + if (!t.startsWith(']*>([\s\S]*?)<\/pre>/i); + if (pre?.[1]) { + return pre[1].trim(); + } + const title = t.match(/]*>([\s\S]*?)<\/title>/i); + if (title?.[1]) { + return title[1].trim(); + } + return 'Сервер вернул HTML вместо JSON (часто редирект на вход или ошибка прокси).'; +} + +export function httpErrorMessage(err: unknown): string { + if (err instanceof HttpErrorResponse) { + const body = err.error as ApiErrorBody | string | null | undefined; + if (body && typeof body === 'object' && typeof body.error === 'string') { + return body.error; + } + if (typeof body === 'string' && body.length > 0) { + const fromHtml = summarizeHtmlError(body); + if (fromHtml) { + return fromHtml; + } + return body.length > 200 ? `${body.slice(0, 200)}…` : body; + } + if (err.message?.includes('Http failure during parsing')) { + return 'Ответ не JSON (часто HTML страницы входа или ошибка прокси /api). Перезапустите `npm start` и проверьте proxy.conf.cjs и `SG_DEV_PROXY_TARGET` в `.env`.'; + } + return err.message || `HTTP ${err.status}`; + } + if (err instanceof Error) { + if (err.message.includes('Http failure during parsing')) { + return 'Ответ не JSON — проверьте авторизацию и прокси для API.'; + } + return err.message; + } + return 'Неизвестная ошибка'; +} diff --git a/src/app/core/keyboard/keyboard-key-name-map.ts b/src/app/core/keyboard/keyboard-key-name-map.ts new file mode 100644 index 0000000..c5c6106 --- /dev/null +++ b/src/app/core/keyboard/keyboard-key-name-map.ts @@ -0,0 +1,71 @@ +import { charKeyNameToSvgKeyId } from './vk-to-keyboard-svg-id'; + +/** + * Токены из `key_name` и из `modifiers` (через "+"), в нижнем регистре. + * Соответствие имён бэкенда → id прямоугольников в KB_USA-standard.svg. + */ +function tokenToSvgIds(token: string): string[] { + const t = token.trim().toLowerCase(); + if (!t) { + return []; + } + + const named: Record = { + shift: ['K_kb5a'], + meta: ['K_kb6b'], + super: ['K_kb6b'], + win: ['K_kb6b'], + windows: ['K_kb6b'], + control: ['K_kb6a'], + ctrl: ['K_kb6a'], + alt: ['K_kb6c'], + tab: ['K_kb3a'], + enter: ['K_kb4n'], + return: ['K_kb4n'], + backspace: ['K_kb2n'], + space: ['K_kb6d'], + caps: ['K_kb4a'], + menu: ['K_kb6m'], + }; + + if (named[t]) { + return named[t]!; + } + + const single = charKeyNameToSvgKeyId(t); + if (single) { + return [single]; + } + + return []; +} + +/** + * Формат бэкенда: `{ key_name, modifiers: "Shift+Meta", action }` и т.п. + * Собирает уникальные id клавиш для подсветки (модификаторы + основная клавиша). + */ +export function keyNameModifiersPayloadToSvgIds(o: Record): string[] { + const out: string[] = []; + const seen = new Set(); + + const add = (ids: string[]) => { + for (const id of ids) { + if (id && !seen.has(id)) { + seen.add(id); + out.push(id); + } + } + }; + + const modifiers = typeof o['modifiers'] === 'string' ? o['modifiers'] : ''; + const keyName = typeof o['key_name'] === 'string' ? o['key_name'] : ''; + + for (const part of modifiers.split('+').map((s) => s.trim()).filter(Boolean)) { + add(tokenToSvgIds(part)); + } + if (keyName.trim()) { + add(tokenToSvgIds(keyName)); + } + + return out; +} diff --git a/src/app/core/keyboard/keyboard-payload.util.ts b/src/app/core/keyboard/keyboard-payload.util.ts new file mode 100644 index 0000000..9f7c3d5 --- /dev/null +++ b/src/app/core/keyboard/keyboard-payload.util.ts @@ -0,0 +1,88 @@ +import type { ParsedEvent } from '../models/api.types'; + +import { keyNameModifiersPayloadToSvgIds } from './keyboard-key-name-map'; +import { normalizeVirtualKey, vkToKeyboardSvgKeyId } from './vk-to-keyboard-svg-id'; + +export function isKeyboardTelemetryEvent(event: ParsedEvent): boolean { + const t = (event.event_type ?? '').toLowerCase(); + return t.includes('keyboard') || t.includes('key'); +} + +/** + * Извлекает виртуальный код клавиши из payload (массив чисел, объект с полями vk/keyCode и т.д.). + */ +export function parseKeyboardVirtualKey(data: unknown): number | null { + if (data == null) { + return null; + } + if (Array.isArray(data)) { + if (data.length >= 1) { + const v = data[0]; + if (typeof v === 'number' && Number.isFinite(v)) { + return v; + } + if (typeof v === 'string') { + const n = parseInt(v, 10); + return Number.isFinite(n) ? n : null; + } + } + return null; + } + if (typeof data === 'object') { + const o = data as Record; + for (const key of ['vk', 'virtualKey', 'VirtualKey', 'keyCode', 'KeyCode', 'wParam']) { + const v = o[key]; + if (typeof v === 'number' && Number.isFinite(v)) { + return v; + } + if (typeof v === 'string') { + const n = parseInt(v, 10); + if (Number.isFinite(n)) { + return n; + } + } + } + } + return null; +} + +export function eventPayloadJson(data: unknown): string { + try { + return JSON.stringify(data, null, 2); + } catch { + return String(data); + } +} + +function unwrapJsonPayload(data: unknown): unknown { + if (typeof data === 'string') { + try { + return JSON.parse(data) as unknown; + } catch { + return data; + } + } + return data; +} + +/** + * Id элементов `K_kb*` для подсветки: сначала объект `key_name` / `modifiers`, иначе VK из массива/полей. + */ +export function parseKeyboardHighlightKeyIds(data: unknown): string[] { + const raw = unwrapJsonPayload(data); + if (raw != null && typeof raw === 'object' && !Array.isArray(raw)) { + const o = raw as Record; + if (typeof o['key_name'] === 'string' || typeof o['modifiers'] === 'string') { + const fromNames = keyNameModifiersPayloadToSvgIds(o); + if (fromNames.length > 0) { + return fromNames; + } + } + } + const vk = parseKeyboardVirtualKey(raw); + if (vk != null) { + const id = vkToKeyboardSvgKeyId(normalizeVirtualKey(vk)); + return id ? [id] : []; + } + return []; +} diff --git a/src/app/core/keyboard/keyboard-svg-highlight.service.ts b/src/app/core/keyboard/keyboard-svg-highlight.service.ts new file mode 100644 index 0000000..8dc0168 --- /dev/null +++ b/src/app/core/keyboard/keyboard-svg-highlight.service.ts @@ -0,0 +1,54 @@ +import { Injectable, inject } from '@angular/core'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { Observable, defer, from, shareReplay } from 'rxjs'; +import { map } from 'rxjs/operators'; + +/** + * Файл из `public/KB_USA-standard.svg` — отдаётся с корня приложения (`/KB_USA-standard.svg`), + * не через API. HttpClient нельзя: `apiBaseUrlInterceptor` превратил бы путь в `/api/v1/...`. + */ +const KEYBOARD_SVG_PATH = '/KB_USA-standard.svg'; + +@Injectable({ providedIn: 'root' }) +export class KeyboardSvgHighlightService { + private readonly sanitizer = inject(DomSanitizer); + + private readonly baseSvg$ = defer(() => + from( + fetch(KEYBOARD_SVG_PATH).then((r) => { + if (!r.ok) { + throw new Error(`Не удалось загрузить клавиатуру: ${r.status} ${r.statusText}`); + } + return r.text(); + }), + ), + ).pipe(shareReplay({ bufferSize: 1, refCount: false })); + + svgWithHighlight(keyIds: string[] | null): Observable { + return this.baseSvg$.pipe( + map((svg) => this.injectHighlight(svg, keyIds ?? [])), + map((html) => this.sanitizer.bypassSecurityTrustHtml(html)), + ); + } + + /** Подсветка: заливка `--sg-keyboard-key-pressed-fill` (акцент), подписи/иконки `--sg-keyboard-key-pressed-ink`. */ + private injectHighlight(svgText: string, keyIds: string[]): string { + const valid = keyIds.filter((id) => /^K_kb[0-9a-z]+$/i.test(id)); + if (valid.length === 0) { + return svgText; + } + const rules: string[] = []; + for (const id of valid) { + const suffix = id.slice(2); + rules.push( + `#${id}{fill:var(--sg-keyboard-key-pressed-fill)!important;stroke:none!important;}`, + ); + rules.push(`[id^="T_${suffix}"]{fill:var(--sg-keyboard-key-pressed-ink)!important;}`); + rules.push( + `#S_${suffix} path,#S_${suffix} line,#S_${suffix} polyline,#S_${suffix} rect{stroke:var(--sg-keyboard-key-pressed-ink)!important;fill:none!important;}`, + ); + } + const styleBlock = ``; + return svgText.replace(/]*>/i, (open) => `${open}${styleBlock}`); + } +} diff --git a/src/app/core/keyboard/vk-to-keyboard-svg-id.ts b/src/app/core/keyboard/vk-to-keyboard-svg-id.ts new file mode 100644 index 0000000..994dd55 --- /dev/null +++ b/src/app/core/keyboard/vk-to-keyboard-svg-id.ts @@ -0,0 +1,115 @@ +/** + * Соответствие Windows Virtual Key → id элемента в `public/KB_USA-standard.svg` (K_kb*). + * Раскладка US QWERTY, без блока F1–F12 (на схеме их нет). + */ +export function normalizeVirtualKey(vk: number): number { + if (vk >= 0x61 && vk <= 0x7a) { + return vk - 0x20; + } + return vk; +} + +function buildDigitMap(): Map { + const ids = ['K_kb2k', 'K_kb2b', 'K_kb2c', 'K_kb2d', 'K_kb2e', 'K_kb2f', 'K_kb2g', 'K_kb2h', 'K_kb2i', 'K_kb2j']; + const m = new Map(); + for (let d = 0; d <= 9; d++) { + m.set(0x30 + d, ids[d]!); + } + return m; +} + +const DIGIT_TO_ID = buildDigitMap(); + +const LETTER_TO_ID: Record = { + A: 'K_kb4c', + B: 'K_kb5g', + C: 'K_kb5e', + D: 'K_kb4e', + E: 'K_kb3d', + F: 'K_kb4f', + G: 'K_kb4g', + H: 'K_kb4h', + I: 'K_kb3i', + J: 'K_kb4i', + K: 'K_kb4j', + L: 'K_kb4l', + M: 'K_kb5i', + N: 'K_kb5h', + O: 'K_kb3j', + P: 'K_kb3k', + Q: 'K_kb3b', + R: 'K_kb3e', + S: 'K_kb4d', + T: 'K_kb3f', + U: 'K_kb3h', + V: 'K_kb5f', + W: 'K_kb3c', + X: 'K_kb5d', + Y: 'K_kb3g', + Z: 'K_kb5c', +}; + +/** OEM и прочие VK (Windows). */ +const EXTRA_VK: Record = { + 0x08: 'K_kb2n', + 0x09: 'K_kb3a', + 0x0d: 'K_kb4n', + 0x10: 'K_kb5a', + 0x11: 'K_kb6a', + 0x12: 'K_kb6c', + 0x14: 'K_kb4a', + 0x20: 'K_kb6d', + 0x5b: 'K_kb6b', + 0x5c: 'K_kb6l', + 0x5d: 'K_kb6m', + 0xa0: 'K_kb5a', + 0xa1: 'K_kb5m', + 0xa2: 'K_kb6a', + 0xa3: 'K_kb6n', + 0xa4: 'K_kb6c', + 0xa5: 'K_kb6k', + 0xba: 'K_kb4l', + 0xbb: 'K_kb2m', + 0xbc: 'K_kb5j', + 0xbd: 'K_kb2l', + 0xbe: 'K_kb5k', + 0xbf: 'K_kb5l', + 0xc0: 'K_kb2a', + 0xdb: 'K_kb3l', + 0xdc: 'K_kb3n', + 0xdd: 'K_kb3m', + 0xde: 'K_kb4m', +}; + +export function vkToKeyboardSvgKeyId(vk: number): string | null { + const k = normalizeVirtualKey(vk); + const fromExtra = EXTRA_VK[k]; + if (fromExtra) { + return fromExtra; + } + const digit = DIGIT_TO_ID.get(k); + if (digit) { + return digit; + } + if (k >= 0x41 && k <= 0x5a) { + const ch = String.fromCharCode(k); + return LETTER_TO_ID[ch] ?? null; + } + return null; +} + +/** Одна буква A–Z или цифра 0–9 как в подписи клавиши (`key_name`). */ +export function charKeyNameToSvgKeyId(name: string): string | null { + const c = name.trim(); + if (c.length !== 1) { + return null; + } + if (c >= '0' && c <= '9') { + return DIGIT_TO_ID.get(c.charCodeAt(0)) ?? null; + } + const ch = c.toUpperCase(); + if (ch >= 'A' && ch <= 'Z') { + return LETTER_TO_ID[ch] ?? null; + } + return null; +} diff --git a/src/app/core/models/api.types.ts b/src/app/core/models/api.types.ts new file mode 100644 index 0000000..55433bc --- /dev/null +++ b/src/app/core/models/api.types.ts @@ -0,0 +1,71 @@ +/** Соответствует `handler.ErrorResponse` из OpenAPI. */ +export interface ApiErrorBody { + code?: string; + error?: string; +} + +export interface CreateSessionRequest { + title: string; +} + +export interface CreateSessionResponse { + id: number; + session_key: string; + status: string; + title: string; + created_at: string; +} + +export interface SessionSummary { + id: string; + user_id?: string; + status: string; + started_at?: string; + ended_at?: string; + chunks_total?: number; + events_total?: number; + /** Не в swagger, но бэкенд может отдавать для списка. */ + title?: string; +} + +export interface SessionListResponse { + sessions: SessionSummary[]; + total: number; + limit: number; + offset: number; +} + +export interface StreamInfo { + stream_type: string; + chunk_count?: number; + duration_ms?: number; + playlist_url: string; +} + +export interface SessionDetailResponse { + session: SessionSummary; + streams: StreamInfo[]; +} + +export interface TelemetryEvent { + id: number; + session_id: number; + user_id?: number; + event_type: string; + created_at: string; + payload?: number[]; +} + +export interface ParsedEvent { + event_type: string; + timestamp: number; + data?: unknown; +} + +export interface ParsedEventsResponse { + session_id: number; + count: number; + events: ParsedEvent[]; +} + +export type StreamType = 'screen' | 'webcam'; diff --git a/src/app/core/notifications/user-error-messages.config.ts b/src/app/core/notifications/user-error-messages.config.ts new file mode 100644 index 0000000..b555ca5 --- /dev/null +++ b/src/app/core/notifications/user-error-messages.config.ts @@ -0,0 +1,34 @@ +/** + * Типы ошибок для подбора дружелюбного текста во всплывающих уведомлениях. + * Соответствие «тип → фраза» задаётся здесь; технические детали — только в Dev Console. + */ +export const USER_ERROR_FRIENDLY_MESSAGES = { + /** Нет сети / соединение сброшено (часто HTTP 0). */ + network: 'Проверьте интернет-соединение.', + /** Таймаут запроса (в т.ч. RxJS timeout, HTTP 408). */ + timeout: 'Запрос занял слишком много времени. Попробуйте позже.', + /** Ответ сервера 5xx. */ + server_error: 'Уже работаем над этим.', + /** 404. */ + not_found: 'Не удалось найти запрашиваемые данные.', + /** 401. */ + unauthorized: 'Требуется вход в систему.', + /** 403. */ + forbidden: 'Недостаточно прав для этого действия.', + /** 400. */ + bad_request: 'Некорректный запрос. Попробуйте позже.', + /** Прочие 4xx (кроме перечисленных выше). */ + client_error: 'Не удалось выполнить запрос. Попробуйте позже.', + /** Не JSON / ошибка разбора ответа. */ + parse_error: 'Получен неожиданный ответ сервера. Попробуйте позже.', + /** Локальная валидация (неверный id и т.п.). */ + invalid_input: 'Проверьте введённые данные.', + /** Не удалось отнести к категории. */ + unknown: 'Попробуйте позже.', +} as const; + +export type UserErrorKind = keyof typeof USER_ERROR_FRIENDLY_MESSAGES; + +export function friendlyMessageForUserError(kind: UserErrorKind): string { + return USER_ERROR_FRIENDLY_MESSAGES[kind]; +} diff --git a/src/app/core/notifications/user-error-notify.service.ts b/src/app/core/notifications/user-error-notify.service.ts new file mode 100644 index 0000000..5eff160 --- /dev/null +++ b/src/app/core/notifications/user-error-notify.service.ts @@ -0,0 +1,40 @@ +import { isDevMode, inject, Injectable } from '@angular/core'; +import { TuiNotificationService } from '@taiga-ui/core/components/notification'; + +import { DevLogService } from '../devtools/dev-log.service'; +import { classifyUserError } from '../http/error-classification.util'; +import { escapeHtml, httpErrorMessage } from '../http/http-error.util'; +import { friendlyMessageForUserError } from './user-error-messages.config'; + +const ERROR_TOAST_TITLE = 'Что-то пошло не так...'; + +/** + * Показывает пользователю обобщённое уведомление; точный текст ошибки — только в DevLog (в dev). + */ +@Injectable({ providedIn: 'root' }) +export class UserErrorNotifyService { + private readonly notifications = inject(TuiNotificationService); + private readonly devLog = inject(DevLogService); + + notifyError(err: unknown, source: string): void { + const kind = classifyUserError(err); + const userSubtitle = friendlyMessageForUserError(kind); + const technical = httpErrorMessage(err); + + if (isDevMode()) { + this.devLog.add({ + level: 'error', + source: 'system', + message: `${source}: ${technical}`, + }); + } + + this.notifications + .open(escapeHtml(userSubtitle), { + label: ERROR_TOAST_TITLE, + appearance: 'negative', + autoClose: 0, + }) + .subscribe(); + } +} diff --git a/src/app/core/services/sessions-api.service.ts b/src/app/core/services/sessions-api.service.ts new file mode 100644 index 0000000..ff82622 --- /dev/null +++ b/src/app/core/services/sessions-api.service.ts @@ -0,0 +1,80 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable, inject } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { API_ORIGIN } from '../config/api.tokens'; +import type { + CreateSessionRequest, + CreateSessionResponse, + ParsedEventsResponse, + SessionDetailResponse, + SessionListResponse, + StreamType, + TelemetryEvent, +} from '../models/api.types'; + +@Injectable({ providedIn: 'root' }) +export class SessionsApiService { + private readonly http = inject(HttpClient); + private readonly apiOrigin = inject(API_ORIGIN); + + listSessions(limit = 50, offset = 0): Observable { + const params = new HttpParams() + .set('limit', String(limit)) + .set('offset', String(offset)); + return this.http.get('/sessions', { params }); + } + + createSession(body: CreateSessionRequest): Observable { + return this.http.post('/sessions', body); + } + + getSession(id: number): Observable { + return this.http.get(`/sessions/${id}`); + } + + getTelemetry(sessionId: number): Observable { + return this.http.get(`/sessions/${sessionId}/telemetry`); + } + + getParsedEvents(sessionId: number, from?: number, to?: number): Observable { + let params = new HttpParams(); + if (typeof from === 'number') { + params = params.set('from', String(from)); + } + if (typeof to === 'number') { + params = params.set('to', String(to)); + } + return this.http.get(`/sessions/${sessionId}/events`, { params }); + } + + /** + * Относительный `playlist_url` из API нужно разрешить относительно хоста (`API_ORIGIN`). + */ + resolvePlaylistUrl(playlistUrl: string): string { + if (/^https?:\/\//i.test(playlistUrl)) { + return playlistUrl; + } + return new URL(playlistUrl, `${this.apiOrigin}/`).href; + } + + /** + * Ручка для агента (Bearer). В веб-интерфейсе обычно не нужна — оставлена для отладки/скриптов. + */ + uploadChunk( + sessionId: number, + chunkIdx: number, + file: File, + streamType: StreamType = 'screen', + bearerToken: string, + ): Observable { + const form = new FormData(); + form.set('session_id', String(sessionId)); + form.set('chunk_idx', String(chunkIdx)); + form.set('stream_type', streamType); + form.set('file', file, file.name); + return this.http.post('/upload', form, { + headers: { Authorization: `Bearer ${bearerToken}` }, + }); + } +} diff --git a/src/app/core/sessions/session-status-chip-classes.pipe.ts b/src/app/core/sessions/session-status-chip-classes.pipe.ts new file mode 100644 index 0000000..53fb74f --- /dev/null +++ b/src/app/core/sessions/session-status-chip-classes.pipe.ts @@ -0,0 +1,24 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +/** + * Классы-модификаторы для чипа статуса. `finished` — без модификатора (дефолтный вид Taiga). + */ +@Pipe({ + name: 'sessionStatusChipClasses', + standalone: true, +}) +export class SessionStatusChipClassesPipe implements PipeTransform { + transform(value: string | null | undefined): Record { + const k = value?.trim().toLowerCase() ?? ''; + if (k === '' || k === 'finished') { + return {}; + } + if (k === 'active') { + return { 'status-chip--active': true }; + } + if (k === 'pending') { + return { 'status-chip--pending': true }; + } + return { 'status-chip--unknown': true }; + } +} diff --git a/src/app/core/sessions/session-status-labels.config.ts b/src/app/core/sessions/session-status-labels.config.ts new file mode 100644 index 0000000..c94b1ea --- /dev/null +++ b/src/app/core/sessions/session-status-labels.config.ts @@ -0,0 +1,11 @@ +/** + * Отображаемые подписи для статусов сессий (значения с API — обычно в нижнем регистре). + * Неизвестный ключ в UI показывается как есть; в dev — предупреждение в консоли разработчика. + */ +export const SESSION_STATUS_LABELS: Readonly> = { + active: 'Активная', + pending: 'Ожидается', + finished: 'Завершена', + paused: 'Приостановлена', + failed: 'Ошибка', +}; diff --git a/src/app/core/sessions/session-status.pipe.ts b/src/app/core/sessions/session-status.pipe.ts new file mode 100644 index 0000000..b604b8d --- /dev/null +++ b/src/app/core/sessions/session-status.pipe.ts @@ -0,0 +1,33 @@ +import { isDevMode, Pipe, PipeTransform, inject } from '@angular/core'; + +import { DevLogService } from '../devtools/dev-log.service'; +import { SESSION_STATUS_LABELS } from './session-status-labels.config'; + +@Pipe({ + name: 'sessionStatus', + standalone: true, +}) +export class SessionStatusPipe implements PipeTransform { + private readonly devLog = inject(DevLogService); + private readonly warnedUnknown = new Set(); + + transform(value: string | null | undefined): string { + if (value == null || value === '') { + return '—'; + } + const key = value.trim(); + const mapped = SESSION_STATUS_LABELS[key.toLowerCase()]; + if (mapped !== undefined) { + return mapped; + } + if (isDevMode() && !this.warnedUnknown.has(key)) { + this.warnedUnknown.add(key); + this.devLog.add({ + level: 'warn', + source: 'system', + message: `Неизвестный статус сессии (нет подписи в session-status-labels.config): ${key}`, + }); + } + return key; + } +} diff --git a/src/app/features/devtools/dev-console/dev-console.component.ts b/src/app/features/devtools/dev-console/dev-console.component.ts new file mode 100644 index 0000000..6db4871 --- /dev/null +++ b/src/app/features/devtools/dev-console/dev-console.component.ts @@ -0,0 +1,89 @@ +import { DatePipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, computed, inject, isDevMode, signal } from '@angular/core'; + +import { DevLogService } from '../../../core/devtools/dev-log.service'; + +@Component({ + selector: 'app-dev-console', + imports: [DatePipe], + templateUrl: './dev-console.html', + styleUrl: './dev-console.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DevConsoleComponent { + private readonly logs = inject(DevLogService); + protected readonly isDev = isDevMode(); + protected readonly collapsed = signal(false); + protected readonly minimized = signal(false); + protected readonly entries = this.logs.entries; + protected readonly count = computed(() => this.entries().length); + protected readonly expandedIds = signal>({}); + + constructor() { + if (!this.isDev || typeof window === 'undefined') { + return; + } + + window.addEventListener('error', (e) => { + this.logs.add({ + level: 'error', + source: 'system', + message: `${e.message} (${e.filename}:${e.lineno})`, + }); + }); + + window.addEventListener('unhandledrejection', (e: PromiseRejectionEvent) => { + const reason = + typeof e.reason === 'string' + ? e.reason + : e.reason instanceof Error + ? e.reason.message + : 'Unhandled promise rejection'; + this.logs.add({ + level: 'warn', + source: 'system', + message: reason, + }); + }); + } + + protected toggleCollapsed(): void { + this.collapsed.update((v) => !v); + } + + protected minimize(): void { + this.minimized.set(true); + this.collapsed.set(false); + } + + protected restore(): void { + this.minimized.set(false); + } + + protected clear(): void { + this.logs.clear(); + this.expandedIds.set({}); + } + + protected toggleExpanded(id: number): void { + this.expandedIds.update((curr) => ({ ...curr, [id]: !curr[id] })); + } + + protected isExpanded(id: number): boolean { + return !!this.expandedIds()[id]; + } + + protected pretty(value: unknown): string { + if (value === undefined) { + return ''; + } + if (typeof value === 'string') { + return value; + } + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } + } +} diff --git a/src/app/features/devtools/dev-console/dev-console.css b/src/app/features/devtools/dev-console/dev-console.css new file mode 100644 index 0000000..ab13b83 --- /dev/null +++ b/src/app/features/devtools/dev-console/dev-console.css @@ -0,0 +1,144 @@ +:host { + position: fixed; + right: 1rem; + bottom: 1rem; + z-index: 1000; +} + +.dev-console-mini { + border: 1px solid var(--tui-border-normal); + border-radius: 999px; + background: #1b1d22; + color: #e8edf1; + padding: 0.45rem 0.7rem; + font-size: 0.78rem; + line-height: 1; + cursor: pointer; + box-shadow: 0 8px 24px rgb(0 0 0 / 28%); +} + +.dev-console { + width: min(760px, calc(100vw - 2rem)); + max-height: min(46vh, 380px); + border: 1px solid var(--tui-border-normal); + border-radius: 0.75rem; + background: #121317; + color: #e8edf1; + box-shadow: 0 8px 24px rgb(0 0 0 / 28%); + overflow: hidden; +} + +.dev-console_collapsed { + max-height: 2.5rem; +} + +.dev-console__header { + display: flex; + gap: 0.5rem; + align-items: center; + padding: 0.4rem 0.55rem; + background: #1b1d22; + border-bottom: 1px solid rgb(255 255 255 / 8%); + font-size: 0.8rem; +} + +.dev-console__count { + margin-right: auto; + color: #9aa4b2; +} + +.dev-console__header button { + border: 1px solid rgb(255 255 255 / 18%); + background: transparent; + color: inherit; + border-radius: 0.35rem; + font-size: 0.75rem; + line-height: 1; + padding: 0.35rem 0.45rem; + cursor: pointer; +} + +.dev-console__list { + overflow: auto; + max-height: calc(min(46vh, 380px) - 2.5rem); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 0.76rem; +} + +.dev-console__entry { + display: grid; + grid-template-columns: 6rem 3.2rem 1fr; + gap: 0.5rem; + padding: 0.35rem 0.55rem; + border-bottom: 1px solid rgb(255 255 255 / 6%); +} + +.dev-console__main { + min-width: 0; +} + +.dev-console__entry[data-level='warn'] { + background: rgb(255 199 0 / 8%); +} + +.dev-console__entry[data-level='error'] { + background: rgb(217 45 32 / 12%); +} + +.dev-console__time { + color: #9aa4b2; +} + +.dev-console__source { + color: #c7cbd1; + text-transform: uppercase; +} + +.dev-console__message { + word-break: break-word; +} + +.dev-console__expand { + margin-top: 0.25rem; + border: 1px solid rgb(255 255 255 / 18%); + background: transparent; + color: #d6dde7; + border-radius: 0.3rem; + font-size: 0.72rem; + line-height: 1.2; + padding: 0.2rem 0.35rem; + cursor: pointer; +} + +.dev-console__details { + margin-top: 0.35rem; + padding: 0.4rem; + border: 1px solid rgb(255 255 255 / 12%); + border-radius: 0.4rem; + background: rgb(0 0 0 / 18%); +} + +.dev-console__meta { + margin-bottom: 0.35rem; + display: flex; + flex-wrap: wrap; + gap: 0.6rem; +} + +.dev-console__details details { + margin: 0.35rem 0; +} + +.dev-console__details summary { + cursor: pointer; + color: #d4d9e0; +} + +.dev-console__details pre { + margin: 0.35rem 0 0; + padding: 0.35rem; + border-radius: 0.35rem; + background: rgb(255 255 255 / 4%); + white-space: pre-wrap; + word-break: break-word; +} diff --git a/src/app/features/devtools/dev-console/dev-console.html b/src/app/features/devtools/dev-console/dev-console.html new file mode 100644 index 0000000..e2b11ee --- /dev/null +++ b/src/app/features/devtools/dev-console/dev-console.html @@ -0,0 +1,79 @@ +@if (isDev) { + @if (minimized()) { + + } @else { + + } +} diff --git a/src/app/features/sessions/hls-player/hls-player.component.ts b/src/app/features/sessions/hls-player/hls-player.component.ts new file mode 100644 index 0000000..08140fe --- /dev/null +++ b/src/app/features/sessions/hls-player/hls-player.component.ts @@ -0,0 +1,48 @@ +import { + ChangeDetectionStrategy, + Component, + effect, + ElementRef, + input, + viewChild, +} from '@angular/core'; +import Hls from 'hls.js'; + +@Component({ + selector: 'app-hls-player', + templateUrl: './hls-player.html', + styleUrl: './hls-player.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class HlsPlayerComponent { + /** Полный URL плейлиста `.m3u8`. */ + readonly src = input.required(); + + private readonly videoRef = viewChild>('videoEl'); + + constructor() { + effect((onCleanup) => { + const url = this.src(); + const ref = this.videoRef(); + if (!ref) { + return; + } + const video = ref.nativeElement; + let hls: Hls | null = null; + + if (Hls.isSupported()) { + hls = new Hls({ enableWorker: true, lowLatencyMode: true }); + hls.loadSource(url); + hls.attachMedia(video); + } else if (video.canPlayType('application/vnd.apple.mpegurl')) { + video.src = url; + } + + onCleanup(() => { + hls?.destroy(); + video.removeAttribute('src'); + video.load(); + }); + }); + } +} diff --git a/src/app/features/sessions/hls-player/hls-player.css b/src/app/features/sessions/hls-player/hls-player.css new file mode 100644 index 0000000..056c86a --- /dev/null +++ b/src/app/features/sessions/hls-player/hls-player.css @@ -0,0 +1,11 @@ +:host { + display: block; + width: 100%; + max-width: 960px; +} + +.player { + width: 100%; + border-radius: var(--tui-radius-l, 0.75rem); + background: var(--tui-background-neutral-1, #000); +} diff --git a/src/app/features/sessions/hls-player/hls-player.html b/src/app/features/sessions/hls-player/hls-player.html new file mode 100644 index 0000000..34fd645 --- /dev/null +++ b/src/app/features/sessions/hls-player/hls-player.html @@ -0,0 +1 @@ + diff --git a/src/app/features/sessions/session-detail/session-detail.component.ts b/src/app/features/sessions/session-detail/session-detail.component.ts new file mode 100644 index 0000000..6115703 --- /dev/null +++ b/src/app/features/sessions/session-detail/session-detail.component.ts @@ -0,0 +1,325 @@ +import { AsyncPipe, NgClass } from '@angular/common'; +import { HttpErrorResponse } from '@angular/common/http'; +import { ChangeDetectionStrategy, Component, inject, model, signal } from '@angular/core'; +import { toObservable } from '@angular/core/rxjs-interop'; +import { ActivatedRoute, RouterLink } from '@angular/router'; +import { TuiButton } from '@taiga-ui/core/components/button'; +import { TuiLink } from '@taiga-ui/core/components/link'; +import { TuiLoader } from '@taiga-ui/core/components/loader'; +import { TuiTitle } from '@taiga-ui/core/components/title'; +import { TuiChip } from '@taiga-ui/kit/components/chip'; +import { TuiTabs } from '@taiga-ui/kit/components/tabs'; +import { catchError, combineLatest, distinctUntilChanged, map, of, startWith, switchMap, tap, timeout } from 'rxjs'; + +import { UserErrorNotifyService } from '../../../core/notifications/user-error-notify.service'; +import { SessionStatusChipClassesPipe } from '../../../core/sessions/session-status-chip-classes.pipe'; +import { SessionStatusPipe } from '../../../core/sessions/session-status.pipe'; +import type { ParsedEvent, SessionDetailResponse, StreamInfo } from '../../../core/models/api.types'; +import { SessionsApiService } from '../../../core/services/sessions-api.service'; +import { formatTimestamp } from '../../../shared/utils/date-time.util'; +import { HlsPlayerComponent } from '../hls-player/hls-player.component'; +import { TelemetryEventDetailComponent } from '../telemetry-event-detail/telemetry-event-detail.component'; + +@Component({ + selector: 'app-session-detail', + imports: [ + AsyncPipe, + NgClass, + RouterLink, + HlsPlayerComponent, + TuiButton, + TuiChip, + ...TuiTabs, + TuiLink, + TuiLoader, + TuiTitle, + SessionStatusChipClassesPipe, + SessionStatusPipe, + TelemetryEventDetailComponent, + ], + templateUrl: './session-detail.html', + styleUrl: './session-detail.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SessionDetailComponent { + private readonly route = inject(ActivatedRoute); + private readonly api = inject(SessionsApiService); + private readonly userErrors = inject(UserErrorNotifyService); + + private readonly telemetryToMs = signal(null); + private readonly recordingStartMs = signal(null); + private readonly recordingEndMs = signal(null); + protected readonly customToLocal = signal(''); + + /** Выбранный тип потока (или первая вкладка по умолчанию в шаблоне). */ + protected readonly selectedStreamType = signal(null); + + /** 0 — просмотр, 1 — служебная информация. */ + protected readonly activeTabIndex = model(0); + + /** null — все типы событий в таблице телеметрии. */ + protected readonly telemetryEventTypeFilter = signal(null); + + /** Раскрытая строка телеметрии: ключ или null. */ + protected readonly expandedTelemetryRowKey = signal(null); + + private readonly sessionId$ = this.route.paramMap.pipe( + map((p) => Number(p.get('id'))), + distinctUntilChanged(), + ); + + protected readonly vm$ = this.sessionId$.pipe( + switchMap((id) => { + if (!Number.isFinite(id)) { + this.userErrors.notifyError(new Error('Некорректный идентификатор сессии'), 'Сессия'); + return of({ status: 'error' as const }); + } + this.selectedStreamType.set(null); + this.activeTabIndex.set(0); + this.telemetryEventTypeFilter.set(null); + this.expandedTelemetryRowKey.set(null); + return this.api.getSession(id).pipe( + map((detail) => ({ status: 'ok' as const, id, detail })), + tap((state) => { + if (state.status !== 'ok') { + return; + } + const start = this.toUnixMs(state.detail.session.started_at); + const end = this.toUnixMs(state.detail.session.ended_at); + this.recordingStartMs.set(start); + this.recordingEndMs.set(end); + // Дефолт для телеметрии: до текущего момента (или конца записи, если завершена). + if (this.telemetryToMs() === null) { + this.telemetryToMs.set(end ?? Date.now()); + } + }), + catchError((e: HttpErrorResponse) => { + this.userErrors.notifyError(e, 'Сессия'); + return of({ status: 'error' as const }); + }), + startWith({ status: 'loading' as const }), + ); + }), + ); + + protected readonly telemetry$ = combineLatest([ + this.sessionId$, + toObservable(this.telemetryToMs), + toObservable(this.recordingStartMs), + toObservable(this.recordingEndMs), + ]).pipe( + switchMap(([id, toMs, recordingStartMs, recordingEndMs]) => { + if (!Number.isFinite(id)) { + return of({ + status: 'error' as const, + toMs, + fromMs: recordingStartMs ?? 0, + telemetry: [] as ParsedEvent[], + parsedMeta: undefined, + }); + } + const now = Date.now(); + const lowerBound = recordingStartMs ?? 0; + const upperBound = recordingEndMs ?? now; + const normalizedTo = this.clamp(toMs ?? upperBound, lowerBound, upperBound); + return this.api + .getParsedEvents( + id, + lowerBound, + normalizedTo, + ) + .pipe( + timeout(12000), + map((resp) => ({ + status: 'ok' as const, + toMs: normalizedTo, + fromMs: lowerBound, + telemetry: Array.isArray(resp.events) ? resp.events : [], + parsedMeta: { + session_id: resp.session_id, + count: resp.count, + }, + })), + catchError((e: unknown) => { + this.userErrors.notifyError(e, 'Телеметрия'); + return of({ + status: 'error' as const, + toMs: normalizedTo, + fromMs: lowerBound, + telemetry: [] as ParsedEvent[], + parsedMeta: undefined, + }); + }), + startWith({ + status: 'loading' as const, + toMs: normalizedTo, + fromMs: lowerBound, + telemetry: [] as ParsedEvent[], + parsedMeta: undefined, + }), + ); + }), + ); + + protected pickStream(type: string): void { + this.selectedStreamType.set(type); + } + + protected activeStreamType(detail: SessionDetailResponse): string | null { + const streams = detail.streams; + if (!streams?.length) { + return null; + } + const picked = this.selectedStreamType(); + if (picked && streams.some((s) => s.stream_type === picked)) { + return picked; + } + return streams[0].stream_type; + } + + protected playlistUrl(detail: SessionDetailResponse): string | null { + const t = this.activeStreamType(detail); + if (!t) { + return null; + } + const stream = detail.streams.find((s) => s.stream_type === t); + return stream ? this.api.resolvePlaylistUrl(stream.playlist_url) : null; + } + + protected formatDate(value: string | null | undefined): string { + return formatTimestamp(value); + } + + protected formatUnixMs(value: number | null | undefined): string { + if (!value) { + return '—'; + } + return formatTimestamp(new Date(value).toISOString()); + } + + protected pickTelemetryEventTypeFilter(type: string | null): void { + this.telemetryEventTypeFilter.set(type); + this.expandedTelemetryRowKey.set(null); + } + + protected telemetryRowKey(row: ParsedEvent, index: number): string { + return `${row.timestamp}\u0000${row.event_type}\u0000${index}`; + } + + protected toggleTelemetryRow(row: ParsedEvent, index: number): void { + const key = this.telemetryRowKey(row, index); + this.expandedTelemetryRowKey.update((cur) => (cur === key ? null : key)); + } + + protected isTelemetryRowExpanded(row: ParsedEvent, index: number): boolean { + return this.expandedTelemetryRowKey() === this.telemetryRowKey(row, index); + } + + /** Уникальные значения `event_type` (пустая строка — отдельная вкладка). */ + protected uniqueTelemetryEventTypes(events: ParsedEvent[]): string[] { + const set = new Set(); + for (const e of events) { + set.add(e.event_type ?? ''); + } + return [...set].sort((a, b) => a.localeCompare(b, 'ru')); + } + + protected telemetryEventsOfType(events: ParsedEvent[], typeKey: string): number { + return events.filter((e) => (e.event_type ?? '') === typeKey).length; + } + + protected filteredTelemetryEvents(events: ParsedEvent[]): ParsedEvent[] { + const filter = this.telemetryEventTypeFilter(); + if (filter === null) { + return events; + } + return events.filter((e) => (e.event_type ?? '') === filter); + } + + protected telemetryTypeTabLabel(typeKey: string): string { + return typeKey === '' ? 'Без типа' : typeKey; + } + + protected eventDataPreview(event: ParsedEvent): string { + const data = event.data; + if (data === null || data === undefined) { + return '—'; + } + if (Array.isArray(data)) { + return data.map((v) => String(v)).join(', '); + } + if (typeof data === 'object') { + try { + return JSON.stringify(data); + } catch { + return '[object]'; + } + } + return String(data); + } + + protected selectRecentWindow(seconds: number): void { + this.customToLocal.set(''); + const start = this.recordingStartMs() ?? Date.now(); + const end = this.recordingEndMs() ?? Date.now(); + this.telemetryToMs.set(this.clamp(start + seconds * 1000, start, end)); + } + + protected loadUntilEndTelemetry(): void { + this.customToLocal.set(''); + const end = this.recordingEndMs() ?? Date.now(); + this.telemetryToMs.set(end); + } + + protected applyCustomTo(value: string): void { + this.customToLocal.set(value); + if (!value) { + return; + } + const ms = new Date(value).getTime(); + if (Number.isFinite(ms)) { + const start = this.recordingStartMs() ?? Date.now(); + const end = this.recordingEndMs() ?? Date.now(); + this.telemetryToMs.set(this.clamp(ms, start, end)); + } + } + + protected telemetryRangeLabel(toMs: number | null): string { + return `С ${this.formatUnixMs(this.recordingStartMs())} до ${this.formatUnixMs(toMs)}`; + } + + protected detailPayloadJson(detail: SessionDetailResponse): string { + try { + return JSON.stringify(detail, null, 2); + } catch { + return '{}'; + } + } + + protected formatDurationMs(ms: number | null | undefined): string { + if (ms === null || ms === undefined || !Number.isFinite(ms)) { + return '—'; + } + return `${ms} ms`; + } + + protected streamResolvedPlaylistUrl(stream: StreamInfo): string { + return this.api.resolvePlaylistUrl(stream.playlist_url); + } + + private toUnixMs(value: string | null | undefined): number | null { + if (!value) { + return null; + } + const ms = new Date(value).getTime(); + return Number.isFinite(ms) ? ms : null; + } + + private clamp(value: number, min: number, max: number): number { + if (max < min) { + return min; + } + return Math.min(max, Math.max(min, value)); + } + +} diff --git a/src/app/features/sessions/session-detail/session-detail.css b/src/app/features/sessions/session-detail/session-detail.css new file mode 100644 index 0000000..6ad8a09 --- /dev/null +++ b/src/app/features/sessions/session-detail/session-detail.css @@ -0,0 +1,258 @@ +.page { + max-width: 960px; + margin: 0 auto; + padding: 1.5rem 1rem 3rem; + box-sizing: border-box; +} + +.back { + margin-bottom: 1rem; +} + +.heading { + margin-bottom: 1.25rem; +} + +/* Не задавать display: block — ломает горизонтальный flex у tui-tabs и переносит вкладки. */ +.session-tabs { + display: flex; + flex-wrap: nowrap; + align-items: flex-end; + margin-bottom: 1.25rem; + min-width: 0; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +/* Воздух между подписью и нижней линией (у Taiga у [tuiTab] padding: 0). */ +.session-tabs [tuiTab] { + padding-block-end: 0.5rem; +} + +/* + * У Taiga для неактивной вкладки: box-shadow inset 0 -.125rem — линия hover оказывается + * выше общей нижней границы полосы (inset 0 -1px на tui-tabs). Убираем дублирующую линию. + */ +.session-tabs [tuiTab]:hover:not(._active) { + box-shadow: none; +} + +.session-title { + margin: 0 0 0.75rem; + font: var(--tui-font-heading-6); + color: var(--tui-text-primary); +} + +.kv { + display: grid; + grid-template-columns: minmax(10rem, 14rem) 1fr; + gap: 0.35rem 1rem; + margin: 0; + font: var(--tui-font-text-s); +} + +.kv dt { + margin: 0; + color: var(--tui-text-tertiary); +} + +.kv dd { + margin: 0; + word-break: break-word; +} + +.mono { + font-family: ui-monospace, monospace; + font-size: 0.92em; +} + +.json-block { + margin: 0.75rem 0 0; + padding: 1rem; + max-height: min(360px, 50vh); + overflow: auto; + border-radius: var(--tui-radius-m); + background: color-mix(in srgb, var(--tui-background-base) 88%, var(--tui-text-primary)); + font-size: 0.8rem; + line-height: 1.45; + white-space: pre-wrap; + word-break: break-word; +} + +.meta-table { + width: 100%; + border-collapse: collapse; + font: var(--tui-font-text-s); +} + +.meta-table th, +.meta-table td { + padding: 0.5rem 0.75rem; + text-align: left; + vertical-align: top; + border-bottom: 1px solid var(--tui-border-normal); +} + +.meta-table th { + font-weight: 600; + color: var(--tui-text-secondary); +} + +.table-wrap_flat { + max-height: none; +} + +.loading-wrap { + display: flex; + justify-content: center; + padding: 3rem; +} + +.loading-wrap_small { + padding: 1rem 0; +} + +.error { + color: var(--tui-status-negative); +} + +.card { + padding: 1.25rem 1.5rem; + border-radius: var(--tui-radius-l); + background: var(--tui-background-elevation-1); + margin-bottom: 1.5rem; +} + +.section-title { + margin: 0 0 1rem; + font: var(--tui-font-heading-6); + color: var(--sg-color-subtitle); +} + +.telemetry-head { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 0.75rem; + margin-bottom: 0.75rem; +} + +.telemetry-head .section-title { + margin: 0; +} + +.telemetry-actions { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.5rem; +} + +.telemetry-presets { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} + +.from-picker { + display: inline-flex; + align-items: center; + gap: 0.35rem; +} + +.summary-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 1rem; + margin-bottom: 0.5rem; +} + +.small { + font: var(--tui-font-text-s); + margin-top: 0.35rem; +} + +.muted { + color: var(--tui-text-tertiary); +} + +.stream-tabs { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.stream-tabs button { + transition: + background-color 0.15s ease, + color 0.15s ease, + border-color 0.15s ease, + outline-color 0.15s ease; +} + +/* Контраст к карточке — жёлтый акцент при наведении (специфичнее secondary appearance Taiga) */ +.stream-tabs + button[tuiButton][tuiAppearance][data-appearance='secondary']:hover:not(:disabled):not([data-state='disabled']) { + background: var(--sg-color-accent); + color: var(--sg-color-text); + border-color: color-mix(in srgb, var(--sg-color-accent) 78%, black); + outline: 2px solid transparent; +} + +.stream-tabs button.stream-active[tuiButton][tuiAppearance][data-appearance='secondary'] { + outline: 2px solid var(--sg-color-accent); + background: color-mix(in srgb, var(--sg-color-accent) 22%, white); + border-color: var(--sg-color-accent); + color: var(--sg-color-text); +} + +.table-wrap { + overflow: auto; + max-height: min(480px, 70vh); +} + +.telemetry { + width: 100%; + border-collapse: collapse; + font: var(--tui-font-text-s); +} + +.telemetry th, +.telemetry td { + padding: 0.5rem 0.75rem; + text-align: left; + border-bottom: 1px solid var(--tui-border-normal); +} + +.telemetry th { + position: sticky; + top: 0; + background: var(--tui-background-elevation-1); + z-index: 1; +} + +.telemetry-row { + cursor: pointer; + transition: background-color 0.12s ease; +} + +.telemetry-row:hover { + background: color-mix(in srgb, var(--tui-background-accent-1) 12%, transparent); +} + +.telemetry-row.telemetry-row_expanded { + background: color-mix(in srgb, var(--tui-background-accent-1) 18%, transparent); +} + +.telemetry-row-detail td { + padding: 0 0.75rem 1rem; + border-bottom: 1px solid var(--tui-border-normal); + vertical-align: top; +} + +.payload { + font-family: ui-monospace, monospace; + word-break: break-all; +} diff --git a/src/app/features/sessions/session-detail/session-detail.html b/src/app/features/sessions/session-detail/session-detail.html new file mode 100644 index 0000000..df2a686 --- /dev/null +++ b/src/app/features/sessions/session-detail/session-detail.html @@ -0,0 +1,271 @@ +
+ + + @if (vm$ | async; as state) { + @switch (state.status) { + @case ('loading') { +
+ +
+ } + @case ('error') { +

Не удалось загрузить сессию.

+ } + @case ('ok') { +

Сессия {{ state.id }}

+ + + + + + + @if (telemetry$ | async; as telemetryState) { + @if (activeTabIndex() === 0) { +
+ @if (state.detail.session.title) { +

{{ state.detail.session.title }}

+ } +
+ {{ state.detail.session.status | sessionStatus }} +
+
+ Начало: {{ formatDate(state.detail.session.started_at) }} · Окончание: + {{ formatDate(state.detail.session.ended_at) }} +
+
+ +
+

Трансляция

+ @if (state.detail.streams.length === 0) { +

Потоки ещё не готовы.

+ } @else { +
+ @for (s of state.detail.streams; track s.stream_type) { + + } +
+ @if (playlistUrl(state.detail); as src) { + + } + } +
+ +
+
+

+ События телеметрии ({{ filteredTelemetryEvents(telemetryState.telemetry).length }}) +

+
+ {{ telemetryRangeLabel(telemetryState.toMs) }} +
+ + + + + + +
+ +
+
+ + @if (telemetryState.status === 'loading') { +
+ +
+ } @else if (telemetryState.status === 'error') { +

События телеметрии временно недоступны.

+ } @else { + @if (telemetryState.telemetry.length === 0) { +

Событий пока нет.

+ } @else { +
+ + @for (t of uniqueTelemetryEventTypes(telemetryState.telemetry); track t) { + + } +
+ @if (filteredTelemetryEvents(telemetryState.telemetry).length === 0) { +

Нет событий выбранного типа.

+ } @else { +
+ + + + + + + + + + @for ( + row of filteredTelemetryEvents(telemetryState.telemetry); + track row.timestamp + '-' + row.event_type + '-' + $index; + let i = $index + ) { + + + + + + @if (isTelemetryRowExpanded(row, i)) { + + + + } + } + +
ВремяТипPayload
{{ formatUnixMs(row.timestamp) }}{{ row.event_type || '—' }}{{ eventDataPreview(row) }}
+ +
+
+ } + } + } +
+ } + + @if (activeTabIndex() === 1) { +
+

Сессия

+
+
Идентификатор
+
{{ state.detail.session.id }}
+
ID пользователя
+
{{ state.detail.session.user_id ?? '—' }}
+
Статус
+
{{ state.detail.session.status }}
+
Начало
+
{{ state.detail.session.started_at ?? '—' }}
+
Окончание
+
{{ state.detail.session.ended_at ?? '—' }}
+
Всего чанков
+
{{ state.detail.session.chunks_total ?? '—' }}
+
Всего событий
+
{{ state.detail.session.events_total ?? '—' }}
+
Название
+
{{ state.detail.session.title ?? '—' }}
+
+
+ +
+

Потоки

+ @if (state.detail.streams.length === 0) { +

Нет записей о потоках.

+ } @else { +
+ + + + + + + + + + + + @for (stream of state.detail.streams; track stream.stream_type) { + + + + + + + + } + +
Тип потокаЧанковДлительность (мс)URL плейлиста (как в API)URL плейлиста (абсолютный)
{{ stream.stream_type }}{{ stream.chunk_count ?? '—' }}{{ formatDurationMs(stream.duration_ms) }}{{ stream.playlist_url }}{{ streamResolvedPlaylistUrl(stream) }}
+
+ } +
+ +
+

Запрос событий

+
+
От (мс)
+
{{ telemetryState.fromMs }}
+
До (мс)
+
{{ telemetryState.toMs }}
+
ID сессии
+
+ @if (telemetryState.parsedMeta) { + {{ telemetryState.parsedMeta.session_id }} + } @else { + — + } +
+
Количество
+
+ @if (telemetryState.parsedMeta) { + {{ telemetryState.parsedMeta.count }} + } @else { + — + } +
+
Строк в выборке
+
{{ telemetryState.telemetry.length }}
+
Статус загрузки
+
{{ telemetryState.status }}
+
+
+ +
+

Исходный JSON

+
{{ detailPayloadJson(state.detail) }}
+
+ } + } + } + } + } +
diff --git a/src/app/features/sessions/sessions-list/sessions-list.component.ts b/src/app/features/sessions/sessions-list/sessions-list.component.ts new file mode 100644 index 0000000..274073d --- /dev/null +++ b/src/app/features/sessions/sessions-list/sessions-list.component.ts @@ -0,0 +1,106 @@ +import { AsyncPipe, NgClass } from '@angular/common'; +import { HttpErrorResponse } from '@angular/common/http'; +import { + ChangeDetectionStrategy, + Component, + inject, + signal, +} from '@angular/core'; +import { toObservable } from '@angular/core/rxjs-interop'; +import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { Router, RouterLink } from '@angular/router'; +import { TuiButton } from '@taiga-ui/core/components/button'; +import { TuiLink } from '@taiga-ui/core/components/link'; +import { TuiLoader } from '@taiga-ui/core/components/loader'; +import { TuiTitle } from '@taiga-ui/core/components/title'; +import { TuiInput } from '@taiga-ui/core/components/input'; +import { TuiChip } from '@taiga-ui/kit/components/chip'; +import { TuiPagination } from '@taiga-ui/kit/components/pagination'; +import { catchError, map, of, startWith, switchMap } from 'rxjs'; + +import { UserErrorNotifyService } from '../../../core/notifications/user-error-notify.service'; +import { SessionStatusChipClassesPipe } from '../../../core/sessions/session-status-chip-classes.pipe'; +import { SessionStatusPipe } from '../../../core/sessions/session-status.pipe'; +import { SessionsApiService } from '../../../core/services/sessions-api.service'; +import { formatTimestamp } from '../../../shared/utils/date-time.util'; + +@Component({ + selector: 'app-sessions-list', + imports: [ + AsyncPipe, + NgClass, + ReactiveFormsModule, + RouterLink, + TuiButton, + TuiChip, + ...TuiInput, + TuiLink, + TuiLoader, + TuiPagination, + TuiTitle, + SessionStatusChipClassesPipe, + SessionStatusPipe, + ], + templateUrl: './sessions-list.html', + styleUrl: './sessions-list.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SessionsListComponent { + private readonly api = inject(SessionsApiService); + private readonly router = inject(Router); + private readonly userErrors = inject(UserErrorNotifyService); + + protected readonly limit = 10; + protected readonly pageIndex = signal(0); + + protected readonly titleControl = new FormControl('', { + nonNullable: true, + validators: [Validators.required, Validators.minLength(1)], + }); + + protected readonly creating = signal(false); + + protected readonly listState$ = toObservable(this.pageIndex).pipe( + switchMap((page) => + this.api.listSessions(this.limit, page * this.limit).pipe( + map((data) => ({ status: 'ok' as const, data })), + catchError((e: HttpErrorResponse) => { + this.userErrors.notifyError(e, 'Список сессий'); + return of({ status: 'error' as const }); + }), + startWith({ status: 'loading' as const }), + ), + ), + ); + + protected onPageIndexChange(index: number): void { + this.pageIndex.set(index); + } + + protected pageCount(total: number): number { + return Math.max(1, Math.ceil(total / this.limit)); + } + + protected formatDate(value: string | null | undefined): string { + return formatTimestamp(value); + } + + protected createSession(): void { + if (this.titleControl.invalid || this.creating()) { + this.titleControl.markAsTouched(); + return; + } + const title = this.titleControl.value.trim(); + this.creating.set(true); + this.api.createSession({ title }).subscribe({ + next: (res) => { + this.creating.set(false); + void this.router.navigate(['/sessions', res.id]); + }, + error: (e: HttpErrorResponse) => { + this.creating.set(false); + this.userErrors.notifyError(e, 'Создание сессии'); + }, + }); + } +} diff --git a/src/app/features/sessions/sessions-list/sessions-list.css b/src/app/features/sessions/sessions-list/sessions-list.css new file mode 100644 index 0000000..c99a397 --- /dev/null +++ b/src/app/features/sessions/sessions-list/sessions-list.css @@ -0,0 +1,119 @@ +.page { + max-width: 960px; + margin: 0 auto; + padding: 1.5rem 1rem 3rem; + box-sizing: border-box; +} + +.heading { + margin-bottom: 1.5rem; +} + +.card { + padding: 1.25rem 1.5rem; + border-radius: var(--tui-radius-l); + background: var(--tui-background-elevation-1); + margin-bottom: 1.5rem; +} + +.section-title { + margin: 0 0 1rem; + font: var(--tui-font-heading-6); + color: var(--sg-color-subtitle); +} + +.create-row { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: flex-end; +} + +.create-field { + flex: 1 1 240px; + min-width: 0; +} + +button.accent-cta[tuiAppearance][data-appearance='primary'] { + min-inline-size: var(--sg-primary-action-min-inline-size); + --t-bg: var(--sg-color-accent); + background: var(--t-bg); + border-color: var(--sg-color-accent); + color: var(--sg-color-text); + font-weight: 400; +} + +button.accent-cta[tuiAppearance][data-appearance='primary']:hover:not([data-state='disabled']):not(:disabled) { + --t-bg: var(--tui-background-accent-1-hover); + filter: brightness(0.96); +} + +.loading-wrap { + display: flex; + justify-content: center; + padding: 3rem; +} + +.error { + color: var(--tui-status-negative); + margin: 0.75rem 0 0; +} + +.muted { + color: var(--tui-text-tertiary); +} + +.session-list { + list-style: none; + margin: 0; + padding: 0; +} + +.session-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 0; + border-bottom: 1px solid var(--tui-border-normal); +} + +.session-row:last-child { + border-bottom: none; +} + +.session-link { + font: var(--tui-font-text-m); +} + +.status-chip { + flex-shrink: 0; +} + +.meta { + margin-left: auto; + font: var(--tui-font-text-s); +} + +.pager { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--tui-border-normal); +} + +.total { + font: var(--tui-font-text-s); +} + +/* Активная страница пагинации (Taiga 5: активная кнопка — appearance primary) */ +tui-pagination button.t-button[tuiButton][tuiAppearance][data-appearance='primary'] { + --t-bg: var(--sg-color-accent); + background: var(--t-bg); + border-color: var(--sg-color-accent); + color: var(--sg-color-text); +} diff --git a/src/app/features/sessions/sessions-list/sessions-list.html b/src/app/features/sessions/sessions-list/sessions-list.html new file mode 100644 index 0000000..4ce9341 --- /dev/null +++ b/src/app/features/sessions/sessions-list/sessions-list.html @@ -0,0 +1,82 @@ +
+

Сессии прокторинга

+ +
+

Новая сессия

+
+ + + + + +
+
+ + @if (listState$ | async; as state) { + @switch (state.status) { + @case ('loading') { +
+ +
+ } + @case ('error') { +
+

Список временно недоступен.

+
+ } + @case ('ok') { +
+ @if (state.data.sessions.length === 0) { +

Сессий пока нет.

+ } @else { + + } +
+ Всего: {{ state.data.total }} + +
+
+ } + } + } +
diff --git a/src/app/features/sessions/telemetry-event-detail/telemetry-event-detail.component.ts b/src/app/features/sessions/telemetry-event-detail/telemetry-event-detail.component.ts new file mode 100644 index 0000000..e4d8990 --- /dev/null +++ b/src/app/features/sessions/telemetry-event-detail/telemetry-event-detail.component.ts @@ -0,0 +1,58 @@ +import { AsyncPipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core'; +import { toObservable } from '@angular/core/rxjs-interop'; +import { SafeHtml } from '@angular/platform-browser'; +import { TuiLoader } from '@taiga-ui/core/components/loader'; +import { Observable, of } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +import { + eventPayloadJson, + isKeyboardTelemetryEvent, + parseKeyboardHighlightKeyIds, + parseKeyboardVirtualKey, +} from '../../../core/keyboard/keyboard-payload.util'; +import { KeyboardSvgHighlightService } from '../../../core/keyboard/keyboard-svg-highlight.service'; +import type { ParsedEvent } from '../../../core/models/api.types'; +import { formatTimestamp } from '../../../shared/utils/date-time.util'; + +@Component({ + selector: 'app-telemetry-event-detail', + imports: [AsyncPipe, TuiLoader], + templateUrl: './telemetry-event-detail.html', + styleUrl: './telemetry-event-detail.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TelemetryEventDetailComponent { + private readonly keyboardSvg = inject(KeyboardSvgHighlightService); + + readonly event = input.required(); + + protected readonly keyboardModel = computed(() => { + const e = this.event(); + if (!isKeyboardTelemetryEvent(e)) { + return { kind: 'none' as const }; + } + const keyIds = parseKeyboardHighlightKeyIds(e.data); + const vk = parseKeyboardVirtualKey(e.data); + return { kind: 'keyboard' as const, vk, keyIds }; + }); + + protected readonly keyboardSvg$: Observable = toObservable(this.keyboardModel).pipe( + switchMap((m) => (m.kind === 'keyboard' ? this.keyboardSvg.svgWithHighlight(m.keyIds) : of(null))), + ); + + protected formatTime(ms: number): string { + return formatTimestamp(new Date(ms).toISOString()); + } + + protected payloadText(e: ParsedEvent): string { + return eventPayloadJson(e.data); + } + + /** Удобно в шаблоне, где нет сужения типа для `keyboardModel()`. */ + protected keyboardKeyIds(): string[] { + const m = this.keyboardModel(); + return m.kind === 'keyboard' ? m.keyIds : []; + } +} diff --git a/src/app/features/sessions/telemetry-event-detail/telemetry-event-detail.css b/src/app/features/sessions/telemetry-event-detail/telemetry-event-detail.css new file mode 100644 index 0000000..f25a43d --- /dev/null +++ b/src/app/features/sessions/telemetry-event-detail/telemetry-event-detail.css @@ -0,0 +1,88 @@ +.detail { + padding: 0.5rem 0 0; +} + +.detail-title { + margin: 0 0 0.75rem; + font: var(--tui-font-text-m); + font-weight: 600; + color: var(--tui-text-primary); +} + +.detail-kv { + display: grid; + grid-template-columns: minmax(8rem, 12rem) 1fr; + gap: 0.35rem 1rem; + margin: 0 0 1rem; + font: var(--tui-font-text-s); +} + +.detail-kv dt { + margin: 0; + color: var(--tui-text-tertiary); +} + +.detail-kv dd { + margin: 0; + word-break: break-word; +} + +.payload-json { + margin: 0; + padding: 0.75rem 1rem; + max-height: 200px; + overflow: auto; + border-radius: var(--tui-radius-m); + background: color-mix(in srgb, var(--tui-background-base) 88%, var(--tui-text-primary)); + font-size: 0.8rem; + line-height: 1.45; + white-space: pre-wrap; + word-break: break-word; +} + +.mono { + font-family: ui-monospace, monospace; + font-size: 0.92em; +} + +.keyboard-block { + margin-top: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid var(--tui-border-normal); +} + +.keyboard-title { + margin: 0 0 0.5rem; + font: var(--tui-font-text-s); + font-weight: 600; + color: var(--tui-text-secondary); +} + +.keyboard-svg-host { + max-width: 100%; + overflow: auto; + border-radius: var(--tui-radius-m); + background: transparent; +} + +.keyboard-svg-host ::ng-deep svg { + display: block; + width: 100%; + height: auto; + max-width: 800px; +} + +.keyboard-loading { + display: flex; + justify-content: center; + padding: 1rem; +} + +.muted { + color: var(--tui-text-tertiary); +} + +.small { + font: var(--tui-font-text-s); + margin: 0.5rem 0 0; +} diff --git a/src/app/features/sessions/telemetry-event-detail/telemetry-event-detail.html b/src/app/features/sessions/telemetry-event-detail/telemetry-event-detail.html new file mode 100644 index 0000000..50a563a --- /dev/null +++ b/src/app/features/sessions/telemetry-event-detail/telemetry-event-detail.html @@ -0,0 +1,44 @@ +
+

Подробности

+
+
Тип
+
{{ event().event_type || '—' }}
+
Время
+
{{ formatTime(event().timestamp) }}
+ @if (keyboardModel().kind === 'keyboard') { +
Виртуальный код (VK)
+
+ @if (keyboardModel().vk != null) { + {{ keyboardModel().vk }} (0x{{ keyboardModel().vk!.toString(16).toUpperCase() }}) + } @else { + — + } +
+ @if (keyboardKeyIds().length > 0) { +
Клавиши на схеме
+
{{ keyboardKeyIds().join(', ') }}
+ } + } +
Данные
+
{{ payloadText(event()) }}
+
+ + @if (keyboardModel().kind === 'keyboard') { +
+

Клавиатура (US)

+ @if (keyboardSvg$ | async; as svg) { +
+ } @else { +
+ +
+ } + @if (keyboardKeyIds().length === 0) { +

+ Не удалось сопоставить событие с клавишами на схеме (проверьте поля key_name / + modifiers или VK). +

+ } +
+ } +
diff --git a/src/app/shared/utils/date-time.util.ts b/src/app/shared/utils/date-time.util.ts new file mode 100644 index 0000000..82cb377 --- /dev/null +++ b/src/app/shared/utils/date-time.util.ts @@ -0,0 +1,23 @@ +/** + * Преобразует ISO-время (timestamptz), например `2026-04-06T23:58:27Z`, + * в человекочитаемый локальный формат. + */ +export function formatTimestamp(value: string | null | undefined): string { + if (!value) { + return '—'; + } + + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + return value; + } + + return new Intl.DateTimeFormat('ru-RU', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }).format(date); +} diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts new file mode 100644 index 0000000..9e11d42 --- /dev/null +++ b/src/environments/environment.prod.ts @@ -0,0 +1,6 @@ +/** Production-сборка (см. fileReplacements в angular.json). При деплое при необходимости меняйте здесь или генерируйте шагом CI. */ +export const environment = { + production: true, + apiFallbackOrigin: 'https://sparkguardian.ru', + apiBasePath: '/api/v1', +} as const; diff --git a/src/environments/environment.ts b/src/environments/environment.ts new file mode 100644 index 0000000..17719a0 --- /dev/null +++ b/src/environments/environment.ts @@ -0,0 +1,6 @@ +// Сгенерировано npm run env:sync — правьте .env и снова запустите sync +export const environment = { + production: false, + apiFallbackOrigin: "https://sparkguardian.ru", + apiBasePath: "/api/v1", +} as const; diff --git a/src/styles.css b/src/styles.css index 90d4ee0..03ada7a 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1 +1,68 @@ -/* You can add global styles to this file, and also import other style files */ +@import './styles/color-tokens.css'; +@import './styles/session-status-chips.css'; +@import './styles/sg-input-fields.css'; + +/* Базовая вёрстка под Taiga UI: тема и шрифты подключаются в angular.json */ +@font-face { + font-family: 'Tinkoff Sans'; + src: url('/fonts/TinkoffSans-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Tinkoff Sans'; + src: url('/fonts/TinkoffSans-Medium.ttf') format('truetype'); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Tinkoff Sans'; + src: url('/fonts/TinkoffSans-Bold.ttf') format('truetype'); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +html, +body { + height: 100%; + margin: 0; +} + +body { + box-sizing: border-box; + background: var(--sg-color-bg); + color: var(--sg-color-text); + font: var(--tui-font-text-m, 15px/1.5 'Tinkoff Sans', sans-serif); + font-family: 'Tinkoff Sans', sans-serif; +} + +*, +*::before, +*::after { + box-sizing: inherit; +} + +/* + * Taiga задаёт font через shorthand на кнопках/инпутах; без !important родной шрифт + * перебивает body. Трогать только при смене темы через переменные Taiga. + */ +* { + font-family: 'Tinkoff Sans', sans-serif !important; +} + +/* Form defaults */ +input::placeholder, +textarea::placeholder { + color: var(--sg-color-placeholder); + opacity: 1; +} + +/* Уведомления об ошибках: заголовок — Medium (500) */ +tui-notification-alert [tuiTitle] { + font-weight: 500; +} diff --git a/src/styles/color-tokens.css b/src/styles/color-tokens.css new file mode 100644 index 0000000..70979e5 --- /dev/null +++ b/src/styles/color-tokens.css @@ -0,0 +1,80 @@ +:root { + /* SparkGuardian color tokens */ + --sg-color-accent: #ffdb00; + --sg-color-bg: #f6f7f8; + --sg-color-text: #383839; + --sg-color-subtitle: #313132; + --sg-color-form-bg: #e8edf1; + --sg-color-placeholder: #6b6d6f; + + /* Поля ввода: .sg-tui-textfield на tui-textfield, .sg-native-input на нативных input */ + --sg-color-textfield-bg: #f3f4f7; + --sg-color-textfield-hover-bg: #eaeff3; + --sg-color-textfield-focus-bg: #ffffff; + --sg-color-textfield-focus-border: #333333; + --sg-color-textfield-focus-label: #333333; + --sg-textfield-radius: var(--tui-radius-l); + --sg-native-input-min-height: 2rem; + --sg-native-input-padding: 0.35rem 0.5rem; + /* Основные кнопки (например «Создать»): минимальная ширина без !important */ + --sg-primary-action-min-inline-size: 11rem; + + /* Semantic aliases */ + --sg-color-card-bg: #ffffff; + --sg-color-border: color-mix(in srgb, var(--sg-color-text) 12%, transparent); + --sg-color-danger: #d92d20; + + /* Taiga accent palette override (used by primary/secondary appearances, including pagination active item) */ + --tui-background-accent-1: var(--sg-color-accent); + --tui-background-accent-1-hover: color-mix(in srgb, var(--sg-color-accent) 92%, black); + --tui-background-accent-1-pressed: color-mix(in srgb, var(--sg-color-accent) 84%, black); + --tui-text-primary-on-accent-1: var(--sg-color-text); + + /* + * Bridge to Taiga tokens used in the app. + * This keeps styling centralized and avoids hardcoding colors in components. + */ + --tui-background-base: var(--sg-color-bg); + --tui-background-elevation-1: var(--sg-color-card-bg); + --tui-text-primary: var(--sg-color-text); + --tui-text-tertiary: color-mix(in srgb, var(--sg-color-text) 70%, white); + --tui-text-action: color-mix(in srgb, var(--sg-color-text) 80%, black); + --tui-border-normal: var(--sg-color-border); + --tui-focus: var(--sg-color-accent); + --tui-status-negative: var(--sg-color-danger); + + /* Чипы статусов сессии (кроме «Завершена» — без отдельной раскраски) */ + --sg-session-status-active-bg: color-mix(in srgb, #16a34a 18%, var(--sg-color-card-bg)); + --sg-session-status-active-fg: #166534; + --sg-session-status-active-border: color-mix(in srgb, #16a34a 38%, transparent); + + --sg-session-status-pending-bg: color-mix(in srgb, #d97706 18%, var(--sg-color-card-bg)); + --sg-session-status-pending-fg: #92400e; + --sg-session-status-pending-border: color-mix(in srgb, #d97706 34%, transparent); + + --sg-session-status-unknown-bg: color-mix(in srgb, var(--sg-color-text) 10%, var(--sg-color-card-bg)); + --sg-session-status-unknown-fg: var(--sg-color-text); + --sg-session-status-unknown-border: var(--sg-color-border); + + /* Встроенная SVG-клавиатура (public/KB_USA-standard.svg) */ + --sg-keyboard-font-family: 'Tinkoff Sans', system-ui, sans-serif; + --sg-keyboard-font-weight: 400; + --sg-keyboard-letter-spacing: 0.03em; + /* Не нажатые клавиши — светлый «фоновый» тон (как подложка интерфейса) */ + --sg-keyboard-key-surface-idle: color-mix( + in srgb, + var(--sg-color-form-bg) 78%, + var(--sg-color-border) + ); + --sg-keyboard-key-main: var(--sg-keyboard-key-surface-idle); + --sg-keyboard-key-mod: var(--sg-keyboard-key-surface-idle); + --sg-keyboard-key-other: var(--sg-keyboard-key-surface-idle); + --sg-keyboard-key-stroke: color-mix(in srgb, var(--sg-color-border) 65%, transparent); + /* Символы на клавишах — основной тёмный текст (тот же оттенок, что раньше был заливкой нажатой) */ + --sg-keyboard-ink: var(--sg-color-text); + --sg-keyboard-ink-soft: var(--tui-text-tertiary); + /* Нажатие: контрастный жёлтый акцент, символы на жёлтом — тёмные */ + --sg-keyboard-key-pressed-fill: var(--sg-color-accent); + --sg-keyboard-key-pressed-ink: var(--sg-color-text); + --sg-keyboard-highlight: var(--sg-keyboard-key-pressed-fill); +} diff --git a/src/styles/session-status-chips.css b/src/styles/session-status-chips.css new file mode 100644 index 0000000..eba2ce3 --- /dev/null +++ b/src/styles/session-status-chips.css @@ -0,0 +1,18 @@ +/* Раскраска чипов статуса сессии (см. SessionStatusChipClassesPipe). «Завершена» — без этих классов. */ +[tuiChip].status-chip.status-chip--active { + background: var(--sg-session-status-active-bg); + color: var(--sg-session-status-active-fg); + border-color: var(--sg-session-status-active-border); +} + +[tuiChip].status-chip.status-chip--pending { + background: var(--sg-session-status-pending-bg); + color: var(--sg-session-status-pending-fg); + border-color: var(--sg-session-status-pending-border); +} + +[tuiChip].status-chip.status-chip--unknown { + background: var(--sg-session-status-unknown-bg); + color: var(--sg-session-status-unknown-fg); + border-color: var(--sg-session-status-unknown-border); +} diff --git a/src/styles/sg-input-fields.css b/src/styles/sg-input-fields.css new file mode 100644 index 0000000..e95fe2e --- /dev/null +++ b/src/styles/sg-input-fields.css @@ -0,0 +1,110 @@ +/* + * Единый вид полей ввода SparkGuardian. + * Taiga: добавьте class="sg-tui-textfield" на . + * Нативные input/textarea/select: class="sg-native-input". + */ + +/* --- tui-textfield (Taiga Input) --- */ +tui-textfield.sg-tui-textfield[tuiAppearance][data-appearance='textfield'] { + --tui-focus: var(--sg-color-textfield-focus-border); + --t-shadow: none; + box-shadow: none; + background-color: var(--sg-color-textfield-bg); + outline: 1px solid transparent; + outline-offset: -1px; + border-width: 0; + border-radius: var(--sg-textfield-radius, var(--tui-radius-l)); + filter: none; + transition: + background-color 0.15s ease, + outline-color 0.15s ease; +} + +tui-textfield.sg-tui-textfield[tuiAppearance][data-appearance='textfield']::before, +tui-textfield.sg-tui-textfield[tuiAppearance][data-appearance='textfield']::after { + box-shadow: none; +} + +tui-textfield.sg-tui-textfield[tuiAppearance][data-appearance='textfield']:hover:not(:focus-within):not( + [data-state='disabled'] + ):not(._disabled), +tui-textfield.sg-tui-textfield[tuiAppearance][data-appearance='textfield'][data-state='hover']:not(:focus-within) { + --t-shadow: none; + box-shadow: none; + background-color: var(--sg-color-textfield-hover-bg); + outline: 1px solid transparent; + outline-offset: -1px; +} + +tui-textfield.sg-tui-textfield[tuiAppearance][data-appearance='textfield']:focus-within, +tui-textfield.sg-tui-textfield[tuiAppearance][data-appearance='textfield'][data-focus='true'], +tui-textfield.sg-tui-textfield[tuiAppearance][data-appearance='textfield']:focus-visible:not([data-focus='false']) { + --t-shadow: none; + box-shadow: none; + background-color: var(--sg-color-textfield-focus-bg); + outline: 1px solid var(--sg-color-textfield-focus-border); + outline-offset: -1px; +} + +tui-textfield.sg-tui-textfield[tuiAppearance][data-appearance='textfield'] input:not(.t-filler) { + background: transparent; + outline: none; + box-shadow: none; +} + +tui-textfield.sg-tui-textfield[tuiAppearance][data-appearance='textfield']:focus-within input:not(.t-filler), +tui-textfield.sg-tui-textfield[tuiAppearance][data-appearance='textfield'][data-focus='true'] input:not(.t-filler) { + outline: none; + box-shadow: none; +} + +tui-textfield.sg-tui-textfield[tuiAppearance][data-appearance='textfield']:focus-within [tuiLabel], +tui-textfield.sg-tui-textfield[tuiAppearance][data-appearance='textfield'][data-focus='true'] [tuiLabel] { + color: var(--sg-color-textfield-focus-label) !important; /* Taiga: color … !important на [tuiLabel] */ +} + +tui-textfield.sg-tui-textfield[tuiAppearance][data-appearance='textfield']:focus-within input:not(.t-filler)::placeholder, +tui-textfield.sg-tui-textfield[tuiAppearance][data-appearance='textfield'][data-focus='true'] + input:not(.t-filler)::placeholder { + color: var(--sg-color-textfield-focus-label); +} + +/* --- нативные поля (datetime-local, text, …) --- */ +.sg-native-input { + box-sizing: border-box; + min-block-size: var(--sg-native-input-min-height, 2rem); + padding: var(--sg-native-input-padding, 0.35rem 0.5rem); + border: 1px solid transparent; + outline: 1px solid transparent; + outline-offset: -1px; + border-radius: var(--sg-textfield-radius, var(--tui-radius-l)); + background: var(--sg-color-textfield-bg); + color: var(--sg-color-text); + font: inherit; + box-shadow: none; + transition: + background-color 0.15s ease, + outline-color 0.15s ease; +} + +.sg-native-input:hover:not(:disabled) { + background: var(--sg-color-textfield-hover-bg); + outline: 1px solid transparent; + outline-offset: -1px; +} + +.sg-native-input:focus { + outline: 1px solid var(--sg-color-textfield-focus-border); + outline-offset: -1px; + background: var(--sg-color-textfield-focus-bg); + border-color: transparent; +} + +.sg-native-input:focus-visible { + outline: 1px solid var(--sg-color-textfield-focus-border); + outline-offset: -1px; +} + +.sg-native-input:disabled { + opacity: var(--tui-disabled-opacity); +}