Compare commits

...
3 Commits
Author SHA1 Message Date
Denozordec 009011a917 chore(docs): обновить инструкции по локальному запуску проекта
Docker images / prepare-release (push) Successful in 7s
Docker images / backend-image (push) Successful in 3m42s
Docker images / frontend-image (push) Successful in 2m40s
Docker images / updater-image (push) Successful in 53s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 8s
2026-06-30 20:07:20 +07:00
Denozordec 399871f4f9 fix(config): улучшить обработку URL для бэкенда и увеличить таймаут запросов
Docker images / prepare-release (push) Successful in 6s
Docker images / backend-image (push) Successful in 1m36s
Docker images / frontend-image (push) Successful in 1m39s
Docker images / updater-image (push) Successful in 50s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 8s
2026-05-12 23:19:51 +07:00
Denozordec 158fc36294 fix(api): увеличить лимит размера тела запроса и улучшить обработку URL для API
Docker images / prepare-release (push) Successful in 6s
Docker images / backend-image (push) Successful in 1m36s
Docker images / frontend-image (push) Successful in 1m40s
Docker images / updater-image (push) Successful in 38s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 6s
2026-05-12 23:11:23 +07:00
11 changed files with 155 additions and 17 deletions
+16
View File
@@ -0,0 +1,16 @@
# CodeGraph data files
# These are local to each machine and should not be committed
# Database
*.db
*.db-wal
*.db-shm
# Cache
cache/
# Logs
*.log
# Hook markers
.dirty
+30 -5
View File
@@ -5,15 +5,40 @@ alwaysApply: true
# Локальный запуск проекта
Для запуска всего проекта в dev-режиме поднимать два процесса:
## Требования
- **Node.js 22**, npm с workspaces.
- Первый раз (или после смены зависимостей): `npm install` из корня репозитория.
- Backend: скопировать `backend/.env.example` → `backend/.env` (по умолчанию `PORT=8000`, `CORS_ORIGIN=http://localhost:3000`).
## Запуск (два процесса)
Из корня репозитория поднять **два** long-running процесса в **отдельных** терминалах:
```powershell
npm run dev
npm --prefix backend run dev
```
- Frontend: `http://localhost:3000`
- Backend: `http://localhost:8000`
- Health check backend: `http://localhost:8000/health`
| Сервис | URL | Проверка |
|--------|-----|----------|
| Frontend (Next.js 16, Turbopack) | http://localhost:3000 | открыть в браузере |
| Backend (Fastify) | http://localhost:8000 | `GET /health` |
Если пользователь просит “запусти проект”, “запусти фронт и бэк” или похожую команду, сначала проверь уже запущенные терминалы, затем запускай эти две команды отдельными long-running процессами.
Проверка backend в PowerShell:
```powershell
Invoke-WebRequest -Uri http://localhost:8000/health -UseBasicParsing | Select-Object -ExpandProperty Content
```
Ожидаемый ответ: `{"status":"ok",...}`.
## Поведение агента
Если пользователь просит «запусти проект», «запусти фронт и бэк» или похожее:
1. Сначала проверить уже запущенные терминалы — не дублировать процессы.
2. Запустить обе команды выше как фоновые long-running процессы.
3. Дождаться готовности: frontend — `Ready`, backend — `Server listening` / успешный `/health`.
Подробности архитектуры и env — `README.md`, раздел «Запуск».
+4
View File
@@ -10,3 +10,7 @@
4. Локальный hook `.githooks/commit-msg` отклоняет subject без кириллицы; подключение — `npm install` / `npm run prepare`.
Полные правила: `.cursor/rules/commit-messages-ru.mdc`, semver — `.cursor/rules/release-versioning.mdc`.
## Локальный запуск
Два процесса из корня: `npm run dev` (frontend :3000) и `npm --prefix backend run dev` (backend :8000). Перед первым запуском — `npm install`, для backend — `backend/.env` из `backend/.env.example`. Подробности — `.cursor/rules/dev-run-command.mdc` и `README.md`.
+13
View File
@@ -0,0 +1,13 @@
import type { NextRequest } from "next/server"
import { proxyBackendRequest } from "@/lib/proxy-backend-request"
export const runtime = "nodejs"
export const dynamic = "force-dynamic"
export const maxDuration = 600
export async function GET(request: NextRequest) {
return proxyBackendRequest(request, "/api/system/database/backup", {
method: "GET",
forwardRequestBody: false,
})
}
+10
View File
@@ -0,0 +1,10 @@
import type { NextRequest } from "next/server"
import { proxyBackendRequest } from "@/lib/proxy-backend-request"
export const runtime = "nodejs"
export const dynamic = "force-dynamic"
export const maxDuration = 600
export async function POST(request: NextRequest) {
return proxyBackendRequest(request, "/api/system/database/restore", { method: "POST" })
}
+2
View File
@@ -27,6 +27,8 @@ import { refreshScheduler, stopScheduler } from "./services/scheduler.js"
// ── app factory ────────────────────────────────────────────────────────────────
const app = Fastify({
bodyLimit: 512 * 1024 * 1024,
requestTimeout: 10 * 60 * 1000,
logger: {
transport: {
target: "pino-pretty",
+3
View File
@@ -0,0 +1,3 @@
export function backendInternalUrl(): string {
return (process.env.BACKEND_INTERNAL_URL ?? "http://127.0.0.1:8000").replace(/\/$/, "")
}
+50
View File
@@ -0,0 +1,50 @@
import type { NextRequest } from "next/server"
import { backendInternalUrl } from "@/lib/backend-internal-url"
type ProxyBackendRequestOptions = {
method?: string
forwardRequestBody?: boolean
}
export async function proxyBackendRequest(
request: NextRequest,
backendPath: string,
options: ProxyBackendRequestOptions = {},
): Promise<Response> {
const method = options.method ?? request.method
const headers = new Headers()
for (const [key, value] of request.headers.entries()) {
const lower = key.toLowerCase()
if (lower === "host" || lower === "connection") continue
headers.set(key, value)
}
const fetchInit: RequestInit & { duplex?: "half" } = {
method,
headers,
cache: "no-store",
}
const forwardRequestBody = options.forwardRequestBody ?? !["GET", "HEAD"].includes(method)
if (forwardRequestBody && request.body) {
fetchInit.body = request.body
fetchInit.duplex = "half"
}
try {
const upstream = await fetch(`${backendInternalUrl()}${backendPath}`, fetchInit)
const responseHeaders = new Headers()
const contentType = upstream.headers.get("Content-Type")
const contentDisposition = upstream.headers.get("Content-Disposition")
if (contentType) responseHeaders.set("Content-Type", contentType)
if (contentDisposition) responseHeaders.set("Content-Disposition", contentDisposition)
return new Response(upstream.body, {
status: upstream.status,
headers: responseHeaders,
})
} catch (error) {
const message = error instanceof Error ? error.message : "Не удалось связаться с бекендом"
return Response.json({ error: message }, { status: 502 })
}
}
+7 -7
View File
@@ -1,16 +1,16 @@
import type { NextConfig } from "next";
const backendInternalUrl = (process.env.BACKEND_INTERNAL_URL ?? "http://127.0.0.1:8000").replace(
/\/$/,
"",
);
import { backendInternalUrl } from "./lib/backend-internal-url";
const nextConfig: NextConfig = {
output: "standalone",
experimental: {
proxyClientMaxBodySize: "512mb",
},
async rewrites() {
const backendUrl = backendInternalUrl();
return [
{ source: "/health", destination: `${backendInternalUrl}/health` },
{ source: "/api/:path*", destination: `${backendInternalUrl}/api/:path*` },
{ source: "/health", destination: `${backendUrl}/health` },
{ source: "/api/:path*", destination: `${backendUrl}/api/:path*` },
];
},
};
+19 -4
View File
@@ -1,9 +1,19 @@
import { ApiClientError } from "@/shared/api/http-client"
import { configuredBackendUrl } from "@/lib/backend-url"
const MAX_RESTORE_BYTES = 512 * 1024 * 1024
function trimBaseUrl(baseUrl: string): string {
return baseUrl.replace(/\/$/, "")
}
function resolveDatabaseApiUrl(baseUrl: string, path: string): string {
if (configuredBackendUrl().kind === "same-origin") {
return path
}
return `${trimBaseUrl(baseUrl)}${path}`
}
function parseFilename(contentDisposition: string | null, fallback: string): string {
if (!contentDisposition) return fallback
const utfMatch = /filename\*=UTF-8''([^;]+)/i.exec(contentDisposition)
@@ -22,7 +32,7 @@ function parseFilename(contentDisposition: string | null, fallback: string): str
export async function downloadSystemDatabaseBackup(
baseUrl: string,
): Promise<{ blob: Blob; filename: string }> {
const res = await fetch(`${trimBaseUrl(baseUrl)}/api/system/database/backup`)
const res = await fetch(resolveDatabaseApiUrl(baseUrl, "/api/system/database/backup"))
if (!res.ok) {
const payload = await res.json().catch(() => undefined)
const msg =
@@ -40,11 +50,16 @@ export async function downloadSystemDatabaseBackup(
}
export async function restoreSystemDatabaseBackup(baseUrl: string, file: File): Promise<void> {
const body = await file.arrayBuffer()
const res = await fetch(`${trimBaseUrl(baseUrl)}/api/system/database/restore`, {
if (file.size > MAX_RESTORE_BYTES) {
throw new ApiClientError(
`Файл больше ${MAX_RESTORE_BYTES / (1024 * 1024)} МБ — уменьшите бэкап или обратитесь к администратору`,
413,
)
}
const res = await fetch(resolveDatabaseApiUrl(baseUrl, "/api/system/database/restore"), {
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
body,
body: file,
})
if (!res.ok) {
const payload = await res.json().catch(() => undefined)
+1 -1
View File
File diff suppressed because one or more lines are too long