deleted todo, updated docs

This commit is contained in:
Dmitri 2026-07-07 11:30:24 +02:00
parent 7bfdae0519
commit 9c12c3f539
8 changed files with 149 additions and 83 deletions

View File

@ -192,7 +192,8 @@ rm -rf data && docker-compose up
docker-compose up docker-compose up
``` ```
Access at `http://localhost:3000` Access the frontend at `http://localhost:3001`, the backend API at
`http://localhost:3000/api`, and Swagger at `http://localhost:3000/api-docs`.
## Environment Variables ## Environment Variables
@ -204,7 +205,7 @@ DB_HOST=localhost
DB_PORT=5432 DB_PORT=5432
DB_NAME=app_39215 DB_NAME=app_39215
DB_USER=app_39215 DB_USER=app_39215
DB_PASSWORD=your-password DB_PASS=your-password
# JWT # JWT
SECRET_KEY=your-secret-key SECRET_KEY=your-secret-key

View File

@ -91,7 +91,7 @@ Services with domain-specific business logic beyond simple CRUD.
| project_element_defaults | `project_element_defaults.ts` | Element defaults with reset/diff | 34 | | project_element_defaults | `project_element_defaults.ts` | Element defaults with reset/diff | 34 |
| global_ui_control_defaults | `global_ui_control_defaults.ts` | Global defaults CRUD service for system controls | 6 | | global_ui_control_defaults | `global_ui_control_defaults.ts` | Global defaults CRUD service for system controls | 6 |
| project_ui_control_settings | `project_ui_control_settings.ts` | Transactional find/upsert/delete for project UI-control overrides | 51 | | project_ui_control_settings | `project_ui_control_settings.ts` | Transactional find/upsert/delete for project UI-control overrides | 51 |
| videoProcessing | `videoProcessing.ts` | FFmpeg video reversal for transition videos with single-worker queue, `-threads 1`, hard timeout, metadata logs, and circuit breaker | ~240 | | videoProcessing | `videoProcessing.ts` | Direct bundled `ffmpeg-static`/`ffprobe-static` execution for transition video reversal and metadata probing with single-worker queue, `-threads 1`, hard timeout, metadata logs, and circuit breaker | ~400 |
### 3. Specialized Module Directories ### 3. Specialized Module Directories

View File

@ -24,6 +24,9 @@ The suite is split into three layers:
file-storage provider overrides. file-storage provider overrides.
- Circuit breaker behavior for ignored failures, recorded failures, and open - Circuit breaker behavior for ignored failures, recorded failures, and open
breaker rejection. breaker rejection.
- Bundled FFmpeg/FFprobe video processing smoke coverage: availability,
metadata probing, and reverse video generation through
`backend/src/services/videoProcessing.ts`.
- Auth service password reset, password update rejection rules, and email - Auth service password reset, password update rejection rules, and email
verification token behavior with real bcrypt and DB writes. verification token behavior with real bcrypt and DB writes.
- Route ID/body ID update contract enforcement. - Route ID/body ID update contract enforcement.
@ -52,6 +55,11 @@ E2E tests bind a local HTTP listener on `127.0.0.1` with an ephemeral port. In
restricted sandboxes this may require elevated permission for local socket restricted sandboxes this may require elevated permission for local socket
binding. binding.
The video processing unit test uses the bundled `ffmpeg-static` binary to
generate a tiny temporary fixture and then exercises `ffprobe-static` plus the
backend reverse-video service. It skips only if `ffmpeg-static` does not expose
a local binary path.
## Test Helpers ## Test Helpers
`backend/tests/http-test-utils.ts` provides `startTestServer(app)`, which starts `backend/tests/http-test-utils.ts` provides `startTestServer(app)`, which starts

View File

