release: Multi-Hoster-Upload v2.0.4

This commit is contained in:
Sucukdeluxe
2026-08-09 15:52:23 +02:00
parent 1c4d096cb9
commit 48891503ad
10 changed files with 184 additions and 29 deletions
+36
View File
@@ -0,0 +1,36 @@
(function (scope) {
const STATUS_PRESENTATIONS = {
ok: { statusClass: 'ok', label: 'Bereit', requiresOtp: false },
warn: { statusClass: 'warn', label: 'Warnung', requiresOtp: false },
checking: { statusClass: 'checking', label: 'Prüfe...', requiresOtp: false },
error: { statusClass: 'error', label: 'Fehler', requiresOtp: false },
otp_required: { statusClass: 'warn', label: 'OTP erforderlich', requiresOtp: true },
unchecked: { statusClass: 'unchecked', label: 'Nicht geprüft', requiresOtp: false },
disabled: { statusClass: 'disabled', label: 'Deaktiviert', requiresOtp: false }
};
function getAccountStatusPresentation(status) {
const presentation = STATUS_PRESENTATIONS[status] || STATUS_PRESENTATIONS.unchecked;
return { ...presentation };
}
function getAccountGroupStatus(summary) {
const total = Math.max(0, Number(summary && summary.total) || 0);
const disabled = Math.min(total, Math.max(0, Number(summary && summary.disabled) || 0));
const active = total - disabled;
const errors = Math.max(0, Number(summary && summary.error) || 0);
const ok = Math.max(0, Number(summary && summary.ok) || 0);
const warning = Math.max(0, Number(summary && summary.warn) || 0);
const checking = Math.max(0, Number(summary && summary.checking) || 0);
const unchecked = Math.max(0, Number(summary && summary.unchecked) || 0);
if (active === 0) return 'unchecked';
if (errors >= active) return 'error';
if (errors > 0 || warning > 0 || checking > 0 || unchecked > 0) return 'warn';
if (ok >= active) return 'ok';
return 'warn';
}
const accountStatus = { getAccountGroupStatus, getAccountStatusPresentation };
if (typeof module !== 'undefined' && module.exports) module.exports = accountStatus;
if (scope) scope.AccountStatus = accountStatus;
})(typeof window !== 'undefined' ? window : globalThis);
+2 -2
View File
@@ -1,6 +1,6 @@
(function (scope) {
function getAccountSubmitLabel({ isEdit } = {}) {
return isEdit ? 'Prüfen und speichern' : 'Prüfen und anlegen';
function getAccountSubmitLabel() {
return 'Prüfen und speichern';
}
async function submitValidatedAccount({ validate, commit, afterCommit, isCurrent }) {
+57 -15
View File
@@ -3920,13 +3920,12 @@ function getCredentialLabel(name, account) {
return 'Keine Zugangsdaten';
}
const _STATUS_LABELS = { ok: 'Bereit', warn: 'Warnung', checking: 'Prüfe...', error: 'Fehler', unchecked: 'Nicht geprüft' };
function _buildAccountCardHtml(name, account, idx) {
const isDisabled = account.enabled === false;
const st = accountStatuses[account.id] || { status: 'unchecked', message: '' };
const statusLabel = isDisabled ? 'Deaktiviert' : (_STATUS_LABELS[st.status] || 'Nicht geprüft');
const statusClass = isDisabled ? 'disabled' : st.status;
const statusPresentation = window.AccountStatus.getAccountStatusPresentation(isDisabled ? 'disabled' : st.status);
const statusLabel = statusPresentation.label;
const statusClass = statusPresentation.statusClass;
const credLabel = getCredentialLabel(name, account);
const userLabel = account.label && String(account.label).trim();
// Subtitle: "Label: XYZ • API: ABC… • <status>" — the user-set label is the
@@ -3940,6 +3939,12 @@ function _buildAccountCardHtml(name, account, idx) {
const sessionPausedBadge = isSessionPaused
? `<span class="account-session-paused" title="Account wurde diese Session als fehlerhaft markiert. Klick = Wieder als aktiv markieren.">Pausiert (Session) <button class="account-session-reactivate" data-account-reactivate="${account.id}" data-account-reactivate-hoster="${name}" title="Wieder aktivieren"></button></span>`
: '';
const otpAction = !isDisabled && statusPresentation.requiresOtp
? `<div class="account-otp-action">
<input class="key-input account-otp-input" data-account-otp-input="${escapeAttr(account.id)}" type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="10" placeholder="Code aus E-Mail">
<button class="btn btn-xs btn-primary" data-account-otp-submit="${escapeAttr(account.id)}">Prüfen und speichern</button>
</div>`
: '';
return `
<div class="account-card${isDisabled ? ' account-disabled' : ''}${isSessionPaused ? ' account-session-paused-card' : ''}" data-account-id="${account.id}" data-account-hoster="${name}" draggable="true">
@@ -3947,6 +3952,7 @@ function _buildAccountCardHtml(name, account, idx) {
<div class="account-card-info">
<div class="account-card-title">${escapeHtml(getAccountDisplayName(name, account))} <span class="account-priority-badge">${priorityLabel}</span> ${sessionPausedBadge}</div>
<div class="account-card-subtitle" title="${escapeAttr(subtitleText)}">${escapeHtml(subtitleText)}${st.message && !isDisabled ? `${escapeHtml(st.message)}` : ''}</div>
${otpAction}
</div>
<span class="account-status status-${statusClass}">
<span class="account-status-dot"></span>
@@ -3987,10 +3993,7 @@ function _refreshHosterGroupHeader(name) {
if (!group) return;
const accounts = config.hosters[name] || [];
const summary = _summarizeHosterGroup(accounts);
let dot = 'unchecked';
if (summary.error > 0) dot = 'error';
else if (summary.checking > 0) dot = 'checking';
else if (summary.ok > 0 && summary.unchecked === 0) dot = 'ok';
const dot = window.AccountStatus.getAccountGroupStatus(summary);
const dotEl = group.querySelector('.account-hoster-group-header .account-status-dot');
if (dotEl) dotEl.className = `account-status-dot status-${dot}`;
const countEl = group.querySelector('.account-hoster-group-count');
@@ -4060,16 +4063,17 @@ function renderAccounts() {
}
function _summarizeHosterGroup(accounts) {
let ok = 0, error = 0, checking = 0, unchecked = 0, disabled = 0;
let ok = 0, warn = 0, error = 0, checking = 0, unchecked = 0, disabled = 0;
for (const a of accounts) {
if (a.enabled === false) { disabled++; continue; }
const s = (accountStatuses[a.id] && accountStatuses[a.id].status) || 'unchecked';
if (s === 'ok' || s === 'warn') ok++;
if (s === 'ok') ok++;
else if (s === 'warn' || s === 'otp_required') warn++;
else if (s === 'error') error++;
else if (s === 'checking') checking++;
else unchecked++;
}
return { ok, error, checking, unchecked, disabled, total: accounts.length };
return { ok, warn, error, checking, unchecked, disabled, total: accounts.length };
}
function _hosterGroupOpenState(name, summary) {
@@ -4192,10 +4196,7 @@ function _buildHosterSettingsHtml(name) {
function _buildAccountHosterGroupHtml(name, accounts) {
const summary = _summarizeHosterGroup(accounts);
const isOpen = _hosterGroupOpenState(name, summary);
let dot = 'unchecked';
if (summary.error > 0) dot = 'error';
else if (summary.checking > 0) dot = 'checking';
else if (summary.ok > 0 && summary.unchecked === 0) dot = 'ok';
const dot = window.AccountStatus.getAccountGroupStatus(summary);
const countLabel = `${summary.ok}/${summary.total}`;
const arrow = isOpen ? '&#9660;' : '&#9654;';
let cardsHtml = '';
@@ -4299,6 +4300,7 @@ function bindAccountListeners(container) {
if (btn.dataset.accountEdit) return openAccountModal(btn.dataset.accountEdit);
if (btn.dataset.accountDelete) return openDeleteAccountModal(btn.dataset.accountDelete);
if (btn.dataset.accountCheck) return checkSingleAccount(btn.dataset.accountCheck);
if (btn.dataset.accountOtpSubmit) return submitAccountOtp(btn.dataset.accountOtpSubmit);
if (btn.dataset.accountReactivate) {
const accountId = btn.dataset.accountReactivate;
const hoster = btn.dataset.accountReactivateHoster;
@@ -4413,6 +4415,46 @@ async function checkSingleAccount(accountId) {
updateAccountCard(accountId);
}
async function submitAccountOtp(accountId) {
if (!accountId || healthCheckRunning) return;
const found = findAccountById(accountId);
if (!found || found.account.enabled === false) return;
const card = Array.from(document.querySelectorAll('.account-card')).find(el => el.dataset.accountId === accountId);
const otpInput = card?.querySelector('[data-account-otp-input]');
const otp = otpInput?.value.trim() || '';
if (otpInput) otpInput.setCustomValidity('');
if (!otp) {
if (otpInput) {
otpInput.setCustomValidity('Bitte den OTP-Code eingeben.');
otpInput.reportValidity();
}
return;
}
const submitButton = card?.querySelector('[data-account-otp-submit]');
healthCheckRunning = true;
accountStatuses[accountId] = { status: 'checking', message: 'OTP wird geprüft…' };
if (otpInput) otpInput.disabled = true;
if (submitButton) {
submitButton.disabled = true;
submitButton.textContent = 'Prüfe…';
}
try {
const result = await window.api.runHealthCheck({ hosters: [{ hoster: found.name, accountId, otp }] });
const row = result && Array.isArray(result.results)
? result.results.find(item => item.accountId === accountId)
: null;
accountStatuses[accountId] = row
? { status: row.status || 'error', message: row.message || '' }
: { status: 'error', message: 'Keine Antwort vom Hoster erhalten' };
} catch (err) {
accountStatuses[accountId] = { status: 'error', message: err.message || 'OTP-Prüfung fehlgeschlagen' };
} finally {
healthCheckRunning = false;
updateAccountCard(accountId);
renderHosterModal();
}
}
// Per-hoster overrides for the login form. VOE only accepts emails — the
// generic "Username / E-Mail" label sent users down a confusing rabbit hole
// (login fails → upload fetches login redirect → "CSRF token nicht gefunden").
+1
View File
@@ -425,6 +425,7 @@
<script src="../lib/throttle-timer.js"></script>
<script src="../lib/serialized-runner.js"></script>
<script src="account-submit.js"></script>
<script src="account-status.js"></script>
<script src="app.js"></script>
</body>
</html>
+3
View File
@@ -1004,6 +1004,8 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
.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-otp-action { display: flex; align-items: center; gap: 6px; margin-top: 8px; }
.account-otp-input { width: 150px; min-height: 28px; padding: 5px 8px; font-size: 11px; }
.account-card-actions { display: flex; gap: 6px; flex-shrink: 0; }
.account-status {
@@ -1184,6 +1186,7 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
.account-hoster-group .account-card:last-child { margin-bottom: 0; }
.account-status-dot.status-ok { background: #4caf50; }
.account-status-dot.status-error { background: #e57373; }
.account-status-dot.status-warn { background: #f0c36c; }
.account-status-dot.status-checking { background: #f0c36c; }
.account-status-dot.status-unchecked { background: #6c757d; }
.account-hoster-group-header .account-status-dot {