Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8d907ae
feat: add System tab to Help component
heavy-d Aug 15, 2025
8390e1b
feat: enhance Help component with scrollable tabs and system path upd…
heavy-d Aug 15, 2025
0a762af
feat: update SystemTab to include conditional logging paths
heavy-d Aug 15, 2025
95e916b
feat: conditionally render System tab in Help component based on envi…
heavy-d Aug 15, 2025
a6f4bcb
refactor: improve layout of SystemTab component in Help section
heavy-d Aug 15, 2025
cc0c2fb
feat: update ThemeNodetool and vars.css for improved theming
heavy-d Aug 15, 2025
dea0f16
refactor: streamline SystemTab component and enhance error handling
heavy-d Aug 16, 2025
c0b6266
chore: update path validation logic in fileExplorer utility
heavy-d Aug 16, 2025
42d5e7e
chore: clean up SystemTab component by removing unnecessary blank lin…
heavy-d Aug 16, 2025
b625045
feat: add SystemDiagnostics component for displaying system information
heavy-d Aug 16, 2025
a8f21fc
feat: implement fetchBasicSystemInfo API and integrate with IPC
heavy-d Aug 16, 2025
b078c44
feat: integrate boot message system info toggle and styling
heavy-d Aug 16, 2025
0332091
feat: enhance system diagnostics UI and functionality
heavy-d Aug 16, 2025
6b82ca5
feat: enhance local system information retrieval and UI updates
heavy-d Aug 16, 2025
b7b2659
feat: add CUDA version retrieval to system diagnostics
heavy-d Aug 16, 2025
4dd5028
feat: extend system diagnostics to include GPU information
heavy-d Aug 16, 2025
2675328
feat: update SystemTab to handle missing API endpoints and improve sy…
heavy-d Aug 19, 2025
eed347c
refactor: clean up formatting and improve readability in SystemTab co…
heavy-d Aug 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
309 changes: 308 additions & 1 deletion electron/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { Workflow } from "./types";
import { Workflow, BasicSystemInfo } from "./types";
import { logMessage } from "./logger";
import { serverState } from "./state";
import { app } from "electron";
import { spawn } from "child_process";
import { getPythonPath } from "./config";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";

export let isConnected = false;
let healthCheckTimer: NodeJS.Timeout | null = null;
Expand Down Expand Up @@ -75,3 +81,304 @@ export function stopPeriodicHealthCheck(): void {
healthCheckTimer = null;
}
}

/**
* Gets Python version locally by executing python --version
* @returns {Promise<string | null>} Python version or null if unavailable
*/
async function getLocalPythonVersion(): Promise<string | null> {
try {
const pythonPath = getPythonPath();

// Check if Python executable exists
if (!fs.existsSync(pythonPath)) {
return null;
}

return new Promise((resolve) => {
const child = spawn(pythonPath, ["--version"], {
timeout: 3000, // 3 second timeout
stdio: ["ignore", "pipe", "pipe"],
});

let output = "";
let errorOutput = "";

child.stdout?.on("data", (data) => {
output += data.toString();
});

child.stderr?.on("data", (data) => {
errorOutput += data.toString();
});

child.on("close", (code) => {
if (code === 0) {
// Python version is usually in format "Python 3.x.x"
const version = (output || errorOutput)
.trim()
.replace(/^Python\s+/, "");
resolve(version || null);
} else {
resolve(null);
}
});

child.on("error", () => {
resolve(null);
});
});
} catch (error) {
logMessage(`Failed to get local Python version: ${error}`, "error");
return null;
}
}

