@@ -1 +0,0 @@
-npm run build
\ No newline at end of file
@@ -7,7 +7,7 @@ import { Raleway } from 'next/font/google'
import { SessionProvider } from 'next-auth/react'
import { appWithTranslation } from 'next-i18next'
import { NextStep, NextStepProvider } from 'nextstepjs'
-import { FlagProvider } from '@unleash/proxy-client-react'
+import { FlagProvider, UnleashClient } from '@unleash/proxy-client-react'
import { store } from '#/app/store/store'
import { Error } from '#/shared'
@@ -90,30 +90,56 @@ function AppContent({
function App({ Component, pageProps: { session, ...pageProps } }: AppPropsWithLayout) {
useBlockTelegram()
- const { env } = useEnv()
+ const { env, loading: envLoading } = useEnv()
const unleashUrl = env?.NEXT_PUBLIC_UNLEASH_URL || process.env.NEXT_PUBLIC_UNLEASH_URL || ''
const unleashClientKey = env?.NEXT_PUBLIC_UNLEASH_CLIENT_KEY || process.env.NEXT_PUBLIC_UNLEASH_CLIENT_KEY || ''
const unleashAppName = env?.NEXT_PUBLIC_UNLEASH_APP_NAME || process.env.NEXT_PUBLIC_UNLEASH_APP_NAME || ''
+ const unleashClient = React.useMemo(() => {
+ if (envLoading) return null
+ if (!unleashUrl || !unleashClientKey || !unleashAppName) return null
+ return new UnleashClient({
+ url: unleashUrl,
+ clientKey: unleashClientKey,
+ appName: unleashAppName,
+ })
+ }, [envLoading, unleashAppName, unleashClientKey, unleashUrl])
+
+ React.useEffect(() => {
+ if (!unleashClient) return
+ unleashClient.start()
+ return () => {
+ unleashClient.stop?.()
+ }
+ }, [unleashClient])
+
+ if (envLoading) {
+ return null
+ }
+
return (
<>
-
+ {unleashClient ? (
+
+
+
+
+
+ ) : (
+ Component={Component}
+ pageProps={{ session, ...pageProps }}
+ refetchInterval={Number(env?.NEXT_PUBLIC_REFETCH_INTERVAL) || undefined}
+ />
-
+ )}
>
)
}
@@ -27,7 +27,8 @@
color: #a6a5a5;
font-size: 14px;
margin-bottom: 8px;
- width: 100%;
+ width: 100%;
+ font-family: inherit;
&_primary {
font-weight: 500;
@@ -47,7 +48,7 @@
outline: none;
font-size: 16px;
width: 100%;
- font-family: '__Raleway_9d395e' !important;
+ font-family: inherit;
&:focus {
border: none;
@@ -390,12 +390,20 @@ export function VoiceCloneSelect({
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onKeyDown={(e) => {
+ // Иначе MUI MenuList перехватывает печатные клавиши (typeahead) и перескакивает на другую опцию
+ if (e.key !== 'Escape') {
+ e.stopPropagation()
+ }
if (e.key === 'Enter') {
e.preventDefault()
- e.stopPropagation()
if (isMediaVoice(item)) void commitRename(item)
}
}}
+ onKeyUp={(e) => {
+ if (e.key !== 'Escape') {
+ e.stopPropagation()
+ }
+ }}
aria-label='Название голоса'
autoFocus
/>
@@ -2,7 +2,7 @@ import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
- integrations: [Sentry.replayIntegration({ maskAllText: false, blockAllMedia: false })],
+ integrations: [Sentry.replayIntegration({ maskAllText: false, maskAllInputs: false, blockAllMedia: false })],
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
sendDefaultPii: true,
@@ -8,7 +8,6 @@ export async function register() {
if (process.env.NEXT_RUNTIME === "edge") {
await import("../sentry.edge.config");
}
-
}
export const onRequestError = Sentry.captureRequestError;
@@ -3,96 +3,62 @@ stages:
- Deploy
default:
- image: docker:latest
+ image: docker:cli
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" $CI_REGISTRY --password-stdin
-build_staging:
+build:
stage: Build
- image: docker:cli
script:
- - export RELEASE="$(date -Iseconds)"
- - echo -e "\nRELEASE=$RELEASE\nNEXT_PUBLIC_RELEASE=$RELEASE\nCI=true" >> $ENV
- - cp $ENV .env
- - docker compose --env-file $ENV build
- - docker compose push
- environment:
- name: Staging
- url: https://staging.air.fail
- deployment_tier: staging
- services:
- - docker:dind
+ - cp $BUILD_ENV .env
+ - docker compose build --push
only:
+ - main
- staging
+ when: on_success
-deploy_staging:
+.deploy_template: &default_deploy_job
stage: Deploy
- image: docker:cli
+ services:
+ - docker:dind
variables:
- DOCKER_HOST: tcp://$STAGING_CLUSTER_HOST:2376
+ ENVIRONMENT: $CI_ENVIRONMENT_TIER
DOCKER_TLS_VERIFY: 1
DOCKER_CERT_PATH: "/certs"
- services:
- - docker:dind
- environment:
- name: Staging
- url: https://staging.air.fail
- deployment_tier: staging
- only:
- - staging
+ COMPOSE_ENV_FILES: $ENV
+ COMPOSE_REMOVE_ORPHANS: true
+ COMPOSE_PROJECT_NAME: $CI_PROJECT_NAME
+ when: on_success
before_script:
- - export RELEASE="$(date -Iseconds)"
- - echo -e "\nRELEASE=$RELEASE\nNEXT_PUBLIC_RELEASE=$RELEASE" >> $ENV
- - cp $ENV .env
- mkdir -p $DOCKER_CERT_PATH
- - echo "$STAGING_CLUSTER_CA" > $DOCKER_CERT_PATH/ca.pem
- - echo "$STAGING_CLUSTER_CERT" > $DOCKER_CERT_PATH/cert.pem
- - echo "$STAGING_CLUSTER_KEY" > $DOCKER_CERT_PATH/key.pem
+ - export ENVNAME=$(echo ${ENVIRONMENT} | tr '[:lower:]' '[:upper:]')
+ - export DOCKER_HOST="tcp://$(printenv "${ENVNAME}_CLUSTER_HOST"):2376"
+ - cp "$(printenv ${ENVNAME}_CLUSTER_CA)" $DOCKER_CERT_PATH/ca.pem
+ - cp "$(printenv ${ENVNAME}_CLUSTER_CERT)" $DOCKER_CERT_PATH/cert.pem
+ - cp "$(printenv ${ENVNAME}_CLUSTER_KEY)" $DOCKER_CERT_PATH/key.pem
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" $CI_REGISTRY --password-stdin
script:
- - docker compose pull
- - docker compose --env-file $ENV --project-name air-ui-web up -d
+ - sort -u -t '=' -k 1,1 $ENV $BUILD_ENV <(echo "RELEASE=$(echo -n $(date '+%D %X'))") > .env
+ - if [ "$(docker info --format '{{.Swarm.LocalNodeState}}' 2>/dev/null)" = "active" ]; then
+ docker stack deploy --prune --with-registry-auth -c <(docker compose config | grep -v "^name:") $COMPOSE_PROJECT_NAME;
+ else
+ docker compose up -d;
+ fi
-build_production:
- stage: Build
- script:
- - export RELEASE="$(date -Iseconds)"
- - echo -e "\nRELEASE=$RELEASE\nNEXT_PUBLIC_RELEASE=$RELEASE\nCI=true" >> $ENV
- - cp $ENV .env
- - docker compose -f stack.yml --env-file $ENV build
- - docker compose -f stack.yml push
+deploy_staging:
+ <<: *default_deploy_job
environment:
- name: Production
- url: https://app.air.fail
- deployment_tier: production
- services:
- - docker:dind
+ name: Staging
+ deployment_tier: staging
+ url: $DOMAIN
only:
- - main
+ - staging
deploy_production:
- stage: Deploy
- image: docker:cli
- variables:
- DOCKER_HOST: tcp://$PRODUCTION_CLUSTER_HOST:2376
- DOCKER_TLS_VERIFY: 1
- DOCKER_CERT_PATH: "/certs"
- services:
- - docker:dind
+ <<: *default_deploy_job
environment:
name: Production
- url: https://app.air.fail
deployment_tier: production
+ url: $DOMAIN
only:
- - main
- before_script:
- - export RELEASE="$(date -Iseconds)"
- - echo -e "\nRELEASE=$RELEASE\nNEXT_PUBLIC_RELEASE=$RELEASE" >> $ENV
- - cp $ENV .env
- - mkdir -p $DOCKER_CERT_PATH
- - echo "$PRODUCTION_CLUSTER_CA" > $DOCKER_CERT_PATH/ca.pem
- - echo "$PRODUCTION_CLUSTER_CERT" > $DOCKER_CERT_PATH/cert.pem
- - echo "$PRODUCTION_CLUSTER_KEY" > $DOCKER_CERT_PATH/key.pem
- - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" $CI_REGISTRY --password-stdin
- script:
- - docker stack deploy --prune --with-registry-auth --resolve-image=always --compose-file stack.yml --detach frontend
\ No newline at end of file
+ - main
\ No newline at end of file
@@ -1,3 +0,0 @@
-
-[auth]
-token=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
@@ -1,26 +1,36 @@
-FROM node:alpine as dependencies
+FROM node:22.22-alpine3.22 AS dependencies
WORKDIR /app
-COPY package.json ./package.json
-RUN --mount=type=cache,target=/root/.npm npm install
+COPY package.json package-lock.json ./
+RUN npm ci --no-audit --no-fund
-FROM node:alpine as builder
+FROM node:22.22-alpine3.22 AS builder
WORKDIR /app
COPY . .
COPY --from=dependencies /app/node_modules ./node_modules
RUN npm run build
-FROM node:alpine as final
+FROM node:22.22-alpine3.22 AS production
WORKDIR /app
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
+COPY --from=builder /app/package-lock.json ./package-lock.json
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/next.config.js ./next.config.js
COPY --from=builder /app/next-i18next.config.js ./next-i18next.config.js
+CMD ["npm", "run", "start"]
-CMD ["npm", "run", "start"]
\ No newline at end of file
+
+FROM node:22.22-alpine3.22 AS dev
+
+WORKDIR /app
+
+COPY . .
+RUN npm install
+
+CMD ["npm", "run", "dev"]
\ No newline at end of file
@@ -1,9 +0,0 @@
-FROM node:alpine as dependencies
-
-WORKDIR /app
-
-COPY . .
-RUN npm install
-
-
-CMD ["npm", "run", "dev"]
\ No newline at end of file
@@ -1,23 +0,0 @@
-.DEFAULT_GOAL=start
-
-# Setup environment commands by specific shell (Powershell or Bash)
-ifeq ($(OS),Windows_NT)
- COPY_ENV=xcopy .env.dist .env /D /I
- WSL=wsl
-else
- COPY_ENV=cp -n .env.dist .env
-endif
-
-start:
- $(COPY_ENV)
- $(WSL) docker compose -f docker-compose.debug.yml up
-.PHONY=start
-
-rebuild:
- $(COPY_ENV)
- $(WSL) docker compose -f docker-compose.debug.yml up --build
-.PHONY=rebuild
-
-stop:
- $(WSL) docker compose -f docker-compose.debug.yml down --remove-orphans -v
-.PHONY=stop
\ No newline at end of file
@@ -1,17 +0,0 @@
-services:
- app:
- build:
- context: .
- dockerfile: Dockerfile.dev
- container_name: frontend
- restart: unless-stopped
- volumes:
- - .:/app
- ports:
- - '3000:3000'
- env_file:
- - .env
-
-networks:
- default:
- name: 'air'
@@ -0,0 +1,6 @@
+services:
+ app:
+ build:
+ target: dev
+ ports:
+ - "3000:3000"
\ No newline at end of file
@@ -1,34 +1,49 @@
services:
app:
- image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
+ image: ${CI_REGISTRY_IMAGE:-air/ui/web}:${CI_COMMIT_SHA:-latest}
build:
context: .
dockerfile: Dockerfile
+ target: production
+ env_file: ${ENV:-.env}
restart: unless-stopped
- networks:
- - infrastructure
- env_file:
- - .env
- labels:
+ labels: &app-labels
- traefik.enable=true
- - traefik.docker.network=infrastructure
+ - traefik.${PROVIDER:-docker}.network=${PROXY_NETWORK:-infrastructure}
- - traefik.http.routers.frontend-web-http.rule=Host(`$DOMAIN`)
+ - traefik.http.routers.frontend-web-http.rule=HostRegexp(`$DOMAIN`)
- traefik.http.routers.frontend-web-http.entrypoints=web
- traefik.http.routers.frontend-web-http.tls=false
- traefik.http.routers.frontend-web-http.service=frontend
- traefik.http.routers.frontend-web-http.middlewares=sts-header@file,https-redirect@file
- - traefik.http.routers.frontend-web-https.rule=Host(`$DOMAIN`)
+ - traefik.http.routers.frontend-web-https.rule=HostRegexp(`$DOMAIN`)
- traefik.http.routers.frontend-web-https.entrypoints=websecure
- traefik.http.routers.frontend-web-https.tls=true
- traefik.http.routers.frontend-web-https.tls.certresolver=defaultresolver
- traefik.http.routers.frontend-web-https.service=frontend
- traefik.http.services.frontend.loadbalancer.server.port=3000
+ deploy:
+ replicas: ${REPLICAS:-1}
+ labels: *app-labels
+ update_config:
+ parallelism: 1
+ delay: 10s
+ order: start-first
+ restart_policy:
+ condition: on-failure
+ delay: 5s
+ max_attempts: 3
+ window: 30s
+ placement:
+ constraints:
+ - node.role == worker
+ - node.labels.type != observer
+ networks:
+ - infrastructure
networks:
infrastructure:
+ name: ${PROXY_NETWORK:-infrastructure}
external: true
- tracing:
- external: true
\ No newline at end of file
@@ -50,5 +50,6 @@ const { withSentryConfig } = require("@sentry/nextjs");
module.exports = withSentryConfig(analyzerConfig, {
silent: !process.env.CI,
widenClientFileUpload: true,
- tunnelRoute: "/monitoring"
+ tunnelRoute: process.env.SENTRY_TUNNEL_PATH ?? '/monitoring',
+ debug: process.env.SENTRY_DEBUG ?? false
});
@@ -9,8 +9,7 @@
"start": "next start",
"build": "next build",
"dev": "next dev",
- "lint": "next lint",
- "prepare": "husky"
+ "lint": "next lint"
},
"engines": {
"node": ">=12.0.0"
@@ -1,48 +0,0 @@
-services:
- app:
- image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- build:
- context: .
- dockerfile: Dockerfile
- networks:
- - infrastructure
- deploy:
- replicas: 1
- update_config:
- parallelism: 1
- delay: 10s
- order: start-first
- restart_policy:
- condition: on-failure
- delay: 5s
- max_attempts: 3
- window: 30s
- placement:
- constraints:
- - node.role == worker
- labels:
- - traefik.enable=true
- - traefik.swarm.network=infrastructure
-
- - traefik.http.routers.frontend-web-http.rule=Host(`$DOMAIN`)
- - traefik.http.routers.frontend-web-http.entrypoints=web
- - traefik.http.routers.frontend-web-http.tls=false
- - traefik.http.routers.frontend-web-http.service=frontend
- - traefik.http.routers.frontend-web-http.middlewares=sts-header@file,https-redirect@file,fallback@file
-
- - traefik.http.routers.frontend-web-https.rule=Host(`$DOMAIN`)
- - traefik.http.routers.frontend-web-https.entrypoints=websecure
- - traefik.http.routers.frontend-web-https.tls=true
- - traefik.http.routers.frontend-web-https.tls.certresolver=defaultresolver
- - traefik.http.routers.frontend-web-https.service=frontend
-
- - traefik.http.routers.frontend-web-https.middlewares=fallback@file
-
- - traefik.http.services.frontend.loadbalancer.server.port=3000
- env_file:
- - .env
-
-networks:
- infrastructure:
- name: infrastructure
- external: true