@@ -1,6 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' import { getToken } from 'next-auth/jwt' -import { trace } from '@opentelemetry/api' export const deprecatedPaths = ['/admin'] @@ -15,20 +14,15 @@ export async function middleware(req: NextRequest) { if (!token && !pathname.startsWith('/login')) { return NextResponse.redirect(new URL('/login', req.url)) - } + } const response = NextResponse.next() - const current = trace.getActiveSpan(); - - if (current) { - response.headers.set('server-timing', `traceparent;desc="00-${current.spanContext().traceId}-${current.spanContext().spanId}-01"`) - } - return response + return response } export const config = { matcher: [ - '/((?!api|_next/static|healthz|_next/image|favicon.ico|sitemap.xml|manifest.json|monitoring|svg|robots.txt|login|confirm|register|auth|change-password|reset|.*\\.[svg|json|png|jpg|jpeg|gif|css|js]).*)', + '/((?!api|_next/static|healthz|_next/image|favicon.ico|sitemap.xml|manifest.json|svg|robots.txt|login|confirm|register|auth|change-password|reset|.*\\.[svg|json|png|jpg|jpeg|gif|css|js]).*)', '/', ], } @@ -1,4 +1,10 @@ -.idea +.idea/ +.vscode/ +.next/ +.yarn/ +.husky/ +.gitlab/ Dockerfile docker-compose.yml README.md +node_modules/ \ No newline at end of file @@ -1,11 +1,10 @@ - /node_modules /.pnp -.pnp.js +.pnp* +.yarn/* .env* !.env.dist - /coverage # next.js @@ -9,6 +9,8 @@ default: .build_template: &default_build_job stage: Build + variables: + IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA script: - cp $ENV .env - docker compose build --push @@ -45,17 +47,21 @@ default: build_staging: <<: *default_build_job - variables: - ENV: $STAGING_ENV - IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + environment: + name: Staging + deployment_tier: staging + action: prepare + url: $DOMAIN only: - staging build_production: <<: *default_build_job - variables: - ENV: $PRODUCTION_ENV - IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + environment: + name: Production + deployment_tier: production + action: prepare + url: $DOMAIN only: - main @@ -1,11 +0,0 @@ -Options +FollowSymlinks -IndexIgnore */* - -RewriteEngine on - -# Если файл или директория существует — отдаём напрямую -RewriteCond %{REQUEST_FILENAME} !-f -RewriteCond %{REQUEST_FILENAME} !-d - -# Всё остальное — на index.php -RewriteRule . index.php @@ -1 +0,0 @@ -legacy-peer-deps=true @@ -0,0 +1 @@ +nodeLinker: node-modules @@ -1,26 +1,34 @@ -FROM node:22.22-alpine3.22 AS dependencies +FROM node:lts-alpine3.23 AS base WORKDIR /app -COPY package.json package-lock.json .npmrc ./ -RUN npm ci --no-audit --no-fund -FROM node:22.22-alpine3.22 AS builder +ENV NODE_ENV=${ENVIRONMENT:-production} -WORKDIR /app -COPY . . -COPY --from=dependencies /app/node_modules ./node_modules -RUN npm run build +COPY package.json yarn.lock .yarnrc.yml ./ -FROM node:22.22-alpine3.22 +RUN corepack enable +RUN --mount=type=cache,target=/root/.node/corepack/ corepack prepare -WORKDIR /app +FROM base AS dependencies + +RUN --mount=type=cache,target=/root/.cache/yarn \ + yarn install --immutable + +FROM base AS build -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 +ENV NEXT_TELEMETRY_DISABLED=1 -CMD ["npm", "run", "start"] \ No newline at end of file +COPY . . + +RUN --mount=from=dependencies,source=/app/node_modules,target=node_modules \ + --mount=type=cache,target=/app/.next/cache \ + --mount=type=cache,target=/root/.cache/yarn \ + yarn build + +FROM base + +COPY --from=dependencies /app/node_modules ./node_modules +COPY --from=build /app/.next .next +COPY --from=build /app/public public +COPY --from=build /app/next.config.js /app/next-i18next.config.js ./ +COPY --from=build /app/.next .next @@ -0,0 +1,36 @@ +# frontend + +Node.js **>= 20.9.0**, Yarn **4.9.2** (`packageManager` в `package.json`). + +## Установка + +```bash +yarn install +``` + +CI / Docker (строго по lockfile): + +```bash +yarn install --immutable +``` + +Windows, если `yarn` не в PATH: + +```bash +corepack yarn install +``` + +или: + +```bash +node .yarn/releases/yarn-4.9.2.cjs install +``` + +## Команды + +```bash +yarn dev # локальная разработка +yarn build # production-сборка +yarn start # запуск собранного приложения +yarn lint # ESLint +``` @@ -1,2 +0,0 @@ -[.ShellClassInfo] -LocalizedResourceName=@web,0 @@ -4,27 +4,32 @@ services: build: context: . dockerfile: Dockerfile + command: yarn start + ports: + - "3000:3000" env_file: ${ENV:-.env} + environment: + - NODE_ENV=${ENVIRONMENT:-production} restart: unless-stopped labels: &app-labels - traefik.enable=true - traefik.${PROVIDER:-docker}.network=${PROXY_NETWORK:-infrastructure} - - 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.ui-web-http.rule=HostRegexp(`$DOMAIN`) + - traefik.http.routers.ui-web-http.entrypoints=web + - traefik.http.routers.ui-web-http.tls=false + - traefik.http.routers.ui-web-http.service=ui + - traefik.http.routers.ui-web-http.middlewares=sts-header@file,https-redirect@file - - 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 + - traefik.http.routers.ui-web-https.rule=HostRegexp(`$DOMAIN`) + - traefik.http.routers.ui-web-https.entrypoints=websecure + - traefik.http.routers.ui-web-https.tls=true + - traefik.http.routers.ui-web-https.tls.certresolver=defaultresolver + - traefik.http.routers.ui-web-https.service=ui + + - traefik.http.services.ui.loadbalancer.server.port=3000 deploy: - replicas: ${REPLICAS:-1} + replicas: ${APP_REPLICAS:-1} labels: *app-labels update_config: parallelism: 1 @@ -1,5 +1,5 @@ { - "name": "frontend", + "name": "ui-web", "private": true, "version": "1.0.0", "imports": { @@ -14,6 +14,7 @@ "engines": { "node": ">=20.9.0" }, + "packageManager": "yarn@4.9.2", "keywords": [], "license": "MIT", "lint-staged": { @@ -24,27 +25,28 @@ }, "dependencies": { "@dqbd/tiktoken": "^1.0.7", - "@emotion/react": "^11.11.0", - "@emotion/styled": "^11.11.0", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", "@fontsource/roboto": "^4.5.8", - "@mui/icons-material": "^5.11.11", - "@mui/material": "^5.11.12", - "@mui/styled-engine-sc": "^5.11.11", - "@mui/x-date-pickers": "^6.6.0", + "@mui/icons-material": "^5.18.0", + "@mui/material": "^5.18.0", + "@mui/styled-engine-sc": "^5.14.12", + "@mui/system": "^5.18.0", + "@mui/x-date-pickers": "^7.29.4", "@next/bundle-analyzer": "^16.0.0", - "@reduxjs/toolkit": "^1.9.5", + "@opentelemetry/api": "^1.9.0", + "@reduxjs/toolkit": "^2.12.0", "@sentry/cli": "^2.58.4", "@sentry/nextjs": "^10.54.0", "@testing-library/user-event": "^14.6.1", "@types/cookie": "^0.5.1", "@types/intro.js": "^5.1.1", "@types/lodash": "^4.14.195", - "@types/node": "18.15.2", + "@types/node": "^22.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@types/react-syntax-highlighter": "^15.5.7", "@unleash/proxy-client-react": "^5.0.1", - "@vercel/otel": "^1.13.0", "axios": "^1.7.9", "base64-encode-file": "^1.0.7", "buffer": "^6.0.3", @@ -52,31 +54,27 @@ "cookie": "^0.5.0", "cross-env": "^7.0.3", "dayjs": "^1.11.8", - "draft-js": "^0.11.7", - "eslint-config-next": "^16.0.0", - "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-simple-import-sort": "^10.0.0", "framer-motion": "^12.23.12", "i18next": "^23.4.1", "intro.js": "^7.2.0", "intro.js-react": "^1.0.0", - "lint-staged": "^13.2.2", + "lint-staged": "^15.0.0", "lodash.debounce": "^4.0.8", "motion": "^12.23.12", "next": "^16.0.0", "next-auth": "^4.24.11", - "next-i18next": "^14.0.0", + "next-i18next": "^15.0.0", "nextstepjs": "^2.2.0", - "prettier": "^2.8.8", "react": "^19.0.0", "react-chartjs-2": "^5.2.0", - "react-cookie": "^4.1.1", + "react-cookie": "^7.2.0", "react-dom": "^19.0.0", - "react-draft-wysiwyg": "^1.15.0", "react-hook-form": "^7.54.2", - "react-i18next": "^13.0.3", + "react-i18next": "^15.0.0", + "react-is": "^19.0.0", "react-markdown": "^10.1.0", - "react-redux": "^8.0.5", + "react-redux": "^9.3.0", "react-syntax-highlighter": "^15.5.0", "react-transition-group": "^4.4.5", "remark-gfm": "^4.0.1", @@ -96,23 +94,20 @@ "@types/jest": "^29.5.14", "@types/lodash": "^4.14.195", "@types/lodash.debounce": "^4.0.9", - "@types/react-draft-wysiwyg": "^1.13.8", "@types/react-syntax-highlighter": "^15.5.7", "cross-fetch": "^4.1.0", "eslint": "^9.39.4", - "eslint-config-prettier": "^8.8.0", + "eslint-config-next": "^16.0.0", + "eslint-config-prettier": "^10.1.0", + "eslint-plugin-prettier": "^5.5.6", "husky": "^9.1.7", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "lint-staged": "^13.2.2", - "prettier": "^2.8.8", + "prettier": "^3.6.0", "ts-node": "^10.9.2", "typescript": "^5.7.3" }, "resolutions": { - "react-scripts/webpack-dev-server/yargs/yargs-parser": ">=18.1.2" - }, - "overrides": { "es5-ext@^0.10.50": "0.10.53", "@opentelemetry/semantic-conventions": "^1.41.0" },