/**
* Gets NodeTool package versions from local package.json files
* @returns {Promise<{core?: string, base?: string}>} Package versions
*/
async function getLocalNodeToolVersions(): Promise<{
core?: string;
base?: string;
}> {
const versions: { core?: string; base?: string } = {};

try {
// Try to find package.json files in common locations
const appPath = app.getAppPath();
const possiblePaths = [
path.join(appPath, "..", "..", "package.json"), // From electron app to root
path.join(appPath, "..", "package.json"),
path.join(process.cwd(), "package.json"),
];

for (const pkgPath of possiblePaths) {
try {
if (fs.existsSync(pkgPath)) {
const pkgContent = JSON.parse(fs.readFileSync(pkgPath, "utf8"));

// Check dependencies for nodetool packages
const allDeps = {
...pkgContent.dependencies,
...pkgContent.devDependencies,
...pkgContent.peerDependencies,
};

if (allDeps["nodetool-core"]) {
versions.core = allDeps["nodetool-core"];
}
if (allDeps["nodetool-base"]) {
versions.base = allDeps["nodetool-base"];
}
}
} catch (e) {
// Continue to next path
}
}
} catch (error) {
logMessage(`Failed to get local NodeTool versions: ${error}`, "error");
}

return versions;
}

/**
* Gets CUDA version locally by checking nvcc or nvidia-smi
* @returns {Promise<string | null>} CUDA version or null if unavailable
*/
async function getLocalCudaVersion(): Promise<string | null> {
try {
// Try nvcc first to get actual CUDA toolkit version
const nvccVersion = await new Promise<string | null>((resolve) => {
const child = spawn("nvcc", ["--version"], {
timeout: 3000,
stdio: ["ignore", "pipe", "pipe"],
});

let output = "";
child.stdout?.on("data", (data) => {
output += data.toString();
});

child.on("close", (code) => {
if (code === 0) {
// Extract CUDA version from nvcc output (e.g., "release 12.1")
const match = output.match(/release (\d+\.\d+)/);
resolve(match ? match[1] : null);
} else {
resolve(null);
}
});

child.on("error", () => {
resolve(null);
});
});

if (nvccVersion) {
return nvccVersion;
}

// Try nvidia-smi to get CUDA runtime version as fallback
const nvidiaSmiCudaVersion = await new Promise<string | null>((resolve) => {
const child = spawn(
"nvidia-smi",
["--query-gpu=cuda_version", "--format=csv,noheader,nounits"],
{
timeout: 3000,
stdio: ["ignore", "pipe", "pipe"],
}
);

let output = "";
child.stdout?.on("data", (data) => {
output += data.toString();
});

child.on("close", (code) => {
if (code === 0 && output.trim()) {
const cudaVersion = output.trim().split("\n")[0];
// nvidia-smi sometimes returns "N/A" for CUDA version
resolve(cudaVersion !== "N/A" ? cudaVersion : null);
} else {
resolve(null);
}
});

child.on("error", () => {
resolve(null);
});
});

return nvidiaSmiCudaVersion;
} catch (error) {
logMessage(`Failed to get local CUDA version: ${error}`, "error");
return null;
}
}

