diff --git a/.env.example b/.env.example index 3265047c..3585dbf6 100644 --- a/.env.example +++ b/.env.example @@ -50,6 +50,16 @@ VITE_API_URL=/api # DB_PORT=5432 # REDIS_PORT=6379 +# Release Channel +# Options: 'stable' (default), 'beta', or specific version like 'v2.3.0' +# 'stable' uses the :stable tag (same as :latest on main) +# 'beta' uses the :beta tag for pre-release versions +PICPEAK_CHANNEL=stable + +# Update Check Configuration +# Set to 'false' to disable update notifications in admin UI +UPDATE_CHECK_ENABLED=true + # Timezone TZ=UTC diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index faa24054..6395d081 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -8,10 +8,10 @@ name: Build and Push Docker Images on: push: - branches: [ main, develop ] - tags: [ 'v*.*.*' ] # Triggered by Release Please tags + branches: [ main, beta ] + tags: [ 'v*.*.*', 'v*.*.*-beta.*' ] # Triggered by Release Please tags (stable and beta) pull_request: - branches: [ main ] + branches: [ main, beta ] release: types: [ published ] # Triggered when Release Please creates a release workflow_dispatch: @@ -42,6 +42,18 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Determine build context + id: context + run: | + # Determine if this is a beta or stable release + if [[ "${{ github.ref }}" == refs/tags/v*-beta* ]] || [[ "${{ github.ref }}" == refs/heads/beta ]]; then + echo "channel=beta" >> $GITHUB_OUTPUT + echo "is_prerelease=true" >> $GITHUB_OUTPUT + else + echo "channel=stable" >> $GITHUB_OUTPUT + echo "is_prerelease=false" >> $GITHUB_OUTPUT + fi + - name: Determine build platforms id: platforms run: | @@ -88,10 +100,12 @@ jobs: type=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} + type=semver,pattern={{major}}.{{minor}},enable=${{ steps.context.outputs.is_prerelease == 'false' }} + type=semver,pattern={{major}},enable=${{ steps.context.outputs.is_prerelease == 'false' }} type=sha,format=short type=raw,value=latest,enable={{is_default_branch}} + type=raw,value=stable,enable=${{ github.ref == 'refs/heads/main' || (startsWith(github.ref, 'refs/tags/v') && steps.context.outputs.is_prerelease == 'false') }} + type=raw,value=beta,enable=${{ github.ref == 'refs/heads/beta' || steps.context.outputs.is_prerelease == 'true' }} - name: Build and push Backend Docker image uses: docker/build-push-action@v5 @@ -139,6 +153,18 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Determine build context + id: context + run: | + # Determine if this is a beta or stable release + if [[ "${{ github.ref }}" == refs/tags/v*-beta* ]] || [[ "${{ github.ref }}" == refs/heads/beta ]]; then + echo "channel=beta" >> $GITHUB_OUTPUT + echo "is_prerelease=true" >> $GITHUB_OUTPUT + else + echo "channel=stable" >> $GITHUB_OUTPUT + echo "is_prerelease=false" >> $GITHUB_OUTPUT + fi + - name: Determine build platforms id: platforms run: | @@ -185,10 +211,12 @@ jobs: type=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} + type=semver,pattern={{major}}.{{minor}},enable=${{ steps.context.outputs.is_prerelease == 'false' }} + type=semver,pattern={{major}},enable=${{ steps.context.outputs.is_prerelease == 'false' }} type=sha,format=short type=raw,value=latest,enable={{is_default_branch}} + type=raw,value=stable,enable=${{ github.ref == 'refs/heads/main' || (startsWith(github.ref, 'refs/tags/v') && steps.context.outputs.is_prerelease == 'false') }} + type=raw,value=beta,enable=${{ github.ref == 'refs/heads/beta' || steps.context.outputs.is_prerelease == 'true' }} - name: Build and push Frontend Docker image uses: docker/build-push-action@v5 diff --git a/.github/workflows/release-please-beta.yml b/.github/workflows/release-please-beta.yml new file mode 100644 index 00000000..5db96823 --- /dev/null +++ b/.github/workflows/release-please-beta.yml @@ -0,0 +1,72 @@ +name: Release Please (Beta) + +on: + push: + branches: [beta] + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} + version: ${{ steps.release.outputs.version }} + steps: + - name: Run Release Please + uses: googleapis/release-please-action@v4 + id: release + with: + token: ${{ secrets.GITHUB_TOKEN }} + config-file: release-please-config-beta.json + manifest-file: .release-please-manifest-beta.json + target-branch: beta + + - name: Output Release Info + if: ${{ steps.release.outputs.release_created }} + run: | + echo "## Beta Release Created!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Tag:** ${{ steps.release.outputs.tag_name }}" >> $GITHUB_STEP_SUMMARY + echo "**Version:** ${{ steps.release.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Docker images will be built and tagged with this beta version." >> $GITHUB_STEP_SUMMARY + + # Sync version to package.json files after release + sync-versions: + needs: release-please + if: ${{ needs.release-please.outputs.release_created }} + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: beta + + - name: Update package.json versions + run: | + VERSION="${{ needs.release-please.outputs.version }}" + echo "Updating package.json files to version $VERSION" + + # Update backend package.json + cd backend + npm version $VERSION --no-git-tag-version --allow-same-version + cd .. + + # Update frontend package.json + cd frontend + npm version $VERSION --no-git-tag-version --allow-same-version + cd .. + + - name: Commit version updates + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add backend/package.json frontend/package.json + git diff --staged --quiet || git commit -m "chore: sync package.json versions to ${{ needs.release-please.outputs.version }}" + git push diff --git a/.release-please-manifest-beta.json b/.release-please-manifest-beta.json new file mode 100644 index 00000000..da921703 --- /dev/null +++ b/.release-please-manifest-beta.json @@ -0,0 +1,3 @@ +{ + ".": "2.3.0-beta.0" +} diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md index 3e0cbece..a68d0e1e 100644 --- a/DEPLOYMENT_GUIDE.md +++ b/DEPLOYMENT_GUIDE.md @@ -9,6 +9,7 @@ This guide covers multiple deployment options for PicPeak, from simple local set - [Configuration](#-configuration) - [Deployment](#-deployment) - [First Login](#-first-login) +- [Release Channels](#-release-channels) - [Reverse Proxy Setup](#-reverse-proxy-setup) - [External Media Library](#external-media-library) - [Maintenance](#-maintenance) @@ -52,7 +53,14 @@ docker compose -f docker-compose.production.yml up -d docker compose -f docker-compose.production.yml logs -f ``` -Available image tags: `latest` (stable), `main`, `develop`, `v1.0.0` (version tags) +**Available image tags:** +| Channel | Tags | Description | +|---------|------|-------------| +| Stable | `stable`, `latest`, `v2.3.0` | Production-ready releases | +| Beta | `beta`, `v2.3.0-beta.1` | Early access to new features | +| Branch | `main`, `beta` | Latest from each branch | + +To select a channel, set `PICPEAK_CHANNEL` in your `.env` file (see [Release Channels](#release-channels) section) ### Option 3: Build from Source @@ -344,6 +352,61 @@ ADMIN_EMAIL=your-email@yourdomain.com **Note**: This only works on first deployment. To change the admin email after deployment, you'll need to update it in the database or create a new admin user through the admin panel. +## 🔄 Release Channels + +PicPeak offers two release channels for different needs: + +### Stable Channel (Recommended) +- Production-ready releases +- Thoroughly tested before release +- Docker tags: `stable`, `latest`, or specific version like `v2.3.0` + +### Beta Channel +- Early access to new features +- May contain bugs or incomplete functionality +- Docker tags: `beta` or specific version like `v2.3.0-beta.1` + +### Configuring Your Channel + +Set the `PICPEAK_CHANNEL` environment variable in your `.env` file: + +```bash +# For stable releases (default) +PICPEAK_CHANNEL=stable + +# For beta releases +PICPEAK_CHANNEL=beta + +# For a specific version +PICPEAK_CHANNEL=v2.3.0 +``` + +The `docker-compose.production.yml` uses this variable for both backend and frontend images: +```yaml +image: ghcr.io/the-luap/picpeak/backend:${PICPEAK_CHANNEL:-stable} +``` + +### Switching Channels + +To switch between channels: + +```bash +# Edit your .env file +nano .env +# Change PICPEAK_CHANNEL=stable to PICPEAK_CHANNEL=beta (or vice versa) + +# Pull the new images and restart +docker compose -f docker-compose.production.yml pull +docker compose -f docker-compose.production.yml up -d +``` + +### Update Notifications + +The admin dashboard automatically notifies you when updates are available for your channel. This feature: +- Checks GitHub releases hourly (cached to avoid rate limits) +- Shows updates relevant to your current channel (stable or beta) +- Can be disabled by setting `UPDATE_CHECK_ENABLED=false` in your `.env` + ## 🔒 Reverse Proxy Setup For production deployments, you should use a reverse proxy for SSL/HTTPS. The application exposes ports directly, allowing you to use any reverse proxy solution. @@ -554,20 +617,27 @@ docker compose up -d docker compose ps ``` -#### Specific Version Updates +#### Specific Version or Channel Updates -To use a specific version of the images: +To use a specific version or switch channels, update your `.env` file: ```bash -# Edit docker-compose.production.yml to specify version tags -# Change: ghcr.io/the-luap/picpeak/backend:latest -# To: ghcr.io/the-luap/picpeak/backend:v1.0.0 +# Edit .env to change the channel or pin to a specific version +nano .env + +# Options for PICPEAK_CHANNEL: +# - stable (recommended, production-ready) +# - beta (early access to new features) +# - v2.3.0 (pin to specific stable version) +# - v2.3.0-beta.1 (pin to specific beta version) # Then pull and restart docker compose -f docker-compose.production.yml pull docker compose -f docker-compose.production.yml up -d ``` +The admin dashboard will notify you when updates are available for your configured channel. + ### Database Migrations Migrations run automatically on startup, but you can run them manually: diff --git a/README.md b/README.md index 30b07bbd..9047c055 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,51 @@ Note on Docker file permissions (PUID/PGID) - Example in `.env`: - `PUID=1000` - `PGID=1000` -- Without this, creating events, uploads, thumbnails, or logs can fail with “Permission denied”. +- Without this, creating events, uploads, thumbnails, or logs can fail with "Permission denied". + +## 🔄 Release Channels + +PicPeak offers two release channels for different needs: + +### Stable Channel (Recommended) +- Production-ready releases +- Thoroughly tested before release +- Docker tags: `stable`, `latest`, or specific version like `v2.3.0` + +### Beta Channel +- Early access to new features +- May contain bugs or incomplete functionality +- Docker tags: `beta` or specific version like `v2.3.0-beta.1` + +### Switching Channels + +Set the `PICPEAK_CHANNEL` environment variable in your `.env` file: + +```bash +# For stable releases (default) +PICPEAK_CHANNEL=stable + +# For beta releases +PICPEAK_CHANNEL=beta + +# For a specific version +PICPEAK_CHANNEL=v2.3.0 +``` + +Then update your containers: + +```bash +docker-compose -f docker-compose.production.yml pull +docker-compose -f docker-compose.production.yml up -d +``` + +### Update Notifications + +The admin dashboard automatically notifies you when updates are available for your channel. To disable update checks, set: + +```bash +UPDATE_CHECK_ENABLED=false +``` ## 📖 Documentation diff --git a/backend/src/routes/adminSystem.js b/backend/src/routes/adminSystem.js index 491bbb41..da93b30c 100644 --- a/backend/src/routes/adminSystem.js +++ b/backend/src/routes/adminSystem.js @@ -7,6 +7,7 @@ const path = require('path'); const os = require('os'); const { formatBoolean } = require('../utils/dbCompat'); const logger = require('../utils/logger'); +const { checkForUpdates, getCurrentChannel } = require('../services/updateCheckService'); const router = express.Router(); // Get system version @@ -22,12 +23,15 @@ router.get('/version', adminAuth, requirePermission('settings.view'), async (req } catch (err) { console.error('Could not read package.json:', err); } - + + const channel = getCurrentChannel(backendVersion); + res.json({ backend: backendVersion, frontend: '1.0.0', // This will be set by frontend node: process.version, - environment: process.env.NODE_ENV || 'production' + environment: process.env.NODE_ENV || 'production', + channel: channel }); } catch (error) { console.error('Error fetching version:', error); @@ -35,6 +39,32 @@ router.get('/version', adminAuth, requirePermission('settings.view'), async (req } }); +// Check for updates +router.get('/updates', adminAuth, requirePermission('settings.view'), async (req, res) => { + try { + // Check if update checking is enabled + const updateCheckEnabled = process.env.UPDATE_CHECK_ENABLED !== 'false'; + + if (!updateCheckEnabled) { + return res.json({ + enabled: false, + message: 'Update checking is disabled' + }); + } + + const forceRefresh = req.query.refresh === 'true'; + const updateInfo = await checkForUpdates(forceRefresh); + + res.json({ + enabled: true, + ...updateInfo + }); + } catch (error) { + logger.error('Error checking for updates:', error); + res.status(500).json({ error: 'Failed to check for updates' }); + } +}); + // Get comprehensive system status router.get('/status', adminAuth, requirePermission('settings.view'), async (req, res) => { try { diff --git a/backend/src/services/updateCheckService.js b/backend/src/services/updateCheckService.js new file mode 100644 index 00000000..549b29b6 --- /dev/null +++ b/backend/src/services/updateCheckService.js @@ -0,0 +1,212 @@ +const axios = require('axios'); +const fs = require('fs').promises; +const path = require('path'); +const logger = require('../utils/logger'); + +// Cache for version info (avoid hitting GitHub API too often) +let versionCache = null; +let lastCheck = 0; +const CACHE_TTL = 60 * 60 * 1000; // 1 hour cache + +/** + * Get current installed version from package.json + */ +async function getCurrentVersion() { + try { + const packagePath = path.join(__dirname, '../../package.json'); + const packageContent = await fs.readFile(packagePath, 'utf8'); + const packageJson = JSON.parse(packageContent); + return packageJson.version || '0.0.0'; + } catch (err) { + logger.error('Could not read package.json for version:', err); + return '0.0.0'; + } +} + +/** + * Determine current release channel from version or environment + */ +function getCurrentChannel(version) { + // Check environment variable first + const envChannel = process.env.PICPEAK_RELEASE_CHANNEL; + if (envChannel && ['stable', 'beta'].includes(envChannel)) { + return envChannel; + } + + // Infer from version string + if (version && version.includes('-beta')) { + return 'beta'; + } + return 'stable'; +} + +/** + * Parse version string into comparable parts + */ +function parseVersion(version) { + if (!version) return null; + + // Handle versions like "2.3.0" or "2.3.0-beta.1" + const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-beta\.(\d+))?$/); + if (!match) return null; + + return { + major: parseInt(match[1], 10), + minor: parseInt(match[2], 10), + patch: parseInt(match[3], 10), + beta: match[4] ? parseInt(match[4], 10) : null, + isBeta: !!match[4] + }; +} + +/** + * Compare two versions + * Returns: 1 if a > b, -1 if a < b, 0 if equal + */ +function compareVersions(a, b) { + const va = parseVersion(a); + const vb = parseVersion(b); + + if (!va || !vb) return 0; + + // Compare major.minor.patch + if (va.major !== vb.major) return va.major > vb.major ? 1 : -1; + if (va.minor !== vb.minor) return va.minor > vb.minor ? 1 : -1; + if (va.patch !== vb.patch) return va.patch > vb.patch ? 1 : -1; + + // Handle beta vs stable + if (va.isBeta && !vb.isBeta) return -1; // beta < stable + if (!va.isBeta && vb.isBeta) return 1; // stable > beta + + // Both are beta - compare beta numbers + if (va.isBeta && vb.isBeta) { + if (va.beta !== vb.beta) return va.beta > vb.beta ? 1 : -1; + } + + return 0; +} + +/** + * Fetch available versions from GitHub Releases + * Uses GitHub Releases API which is publicly accessible without authentication + */ +async function fetchAvailableVersions() { + try { + // Use GitHub Releases API (public, no auth required) + const response = await axios.get( + 'https://api.github.com/repos/the-luap/picpeak/releases', + { + headers: { + 'Accept': 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + 'User-Agent': 'PicPeak-Update-Checker' + }, + timeout: 10000 + } + ); + + // Extract version tags from releases + const versions = { + stable: [], + beta: [] + }; + + for (const release of response.data) { + const tag = release.tag_name; + if (!tag) continue; + + // Remove 'v' prefix if present + const version = tag.startsWith('v') ? tag.substring(1) : tag; + + if (version.match(/^\d+\.\d+\.\d+$/)) { + // Stable version + versions.stable.push(version); + } else if (version.match(/^\d+\.\d+\.\d+-beta\.\d+$/)) { + // Beta version + versions.beta.push(version); + } + } + + // Sort versions descending (newest first) + versions.stable.sort((a, b) => compareVersions(b, a)); + versions.beta.sort((a, b) => compareVersions(b, a)); + + return versions; + } catch (error) { + logger.error('Failed to fetch available versions from GitHub:', error.message); + return null; + } +} + +/** + * Check for available updates + */ +async function checkForUpdates(forceRefresh = false) { + const now = Date.now(); + + // Use cache if available and not expired + if (!forceRefresh && versionCache && (now - lastCheck) < CACHE_TTL) { + return versionCache; + } + + const currentVersion = await getCurrentVersion(); + const currentChannel = getCurrentChannel(currentVersion); + const availableVersions = await fetchAvailableVersions(); + + if (!availableVersions) { + return { + current: currentVersion, + channel: currentChannel, + updateAvailable: false, + error: 'Unable to check for updates' + }; + } + + // Determine latest version for current channel + const latestStable = availableVersions.stable[0] || currentVersion; + const latestBeta = availableVersions.beta[0] || currentVersion; + const latestForChannel = currentChannel === 'beta' ? latestBeta : latestStable; + + const updateAvailable = compareVersions(latestForChannel, currentVersion) > 0; + + // Also check if there's a newer beta for stable users who want to preview + const newerBetaAvailable = currentChannel === 'stable' && + availableVersions.beta.length > 0 && + compareVersions(latestBeta, currentVersion) > 0; + + const result = { + current: currentVersion, + channel: currentChannel, + latest: { + stable: latestStable, + beta: latestBeta, + forChannel: latestForChannel + }, + updateAvailable, + newerBetaAvailable, + lastChecked: new Date().toISOString() + }; + + // Update cache + versionCache = result; + lastCheck = now; + + return result; +} + +/** + * Clear the version cache (useful for testing) + */ +function clearCache() { + versionCache = null; + lastCheck = 0; +} + +module.exports = { + checkForUpdates, + getCurrentVersion, + getCurrentChannel, + compareVersions, + parseVersion, + clearCache +}; diff --git a/docker-compose.production.yml b/docker-compose.production.yml index c0c869d4..fc53952d 100644 --- a/docker-compose.production.yml +++ b/docker-compose.production.yml @@ -38,7 +38,8 @@ services: backend: # Use pre-built image from GitHub Container Registry - image: ghcr.io/the-luap/picpeak/backend:latest + # PICPEAK_CHANNEL: 'stable' (default), 'beta', or specific version like 'v2.3.0' + image: ghcr.io/the-luap/picpeak/backend:${PICPEAK_CHANNEL:-stable} container_name: picpeak-backend env_file: .env environment: @@ -46,6 +47,7 @@ services: - DB_HOST=${DB_HOST:-postgres} - REDIS_HOST=redis - PHOTOS_DIR=/app/storage/events + - PICPEAK_RELEASE_CHANNEL=${PICPEAK_CHANNEL:-stable} volumes: - ${APP_STORAGE}:/app/storage - ${LOGS}:/app/logs @@ -69,7 +71,8 @@ services: frontend: # Use pre-built image from GitHub Container Registry - image: ghcr.io/the-luap/picpeak/frontend:latest + # Uses same channel as backend for consistency + image: ghcr.io/the-luap/picpeak/frontend:${PICPEAK_CHANNEL:-stable} container_name: picpeak-frontend # Note: Pre-built frontend uses Nginx to proxy /api to backend:3001. # Prefer keeping API base as '/api' in builds to avoid CORS. diff --git a/frontend/src/components/admin/UpdateNotification.tsx b/frontend/src/components/admin/UpdateNotification.tsx new file mode 100644 index 00000000..4284eb02 --- /dev/null +++ b/frontend/src/components/admin/UpdateNotification.tsx @@ -0,0 +1,103 @@ +import React, { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; +import { ArrowUpCircle, X, ExternalLink } from 'lucide-react'; +import { api } from '../../config/api'; + +interface UpdateInfo { + enabled: boolean; + current: string; + channel: 'stable' | 'beta'; + latest: { + stable: string; + beta: string; + forChannel: string; + }; + updateAvailable: boolean; + newerBetaAvailable?: boolean; + lastChecked: string; + error?: string; + message?: string; +} + +async function fetchUpdateInfo(): Promise { + const response = await api.get('/admin/system/updates'); + return response.data; +} + +interface UpdateNotificationProps { + onDismiss?: () => void; +} + +export const UpdateNotification: React.FC = ({ onDismiss }) => { + const { t } = useTranslation(); + const [dismissed, setDismissed] = useState(false); + + const { data: updateInfo } = useQuery({ + queryKey: ['update-check'], + queryFn: fetchUpdateInfo, + staleTime: 60 * 60 * 1000, // 1 hour + retry: false, + refetchOnWindowFocus: false + }); + + // Don't render if no update available, not enabled, or dismissed + if (!updateInfo?.enabled || !updateInfo?.updateAvailable || dismissed) { + return null; + } + + const handleDismiss = () => { + setDismissed(true); + onDismiss?.(); + }; + + const channelLabel = updateInfo.channel === 'beta' + ? t('admin.updates.channelBeta', 'Beta') + : t('admin.updates.channelStable', 'Stable'); + + return ( +
+
+
+ +
+

