feat: add account management tab with login validation

New "Accounts" tab for managing hoster credentials separately from
upload settings. Accounts can be added, edited, and deleted via modal
dialogs. Login credentials are automatically verified on save, showing
status (Bereit/Fehler) in the account list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-03-11 00:34:17 +01:00
parent d9b00f8fd7
commit d9dec33ecc
4 changed files with 453 additions and 76 deletions

View File

@ -1,6 +1,6 @@
{
"name": "multi-hoster-uploader",
"version": "1.2.1",
"version": "1.3.0",
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
"main": "main.js",
"scripts": {

View File

@ -7,6 +7,8 @@ let config = { hosters: {}, hosterSettings: {}, globalSettings: {} };
let hosterSettings = {};
let uploading = false;
let healthCheckRunning = false;
let accountStatuses = {}; // { 'voe.sx': { status: 'ok'|'error'|'checking'|'unchecked', message: '' } }
let editingAccountHoster = null; // null = adding, string = editing
let autoHealthCheckEnabled = true;
let queuePersistTimer = null;
const AUTO_CHECK_PREF_KEY = 'autoHealthCheckBeforeUpload';
@ -33,6 +35,7 @@ async function init() {
renderHosterSummary();
renderHosterModal();
renderSettings();
renderAccounts();
setupListeners();
setupDragDrop();
loadHistory();
@ -150,9 +153,10 @@ function renderHosterModal() {
list.innerHTML = available.map(item => {
const checked = selectedUploadHosters.includes(item.name);
const h = config.hosters[item.name] || {};
const subtitle = item.name === 'vidmoly.me' ? 'Login hinterlegt'
: (item.name === 'voe.sx' && h.username && h.password) ? 'Login hinterlegt'
: 'API-Key hinterlegt';
const st = accountStatuses[item.name];
const subtitle = st && st.status === 'ok' ? 'Bereit'
: st && st.status === 'error' ? 'Login-Fehler'
: getCredentialLabel(item.name, h) + ' hinterlegt';
return `
<label class="hoster-option${checked ? ' selected' : ''}" data-hoster-option="${item.name}">
<input type="checkbox" data-hoster-modal="${item.name}" ${checked ? 'checked' : ''}>
@ -897,44 +901,6 @@ function renderSettings() {
const panel = document.createElement('div');
panel.className = 'hoster-settings-panel';
let credsHtml = '';
if (name === 'vidmoly.me') {
credsHtml = `
<div class="settings-row">
<label>Username</label>
<input type="text" class="key-input" data-hoster="${name}" data-field="username" value="${escapeAttr(hoster.username || '')}" placeholder="Username">
</div>
<div class="settings-row">
<label>Passwort</label>
<input type="password" class="key-input" data-hoster="${name}" data-field="password" value="${escapeAttr(hoster.password || '')}" placeholder="Passwort">
<button class="toggle-vis" title="Anzeigen">&#128065;</button>
</div>`;
} else if (name === 'voe.sx') {
credsHtml = `
<div class="settings-row">
<label>E-Mail (Login)</label>
<input type="text" class="key-input" data-hoster="${name}" data-field="username" value="${escapeAttr(hoster.username || '')}" placeholder="E-Mail fuer Login">
</div>
<div class="settings-row">
<label>Passwort (Login)</label>
<input type="password" class="key-input" data-hoster="${name}" data-field="password" value="${escapeAttr(hoster.password || '')}" placeholder="Passwort fuer Login">
<button class="toggle-vis" title="Anzeigen">&#128065;</button>
</div>
<div class="settings-row">
<label>API Key (optional)</label>
<input type="password" class="key-input" data-hoster="${name}" data-field="apiKey" value="${escapeAttr(hoster.apiKey || '')}" placeholder="API Key (Fallback)">
<button class="toggle-vis" title="Anzeigen">&#128065;</button>
</div>
<p class="hint" style="margin:4px 0 8px;opacity:0.6">Login wird bevorzugt. API-Key nur als Fallback.</p>`;
} else {
credsHtml = `
<div class="settings-row">
<label>API Key</label>
<input type="password" class="key-input" data-hoster="${name}" data-field="apiKey" value="${escapeAttr(hoster.apiKey || '')}" placeholder="API Key">
<button class="toggle-vis" title="Anzeigen">&#128065;</button>
</div>`;
}
panel.innerHTML = `
<div class="hoster-panel-header" data-hoster="${name}">
<span class="panel-arrow">&#9654;</span>
@ -942,8 +908,6 @@ function renderSettings() {
<span class="panel-status ${hosterHasCredentials(name, hoster) ? 'active' : 'inactive'}">${hosterHasCredentials(name, hoster) ? 'Aktiv' : 'Inaktiv'}</span>
</div>
<div class="hoster-panel-body" data-panel="${name}" style="display:none">
${credsHtml}
<div class="settings-divider"></div>
<h4>Upload Einstellungen</h4>
<div class="settings-grid-mini">
<div class="settings-row">
@ -987,14 +951,6 @@ function renderSettings() {
body.style.display = isOpen ? 'none' : 'block';
arrow.innerHTML = isOpen ? '&#9654;' : '&#9660;';
});
// Toggle visibility
panel.querySelectorAll('.toggle-vis').forEach(btn => {
btn.addEventListener('click', () => {
const input = btn.previousElementSibling;
input.type = input.type === 'password' ? 'text' : 'password';
});
});
}
document.getElementById('chooseLogFilePathBtn')?.addEventListener('click', chooseLogFilePath);
@ -1008,7 +964,6 @@ async function chooseLogFilePath() {
}
async function saveSettings() {
const hosters = {};
const newHosterSettings = {};
const globalSettings = {
...(config.globalSettings || {}),
@ -1017,22 +972,6 @@ async function saveSettings() {
};
for (const name of HOSTERS) {
// Credentials
if (name === 'vidmoly.me') {
const username = (document.querySelector(`.key-input[data-hoster="${name}"][data-field="username"]`)?.value || '').trim();
const password = (document.querySelector(`.key-input[data-hoster="${name}"][data-field="password"]`)?.value || '').trim();
hosters[name] = { enabled: !!(username && password), authType: 'login', username, password };
} else if (name === 'voe.sx') {
const username = (document.querySelector(`.key-input[data-hoster="${name}"][data-field="username"]`)?.value || '').trim();
const password = (document.querySelector(`.key-input[data-hoster="${name}"][data-field="password"]`)?.value || '').trim();
const apiKey = (document.querySelector(`.key-input[data-hoster="${name}"][data-field="apiKey"]`)?.value || '').trim();
hosters[name] = { enabled: !!(username && password) || !!apiKey, username, password, apiKey };
} else {
const apiKey = (document.querySelector(`.key-input[data-hoster="${name}"][data-field="apiKey"]`)?.value || '').trim();
hosters[name] = { enabled: !!apiKey, apiKey };
}
// Upload settings
const hs = {};
document.querySelectorAll(`.hs-input[data-hoster="${name}"]`).forEach(input => {
const field = input.dataset.hs;
@ -1041,22 +980,303 @@ async function saveSettings() {
newHosterSettings[name] = hs;
}
await window.api.saveConfig({ hosters });
await window.api.saveHosterSettings(newHosterSettings);
await window.api.saveGlobalSettings(globalSettings);
config = await window.api.getConfig();
hosterSettings = config.hosterSettings || {};
syncSelectedUploadHosters();
renderHosterSummary();
renderHosterModal();
renderSettings();
renderHealthCheckResults([]);
const feedback = document.getElementById('saveFeedback');
feedback.textContent = 'Gespeichert!';
setTimeout(() => { feedback.textContent = ''; }, 2000);
}
// --- Accounts ---
function getAccountsWithCreds() {
return HOSTERS
.map(name => ({ name, hoster: config.hosters[name] || {} }))
.filter(item => hosterHasCredentials(item.name, item.hoster));
}
function getHostersWithoutCreds() {
return HOSTERS.filter(name => !hosterHasCredentials(name, config.hosters[name] || {}));
}
function getCredentialLabel(name, hoster) {
if (name === 'vidmoly.me') return hoster.username || 'Login';
if (name === 'voe.sx') return hoster.username && hoster.password ? (hoster.username || 'Login') : 'API-Key';
return 'API-Key';
}
function renderAccounts() {
const container = document.getElementById('accountsList');
if (!container) return;
const accounts = getAccountsWithCreds();
if (accounts.length === 0) {
container.innerHTML = `
<div class="accounts-empty">
<p>Keine Accounts vorhanden</p>
<span class="hint">Klicke auf "Account hinzufuegen" um einen Hoster einzurichten.</span>
</div>`;
return;
}
container.innerHTML = accounts.map(({ name, hoster }) => {
const st = accountStatuses[name] || { status: 'unchecked', message: '' };
const statusLabels = { ok: 'Bereit', checking: 'Pruefe...', error: 'Fehler', unchecked: 'Nicht geprueft' };
const statusLabel = statusLabels[st.status] || 'Nicht geprueft';
const credLabel = getCredentialLabel(name, hoster);
return `
<div class="account-card" data-account="${name}">
<div class="account-card-info">
<div class="account-card-title">${escapeHtml(name)}</div>
<div class="account-card-subtitle" title="${escapeAttr(credLabel)}">${escapeHtml(credLabel)}${st.status === 'error' && st.message ? ' — ' + escapeHtml(st.message) : ''}</div>
</div>
<span class="account-status status-${st.status}">
<span class="account-status-dot"></span>
${statusLabel}
</span>
<div class="account-card-actions">
<button class="btn btn-xs btn-secondary" data-account-check="${name}">Pruefen</button>
<button class="btn btn-xs btn-secondary" data-account-edit="${name}">Bearbeiten</button>
<button class="btn btn-xs btn-danger" data-account-delete="${name}">Loeschen</button>
</div>
</div>`;
}).join('');
// Wire up buttons
container.querySelectorAll('[data-account-edit]').forEach(btn => {
btn.addEventListener('click', () => openAccountModal(btn.dataset.accountEdit));
});
container.querySelectorAll('[data-account-delete]').forEach(btn => {
btn.addEventListener('click', () => openDeleteAccountModal(btn.dataset.accountDelete));
});
container.querySelectorAll('[data-account-check]').forEach(btn => {
btn.addEventListener('click', () => checkSingleAccount(btn.dataset.accountCheck));
});
}
async function checkSingleAccount(hosterName) {
accountStatuses[hosterName] = { status: 'checking', message: '' };
renderAccounts();
try {
const rows = await executeHealthCheck([hosterName], 'auto');
const row = rows.find(r => r.hoster === hosterName);
if (row && row.status === 'ok') {
accountStatuses[hosterName] = { status: 'ok', message: row.message || '' };
} else {
accountStatuses[hosterName] = { status: 'error', message: (row && row.message) || 'Pruefung fehlgeschlagen' };
}
} catch (err) {
accountStatuses[hosterName] = { status: 'error', message: err.message || 'Pruefung fehlgeschlagen' };
}
renderAccounts();
}
function getCredsFieldsHtml(name, hoster) {
hoster = hoster || {};
if (name === 'vidmoly.me') {
return `
<div class="settings-row">
<label>Username</label>
<input type="text" class="key-input" id="accField_username" value="${escapeAttr(hoster.username || '')}" placeholder="Username">
</div>
<div class="settings-row">
<label>Passwort</label>
<input type="password" class="key-input" id="accField_password" value="${escapeAttr(hoster.password || '')}" placeholder="Passwort">
<button class="toggle-vis" type="button" title="Anzeigen">&#128065;</button>
</div>`;
}
if (name === 'voe.sx') {
return `
<div class="settings-row">
<label>E-Mail (Login)</label>
<input type="text" class="key-input" id="accField_username" value="${escapeAttr(hoster.username || '')}" placeholder="E-Mail fuer Login">
</div>
<div class="settings-row">
<label>Passwort (Login)</label>
<input type="password" class="key-input" id="accField_password" value="${escapeAttr(hoster.password || '')}" placeholder="Passwort fuer Login">
<button class="toggle-vis" type="button" title="Anzeigen">&#128065;</button>
</div>
<div class="settings-row">
<label>API Key (optional)</label>
<input type="password" class="key-input" id="accField_apiKey" value="${escapeAttr(hoster.apiKey || '')}" placeholder="API Key (Fallback)">
<button class="toggle-vis" type="button" title="Anzeigen">&#128065;</button>
</div>
<p class="hint" style="margin:4px 0 0;opacity:0.6">Login wird bevorzugt. API-Key nur als Fallback.</p>`;
}
// Default: API key only
return `
<div class="settings-row">
<label>API Key</label>
<input type="password" class="key-input" id="accField_apiKey" value="${escapeAttr(hoster.apiKey || '')}" placeholder="API Key">
<button class="toggle-vis" type="button" title="Anzeigen">&#128065;</button>
</div>`;
}
function openAccountModal(editHoster) {
editingAccountHoster = editHoster || null;
const modal = document.getElementById('accountModal');
const title = document.getElementById('accountModalTitle');
const subtitle = document.getElementById('accountModalSubtitle');
const hosterRow = document.getElementById('accountHosterRow');
const hosterSelect = document.getElementById('accountHosterSelect');
const credsContainer = document.getElementById('accountCredsFields');
const statusEl = document.getElementById('accountModalStatus');
const saveBtn = document.getElementById('saveAccountBtn');
statusEl.textContent = '';
statusEl.className = 'account-modal-status';
if (editingAccountHoster) {
// Edit mode
title.textContent = 'Account bearbeiten';
subtitle.textContent = `Zugangsdaten fuer ${editingAccountHoster} bearbeiten.`;
hosterRow.style.display = 'none';
saveBtn.textContent = 'Speichern & Pruefen';
const hoster = config.hosters[editingAccountHoster] || {};
credsContainer.innerHTML = getCredsFieldsHtml(editingAccountHoster, hoster);
} else {
// Add mode
title.textContent = 'Account hinzufuegen';
subtitle.textContent = 'Waehle einen Hoster und gib deine Zugangsdaten ein.';
hosterRow.style.display = 'flex';
saveBtn.textContent = 'Anlegen & Pruefen';
const available = getHostersWithoutCreds();
if (available.length === 0) {
hosterSelect.innerHTML = '<option value="">Alle Hoster bereits eingerichtet</option>';
credsContainer.innerHTML = '';
} else {
hosterSelect.innerHTML = available.map(name => `<option value="${name}">${name}</option>`).join('');
credsContainer.innerHTML = getCredsFieldsHtml(available[0], {});
}
}
// Toggle visibility buttons
credsContainer.querySelectorAll('.toggle-vis').forEach(btn => {
btn.addEventListener('click', () => {
const input = btn.previousElementSibling;
input.type = input.type === 'password' ? 'text' : 'password';
});
});
modal.style.display = 'flex';
}
function closeAccountModal() {
document.getElementById('accountModal').style.display = 'none';
editingAccountHoster = null;
}
function openDeleteAccountModal(hosterName) {
const modal = document.getElementById('deleteAccountModal');
const msg = document.getElementById('deleteAccountMessage');
msg.textContent = `Account fuer "${hosterName}" wirklich loeschen? Alle Zugangsdaten werden entfernt.`;
modal.dataset.hoster = hosterName;
modal.style.display = 'flex';
}
function closeDeleteModal() {
document.getElementById('deleteAccountModal').style.display = 'none';
}
async function deleteAccount(hosterName) {
const hosters = { ...config.hosters };
// Reset credentials to defaults
if (hosterName === 'vidmoly.me') {
hosters[hosterName] = { enabled: false, authType: 'login', username: '', password: '' };
} else if (hosterName === 'voe.sx') {
hosters[hosterName] = { enabled: false, username: '', password: '', apiKey: '' };
} else {
hosters[hosterName] = { enabled: false, apiKey: '' };
}
delete accountStatuses[hosterName];
await window.api.saveConfig({ hosters });
config = await window.api.getConfig();
syncSelectedUploadHosters();
renderAccounts();
renderHosterSummary();
renderHosterModal();
renderSettings();
closeDeleteModal();
}
function readAccountCredsFromModal(hosterName) {
if (hosterName === 'vidmoly.me') {
const username = (document.getElementById('accField_username')?.value || '').trim();
const password = (document.getElementById('accField_password')?.value || '').trim();
return { enabled: !!(username && password), authType: 'login', username, password };
}
if (hosterName === 'voe.sx') {
const username = (document.getElementById('accField_username')?.value || '').trim();
const password = (document.getElementById('accField_password')?.value || '').trim();
const apiKey = (document.getElementById('accField_apiKey')?.value || '').trim();
return { enabled: !!(username && password) || !!apiKey, username, password, apiKey };
}
const apiKey = (document.getElementById('accField_apiKey')?.value || '').trim();
return { enabled: !!apiKey, apiKey };
}
async function saveAccount() {
const hosterName = editingAccountHoster || document.getElementById('accountHosterSelect')?.value;
if (!hosterName) return;
const creds = readAccountCredsFromModal(hosterName);
if (!creds.enabled) {
const statusEl = document.getElementById('accountModalStatus');
statusEl.textContent = 'Bitte Zugangsdaten eingeben.';
statusEl.className = 'account-modal-status error';
return;
}
// Save credentials
const hosters = { ...config.hosters };
hosters[hosterName] = creds;
await window.api.saveConfig({ hosters });
config = await window.api.getConfig();
// Show checking status
const statusEl = document.getElementById('accountModalStatus');
const saveBtn = document.getElementById('saveAccountBtn');
statusEl.textContent = 'Pruefe Login...';
statusEl.className = 'account-modal-status checking';
saveBtn.disabled = true;
accountStatuses[hosterName] = { status: 'checking', message: '' };
syncSelectedUploadHosters();
renderAccounts();
renderHosterSummary();
renderHosterModal();
renderSettings();
// Run health check
try {
const rows = await executeHealthCheck([hosterName], 'auto');
const row = rows.find(r => r.hoster === hosterName);
if (row && row.status === 'ok') {
accountStatuses[hosterName] = { status: 'ok', message: row.message || '' };
statusEl.textContent = 'Login erfolgreich!';
statusEl.className = 'account-modal-status ok';
setTimeout(() => closeAccountModal(), 1200);
} else {
const msg = (row && row.message) || 'Login fehlgeschlagen';
accountStatuses[hosterName] = { status: 'error', message: msg };
statusEl.textContent = msg;
statusEl.className = 'account-modal-status error';
}
} catch (err) {
accountStatuses[hosterName] = { status: 'error', message: err.message || 'Pruefung fehlgeschlagen' };
statusEl.textContent = err.message || 'Pruefung fehlgeschlagen';
statusEl.className = 'account-modal-status error';
} finally {
saveBtn.disabled = false;
renderAccounts();
}
}
// --- History ---
async function loadHistory() {
const history = await window.api.getHistory();
@ -1250,6 +1470,41 @@ function setupListeners() {
document.getElementById('hosterModal').addEventListener('click', (e) => {
if (e.target.id === 'hosterModal') closeHosterModal();
});
// Account management
document.getElementById('addAccountBtn').addEventListener('click', () => openAccountModal(null));
document.getElementById('closeAccountModalBtn').addEventListener('click', closeAccountModal);
document.getElementById('cancelAccountModalBtn').addEventListener('click', closeAccountModal);
document.getElementById('saveAccountBtn').addEventListener('click', saveAccount);
document.getElementById('accountModal').addEventListener('click', (e) => {
if (e.target.id === 'accountModal') closeAccountModal();
});
// Account hoster select change → update credential fields
document.getElementById('accountHosterSelect').addEventListener('change', (e) => {
const credsContainer = document.getElementById('accountCredsFields');
credsContainer.innerHTML = getCredsFieldsHtml(e.target.value, {});
credsContainer.querySelectorAll('.toggle-vis').forEach(btn => {
btn.addEventListener('click', () => {
const input = btn.previousElementSibling;
input.type = input.type === 'password' ? 'text' : 'password';
});
});
document.getElementById('accountModalStatus').textContent = '';
document.getElementById('accountModalStatus').className = 'account-modal-status';
});
// Delete account modal
document.getElementById('closeDeleteModalBtn').addEventListener('click', closeDeleteModal);
document.getElementById('cancelDeleteBtn').addEventListener('click', closeDeleteModal);
document.getElementById('confirmDeleteBtn').addEventListener('click', () => {
const modal = document.getElementById('deleteAccountModal');
const hoster = modal.dataset.hoster;
if (hoster) deleteAccount(hoster);
});
document.getElementById('deleteAccountModal').addEventListener('click', (e) => {
if (e.target.id === 'deleteAccountModal') closeDeleteModal();
});
}
// --- Update UI ---

View File

@ -9,6 +9,7 @@
<body>
<nav class="tab-bar">
<button class="tab active" data-view="upload">Upload</button>
<button class="tab" data-view="accounts">Accounts</button>
<button class="tab" data-view="settings">Einstellungen</button>
<button class="tab" data-view="history">Verlauf</button>
<span class="version-label" id="versionLabel"></span>
@ -101,11 +102,67 @@
</div>
</div>
<!-- Accounts View -->
<div id="accounts-view" class="view">
<div class="accounts-container">
<div class="accounts-header">
<div>
<h2>Accounts</h2>
<p class="settings-hint">Hoster-Zugangsdaten verwalten</p>
</div>
<button class="btn btn-primary" id="addAccountBtn">+ Account hinzufuegen</button>
</div>
<div class="accounts-list" id="accountsList"></div>
</div>
</div>
<!-- Account Modal -->
<div class="modal-overlay" id="accountModal" style="display:none">
<div class="modal-card">
<div class="modal-header">
<div>
<h3 id="accountModalTitle">Account hinzufuegen</h3>
<p id="accountModalSubtitle">Waehle einen Hoster und gib deine Zugangsdaten ein.</p>
</div>
<button class="icon-btn" id="closeAccountModalBtn" aria-label="Schliessen">&times;</button>
</div>
<div class="modal-body">
<div class="settings-row" id="accountHosterRow">
<label>Hoster</label>
<select class="key-input" id="accountHosterSelect" style="max-width:300px"></select>
</div>
<div id="accountCredsFields"></div>
<div class="account-modal-status" id="accountModalStatus"></div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancelAccountModalBtn">Abbrechen</button>
<button class="btn btn-primary" id="saveAccountBtn">Anlegen & Pruefen</button>
</div>
</div>
</div>
<!-- Delete Confirm Modal -->
<div class="modal-overlay" id="deleteAccountModal" style="display:none">
<div class="modal-card" style="width:min(400px,100%)">
<div class="modal-header">
<div><h3>Account loeschen?</h3></div>
<button class="icon-btn" id="closeDeleteModalBtn" aria-label="Schliessen">&times;</button>
</div>
<div class="modal-body">
<p id="deleteAccountMessage">Account wirklich loeschen?</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancelDeleteBtn">Abbrechen</button>
<button class="btn btn-danger" id="confirmDeleteBtn">Loeschen</button>
</div>
</div>
</div>
<!-- Settings View -->
<div id="settings-view" class="view">
<div class="settings-container">
<h2>Hoster Konfiguration</h2>
<p class="settings-hint">API-Keys und Upload-Einstellungen pro Hoster.</p>
<h2>Upload Einstellungen</h2>
<p class="settings-hint">Upload-Einstellungen pro Hoster. Zugangsdaten werden im Accounts-Tab verwaltet.</p>
<div class="settings-hosters" id="settingsHosters"></div>
<button class="btn btn-primary" id="saveSettingsBtn">Alles Speichern</button>
<span class="save-feedback" id="saveFeedback"></span>

View File

@ -577,6 +577,71 @@ body {
gap: 4px 16px;
}
/* Accounts View */
.accounts-container { padding: 16px; overflow: auto; flex: 1; background: linear-gradient(180deg, rgba(255,255,255,0.015), transparent 24%); }
.accounts-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 16px; }
.accounts-header h2 { font-size: 18px; margin-bottom: 2px; }
.accounts-list { display: grid; gap: 8px; }
.account-card {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 16px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 10px;
transition: background 0.2s;
}
.account-card:hover { background: var(--bg-card-hover); }
.account-card-info { flex: 1; min-width: 0; }
.account-card-title { font-size: 14px; font-weight: 600; }
.account-card-subtitle { font-size: 11px; color: var(--text-muted); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.account-card-actions { display: flex; gap: 6px; flex-shrink: 0; }
.account-status {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
padding: 3px 10px;
border-radius: 4px;
font-weight: 600;
flex-shrink: 0;
}
.account-status.status-ok { background: rgba(0, 184, 148, 0.2); color: var(--success); }
.account-status.status-checking { background: rgba(253, 203, 110, 0.2); color: var(--warning); }
.account-status.status-error { background: rgba(231, 76, 60, 0.2); color: var(--danger); }
.account-status.status-unchecked { background: rgba(255, 255, 255, 0.05); color: var(--text-dim); }
.account-status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
display: inline-block;
}
.status-ok .account-status-dot { background: var(--success); }
.status-checking .account-status-dot { background: var(--warning); }
.status-error .account-status-dot { background: var(--danger); }
.status-unchecked .account-status-dot { background: var(--text-dim); }
.account-modal-status {
margin-top: 12px;
font-size: 12px;
min-height: 20px;
}
.account-modal-status.checking { color: var(--warning); }
.account-modal-status.ok { color: var(--success); }
.account-modal-status.error { color: var(--danger); }
.accounts-empty {
text-align: center;
padding: 48px 16px;
color: var(--text-dim);
}
.accounts-empty p { font-size: 14px; margin-bottom: 4px; }
.accounts-empty .hint { font-size: 12px; }
/* History View */
.history-container { padding: 16px; overflow: auto; flex: 1; background: linear-gradient(180deg, rgba(255,255,255,0.015), transparent 24%); }
.history-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }