Migrate frontend from monolithic app.js to vanilla ES modules
Build and Push Docker Image / build (push) Successful in 35s
Licence Check / Licence compatibility and copyright header verification (push) Failing after 49s
CI / Security audit (push) Successful in 1m9s
CI / Tests & coverage (push) Successful in 1m23s

- 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:
2026-05-20 23:30:24 +01:00
parent a38fc4a8ce
commit 8dc105ff3e
19 changed files with 2173 additions and 2854 deletions
+171
View File
@@ -0,0 +1,171 @@
// Copyright (c) 2026 Gordon Bolton. MIT License.
import { state, SPLASH_MIN_MS } from '../state.js';
import { checkAuthentication, handleLogin as apiHandleLogin, handleLogout as apiHandleLogout } from '../api.js';
import { startSSE, stopSSE } from '../sse.js';
import { stopHistoryRefresh, clearHistory, startHistoryRefresh } from './history.js';
import { closeStatusPanel } from './statusPanel.js';
export function fadeOutLogin() {
return new Promise(resolve => {
const login = document.getElementById('login-container');
login.classList.add('fade-out');
login.addEventListener('transitionend', () => {
login.style.display = 'none';
login.classList.remove('fade-out');
resolve();
}, { once: true });
});
}
export function showSplash() {
const splash = document.getElementById('splash-screen');
splash.style.display = 'flex';
splash.style.opacity = '1';
splash.classList.remove('fade-out');
}
export function dismissSplash(startTime) {
return new Promise(resolve => {
const elapsed = Date.now() - (startTime || 0);
const remaining = Math.max(0, SPLASH_MIN_MS - elapsed);
setTimeout(() => {
const splash = document.getElementById('splash-screen');
splash.classList.add('fade-out');
// Fallback: resolve after transition duration + buffer in case
// transitionend never fires (e.g. display was toggled in same frame)
const TRANSITION_MS = 400;
const fallback = setTimeout(() => {
splash.style.display = 'none';
resolve();
}, TRANSITION_MS + 100);
splash.addEventListener('transitionend', () => {
clearTimeout(fallback);
splash.style.display = 'none';
resolve();
}, { once: true });
}, remaining);
});
}
export async function checkAuthenticationAndInit() {
const splashStart = Date.now();
try {
const result = await checkAuthentication();
if (result.authenticated) {
showDashboard();
showLoading();
startSSE();
await dismissSplash(splashStart);
} else {
await dismissSplash(splashStart);
showLogin();
}
} catch (err) {
console.error('Authentication check failed:', err);
await dismissSplash(splashStart);
showLogin();
}
}
export async function handleLogin(e) {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const rememberMe = document.getElementById('remember-me').checked;
try {
const result = await apiHandleLogin(username, password, rememberMe);
if (result.success) {
// Fade out login, then show splash while opening SSE stream.
// requestAnimationFrame ensures the browser paints the splash at
// opacity:1 before dismissSplash adds fade-out, so the CSS
// transition fires and transitionend is guaranteed.
await fadeOutLogin();
showSplash();
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
showDashboard();
showLoading();
const splashStart = Date.now();
startSSE();
await dismissSplash(splashStart);
} else {
showLoginError(result.error || 'Login failed');
}
} catch (err) {
showLoginError('Login failed. Please try again.');
console.error(err);
}
}
export async function handleLogoutClick() {
try {
stopSSE();
stopHistoryRefresh();
if (statusRefreshHandle) { clearInterval(statusRefreshHandle); statusRefreshHandle.value = null; }
await apiHandleLogout();
currentUser = null;
clearHistory();
showLogin();
} catch (err) {
console.error('Logout failed:', err);
}
}
export function showLogin() {
document.getElementById('login-container').style.display = 'flex';
document.getElementById('dashboard-container').style.display = 'none';
hideLoginError();
}
export function showDashboard() {
document.getElementById('login-container').style.display = 'none';
document.getElementById('dashboard-container').style.display = 'block';
document.getElementById('currentUser').textContent = state.currentUser.name || '-';
// Always start with status panel hidden (guards against stale display value on re-login)
const sp = document.getElementById('status-panel');
sp.style.display = 'none';
// Also hide webhooks-section to keep them in sync (both show/hide together)
const webhooksSection = document.getElementById('webhooks-section');
if (webhooksSection) webhooksSection.style.display = 'none';
document.getElementById('admin-controls').style.display = state.isAdmin ? 'flex' : 'none';
// Note: webhooks-section visibility is controlled by toggleStatusPanel()
// Initialise days input from saved value
const daysInput = document.getElementById('history-days');
if (daysInput) daysInput.value = state.historyDays;
startHistoryRefresh();
}
export function showLoginError(message) {
const errorDiv = document.getElementById('login-error');
errorDiv.textContent = message;
errorDiv.style.display = 'block';
}
export function hideLoginError() {
const errorDiv = document.getElementById('login-error');
errorDiv.style.display = 'none';
}
export function showError(message) {
const errorDiv = document.getElementById('error-message');
errorDiv.textContent = message;
errorDiv.style.display = 'block';
}
export function hideError() {
const errorDiv = document.getElementById('error-message');
errorDiv.style.display = 'none';
}
export function showLoading() {
const loading = document.getElementById('loading');
loading.style.display = 'block';
}
export function hideLoading() {
const loading = document.getElementById('loading');
loading.style.display = 'none';
}