refactor: Phase 1 code cleanup - constants, logging, and repository organization
This commit is contained in:
@@ -58,6 +58,23 @@
|
||||
style="width: 100%; padding: 8px 10px; background: var(--bg); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; font-size: 0.85rem; font-family: monospace; box-sizing: border-box;" />
|
||||
</div>
|
||||
|
||||
<!-- Quality Profile (shown for radarr/sonarr only) -->
|
||||
<div id="svc-creds-quality" style="display: none; margin-bottom: 14px;">
|
||||
<label class="label-bold">Quality Profile</label>
|
||||
<p class="hint-micro">Used when requesting via Seerr</p>
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<select id="svc-quality-select"
|
||||
style="flex: 1; padding: 8px 10px; background: var(--bg); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; font-size: 0.85rem; box-sizing: border-box;">
|
||||
<option value="">-- Enter API key first --</option>
|
||||
</select>
|
||||
<button id="svc-quality-fetch" type="button"
|
||||
style="padding: 8px 12px; font-size: 0.75rem; cursor: pointer; white-space: nowrap;">
|
||||
Fetch
|
||||
</button>
|
||||
</div>
|
||||
<div id="svc-quality-status" style="font-size: 0.75rem; margin-top: 4px; min-height: 1em;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Per-service Basic Auth (shown for non-external services) -->
|
||||
<div id="svc-creds-basic" style="display: none; margin-bottom: 14px;">
|
||||
<label class="label-bold">Service Login</label>
|
||||
@@ -67,9 +84,12 @@
|
||||
class="input-creds" />
|
||||
</div>
|
||||
|
||||
<!-- Error message -->
|
||||
<div id="svc-creds-error" style="display: none; padding: 8px 10px; margin-bottom: 10px; background: color-mix(in srgb, var(--error, #c62828) 12%, transparent); border: 1px solid var(--error, #c62828); border-radius: 6px; font-size: 0.8rem; color: var(--error, #c62828);"></div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div style="display: flex; gap: 8px; margin-top: 14px;">
|
||||
<button id="svc-creds-save" style="flex: 1; padding: 9px; background: var(--accent, #8FD6FF); color: #0b0f1a; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.85rem;">
|
||||
<button id="svc-creds-save" class="btn-accent-solid" style="flex: 1; padding: 9px; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.85rem;">
|
||||
Save
|
||||
</button>
|
||||
<button id="svc-creds-clear" style="padding: 9px 14px; background: transparent; color: var(--bad-fg, #ff9aa3); border: 1px solid var(--bad-fg, #ff9aa3); border-radius: 6px; cursor: pointer; font-size: 0.85rem; display: none;">
|
||||
@@ -85,24 +105,50 @@
|
||||
const modal = document.getElementById('service-creds-modal');
|
||||
let currentService = null;
|
||||
const arrServices = ['sonarr', 'radarr', 'prowlarr', 'overseerr'];
|
||||
const qualityProfileServices = ['sonarr', 'radarr'];
|
||||
|
||||
function getServiceUrl(service) {
|
||||
return service.externalUrl || service.url || '';
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
const el = document.getElementById('svc-creds-error');
|
||||
el.textContent = msg;
|
||||
el.style.display = '';
|
||||
}
|
||||
|
||||
function hideError() {
|
||||
const el = document.getElementById('svc-creds-error');
|
||||
el.textContent = '';
|
||||
el.style.display = 'none';
|
||||
}
|
||||
|
||||
window.openServiceCredsModal = async function(service) {
|
||||
currentService = service;
|
||||
hideError();
|
||||
const title = document.getElementById('svc-creds-title');
|
||||
const desc = document.getElementById('svc-creds-desc');
|
||||
const seedhostSection = document.getElementById('svc-creds-seedhost');
|
||||
const apikeySection = document.getElementById('svc-creds-apikey');
|
||||
const basicSection = document.getElementById('svc-creds-basic');
|
||||
const qualitySection = document.getElementById('svc-creds-quality');
|
||||
|
||||
title.textContent = service.name + ' Credentials';
|
||||
// Determine which sections to show
|
||||
const isExt = !!service.isExternal;
|
||||
const isArr = arrServices.includes(service.id) || arrServices.includes(service.appTemplate);
|
||||
const hasQuality = qualityProfileServices.includes(service.id) || qualityProfileServices.includes(service.appTemplate);
|
||||
|
||||
seedhostSection.style.display = isExt ? '' : 'none';
|
||||
apikeySection.style.display = isArr ? '' : 'none';
|
||||
qualitySection.style.display = hasQuality ? '' : 'none';
|
||||
basicSection.style.display = !isExt ? '' : 'none';
|
||||
|
||||
// Reset quality dropdown
|
||||
const qualSelect = document.getElementById('svc-quality-select');
|
||||
qualSelect.innerHTML = '<option value="">-- Enter API key first --</option>';
|
||||
document.getElementById('svc-quality-status').textContent = '';
|
||||
|
||||
if (isExt) {
|
||||
desc.textContent = 'Seedhost credentials auto-login past the HTTP prompt. API key bypasses the app login.';
|
||||
// Update password placeholder with service name
|
||||
@@ -160,6 +206,12 @@
|
||||
} catch (e) { /* ignore */ }
|
||||
if (document.getElementById('svc-basic-pass')) document.getElementById('svc-basic-pass').value = '';
|
||||
|
||||
// Load quality profile for arr services
|
||||
const svcId = service.id || service.appTemplate;
|
||||
if (qualityProfileServices.includes(svcId)) {
|
||||
await loadQualityProfiles(service);
|
||||
}
|
||||
|
||||
if (hasCreds) {
|
||||
dot.style.background = 'var(--ok-fg, #74dfc4)';
|
||||
status.style.color = 'var(--ok-fg, #74dfc4)';
|
||||
@@ -176,14 +228,129 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch and populate quality profiles dropdown
|
||||
async function loadQualityProfiles(service) {
|
||||
const qualSelect = document.getElementById('svc-quality-select');
|
||||
const qualStatus = document.getElementById('svc-quality-status');
|
||||
const svcId = service.id || service.appTemplate;
|
||||
const svcUrl = getServiceUrl(service);
|
||||
|
||||
if (!svcUrl) {
|
||||
qualSelect.innerHTML = '<option value="">-- No service URL --</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
qualSelect.innerHTML = '<option value="">Loading...</option>';
|
||||
qualStatus.textContent = '';
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({ service: svcId, url: svcUrl });
|
||||
const res = await fetch(`/api/v1/arr/quality-profiles?${params}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.success || !data.profiles?.length) {
|
||||
qualSelect.innerHTML = '<option value="">-- No profiles found (enter API key and click Fetch) --</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
qualSelect.innerHTML = '';
|
||||
for (const p of data.profiles) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = p.id;
|
||||
opt.textContent = p.name;
|
||||
qualSelect.appendChild(opt);
|
||||
}
|
||||
|
||||
// Pre-select stored profile or best match for "720"
|
||||
if (data.storedProfileId) {
|
||||
qualSelect.value = String(data.storedProfileId);
|
||||
}
|
||||
if (!qualSelect.value) {
|
||||
// Try to find a 720p-ish profile
|
||||
const match720 = data.profiles.find(p => /720/i.test(p.name));
|
||||
if (match720) qualSelect.value = String(match720.id);
|
||||
}
|
||||
if (!qualSelect.value && data.profiles.length) {
|
||||
qualSelect.value = String(data.profiles[0].id);
|
||||
}
|
||||
|
||||
qualStatus.innerHTML = `<span style="color: var(--ok-fg);">${data.profiles.length} profiles loaded</span>`;
|
||||
} catch (e) {
|
||||
qualSelect.innerHTML = '<option value="">-- Failed to load --</option>';
|
||||
qualStatus.innerHTML = `<span style="color: var(--error, #c62828);">Error: ${e.message}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch button for quality profiles
|
||||
document.getElementById('svc-quality-fetch')?.addEventListener('click', async () => {
|
||||
if (!currentService) return;
|
||||
const svcId = currentService.id || currentService.appTemplate;
|
||||
const svcUrl = getServiceUrl(currentService);
|
||||
const apiKeyInput = document.getElementById('svc-apikey-input');
|
||||
const apiKey = apiKeyInput?.value.trim();
|
||||
const qualSelect = document.getElementById('svc-quality-select');
|
||||
const qualStatus = document.getElementById('svc-quality-status');
|
||||
|
||||
if (!svcUrl) {
|
||||
qualStatus.innerHTML = '<span style="color: var(--error, #c62828);">No service URL available</span>';
|
||||
return;
|
||||
}
|
||||
if (!apiKey || apiKey === '••••••••') {
|
||||
qualStatus.innerHTML = '<span style="color: var(--error, #c62828);">Enter an API key first</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
qualSelect.innerHTML = '<option value="">Fetching...</option>';
|
||||
qualStatus.textContent = '';
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({ service: svcId, url: svcUrl, apiKey });
|
||||
const res = await fetch(`/api/v1/arr/quality-profiles?${params}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.success) {
|
||||
qualSelect.innerHTML = '<option value="">-- Error --</option>';
|
||||
qualStatus.innerHTML = `<span style="color: var(--error, #c62828);">${data.error || 'Failed to fetch profiles'}</span>`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.profiles?.length) {
|
||||
qualSelect.innerHTML = '<option value="">-- No profiles found --</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
qualSelect.innerHTML = '';
|
||||
for (const p of data.profiles) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = p.id;
|
||||
opt.textContent = p.name;
|
||||
qualSelect.appendChild(opt);
|
||||
}
|
||||
|
||||
// Pre-select 720p match
|
||||
const match720 = data.profiles.find(p => /720/i.test(p.name));
|
||||
if (match720) qualSelect.value = String(match720.id);
|
||||
else if (data.profiles.length) qualSelect.value = String(data.profiles[0].id);
|
||||
|
||||
qualStatus.innerHTML = `<span style="color: var(--ok-fg);">${data.profiles.length} profiles loaded</span>`;
|
||||
} catch (e) {
|
||||
qualSelect.innerHTML = '<option value="">-- Error --</option>';
|
||||
qualStatus.innerHTML = `<span style="color: var(--error, #c62828);">${e.message}</span>`;
|
||||
}
|
||||
});
|
||||
|
||||
// Save button
|
||||
document.getElementById('svc-creds-save')?.addEventListener('click', async () => {
|
||||
if (!currentService) return;
|
||||
const saveBtn = document.getElementById('svc-creds-save');
|
||||
saveBtn.textContent = 'Saving...';
|
||||
saveBtn.disabled = true;
|
||||
hideError();
|
||||
|
||||
try {
|
||||
const isArr = arrServices.includes(currentService.id) || arrServices.includes(currentService.appTemplate);
|
||||
const svcId = currentService.id || currentService.appTemplate;
|
||||
|
||||
// Save seedhost creds (shared username + per-service password)
|
||||
if (currentService.isExternal) {
|
||||
const user = document.getElementById('svc-seedhost-user').value.trim();
|
||||
@@ -197,15 +364,58 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Save API key
|
||||
// Save API key — for arr services, use the arr credentials endpoint (correct namespace)
|
||||
const apiKeyInput = document.getElementById('svc-apikey-input');
|
||||
const apiKey = apiKeyInput.value.trim();
|
||||
const apiKey = apiKeyInput?.value.trim();
|
||||
if (apiKey && apiKey !== '••••••••') {
|
||||
await secureFetch(`/api/v1/services/${currentService.id}/credentials`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ apiKey })
|
||||
});
|
||||
if (isArr) {
|
||||
// Use arr credentials endpoint — validates key, tests connection, stores in arr.* namespace
|
||||
const svcUrl = getServiceUrl(currentService);
|
||||
const qualSelect = document.getElementById('svc-quality-select');
|
||||
const qualityProfileId = qualSelect?.value ? parseInt(qualSelect.value) : undefined;
|
||||
const qualityProfileName = qualSelect?.selectedOptions?.[0]?.textContent || undefined;
|
||||
|
||||
const res = await secureFetch('/api/v1/arr/credentials', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
service: svcId,
|
||||
apiKey,
|
||||
url: svcUrl || undefined,
|
||||
qualityProfileId: qualityProfileId || undefined,
|
||||
qualityProfileName: qualityProfileName || undefined
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.success) {
|
||||
showError(data.error || 'Failed to save API key');
|
||||
saveBtn.textContent = 'Save';
|
||||
saveBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
if (data.connectionTest && !data.connectionTest.success) {
|
||||
showError(`API key saved but connection test failed: ${data.connectionTest.error}`);
|
||||
}
|
||||
} else {
|
||||
// Non-arr services use the generic endpoint
|
||||
await secureFetch(`/api/v1/services/${currentService.id}/credentials`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ apiKey })
|
||||
});
|
||||
}
|
||||
} else if (isArr && qualityProfileServices.includes(svcId)) {
|
||||
// API key unchanged but user may have changed quality profile — save profile only
|
||||
const qualSelect = document.getElementById('svc-quality-select');
|
||||
const qualityProfileId = qualSelect?.value ? parseInt(qualSelect.value) : undefined;
|
||||
const qualityProfileName = qualSelect?.selectedOptions?.[0]?.textContent || undefined;
|
||||
if (qualityProfileId) {
|
||||
await secureFetch('/api/v1/arr/quality-profiles', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ service: svcId, qualityProfileId, qualityProfileName })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Save per-service basic auth
|
||||
@@ -224,6 +434,7 @@
|
||||
await loadServiceCreds(currentService);
|
||||
} catch (e) {
|
||||
console.error('Failed to save credentials:', e);
|
||||
showError('Failed to save: ' + (e.message || 'Unknown error'));
|
||||
}
|
||||
saveBtn.textContent = 'Save';
|
||||
saveBtn.disabled = false;
|
||||
@@ -233,16 +444,24 @@
|
||||
document.getElementById('svc-creds-clear')?.addEventListener('click', async () => {
|
||||
if (!currentService) return;
|
||||
if (!confirm(`Remove stored credentials for ${currentService.name}?`)) return;
|
||||
hideError();
|
||||
try {
|
||||
const svcId = currentService.id || currentService.appTemplate;
|
||||
const isArr = arrServices.includes(svcId);
|
||||
if (currentService.isExternal) {
|
||||
await secureFetch(`/api/v1/seedhost-creds?serviceId=${currentService.id}`, { method: 'DELETE' });
|
||||
}
|
||||
// Delete from both namespaces
|
||||
await secureFetch(`/api/v1/services/${currentService.id}/credentials`, { method: 'DELETE' });
|
||||
if (isArr) {
|
||||
await secureFetch(`/api/v1/arr/credentials/${svcId}`, { method: 'DELETE' });
|
||||
}
|
||||
const btn = document.getElementById(`creds-btn-${currentService.id}`);
|
||||
if (btn) btn.classList.remove('has-creds');
|
||||
await loadServiceCreds(currentService);
|
||||
} catch (e) {
|
||||
console.error('Failed to clear credentials:', e);
|
||||
showError('Failed to clear: ' + (e.message || 'Unknown error'));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<!-- Setup Button (not configured state) -->
|
||||
<div id="totp-setup-section">
|
||||
<button id="totp-setup-btn" style="width: 100%; padding: 12px; background: var(--accent, #8FD6FF); color: #0b0f1a; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 0.95rem;">
|
||||
<button id="totp-setup-btn" class="btn-accent-solid" style="width: 100%; padding: 12px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 0.95rem;">
|
||||
Generate New Secret
|
||||
</button>
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin: 10px 0 0;">
|
||||
@@ -29,6 +29,7 @@
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
<div id="totp-import-error" style="color: var(--bad-fg, #ff9aa3); font-size: 0.8rem; min-height: 1.2em; margin-top: 6px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +56,7 @@
|
||||
<div style="display: flex; gap: 8px; margin-top: 8px;">
|
||||
<input type="text" id="totp-setup-code" maxlength="6" inputmode="numeric" pattern="[0-9]{6}" placeholder="000000"
|
||||
style="flex: 1; text-align: center; font-size: 1.3rem; padding: 10px; font-family: 'Sami Grotesk', monospace; letter-spacing: 4px; background: var(--bg); color: var(--fg); border: 2px solid var(--border); border-radius: 8px; outline: none;" />
|
||||
<button id="totp-confirm-setup" style="padding: 10px 20px; background: var(--accent, #8FD6FF); color: #0b0f1a; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
|
||||
<button id="totp-confirm-setup" class="btn-accent-solid" style="padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
@@ -191,7 +192,12 @@
|
||||
// Import existing secret button
|
||||
document.getElementById('totp-import-btn')?.addEventListener('click', async () => {
|
||||
const secret = document.getElementById('totp-import-key').value.trim();
|
||||
if (!secret) return;
|
||||
const errorEl = document.getElementById('totp-import-error');
|
||||
errorEl.textContent = '';
|
||||
if (!secret) {
|
||||
errorEl.textContent = 'Paste a Base32 secret key first';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await secureFetch('/api/v1/totp/setup', {
|
||||
method: 'POST',
|
||||
@@ -200,6 +206,7 @@
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
errorEl.textContent = '';
|
||||
document.getElementById('totp-qr-image').src = data.qrCode;
|
||||
document.getElementById('totp-manual-key').textContent = data.manualKey;
|
||||
document.getElementById('totp-setup-section').style.display = 'none';
|
||||
@@ -208,11 +215,10 @@
|
||||
document.getElementById('totp-setup-error').textContent = '';
|
||||
document.getElementById('totp-setup-code').focus();
|
||||
} else {
|
||||
document.getElementById('totp-import-key').style.borderColor = 'var(--bad-fg)';
|
||||
setTimeout(() => { document.getElementById('totp-import-key').style.borderColor = ''; }, 2000);
|
||||
errorEl.textContent = data.error || data.message || 'Import failed';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('TOTP import failed:', e);
|
||||
errorEl.textContent = 'Connection error — try refreshing the page';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user