From 29dbc27c416bfec2651fd5ec6e8089e06ec87048 Mon Sep 17 00:00:00 2001 From: Schwitzz Date: Fri, 30 Oct 2026 14:23:12 -0500 Subject: [PATCH] feat: add Web App Port management alongside API Server Port and fix square 512x512 app launcher icon --- apps/web/public/logo.svg | 18 +++---- apps/web/src/api.ts | 15 ++++++ apps/web/src/components/Settings.tsx | 76 +++++++++++++++++++++++++--- apps/web/src/store/settings.ts | 2 + packages/ui/src/AppShell.css | 8 +-- server/src/index.ts | 60 ++++++++++++++++++++++ 6 files changed, 161 insertions(+), 18 deletions(-) diff --git a/apps/web/public/logo.svg b/apps/web/public/logo.svg index a55b716..d43f6d1 100644 --- a/apps/web/public/logo.svg +++ b/apps/web/public/logo.svg @@ -1,4 +1,4 @@ - + @@ -19,18 +19,18 @@ - - - - + + + P - - - - + + + S diff --git a/apps/web/src/api.ts b/apps/web/src/api.ts index 798354a..28e8e6d 100644 --- a/apps/web/src/api.ts +++ b/apps/web/src/api.ts @@ -372,6 +372,8 @@ export const api = { enabled: boolean; port: number; activePort?: number; + webPort?: number; + activeWebPort?: number; startOnBoot: boolean; projectDirectory?: string; }> => { @@ -388,6 +390,8 @@ export const api = { enabled: boolean; port: number; activePort?: number; + webPort?: number; + activeWebPort?: number; startOnBoot: boolean; projectDirectory?: string; }; @@ -396,6 +400,8 @@ export const api = { enabled: data.enabled, port: data.port, ...(data.activePort !== undefined ? { activePort: data.activePort } : {}), + ...(data.webPort !== undefined ? { webPort: data.webPort } : {}), + ...(data.activeWebPort !== undefined ? { activeWebPort: data.activeWebPort } : {}), startOnBoot: data.startOnBoot, ...(data.projectDirectory ? { projectDirectory: data.projectDirectory } : {}), }; @@ -408,6 +414,7 @@ export const api = { online: false, enabled: true, port: portHint ?? 8081, + webPort: 5173, startOnBoot: false, }; }, @@ -416,6 +423,7 @@ export const api = { patch: { enabled?: boolean; port?: number; + webPort?: number; startOnBoot?: boolean; projectDirectory?: string; }, @@ -425,6 +433,8 @@ export const api = { enabled: boolean; port: number; activePort?: number; + webPort?: number; + activeWebPort?: number; startOnBoot: boolean; projectDirectory?: string; }> => { @@ -446,6 +456,8 @@ export const api = { enabled: boolean; port: number; activePort?: number; + webPort?: number; + activeWebPort?: number; startOnBoot: boolean; projectDirectory?: string; }; @@ -454,6 +466,8 @@ export const api = { enabled: data.enabled, port: data.port, ...(data.activePort !== undefined ? { activePort: data.activePort } : {}), + ...(data.webPort !== undefined ? { webPort: data.webPort } : {}), + ...(data.activeWebPort !== undefined ? { activeWebPort: data.activeWebPort } : {}), startOnBoot: data.startOnBoot, ...(data.projectDirectory ? { projectDirectory: data.projectDirectory } : {}), }; @@ -466,6 +480,7 @@ export const api = { online: false, enabled: patch.enabled ?? true, port: patch.port ?? portHint ?? 8081, + ...(patch.webPort !== undefined ? { webPort: patch.webPort } : {}), startOnBoot: patch.startOnBoot ?? false, ...(patch.projectDirectory ? { projectDirectory: patch.projectDirectory } : {}), }; diff --git a/apps/web/src/components/Settings.tsx b/apps/web/src/components/Settings.tsx index c430d9d..36d68b7 100644 --- a/apps/web/src/components/Settings.tsx +++ b/apps/web/src/components/Settings.tsx @@ -67,6 +67,7 @@ export function Settings() { const [probing, setProbing] = useState(false); // Server & Project Directory state + const [webPortDraft, setWebPortDraft] = useState(String(s.webPort || 5173)); const [portDraft, setPortDraft] = useState(String(s.serverPort)); const [projectDirDraft, setProjectDirDraft] = useState( s.projectDirectory || "~/Documents/PubScript", @@ -74,6 +75,7 @@ export function Settings() { const [browseProjectDirOpen, setBrowseProjectDirOpen] = useState(false); const [serverOnline, setServerOnline] = useState(false); const [activeRunningPort, setActiveRunningPort] = useState(null); + const [activeRunningWebPort, setActiveRunningWebPort] = useState(null); const [savingServerCfg, setSavingServerCfg] = useState(false); useEffect(() => { @@ -82,14 +84,19 @@ export function Settings() { if (cancelled) return; setServerOnline(res.online); setActiveRunningPort(res.activePort ?? res.port); + setActiveRunningWebPort(res.activeWebPort ?? res.webPort ?? s.webPort); if (res.online) { setPortDraft(String(res.port)); + if (res.webPort) { + setWebPortDraft(String(res.webPort)); + } if (res.projectDirectory) { setProjectDirDraft(res.projectDirectory); } set({ serverEnabled: res.enabled, serverPort: res.port, + ...(res.webPort ? { webPort: res.webPort } : {}), serverStartOnBoot: res.startOnBoot, ...(res.projectDirectory ? { projectDirectory: res.projectDirectory } : {}), }); @@ -139,10 +146,28 @@ export function Settings() { } }; + const handleApplyWebPort = async () => { + const parsed = parseInt(webPortDraft.trim(), 10); + if (Number.isNaN(parsed) || parsed < 1024 || parsed > 65535) { + toast.error("Please enter a valid web app port between 1024 and 65535"); + return; + } + set({ webPort: parsed }); + setSavingServerCfg(true); + try { + const res = await api.updateServerConfig({ webPort: parsed }, s.serverPort); + setServerOnline(res.online); + setActiveRunningWebPort(res.activeWebPort ?? parsed); + toast.success(`Web App port saved and active on http://localhost:${parsed}`); + } 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"); + toast.error("Please enter a valid API server port between 1024 and 65535"); return; } set({ serverPort: parsed }); @@ -151,7 +176,7 @@ export function Settings() { const res = await api.updateServerConfig({ port: parsed }, s.serverPort); setServerOnline(res.online); setActiveRunningPort(res.activePort ?? parsed); - toast.success(`Server port saved (${parsed})`); + toast.success(`API Server port saved (${parsed})`); } finally { setSavingServerCfg(false); } @@ -1004,9 +1029,48 @@ export function Settings() {
-
Server port
+
+ Web App Port + + {`http://localhost:${activeRunningWebPort ?? s.webPort}`} + +
- TCP port for the local/self-hosted PubScript server (1024–65535). Default: 8081 (web) / 18081 (desktop). + TCP port where the PubScript Web App UI is served (1024–65535). Default: 5173. Changing this immediately binds the web UI on the selected port. +
+
+
+ setWebPortDraft(e.target.value)} + aria-label="Web app port" + /> + +
+
+ +
+
+
+ API Server Port + + {`Port ${activeRunningPort ?? s.serverPort}`} + +
+
+ TCP port for the PubScript API & SQLite server (1024–65535). Default: 8081 (web) / 18081 (desktop).
@@ -1018,7 +1082,7 @@ export function Settings() { style={{ width: 110 }} value={portDraft} onChange={(e) => setPortDraft(e.target.value)} - aria-label="Server port" + aria-label="API server port" />
diff --git a/apps/web/src/store/settings.ts b/apps/web/src/store/settings.ts index bebb830..aca7b96 100644 --- a/apps/web/src/store/settings.ts +++ b/apps/web/src/store/settings.ts @@ -274,6 +274,7 @@ export interface AppSettings { activeAgentId: string; // Local / Self-Hosted Server & Startup Settings serverEnabled: boolean; + webPort: number; serverPort: number; serverStartOnBoot: boolean; } @@ -309,6 +310,7 @@ export const DEFAULT_SETTINGS: AppSettings = { activeAgentId: DEFAULT_AI_AGENTS[0]!.id, // Local / Self-Hosted Server & Startup Settings serverEnabled: true, + webPort: 5173, serverPort: 8081, serverStartOnBoot: false, }; diff --git a/packages/ui/src/AppShell.css b/packages/ui/src/AppShell.css index 5d1360c..8ccc6f1 100644 --- a/packages/ui/src/AppShell.css +++ b/packages/ui/src/AppShell.css @@ -144,8 +144,10 @@ body { } .ps-sidebar-logo-only { - width: 88px; - height: auto; + width: 92px; + height: 52px; + object-fit: cover; + object-position: center; background: transparent; display: block; flex-shrink: 0; @@ -153,7 +155,7 @@ body { } .ps-sidebar.is-collapsed .ps-sidebar-logo-only { width: 48px; - height: auto; + height: 28px; } .ps-sidebar-toggle { diff --git a/server/src/index.ts b/server/src/index.ts index 5e71e59..f90e4fe 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -8,6 +8,7 @@ import { statSync, writeFileSync, } from "node:fs"; +import { createServer, type Server as HttpServer } from "node:http"; import { homedir } from "node:os"; import { basename, dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -101,10 +102,14 @@ export async function buildApp() { interface PersistedServerSysConfig { enabled: boolean; port: number; + webPort: number; startOnBoot: boolean; projectDirectory: string; } + let secondaryWebServer: HttpServer | null = null; + let activeWebPort: number = 5173; + function loadPersistedServerSysConfig(): PersistedServerSysConfig { try { if (existsSync(sysConfigPath)) { @@ -115,6 +120,10 @@ export async function buildApp() { typeof raw.port === "number" && raw.port >= 1024 && raw.port <= 65535 ? raw.port : config.port, + webPort: + typeof raw.webPort === "number" && raw.webPort >= 1024 && raw.webPort <= 65535 + ? raw.webPort + : 5173, startOnBoot: typeof raw.startOnBoot === "boolean" ? raw.startOnBoot @@ -131,11 +140,36 @@ export async function buildApp() { return { enabled: true, port: config.port, + webPort: 5173, startOnBoot: existsSync(autostartDesktopPath) || existsSync(systemdServicePath), projectDirectory: defaultProjectDir, }; } + async function bindSecondaryWebPort(targetWebPort: number, enabled: boolean): Promise { + activeWebPort = targetWebPort; + if (secondaryWebServer) { + await new Promise((res) => secondaryWebServer!.close(() => res())); + secondaryWebServer = null; + } + if (!enabled || targetWebPort === config.port) { + return targetWebPort; + } + return await new Promise((res) => { + const srv = createServer((req, reply) => { + app.server.emit("request", req, reply); + }); + srv.once("error", () => { + // Port may already be served by Vite dev server (e.g. :5173); keep activeWebPort + res(targetWebPort); + }); + srv.listen(targetWebPort, "0.0.0.0", () => { + secondaryWebServer = srv; + res(targetWebPort); + }); + }); + } + function applySystemStartupFiles(cfg: PersistedServerSysConfig): boolean { try { mkdirSync(sysConfigDir, { recursive: true }); @@ -225,6 +259,8 @@ export async function buildApp() { enabled: persisted.enabled, port: persisted.port, activePort: config.port, + webPort: persisted.webPort, + activeWebPort: activeWebPort || persisted.webPort, startOnBoot: persisted.startOnBoot, projectDirectory: persisted.projectDirectory, autostartPath: autostartDesktopPath, @@ -240,6 +276,11 @@ export async function buildApp() { if (!Number.isFinite(nextPort) || nextPort < 1024 || nextPort > 65535) { return reply.code(400).send({ error: "invalid_port" }); } + const nextWebPort = + typeof body.webPort === "number" ? Math.trunc(body.webPort) : current.webPort; + if (!Number.isFinite(nextWebPort) || nextWebPort < 1024 || nextWebPort > 65535) { + return reply.code(400).send({ error: "invalid_web_port" }); + } const nextProjectDir = typeof body.projectDirectory === "string" && body.projectDirectory.trim() ? body.projectDirectory.trim() @@ -247,16 +288,20 @@ export async function buildApp() { const next: PersistedServerSysConfig = { enabled: typeof body.enabled === "boolean" ? body.enabled : current.enabled, port: nextPort, + webPort: nextWebPort, startOnBoot: typeof body.startOnBoot === "boolean" ? body.startOnBoot : current.startOnBoot, projectDirectory: nextProjectDir, }; const configured = applySystemStartupFiles(next); + const boundWebPort = await bindSecondaryWebPort(next.webPort, next.enabled); return { ok: true, enabled: next.enabled, port: next.port, activePort: config.port, + webPort: next.webPort, + activeWebPort: boundWebPort, startOnBoot: next.startOnBoot, projectDirectory: next.projectDirectory, autostartConfigured: configured, @@ -355,6 +400,21 @@ export async function buildApp() { }); } + app.addHook("onClose", async () => { + if (secondaryWebServer) { + await new Promise((res) => secondaryWebServer!.close(() => res())); + secondaryWebServer = null; + } + }); + + if ( + process.argv[1] !== undefined && + fileURLToPath(import.meta.url) === process.argv[1] + ) { + const persisted = loadPersistedServerSysConfig(); + void bindSecondaryWebPort(persisted.webPort, persisted.enabled); + } + return app; }