deleted todo, updated docs
This commit is contained in:
parent
7bfdae0519
commit
9c12c3f539
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
117
backend/tests/video-processing.test.ts
Normal file
117
backend/tests/video-processing.test.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -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`
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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.
|
|
||||||
- Документация обновлена только там, где изменилось поведение.
|
|
||||||
Loading…
x
Reference in New Issue
Block a user