From 688c2ba52c51f147c81840d38cfaf7cad317b205 Mon Sep 17 00:00:00 2001 From: Danijel Micic Date: Mon, 15 Dec 2025 14:00:32 +1100 Subject: [PATCH] feat: Phase 1 Quick Wins - Dark mode, presets, history, and enhanced errors - Add dark/light theme toggle with IndexedDB persistence - Implement configuration presets (Office365, Gmail, SendGrid, Mailgun, Amazon SES) - Add test history panel with last 50 tests and click-to-load - Create IndexedDB wrapper for persistent storage - Add enhanced error messages with troubleshooting tips - Improve UX with auto-save last configuration - Add custom scrollbars and smooth theme transitions --- index.html | 352 ++++++++++++++++++++++------------------------ public/db.js | 199 ++++++++++++++++++++++++++ public/script.js | 311 ++++++++++++++++++++++++++++++++++++++-- public/styles.css | 286 +++++++++++++++++++++++++++++++++---- 4 files changed, 930 insertions(+), 218 deletions(-) create mode 100644 public/db.js diff --git a/index.html b/index.html index 5eafb1e..1813d6e 100644 --- a/index.html +++ b/index.html @@ -1,197 +1,177 @@ - - - - - Advanced SMTP Tester - - - - - - - + + + + + Advanced SMTP Tester + + - -
+ + + + + -
- -
-
-

- Logo - SMTP Tester -

-

- Advanced SMTP Configuration & Delivery Testing Utility -

- - - - - Watch on YouTube @beyondcloudtechnology - -
-
+ + + - -
-
-
-
📧
-

Server Configuration

-
+
-
-
- -
- - -
-
- - -
-
- - -
- - -
- - -
-
- -
- - -
-
- - -
- - -
-
- - -
-
- -
- - -
- - -
- ⚠️ -

- Note: Per your request, the password used for - this test will be included in the body of the test email. -

-
-
- - - -
-
- - - -
+ + - - - + +
+
+
+
📧
+

Server Configuration

+
+ +
+ +
+ + +
+ +
+ +
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + +
+ + +
+ ⚠️ +

+ Note: Per your request, the password used for + this test will be included in the body of the test email. +

+
+
+ + + + + +
+
+

📋 Test History

