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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}`,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
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:
|
||||
|
||||
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`
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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