feat: add admin links management and updaterup page

- Implemented AdminLinksPage for managing link entries with admin role check.
- Created API routes for fetching, updating, and deleting link entries.
- Added ProductTabs component for navigation between Backerup and Updaterup.
- Developed UpdaterupPage with detailed features, setup instructions, and statistics.
- Introduced links database functionality for runtime-editable link overrides.
This commit is contained in:
Anders Böttcher
2026-05-13 10:53:21 +02:00
parent 33e339bde6
commit 984b282a95
16 changed files with 1342 additions and 51 deletions
+34
View File
@@ -0,0 +1,34 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
export default function ProductTabs() {
const pathname = usePathname();
const isUpdaterup = pathname.startsWith("/updaterup");
return (
<div className="flex items-center gap-1 rounded-xl border border-white/8 bg-white/3 p-1">
<Link
href="/"
className={`px-4 py-1.5 rounded-lg text-sm font-semibold transition-all ${
!isUpdaterup
? "bg-cyan-500/20 text-cyan-300 border border-cyan-500/30 shadow-sm"
: "text-slate-400 hover:text-slate-200 hover:bg-white/5"
}`}
>
Backerup
</Link>
<Link
href="/updaterup"
className={`px-4 py-1.5 rounded-lg text-sm font-semibold transition-all ${
isUpdaterup
? "bg-violet-500/20 text-violet-300 border border-violet-500/30 shadow-sm"
: "text-slate-400 hover:text-slate-200 hover:bg-white/5"
}`}
>
Updaterup
</Link>
</div>
);
}
+66 -12
View File
@@ -3,39 +3,49 @@
import { useEffect, useState } from "react";
import { LINKS } from "@/lib/links";
export interface TerminalLinks {
dockerImage?: string;
uiPort?: string;
updaterupDockerImage?: string;
}
interface Line {
type: "comment" | "cmd" | "cmd-cont" | "output" | "success" | "error";
text: string;
copyable?: boolean;
}
function buildDockerRunLines(): Line[] {
function buildDockerRunLines(links: TerminalLinks): Line[] {
const dockerImage = links.dockerImage ?? LINKS.dockerImage;
const uiPort = links.uiPort ?? LINKS.uiPort;
return [
{ type: "comment", text: "# Quick start: Run Backerup directly" },
{ type: "cmd", text: "docker run -d --name backerup \\", copyable: true },
{ type: "cmd-cont", text: ` -p ${LINKS.uiPort}:80 \\`, copyable: true },
{ type: "cmd-cont", text: ` -p ${uiPort}:80 \\`, copyable: true },
{ type: "cmd-cont", text: " -v /var/run/docker.sock:/var/run/docker.sock \\", copyable: true },
{ type: "cmd-cont", text: " -v ${BACKUP_DIR:-/backups}:/backups \\", copyable: true },
{ type: "cmd-cont", text: " -v backerup-config:/app/config \\", copyable: true },
{ type: "cmd-cont", text: " -e BACKUP_DIR=/backups \\", copyable: true },
{ type: "cmd-cont", text: " -e BACKUP_CONFIG_DIR=/app/config \\", copyable: true },
{ type: "cmd-cont", text: ` -e HOST_BACKUP_DIR=/backups \\`, copyable: true },
{ type: "cmd-cont", text: ` ${LINKS.dockerImage}`, copyable: true },
{ type: "cmd-cont", text: ` ${dockerImage}`, copyable: true },
{ type: "output", text: "a7f82c3d1b09" },
{ type: "success", text: "✓ Container started" },
{ type: "success", text: "✓ Ready at http://localhost:8080" },
{ type: "success", text: `✓ Ready at http://localhost:${uiPort}` },
];
}
function buildDockerComposeLines(): Line[] {
function buildDockerComposeLines(links: TerminalLinks): Line[] {
const dockerImage = links.dockerImage ?? LINKS.dockerImage;
const uiPort = links.uiPort ?? LINKS.uiPort;
return [
{ type: "comment", text: "# Using docker-compose for easier management" },
{ type: "cmd", text: "cat > docker-compose.yml << 'EOF'", copyable: false },
{ type: "output", text: "services:" },
{ type: "output", text: " backerup:" },
{ type: "output", text: ` image: ${LINKS.dockerImage}` },
{ type: "output", text: ` image: ${dockerImage}` },
{ type: "output", text: " ports:" },
{ type: "output", text: ` - \"${LINKS.uiPort}:80\"` },
{ type: "output", text: ` - \"${uiPort}:80\"` },
{ type: "output", text: " volumes:" },
{ type: "output", text: " - /var/run/docker.sock:/var/run/docker.sock" },
{ type: "output", text: " - ${BACKUP_DIR:-/backups}:/backups" },
@@ -51,12 +61,56 @@ function buildDockerComposeLines(): Line[] {
{ type: "success", text: "✓ docker-compose.yml created" },
{ type: "cmd", text: "docker compose up -d", copyable: false },
{ type: "output", text: "[+] Building 2.3s (12/12) FINISHED" },
{ type: "success", text: `✓ Ready at http://localhost:${uiPort}` },
];
}
function buildUpdaterupRunLines(links: TerminalLinks): Line[] {
const updaterupDockerImage = links.updaterupDockerImage ?? LINKS.updaterupDockerImage;
return [
{ type: "comment", text: "# Quick start: Run Updaterup directly" },
{ type: "cmd", text: "docker run -d --name updaterup \\", copyable: true },
{ type: "cmd-cont", text: " -p 8080:80 \\", copyable: true },
{ type: "cmd-cont", text: " -v /var/run/docker.sock:/var/run/docker.sock \\", copyable: true },
{ type: "cmd-cont", text: " -v updaterup-config:/app/config \\", copyable: true },
{ type: "cmd-cont", text: ` ${updaterupDockerImage}`, copyable: true },
{ type: "output", text: "b3c91d2e4f07" },
{ type: "success", text: "✓ Container started" },
{ type: "success", text: "✓ Ready at http://localhost:8080" },
];
}
function buildLines(mode: "docker-run" | "docker-compose" = "docker-run"): Line[] {
return mode === "docker-run" ? buildDockerRunLines() : buildDockerComposeLines();
function buildUpdaterupComposeLines(links: TerminalLinks): Line[] {
const updaterupDockerImage = links.updaterupDockerImage ?? LINKS.updaterupDockerImage;
return [
{ type: "comment", text: "# Using docker-compose for Updaterup" },
{ type: "cmd", text: "cat > docker-compose.yml << 'EOF'", copyable: false },
{ type: "output", text: "services:" },
{ type: "output", text: " updaterup:" },
{ type: "output", text: ` image: ${updaterupDockerImage}` },
{ type: "output", text: " ports:" },
{ type: "output", text: " - \"8080:80\"" },
{ type: "output", text: " volumes:" },
{ type: "output", text: " - /var/run/docker.sock:/var/run/docker.sock" },
{ type: "output", text: " - updaterup-config:/app/config" },
{ type: "output", text: " restart: unless-stopped" },
{ type: "output", text: "volumes:" },
{ type: "output", text: " updaterup-config:" },
{ type: "output", text: "EOF" },
{ type: "success", text: "✓ docker-compose.yml created" },
{ type: "cmd", text: "docker compose up -d", copyable: false },
{ type: "output", text: "[+] Building 1.8s (10/10) FINISHED" },
{ type: "success", text: "✓ Ready at http://localhost:8080" },
];
}
function buildLines(mode: "docker-run" | "docker-compose" | "updaterup" | "updaterup-compose" = "docker-run", links: TerminalLinks = {}): Line[] {
switch (mode) {
case "docker-compose": return buildDockerComposeLines(links);
case "updaterup": return buildUpdaterupRunLines(links);
case "updaterup-compose": return buildUpdaterupComposeLines(links);
default: return buildDockerRunLines(links);
}
}
function lineSpeed(line: Line) {
@@ -88,8 +142,8 @@ function linePrompt(line: Line): string {
return "";
}
export default function TerminalTyper({ mode = "docker-run" }: { mode?: "docker-run" | "docker-compose" } = {}) {
const LINES = buildLines(mode);
export default function TerminalTyper({ mode = "docker-run", links = {} }: { mode?: "docker-run" | "docker-compose" | "updaterup" | "updaterup-compose"; links?: TerminalLinks } = {}) {
const LINES = buildLines(mode, links);
const [doneLines, setDoneLines] = useState<number[]>([]);
const [current, setCurrent] = useState<{ idx: number; chars: number } | null>({ idx: 0, chars: 0 });
const [copied, setCopied] = useState(false);
@@ -97,7 +151,7 @@ export default function TerminalTyper({ mode = "docker-run" }: { mode?: "docker-
const copyToClipboard = () => {
let textToCopy = "";
if (mode === "docker-compose") {
if (mode === "docker-compose" || mode === "updaterup-compose") {
// For docker-compose, copy the YAML content only
const startIdx = LINES.findIndex(line => line.type === "output" && line.text === "services:");
const endIdx = LINES.findIndex((line, i) => i > startIdx && line.type === "output" && line.text === "EOF");