feat: add server settings (toggle on/off, port selection, start on system start) to Settings UI and backend
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { createUlid, type AuditLogEntry, type Role, type User } from "@pubscript/core";
|
import { createUlid, type AuditLogEntry, type Role, type User } from "@pubscript/core";
|
||||||
|
import { useSettings } from "./store/settings.js";
|
||||||
|
|
||||||
const BASE = "/api";
|
const BASE = "/api";
|
||||||
const TOKEN_KEY = "pubscript.admin.token";
|
const TOKEN_KEY = "pubscript.admin.token";
|
||||||
@@ -138,6 +139,10 @@ async function bootstrapAdminSession(): Promise<string | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function request<T>(path: string, init?: RequestInit, retried = false): Promise<T> {
|
async function request<T>(path: string, init?: RequestInit, retried = false): Promise<T> {
|
||||||
|
const { serverEnabled } = useSettings.getState();
|
||||||
|
if (!serverEnabled) {
|
||||||
|
throw new Error("Server disabled in settings (Local Storage Mode)");
|
||||||
|
}
|
||||||
const token = getStoredToken();
|
const token = getStoredToken();
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
...(init?.body ? { "Content-Type": "application/json" } : {}),
|
...(init?.body ? { "Content-Type": "application/json" } : {}),
|
||||||
@@ -361,4 +366,96 @@ export const api = {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
logout: () => request<{ ok: true }>("/auth/logout", { method: "POST" }),
|
logout: () => request<{ ok: true }>("/auth/logout", { method: "POST" }),
|
||||||
|
|
||||||
|
getServerConfig: async (portHint?: number): Promise<{
|
||||||
|
online: boolean;
|
||||||
|
enabled: boolean;
|
||||||
|
port: number;
|
||||||
|
activePort?: number;
|
||||||
|
startOnBoot: boolean;
|
||||||
|
}> => {
|
||||||
|
const endpoints = [
|
||||||
|
"/api/system/server-config",
|
||||||
|
...(portHint ? [`http://localhost:${portHint}/api/system/server-config`] : []),
|
||||||
|
];
|
||||||
|
for (const url of endpoints) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { credentials: "include" });
|
||||||
|
const ct = res.headers.get("content-type") ?? "";
|
||||||
|
if (res.ok && ct.includes("application/json")) {
|
||||||
|
const data = (await res.json()) as {
|
||||||
|
enabled: boolean;
|
||||||
|
port: number;
|
||||||
|
activePort?: number;
|
||||||
|
startOnBoot: boolean;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
online: true,
|
||||||
|
enabled: data.enabled,
|
||||||
|
port: data.port,
|
||||||
|
...(data.activePort !== undefined ? { activePort: data.activePort } : {}),
|
||||||
|
startOnBoot: data.startOnBoot,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* try next endpoint */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
online: false,
|
||||||
|
enabled: true,
|
||||||
|
port: portHint ?? 8081,
|
||||||
|
startOnBoot: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
updateServerConfig: async (
|
||||||
|
patch: { enabled?: boolean; port?: number; startOnBoot?: boolean },
|
||||||
|
portHint?: number,
|
||||||
|
): Promise<{
|
||||||
|
online: boolean;
|
||||||
|
enabled: boolean;
|
||||||
|
port: number;
|
||||||
|
activePort?: number;
|
||||||
|
startOnBoot: boolean;
|
||||||
|
}> => {
|
||||||
|
const endpoints = [
|
||||||
|
"/api/system/server-config",
|
||||||
|
...(portHint ? [`http://localhost:${portHint}/api/system/server-config`] : []),
|
||||||
|
];
|
||||||
|
for (const url of endpoints) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(patch),
|
||||||
|
});
|
||||||
|
const ct = res.headers.get("content-type") ?? "";
|
||||||
|
if (res.ok && ct.includes("application/json")) {
|
||||||
|
const data = (await res.json()) as {
|
||||||
|
enabled: boolean;
|
||||||
|
port: number;
|
||||||
|
activePort?: number;
|
||||||
|
startOnBoot: boolean;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
online: true,
|
||||||
|
enabled: data.enabled,
|
||||||
|
port: data.port,
|
||||||
|
...(data.activePort !== undefined ? { activePort: data.activePort } : {}),
|
||||||
|
startOnBoot: data.startOnBoot,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* try next endpoint */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
online: false,
|
||||||
|
enabled: patch.enabled ?? true,
|
||||||
|
port: patch.port ?? portHint ?? 8081,
|
||||||
|
startOnBoot: patch.startOnBoot ?? false,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
useSettings,
|
useSettings,
|
||||||
DEFAULT_AI_AGENTS,
|
DEFAULT_AI_AGENTS,
|
||||||
type Density,
|
type Density,
|
||||||
type AppSettings,
|
type AppSettings,
|
||||||
} from "../store/settings.js";
|
} from "../store/settings.js";
|
||||||
|
import { api } from "../api.js";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
Button,
|
Button,
|
||||||
@@ -35,6 +36,82 @@ export function Settings() {
|
|||||||
const [selectedAgentId, setSelectedAgentId] = useState<string>(s.activeAgentId);
|
const [selectedAgentId, setSelectedAgentId] = useState<string>(s.activeAgentId);
|
||||||
const [probing, setProbing] = useState(false);
|
const [probing, setProbing] = useState(false);
|
||||||
|
|
||||||
|
// Server settings state
|
||||||
|
const [portDraft, setPortDraft] = useState<string>(String(s.serverPort));
|
||||||
|
const [serverOnline, setServerOnline] = useState<boolean>(false);
|
||||||
|
const [activeRunningPort, setActiveRunningPort] = useState<number | null>(null);
|
||||||
|
const [savingServerCfg, setSavingServerCfg] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
void api.getServerConfig(s.serverPort).then((res) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setServerOnline(res.online);
|
||||||
|
setActiveRunningPort(res.activePort ?? res.port);
|
||||||
|
if (res.online) {
|
||||||
|
setPortDraft(String(res.port));
|
||||||
|
set({
|
||||||
|
serverEnabled: res.enabled,
|
||||||
|
serverPort: res.port,
|
||||||
|
serverStartOnBoot: res.startOnBoot,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleToggleServerEnabled = async (enabled: boolean) => {
|
||||||
|
set({ serverEnabled: enabled });
|
||||||
|
setSavingServerCfg(true);
|
||||||
|
try {
|
||||||
|
const res = await api.updateServerConfig({ enabled }, s.serverPort);
|
||||||
|
setServerOnline(res.online && enabled);
|
||||||
|
toast.success(
|
||||||
|
enabled
|
||||||
|
? `Server enabled (port ${s.serverPort})`
|
||||||
|
: "Server toggled off — using Local Offline Storage mode",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setSavingServerCfg(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApplyServerPort = async () => {
|
||||||
|
const parsed = parseInt(portDraft.trim(), 10);
|
||||||
|
if (Number.isNaN(parsed) || parsed < 1024 || parsed > 65535) {
|
||||||
|
toast.error("Please enter a valid port between 1024 and 65535");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
set({ serverPort: parsed });
|
||||||
|
setSavingServerCfg(true);
|
||||||
|
try {
|
||||||
|
const res = await api.updateServerConfig({ port: parsed }, s.serverPort);
|
||||||
|
setServerOnline(res.online);
|
||||||
|
setActiveRunningPort(res.activePort ?? parsed);
|
||||||
|
toast.success(`Server port saved (${parsed})`);
|
||||||
|
} finally {
|
||||||
|
setSavingServerCfg(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleStartOnBoot = async (startOnBoot: boolean) => {
|
||||||
|
set({ serverStartOnBoot: startOnBoot });
|
||||||
|
setSavingServerCfg(true);
|
||||||
|
try {
|
||||||
|
const res = await api.updateServerConfig({ startOnBoot }, s.serverPort);
|
||||||
|
setServerOnline(res.online);
|
||||||
|
toast.success(
|
||||||
|
startOnBoot
|
||||||
|
? "Enabled Start on System Start (systemd user service & desktop autostart)"
|
||||||
|
: "Disabled Start on System Start",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setSavingServerCfg(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const currentProvider =
|
const currentProvider =
|
||||||
s.aiProviders.find((p) => p.id === selectedProviderId) ?? s.aiProviders[0]!;
|
s.aiProviders.find((p) => p.id === selectedProviderId) ?? s.aiProviders[0]!;
|
||||||
const currentAgent =
|
const currentAgent =
|
||||||
@@ -429,6 +506,91 @@ export function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* SERVER & SYSTEM STARTUP */}
|
||||||
|
<div className="ps-settings-group">
|
||||||
|
<h3>Server & System Startup</h3>
|
||||||
|
|
||||||
|
<div className="ps-settings-row">
|
||||||
|
<div>
|
||||||
|
<div className="ps-settings-label" style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
|
<span>PubScript Server</span>
|
||||||
|
<Badge
|
||||||
|
variant={!s.serverEnabled ? "neutral" : serverOnline ? "success" : "warning"}
|
||||||
|
>
|
||||||
|
{!s.serverEnabled
|
||||||
|
? "Off (Local Mode)"
|
||||||
|
: serverOnline
|
||||||
|
? `Online · Port ${activeRunningPort ?? s.serverPort}`
|
||||||
|
: `Standby · Port ${s.serverPort}`}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="ps-settings-desc">
|
||||||
|
Toggle the PubScript API & SQLite server on or off. When toggled off, PubScript runs in local offline storage mode.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label style={{ display: "flex", alignItems: "center", gap: 8, cursor: "pointer" }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={s.serverEnabled}
|
||||||
|
disabled={savingServerCfg}
|
||||||
|
onChange={(e) => void handleToggleServerEnabled(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span style={{ fontSize: 13, fontWeight: 600 }}>
|
||||||
|
{s.serverEnabled ? "Server On" : "Server Off"}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ps-settings-row">
|
||||||
|
<div>
|
||||||
|
<div className="ps-settings-label">Server port</div>
|
||||||
|
<div className="ps-settings-desc">
|
||||||
|
TCP port for the local/self-hosted PubScript server (1024–65535). Default: 8081 (web) / 18081 (desktop).
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="ps-input"
|
||||||
|
min={1024}
|
||||||
|
max={65535}
|
||||||
|
style={{ width: 110 }}
|
||||||
|
value={portDraft}
|
||||||
|
onChange={(e) => setPortDraft(e.target.value)}
|
||||||
|
aria-label="Server port"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
disabled={savingServerCfg}
|
||||||
|
onClick={() => void handleApplyServerPort()}
|
||||||
|
>
|
||||||
|
Apply Port
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ps-settings-row">
|
||||||
|
<div>
|
||||||
|
<div className="ps-settings-label">Start on system start</div>
|
||||||
|
<div className="ps-settings-desc">
|
||||||
|
Automatically launch the PubScript server when you log in or boot the system (configures systemd user service & XDG autostart).
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label style={{ display: "flex", alignItems: "center", gap: 8, cursor: "pointer" }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={s.serverStartOnBoot}
|
||||||
|
disabled={savingServerCfg}
|
||||||
|
onChange={(e) => void handleToggleStartOnBoot(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span style={{ fontSize: 13 }}>
|
||||||
|
{s.serverStartOnBoot ? "Enabled" : "Disabled"}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* WRITING & PROOFING */}
|
{/* WRITING & PROOFING */}
|
||||||
<div className="ps-settings-group">
|
<div className="ps-settings-group">
|
||||||
<h3>Writing & Proofing</h3>
|
<h3>Writing & Proofing</h3>
|
||||||
|
|||||||
@@ -263,6 +263,10 @@ export interface AppSettings {
|
|||||||
activeModel: string;
|
activeModel: string;
|
||||||
aiAgents: AIAgentConfig[];
|
aiAgents: AIAgentConfig[];
|
||||||
activeAgentId: string;
|
activeAgentId: string;
|
||||||
|
// Local / Self-Hosted Server & Startup Settings
|
||||||
|
serverEnabled: boolean;
|
||||||
|
serverPort: number;
|
||||||
|
serverStartOnBoot: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: AppSettings = {
|
export const DEFAULT_SETTINGS: AppSettings = {
|
||||||
@@ -292,6 +296,10 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
|||||||
activeModel: DEFAULT_AI_PROVIDERS[0]!.defaultModel,
|
activeModel: DEFAULT_AI_PROVIDERS[0]!.defaultModel,
|
||||||
aiAgents: DEFAULT_AI_AGENTS,
|
aiAgents: DEFAULT_AI_AGENTS,
|
||||||
activeAgentId: DEFAULT_AI_AGENTS[0]!.id,
|
activeAgentId: DEFAULT_AI_AGENTS[0]!.id,
|
||||||
|
// Local / Self-Hosted Server & Startup Settings
|
||||||
|
serverEnabled: true,
|
||||||
|
serverPort: 8081,
|
||||||
|
serverStartOnBoot: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const STORAGE_KEY = "pubscript.settings.v1";
|
const STORAGE_KEY = "pubscript.settings.v1";
|
||||||
|
|||||||
@@ -157,6 +157,25 @@ int main(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int port = 18081;
|
int port = 18081;
|
||||||
|
char cfg_path[2048];
|
||||||
|
snprintf(cfg_path, sizeof(cfg_path), "%s/.config/pubscript/server.json", home);
|
||||||
|
FILE *cfg_fp = fopen(cfg_path, "r");
|
||||||
|
if (cfg_fp) {
|
||||||
|
char buf[1024];
|
||||||
|
size_t n = fread(buf, 1, sizeof(buf) - 1, cfg_fp);
|
||||||
|
buf[n] = '\0';
|
||||||
|
fclose(cfg_fp);
|
||||||
|
const char *port_key = strstr(buf, "\"port\"");
|
||||||
|
if (port_key) {
|
||||||
|
const char *colon = strchr(port_key, ':');
|
||||||
|
if (colon) {
|
||||||
|
int cfg_port = atoi(colon + 1);
|
||||||
|
if (cfg_port >= 1024 && cfg_port <= 65535) {
|
||||||
|
port = cfg_port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const char *env_port = getenv("PUBSCRIPT_DESKTOP_PORT");
|
const char *env_port = getenv("PUBSCRIPT_DESKTOP_PORT");
|
||||||
if (env_port && *env_port) {
|
if (env_port && *env_port) {
|
||||||
int p = atoi(env_port);
|
int p = atoi(env_port);
|
||||||
|
|||||||
+167
-3
@@ -1,5 +1,7 @@
|
|||||||
import { existsSync } from "node:fs";
|
import { execFileSync } from "node:child_process";
|
||||||
import { dirname, resolve } from "node:path";
|
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 { fileURLToPath } from "node:url";
|
||||||
import Fastify from "fastify";
|
import Fastify from "fastify";
|
||||||
import cors from "@fastify/cors";
|
import cors from "@fastify/cors";
|
||||||
@@ -62,7 +64,169 @@ export async function buildApp() {
|
|||||||
} catch {
|
} catch {
|
||||||
dbUp = false;
|
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);
|
registerAuthHook(app);
|
||||||
|
|||||||
Reference in New Issue
Block a user