diff --git a/Documentation/Plans.md b/Documentation/Plans.md new file mode 100644 index 0000000..40c4f12 --- /dev/null +++ b/Documentation/Plans.md @@ -0,0 +1,8 @@ +this file contains draft for future releases and features + +1. Multy language support, russian version at least, search for best implementation, preferably story in db and easily expandable. tool to check and correct translation +2. tool to manage links (influence/influenced by ) import csv's ( define format), edit ,add, delete, visualize, map to pictures/ entities +3. tool to monitor/manage (plan actions) of curator actions, markers to check painting/text ? +4. tool to sync prod /env resources (both ways), db structure, db data, images, users etc +5. curator_audit_log should contain log of actions like fixit, checked, upload etc with details for which entity it was made and details what was the action and outcome + diff --git a/Documentation/deploy-dev-to-prod.md b/Documentation/deploy-dev-to-prod.md index 9d45500..bd7a07b 100644 --- a/Documentation/deploy-dev-to-prod.md +++ b/Documentation/deploy-dev-to-prod.md @@ -31,12 +31,16 @@ For a typical weekly release, use the orchestrator instead of running each step 2. Edit [`infra/deploy/devtoprod.config.json`](../infra/deploy/devtoprod.config.json) (gitignored — safe for SMB password): - `profile`: `full` | `code` | `data` (base step set; individual `steps` overrides win) - `autoConfirm`: `true` skips restore/image-sync prompts (`CONFIRM_PROD=1`, `-SkipConfirm`) + - `autoStartDevStack`: `true` starts `dev:server` (or `dev:web`) automatically when `validateBuild` finds API down - `git.message`, `git.branch` — used when `gitCommitPush` is enabled - `schemaChanged`: `true` enables prod schema migration on `full` profile + - `release.imageTag`: image/version tag shared between Gitea push and runtime (`latest`, `2026-07-09`, `v1.3.0`, etc.) - `smb.user` / `smb.password` — optional; maps `\\host\share` before image sync - `backupFile` — optional fixed path; otherwise uses the newest `gallery_dev_data_*.txt` after backup 3. Prerequisites still apply: **`npm run dev:web` running** (Vite `:5173` + API `:3451`), Docker Desktop running, `docker login gitea.mysuperlab.netcraze.pro`, [`infra/docker/.env.prod`](../infra/docker/.env.prod) present. + - Before migration/restore steps, verify prod DB target in `.env.prod`: `DB_NAME=gallery_prod`. + - Set `IMAGE_TAG=` in prod env (TrueNAS app vars or `.env.prod`) to control which pushed Gitea image tag is active. ### Run @@ -64,7 +68,7 @@ npm run devtoprod:release -- -DryRun | `migrateProdSchema` | Step 4 — `dev:migrate` on `gallery_prod` | | `restoreProd` | Step 5 — `devtoprod:db:restore` | | `syncImages` | Step 6 — `devtoprod:images` | -| `dockerPublish` | Step 7 — `prod:docker:publish` | +| `dockerPublish` | Step 7 — `prod:docker:publish -- -Tag ` | | `truenasRestartPause` | Step 8 — **manual** pause; script waits for Enter after you restart **gallery-web** | | `verify` | Step 9 — curl LAN + public `/api/bounds` | diff --git a/Documentation/environments.md b/Documentation/environments.md index dd1247f..6c089cb 100644 --- a/Documentation/environments.md +++ b/Documentation/environments.md @@ -7,6 +7,11 @@ Gallery uses **one PostgreSQL server** on TrueNAS (`192.168.10.122`) with **two | **Development** | `gallery_dev` | `https://devgallery.mysuperlab.netcraze.pro` | Dev PC `192.168.10.70:5173` | | **Production** | `gallery_prod` | `https://gallery.mysuperlab.netcraze.pro` | TrueNAS container `192.168.10.122:5173` | +Version tracking: +- Runtime version endpoint: `/api/version` (reports `app_env`, `app_version`, `image_tag`, `git_sha`, `db_name`) +- Prod image selection: set `IMAGE_TAG` in TrueNAS app env / `infra/docker/.env.prod` +- Release push tag: set `release.imageTag` in `infra/deploy/devtoprod.config.json` + --- ## Development-first workflow (default) diff --git a/Inputs/artist_influences_web_sources.xlsx b/Inputs/artist_influences_web_sources.xlsx new file mode 100644 index 0000000..ff0cc51 Binary files /dev/null and b/Inputs/artist_influences_web_sources.xlsx differ diff --git a/infra/deploy/devtoprod.config.example.json b/infra/deploy/devtoprod.config.example.json index 08239e9..a977ab9 100644 --- a/infra/deploy/devtoprod.config.example.json +++ b/infra/deploy/devtoprod.config.example.json @@ -2,6 +2,7 @@ "_comment": "Copy to devtoprod.config.json (gitignored) and edit before running npm run devtoprod:release", "profile": "full", "autoConfirm": true, + "autoStartDevStack": true, "steps": { "validateBuild": true, "gitCommitPush": true, @@ -21,6 +22,9 @@ "stageAll": true }, "schemaChanged": false, + "release": { + "imageTag": "latest" + }, "backupFile": "", "smb": { "host": "192.168.10.122", diff --git a/infra/docker/.env.prod.example b/infra/docker/.env.prod.example index eead4ae..2f6ae89 100644 --- a/infra/docker/.env.prod.example +++ b/infra/docker/.env.prod.example @@ -10,6 +10,8 @@ DB_NAME=gallery_prod PORT=5173 HOST=0.0.0.0 +APP_ENV=prod +IMAGE_TAG=latest PUBLIC_URL=https://gallery.mysuperlab.netcraze.pro TRUST_PROXY=true IMAGE_DIR=/app/data/images diff --git a/infra/docker/compose.prod.yaml b/infra/docker/compose.prod.yaml index b08b34f..9f91bae 100644 --- a/infra/docker/compose.prod.yaml +++ b/infra/docker/compose.prod.yaml @@ -6,7 +6,7 @@ services: build: context: ../.. dockerfile: infra/docker/Dockerfile - image: gitea.mysuperlab.netcraze.pro/danilka/gallery-web:latest + image: gitea.mysuperlab.netcraze.pro/danilka/gallery-web:${IMAGE_TAG:-latest} container_name: gallery-web restart: unless-stopped ports: diff --git a/infra/docker/compose.truenas.yaml b/infra/docker/compose.truenas.yaml index d7e49c4..8647c8e 100644 --- a/infra/docker/compose.truenas.yaml +++ b/infra/docker/compose.truenas.yaml @@ -10,7 +10,7 @@ # See infra/docker/DEPLOY-truenas.md and Documentation/environments.md services: web: - image: gitea.mysuperlab.netcraze.pro/danilka/gallery-web:latest + image: gitea.mysuperlab.netcraze.pro/danilka/gallery-web:${IMAGE_TAG:-latest} container_name: gallery-web restart: unless-stopped pull_policy: always @@ -18,6 +18,8 @@ services: - "5173:5173" environment: NODE_ENV: production + APP_ENV: "prod" + IMAGE_TAG: "${IMAGE_TAG:-latest}" PORT: "5173" HOST: "0.0.0.0" DB_HOST: "192.168.10.122" diff --git a/infra/scripts/deploy-dev-to-prod.ps1 b/infra/scripts/deploy-dev-to-prod.ps1 index 447abde..d550c99 100644 --- a/infra/scripts/deploy-dev-to-prod.ps1 +++ b/infra/scripts/deploy-dev-to-prod.ps1 @@ -203,6 +203,31 @@ function Get-LatestDevBackup { return $latest.FullName } +function Assert-ProdDbTarget { + $prodEnvPath = Join-Path $RepoRoot 'infra\docker\.env.prod' + if (-not (Test-Path $prodEnvPath)) { + throw "Missing production env file: $prodEnvPath" + } + + $prodDbName = $null + foreach ($line in Get-Content -LiteralPath $prodEnvPath) { + if ($line -match '^\s*DB_NAME\s*=\s*(.+?)\s*$') { + $prodDbName = $Matches[1].Trim().Trim('"', "'") + break + } + } + + if (-not $prodDbName) { + throw "DB_NAME is not set in $prodEnvPath" + } + + if ($prodDbName -ne 'gallery_prod') { + throw "Prod DB target is '$prodDbName' in .env.prod. Set DB_NAME=gallery_prod before dev -> prod migration." + } + + Write-Host "Prod DB target confirmed: $prodDbName" -ForegroundColor DarkGray +} + function Get-DevApiPort { $envFile = Join-Path $RepoRoot '.env' $port = '3451' @@ -217,6 +242,72 @@ function Get-DevApiPort { return $port } +function Test-TcpPortListening { + param([int]$Port) + + $matches = netstat -ano | Select-String ":$Port\s" + foreach ($line in $matches) { + if ($line -match 'LISTENING') { + return $true + } + } + return $false +} + +function Start-DevNpmProcess { + param([string]$ScriptName) + + $logPath = Join-Path $RepoRoot "infra\deploy\dev-autostart-$ScriptName.log" + Write-Host "Starting background process: npm run $ScriptName (log: $logPath)" -ForegroundColor Yellow + Start-Process -FilePath 'cmd.exe' -ArgumentList @( + '/c', + "npm run $ScriptName > `"$logPath`" 2>&1" + ) -WorkingDirectory $RepoRoot -WindowStyle Hidden | Out-Null +} + +function Ensure-DevApiReady { + param( + [int]$ApiPort, + [bool]$AutoStart = $true, + [int]$WaitSeconds = 90 + ) + + $apiUrl = "http://127.0.0.1:$ApiPort/api/bounds" + $detail = '' + if (Test-BoundsJson -Url $apiUrl -MaxTimeSeconds 5 -Retries 1 -Detail ([ref]$detail)) { + Write-Host "Dev API already running: $apiUrl" + return $true + } + + if (-not $AutoStart) { + return $false + } + + $viteUp = Test-TcpPortListening -Port 5173 + $apiUp = Test-TcpPortListening -Port $ApiPort + + if ($viteUp -and -not $apiUp) { + Write-Host "Vite is running on :5173 but API is down on :$ApiPort." -ForegroundColor Yellow + Start-DevNpmProcess -ScriptName 'dev:server' + } elseif (-not $viteUp -and -not $apiUp) { + Write-Host 'Dev stack is not running.' -ForegroundColor Yellow + Start-DevNpmProcess -ScriptName 'dev:web' + } else { + Write-Host "API port :$ApiPort is listening but /api/bounds is not ready yet. Waiting..." -ForegroundColor Yellow + } + + for ($elapsed = 3; $elapsed -le $WaitSeconds; $elapsed += 3) { + Start-Sleep -Seconds 3 + if (Test-BoundsJson -Url $apiUrl -MaxTimeSeconds 5 -Retries 1 -Detail ([ref]$detail)) { + Write-Host "Dev API ready: $apiUrl" + return $true + } + Write-Host "Waiting for dev API... (${elapsed}s, $detail)" -ForegroundColor DarkGray + } + + return $false +} + function Test-BoundsJson { param( [string]$Url, @@ -312,6 +403,14 @@ if (Get-StepEnabled $steps 'validateBuild') { # Dev-only endpoints. Do not use verify.lanUrl here (that is prod on TrueNAS). $devApiPort = Get-DevApiPort + $autoStartDev = if ($null -ne $cfg.autoStartDevStack) { [bool]$cfg.autoStartDevStack } else { $true } + if (-not (Ensure-DevApiReady -ApiPort $devApiPort -AutoStart:$autoStartDev)) { + Write-Host "Dev API check failed on all URLs." -ForegroundColor Red + Write-Host "Start the full dev stack before release: npm run dev:web" -ForegroundColor Yellow + Write-Host "Vite on :5173 alone is not enough - the API must listen on :$devApiPort (PORT in .env)." -ForegroundColor Yellow + return 1 + } + $devCandidates = @( "http://127.0.0.1:$devApiPort/api/bounds" 'http://localhost:5173/api/bounds' @@ -395,6 +494,7 @@ if (Get-StepEnabled $steps 'backupDev') { if (Get-StepEnabled $steps 'backupProd') { $ok = Invoke-Step 'backupProd' { + Assert-ProdDbTarget $code = Invoke-Npm 'prod:db:backup' return $code } @@ -403,6 +503,7 @@ if (Get-StepEnabled $steps 'backupProd') { if (Get-StepEnabled $steps 'migrateProdSchema') { $ok = Invoke-Step 'migrateProdSchema' { + Assert-ProdDbTarget $env:DB_NAME = 'gallery_prod' try { $code = Invoke-Npm 'dev:migrate' @@ -416,6 +517,7 @@ if (Get-StepEnabled $steps 'migrateProdSchema') { if (Get-StepEnabled $steps 'restoreProd') { $ok = Invoke-Step 'restoreProd' { + Assert-ProdDbTarget if (-not $script:backupFile) { if ($cfg.backupFile -and $cfg.backupFile.Trim()) { $script:backupFile = (Resolve-Path (Join-Path $RepoRoot $cfg.backupFile)).Path @@ -446,7 +548,9 @@ if (Get-StepEnabled $steps 'syncImages') { if (Get-StepEnabled $steps 'dockerPublish') { $ok = Invoke-Step 'dockerPublish' { - $code = Invoke-Npm 'prod:docker:publish' + $tag = if ($cfg.release.imageTag) { $cfg.release.imageTag } else { 'latest' } + Write-Host "Docker release tag: $tag" + $code = Invoke-Npm 'prod:docker:publish' @('-Tag', $tag) return $code } if (-not $ok) { Write-ReleaseBanner -Success $false -FailedAt $FailedStep; exit 1 } diff --git a/scripts/dev-web.js b/scripts/dev-web.js index 33d0cc1..a749a5b 100644 --- a/scripts/dev-web.js +++ b/scripts/dev-web.js @@ -10,7 +10,12 @@ require('dotenv').config({ path: path.join(__dirname, '..', '.env') }); const apiPort = process.env.DEV_API_PORT || process.env.PORT || '3451'; const vitePort = process.env.DEV_WEB_PORT || '5173'; -const env = { ...process.env, PORT: apiPort }; +const env = { + ...process.env, + PORT: apiPort, + APP_ENV: process.env.APP_ENV || 'dev', + IMAGE_TAG: process.env.IMAGE_TAG || 'dev-local', +}; function run(label, command, args, cwd) { const child = spawn(command, args, { diff --git a/server/index.js b/server/index.js index 8b65a3a..667dbae 100644 --- a/server/index.js +++ b/server/index.js @@ -18,6 +18,10 @@ const app = express(); const HOST = process.env.HOST || '0.0.0.0'; const PORT = Number(process.env.PORT) || 3001; const PUBLIC_URL = process.env.PUBLIC_URL || ''; +const APP_ENV = process.env.APP_ENV || (process.env.DB_NAME === 'gallery_prod' ? 'prod' : 'dev'); +const IMAGE_TAG = process.env.IMAGE_TAG || 'local'; +const GIT_SHA = process.env.GIT_SHA || ''; +const APP_VERSION = process.env.npm_package_version || '0.0.0'; if (process.env.TRUST_PROXY === '1' || process.env.TRUST_PROXY === 'true') { app.set('trust proxy', 1); @@ -1039,6 +1043,17 @@ app.get('/api/bounds', async (req, res) => { } }); +app.get('/api/version', async (_req, res) => { + res.json({ + app_env: APP_ENV, + app_version: APP_VERSION, + image_tag: IMAGE_TAG, + git_sha: GIT_SHA, + db_name: process.env.DB_NAME || '', + public_url: PUBLIC_URL || '', + }); +}); + const CLIENT_DIST = path.join(__dirname, '..', 'client', 'dist'); if (fs.existsSync(CLIENT_DIST)) { app.use(express.static(CLIENT_DIST));