617e778a48
Add dual-channel release strategy for stable and beta releases: Release Channels: - Stable channel: production-ready releases (stable, latest, v2.3.0) - Beta channel: early access features (beta, v2.3.0-beta.1) - Configurable via PICPEAK_CHANNEL environment variable Update Notifications: - Admin dashboard shows available updates for configured channel - Checks GitHub Releases API with 1-hour cache - Can be disabled with UPDATE_CHECK_ENABLED=false CI/CD Changes: - New release-please-beta.yml workflow for beta prereleases - Docker build workflow produces stable/beta tags based on branch - Beta versions use v2.3.0-beta.1 format New Files: - .github/workflows/release-please-beta.yml - release-please-config-beta.json - .release-please-manifest-beta.json - backend/src/services/updateCheckService.js - frontend/src/components/admin/UpdateNotification.tsx Modified Files: - docker-compose.production.yml (channel selection) - .env.example (PICPEAK_CHANNEL, UPDATE_CHECK_ENABLED) - backend/src/routes/adminSystem.js (/updates endpoint) - frontend components (VersionInfo, AdminDashboard) - i18n locales (en.json, de.json) - README.md and DEPLOYMENT_GUIDE.md (documentation)
104 lines
3.2 KiB
TypeScript
104 lines
3.2 KiB
TypeScript
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<UpdateInfo> {
|
|
const response = await api.get<UpdateInfo>('/admin/system/updates');
|
|
return response.data;
|
|
}
|
|
|
|
interface UpdateNotificationProps {
|
|
onDismiss?: () => void;
|
|
}
|
|
|
|
export const UpdateNotification: React.FC<UpdateNotificationProps> = ({ 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 (
|
|
<div className="bg-blue-50 border-l-4 border-blue-500 p-4 mb-4 rounded-r-lg">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-start">
|
|
<ArrowUpCircle className="w-5 h-5 text-blue-500 mt-0.5 mr-3 flex-shrink-0" />
|
|
<div>
|
|
<h4 className="text-sm font-semibold text-blue-800">
|
|
{t('admin.updates.available', 'Update Available')}
|
|
</h4>
|
|
<p className="text-sm text-blue-700 mt-1">
|
|
{t('admin.updates.newVersion', 'Version {{version}} is available', {
|
|
version: updateInfo.latest.forChannel
|
|
})}
|
|
<span className="text-blue-500 ml-2">
|
|
({t('admin.updates.currentVersion', 'Current: {{version}}', {
|
|
version: updateInfo.current
|
|
})})
|
|
</span>
|
|
</p>
|
|
<p className="text-xs text-blue-600 mt-1">
|
|
{t('admin.updates.channel', 'Channel: {{channel}}', {
|
|
channel: channelLabel
|
|
})}
|
|
</p>
|
|
<a
|
|
href="https://github.com/the-luap/picpeak/releases"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center text-xs text-blue-600 hover:text-blue-800 mt-2"
|
|
>
|
|
{t('admin.updates.viewReleaseNotes', 'View Release Notes')}
|
|
<ExternalLink className="w-3 h-3 ml-1" />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={handleDismiss}
|
|
className="text-blue-400 hover:text-blue-600 p-1"
|
|
aria-label={t('common.close', 'Close')}
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|