feat: self-contained Docker installer with web setup wizard

- settings.ts: persistent settings.json in data volume (survives restarts)
- /api/setup: POST to save API_BASE_URL, API_KEY, WEB_PASSWORD, WEBHOOK_SECRET
- /setup: web-based setup wizard for first-time configuration
- /api/health: JSON health check endpoint for Docker healthcheck
- api.ts: dynamic API_BASE_URL/API_KEY resolution from settings
- login route: fallback WEB_PASSWORD from settings.json
- middleware: /setup and /api/setup are public paths
- Dockerfile: HEALTHCHECK instruction
- docker-compose.yml: healthcheck + optional .env file
- .env.example: full documentation of all env vars
- install.sh: single-command VPS installer (clone, build, run)
This commit is contained in:
Renato
2026-07-27 23:24:57 +02:00
parent 571b6d1a7c
commit 0fc421f234
12 changed files with 424 additions and 15 deletions
+6 -5
View File
@@ -11,19 +11,20 @@ import type {
SystemStatus,
} from "./types"
const API_BASE = process.env.API_BASE_URL || "http://127.0.0.1:8080/api/v1"
const API_KEY = process.env.API_KEY || ""
import { get } from "./settings"
async function fetchApi<T>(
path: string,
init?: RequestInit,
): Promise<T> {
const url = `${API_BASE}${path}`
const apiBase = get("API_BASE_URL", "http://127.0.0.1:8080/api/v1")
const apiKey = get("API_KEY")
const url = `${apiBase}${path}`
const headers: Record<string, string> = {
...(init?.headers as Record<string, string>),
}
if (API_KEY) {
headers["X-API-Key"] = API_KEY
if (apiKey) {
headers["X-API-Key"] = apiKey
}
if (init?.body && typeof init.body === "string" && !(headers["Content-Type"])) {
headers["Content-Type"] = "application/json"
+45
View File
@@ -0,0 +1,45 @@
import "server-only"
import fs from "fs"
import path from "path"
const DATA_DIR = process.env.DB_PATH
? path.dirname(process.env.DB_PATH)
: path.join(process.cwd(), "data")
const SETTINGS_PATH = path.join(DATA_DIR, "settings.json")
let settingsCache: Record<string, string> = {}
export function initSettings(): void {
try {
if (!fs.existsSync(SETTINGS_PATH)) {
settingsCache = {}
return
}
const raw = fs.readFileSync(SETTINGS_PATH, "utf-8")
settingsCache = JSON.parse(raw)
} catch {
settingsCache = {}
}
}
export function get(key: string, fallback = ""): string {
return settingsCache[key] || process.env[key] || fallback
}
export function isConfigured(): boolean {
return Boolean(get("API_BASE_URL", ""))
}
export function saveSettings(
values: Record<string, string>,
): void {
fs.mkdirSync(DATA_DIR, { recursive: true })
Object.assign(settingsCache, values)
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settingsCache, null, 2) + "\n")
}
export function allSettings(): Record<string, string> {
return { ...settingsCache }
}