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
+6
View File
@@ -65,6 +65,12 @@ async function applyMigrations(db: Client): Promise<void> {
role TEXT NOT NULL DEFAULT 'user',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS links (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`)
}
+78
View File
@@ -0,0 +1,78 @@
/**
* Runtime-editable link overrides stored in SQLite.
* Falls back to the static LINKS defaults for any key without an override.
*/
import { getDb, ensureReady } from './db'
import { LINKS } from './links'
export type LinkKey = keyof typeof LINKS
/** Human-readable labels for each link key (used in the admin UI). */
export const LINK_META: Record<LinkKey, { label: string; description: string; group: string }> = {
gitea: { label: 'Backerup — Gitea repo', description: 'Repository root used in nav, footer, and CTA buttons', group: 'Backerup' },
giteaReleases: { label: 'Backerup — Releases', description: 'Releases / changelog page', group: 'Backerup' },
giteaIssues: { label: 'Backerup — Issues', description: 'Issue tracker', group: 'Backerup' },
giteaDiscussions: { label: 'Backerup — Discussions', description: 'Gitea Discussions forum', group: 'Backerup' },
giteaContributing: { label: 'Backerup — Contributing', description: 'CONTRIBUTING.md guide', group: 'Backerup' },
docs: { label: 'Backerup — Docs', description: 'Primary documentation URL', group: 'Backerup' },
dockerImage: { label: 'Backerup — Docker image', description: 'Full image reference shown in install snippets', group: 'Backerup' },
uiPort: { label: 'Backerup — UI port', description: 'Default port the Backerup container exposes', group: 'Backerup' },
updaterupGitea: { label: 'Updaterup — Gitea repo', description: 'Repository root used in Updaterup pages', group: 'Updaterup' },
updaterupDockerImage: { label: 'Updaterup — Docker image', description: 'Full image reference shown in Updaterup install snippets', group: 'Updaterup' },
}
export interface LinkEntry {
key: LinkKey
label: string
description: string
group: string
defaultValue: string
override: string | null
currentValue: string
}
/** Returns merged links: DB overrides take precedence over static defaults. */
export async function getLinks(): Promise<typeof LINKS> {
await ensureReady()
const { rows } = await getDb().execute('SELECT key, value FROM links')
const overrides: Partial<Record<string, string>> = {}
for (const row of rows) {
overrides[row.key as string] = row.value as string
}
return { ...LINKS, ...overrides } as typeof LINKS
}
/** Returns all link entries with defaults, overrides, and current values — for the admin UI. */
export async function getAllLinkEntries(): Promise<LinkEntry[]> {
await ensureReady()
const { rows } = await getDb().execute('SELECT key, value FROM links')
const overrides: Partial<Record<string, string>> = {}
for (const row of rows) {
overrides[row.key as string] = row.value as string
}
return (Object.keys(LINK_META) as LinkKey[]).map((key) => ({
key,
...LINK_META[key],
defaultValue: LINKS[key],
override: overrides[key] ?? null,
currentValue: overrides[key] ?? LINKS[key],
}))
}
/** Upserts a link override. Pass the static default to effectively reset it, or use resetLink(). */
export async function setLink(key: LinkKey, value: string): Promise<void> {
await ensureReady()
await getDb().execute({
sql: `INSERT INTO links (key, value, updated_at)
VALUES (?, ?, datetime('now'))
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,
args: [key, value],
})
}
/** Removes the DB override for a key, restoring the static default. */
export async function resetLink(key: LinkKey): Promise<void> {
await ensureReady()
await getDb().execute({ sql: 'DELETE FROM links WHERE key = ?', args: [key] })
}
+6
View File
@@ -26,6 +26,12 @@ export const LINKS = {
dockerImage: "gitea.doomlabs.de/kptltd00m/backerup:latest",
/** Default UI port the container exposes */
uiPort: "8080",
// ── Updaterup ─────────────────────────────────────────────
/** Updaterup repository root */
updaterupGitea: "https://gitea.doomlabs.de/KptltD00M/updaterup",
/** Updaterup Docker image reference */
updaterupDockerImage: "gitea.doomlabs.de/kptltd00m/updaterup:latest",
} as const;
export type Links = typeof LINKS;