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
```
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
@ -204,7 +205,7 @@ DB_HOST=localhost
DB_PORT=5432
DB_NAME=app_39215
DB_USER=app_39215
DB_PASSWORD=your-password
DB_PASS=your-password
# JWT
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 |
| 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 |
| 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

View File

@ -24,6 +24,9 @@ The suite is split into three layers:
file-storage provider overrides.
- Circuit breaker behavior for ignored failures, recorded failures, and open
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
verification token behavior with real bcrypt and DB writes.
- 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
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
`backend/tests/http-test-utils.ts` provides `startTestServer(app)`, which starts

View File

@ -57,6 +57,10 @@ interface ProcessResult {
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 {
if (!value) {
return null;
@ -103,7 +107,15 @@ function toError(value: unknown): Error {
}
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 {
@ -115,7 +127,7 @@ function getFfmpegExecutablePath(): string {
async function runProcess(
executablePath: string,
args: string[],
args: readonly string[],
options: { timeoutMs: number; timeoutMessage: string },
): Promise<ProcessResult> {
return new Promise((resolve, reject) => {
@ -164,7 +176,7 @@ async function runProcess(
finish(
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:
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.
@ -22,7 +22,7 @@
``` 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.
@ -32,7 +32,7 @@
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:
@ -43,4 +43,3 @@
9. Stop services:
9.1. Just press `Ctr+C`

View File

@ -6,7 +6,7 @@ services:
tty: true # docker run -t
environment:
- FRONT_PORT=3001
- NEXT_PUBLIC_BACK_API=http://localhost:3000
- NEXT_PUBLIC_BACK_API=http://localhost:3000/api
ports:
- "3001:3001"
logging:
@ -16,7 +16,7 @@ services:
max-file: "3"
db:
image: postgres
image: postgres:14
volumes:
- ./data/db:/var/lib/postgresql/data
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.
- Документация обновлена только там, где изменилось поведение.