/**
* Fetches basic system information for diagnostics
* @returns {Promise<BasicSystemInfo | null>} Basic system info or null if unavailable
*/
export async function fetchBasicSystemInfo(): Promise<BasicSystemInfo | null> {
logMessage("Fetching basic system information...");

try {
const port = serverState.serverPort ?? 8000;
const baseUrl = `http://127.0.0.1:${port}`;

// Try to fetch system info from backend
const [systemResponse, healthResponse] = await Promise.allSettled([
fetch(`${baseUrl}/api/system/`, {
method: "GET",
headers: { Accept: "application/json" },
signal: AbortSignal.timeout(5000), // 5 second timeout
}),
fetch(`${baseUrl}/api/system/health`, {
method: "GET",
headers: { Accept: "application/json" },
signal: AbortSignal.timeout(5000),
}),
]);

let systemData: any = null;
let serverStatus: "connected" | "disconnected" | "checking" =
"disconnected";

// Process system info response
if (systemResponse.status === "fulfilled" && systemResponse.value.ok) {
systemData = await systemResponse.value.json();
serverStatus = "connected";
logMessage("Successfully fetched system information from backend");
}

// Process health response
if (healthResponse.status === "fulfilled" && healthResponse.value.ok) {
serverStatus = "connected";
}

// Get local fallback information when backend is unavailable
let localPythonVersion: string | null = null;
let localNodeToolVersions: { core?: string; base?: string } = {};
let localCudaVersion: string | null = null;

if (serverStatus === "disconnected") {
logMessage("Backend unavailable, fetching local system information...");
[localPythonVersion, localNodeToolVersions, localCudaVersion] =
await Promise.all([
getLocalPythonVersion(),
getLocalNodeToolVersions(),
getLocalCudaVersion(),
]);
}

// Build system info object with enhanced fallbacks
const systemInfo: BasicSystemInfo = {
os: {
platform: systemData?.os?.platform || process.platform,
release: systemData?.os?.release || os.release(),
arch: systemData?.os?.arch || process.arch,
},
versions: {
python: systemData?.versions?.python || localPythonVersion || undefined,
nodetool_core:
systemData?.versions?.nodetool_core ||
localNodeToolVersions.core ||
undefined,
nodetool_base:
systemData?.versions?.nodetool_base ||
localNodeToolVersions.base ||
undefined,
cuda: systemData?.versions?.cuda || localCudaVersion || undefined,
},
paths: {
data_dir: systemData?.paths?.data_dir || app.getPath("userData"),
core_logs_dir: systemData?.paths?.core_logs_dir || "-",
electron_logs_dir: app.getPath("logs"),
},
server: {
status: serverStatus,
port: serverStatus === "connected" ? port : undefined,
},
};

return systemInfo;
} catch (error) {
logMessage(`Failed to fetch system information: ${error}`, "error");

// Enhanced fallback with local information
const [localPythonVersion, localNodeToolVersions, localCudaVersion] =
await Promise.all([
getLocalPythonVersion().catch(() => null),
getLocalNodeToolVersions().catch(() => ({
core: undefined,
base: undefined,
})),
getLocalCudaVersion().catch(() => null),
]);

return {
os: {
platform: process.platform,
release: os.release(),
arch: process.arch,
},
versions: {
python: localPythonVersion || undefined,
nodetool_core: localNodeToolVersions.core || undefined,
nodetool_base: localNodeToolVersions.base || undefined,
cuda: localCudaVersion || undefined,
},
paths: {
data_dir: app.getPath("userData"),
core_logs_dir: "-",
electron_logs_dir: app.getPath("logs"),
},
server: {
status: "disconnected",
},
};
}
}
32 changes: 27 additions & 5 deletions electron/src/components/BootMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import React, { useState } from "react";
import SystemDiagnostics from "./SystemDiagnostics";

interface UpdateProgressData {
componentName: string;
Expand All @@ -13,11 +14,26 @@ interface BootMessageProps {
progressData: UpdateProgressData;
}

const BootMessage: React.FC<BootMessageProps> = ({ message, showUpdateSteps, progressData }) => {
const BootMessage: React.FC<BootMessageProps> = ({
message,
showUpdateSteps,
progressData,
}) => {
const [showSystemInfo, setShowSystemInfo] = useState(false);

return (
<div id="boot-message">
<div className="boot-panel">
<div className="brand">NodeTool</div>
<div className="boot-panel-header">
<div className="brand">NodeTool</div>
<button
className="system-info-toggle"
onClick={() => setShowSystemInfo(!showSystemInfo)}
title="System Information"
>
{showSystemInfo ? "✕" : "ℹ"}
</button>
</div>
<div className="brand-ring" aria-hidden="true" />

<svg
Expand Down Expand Up @@ -71,7 +87,7 @@ const BootMessage: React.FC<BootMessageProps> = ({ message, showUpdateSteps, pro
{Math.round(progressData.progress)}%
</span>
<span className="progress-eta">
{progressData.eta ? ` (${progressData.eta})` : ''}
{progressData.eta ? ` (${progressData.eta})` : ""}
</span>
</span>
</div>
Expand All @@ -85,8 +101,14 @@ const BootMessage: React.FC<BootMessageProps> = ({ message, showUpdateSteps, pro
</div>
)}
</div>

{/* System Diagnostics Overlay */}
<SystemDiagnostics
isVisible={showSystemInfo}
onToggle={() => setShowSystemInfo(!showSystemInfo)}
/>
</div>
);
};

export default BootMessage;
export default BootMessage;
Loading
Loading