From 878d632dc894ea1fc61f9d863a92315355c26958 Mon Sep 17 00:00:00 2001 From: elpatron Date: Fri, 29 May 2026 15:13:16 +0200 Subject: [PATCH] feat: Semantische Versionierung mit Git-Tags und App-Footer. VERSION-Datei (0.1.0.0), Release-Flow in update-prod.sh und Build-time-Einbindung der Versionsnummer im Footer mit Copyright-Link. Co-authored-by: Cursor --- VERSION | 1 + client/Dockerfile | 12 ++- client/src/App.css | 46 ++++++++- client/src/App.tsx | 2 + client/src/components/AppFooter.tsx | 15 +++ client/src/vite-env.d.ts | 3 + client/vite.config.ts | 19 ++++ docker-compose.yml | 6 +- scripts/update-prod.sh | 139 +++++++++++++++++++++++++--- 9 files changed, 221 insertions(+), 22 deletions(-) create mode 100644 VERSION create mode 100644 client/src/components/AppFooter.tsx create mode 100644 client/src/vite-env.d.ts diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..482e997 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0.0 diff --git a/client/Dockerfile b/client/Dockerfile index e83680c..ac132cf 100644 --- a/client/Dockerfile +++ b/client/Dockerfile @@ -2,12 +2,16 @@ FROM node:20-alpine AS builder WORKDIR /app +ARG APP_VERSION=0.1.0.0-dev +ENV VITE_APP_VERSION=$APP_VERSION + # Install dependencies -COPY package*.json ./ +COPY client/package*.json ./ RUN npm ci -# Copy code and build production bundle -COPY . . +# Copy client sources and repo version file +COPY client/ . +COPY VERSION ./VERSION RUN npm run build # --- Production Stage --- @@ -15,7 +19,7 @@ FROM nginx:1.25-alpine WORKDIR /usr/share/nginx/html # Copy custom Nginx configuration -COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY client/nginx.conf /etc/nginx/conf.d/default.conf # Copy built assets from builder COPY --from=builder /app/dist . diff --git a/client/src/App.css b/client/src/App.css index c55b155..6f30f07 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -1949,7 +1949,7 @@ body:has(.theme-cupertino) { position: fixed; left: 16px; right: 16px; - bottom: calc(16px + env(safe-area-inset-bottom, 0px)); + bottom: calc(36px + env(safe-area-inset-bottom, 0px)); z-index: 1200; display: grid; grid-template-columns: auto 1fr auto; @@ -2107,4 +2107,48 @@ body:has(.theme-cupertino) { } } +.app-version-footer { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 900; + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 6px; + padding: 6px 12px calc(6px + env(safe-area-inset-bottom, 0px)); + font-size: 11px; + line-height: 1.4; + color: #64748b; + background: rgba(11, 12, 16, 0.72); + border-top: 1px solid rgba(255, 255, 255, 0.06); + pointer-events: none; +} + +.app-version-footer a, +.app-version-footer button { + pointer-events: auto; +} + +.app-version-footer__version { + font-variant-numeric: tabular-nums; + color: #94a3b8; +} + +.app-version-footer__sep { + opacity: 0.65; +} + +.app-version-footer__copyright { + color: #94a3b8; + text-decoration: none; +} + +.app-version-footer__copyright:hover { + color: #e2e8f0; + text-decoration: underline; +} + diff --git a/client/src/App.tsx b/client/src/App.tsx index 42d26f4..5b4363e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -13,6 +13,7 @@ import { getActiveMasterKey, logoutUser } from './services/auth.js' import { startBackgroundSync, stopBackgroundSync, syncAllLogbooks, subscribeToSyncState } from './services/sync.js' import ReadOnlyViewer from './components/ReadOnlyViewer.tsx' import PwaInstallPrompt from './components/PwaInstallPrompt.tsx' +import AppFooter from './components/AppFooter.tsx' import { db } from './services/db.js' import { useLiveQuery } from 'dexie-react-hooks' import { Ship, LogOut, ChevronLeft, Users, Compass, FileText, Settings, Wifi, WifiOff } from 'lucide-react' @@ -319,6 +320,7 @@ export default function AppWrapper() { return ( + ) } diff --git a/client/src/components/AppFooter.tsx b/client/src/components/AppFooter.tsx new file mode 100644 index 0000000..456b2a0 --- /dev/null +++ b/client/src/components/AppFooter.tsx @@ -0,0 +1,15 @@ +const APP_VERSION = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : 'dev' + +export default function AppFooter() { + return ( + + ) +} diff --git a/client/src/vite-env.d.ts b/client/src/vite-env.d.ts new file mode 100644 index 0000000..54eaa07 --- /dev/null +++ b/client/src/vite-env.d.ts @@ -0,0 +1,3 @@ +/// + +declare const __APP_VERSION__: string diff --git a/client/vite.config.ts b/client/vite.config.ts index c83b0c7..d8115a2 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,9 +1,28 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import { VitePWA } from 'vite-plugin-pwa' +import { readFileSync } from 'node:fs' +import { resolve, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +function readAppVersion(): string { + if (process.env.VITE_APP_VERSION) { + return process.env.VITE_APP_VERSION.trim() + } + try { + return readFileSync(resolve(__dirname, '../VERSION'), 'utf8').trim() + } catch { + return '0.1.0.0-dev' + } +} // https://vite.dev/config/ export default defineConfig({ + define: { + __APP_VERSION__: JSON.stringify(readAppVersion()) + }, server: { port: 5173, proxy: { diff --git a/docker-compose.yml b/docker-compose.yml index 6c522bd..54b6bd9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,8 +33,10 @@ services: frontend: build: - context: ./client - dockerfile: Dockerfile + context: . + dockerfile: client/Dockerfile + args: + APP_VERSION: ${APP_VERSION:-0.1.0.0-dev} container_name: daagbox-prod-frontend restart: always ports: diff --git a/scripts/update-prod.sh b/scripts/update-prod.sh index 5038605..3555c3d 100755 --- a/scripts/update-prod.sh +++ b/scripts/update-prod.sh @@ -1,5 +1,7 @@ #!/bin/bash +set -euo pipefail + # Remote deployment configuration # Override any of these via environment variables if needed, e.g.: # REMOTE_HOST=192.168.1.10 ./scripts/update-prod.sh @@ -13,18 +15,123 @@ COMPOSE_FILE="docker-compose.yml" BACKEND_CONTAINER="daagbox-prod-backend" MAX_WAIT=35 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +VERSION_FILE="$REPO_ROOT/VERSION" +DEFAULT_VERSION="0.1.0.0" + echo "==================================================" -# Translates to: Kapteins Daagbox Production Environment Update echo " Kapteins Daagbox Prod Environment Update " echo "==================================================" echo "Target: ${REMOTE_TARGET}:${REMOTE_DIR}" echo "==================================================" +cd "$REPO_ROOT" + +read_current_version() { + if [ -f "$VERSION_FILE" ]; then + tr -d '[:space:]' < "$VERSION_FILE" + return + fi + + local latest_tag + latest_tag="$(git tag -l 'v*' --sort=-v:refname | head -n 1 || true)" + if [ -n "$latest_tag" ]; then + echo "${latest_tag#v}" + return + fi + + echo "$DEFAULT_VERSION" +} + +bump_patch_version() { + local version="$1" + local major minor patch build + + IFS='.' read -r major minor patch build <<< "$version" + major="${major:-0}" + minor="${minor:-1}" + patch="${patch:-0}" + build="${build:-0}" + + build=$((10#$build + 1)) + echo "${major}.${minor}.${patch}.${build}" +} + +ensure_clean_git_tree() { + if git diff-index --quiet HEAD -- && [ -z "$(git status --porcelain)" ]; then + return 0 + fi + + echo "" + echo "Uncommitted local changes detected:" + git status --short + echo "" + read -r -p "Commit all changes now before release? [y/N] " answer + + if [[ ! "$answer" =~ ^[yY]$ ]]; then + echo "Aborting: working tree is not clean." + exit 1 + fi + + read -r -p "Commit message: " commit_message + if [ -z "$commit_message" ]; then + echo "Aborting: commit message is required." + exit 1 + fi + + git add -A + git commit -m "$commit_message" +} + +prepare_release() { + local current_version release_version next_version tag_name + + ensure_clean_git_tree + + current_version="$(read_current_version)" + release_version="$current_version" + next_version="$(bump_patch_version "$current_version")" + tag_name="v${release_version}" + + if git rev-parse "$tag_name" >/dev/null 2>&1; then + echo "Error: Git tag '$tag_name' already exists." + exit 1 + fi + + echo "$next_version" > "$VERSION_FILE" + git add "$VERSION_FILE" + git commit -m "chore: release ${tag_name}" + git tag -a "$tag_name" -m "Release ${tag_name}" + + echo "" + echo "Prepared release ${tag_name}" + echo " Released: ${tag_name}" + echo " Next prep: v${next_version}" + echo "" + + read -r -p "Push commit and tag to origin? [Y/n] " push_answer + if [[ ! "$push_answer" =~ ^[nN]$ ]]; then + current_branch="$(git branch --show-current)" + git push origin "$current_branch" + git push origin "$tag_name" + echo "Pushed ${current_branch} and ${tag_name} to origin." + else + echo "Skipped push. Remote host must receive this commit/tag manually." + fi + + export APP_VERSION="$release_version" +} + +prepare_release + +echo "==================================================" +echo "Deploying ${APP_VERSION} to ${REMOTE_TARGET}:${REMOTE_DIR}" +echo "==================================================" + # Run the whole update procedure remotely over SSH. -# The remote arguments are forwarded positionally so the heredoc can stay -# single-quoted (no local variable expansion / escaping surprises). ssh -o ConnectTimeout=10 "$REMOTE_TARGET" 'bash -s' -- \ - "$REMOTE_DIR" "$COMPOSE_FILE" "$BACKEND_CONTAINER" "$MAX_WAIT" "$REMOTE_HOST" <<'REMOTE_SCRIPT' + "$REMOTE_DIR" "$COMPOSE_FILE" "$BACKEND_CONTAINER" "$MAX_WAIT" "$REMOTE_HOST" "$APP_VERSION" <<'REMOTE_SCRIPT' set -uo pipefail REMOTE_DIR="$1" @@ -32,27 +139,32 @@ COMPOSE_FILE="$2" BACKEND_CONTAINER="$3" MAX_WAIT="$4" REMOTE_HOST="$5" +APP_VERSION="$6" -# Change to the deployment directory on the remote host cd "$REMOTE_DIR" || { echo "Error: Remote directory '$REMOTE_DIR' not found."; exit 1; } -# 1. Pull latest code changes echo "Pulling latest changes from Git..." -git pull +git pull --tags if [ $? -ne 0 ]; then echo "Error: Git pull failed." exit 1 fi -# 2. Build docker images without cache -echo "Rebuilding Docker images without cache..." +REMOTE_VERSION="$(tr -d '[:space:]' < VERSION)" +if [ "$REMOTE_VERSION" != "$APP_VERSION" ]; then + echo "Note: Remote VERSION file already points to next release (v${REMOTE_VERSION})." + echo " Building deployed release v${APP_VERSION}." +fi + +export APP_VERSION="$APP_VERSION" + +echo "Rebuilding Docker images without cache (APP_VERSION=${APP_VERSION})..." docker compose -f "$COMPOSE_FILE" build --no-cache if [ $? -ne 0 ]; then echo "Error: Docker compose build failed." exit 1 fi -# 3. Spin up the containers echo "Starting updated container stack..." docker compose -f "$COMPOSE_FILE" up -d if [ $? -ne 0 ]; then @@ -60,14 +172,12 @@ if [ $? -ne 0 ]; then exit 1 fi -# 4. Clean up old/stopped Docker assets (containers, networks, dangling images, cache) echo "Cleaning up old/unused Docker resources..." docker system prune -f if [ $? -ne 0 ]; then echo "Warning: Docker system prune failed to run completely." fi -# 5. Wait for services to become healthy (Prisma migrations & DB check) echo "Waiting for services to become healthy..." COUNTER=0 IS_READY=false @@ -82,7 +192,6 @@ while [ $COUNTER -lt $MAX_WAIT ]; do sleep 1 COUNTER=$((COUNTER + 1)) - # Show simple progress dots printf "." done echo "" @@ -94,6 +203,7 @@ echo "==================================================" if [ "$IS_READY" = true ]; then echo "SUCCESS: Production environment updated and healthy!" + echo " -> Version: v${APP_VERSION}" echo " -> App Frontend (Nginx): http://${REMOTE_HOST}" echo " -> Backend API Health: http://${REMOTE_HOST}/api/health" echo "==================================================" @@ -106,11 +216,10 @@ else fi REMOTE_SCRIPT -# Capture and report the remote exit status locally REMOTE_EXIT=$? echo "==================================================" if [ $REMOTE_EXIT -eq 0 ]; then - echo "Remote update completed successfully on ${REMOTE_TARGET}." + echo "Remote update completed successfully on ${REMOTE_TARGET} (v${APP_VERSION})." elif [ $REMOTE_EXIT -eq 3 ]; then echo "Remote update finished, but the backend was not healthy in time on ${REMOTE_TARGET}." else