Migrate frontend from monolithic app.js to vanilla ES modules
- Delete React files (App.jsx, main.jsx, App.css) - Create modular vanilla JS structure in client/src/: - state.js (global state object) - api.js (all fetch calls) - sse.js (SSE connection management) - ui/auth.js (authentication UI) - ui/downloads.js (downloads rendering) - ui/history.js (history section) - ui/statusPanel.js (status panel) - ui/webhooks.js (webhook management) - ui/filters.js (download client filter) - ui/theme.js (theme switching) - ui/tabs.js (tab navigation) - utils/format.js (formatting utilities) - utils/storage.js (localStorage helpers) - main.js (DOMContentLoaded bootstrap) - Update vite.config.js for vanilla build outputting to ../public/app.js - Build succeeds: 14 modules, 43.88 kB output
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||
|
||||
import { state } from './state.js';
|
||||
|
||||
export async function checkAuthentication() {
|
||||
try {
|
||||
// Fetch both auth state and a fresh CSRF token in parallel
|
||||
const [meRes, csrfRes] = await Promise.all([
|
||||
fetch('/api/auth/me'),
|
||||
fetch('/api/auth/csrf')
|
||||
]);
|
||||
const data = await meRes.json();
|
||||
const csrfData = await csrfRes.json();
|
||||
if (csrfData.csrfToken) state.csrfToken = csrfData.csrfToken;
|
||||
|
||||
if (data.authenticated) {
|
||||
state.currentUser = data.user;
|
||||
state.isAdmin = !!data.user.isAdmin;
|
||||
return { authenticated: true, user: data.user };
|
||||
} else {
|
||||
return { authenticated: false };
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Authentication check failed:', err);
|
||||
return { authenticated: false };
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleLogin(username, password, rememberMe) {
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ username, password, rememberMe })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
state.currentUser = data.user;
|
||||
state.isAdmin = !!data.user.isAdmin;
|
||||
// Store CSRF token returned by login for use in subsequent requests
|
||||
if (data.csrfToken) state.csrfToken = data.csrfToken;
|
||||
return { success: true, user: data.user };
|
||||
} else {
|
||||
return { success: false, error: data.error || 'Login failed' };
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return { success: false, error: 'Login failed. Please try again.' };
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleLogout() {
|
||||
try {
|
||||
await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
headers: csrfToken ? { 'X-CSRF-Token': csrfToken } : {}
|
||||
});
|
||||
currentUser = null;
|
||||
csrfToken = null;
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Logout failed:', err);
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadHistory(forceRefresh = false) {
|
||||
try {
|
||||
const params = new URLSearchParams({ days: state.historyDays });
|
||||
if (state.showAll) params.set('showAll', 'true');
|
||||
if (forceRefresh) params.set('_t', Date.now());
|
||||
const res = await fetch(`/api/history/recent?${params}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
return { success: true, history: data.history || [] };
|
||||
} catch (err) {
|
||||
console.error('[History] Load error:', err);
|
||||
return { success: false, error: 'Failed to load history.' };
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleBlocklistSearch(download) {
|
||||
try {
|
||||
const res = await fetch('/api/dashboard/blocklist-search', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
arrQueueId: download.arrQueueId,
|
||||
arrType: download.arrType,
|
||||
arrInstanceUrl: download.arrInstanceUrl,
|
||||
arrInstanceKey: download.arrInstanceKey,
|
||||
arrContentId: download.arrContentId,
|
||||
arrContentType: download.arrContentType
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.error || `HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('[Blocklist] Error:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadAppVersion() {
|
||||
try {
|
||||
const res = await fetch('/health');
|
||||
const data = await res.json();
|
||||
return data.version || null;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchWebhookMetrics() {
|
||||
try {
|
||||
const res = await fetch('/api/dashboard/webhook-metrics');
|
||||
if (!res.ok) return null;
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchWebhookStatus() {
|
||||
try {
|
||||
// Fetch metrics in parallel
|
||||
const metricsPromise = fetchWebhookMetrics();
|
||||
|
||||
// Fetch Sonarr notifications
|
||||
let sonarrEnabled = false;
|
||||
let sonarrTriggers = { onGrab: false, onDownload: false, onImport: false, onUpgrade: false };
|
||||
try {
|
||||
const sonarrRes = await fetch('/api/sonarr/notifications');
|
||||
if (sonarrRes.ok) {
|
||||
const sonarrData = await sonarrRes.json();
|
||||
const sonarrSofarr = sonarrData.find(n => n.name === 'Sofarr');
|
||||
sonarrEnabled = !!sonarrSofarr;
|
||||
if (sonarrSofarr) {
|
||||
sonarrTriggers = {
|
||||
onGrab: sonarrSofarr.onGrab,
|
||||
onDownload: sonarrSofarr.onDownload,
|
||||
onImport: sonarrSofarr.onImport,
|
||||
onUpgrade: sonarrSofarr.onUpgrade
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Sonarr not configured
|
||||
}
|
||||
|
||||
// Fetch Radarr notifications
|
||||
let radarrEnabled = false;
|
||||
let radarrTriggers = { onGrab: false, onDownload: false, onImport: false, onUpgrade: false };
|
||||
try {
|
||||
const radarrRes = await fetch('/api/radarr/notifications');
|
||||
if (radarrRes.ok) {
|
||||
const radarrData = await radarrRes.json();
|
||||
const radarrSofarr = radarrData.find(n => n.name === 'Sofarr');
|
||||
radarrEnabled = !!radarrSofarr;
|
||||
if (radarrSofarr) {
|
||||
radarrTriggers = {
|
||||
onGrab: radarrSofarr.onGrab,
|
||||
onDownload: radarrSofarr.onDownload,
|
||||
onImport: radarrSofarr.onImport,
|
||||
onUpgrade: radarrSofarr.onUpgrade
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Radarr not configured
|
||||
}
|
||||
|
||||
state.webhookMetrics = await metricsPromise;
|
||||
|
||||
// Find instance stats
|
||||
const instanceEntries = state.webhookMetrics ? Object.entries(state.webhookMetrics.instances || {}) : [];
|
||||
const sonarrStats = instanceEntries.find(([url]) => url.includes('sonarr'))?.[1] || null;
|
||||
const radarrStats = instanceEntries.find(([url]) => url.includes('radarr'))?.[1] || null;
|
||||
|
||||
state.sonarrWebhook = { enabled: sonarrEnabled, triggers: sonarrTriggers, stats: sonarrStats };
|
||||
state.radarrWebhook = { enabled: radarrEnabled, triggers: radarrTriggers, stats: radarrStats };
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch webhook status:', err);
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
|
||||
export async function enableSonarrWebhook() {
|
||||
try {
|
||||
const res = await fetch('/api/sonarr/notifications/sofarr-webhook', {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-Token': csrfToken || '' }
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to enable');
|
||||
await fetchWebhookStatus();
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Failed to enable Sonarr webhook:', err);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function enableRadarrWebhook() {
|
||||
try {
|
||||
const res = await fetch('/api/radarr/notifications/sofarr-webhook', {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-Token': state.csrfToken || '' }
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to enable');
|
||||
await fetchWebhookStatus();
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Failed to enable Radarr webhook:', err);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function testSonarrWebhook() {
|
||||
try {
|
||||
const sonarrRes = await fetch('/api/sonarr/notifications');
|
||||
if (!sonarrRes.ok) throw new Error('Failed to fetch notifications');
|
||||
const sonarrData = await sonarrRes.json();
|
||||
const sonarrSofarr = sonarrData.find(n => n.name === 'Sofarr');
|
||||
if (!sonarrSofarr) throw new Error('Sofarr webhook not found');
|
||||
|
||||
const res = await fetch('/api/sonarr/notifications/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': state.csrfToken || ''
|
||||
},
|
||||
body: JSON.stringify(sonarrSofarr)
|
||||
});
|
||||
if (!res.ok) throw new Error('Test failed');
|
||||
await fetchWebhookStatus();
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Failed to test Sonarr webhook:', err);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function testRadarrWebhook() {
|
||||
try {
|
||||
const radarrRes = await fetch('/api/radarr/notifications');
|
||||
if (!radarrRes.ok) throw new Error('Failed to fetch notifications');
|
||||
const radarrData = await radarrRes.json();
|
||||
const radarrSofarr = radarrData.find(n => n.name === 'Sofarr');
|
||||
if (!radarrSofarr) throw new Error('Sofarr webhook not found');
|
||||
|
||||
const res = await fetch('/api/radarr/notifications/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': state.csrfToken || ''
|
||||
},
|
||||
body: JSON.stringify(radarrSofarr)
|
||||
});
|
||||
if (!res.ok) throw new Error('Test failed');
|
||||
await fetchWebhookStatus();
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Failed to test Radarr webhook:', err);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function refreshStatusPanel() {
|
||||
try {
|
||||
const res = await fetch('/api/dashboard/status');
|
||||
if (!res.ok) throw new Error('Failed to fetch status: ' + res.status);
|
||||
const data = await res.json();
|
||||
return { success: true, data };
|
||||
} catch (err) {
|
||||
console.error('[Status] Error fetching status:', err);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user