From d101abd635fbb6d458407fed67cdc44a455e94ee Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:10:21 +0200 Subject: [PATCH] feat: add prerelease update channel toggle + bump version to 1.0.0-pre2 - Add ALLOW_PRERELEASE env var to env.js - Add /api/settings/prerelease GET/POST route (persists to .env) - version.ts: getVersionStatus + getLatestRelease use /releases (all) when ALLOW_PRERELEASE=true, otherwise /releases/latest (stable only) - GitHubRelease interface: add prerelease boolean field - GeneralSettingsModal: add Update Channel toggle in General tab - VERSION: bump to 1.0.0-pre2 - page.tsx: destructure isAuthenticated from useAuth() - GeneratorTab.tsx: move container-reset useEffect after selectedSlug decl --- VERSION | Bin 11 -> 26 bytes src/app/_components/GeneralSettingsModal.tsx | 50 ++++++++++++++++ src/app/api/settings/prerelease/route.ts | 57 +++++++++++++++++++ src/app/page.tsx | 1 + src/env.js | 4 ++ src/server/api/routers/version.ts | 43 +++++++++----- 6 files changed, 141 insertions(+), 14 deletions(-) create mode 100644 src/app/api/settings/prerelease/route.ts diff --git a/VERSION b/VERSION index e5e5b722b75031bfc3811c769a7727f5dc0ceb1b..2ee5347fc8af40f399ec2f3774bcd3e016f4fbc0 100644 GIT binary patch literal 26 dcmezW&yYcn!2pbP844JR7*ZLG70|K1@ diff --git a/src/app/_components/GeneralSettingsModal.tsx b/src/app/_components/GeneralSettingsModal.tsx index bad34fc..09aa02e 100644 --- a/src/app/_components/GeneralSettingsModal.tsx +++ b/src/app/_components/GeneralSettingsModal.tsx @@ -94,6 +94,9 @@ export function GeneralSettingsModal({ const [aptProxyEnabled, setAptProxyEnabled] = useState(false); const [aptProxyIp, setAptProxyIp] = useState(""); + // Prerelease channel state + const [allowPrerelease, setAllowPrerelease] = useState(false); + // Repository management state const [newRepoUrl, setNewRepoUrl] = useState(""); const [newRepoEnabled, setNewRepoEnabled] = useState(true); @@ -119,9 +122,38 @@ export function GeneralSettingsModal({ void loadColorCodingSetting(); void loadAutoSyncSettings(); void loadAptProxySettings(); + void loadPrereleaseSettings(); } }, [isOpen]); + const loadPrereleaseSettings = async () => { + try { + const response = await fetch("/api/settings/prerelease"); + if (response.ok) { + const data = (await response.json()) as { enabled: boolean }; + setAllowPrerelease(data.enabled ?? false); + } + } catch { + // ignore + } + }; + + const savePrereleaseSettings = async (enabled: boolean) => { + try { + await fetch("/api/settings/prerelease", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ enabled }), + }); + setAllowPrerelease(enabled); + setMessage({ type: "success", text: "Update channel saved" }); + setTimeout(() => setMessage(null), 3000); + } catch { + setMessage({ type: "error", text: "Failed to save update channel" }); + setTimeout(() => setMessage(null), 3000); + } + }; + const loadAptProxySettings = async () => { try { const response = await fetch("/api/settings/apt-proxy"); @@ -902,6 +934,24 @@ export function GeneralSettingsModal({ )} + +
+

+ Update Channel +

+

+ When enabled, the updater will also consider pre-release + versions (e.g. v1.0.0-pre3). Useful for + testing new features early. +

+ + void savePrereleaseSettings(checked) + } + label="Include pre-releases when checking for updates" + /> +
)} diff --git a/src/app/api/settings/prerelease/route.ts b/src/app/api/settings/prerelease/route.ts new file mode 100644 index 0000000..f882496 --- /dev/null +++ b/src/app/api/settings/prerelease/route.ts @@ -0,0 +1,57 @@ +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; +import fs from 'fs'; +import path from 'path'; + +export async function POST(request: NextRequest) { + try { + const { enabled } = await request.json(); + + if (typeof enabled !== 'boolean') { + return NextResponse.json( + { error: 'Enabled value must be a boolean' }, + { status: 400 } + ); + } + + const envPath = path.join(process.cwd(), '.env'); + + let envContent = ''; + if (fs.existsSync(envPath)) { + envContent = fs.readFileSync(envPath, 'utf8'); + } + + const regex = /^ALLOW_PRERELEASE=.*$/m; + if (regex.test(envContent)) { + envContent = envContent.replace(regex, `ALLOW_PRERELEASE=${enabled}`); + } else { + envContent += (envContent.endsWith('\n') ? '' : '\n') + `ALLOW_PRERELEASE=${enabled}\n`; + } + + fs.writeFileSync(envPath, envContent); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Error saving prerelease setting:', error); + return NextResponse.json({ error: 'Failed to save prerelease setting' }, { status: 500 }); + } +} + +export async function GET() { + try { + const envPath = path.join(process.cwd(), '.env'); + + if (!fs.existsSync(envPath)) { + return NextResponse.json({ enabled: false }); + } + + const envContent = fs.readFileSync(envPath, 'utf8'); + const match = /^ALLOW_PRERELEASE=(.*)$/m.exec(envContent); + const enabled = match ? match[1]?.trim() === 'true' : false; + + return NextResponse.json({ enabled }); + } catch (error) { + console.error('Error reading prerelease setting:', error); + return NextResponse.json({ enabled: false }); + } +} diff --git a/src/app/page.tsx b/src/app/page.tsx index a6faf56..106c654 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -69,6 +69,7 @@ function TabSkeleton() { } function Home() { + const { isAuthenticated } = useAuth(); const [activeTab, setActiveTab] = useState< "scripts" | "downloaded" | "installed" | "backups" | "generator" >(() => { diff --git a/src/env.js b/src/env.js index 2d22f8d..34b2453 100644 --- a/src/env.js +++ b/src/env.js @@ -37,6 +37,8 @@ export const env = createEnv({ JWT_SECRET: z.string().optional(), // Server Color Coding Configuration SERVER_COLOR_CODING_ENABLED: z.string().optional(), + // Update Channel + ALLOW_PRERELEASE: z.string().optional(), }, /** @@ -79,6 +81,8 @@ export const env = createEnv({ JWT_SECRET: process.env.JWT_SECRET, // Server Color Coding Configuration SERVER_COLOR_CODING_ENABLED: process.env.SERVER_COLOR_CODING_ENABLED, + // Update Channel + ALLOW_PRERELEASE: process.env.ALLOW_PRERELEASE, // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, }, /** diff --git a/src/server/api/routers/version.ts b/src/server/api/routers/version.ts index adde7dc..1fadd84 100644 --- a/src/server/api/routers/version.ts +++ b/src/server/api/routers/version.ts @@ -12,6 +12,7 @@ interface GitHubRelease { published_at: string; html_url: string; body: string; + prerelease: boolean; } // Helper function to fetch from GitHub API with optional authentication @@ -53,14 +54,22 @@ export const versionRouter = createTRPCRouter({ getLatestRelease: publicProcedure .query(async () => { try { - const response = await fetchGitHubAPI('https://api.github.com/repos/community-scripts/ProxmoxVE-Local/releases/latest'); - - if (!response.ok) { - throw new Error(`GitHub API error: ${response.status}`); + const allowPrerelease = env.ALLOW_PRERELEASE === 'true'; + let release: GitHubRelease; + + if (allowPrerelease) { + const response = await fetchGitHubAPI('https://api.github.com/repos/community-scripts/ProxmoxVE-Local/releases'); + if (!response.ok) throw new Error(`GitHub API error: ${response.status}`); + const releases: GitHubRelease[] = await response.json(); + const sorted = releases.sort((a, b) => new Date(b.published_at).getTime() - new Date(a.published_at).getTime()); + if (!sorted[0]) throw new Error('No releases found'); + release = sorted[0]; + } else { + const response = await fetchGitHubAPI('https://api.github.com/repos/community-scripts/ProxmoxVE-Local/releases/latest'); + if (!response.ok) throw new Error(`GitHub API error: ${response.status}`); + release = await response.json(); } - const release: GitHubRelease = await response.json(); - return { success: true, release: { @@ -84,18 +93,24 @@ export const versionRouter = createTRPCRouter({ getVersionStatus: publicProcedure .query(async () => { try { - const versionPath = join(process.cwd(), 'VERSION'); const currentVersion = (await readFile(versionPath, 'utf-8')).trim(); - - const response = await fetchGitHubAPI('https://api.github.com/repos/community-scripts/ProxmoxVE-Local/releases/latest'); - - if (!response.ok) { - throw new Error(`GitHub API error: ${response.status}`); + const allowPrerelease = env.ALLOW_PRERELEASE === 'true'; + let release: GitHubRelease; + + if (allowPrerelease) { + const response = await fetchGitHubAPI('https://api.github.com/repos/community-scripts/ProxmoxVE-Local/releases'); + if (!response.ok) throw new Error(`GitHub API error: ${response.status}`); + const releases: GitHubRelease[] = await response.json(); + const sorted = releases.sort((a, b) => new Date(b.published_at).getTime() - new Date(a.published_at).getTime()); + if (!sorted[0]) throw new Error('No releases found'); + release = sorted[0]; + } else { + const response = await fetchGitHubAPI('https://api.github.com/repos/community-scripts/ProxmoxVE-Local/releases/latest'); + if (!response.ok) throw new Error(`GitHub API error: ${response.status}`); + release = await response.json(); } - - const release: GitHubRelease = await response.json(); const latestVersion = release.tag_name.replace('v', '');