@ -57,6 +57,10 @@ interface ProcessResult {
stderr: string; stderr: string;
} }
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function parseFrameRate(value: unknown): number | null { function parseFrameRate(value: unknown): number | null {
if (!value) { if (!value) {
return null; return null;
@ -103,7 +107,15 @@ function toError(value: unknown): Error {
} }
function isMediaProbeOutput(value: unknown): value is MediaProbeOutput { function isMediaProbeOutput(value: unknown): value is MediaProbeOutput {
return Boolean(value) && typeof value === 'object'; if (!isRecord(value)) {
return false;
}
const { streams, format } = value;
return (
(streams === undefined || Array.isArray(streams)) &&
(format === undefined || isRecord(format))
);
} }
function getFfmpegExecutablePath(): string { function getFfmpegExecutablePath(): string {
@ -115,7 +127,7 @@ function getFfmpegExecutablePath(): string {
async function runProcess( async function runProcess(
executablePath: string, executablePath: string,
args: string[], args: readonly string[],
options: { timeoutMs: number; timeoutMessage: string }, options: { timeoutMs: number; timeoutMessage: string },
): Promise<ProcessResult> { ): Promise<ProcessResult> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -164,7 +176,7 @@ async function runProcess(
finish( finish(
new Error( new Error(
`Process failed: ${executablePath} exited with code ${code ?? 'null'} and signal ${signal ?? 'null'}`, `Process failed: ${executablePath} exited with code ${code ?? 'null'} and signal ${signal ?? 'null'}: ${stderr}`,
), ),
); );
}); });

View File

@ -0,0 +1,117 @@
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { promises as fs } from 'node:fs';
import { createRequire } from 'node:module';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import {
isFFmpegAvailable,
probeMediaMetadata,
reverseVideo,
} from '../src/services/videoProcessing.ts';
const loadCommonJsModule = createRequire(import.meta.url);
const ffmpegStaticValue: unknown = loadCommonJsModule('ffmpeg-static');
const ffmpegPath = typeof ffmpegStaticValue === 'string' ? ffmpegStaticValue : null;
async function runProcess(
executablePath: string,
args: readonly string[],
): Promise<void> {
await new Promise<void>((resolve, reject) => {
const child = spawn(executablePath, args, {
stdio: ['ignore', 'ignore', 'pipe'],
});
let stderr = '';
child.stderr.setEncoding('utf8');
child.stderr.on('data', (chunk: string) => {
stderr += chunk;
});
child.on('error', reject);
child.on('close', (code, signal) => {
if (code === 0) {
resolve();
return;
}
reject(
new Error(
`Process failed with code ${code ?? 'null'} and signal ${signal ?? 'null'}: ${stderr}`,
),
);
});
});
}
async function createFixtureVideo(filePath: string): Promise<void> {
if (!ffmpegPath) {
throw new Error('Bundled FFmpeg binary is unavailable');
}
await runProcess(ffmpegPath, [
'-y',
'-f',
'lavfi',
'-i',
'testsrc=size=32x32:rate=5',
'-f',
'lavfi',
'-i',
'sine=frequency=1000:sample_rate=44100',
'-t',
'0.4',
'-c:v',
'libx264',
'-pix_fmt',
'yuv420p',
'-c:a',
'aac',
filePath,
]);
}
void test('video processing uses bundled FFmpeg and FFprobe binaries', async (t) => {
if (!ffmpegPath) {
t.skip('ffmpeg-static did not provide a bundled binary path');
return;
}
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'video-processing-test-'));
const inputPath = path.join(tempDir, 'input.mp4');
const reversedPath = path.join(tempDir, 'reversed.mp4');
try {
await createFixtureVideo(inputPath);
assert.equal(await isFFmpegAvailable(), true);
const inputMetadata = await probeMediaMetadata(inputPath);
assert.equal(inputMetadata.widthPx, 32);
assert.equal(inputMetadata.heightPx, 32);
assert.equal(inputMetadata.frameRate, 5);
assert.ok(
inputMetadata.durationSec !== null && inputMetadata.durationSec > 0,
'expected probeMediaMetadata to read a positive duration',
);
const reversedBuffer = await reverseVideo(
await fs.readFile(inputPath),
'input.mp4',
);
assert.ok(reversedBuffer.length > 0);
await fs.writeFile(reversedPath, reversedBuffer);
const reversedMetadata = await probeMediaMetadata(reversedPath);
assert.equal(reversedMetadata.widthPx, 32);
assert.equal(reversedMetadata.heightPx, 32);
assert.ok(
reversedMetadata.durationSec !== null && reversedMetadata.durationSec > 0,
'expected reversed video to have a positive duration',
);
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
});

View File

@ -12,7 +12,7 @@
## Run services: ## Run services:
1. Install docker compose (https://docs.docker.com/compose/install/) 1. Install Docker Compose (https://docs.docker.com/compose/install/)
2. Move to `docker` folder. All next steps should be done from this folder. 2. Move to `docker` folder. All next steps should be done from this folder.
@ -22,7 +22,7 @@
``` chmod +x start-backend.sh && chmod +x wait-for-it.sh ``` ``` chmod +x start-backend.sh && chmod +x wait-for-it.sh ```
4. Download dependend projects for services. 4. Download dependent projects for services.
5. Review the docker-compose.yml file. Make sure that all services have Dockerfiles. Only db service doesn't require a Dockerfile. 5. Review the docker-compose.yml file. Make sure that all services have Dockerfiles. Only db service doesn't require a Dockerfile.
@ -32,7 +32,7 @@
7.1. With an empty database `rm -rf data && docker-compose up` 7.1. With an empty database `rm -rf data && docker-compose up`
7.2. With a stored (from previus runs) database data `docker-compose up` 7.2. With stored data from previous runs `docker-compose up`
8. Check the services: 8. Check the services:
@ -43,4 +43,3 @@
9. Stop services: 9. Stop services:
9.1. Just press `Ctr+C` 9.1. Just press `Ctr+C`

View File

@ -6,7 +6,7 @@ services:
tty: true # docker run -t tty: true # docker run -t
environment: environment:
- FRONT_PORT=3001 - FRONT_PORT=3001
- NEXT_PUBLIC_BACK_API=http://localhost:3000 - NEXT_PUBLIC_BACK_API=http://localhost:3000/api
ports: ports:
- "3001:3001" - "3001:3001"
logging: logging:
@ -16,7 +16,7 @@ services:
max-file: "3" max-file: "3"
db: db:
image: postgres image: postgres:14
volumes: volumes:
- ./data/db:/var/lib/postgresql/data - ./data/db:/var/lib/postgresql/data
environment: environment:

View File

@ -1,71 +0,0 @@
# TODO по улучшению проекта
Дата исследования: 2026-06-28
Цель: строгий, но не enterprise-heavy backlog для небольшого production-проекта. Система должна стать более предсказуемой: явные boundaries, единые contracts, единая модель доступа, единый подход к server state и API validation. Улучшения выполняются маленькими PR без большого rewrite.
## Принципы
- Сначала фиксируем текущее поведение тестами/чеклистами, потом рефакторим.
- Для нового кода целевые правила обязательны; старый код приводится к ним постепенно.
- Не добавляем тяжёлую инфраструктуру "на будущее", но добавляем строгие boundaries там, где уже есть production-risk.
- Для risky changes используем маленькие PR и manual verification.
- Документацию в `documentation/` обновляем только когда реально меняется API, schema, workflow или deployment.
## Целевые правила архитектуры
Backend:
- Route/controller layer: только auth/context, validation, вызов service, response mapping.
- Service/domain layer: business logic, permissions decisions, transactions.
- DB API/repository layer: только data access, filters, includes, pagination.
- Policy layer: вся логика доступа в одном месте, не в отдельных routes.
- Validation layer: все внешние body/query/params проходят schema validation.
Frontend:
- Redux: только client/app state.
- TanStack Query: server state, lists/details/mutations/cache invalidation.
- Feature code: новая feature-specific логика живёт рядом с feature, а не размазывается по generic folders.
- `any`, type assertions/casts, disabled hook rules и direct axios calls в feature components не являются целевым стандартом.
## P1 - Frontend
### Auth storage
Сейчас token пишется и в `sessionStorage`, и в `localStorage`.
TODO:
- Решить, нужен ли persistent login.
- Если persistent login не нужен, оставить только `sessionStorage`.
- Если нужен, оставить `localStorage`, но осознанно и с коротким TTL/refresh policy.
- Заменить frontend `jsonwebtoken` decode на `jwt-decode` или `/auth/me`.
## P2 - Security hardening
### CSP
Полный CSP может быть сложным из-за embeds/media. Не вводить сразу.
TODO:
- Сначала собрать список нужных domains для assets/embeds.
- Если будет время, включить report-only CSP на stage.
- Enforced CSP делать только после проверки runtime presentations.
## Рекомендуемый порядок
1. Добавить validation для самых рискованных endpoints и запретить новые routes без validation.
2. Проверить DB slow queries и добавить только реально нужные индексы.
3. Выбрать package manager и Node version.
4. Добавить минимальные smoke checks.
5. Постепенно выносить helpers из больших frontend/backend файлов при изменениях.
6. Делать cleanup/dependency upgrades небольшими отдельными PR.
## Definition of Done
- Production workflows проверены вручную или тестом.
- Нет большого rewrite без явной выгоды.
- DB changes имеют backup/rollback plan.
- Документация обновлена только там, где изменилось поведение.