-
Notifications
You must be signed in to change notification settings - Fork 2
Editor stat managment #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
eabb52c
ab78682
12b070e
95357be
8c5bccc
28085f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import { ipcMain, dialog, BrowserWindow, shell, app } from "electron"; | ||
| import fs from "fs/promises"; | ||
| import path from "path"; | ||
| import AdmZip from "adm-zip"; | ||
| import AutoLaunch from "auto-launch"; | ||
| import { | ||
| getProjects, | ||
|
|
@@ -24,6 +25,7 @@ import { | |
| restartProject, | ||
| getRunningProjects, | ||
| getProjectLogs, | ||
| getAllProjectLogs, | ||
| writeToProcess, | ||
| getProjectStats, | ||
| getProjectStartTime, | ||
|
|
@@ -34,6 +36,7 @@ import { | |
| startTunnel, | ||
| stopTunnel, | ||
| getTunnelLogs, | ||
| getAllTunnelLogs, | ||
| clearTunnelLogs, | ||
| getTunnelStatus, | ||
| } from "../services/tunnelManager.js"; | ||
|
|
@@ -65,6 +68,19 @@ const appLauncher = new AutoLaunch({ | |
| path: process.execPath, | ||
| }); | ||
|
|
||
| /** | ||
| * Create a filesystem-safe filename segment for Windows/macOS/Linux. | ||
| * Removes characters that are invalid on Windows and trims length. | ||
| */ | ||
| const sanitizeFileName = (value) => { | ||
| const name = typeof value === "string" ? value : ""; | ||
| const cleaned = name | ||
| .replace(/[<>:"/\\|?*\u0000-\u001F]/g, "_") | ||
| .replace(/\s+/g, " ") | ||
| .trim(); | ||
| return cleaned.length > 0 ? cleaned.slice(0, 120) : "project"; | ||
| }; | ||
|
|
||
| export const registerHandlers = () => { | ||
| // Helper to log all IPC calls | ||
| const originalHandle = ipcMain.handle.bind(ipcMain); | ||
|
|
@@ -289,11 +305,89 @@ export const registerHandlers = () => { | |
| ipcMain.handle("logs:get", async (_, id) => { | ||
| return getProjectLogs(id); | ||
| }); | ||
| ipcMain.handle("logs:getAll", async () => { | ||
| return getAllProjectLogs(); | ||
| }); | ||
| ipcMain.handle("logs:clear", async (_, id) => { | ||
| clearProjectLogs(id); | ||
| return true; | ||
| }); | ||
|
|
||
| ipcMain.handle("logs:exportProject", async (_, projectId) => { | ||
| const numericId = Number(projectId); | ||
| if (!Number.isFinite(numericId)) { | ||
| throw new Error("Invalid projectId for logs export"); | ||
| } | ||
|
|
||
| const window = BrowserWindow.getFocusedWindow() || global.mainWindow || null; | ||
| const projects = await getProjects(); | ||
| const project = projects.find((p) => Number(p.id) === numericId) || null; | ||
| const projectName = project?.name || `Project ${numericId}`; | ||
|
|
||
| const fileName = `${sanitizeFileName(projectName)}-${numericId}.log`; | ||
| const { canceled, filePath } = await dialog.showSaveDialog(window, { | ||
| title: `Export console logs - ${projectName}`, | ||
| defaultPath: fileName, | ||
| filters: [ | ||
| { name: "Log Files", extensions: ["log"] }, | ||
| { name: "All Files", extensions: ["*"] }, | ||
| ], | ||
| }); | ||
|
|
||
| if (canceled || !filePath) { | ||
| return { canceled: true }; | ||
| } | ||
|
|
||
| const logs = getProjectLogs(numericId); | ||
| const header = `===== Console Logs: ${projectName} (ID: ${numericId}) =====\n`; | ||
| const body = Array.isArray(logs) ? logs.map((l) => l?.data ?? "").join("") : ""; | ||
| const text = `${header}${body}`; | ||
|
|
||
| await fs.writeFile(filePath, text, "utf8"); | ||
| return { success: true, path: filePath }; | ||
| }); | ||
|
|
||
| ipcMain.handle("logs:exportAll", async () => { | ||
| const window = BrowserWindow.getFocusedWindow() || global.mainWindow || null; | ||
|
|
||
| const projects = await getProjects(); | ||
| const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); | ||
| const defaultName = `console-logs-all-${timestamp}.zip`; | ||
|
|
||
| const { canceled, filePath } = await dialog.showSaveDialog(window, { | ||
| title: "Export console logs (.zip)", | ||
| defaultPath: defaultName, | ||
| filters: [ | ||
| { name: "Zip Files", extensions: ["zip"] }, | ||
| { name: "All Files", extensions: ["*"] }, | ||
| ], | ||
| }); | ||
|
|
||
| if (canceled || !filePath) { | ||
| return { canceled: true }; | ||
| } | ||
|
|
||
| const zip = new AdmZip(); | ||
|
|
||
| for (const project of projects) { | ||
| const numericId = Number(project?.id); | ||
| if (!Number.isFinite(numericId)) continue; | ||
|
|
||
| const projectName = project?.name || `Project ${numericId}`; | ||
| const logs = getProjectLogs(numericId); | ||
| const header = `===== Console Logs: ${projectName} (ID: ${numericId}) =====\n`; | ||
| const body = Array.isArray(logs) ? logs.map((l) => l?.data ?? "").join("") : ""; | ||
| const text = `${header}${body}`; | ||
|
|
||
| const fileName = `${sanitizeFileName(projectName)}-${numericId}.log`; | ||
| zip.addFile(fileName, Buffer.from(text, "utf8")); | ||
| } | ||
|
|
||
| const out = zip.toBuffer(); | ||
| await fs.writeFile(filePath, out); | ||
| return { success: true, path: filePath }; | ||
| }); | ||
|
|
||
| ipcMain.handle("project:getStats", async (_, id) => { | ||
| return getProjectStats(id); | ||
| }); | ||
|
|
@@ -314,6 +408,9 @@ export const registerHandlers = () => { | |
| ipcMain.handle("tunnel:getLogs", async (_, id) => { | ||
| return getTunnelLogs(id); | ||
| }); | ||
| ipcMain.handle("tunnel:getAllLogs", async () => { | ||
| return getAllTunnelLogs(); | ||
| }); | ||
|
|
||
| ipcMain.handle("tunnel:clearLogs", async (_, id) => { | ||
| return clearTunnelLogs(id); | ||
|
|
@@ -323,6 +420,96 @@ export const registerHandlers = () => { | |
| return getTunnelStatus(id); | ||
| }); | ||
|
|
||
| ipcMain.handle("tunnel:exportProject", async (_, projectId) => { | ||
| const numericId = Number(projectId); | ||
| if (!Number.isFinite(numericId)) { | ||
| throw new Error("Invalid projectId for tunnel logs export"); | ||
| } | ||
|
|
||
| const window = BrowserWindow.getFocusedWindow() || global.mainWindow || null; | ||
| const projects = await getProjects(); | ||
| const project = projects.find((p) => Number(p.id) === numericId) || null; | ||
| const projectName = project?.name || `Project ${numericId}`; | ||
|
|
||
| const fileName = `${sanitizeFileName(projectName)}-${numericId}.log`; | ||
| const { canceled, filePath } = await dialog.showSaveDialog(window, { | ||
| title: `Export tunnel logs - ${projectName}`, | ||
| defaultPath: fileName, | ||
| filters: [ | ||
| { name: "Log Files", extensions: ["log"] }, | ||
| { name: "All Files", extensions: ["*"] }, | ||
| ], | ||
| }); | ||
|
|
||
| if (canceled || !filePath) { | ||
| return { canceled: true }; | ||
| } | ||
|
|
||
| const logs = getTunnelLogs(numericId); | ||
| const header = `===== Tunnel Logs: ${projectName} (ID: ${numericId}) =====\n`; | ||
|
|
||
| const formatLine = (entry) => { | ||
| const timestamp = entry?.timestamp ? new Date(entry.timestamp) : null; | ||
| const time = timestamp ? timestamp.toLocaleTimeString("en-US", { hour12: false }) : "unknown"; | ||
| const message = entry?.message ?? ""; | ||
| return `[${time}] ${message}\n`; | ||
|
Comment on lines
+451
to
+455
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preserve the full tunnel timestamp in exported lines.
🛠️ Suggested fix const formatLine = (entry) => {
const timestamp = entry?.timestamp ? new Date(entry.timestamp) : null;
- const time = timestamp ? timestamp.toLocaleTimeString("en-US", { hour12: false }) : "unknown";
+ const time =
+ timestamp && !Number.isNaN(timestamp.getTime())
+ ? timestamp.toISOString()
+ : "unknown";
const message = entry?.message ?? "";
return `[${time}] ${message}\n`;
};Apply the same formatter change in both handlers. Also applies to: 485-489 🤖 Prompt for AI Agents |
||
| }; | ||
|
|
||
| const body = Array.isArray(logs) ? logs.map(formatLine).join("") : ""; | ||
| const text = `${header}${body}`; | ||
|
|
||
| await fs.writeFile(filePath, text, "utf8"); | ||
| return { success: true, path: filePath }; | ||
| }); | ||
|
|
||
| ipcMain.handle("tunnel:exportAll", async () => { | ||
| const window = BrowserWindow.getFocusedWindow() || global.mainWindow || null; | ||
|
|
||
| const projects = await getProjects(); | ||
| const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); | ||
| const defaultName = `tunnel-logs-all-${timestamp}.zip`; | ||
|
|
||
| const { canceled, filePath } = await dialog.showSaveDialog(window, { | ||
| title: "Export tunnel logs (.zip)", | ||
| defaultPath: defaultName, | ||
| filters: [ | ||
| { name: "Zip Files", extensions: ["zip"] }, | ||
| { name: "All Files", extensions: ["*"] }, | ||
| ], | ||
| }); | ||
|
|
||
| if (canceled || !filePath) { | ||
| return { canceled: true }; | ||
| } | ||
|
|
||
| const formatLine = (entry) => { | ||
| const timestamp = entry?.timestamp ? new Date(entry.timestamp) : null; | ||
| const time = timestamp ? timestamp.toLocaleTimeString("en-US", { hour12: false }) : "unknown"; | ||
| const message = entry?.message ?? ""; | ||
| return `[${time}] ${message}\n`; | ||
| }; | ||
|
|
||
| const zip = new AdmZip(); | ||
|
|
||
| for (const project of projects) { | ||
| const numericId = Number(project?.id); | ||
| if (!Number.isFinite(numericId)) continue; | ||
|
|
||
| const projectName = project?.name || `Project ${numericId}`; | ||
| const logs = getTunnelLogs(numericId); | ||
| const header = `===== Tunnel Logs: ${projectName} (ID: ${numericId}) =====\n`; | ||
| const body = Array.isArray(logs) ? logs.map(formatLine).join("") : ""; | ||
| const text = `${header}${body}`; | ||
|
|
||
| const fileName = `${sanitizeFileName(projectName)}-${numericId}.log`; | ||
| zip.addFile(fileName, Buffer.from(text, "utf8")); | ||
| } | ||
|
|
||
| const out = zip.toBuffer(); | ||
| await fs.writeFile(filePath, out); | ||
| return { success: true, path: filePath }; | ||
| }); | ||
|
|
||
| // Dialogs | ||
| ipcMain.handle("dialog:openDirectory", async () => { | ||
| const { canceled, filePaths } = await dialog.showOpenDialog({ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These ZIP exports can freeze the Electron main process on large histories.
Each handler builds every project's log text in memory, keeps those buffers in the archive, then materializes the full ZIP buffer before
fs.writeFile. On noisy projects that can stall the whole app or OOM during export. Please move archive creation off the main thread or switch to a streaming/temp-file approach.Also applies to: 492-509
🤖 Prompt for AI Agents