feat: add server settings (toggle on/off, port selection, start on system start) to Settings UI and backend
This commit is contained in:
+167
-3
@@ -1,5 +1,7 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import Fastify from "fastify";
|
||||
import cors from "@fastify/cors";
|
||||
@@ -62,7 +64,169 @@ export async function buildApp() {
|
||||
} catch {
|
||||
dbUp = false;
|
||||
}
|
||||
return { ok: dbUp, db: dbUp ? "up" : "down", version: config.version, uptime: process.uptime() };
|
||||
return {
|
||||
ok: dbUp,
|
||||
db: dbUp ? "up" : "down",
|
||||
version: config.version,
|
||||
port: config.port,
|
||||
uptime: process.uptime(),
|
||||
};
|
||||
});
|
||||
|
||||
const sysConfigDir = join(homedir(), ".config", "pubscript");
|
||||
const sysConfigPath = join(sysConfigDir, "server.json");
|
||||
const autostartDir = join(homedir(), ".config", "autostart");
|
||||
const autostartDesktopPath = join(autostartDir, "pubscript-server.desktop");
|
||||
const systemdUserDir = join(homedir(), ".config", "systemd", "user");
|
||||
const systemdServicePath = join(systemdUserDir, "pubscript-server.service");
|
||||
|
||||
interface PersistedServerSysConfig {
|
||||
enabled: boolean;
|
||||
port: number;
|
||||
startOnBoot: boolean;
|
||||
}
|
||||
|
||||
function loadPersistedServerSysConfig(): PersistedServerSysConfig {
|
||||
try {
|
||||
if (existsSync(sysConfigPath)) {
|
||||
const raw = JSON.parse(readFileSync(sysConfigPath, "utf8")) as Partial<PersistedServerSysConfig>;
|
||||
return {
|
||||
enabled: typeof raw.enabled === "boolean" ? raw.enabled : true,
|
||||
port:
|
||||
typeof raw.port === "number" && raw.port >= 1024 && raw.port <= 65535
|
||||
? raw.port
|
||||
: config.port,
|
||||
startOnBoot:
|
||||
typeof raw.startOnBoot === "boolean"
|
||||
? raw.startOnBoot
|
||||
: existsSync(autostartDesktopPath) || existsSync(systemdServicePath),
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
/* ignore malformed config */
|
||||
}
|
||||
return {
|
||||
enabled: true,
|
||||
port: config.port,
|
||||
startOnBoot: existsSync(autostartDesktopPath) || existsSync(systemdServicePath),
|
||||
};
|
||||
}
|
||||
|
||||
function applySystemStartupFiles(cfg: PersistedServerSysConfig): boolean {
|
||||
try {
|
||||
mkdirSync(sysConfigDir, { recursive: true });
|
||||
writeFileSync(sysConfigPath, JSON.stringify(cfg, null, 2), "utf8");
|
||||
|
||||
const dataDir = join(homedir(), ".local", "share", "pubscript");
|
||||
const dbFile = join(dataDir, "pubscript.sqlite");
|
||||
const optServerEntry = "/opt/pubscript/server/dist/server.mjs";
|
||||
const localServerEntry = resolve(dirname(fileURLToPath(import.meta.url)), "index.ts");
|
||||
const execCmd = existsSync(optServerEntry)
|
||||
? `/usr/bin/env PORT=${cfg.port} PUBSCRIPT_DB=${dbFile} PUBSCRIPT_WEB_DIST=/opt/pubscript/apps/web/dist node ${optServerEntry}`
|
||||
: `/usr/bin/env PORT=${cfg.port} PUBSCRIPT_DB=${dbFile} node --import tsx ${localServerEntry}`;
|
||||
|
||||
if (cfg.startOnBoot) {
|
||||
mkdirSync(autostartDir, { recursive: true });
|
||||
const desktopContent = [
|
||||
"[Desktop Entry]",
|
||||
"Type=Application",
|
||||
"Name=PubScript Server",
|
||||
"Comment=PubScript Background API & Manuscript Server",
|
||||
`Exec=${execCmd}`,
|
||||
"Icon=pubscript",
|
||||
"Terminal=false",
|
||||
"Hidden=false",
|
||||
"X-GNOME-Autostart-enabled=true",
|
||||
"",
|
||||
].join("\n");
|
||||
writeFileSync(autostartDesktopPath, desktopContent, "utf8");
|
||||
|
||||
mkdirSync(systemdUserDir, { recursive: true });
|
||||
const serviceContent = [
|
||||
"[Unit]",
|
||||
"Description=PubScript Local Writing Studio Server",
|
||||
"After=network.target",
|
||||
"",
|
||||
"[Service]",
|
||||
"Type=simple",
|
||||
`Environment=PORT=${cfg.port}`,
|
||||
`Environment=PUBSCRIPT_DB=${dbFile}`,
|
||||
`ExecStart=${existsSync(optServerEntry) ? `/usr/bin/node ${optServerEntry}` : `/usr/bin/node --import tsx ${localServerEntry}`}`,
|
||||
"Restart=on-failure",
|
||||
"RestartSec=3",
|
||||
"",
|
||||
"[Install]",
|
||||
"WantedBy=default.target",
|
||||
"",
|
||||
].join("\n");
|
||||
writeFileSync(systemdServicePath, serviceContent, "utf8");
|
||||
try {
|
||||
execFileSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
||||
execFileSync("systemctl", ["--user", "enable", "pubscript-server.service"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
} catch {
|
||||
/* systemctl --user optional when XDG autostart is used */
|
||||
}
|
||||
} else {
|
||||
if (existsSync(autostartDesktopPath)) {
|
||||
rmSync(autostartDesktopPath, { force: true });
|
||||
}
|
||||
try {
|
||||
execFileSync("systemctl", ["--user", "disable", "pubscript-server.service"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
if (existsSync(systemdServicePath)) {
|
||||
rmSync(systemdServicePath, { force: true });
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
app.get("/api/system/server-config", async () => {
|
||||
const persisted = loadPersistedServerSysConfig();
|
||||
return {
|
||||
ok: true,
|
||||
enabled: persisted.enabled,
|
||||
port: persisted.port,
|
||||
activePort: config.port,
|
||||
startOnBoot: persisted.startOnBoot,
|
||||
autostartPath: autostartDesktopPath,
|
||||
systemdServicePath,
|
||||
};
|
||||
});
|
||||
|
||||
app.post("/api/system/server-config", async (req, reply) => {
|
||||
const body = (req.body ?? {}) as Partial<PersistedServerSysConfig>;
|
||||
const current = loadPersistedServerSysConfig();
|
||||
const nextPort =
|
||||
typeof body.port === "number" ? Math.trunc(body.port) : current.port;
|
||||
if (!Number.isFinite(nextPort) || nextPort < 1024 || nextPort > 65535) {
|
||||
return reply.code(400).send({ error: "invalid_port" });
|
||||
}
|
||||
const next: PersistedServerSysConfig = {
|
||||
enabled: typeof body.enabled === "boolean" ? body.enabled : current.enabled,
|
||||
port: nextPort,
|
||||
startOnBoot:
|
||||
typeof body.startOnBoot === "boolean" ? body.startOnBoot : current.startOnBoot,
|
||||
};
|
||||
const configured = applySystemStartupFiles(next);
|
||||
return {
|
||||
ok: true,
|
||||
enabled: next.enabled,
|
||||
port: next.port,
|
||||
activePort: config.port,
|
||||
startOnBoot: next.startOnBoot,
|
||||
autostartConfigured: configured,
|
||||
autostartPath: autostartDesktopPath,
|
||||
systemdServicePath,
|
||||
};
|
||||
});
|
||||
|
||||
registerAuthHook(app);
|
||||
|
||||
Reference in New Issue
Block a user