+ {t('admin.updates.available', 'Update Available')} +

+

+ {t('admin.updates.newVersion', 'Version {{version}} is available', { + version: updateInfo.latest.forChannel + })} + + ({t('admin.updates.currentVersion', 'Current: {{version}}', { + version: updateInfo.current + })}) + +

+

+ {t('admin.updates.channel', 'Channel: {{channel}}', { + channel: channelLabel + })} +

+ + {t('admin.updates.viewReleaseNotes', 'View Release Notes')} + + +
+
+ +
+
+ ); +}; diff --git a/frontend/src/components/admin/VersionInfo.tsx b/frontend/src/components/admin/VersionInfo.tsx index 18f8d1e1..7063722f 100644 --- a/frontend/src/components/admin/VersionInfo.tsx +++ b/frontend/src/components/admin/VersionInfo.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; -import { Info } from 'lucide-react'; +import { Info, ArrowUpCircle } from 'lucide-react'; import { api } from '../../config/api'; import packageJson from '../../../package.json'; @@ -13,6 +13,15 @@ interface SystemVersion { frontend: string; node: string; environment: string; + channel?: 'stable' | 'beta'; +} + +interface UpdateInfo { + enabled: boolean; + updateAvailable: boolean; + latest?: { + forChannel: string; + }; } async function fetchSystemVersion(): Promise { @@ -20,6 +29,11 @@ async function fetchSystemVersion(): Promise { return response.data; } +async function fetchUpdateInfo(): Promise { + const response = await api.get('/admin/system/updates'); + return response.data; +} + export const VersionInfo: React.FC = () => { const { t } = useTranslation(); const { data: versionInfo } = useQuery({ @@ -28,11 +42,25 @@ export const VersionInfo: React.FC = () => { staleTime: 5 * 60 * 1000, // Cache for 5 minutes }); + const { data: updateInfo } = useQuery({ + queryKey: ['update-check'], + queryFn: fetchUpdateInfo, + staleTime: 60 * 60 * 1000, // 1 hour + retry: false + }); + + const channelBadge = versionInfo?.channel === 'beta' ? ( + + {t('admin.updates.beta', 'BETA')} + + ) : null; + return (
{t('admin.version')} + {channelBadge}
Frontend: v{FRONTEND_VERSION}
@@ -40,6 +68,16 @@ export const VersionInfo: React.FC = () => {
Backend: v{versionInfo.backend}
)}
+ {updateInfo?.enabled && updateInfo?.updateAvailable && ( +
+ + + {t('admin.updates.updateAvailableShort', 'v{{version}} available', { + version: updateInfo.latest?.forChannel + })} + +
+ )}
); }; \ No newline at end of file diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 5105eb20..5f24300f 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -1232,6 +1232,20 @@ "error": "Fehler", "checking": "Prüfe..." }, + "updates": { + "available": "Update verfügbar", + "newVersion": "Version {{version}} ist verfügbar", + "currentVersion": "Aktuell: {{version}}", + "channel": "Kanal: {{channel}}", + "channelStable": "Stabil", + "channelBeta": "Beta", + "beta": "BETA", + "viewReleaseNotes": "Versionshinweise anzeigen", + "updateAvailableShort": "v{{version}} verfügbar", + "checkForUpdates": "Nach Updates suchen", + "upToDate": "Alles aktuell", + "lastChecked": "Zuletzt geprüft: {{time}}" + }, "notifications": "Benachrichtigungen", "viewAllNotifications": "Alle Benachrichtigungen anzeigen", "noNotifications": "Keine neuen Benachrichtigungen", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 8428feb9..3cfdc0ba 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1037,6 +1037,20 @@ "error": "Error", "checking": "Checking..." }, + "updates": { + "available": "Update Available", + "newVersion": "Version {{version}} is available", + "currentVersion": "Current: {{version}}", + "channel": "Channel: {{channel}}", + "channelStable": "Stable", + "channelBeta": "Beta", + "beta": "BETA", + "viewReleaseNotes": "View Release Notes", + "updateAvailableShort": "v{{version}} available", + "checkForUpdates": "Check for Updates", + "upToDate": "You're up to date", + "lastChecked": "Last checked: {{time}}" + }, "notifications": "Notifications", "viewAllNotifications": "View all notifications", "noNotifications": "No new notifications", diff --git a/frontend/src/pages/admin/AdminDashboard.tsx b/frontend/src/pages/admin/AdminDashboard.tsx index c73d3ba9..92d8a24e 100644 --- a/frontend/src/pages/admin/AdminDashboard.tsx +++ b/frontend/src/pages/admin/AdminDashboard.tsx @@ -17,6 +17,7 @@ import { useTranslation } from 'react-i18next'; import { useLocalizedDate } from '../../hooks/useLocalizedDate'; import { Button, Card, Loading } from '../../components/common'; +import { UpdateNotification } from '../../components/admin/UpdateNotification'; import { useQuery } from '@tanstack/react-query'; import { eventsService } from '../../services/events.service'; import { adminService } from '../../services/admin.service'; @@ -140,6 +141,9 @@ export const AdminDashboard: React.FC = () => { return (
+ {/* Update Notification */} + + {/* Page Header */}
diff --git a/release-please-config-beta.json b/release-please-config-beta.json new file mode 100644 index 00000000..8cff4c7f --- /dev/null +++ b/release-please-config-beta.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "release-type": "simple", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "include-component-in-tag": false, + "include-v-in-tag": true, + "prerelease": true, + "prerelease-type": "beta", + "changelog-sections": [ + { "type": "feat", "section": "Features", "hidden": false }, + { "type": "fix", "section": "Bug Fixes", "hidden": false }, + { "type": "perf", "section": "Performance Improvements", "hidden": false }, + { "type": "revert", "section": "Reverts", "hidden": false }, + { "type": "docs", "section": "Documentation", "hidden": false }, + { "type": "style", "section": "Styles", "hidden": true }, + { "type": "chore", "section": "Miscellaneous", "hidden": true }, + { "type": "refactor", "section": "Code Refactoring", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "build", "section": "Build System", "hidden": true }, + { "type": "ci", "section": "CI/CD", "hidden": true } + ], + "packages": { + ".": { + "release-type": "simple", + "changelog-path": "CHANGELOG.md", + "extra-files": [] + } + } +}