+ +
+
+
No test history yet. Run a test to see it here!
+
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/public/db.js b/public/db.js new file mode 100644 index 0000000..70ff016 --- /dev/null +++ b/public/db.js @@ -0,0 +1,199 @@ +// IndexedDB Helper for SMTP Tester +// Manages persistent storage for test history, settings, and preferences + +const DB_NAME = 'SMTPTesterDB'; +const DB_VERSION = 1; +const STORES = { + HISTORY: 'testHistory', + SETTINGS: 'settings', + PREFERENCES: 'preferences' +}; + +class SMTPDatabase { + constructor() { + this.db = null; + } + + // Initialize database + async init() { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + + request.onerror = () => reject(request.error); + request.onsuccess = () => { + this.db = request.result; + resolve(this.db); + }; + + request.onupgradeneeded = (event) => { + const db = event.target.result; + + // Create test history store + if (!db.objectStoreNames.contains(STORES.HISTORY)) { + const historyStore = db.createObjectStore(STORES.HISTORY, { + keyPath: 'id', + autoIncrement: true + }); + historyStore.createIndex('timestamp', 'timestamp', { unique: false }); + historyStore.createIndex('host', 'host', { unique: false }); + } + + // Create settings store + if (!db.objectStoreNames.contains(STORES.SETTINGS)) { + db.createObjectStore(STORES.SETTINGS, { keyPath: 'key' }); + } + + // Create preferences store + if (!db.objectStoreNames.contains(STORES.PREFERENCES)) { + db.createObjectStore(STORES.PREFERENCES, { keyPath: 'key' }); + } + }; + }); + } + + // Save test result to history + async saveTest(testData) { + const transaction = this.db.transaction([STORES.HISTORY], 'readwrite'); + const store = transaction.objectStore(STORES.HISTORY); + + const record = { + ...testData, + timestamp: new Date().toISOString() + }; + + return new Promise((resolve, reject) => { + const request = store.add(record); + request.onsuccess = () => { + this.cleanupOldHistory(); // Keep only last 50 records + resolve(request.result); + }; + request.onerror = () => reject(request.error); + }); + } + + // Get test history (last N records) + async getHistory(limit = 50) { + const transaction = this.db.transaction([STORES.HISTORY], 'readonly'); + const store = transaction.objectStore(STORES.HISTORY); + const index = store.index('timestamp'); + + return new Promise((resolve, reject) => { + const request = index.openCursor(null, 'prev'); // Reverse order (newest first) + const results = []; + + request.onsuccess = (event) => { + const cursor = event.target.result; + if (cursor && results.length < limit) { + results.push(cursor.value); + cursor.continue(); + } else { + resolve(results); + } + }; + request.onerror = () => reject(request.error); + }); + } + + // Clean up old history (keep only last 50) + async cleanupOldHistory() { + const transaction = this.db.transaction([STORES.HISTORY], 'readwrite'); + const store = transaction.objectStore(STORES.HISTORY); + const index = store.index('timestamp'); + + return new Promise((resolve, reject) => { + const request = index.openCursor(null, 'prev'); + let count = 0; + + request.onsuccess = (event) => { + const cursor = event.target.result; + if (cursor) { + count++; + if (count > 50) { + cursor.delete(); + } + cursor.continue(); + } else { + resolve(); + } + }; + request.onerror = () => reject(request.error); + }); + } + + // Save user settings (last used configuration) + async saveSettings(settings) { + const transaction = this.db.transaction([STORES.SETTINGS], 'readwrite'); + const store = transaction.objectStore(STORES.SETTINGS); + + const record = { + key: 'lastConfig', + ...settings, + updatedAt: new Date().toISOString() + }; + + return new Promise((resolve, reject) => { + const request = store.put(record); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } + + // Get user settings + async getSettings() { + const transaction = this.db.transaction([STORES.SETTINGS], 'readonly'); + const store = transaction.objectStore(STORES.SETTINGS); + + return new Promise((resolve, reject) => { + const request = store.get('lastConfig'); + request.onsuccess = () => resolve(request.result || null); + request.onerror = () => reject(request.error); + }); + } + + // Save preference (e.g., theme) + async savePreference(key, value) { + const transaction = this.db.transaction([STORES.PREFERENCES], 'readwrite'); + const store = transaction.objectStore(STORES.PREFERENCES); + + const record = { key, value }; + + return new Promise((resolve, reject) => { + const request = store.put(record); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } + + // Get preference + async getPreference(key) { + const transaction = this.db.transaction([STORES.PREFERENCES], 'readonly'); + const store = transaction.objectStore(STORES.PREFERENCES); + + return new Promise((resolve, reject) => { + const request = store.get(key); + request.onsuccess = () => resolve(request.result ? request.result.value : null); + request.onerror = () => reject(request.error); + }); + } + + // Clear all history + async clearHistory() { + const transaction = this.db.transaction([STORES.HISTORY], 'readwrite'); + const store = transaction.objectStore(STORES.HISTORY); + + return new Promise((resolve, reject) => { + const request = store.clear(); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } +} + +// Create singleton instance +const db = new SMTPDatabase(); + +// Initialize on load +db.init().catch(err => console.error('Failed to initialize database:', err)); + +// Export for use in other scripts +window.smtpDB = db; diff --git a/public/script.js b/public/script.js index 9f9f53d..de853be 100644 --- a/public/script.js +++ b/public/script.js @@ -1,9 +1,250 @@ +// =================================== +// SMTP TESTER - ENHANCED SCRIPT +// Features: Presets, Dark Mode, History, Error Tips +// =================================== + +// Configuration Presets +const PRESETS = { + office365: { + host: 'smtp.office365.com', + port: '587', + secure: 'false', + name: 'Microsoft Office 365' + }, + gmail: { + host: 'smtp.gmail.com', + port: '587', + secure: 'false', + name: 'Google Gmail' + }, + sendgrid: { + host: 'smtp.sendgrid.net', + port: '587', + secure: 'false', + name: 'SendGrid' + }, + mailgun: { + host: 'smtp.mailgun.org', + port: '587', + secure: 'false', + name: 'Mailgun' + }, + amazonses: { + host: 'email-smtp.us-east-1.amazonaws.com', + port: '587', + secure: 'false', + name: 'Amazon SES' + } +}; + +// Error Messages with Troubleshooting Tips +const ERROR_TIPS = { + 'EAUTH': { + title: 'Authentication Failed', + tips: [ + 'Verify your username and password are correct', + 'For Gmail: Enable "Less secure app access" or use an App Password', + 'For Office 365: Check if Modern Authentication is required', + 'Some providers require OAuth2 instead of password authentication' + ] + }, + 'ECONNECTION': { + title: 'Connection Failed', + tips: [ + 'Check if the SMTP host address is correct', + 'Verify the port number (587 for STARTTLS, 465 for SSL/TLS)', + 'Ensure your firewall isn\'t blocking the connection', + 'Try switching between STARTTLS and SSL/TLS encryption' + ] + }, + 'ETIMEDOUT': { + title: 'Connection Timeout', + tips: [ + 'The server took too long to respond', + 'Check your internet connection', + 'The SMTP server might be down or overloaded', + 'Try again in a few moments' + ] + }, + 'ENOTFOUND': { + title: 'Host Not Found', + tips: [ + 'Double-check the SMTP host address for typos', + 'Ensure you have an active internet connection', + 'The SMTP server might be temporarily unavailable' + ] + }, + 'DEFAULT': { + title: 'SMTP Error', + tips: [ + 'Review your SMTP configuration settings', + 'Check the error message for specific details', + 'Consult your email provider\'s documentation', + 'Try using the Auto Discovery feature' + ] + } +}; + +// =================================== +// THEME MANAGEMENT +// =================================== +const themeToggle = document.getElementById('themeToggle'); +let currentTheme = 'dark'; + +async function initTheme() { + // Load saved theme preference + const savedTheme = await window.smtpDB.getPreference('theme'); + if (savedTheme) { + currentTheme = savedTheme; + applyTheme(currentTheme); + } +} + +function applyTheme(theme) { + if (theme === 'light') { + document.documentElement.setAttribute('data-theme', 'light'); + themeToggle.textContent = '☀️'; + } else { + document.documentElement.removeAttribute('data-theme'); + themeToggle.textContent = '🌙'; + } +} + +themeToggle.addEventListener('click', async () => { + currentTheme = currentTheme === 'dark' ? 'light' : 'dark'; + applyTheme(currentTheme); + await window.smtpDB.savePreference('theme', currentTheme); +}); + +// =================================== +// PRESET MANAGEMENT +// =================================== +const presetSelect = document.getElementById('presetSelect'); + +presetSelect.addEventListener('change', async function () { + const presetKey = this.value; + + if (!presetKey) return; + + if (presetKey === 'custom') { + // Load last used configuration + const settings = await window.smtpDB.getSettings(); + if (settings) { + loadConfigToForm(settings); + } + } else if (PRESETS[presetKey]) { + loadConfigToForm(PRESETS[presetKey]); + } + + // Reset selector + setTimeout(() => { + this.value = ''; + }, 100); +}); + +function loadConfigToForm(config) { + if (config.host) document.getElementById('host').value = config.host; + if (config.port) document.getElementById('port').value = config.port; + if (config.secure !== undefined) document.getElementById('secure').value = config.secure; + if (config.user) document.getElementById('user').value = config.user; + if (config.pass) document.getElementById('pass').value = config.pass; + if (config.from) document.getElementById('from').value = config.from; + if (config.to) document.getElementById('to').value = config.to; +} + +// =================================== +// HISTORY MANAGEMENT +// =================================== +async function loadHistory() { + const history = await window.smtpDB.getHistory(); + const historyList = document.getElementById('historyList'); + + if (history.length === 0) { + historyList.innerHTML = '
No test history yet. Run a test to see it here!
'; + return; + } + + historyList.innerHTML = history.map(item => { + const date = new Date(item.timestamp); + const timeStr = date.toLocaleString(); + const statusClass = item.success ? 'success' : 'error'; + const statusText = item.success ? '✅ Success' : '❌ Failed'; + + return ` +
+
+ ${item.host}:${item.port} + ${timeStr} +
+
+ ${statusText} + ${item.secure === 'true' ? 'SSL/TLS' : (item.secure === 'false' ? 'STARTTLS' : 'Unencrypted')} + ${item.user} +
+
+ `; + }).join(''); + + // Add click handlers to load history items + document.querySelectorAll('.history-item').forEach(item => { + item.addEventListener('click', async function () { + const id = parseInt(this.dataset.id); + const history = await window.smtpDB.getHistory(); + const historyItem = history.find(h => h.id === id); + if (historyItem) { + loadConfigToForm(historyItem); + } + }); + }); +} + +// Clear history button +document.getElementById('clearHistoryBtn').addEventListener('click', async () => { + if (confirm('Are you sure you want to clear all test history?')) { + await window.smtpDB.clearHistory(); + await loadHistory(); + } +}); + +// =================================== +// ERROR HANDLING +// =================================== +function getErrorTips(errorMessage) { + for (const [code, info] of Object.entries(ERROR_TIPS)) { + if (errorMessage.includes(code)) { + return info; + } + } + return ERROR_TIPS.DEFAULT; +} + +function displayErrorWithTips(errorMessage, container) { + const tips = getErrorTips(errorMessage); + + const tipsHTML = ` +
+
💡 ${tips.title}
+ +
+ `; + + container.innerHTML = errorMessage + tipsHTML; +} + +// =================================== +// PASSWORD TOGGLE +// =================================== function togglePassword() { const passInput = document.getElementById('pass'); const type = passInput.getAttribute('type') === 'password' ? 'text' : 'password'; passInput.setAttribute('type', type); } +// =================================== +// FORM SUBMISSION +// =================================== document.getElementById('smtpForm').addEventListener('submit', async function (e) { e.preventDefault(); @@ -37,8 +278,8 @@ document.getElementById('smtpForm').addEventListener('submit', async function (e }, body: JSON.stringify(data), }); + if (!response.ok) { - // Handle non-200 responses const contentType = response.headers.get("content-type"); if (contentType && contentType.includes("application/json")) { const errorResult = await response.json(); @@ -58,18 +299,48 @@ document.getElementById('smtpForm').addEventListener('submit', async function (e statusDiv.classList.add('status-success'); statusDiv.textContent = '✅ Success! Email Sent Successfully.'; logOutput.textContent = JSON.stringify(result.details, null, 2); + + // Save to history + await window.smtpDB.saveTest({ + ...data, + success: true, + messageId: result.details.messageId + }); + + // Save as last used configuration + await window.smtpDB.saveSettings(data); + + // Reload history + await loadHistory(); } else { - // This block handles cases where response was 200 OK but success is false (business logic error) statusDiv.classList.add('status-error'); statusDiv.textContent = '❌ Error: ' + result.message; - logOutput.textContent = result.error || 'Unknown error occurred.'; + displayErrorWithTips(result.error || 'Unknown error occurred.', logOutput); + + // Save failed test to history + await window.smtpDB.saveTest({ + ...data, + success: false, + error: result.error + }); + + await loadHistory(); } } catch (error) { resultsDiv.classList.remove('hidden'); statusDiv.classList.add('status-error'); - statusDiv.textContent = '❌ Error Caught'; // Changed from "Network Error" to be more accurate - logOutput.textContent = error.message; // Use error.message instead of error.toString() + statusDiv.textContent = '❌ Error Caught'; + displayErrorWithTips(error.message, logOutput); + + // Save failed test to history + await window.smtpDB.saveTest({ + ...data, + success: false, + error: error.message + }); + + await loadHistory(); } finally { // Reset Button btn.disabled = false; @@ -78,7 +349,9 @@ document.getElementById('smtpForm').addEventListener('submit', async function (e } }); -// Auto Discovery Test Handler +// =================================== +// AUTO DISCOVERY +// =================================== document.getElementById('autoTestBtn').addEventListener('click', async function () { const btn = document.getElementById('autoTestBtn'); const spinner = btn.querySelector('.loading-spinner'); @@ -120,6 +393,7 @@ document.getElementById('autoTestBtn').addEventListener('click', async function }, body: JSON.stringify(autoTestData), }); + if (!response.ok) { const contentType = response.headers.get("content-type"); if (contentType && contentType.includes("application/json")) { @@ -165,14 +439,14 @@ document.getElementById('autoTestBtn').addEventListener('click', async function } else { statusDiv.classList.add('status-error'); statusDiv.textContent = '❌ Error: ' + result.message; - logOutput.textContent = result.error || 'Unknown error occurred.'; + displayErrorWithTips(result.error || 'Unknown error occurred.', logOutput); } } catch (error) { resultsDiv.classList.remove('hidden'); statusDiv.classList.add('status-error'); statusDiv.textContent = '❌ Error Caught'; - logOutput.textContent = error.message; + displayErrorWithTips(error.message, logOutput); } finally { // Reset Button btn.disabled = false; @@ -180,3 +454,24 @@ document.getElementById('autoTestBtn').addEventListener('click', async function btnText.textContent = '🔍 Auto Discovery Test'; } }); + +// =================================== +// INITIALIZATION +// =================================== +window.addEventListener('DOMContentLoaded', async () => { + // Wait for database to initialize + await window.smtpDB.init(); + + // Initialize theme + await initTheme(); + + // Load history + await loadHistory(); + + // Load last used configuration + const settings = await window.smtpDB.getSettings(); + if (settings) { + // Optionally auto-load last config + // loadConfigToForm(settings); + } +}); diff --git a/public/styles.css b/public/styles.css index ce342b5..c594eeb 100644 --- a/public/styles.css +++ b/public/styles.css @@ -3,20 +3,30 @@ =================================== */ :root { - /* Colors */ + /* Dark Theme (Default) */ --bg-primary: #0f172a; --bg-secondary: #1e293b; --bg-glass: rgba(30, 41, 59, 0.7); + --bg-input: rgba(15, 23, 42, 0.6); + --bg-input-focus: rgba(15, 23, 42, 0.8); --text-primary: #f1f5f9; --text-secondary: #94a3b8; - --accent-primary: #6366f1; /* Indigo 500 */ - --accent-hover: #4f46e5; /* Indigo 600 */ + --border-color: rgba(255, 255, 255, 0.1); + --border-color-focus: #6366f1; + + --accent-primary: #6366f1; + --accent-hover: #4f46e5; --accent-glow: rgba(99, 102, 241, 0.5); --success: #10b981; --error: #ef4444; --warning: #f59e0b; + /* Gradients */ + --gradient-bg-1: rgba(99, 102, 241, 0.15); + --gradient-bg-2: rgba(139, 92, 246, 0.15); + --gradient-title: linear-gradient(135deg, #fff 0%, #cbd5e1 100%); + /* Spacing & Borders */ --radius-lg: 16px; --radius-md: 8px; @@ -28,6 +38,23 @@ --spacing-lg: 2rem; } +/* Light Theme */ +[data-theme="light"] { + --bg-primary: #f8fafc; + --bg-secondary: #ffffff; + --bg-glass: rgba(255, 255, 255, 0.9); + --bg-input: #ffffff; + --bg-input-focus: #ffffff; + --text-primary: #0f172a; + --text-secondary: #64748b; + --border-color: rgba(0, 0, 0, 0.1); + --border-color-focus: #6366f1; + + --gradient-bg-1: rgba(99, 102, 241, 0.08); + --gradient-bg-2: rgba(139, 92, 246, 0.08); + --gradient-title: linear-gradient(135deg, #0f172a 0%, #334155 100%); +} + * { margin: 0; padding: 0; @@ -40,6 +67,7 @@ body { color: var(--text-primary); min-height: 100vh; overflow-x: hidden; + transition: background-color 0.3s ease, color 0.3s ease; } /* Background Animation */ @@ -49,18 +77,15 @@ body { left: 0; width: 100%; height: 100%; - background: radial-gradient( - circle at 15% 50%, - rgba(99, 102, 241, 0.15) 0%, - transparent 25% - ), - radial-gradient( - circle at 85% 30%, - rgba(139, 92, 246, 0.15) 0%, - transparent 25% - ); + background: radial-gradient(circle at 15% 50%, + var(--gradient-bg-1) 0%, + transparent 25%), + radial-gradient(circle at 85% 30%, + var(--gradient-bg-2) 0%, + transparent 25%); z-index: -1; pointer-events: none; + transition: background 0.3s ease; } .container { @@ -86,7 +111,7 @@ body { align-items: center; justify-content: center; gap: 1rem; - background: linear-gradient(135deg, #fff 0%, #cbd5e1 100%); + background: var(--gradient-title); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; @@ -138,10 +163,11 @@ body { background: var(--bg-glass); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); - border: 1px solid rgba(255, 255, 255, 0.1); + border: 1px solid var(--border-color); border-radius: var(--radius-lg); padding: var(--spacing-lg); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + transition: background 0.3s ease, border-color 0.3s ease; } .panel-header { @@ -184,8 +210,8 @@ label { input, select { - background: rgba(15, 23, 42, 0.6); - border: 1px solid rgba(255, 255, 255, 0.1); + background: var(--bg-input); + border: 1px solid var(--border-color); border-radius: var(--radius-md); padding: 0.75rem 1rem; color: var(--text-primary); @@ -197,9 +223,9 @@ select { input:focus, select:focus { outline: none; - border-color: var(--accent-primary); + border-color: var(--border-color-focus); box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2); - background: rgba(15, 23, 42, 0.8); + background: var(--bg-input-focus); } /* Password Toggle */ @@ -250,11 +276,9 @@ select:focus { } .btn-primary { - background: linear-gradient( - 135deg, - var(--accent-primary), - var(--accent-hover) - ); + background: linear-gradient(135deg, + var(--accent-primary), + var(--accent-hover)); color: white; box-shadow: 0 4px 12px var(--accent-glow); } @@ -330,6 +354,7 @@ select:focus { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); @@ -424,4 +449,217 @@ select:focus { .github-icon { width: 18px; height: 18px; +}/* Theme Toggle Button */ +.theme-toggle { + position: fixed; + top: 1.5rem; + right: 1.5rem; + width: 50px; + height: 50px; + border-radius: 50%; + background: var(--bg-glass); + border: 1px solid var(--border-color); + color: var(--text-primary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + transition: all 0.3s ease; + z-index: 1000; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +.theme-toggle:hover { + transform: scale(1.1); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3); +} + +.theme-toggle:active { + transform: scale(0.95); +} + +/* Preset Selector */ +.preset-selector { + margin-bottom: var(--spacing-md); + padding: var(--spacing-md); + background: rgba(99, 102, 241, 0.05); + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: var(--radius-md); +} + +.preset-selector label { + color: var(--accent-primary); + font-weight: 600; + margin-bottom: 0.5rem; + display: block; +} + +.preset-selector select { + width: 100%; + background: var(--bg-input); + border: 1px solid var(--accent-primary); +} + +/* History Panel */ +.history-panel { + margin-top: var(--spacing-lg); + padding: var(--spacing-md); + background: var(--bg-glass); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + max-height: 400px; + overflow-y: auto; +} + +.history-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--spacing-sm); + padding-bottom: var(--spacing-sm); + border-bottom: 1px solid var(--border-color); +} + +.history-header h3 { + font-size: 1.1rem; + color: var(--text-primary); +} + +.clear-history-btn { + padding: 0.4rem 0.8rem; + background: rgba(239, 68, 68, 0.1); + border: 1px solid var(--error); + border-radius: var(--radius-sm); + color: var(--error); + font-size: 0.85rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.clear-history-btn:hover { + background: rgba(239, 68, 68, 0.2); +} + +.history-item { + padding: var(--spacing-sm); + margin-bottom: var(--spacing-sm); + background: var(--bg-input); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + cursor: pointer; + transition: all 0.2s ease; +} + +.history-item:hover { + border-color: var(--accent-primary); + transform: translateX(4px); +} + +.history-item-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.history-item-host { + font-family: "JetBrains Mono", monospace; + font-size: 0.9rem; + color: var(--text-primary); + font-weight: 600; +} + +.history-item-time { + font-size: 0.75rem; + color: var(--text-secondary); +} + +.history-item-details { + display: flex; + gap: 1rem; + font-size: 0.8rem; + color: var(--text-secondary); +} + +.history-item-status { + padding: 0.2rem 0.5rem; + border-radius: var(--radius-sm); + font-size: 0.75rem; + font-weight: 600; +} + +.history-item-status.success { + background: rgba(16, 185, 129, 0.1); + color: var(--success); +} + +.history-item-status.error { + background: rgba(239, 68, 68, 0.1); + color: var(--error); +} + +.history-empty { + text-align: center; + padding: var(--spacing-lg); + color: var(--text-secondary); + font-style: italic; +} + +/* Error Tips */ +.error-tips { + margin-top: var(--spacing-sm); + padding: var(--spacing-sm); + background: rgba(239, 68, 68, 0.05); + border-left: 3px solid var(--error); + border-radius: var(--radius-sm); +} + +.error-tips-title { + font-weight: 600; + color: var(--error); + margin-bottom: 0.5rem; + font-size: 0.9rem; +} + +.error-tips ul { + margin-left: 1.2rem; + color: var(--text-secondary); + font-size: 0.85rem; +} + +.error-tips li { + margin-bottom: 0.3rem; +} + +.error-tips a { + color: var(--accent-primary); + text-decoration: none; +} + +.error-tips a:hover { + text-decoration: underline; +} + +/* Scrollbar Styling */ +.history-panel::-webkit-scrollbar, +.log-output::-webkit-scrollbar { + width: 8px; +} + +.history-panel::-webkit-scrollbar-track, +.log-output::-webkit-scrollbar-track { + background: var(--bg-input); + border-radius: var(--radius-sm); +} + +.history-panel::-webkit-scrollbar-thumb, +.log-output::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: var(--radius-sm); +} + +.history-panel::-webkit-scrollbar-thumb:hover, +.log-output::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); }