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