Compare commits

...

4 Commits

Author SHA1 Message Date
Timothy Jaeryang Baek eebd30e29e enh: logging 2025-07-29 12:47:33 +04:00
Timothy Jaeryang Baek 8b62c3c993 refac 2025-07-29 12:46:36 +04:00
Timothy Jaeryang Baek c6189bb7cc enh: logging 2025-07-29 12:46:01 +04:00
Timothy Jaeryang Baek ce3766d3c2 feat: factory reset 2025-07-29 12:44:50 +04:00
6 changed files with 173 additions and 74 deletions
+15 -9
View File
@@ -16,6 +16,7 @@ import {
} from "electron";
import path, { join } from "path";
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
import log from "electron-log";
import icon from "../../resources/icon.png?asset";
import trayIconImage from "../../resources/assets/tray.png?asset";
@@ -30,6 +31,7 @@ import {
isPythonInstalled,
isUvInstalled,
openUrl,
resetApp,
setConfig,
startServer,
stopAllServers,
@@ -267,7 +269,7 @@ const startServerHandler = async () => {
updateTrayMenu("Open WebUI: Starting...", null);
console.log("Server started successfully:", SERVER_URL, SERVER_PID);
log.info("Server started successfully:", SERVER_URL, SERVER_PID);
SERVER_STATUS = "started";
mainWindow?.webContents.send("main:data", {
@@ -374,7 +376,7 @@ if (!gotTheLock) {
// Some APIs can only be used after this event occurs.
app.whenReady().then(async () => {
CONFIG = await getConfig(); // Load initial config
console.log("Initial Config:", CONFIG);
log.info("Initial Config:", CONFIG);
// Set app user model id for windows
electronApp.setAppUserModelId("com.openwebui.desktop");
@@ -386,14 +388,14 @@ if (!gotTheLock) {
});
// IPC test
ipcMain.on("ping", () => console.log("pong"));
ipcMain.on("ping", () => log.info("pong"));
ipcMain.handle("get:version", async () => {
return app.getVersion();
});
ipcMain.handle("install:python", async (event) => {
console.log("Installing package...");
log.info("Installing package...");
try {
const res = await installPython();
if (res) {
@@ -425,7 +427,7 @@ if (!gotTheLock) {
});
ipcMain.handle("install:package", async (event) => {
console.log("Installing package...");
log.info("Installing package...");
try {
const res = await installPackage("open-webui");
if (res) {
@@ -449,7 +451,7 @@ if (!gotTheLock) {
ipcMain.handle("status:package", async (event) => {
const packageStatus = await isPackageInstalled("open-webui");
console.log("Package Status:", packageStatus);
log.info("Package Status:", packageStatus);
return packageStatus;
});
@@ -482,6 +484,10 @@ if (!gotTheLock) {
return SERVER_STATUS;
});
ipcMain.handle("app:reset", async (event) => {
return await resetApp();
});
ipcMain.handle("get:config", async (event) => {
return await getConfig();
});
@@ -494,7 +500,7 @@ if (!gotTheLock) {
if (!url) {
throw new Error("No URL provided to open in browser.");
}
console.log("Opening URL in browser:", url);
log.info("Opening URL in browser:", url);
if (url.startsWith("http://0.0.0.0")) {
url = url.replace("http://0.0.0.0", "http://localhost");
}
@@ -503,7 +509,7 @@ if (!gotTheLock) {
});
ipcMain.handle("notification", async (event, { title, body }) => {
console.log("Received notification:", title, body);
log.info("Received notification:", title, body);
const notification = new Notification({
title: title,
body: body,
@@ -515,7 +521,7 @@ if (!gotTheLock) {
if (isPackageInstalled("open-webui")) {
if (CONFIG?.autoUpdate ?? true) {
try {
console.log("Checking for updates...");
log.info("Checking for updates...");
updateTrayMenu(
"Open WebUI: Checking for updates...",
null
+36 -31
View File
@@ -59,7 +59,7 @@ export const openUrl = (url: string) => {
throw new Error("No URL provided to open in browser.");
}
console.log("Opening URL in browser:", url);
log.info("Opening URL in browser:", url);
if (url.startsWith("http://0.0.0.0")) {
url = url.replace("http://0.0.0.0", "http://localhost");
}
@@ -215,7 +215,7 @@ export const downloadFileWithProgress = async (
// Write to file
fs.writeFileSync(downloadPath, buffer);
console.log("File downloaded successfully:", downloadPath);
log.info("File downloaded successfully:", downloadPath);
return downloadPath;
}
} catch (error) {
@@ -257,13 +257,13 @@ const downloadPython = async (onProgress = null) => {
const url = generateDownloadUrl();
const downloadPath = getPythonDownloadPath();
console.log(`🔍 Detected system: ${os.platform()} ${os.arch()}`);
console.log(`📁 Download path: ${downloadPath}`);
console.log(`🔗 URL: ${url}`);
log.info(`🔍 Detected system: ${os.platform()} ${os.arch()}`);
log.info(`📁 Download path: ${downloadPath}`);
log.info(`🔗 URL: ${url}`);
// Check if file already exists
if (fs.existsSync(downloadPath)) {
console.log(`✅ File already exists: ${downloadPath}`);
log.info(`✅ File already exists: ${downloadPath}`);
return downloadPath;
}
@@ -273,7 +273,7 @@ const downloadPython = async (onProgress = null) => {
downloadPath,
onProgress
);
console.log(`✅ Python downloaded successfully to: ${result}`);
log.info(`✅ Python downloaded successfully to: ${result}`);
return result;
} catch (error) {
console.error(`❌ Download failed: ${error?.message}`);
@@ -311,7 +311,7 @@ export const installPython = async (
}
await downloadPython((progress, downloaded, total) => {
console.log(
log.info(
`Downloading Python: ${progress.toFixed(2)}% (${downloaded} of ${total} bytes)`
);
log.info(
@@ -325,7 +325,7 @@ export const installPython = async (
}
installationPath = installationPath || getPythonInstallationPath();
console.log(installationPath, pythonDownloadPath);
log.info(installationPath, pythonDownloadPath);
try {
const userDataPath = getUserDataPath();
@@ -348,7 +348,7 @@ export const installPython = async (
...process.env,
},
});
console.log("Successfully installed uv package");
log.info("Successfully installed uv package");
return true; // Return true to indicate success
} else {
@@ -389,7 +389,7 @@ export const isPythonInstalled = (installationPath?: string) => {
...process.env,
},
});
console.log("Installed Python Version:", pythonVersion.trim());
log.info("Installed Python Version:", pythonVersion.trim());
return true; // Return true to indicate success
} catch (error) {
@@ -409,7 +409,7 @@ export const isUvInstalled = (installationPath?: string) => {
},
});
console.log("Installed uv Version:", result.trim());
log.info("Installed uv Version:", result.trim());
return true; // Return true if uv is installed
} catch (error) {
log.error(
@@ -446,6 +446,11 @@ export const uninstallPython = (installationPath?: string): boolean => {
return true;
};
export const resetApp = async (): Promise<void> => {
await uninstallPython();
log.info("Uninstalled Python environment");
};
////////////////////////////////////////////////
//
// Fixes code-signing issues in macOS by applying ad-hoc signatures to extracted environment files.
@@ -519,7 +524,7 @@ export const installPackage = (
// Function to handle logging output
const onLog = (data: any) => {
console.log(data);
log.info(data);
};
// Listen to stdout and stderr for logging
@@ -528,7 +533,7 @@ export const installPackage = (
// Handle the exit event
commandProcess.on("exit", (code) => {
console.log(`Child exited with code ${code}`);
log.info(`Child exited with code ${code}`);
if (code !== 0) {
log.error(`Failed to install open-webui: ${code}`);
@@ -568,14 +573,14 @@ export const isPackageInstalled = (packageName: string): boolean => {
);
if (info.includes(`Name: ${packageName}`)) {
console.log(`Package ${packageName} is installed.`);
log.info(`Package ${packageName} is installed.`);
return true; // Return true to indicate success
} else {
console.log(`Package ${packageName} is not installed.`);
log.info(`Package ${packageName} is not installed.`);
return false; // Return false to indicate failure
}
} catch (error) {
console.log("Failed to execute Python binary");
log.info("Failed to execute Python binary");
return false; // Return false to indicate failure
}
};
@@ -603,7 +608,7 @@ export const startServer = async (
throw new Error("open-webui package is not installed");
const pythonPath = getPythonPath();
console.log(`Using Python at: ${pythonPath}`);
log.info(`Using Python at: ${pythonPath}`);
const openWebUIPath = path.join(path.dirname(pythonPath), "open-webui");
@@ -622,17 +627,17 @@ export const startServer = async (
let desiredPort = port || 8080;
let availablePort = desiredPort;
console.log(`Checking port availability starting from ${availablePort}...`);
log.info(`Checking port availability starting from ${availablePort}...`);
while (await portInUse(availablePort, host)) {
availablePort++;
console.log(`Port ${availablePort} is in use, trying next port...`);
log.info(`Port ${availablePort} is in use, trying next port...`);
if (availablePort > desiredPort + 100) {
throw new Error("No available ports found");
}
}
commandArgs.push("--port", availablePort.toString());
console.log(
log.info(
"Starting Open-WebUI server...",
pythonPath,
commandArgs.join(" ")
@@ -656,7 +661,7 @@ export const startServer = async (
const tag = `[${source}][PID:${childProcess.pid}]:`;
logLines.push(`${tag} ${logLine}`);
// (Optional) also log to main process console
// console.log(`${tag} ${logLine}`);
// log.info(`${tag} ${logLine}`);
};
childProcess.stdout?.on("data", appendLog("stdout"));
childProcess.stderr?.on("data", appendLog("stderr"));
@@ -677,7 +682,7 @@ export const startServer = async (
let effectiveHost = host;
if (!expose && host === "0.0.0.0") effectiveHost = "127.0.0.1";
const url = `http://${effectiveHost}:${availablePort}`;
console.log(`Server started with PID: ${childProcess.pid}, URL: ${url}`);
log.info(`Server started with PID: ${childProcess.pid}, URL: ${url}`);
return { url, pid: childProcess.pid };
};
@@ -686,7 +691,7 @@ export const startServer = async (
* Terminates all server processes.
*/
export async function stopAllServers(): Promise<void> {
console.log("Stopping all servers...");
log.info("Stopping all servers...");
for (const pid of Array.from(serverPIDs)) {
try {
terminateProcessTree(pid);
@@ -696,7 +701,7 @@ export async function stopAllServers(): Promise<void> {
console.error(`Error stopping server with PID ${pid}:`, error);
}
}
console.log("All servers stopped successfully.");
log.info("All servers stopped successfully.");
}
/**
* Kills a process tree by PID.
@@ -705,7 +710,7 @@ function terminateProcessTree(pid: number): void {
if (process.platform === "win32") {
try {
execSync(`taskkill /PID ${pid} /T /F`);
console.log(
log.info(
`Terminated server process tree (PID: ${pid}) on Windows.`
);
} catch (error) {
@@ -717,7 +722,7 @@ function terminateProcessTree(pid: number): void {
} else {
try {
process.kill(-pid, "SIGKILL");
console.log(
log.info(
`Terminated server process tree (PID: ${pid}) on Unix-like OS.`
);
} catch (error) {
@@ -760,20 +765,20 @@ export const checkUrlAndOpen = async (
const pollUrl = async () => {
while (attempts < maxAttempts) {
attempts++;
console.log(
log.info(
`Checking URL availability (attempt ${attempts}/${maxAttempts}): ${url}`
);
try {
const isAvailable = await checkUrl();
if (isAvailable) {
console.log("URL is now available, opening browser...");
log.info("URL is now available, opening browser...");
await openUrl(url);
await callback(); // Call the provided callback function
return; // Exit the polling loop
}
} catch (error) {
console.log(`Error checking URL: ${error}`);
log.info(`Error checking URL: ${error}`);
}
// Wait before next attempt
@@ -782,7 +787,7 @@ export const checkUrlAndOpen = async (
// If we exit the loop without success
if (attempts >= maxAttempts) {
console.log("URL check timed out after 5 minutes");
log.info("URL check timed out after 5 minutes");
}
};
+10
View File
@@ -132,6 +132,16 @@ const api = {
return await ipcRenderer.invoke("server:info");
},
resetApp: async () => {
if (!isLocalSource()) {
throw new Error(
"Access restricted: This operation is only allowed in a local environment."
);
}
return await ipcRenderer.invoke("app:reset");
},
startServer: async () => {
if (!isLocalSource()) {
throw new Error(
+74
View File
@@ -1,5 +1,79 @@
@import "tailwindcss";
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}
@layer base {
html,
pre {
font-family:
-apple-system, BlinkMacSystemFont, "Inter", "Vazirmatn",
ui-sans-serif, system-ui, "Segoe UI", Roboto, Ubuntu, Cantarell,
"Noto Sans", sans-serif, "Helvetica Neue", Arial,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
"Noto Color Emoji";
}
pre {
white-space: pre-wrap;
}
button {
@apply cursor-pointer;
}
input::placeholder,
textarea::placeholder {
color: theme(--color-gray-400);
}
input[type="checkbox"] {
@apply appearance-none size-3.5 align-middle bg-white border border-gray-300 rounded transition cursor-pointer focus:ring-2 focus:ring-blue-500 focus:outline-none dark:bg-gray-800 dark:border-gray-600 self-center;
/* Center the custom mark */
display: inline-block;
position: relative;
}
input[type="checkbox"]:checked {
@apply bg-blue-600 border-blue-600;
}
input[type="checkbox"]:after {
content: "";
display: block;
width: 100%;
height: 100%;
/* Hide by default */
opacity: 0;
transition: opacity 0.2s;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
/* SVG checkmark as background image */
background: url('data:image/svg+xml;utf8,<svg viewBox="0 0 16 16" fill="none" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M4 8l3 3l5-5"/></svg>')
center/80% no-repeat;
}
input[type="checkbox"]:checked:after {
opacity: 1;
}
}
@custom-variant hover (&:hover);
@font-face {
font-family: "Archivo";
src: url("./lib/assets/fonts/Archivo-Variable.ttf");
@@ -48,16 +48,18 @@
</script>
<ConfirmDialog
bind:showConfirm
bind:show={showConfirm}
title="Factory Reset"
message="Are you sure you want to reset the app? This will remove all configurations, user data, and the bundled Python environment, restoring the app to its original state."
confirmLabel="Reset"
onConfirm={async () => {
try {
config = null;
await window.electronAPI.resetApp();
toast.success(
"App has been reset successfully. Please restart the app."
);
toast.success("App has been reset successfully.");
// refresh the page to apply changes
window.location.reload();
} catch (error) {
toast.error("Failed to reset the app");
}
@@ -8,23 +8,43 @@
import { flyAndScale } from "../../utils/transitions";
import { marked } from "marked";
export let title = "";
export let message = "";
let {
title,
message,
cancelLabel = "Cancel",
confirmLabel = "Confirm",
onConfirm = () => {},
input,
inputPlaceholder,
inputValue,
show = $bindable(false),
} = $props();
export let cancelLabel = "Cancel";
export let confirmLabel = "Confirm";
$effect(() => {
if (show) {
init();
}
});
export let onConfirm = () => {};
$effect(() => {
if (mounted) {
if (show && modalElement) {
document.body.appendChild(modalElement);
focusTrap = FocusTrap.createFocusTrap(modalElement);
focusTrap.activate();
export let input = false;
export let inputPlaceholder = "";
export let inputValue = "";
window.addEventListener("keydown", handleKeyDown);
document.body.style.overflow = "hidden";
} else if (modalElement) {
focusTrap.deactivate();
export let show = false;
window.removeEventListener("keydown", handleKeyDown);
document.body.removeChild(modalElement);
$: if (show) {
init();
}
document.body.style.overflow = "unset";
}
}
});
let modalElement = null;
let mounted = false;
@@ -57,24 +77,6 @@
mounted = true;
});
$: if (mounted) {
if (show && modalElement) {
document.body.appendChild(modalElement);
focusTrap = FocusTrap.createFocusTrap(modalElement);
focusTrap.activate();
window.addEventListener("keydown", handleKeyDown);
document.body.style.overflow = "hidden";
} else if (modalElement) {
focusTrap.deactivate();
window.removeEventListener("keydown", handleKeyDown);
document.body.removeChild(modalElement);
document.body.style.overflow = "unset";
}
}
onDestroy(() => {
show = false;
if (focusTrap) {