Add Send Logs button for one-click log submission to support

Collects launcher logs, game client logs, and config snapshot into a ZIP
file and uploads to auth server. Shows submission ID for sharing with
support. Includes i18n for all 11 locales.
This commit is contained in:
sanasol
2026-02-24 13:50:23 +01:00
parent e14d56ef48
commit 320ca54758
17 changed files with 510 additions and 0 deletions

View File

@@ -575,6 +575,9 @@
<i class="fas fa-folder-open"></i> <span data-i18n="settings.logsFolder">Open <i class="fas fa-folder-open"></i> <span data-i18n="settings.logsFolder">Open
Folder</span> Folder</span>
</button> </button>
<button class="logs-action-btn logs-send-btn" id="sendLogsBtn" onclick="sendLogs()">
<i class="fas fa-paper-plane"></i> <span data-i18n="settings.logsSend">Send Logs</span>
</button>
</div> </div>
</div> </div>
<div id="logsTerminal" class="logs-terminal"> <div id="logsTerminal" class="logs-terminal">

View File

@@ -66,6 +66,113 @@ async function openLogsFolder() {
await window.electronAPI.openLogsFolder(); await window.electronAPI.openLogsFolder();
} }
async function sendLogs() {
const btn = document.getElementById('sendLogsBtn');
if (!btn || btn.disabled) return;
// Get i18n strings with fallbacks
const i18n = window.i18n || {};
const sendingText = (i18n.settings && i18n.settings.logsSending) || 'Sending...';
const sentText = (i18n.settings && i18n.settings.logsSent) || 'Sent!';
const failedText = (i18n.settings && i18n.settings.logsSendFailed) || 'Failed';
const sendText = (i18n.settings && i18n.settings.logsSend) || 'Send Logs';
const originalHTML = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${sendingText}`;
try {
const result = await window.electronAPI.sendLogs();
if (result.success) {
btn.innerHTML = `<i class="fas fa-check"></i> ${sentText}`;
showLogSubmissionResult(result.id);
} else {
btn.innerHTML = `<i class="fas fa-times"></i> ${failedText}`;
console.error('Send logs failed:', result.error);
// Show error notification if available
if (window.LauncherUI && window.LauncherUI.showNotification) {
window.LauncherUI.showNotification(result.error || 'Failed to send logs', 'error');
}
}
} catch (err) {
console.error('Send logs error:', err);
btn.innerHTML = `<i class="fas fa-times"></i> ${failedText}`;
}
setTimeout(() => {
btn.disabled = false;
btn.innerHTML = originalHTML;
}, 3000);
}
function showLogSubmissionResult(id) {
// Remove existing popup if any
const existing = document.getElementById('logSubmissionPopup');
if (existing) existing.remove();
const i18n = window.i18n || {};
const idLabel = (i18n.settings && i18n.settings.logsSubmissionId) || 'Submission ID';
const copyText = (i18n.common && i18n.common.copy) || 'Copy';
const closeText = (i18n.common && i18n.common.close) || 'Close';
const shareText = (i18n.settings && i18n.settings.logsShareId) || 'Share this ID with support when reporting issues';
const popup = document.createElement('div');
popup.id = 'logSubmissionPopup';
popup.style.cssText = `
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: rgba(20, 20, 35, 0.98); border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 12px; padding: 24px 32px; z-index: 10000;
box-shadow: 0 20px 60px rgba(0,0,0,0.5); text-align: center;
min-width: 320px; backdrop-filter: blur(10px);
`;
popup.innerHTML = `
<div style="margin-bottom: 16px;">
<i class="fas fa-check-circle" style="font-size: 2em; color: #00d4ff;"></i>
</div>
<div style="color: #888; font-size: 0.85em; margin-bottom: 8px;">${idLabel}</div>
<div id="logSubId" style="font-family: monospace; font-size: 1.5em; color: #00d4ff; letter-spacing: 2px; margin-bottom: 12px; user-select: all;">${id}</div>
<div style="color: #666; font-size: 0.8em; margin-bottom: 20px;">${shareText}</div>
<div style="display: flex; gap: 10px; justify-content: center;">
<button onclick="copyLogSubmissionId('${id}')" style="
background: rgba(0,212,255,0.2); border: 1px solid rgba(0,212,255,0.3);
color: #00d4ff; padding: 8px 20px; border-radius: 6px; cursor: pointer;
font-size: 0.9em;
"><i class="fas fa-copy"></i> ${copyText}</button>
<button onclick="document.getElementById('logSubmissionPopup').remove()" style="
background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2);
color: #ccc; padding: 8px 20px; border-radius: 6px; cursor: pointer;
font-size: 0.9em;
">${closeText}</button>
</div>
`;
document.body.appendChild(popup);
// Auto-close after 30s
setTimeout(() => {
if (document.getElementById('logSubmissionPopup')) {
popup.remove();
}
}, 30000);
}
async function copyLogSubmissionId(id) {
try {
await navigator.clipboard.writeText(id);
const btn = event.target.closest('button');
if (btn) {
const orig = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-check"></i> Copied!';
setTimeout(() => { btn.innerHTML = orig; }, 1500);
}
} catch (err) {
console.error('Failed to copy submission ID:', err);
}
}
function openLogs() { function openLogs() {
// Navigation is handled by sidebar logic, but we can trigger a refresh // Navigation is handled by sidebar logic, but we can trigger a refresh
window.LauncherUI.showPage('logs-page'); window.LauncherUI.showPage('logs-page');
@@ -77,6 +184,8 @@ function openLogs() {
window.refreshLogs = refreshLogs; window.refreshLogs = refreshLogs;
window.copyLogs = copyLogs; window.copyLogs = copyLogs;
window.openLogsFolder = openLogsFolder; window.openLogsFolder = openLogsFolder;
window.sendLogs = sendLogs;
window.copyLogSubmissionId = copyLogSubmissionId;
window.openLogs = openLogs; window.openLogs = openLogs;
// Auto-load logs when the page becomes active // Auto-load logs when the page becomes active

View File

@@ -127,6 +127,12 @@
"logsCopy": "نسخ", "logsCopy": "نسخ",
"logsRefresh": "تحديث", "logsRefresh": "تحديث",
"logsFolder": "فتح المجلد", "logsFolder": "فتح المجلد",
"logsSend": "إرسال السجلات",
"logsSending": "جارٍ الإرسال...",
"logsSent": "تم الإرسال!",
"logsSendFailed": "فشل",
"logsSubmissionId": "معرف الإرسال",
"logsShareId": "شارك هذا المعرف مع الدعم عند الإبلاغ عن المشاكل",
"logsLoading": "جاري تحميل السجلات...", "logsLoading": "جاري تحميل السجلات...",
"closeLauncher": "سلوك المشغل", "closeLauncher": "سلوك المشغل",
"closeOnStart": "إغلاق المشغل عند بدء اللعبة", "closeOnStart": "إغلاق المشغل عند بدء اللعبة",

View File

@@ -127,6 +127,12 @@
"logsCopy": "Kopieren", "logsCopy": "Kopieren",
"logsRefresh": "Aktualisieren", "logsRefresh": "Aktualisieren",
"logsFolder": "Ordner öffnen", "logsFolder": "Ordner öffnen",
"logsSend": "Logs senden",
"logsSending": "Senden...",
"logsSent": "Gesendet!",
"logsSendFailed": "Fehlgeschlagen",
"logsSubmissionId": "Einreichungs-ID",
"logsShareId": "Teilen Sie diese ID dem Support mit, wenn Sie Probleme melden",
"logsLoading": "Protokolle werden geladen...", "logsLoading": "Protokolle werden geladen...",
"closeLauncher": "Launcher-Verhalten", "closeLauncher": "Launcher-Verhalten",
"closeOnStart": "Launcher beim Spielstart schließen", "closeOnStart": "Launcher beim Spielstart schließen",

View File

@@ -127,6 +127,12 @@
"logsCopy": "Copy", "logsCopy": "Copy",
"logsRefresh": "Refresh", "logsRefresh": "Refresh",
"logsFolder": "Open Folder", "logsFolder": "Open Folder",
"logsSend": "Send Logs",
"logsSending": "Sending...",
"logsSent": "Sent!",
"logsSendFailed": "Failed",
"logsSubmissionId": "Submission ID",
"logsShareId": "Share this ID with support when reporting issues",
"logsLoading": "Loading logs...", "logsLoading": "Loading logs...",
"closeLauncher": "Launcher Behavior", "closeLauncher": "Launcher Behavior",
"closeOnStart": "Close Launcher on game start", "closeOnStart": "Close Launcher on game start",

View File

@@ -127,6 +127,12 @@
"logsCopy": "Copiar", "logsCopy": "Copiar",
"logsRefresh": "Actualizar", "logsRefresh": "Actualizar",
"logsFolder": "Abrir Carpeta", "logsFolder": "Abrir Carpeta",
"logsSend": "Enviar logs",
"logsSending": "Enviando...",
"logsSent": "Enviado!",
"logsSendFailed": "Fallido",
"logsSubmissionId": "ID de envío",
"logsShareId": "Comparte este ID con soporte al reportar problemas",
"logsLoading": "Cargando registros...", "logsLoading": "Cargando registros...",
"closeLauncher": "Comportamiento del Launcher", "closeLauncher": "Comportamiento del Launcher",
"closeOnStart": "Cerrar Launcher al iniciar el juego", "closeOnStart": "Cerrar Launcher al iniciar el juego",

View File

@@ -127,6 +127,12 @@
"logsCopy": "Copier", "logsCopy": "Copier",
"logsRefresh": "Actualiser", "logsRefresh": "Actualiser",
"logsFolder": "Ouvrir le Dossier", "logsFolder": "Ouvrir le Dossier",
"logsSend": "Envoyer les logs",
"logsSending": "Envoi...",
"logsSent": "Envoyé !",
"logsSendFailed": "Échoué",
"logsSubmissionId": "ID de soumission",
"logsShareId": "Partagez cet ID avec le support pour signaler des problèmes",
"logsLoading": "Chargement des journaux...", "logsLoading": "Chargement des journaux...",
"closeLauncher": "Comportement du Launcher", "closeLauncher": "Comportement du Launcher",
"closeOnStart": "Fermer le Launcher au démarrage du jeu", "closeOnStart": "Fermer le Launcher au démarrage du jeu",

View File

@@ -127,6 +127,12 @@
"logsCopy": "Salin", "logsCopy": "Salin",
"logsRefresh": "Segarkan", "logsRefresh": "Segarkan",
"logsFolder": "Buka Folder", "logsFolder": "Buka Folder",
"logsSend": "Kirim Log",
"logsSending": "Mengirim...",
"logsSent": "Terkirim!",
"logsSendFailed": "Gagal",
"logsSubmissionId": "ID Pengiriman",
"logsShareId": "Bagikan ID ini ke dukungan saat melaporkan masalah",
"logsLoading": "Memuat log...", "logsLoading": "Memuat log...",
"closeLauncher": "Perilaku Launcher", "closeLauncher": "Perilaku Launcher",
"closeOnStart": "Tutup launcher saat game dimulai", "closeOnStart": "Tutup launcher saat game dimulai",

View File

@@ -127,6 +127,12 @@
"logsCopy": "Kopiuj", "logsCopy": "Kopiuj",
"logsRefresh": "Odśwież", "logsRefresh": "Odśwież",
"logsFolder": "Otwórz Folder", "logsFolder": "Otwórz Folder",
"logsSend": "Wyślij logi",
"logsSending": "Wysyłanie...",
"logsSent": "Wysłano!",
"logsSendFailed": "Błąd",
"logsSubmissionId": "ID zgłoszenia",
"logsShareId": "Udostępnij ten ID wsparciu technicznemu przy zgłaszaniu problemów",
"logsLoading": "Ładowanie logów...", "logsLoading": "Ładowanie logów...",
"closeLauncher": "Zachowanie Launchera", "closeLauncher": "Zachowanie Launchera",
"closeOnStart": "Zamknij Launcher przy starcie gry", "closeOnStart": "Zamknij Launcher przy starcie gry",

View File

@@ -127,6 +127,12 @@
"logsCopy": "Copiar", "logsCopy": "Copiar",
"logsRefresh": "Atualizar", "logsRefresh": "Atualizar",
"logsFolder": "Abrir Pasta", "logsFolder": "Abrir Pasta",
"logsSend": "Enviar logs",
"logsSending": "Enviando...",
"logsSent": "Enviado!",
"logsSendFailed": "Falhou",
"logsSubmissionId": "ID de envio",
"logsShareId": "Compartilhe este ID com o suporte ao relatar problemas",
"logsLoading": "Carregando registros...", "logsLoading": "Carregando registros...",
"closeLauncher": "Comportamento do Lançador", "closeLauncher": "Comportamento do Lançador",
"closeOnStart": "Fechar Lançador ao iniciar o jogo", "closeOnStart": "Fechar Lançador ao iniciar o jogo",

View File

@@ -127,6 +127,12 @@
"logsCopy": "Копировать", "logsCopy": "Копировать",
"logsRefresh": "Обновить", "logsRefresh": "Обновить",
"logsFolder": "Открыть папку", "logsFolder": "Открыть папку",
"logsSend": "Отправить логи",
"logsSending": "Отправка...",
"logsSent": "Отправлено!",
"logsSendFailed": "Ошибка",
"logsSubmissionId": "ID отправки",
"logsShareId": "Поделитесь этим ID с поддержкой при обращении",
"logsLoading": "Загрузка логов...", "logsLoading": "Загрузка логов...",
"closeLauncher": "Поведение лаунчера", "closeLauncher": "Поведение лаунчера",
"closeOnStart": "Закрыть лаунчер при старте игры", "closeOnStart": "Закрыть лаунчер при старте игры",

View File

@@ -127,6 +127,12 @@
"logsCopy": "Kopiera", "logsCopy": "Kopiera",
"logsRefresh": "Uppdatera", "logsRefresh": "Uppdatera",
"logsFolder": "Öppna mapp", "logsFolder": "Öppna mapp",
"logsSend": "Skicka loggar",
"logsSending": "Skickar...",
"logsSent": "Skickat!",
"logsSendFailed": "Misslyckades",
"logsSubmissionId": "Inlämnings-ID",
"logsShareId": "Dela detta ID med support vid felanmälan",
"logsLoading": "Laddar loggar...", "logsLoading": "Laddar loggar...",
"closeLauncher": "Launcher-beteende", "closeLauncher": "Launcher-beteende",
"closeOnStart": "Stäng launcher vid spelstart", "closeOnStart": "Stäng launcher vid spelstart",

View File

@@ -127,6 +127,12 @@
"logsCopy": "Kopyala", "logsCopy": "Kopyala",
"logsRefresh": "Yenile", "logsRefresh": "Yenile",
"logsFolder": "Klasörü Aç", "logsFolder": "Klasörü Aç",
"logsSend": "Logları Gönder",
"logsSending": "Gönderiliyor...",
"logsSent": "Gönderildi!",
"logsSendFailed": "Başarısız",
"logsSubmissionId": "Gönderim ID",
"logsShareId": "Sorun bildirirken bu ID'yi destekle paylaşın",
"logsLoading": "Loglar yükleniyor...", "logsLoading": "Loglar yükleniyor...",
"closeLauncher": "Başlatıcı Davranışı", "closeLauncher": "Başlatıcı Davranışı",
"closeOnStart": "Oyun başlatıldığında Başlatıcıyı Kapat", "closeOnStart": "Oyun başlatıldığında Başlatıcıyı Kapat",

View File

@@ -873,6 +873,22 @@ body {
border-color: rgba(255, 255, 255, 0.2); border-color: rgba(255, 255, 255, 0.2);
} }
.logs-action-btn.logs-send-btn {
background: rgba(0, 212, 255, 0.15);
border-color: rgba(0, 212, 255, 0.3);
color: #00d4ff;
}
.logs-action-btn.logs-send-btn:hover {
background: rgba(0, 212, 255, 0.25);
border-color: rgba(0, 212, 255, 0.5);
}
.logs-action-btn.logs-send-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.logs-terminal { .logs-terminal {
flex: 1; flex: 1;
background: #0d1117; background: #0d1117;

View File

@@ -0,0 +1,238 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
const zlib = require('zlib');
const logger = require('../logger');
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB per file
/**
* Get the HytaleSaves directory (game client logs)
*/
function getHytaleSavesDir() {
const home = os.homedir();
if (process.platform === 'win32') {
return path.join(home, 'AppData', 'Local', 'HytaleSaves');
} else if (process.platform === 'darwin') {
return path.join(home, 'Library', 'Application Support', 'HytaleSaves');
} else {
return path.join(home, '.hytalesaves');
}
}
/**
* Read a log file, capping at MAX_FILE_SIZE (keeps tail/most recent lines)
*/
function readLogFile(filePath) {
try {
const stats = fs.statSync(filePath);
if (stats.size <= MAX_FILE_SIZE) {
return fs.readFileSync(filePath, 'utf8');
}
// Read only the last MAX_FILE_SIZE bytes
const fd = fs.openSync(filePath, 'r');
const buffer = Buffer.alloc(MAX_FILE_SIZE);
fs.readSync(fd, buffer, 0, MAX_FILE_SIZE, stats.size - MAX_FILE_SIZE);
fs.closeSync(fd);
const content = buffer.toString('utf8');
// Skip first partial line
const firstNewline = content.indexOf('\n');
const trimmed = firstNewline >= 0 ? content.substring(firstNewline + 1) : content;
return `[... truncated ${stats.size - MAX_FILE_SIZE} bytes ...]\n` + trimmed;
} catch (err) {
return `[Error reading file: ${err.message}]`;
}
}
/**
* Get files matching a date pattern from a directory
*/
function getFilesForDate(dir, dateStr, pattern) {
if (!fs.existsSync(dir)) return [];
try {
return fs.readdirSync(dir)
.filter(f => f.includes(dateStr) && (pattern ? pattern.test(f) : true))
.map(f => ({
name: f,
path: path.join(dir, f),
mtime: fs.statSync(path.join(dir, f)).mtime
}))
.sort((a, b) => b.mtime - a.mtime);
} catch (err) {
return [];
}
}
// ============================================================================
// ZIP BUILDER (pure Node.js, no dependencies)
// ============================================================================
/**
* CRC32 lookup table
*/
const crc32Table = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let c = i;
for (let j = 0; j < 8; j++) {
c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
}
crc32Table[i] = c;
}
function crc32(buf) {
let crc = 0xFFFFFFFF;
for (let i = 0; i < buf.length; i++) {
crc = crc32Table[(crc ^ buf[i]) & 0xFF] ^ (crc >>> 8);
}
return (crc ^ 0xFFFFFFFF) >>> 0;
}
/**
* Create a ZIP file from an array of {name, content} entries
* Uses DEFLATE compression via built-in zlib
*/
function createZipBuffer(files) {
const localHeaders = [];
const centralEntries = [];
let offset = 0;
for (const file of files) {
const nameBytes = Buffer.from(file.name, 'utf8');
const contentBytes = Buffer.from(file.content, 'utf8');
const compressed = zlib.deflateRawSync(contentBytes);
const crcVal = crc32(contentBytes);
// Local file header (30 bytes + filename)
const local = Buffer.alloc(30);
local.writeUInt32LE(0x04034b50, 0); // signature
local.writeUInt16LE(20, 4); // version needed
local.writeUInt16LE(0, 6); // flags
local.writeUInt16LE(8, 8); // compression: DEFLATE
local.writeUInt16LE(0, 10); // mod time
local.writeUInt16LE(0, 12); // mod date
local.writeUInt32LE(crcVal, 14); // crc32
local.writeUInt32LE(compressed.length, 18); // compressed size
local.writeUInt32LE(contentBytes.length, 22); // uncompressed size
local.writeUInt16LE(nameBytes.length, 26); // filename length
local.writeUInt16LE(0, 28); // extra field length
localHeaders.push(local, nameBytes, compressed);
// Central directory entry (46 bytes + filename)
const central = Buffer.alloc(46);
central.writeUInt32LE(0x02014b50, 0); // signature
central.writeUInt16LE(20, 4); // version made by
central.writeUInt16LE(20, 6); // version needed
central.writeUInt16LE(0, 8); // flags
central.writeUInt16LE(8, 10); // compression
central.writeUInt16LE(0, 12); // mod time
central.writeUInt16LE(0, 14); // mod date
central.writeUInt32LE(crcVal, 16); // crc32
central.writeUInt32LE(compressed.length, 20); // compressed size
central.writeUInt32LE(contentBytes.length, 24); // uncompressed size
central.writeUInt16LE(nameBytes.length, 28); // filename length
central.writeUInt16LE(0, 30); // extra field length
central.writeUInt16LE(0, 32); // comment length
central.writeUInt16LE(0, 34); // disk number
central.writeUInt16LE(0, 36); // internal attrs
central.writeUInt32LE(0, 38); // external attrs
central.writeUInt32LE(offset, 42); // local header offset
centralEntries.push(central, nameBytes);
offset += 30 + nameBytes.length + compressed.length;
}
const centralDirBuf = Buffer.concat(centralEntries);
// End of central directory (22 bytes)
const eocd = Buffer.alloc(22);
eocd.writeUInt32LE(0x06054b50, 0); // signature
eocd.writeUInt16LE(0, 4); // disk number
eocd.writeUInt16LE(0, 6); // cd disk number
eocd.writeUInt16LE(files.length, 8); // entries on disk
eocd.writeUInt16LE(files.length, 10); // total entries
eocd.writeUInt32LE(centralDirBuf.length, 12); // cd size
eocd.writeUInt32LE(offset, 16); // cd offset
eocd.writeUInt16LE(0, 20); // comment length
return Buffer.concat([...localHeaders, centralDirBuf, eocd]);
}
// ============================================================================
/**
* Collect all relevant logs for submission
* Returns { files: [{name, content}], meta: {username, platform, version} }
*/
function collectLogs() {
const files = [];
const today = new Date();
const todayStr = today.toISOString().split('T')[0]; // YYYY-MM-DD
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const yesterdayStr = yesterday.toISOString().split('T')[0];
// 1. Launcher logs
const launcherLogDir = logger.getLogDirectory();
if (launcherLogDir && fs.existsSync(launcherLogDir)) {
// Today's launcher logs
const todayLogs = getFilesForDate(launcherLogDir, todayStr, /^launcher-.*\.log$/);
for (const f of todayLogs) {
files.push({ name: `launcher/${f.name}`, content: readLogFile(f.path) });
}
// Most recent from yesterday (just one)
const yesterdayLogs = getFilesForDate(launcherLogDir, yesterdayStr, /^launcher-.*\.log$/);
if (yesterdayLogs.length > 0) {
files.push({ name: `launcher/${yesterdayLogs[0].name}`, content: readLogFile(yesterdayLogs[0].path) });
}
}
// 2. Game client logs
const savesDir = getHytaleSavesDir();
const clientLogDir = path.join(savesDir, 'Logs');
if (fs.existsSync(clientLogDir)) {
const clientLogs = getFilesForDate(clientLogDir, todayStr, /client\.log$/);
for (const f of clientLogs) {
files.push({ name: `client/${f.name}`, content: readLogFile(f.path) });
}
}
// 3. Config snapshot
const appDir = logger.getAppDir ? logger.getAppDir() : logger.getInstallPath();
const configPath = path.join(appDir, 'config.json');
if (fs.existsSync(configPath)) {
try {
const configContent = fs.readFileSync(configPath, 'utf8');
files.push({ name: 'config.json', content: configContent });
} catch (err) {
files.push({ name: 'config.json', content: `[Error reading config: ${err.message}]` });
}
}
// Build metadata
const { app } = require('electron');
let username = 'unknown';
try {
const configFile = path.join(appDir, 'config.json');
if (fs.existsSync(configFile)) {
const cfg = JSON.parse(fs.readFileSync(configFile, 'utf8'));
username = cfg.username || cfg.playerName || 'unknown';
}
} catch (err) {}
return {
files,
meta: {
username,
platform: `${process.platform}-${process.arch}`,
version: app.getVersion()
}
};
}
module.exports = { collectLogs, createZipBuffer };

77
main.js
View File

@@ -1407,6 +1407,83 @@ ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => {
ipcMain.handle('send-logs', async () => {
try {
const https = require('https');
const http = require('http');
const { collectLogs, createZipBuffer } = require('./backend/utils/logCollector');
const { files, meta } = collectLogs();
if (files.length === 0) {
return { success: false, error: 'No log files found' };
}
// Create ZIP with individual log files
const zipBuffer = createZipBuffer(files);
// Get auth server URL from core config
const { getAuthServerUrl } = require('./backend/core/config');
const authUrl = getAuthServerUrl();
// Build file names list
const fileNames = files.map(f => f.name).join(',');
return await new Promise((resolve) => {
const url = new URL(authUrl + '/logs/submit');
const transport = url.protocol === 'https:' ? https : http;
const options = {
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/zip',
'Content-Length': zipBuffer.length,
'X-Log-Username': meta.username || 'unknown',
'X-Log-Platform': meta.platform || 'unknown',
'X-Log-Version': meta.version || 'unknown',
'X-Log-File-Count': String(files.length),
'X-Log-Files': fileNames
},
timeout: 30000
};
const req = transport.request(options, (res) => {
let body = '';
res.on('data', (chunk) => body += chunk);
res.on('end', () => {
try {
const data = JSON.parse(body);
if (res.statusCode === 200) {
resolve({ success: true, id: data.id, message: data.message });
} else {
resolve({ success: false, error: data.error || `Server error ${res.statusCode}` });
}
} catch (e) {
resolve({ success: false, error: `Invalid response: ${res.statusCode}` });
}
});
});
req.on('error', (err) => {
resolve({ success: false, error: err.message });
});
req.on('timeout', () => {
req.destroy();
resolve({ success: false, error: 'Request timed out' });
});
req.write(zipBuffer);
req.end();
});
} catch (error) {
console.error('Error sending logs:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('open-logs-folder', async () => { ipcMain.handle('open-logs-folder', async () => {
try { try {
const logDir = logger.getLogDirectory(); const logDir = logger.getLogDirectory();

View File

@@ -94,6 +94,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
getLogDirectory: () => ipcRenderer.invoke('get-log-directory'), getLogDirectory: () => ipcRenderer.invoke('get-log-directory'),
openLogsFolder: () => ipcRenderer.invoke('open-logs-folder'), openLogsFolder: () => ipcRenderer.invoke('open-logs-folder'),
getRecentLogs: (maxLines) => ipcRenderer.invoke('get-recent-logs', maxLines), getRecentLogs: (maxLines) => ipcRenderer.invoke('get-recent-logs', maxLines),
sendLogs: () => ipcRenderer.invoke('send-logs'),
// UUID Management methods // UUID Management methods
getCurrentUuid: () => ipcRenderer.invoke('get-current-uuid'), getCurrentUuid: () => ipcRenderer.invoke('get-current-uuid'),