Compare commits
8
Commits
d6ff99e8a4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49cfe2d8e0 | ||
|
|
0ac1ca3b08 | ||
|
|
b8c4cebad5 | ||
|
|
baf827df29 | ||
|
|
ba409ca53b | ||
|
|
811c14ee73 | ||
|
|
ba8c7185a7 | ||
|
|
b122ab4ac0 |
@@ -3,6 +3,7 @@ node_modules/
|
||||
|
||||
# Build output
|
||||
backend/dist/
|
||||
backend/tsconfig.tsbuildinfo
|
||||
frontend/.next/
|
||||
frontend/tsconfig.tsbuildinfo
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
node_modules
|
||||
dist
|
||||
tsconfig.tsbuildinfo
|
||||
.env
|
||||
.env.development
|
||||
.env.production
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "BulkJobItem" DROP CONSTRAINT "BulkJobItem_videoId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "LintResult" DROP CONSTRAINT "LintResult_videoId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "VideoConfig" DROP CONSTRAINT "VideoConfig_videoId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "VideoPlaylist" DROP CONSTRAINT "VideoPlaylist_videoId_fkey";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "VideoConfig" ADD CONSTRAINT "VideoConfig_videoId_fkey" FOREIGN KEY ("videoId") REFERENCES "Video"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LintResult" ADD CONSTRAINT "LintResult_videoId_fkey" FOREIGN KEY ("videoId") REFERENCES "Video"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "BulkJobItem" ADD CONSTRAINT "BulkJobItem_videoId_fkey" FOREIGN KEY ("videoId") REFERENCES "Video"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "VideoPlaylist" ADD CONSTRAINT "VideoPlaylist_videoId_fkey" FOREIGN KEY ("videoId") REFERENCES "Video"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -174,7 +174,7 @@ enum LintStatus {
|
||||
model VideoConfig {
|
||||
id String @id @default(cuid())
|
||||
videoId String @unique
|
||||
video Video @relation(fields: [videoId], references: [id])
|
||||
video Video @relation(fields: [videoId], references: [id], onDelete: Cascade)
|
||||
templateId String?
|
||||
blockOrder Json
|
||||
blockOverrides Json
|
||||
@@ -312,7 +312,7 @@ model SavedView {
|
||||
model LintResult {
|
||||
id String @id @default(cuid())
|
||||
videoId String
|
||||
video Video @relation(fields: [videoId], references: [id])
|
||||
video Video @relation(fields: [videoId], references: [id], onDelete: Cascade)
|
||||
ruleCode String
|
||||
severity LintSeverity
|
||||
targetField String?
|
||||
@@ -369,7 +369,7 @@ model BulkJobItem {
|
||||
bulkJobId String
|
||||
bulkJob BulkJob @relation(fields: [bulkJobId], references: [id])
|
||||
videoId String
|
||||
video Video @relation(fields: [videoId], references: [id])
|
||||
video Video @relation(fields: [videoId], references: [id], onDelete: Cascade)
|
||||
beforeSnapshot Json?
|
||||
afterSnapshot Json?
|
||||
status String @default("pending")
|
||||
@@ -415,7 +415,7 @@ model VideoPlaylist {
|
||||
videoId String
|
||||
playlistId String
|
||||
position Int?
|
||||
video Video @relation(fields: [videoId], references: [id])
|
||||
video Video @relation(fields: [videoId], references: [id], onDelete: Cascade)
|
||||
playlist Playlist @relation(fields: [playlistId], references: [id])
|
||||
|
||||
@@id([videoId, playlistId])
|
||||
|
||||
@@ -67,6 +67,7 @@ export class AuthService {
|
||||
youtubeChannelId,
|
||||
name: channelName,
|
||||
uploadsPlaylistId,
|
||||
supplementalVideoIds: [],
|
||||
youtubeAccessToken: this.encrypt(accessToken),
|
||||
youtubeRefreshToken: refreshToken ? this.encrypt(refreshToken) : undefined,
|
||||
youtubeTokenExpiry: new Date(Date.now() + 3600 * 1000),
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
@@ -17,5 +18,6 @@
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "prisma"]
|
||||
}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ The production setup is a single-host Docker Compose deployment using **Traefik*
|
||||
| `postgres` | postgres:16-alpine | Database — bound to `127.0.0.1:5432` (not public) |
|
||||
| `redis` | redis:7-alpine | BullMQ queues — `noeviction` policy, AOF persistence |
|
||||
|
||||
**SSL / routing**: Traefik handles TLS termination with automatic Let's Encrypt certificates. Both API (`/api` prefix) and frontend run on the same domain — Traefik routes by path prefix. The `traefik-network` external network must exist before deploy.
|
||||
**SSL / routing**: Traefik handles TLS termination with automatic Let's Encrypt certificates. Both API (`/api` prefix) and frontend run on the same domain — Traefik routes by path prefix. The `proxy` external network (the host's existing Traefik network) must exist before deploy.
|
||||
|
||||
**Build**: Multi-stage Dockerfile (`node:22-alpine`). Builder compiles TypeScript; runner installs prod-only deps. The Prisma CLI is copied from builder stage so the `migrate` service can run schema migrations.
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ Registry (`git.devils.zone/devil/youtube-studio-flow-{backend,frontend}`), gebau
|
||||
6. Container starten: `docker compose up -d`
|
||||
|
||||
Für ein Update auf eine neue Version: `IMAGE_TAG` in `.env` anpassen (oder `latest`
|
||||
belassen), dann Schritte 4–6 wiederholen. Das externe Docker-Netzwerk `traefik-network`
|
||||
muss vorher existieren (`docker network create traefik-network`), falls Traefik das nicht
|
||||
belassen), dann Schritte 4–6 wiederholen. Das externe Docker-Netzwerk `proxy`
|
||||
muss vorher existieren (`docker network create proxy`), falls Traefik das nicht
|
||||
bereits selbst anlegt.
|
||||
|
||||
## Daten-Persistenz
|
||||
|
||||
@@ -3,6 +3,7 @@ services:
|
||||
# Runs database migrations once before the API and worker start.
|
||||
migrate:
|
||||
image: git.devils.zone/devil/youtube-studio-flow-backend:${IMAGE_TAG:-latest}
|
||||
pull_policy: always
|
||||
command: npx prisma migrate deploy
|
||||
restart: "no"
|
||||
environment:
|
||||
@@ -15,6 +16,7 @@ services:
|
||||
|
||||
api:
|
||||
image: git.devils.zone/devil/youtube-studio-flow-backend:${IMAGE_TAG:-latest}
|
||||
pull_policy: always
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
@@ -44,16 +46,17 @@ services:
|
||||
condition: service_completed_successfully
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:3001/api/v1/health || exit 1"]
|
||||
interval: 15s
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
retries: 10
|
||||
start_period: 60s
|
||||
networks:
|
||||
- app-network
|
||||
- traefik-network
|
||||
- proxy
|
||||
|
||||
worker:
|
||||
image: git.devils.zone/devil/youtube-studio-flow-backend:${IMAGE_TAG:-latest}
|
||||
pull_policy: always
|
||||
restart: unless-stopped
|
||||
command: node dist/worker.js
|
||||
environment:
|
||||
@@ -82,6 +85,7 @@ services:
|
||||
# Dockerfile default, /api/v1) — the relative path works because Traefik serves both
|
||||
# frontend and API on the same domain.
|
||||
image: git.devils.zone/devil/youtube-studio-flow-frontend:${IMAGE_TAG:-latest}
|
||||
pull_policy: always
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
@@ -91,11 +95,14 @@ services:
|
||||
- "traefik.http.routers.studioflow-frontend.priority=1"
|
||||
- "traefik.http.services.studioflow-frontend.loadbalancer.server.port=3000"
|
||||
depends_on:
|
||||
# service_started, not service_healthy: a slow/unhealthy api shouldn't block the
|
||||
# whole `compose up` (and Portainer tearing down what it created) - the frontend
|
||||
# itself doesn't need api to be ready at container-start time.
|
||||
api:
|
||||
condition: service_healthy
|
||||
condition: service_started
|
||||
networks:
|
||||
- app-network
|
||||
- traefik-network
|
||||
- proxy
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
@@ -142,5 +149,5 @@ volumes:
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
traefik-network:
|
||||
proxy:
|
||||
external: true
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-time migration: dump the local dev Postgres and restore it into the
|
||||
# server's Postgres over SSH, replacing whatever is there.
|
||||
#
|
||||
# DESTRUCTIVE on the target: drops and recreates the remote database.
|
||||
#
|
||||
# Fill in the placeholders below (or export them as env vars before running),
|
||||
# then run from the project root: bash scripts/migrate-local-db-to-server.sh
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Local Postgres running: cd infrastructure && docker compose up -d postgres redis
|
||||
# - SSH access to the server
|
||||
# - The remote postgres container's name (find it with `docker ps` on the
|
||||
# server, or check the container list in Portainer for your stack - it'll
|
||||
# be something like <stack-name>-postgres-1)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Git Bash/MSYS auto-translates POSIX-looking path args (e.g. /tmp/foo) into
|
||||
# Windows paths before handing them to docker.exe - but these paths are meant
|
||||
# to be interpreted inside the Linux container, not on the Windows host.
|
||||
export MSYS_NO_PATHCONV=1
|
||||
|
||||
# ── Fill these in ──────────────────────────────────────────────────────────
|
||||
SSH_USER="${SSH_USER:-dummyuser}"
|
||||
SSH_HOST="${SSH_HOST:-dummy.server.example}"
|
||||
SSH_PORT="${SSH_PORT:-22}"
|
||||
REMOTE_POSTGRES_CONTAINER="${REMOTE_POSTGRES_CONTAINER:-studioflow-postgres-1}"
|
||||
REMOTE_POSTGRES_USER="${REMOTE_POSTGRES_USER:-studioflow}"
|
||||
REMOTE_POSTGRES_DB="${REMOTE_POSTGRES_DB:-studioflow}"
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
LOCAL_DB_USER="studioflow"
|
||||
LOCAL_DB_NAME="studioflow"
|
||||
DUMP_FILE="studioflow_migration_$(date +%Y%m%d_%H%M%S).dump"
|
||||
|
||||
echo "==> Finding local Postgres container"
|
||||
LOCAL_PG_CONTAINER=$(docker ps --format '{{.Names}}' | grep -i postgres | head -1)
|
||||
if [ -z "$LOCAL_PG_CONTAINER" ]; then
|
||||
echo "No running local postgres container found. Start it first:"
|
||||
echo " cd infrastructure && docker compose up -d postgres redis"
|
||||
exit 1
|
||||
fi
|
||||
echo " Using: $LOCAL_PG_CONTAINER"
|
||||
|
||||
echo "==> Dumping local database ($LOCAL_DB_NAME)"
|
||||
docker exec "$LOCAL_PG_CONTAINER" pg_dump -U "$LOCAL_DB_USER" -d "$LOCAL_DB_NAME" -F c -f "/tmp/$DUMP_FILE"
|
||||
docker cp "${LOCAL_PG_CONTAINER}:/tmp/$DUMP_FILE" "./$DUMP_FILE"
|
||||
docker exec "$LOCAL_PG_CONTAINER" rm "/tmp/$DUMP_FILE"
|
||||
echo " Dump size: $(du -h "./$DUMP_FILE" | cut -f1)"
|
||||
|
||||
echo ""
|
||||
echo "About to overwrite the database on ${SSH_HOST} (container: ${REMOTE_POSTGRES_CONTAINER})."
|
||||
read -p "Type YES to continue: " CONFIRM
|
||||
if [ "$CONFIRM" != "YES" ]; then
|
||||
echo "Aborted. Local dump kept at ./$DUMP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Copying dump to server"
|
||||
scp -P "$SSH_PORT" "./$DUMP_FILE" "${SSH_USER}@${SSH_HOST}:/tmp/$DUMP_FILE"
|
||||
|
||||
echo "==> Restoring on server"
|
||||
ssh -p "$SSH_PORT" "${SSH_USER}@${SSH_HOST}" bash -s <<EOF
|
||||
set -euo pipefail
|
||||
echo " Copying dump into container..."
|
||||
docker cp "/tmp/$DUMP_FILE" "${REMOTE_POSTGRES_CONTAINER}:/tmp/$DUMP_FILE"
|
||||
|
||||
echo " Terminating existing connections..."
|
||||
docker exec "$REMOTE_POSTGRES_CONTAINER" psql -U "$REMOTE_POSTGRES_USER" -d postgres -c \
|
||||
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$REMOTE_POSTGRES_DB';" >/dev/null
|
||||
|
||||
echo " Dropping and recreating database..."
|
||||
docker exec "$REMOTE_POSTGRES_CONTAINER" psql -U "$REMOTE_POSTGRES_USER" -d postgres -c \
|
||||
"DROP DATABASE IF EXISTS $REMOTE_POSTGRES_DB;" >/dev/null
|
||||
docker exec "$REMOTE_POSTGRES_CONTAINER" psql -U "$REMOTE_POSTGRES_USER" -d postgres -c \
|
||||
"CREATE DATABASE $REMOTE_POSTGRES_DB OWNER $REMOTE_POSTGRES_USER;" >/dev/null
|
||||
|
||||
echo " Restoring data..."
|
||||
docker exec "$REMOTE_POSTGRES_CONTAINER" pg_restore -U "$REMOTE_POSTGRES_USER" -d "$REMOTE_POSTGRES_DB" --no-owner --no-privileges "/tmp/$DUMP_FILE"
|
||||
|
||||
docker exec "$REMOTE_POSTGRES_CONTAINER" rm "/tmp/$DUMP_FILE"
|
||||
rm "/tmp/$DUMP_FILE"
|
||||
echo " Done."
|
||||
EOF
|
||||
|
||||
echo ""
|
||||
echo "==> Migration complete."
|
||||
echo " Restart api/worker in Portainer (or they'll self-heal on next DB query)."
|
||||
echo " Local dump kept at ./$DUMP_FILE - delete it once you've confirmed the server looks right."
|
||||
Reference in New Issue
Block a user