diff --git a/GUI/index.html b/GUI/index.html
index 7ed1028..fe51bbc 100644
--- a/GUI/index.html
+++ b/GUI/index.html
@@ -575,6 +575,9 @@
Open
Folder
+
diff --git a/GUI/js/logs.js b/GUI/js/logs.js
index ecc21a0..795da6f 100644
--- a/GUI/js/logs.js
+++ b/GUI/js/logs.js
@@ -66,6 +66,113 @@ async function 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 = `
${sendingText}`;
+
+ try {
+ const result = await window.electronAPI.sendLogs();
+
+ if (result.success) {
+ btn.innerHTML = `
${sentText}`;
+ showLogSubmissionResult(result.id);
+ } else {
+ btn.innerHTML = `
${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 = `
${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 = `
+
+
+
+
${idLabel}
+
${id}
+
${shareText}
+
+
+
+
+ `;
+
+ 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 = '
Copied!';
+ setTimeout(() => { btn.innerHTML = orig; }, 1500);
+ }
+ } catch (err) {
+ console.error('Failed to copy submission ID:', err);
+ }
+}
+
function openLogs() {
// Navigation is handled by sidebar logic, but we can trigger a refresh
window.LauncherUI.showPage('logs-page');
@@ -77,6 +184,8 @@ function openLogs() {
window.refreshLogs = refreshLogs;
window.copyLogs = copyLogs;
window.openLogsFolder = openLogsFolder;
+window.sendLogs = sendLogs;
+window.copyLogSubmissionId = copyLogSubmissionId;
window.openLogs = openLogs;
// Auto-load logs when the page becomes active
diff --git a/GUI/locales/ar-SA.json b/GUI/locales/ar-SA.json
index cca6824..256bd02 100644
--- a/GUI/locales/ar-SA.json
+++ b/GUI/locales/ar-SA.json
@@ -127,6 +127,12 @@
"logsCopy": "نسخ",
"logsRefresh": "تحديث",
"logsFolder": "فتح المجلد",
+ "logsSend": "إرسال السجلات",
+ "logsSending": "جارٍ الإرسال...",
+ "logsSent": "تم الإرسال!",
+ "logsSendFailed": "فشل",
+ "logsSubmissionId": "معرف الإرسال",
+ "logsShareId": "شارك هذا المعرف مع الدعم عند الإبلاغ عن المشاكل",
"logsLoading": "جاري تحميل السجلات...",
"closeLauncher": "سلوك المشغل",
"closeOnStart": "إغلاق المشغل عند بدء اللعبة",
diff --git a/GUI/locales/de-DE.json b/GUI/locales/de-DE.json
index 9adbbbe..4fe4eb5 100644
--- a/GUI/locales/de-DE.json
+++ b/GUI/locales/de-DE.json
@@ -127,6 +127,12 @@
"logsCopy": "Kopieren",
"logsRefresh": "Aktualisieren",
"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...",
"closeLauncher": "Launcher-Verhalten",
"closeOnStart": "Launcher beim Spielstart schließen",
diff --git a/GUI/locales/en.json b/GUI/locales/en.json
index 722438a..a58ccff 100644
--- a/GUI/locales/en.json
+++ b/GUI/locales/en.json
@@ -127,6 +127,12 @@
"logsCopy": "Copy",
"logsRefresh": "Refresh",
"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...",
"closeLauncher": "Launcher Behavior",
"closeOnStart": "Close Launcher on game start",
diff --git a/GUI/locales/es-ES.json b/GUI/locales/es-ES.json
index c847a4e..a12825f 100644
--- a/GUI/locales/es-ES.json
+++ b/GUI/locales/es-ES.json
@@ -127,6 +127,12 @@
"logsCopy": "Copiar",
"logsRefresh": "Actualizar",
"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...",
"closeLauncher": "Comportamiento del Launcher",
"closeOnStart": "Cerrar Launcher al iniciar el juego",
diff --git a/GUI/locales/fr-FR.json b/GUI/locales/fr-FR.json
index 657aea9..20abc43 100644
--- a/GUI/locales/fr-FR.json
+++ b/GUI/locales/fr-FR.json
@@ -127,6 +127,12 @@
"logsCopy": "Copier",
"logsRefresh": "Actualiser",
"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...",
"closeLauncher": "Comportement du Launcher",
"closeOnStart": "Fermer le Launcher au démarrage du jeu",
diff --git a/GUI/locales/id-ID.json b/GUI/locales/id-ID.json
index 9ba46ec..7b661b4 100644
--- a/GUI/locales/id-ID.json
+++ b/GUI/locales/id-ID.json
@@ -127,6 +127,12 @@
"logsCopy": "Salin",
"logsRefresh": "Segarkan",
"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...",
"closeLauncher": "Perilaku Launcher",
"closeOnStart": "Tutup launcher saat game dimulai",
diff --git a/GUI/locales/pl-PL.json b/GUI/locales/pl-PL.json
index beeac96..14bfaef 100644
--- a/GUI/locales/pl-PL.json
+++ b/GUI/locales/pl-PL.json
@@ -127,6 +127,12 @@
"logsCopy": "Kopiuj",
"logsRefresh": "Odśwież",
"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...",
"closeLauncher": "Zachowanie Launchera",
"closeOnStart": "Zamknij Launcher przy starcie gry",
diff --git a/GUI/locales/pt-BR.json b/GUI/locales/pt-BR.json
index 6502b32..247bf4e 100644
--- a/GUI/locales/pt-BR.json
+++ b/GUI/locales/pt-BR.json
@@ -127,6 +127,12 @@
"logsCopy": "Copiar",
"logsRefresh": "Atualizar",
"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...",
"closeLauncher": "Comportamento do Lançador",
"closeOnStart": "Fechar Lançador ao iniciar o jogo",
diff --git a/GUI/locales/ru-RU.json b/GUI/locales/ru-RU.json
index 91d7389..5d2bd61 100644
--- a/GUI/locales/ru-RU.json
+++ b/GUI/locales/ru-RU.json
@@ -127,6 +127,12 @@
"logsCopy": "Копировать",
"logsRefresh": "Обновить",
"logsFolder": "Открыть папку",
+ "logsSend": "Отправить логи",
+ "logsSending": "Отправка...",
+ "logsSent": "Отправлено!",
+ "logsSendFailed": "Ошибка",
+ "logsSubmissionId": "ID отправки",
+ "logsShareId": "Поделитесь этим ID с поддержкой при обращении",
"logsLoading": "Загрузка логов...",
"closeLauncher": "Поведение лаунчера",
"closeOnStart": "Закрыть лаунчер при старте игры",
diff --git a/GUI/locales/sv-SE.json b/GUI/locales/sv-SE.json
index 5588d8e..f7c199e 100644
--- a/GUI/locales/sv-SE.json
+++ b/GUI/locales/sv-SE.json
@@ -127,6 +127,12 @@
"logsCopy": "Kopiera",
"logsRefresh": "Uppdatera",
"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...",
"closeLauncher": "Launcher-beteende",
"closeOnStart": "Stäng launcher vid spelstart",
diff --git a/GUI/locales/tr-TR.json b/GUI/locales/tr-TR.json
index b392e2e..f0210cd 100644
--- a/GUI/locales/tr-TR.json
+++ b/GUI/locales/tr-TR.json
@@ -127,6 +127,12 @@
"logsCopy": "Kopyala",
"logsRefresh": "Yenile",
"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...",
"closeLauncher": "Başlatıcı Davranışı",
"closeOnStart": "Oyun başlatıldığında Başlatıcıyı Kapat",
diff --git a/GUI/style.css b/GUI/style.css
index ab36fcd..2bb344e 100644
--- a/GUI/style.css
+++ b/GUI/style.css
@@ -873,6 +873,22 @@ body {
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 {
flex: 1;
background: #0d1117;
diff --git a/backend/utils/logCollector.js b/backend/utils/logCollector.js
new file mode 100644
index 0000000..46d454c
--- /dev/null
+++ b/backend/utils/logCollector.js
@@ -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 };
diff --git a/main.js b/main.js
index 94b39bb..a792a29 100644
--- a/main.js
+++ b/main.js
@@ -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 () => {
try {
const logDir = logger.getLogDirectory();
diff --git a/preload.js b/preload.js
index b217e5c..09e5438 100644
--- a/preload.js
+++ b/preload.js
@@ -94,6 +94,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
getLogDirectory: () => ipcRenderer.invoke('get-log-directory'),
openLogsFolder: () => ipcRenderer.invoke('open-logs-folder'),
getRecentLogs: (maxLines) => ipcRenderer.invoke('get-recent-logs', maxLines),
+ sendLogs: () => ipcRenderer.invoke('send-logs'),
// UUID Management methods
getCurrentUuid: () => ipcRenderer.invoke('get-current-uuid'),