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
This commit is contained in:
CanbiZ (MickLesk)
2026-04-21 16:10:21 +02:00
parent 5214061bd2
commit d101abd635
6 changed files with 141 additions and 14 deletions

BIN
VERSION

Binary file not shown.

View File

@@ -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({
</div>
)}
</div>
<div className="border-border rounded-lg border p-4">
<h4 className="text-foreground mb-2 font-medium">
Update Channel
</h4>
<p className="text-muted-foreground mb-4 text-sm">
When enabled, the updater will also consider pre-release
versions (e.g. <code>v1.0.0-pre3</code>). Useful for
testing new features early.
</p>
<Toggle
checked={allowPrerelease}
onCheckedChange={(checked) =>
void savePrereleaseSettings(checked)
}
label="Include pre-releases when checking for updates"
/>
</div>
</div>
</div>
)}

View File

@@ -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 });
}
}

View File

@@ -69,6 +69,7 @@ function TabSkeleton() {
}
function Home() {
const { isAuthenticated } = useAuth();
const [activeTab, setActiveTab] = useState<
"scripts" | "downloaded" | "installed" | "backups" | "generator"
>(() => {

View File

@@ -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,
},
/**

View File

@@ -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', '');