Enhanced setup and settings pages with full configuration

- Extended SetupRequest model to include all configuration fields
- Updated setup API endpoint to handle comprehensive configuration
- Created new setup.html with organized configuration sections
- Enhanced config modal in index.html with all settings
- Updated JavaScript modules to use unified config API
- Added backup configuration section
- Documented new features in features.md and instructions.md
This commit is contained in:
2026-01-17 18:01:15 +01:00
parent d676cb7dca
commit 4e29c4ed80
12 changed files with 1807 additions and 680 deletions

View File

@@ -56,6 +56,9 @@ AniWorld.ConfigManager = (function() {
// NFO configuration
bindNFOEvents();
// Backup configuration
bindBackupEvents();
// Status panel
const closeStatus = document.getElementById('close-status');
if (closeStatus) {
@@ -118,8 +121,16 @@ AniWorld.ConfigManager = (function() {
}
}
/**
* Bind NFO config events
/** * Bind backup config events
*/
function bindBackupEvents() {
const saveBackup = document.getElementById('save-backup-config');
if (saveBackup) {
saveBackup.addEventListener('click', AniWorld.MainConfig.saveBackupConfig);
}
}
/** * Bind NFO config events
*/
function bindNFOEvents() {
const saveNFO = document.getElementById('save-nfo-config');

View File

@@ -113,28 +113,25 @@ AniWorld.LoggingConfig = (function() {
*/
async function save() {
try {
const config = {
log_level: document.getElementById('log-level').value,
enable_console_logging: document.getElementById('enable-console-logging').checked,
enable_console_progress: document.getElementById('enable-console-progress').checked,
enable_fail2ban_logging: document.getElementById('enable-fail2ban-logging').checked
// Get current config
const configResponse = await AniWorld.ApiClient.get(AniWorld.Constants.API.CONFIG);
if (!configResponse) return;
const config = await configResponse.json();
// Update logging settings
config.logging = {
level: document.getElementById('log-level').value.toUpperCase(),
file: document.getElementById('log-file').value.trim() || null,
max_bytes: document.getElementById('log-max-bytes').value ? parseInt(document.getElementById('log-max-bytes').value) : null,
backup_count: parseInt(document.getElementById('log-backup-count').value) || 3
};
const response = await AniWorld.ApiClient.request(API.LOGGING_CONFIG, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
// Save updated config
const response = await AniWorld.ApiClient.put(AniWorld.Constants.API.CONFIG, config);
if (!response) return;
const data = await response.json();
if (data.success) {
AniWorld.UI.showToast('Logging configuration saved successfully', 'success');
await load();
} else {
AniWorld.UI.showToast('Failed to save logging config: ' + data.error, 'error');
}
AniWorld.UI.showToast('Logging configuration saved successfully', 'success');
await load();
} catch (error) {
console.error('Error saving logging config:', error);
AniWorld.UI.showToast('Failed to save logging configuration', 'error');

View File

@@ -20,31 +20,55 @@ AniWorld.MainConfig = (function() {
async function save() {
try {
const animeDirectory = document.getElementById('anime-directory-input').value.trim();
const appName = document.getElementById('app-name-input').value.trim();
const dataDir = document.getElementById('data-dir-input').value.trim();
if (!animeDirectory) {
AniWorld.UI.showToast('Please enter an anime directory path', 'error');
return;
}
const response = await AniWorld.ApiClient.post(API.CONFIG_DIRECTORY, {
directory: animeDirectory
});
// Get current config
const currentConfig = await loadCurrentConfig();
if (!currentConfig) {
AniWorld.UI.showToast('Failed to load current configuration', 'error');
return;
}
// Update fields
if (appName) currentConfig.name = appName;
if (dataDir) currentConfig.data_dir = dataDir;
if (!currentConfig.other) currentConfig.other = {};
currentConfig.other.anime_directory = animeDirectory;
// Save updated config
const response = await AniWorld.ApiClient.put(AniWorld.Constants.API.CONFIG, currentConfig);
if (!response) return;
const data = await response.json();
if (data.success) {
AniWorld.UI.showToast('Main configuration saved successfully', 'success');
await refreshStatus();
} else {
AniWorld.UI.showToast('Failed to save configuration: ' + data.error, 'error');
}
AniWorld.UI.showToast('Main configuration saved successfully', 'success');
await refreshStatus();
} catch (error) {
console.error('Error saving main config:', error);
AniWorld.UI.showToast('Failed to save main configuration', 'error');
}
}
/**
* Load current configuration from API
*/
async function loadCurrentConfig() {
try {
const response = await AniWorld.ApiClient.get(AniWorld.Constants.API.CONFIG);
if (!response) return null;
return await response.json();
} catch (error) {
console.error('Error loading config:', error);
return null;
}
}
/**
* Reset main configuration
*/
@@ -109,12 +133,45 @@ AniWorld.MainConfig = (function() {
*/
async function refreshStatus() {
try {
const response = await AniWorld.ApiClient.get(API.ANIME_STATUS);
if (!response) return;
const data = await response.json();
// Load full configuration
const config = await loadCurrentConfig();
if (!config) return;
document.getElementById('anime-directory-input').value = data.directory || '';
document.getElementById('series-count-input').value = data.series_count || '0';
// Populate general settings
document.getElementById('app-name-input').value = config.name || 'Aniworld';
document.getElementById('data-dir-input').value = config.data_dir || 'data';
document.getElementById('anime-directory-input').value = config.other?.anime_directory || '';
// Populate scheduler settings
document.getElementById('scheduled-rescan-enabled').checked = config.scheduler?.enabled || false;
document.getElementById('scheduled-rescan-interval').value = config.scheduler?.interval_minutes || 60;
// Populate logging settings
document.getElementById('log-level').value = config.logging?.level || 'INFO';
document.getElementById('log-file').value = config.logging?.file || '';
document.getElementById('log-max-bytes').value = config.logging?.max_bytes || '';
document.getElementById('log-backup-count').value = config.logging?.backup_count || 3;
// Populate backup settings
document.getElementById('backup-enabled').checked = config.backup?.enabled || false;
document.getElementById('backup-path').value = config.backup?.path || 'data/backups';
document.getElementById('backup-keep-days').value = config.backup?.keep_days || 30;
// Populate NFO settings
document.getElementById('tmdb-api-key').value = config.nfo?.tmdb_api_key || '';
document.getElementById('nfo-auto-create').checked = config.nfo?.auto_create || false;
document.getElementById('nfo-update-on-scan').checked = config.nfo?.update_on_scan || false;
document.getElementById('nfo-download-poster').checked = config.nfo?.download_poster !== false;
document.getElementById('nfo-download-logo').checked = config.nfo?.download_logo !== false;
document.getElementById('nfo-download-fanart').checked = config.nfo?.download_fanart !== false;
document.getElementById('nfo-image-size').value = config.nfo?.image_size || 'original';
// Get series count from status endpoint
const statusResponse = await AniWorld.ApiClient.get(API.ANIME_STATUS);
if (statusResponse) {
const statusData = await statusResponse.json();
document.getElementById('series-count-input').value = statusData.series_count || '0';
}
} catch (error) {
console.error('Error refreshing status:', error);
}
@@ -278,6 +335,36 @@ AniWorld.MainConfig = (function() {
}
}
/**
* Save backup configuration
*/
async function saveBackupConfig() {
try {
const config = await loadCurrentConfig();
if (!config) {
AniWorld.UI.showToast('Failed to load current configuration', 'error');
return;
}
// Update backup settings
config.backup = {
enabled: document.getElementById('backup-enabled').checked,
path: document.getElementById('backup-path').value.trim(),
keep_days: parseInt(document.getElementById('backup-keep-days').value) || 30
};
// Save updated config
const response = await AniWorld.ApiClient.put(AniWorld.Constants.API.CONFIG, config);
if (!response) return;
AniWorld.UI.showToast('Backup configuration saved successfully', 'success');
} catch (error) {
console.error('Error saving backup config:', error);
AniWorld.UI.showToast('Failed to save backup configuration', 'error');
}
}
// Public API
return {
save: save,
@@ -289,6 +376,7 @@ AniWorld.MainConfig = (function() {
viewBackups: viewBackups,
exportConfig: exportConfig,
validateConfig: validateConfig,
resetAllConfig: resetAllConfig
resetAllConfig: resetAllConfig,
saveBackupConfig: saveBackupConfig
};
})();

View File

@@ -92,8 +92,16 @@ AniWorld.NFOConfig = (function() {
return;
}
const nfoConfig = {
tmdb_api_key: tmdbKey ? tmdbKey.value.trim() : null,
// Get current config
const configResponse = await AniWorld.ApiClient.get(AniWorld.Constants.API.CONFIG);
if (!configResponse) {
throw new Error('Failed to load current configuration');
}
const config = await configResponse.json();
// Update NFO settings
config.nfo = {
tmdb_api_key: tmdbKey ? tmdbKey.value.trim() || null : null,
auto_create: autoCreate ? autoCreate.checked : false,
update_on_scan: updateOnScan ? updateOnScan.checked : false,
download_poster: downloadPoster ? downloadPoster.checked : true,
@@ -103,13 +111,7 @@ AniWorld.NFOConfig = (function() {
};
// Save configuration
const response = await AniWorld.ApiClient.request(
API.CONFIG,
{
method: 'PUT',
body: JSON.stringify({ nfo: nfoConfig })
}
);
const response = await AniWorld.ApiClient.put(AniWorld.Constants.API.CONFIG, config);
if (response) {
AniWorld.UI.showToast('NFO configuration saved successfully', 'success');

View File

@@ -55,24 +55,25 @@ AniWorld.SchedulerConfig = (function() {
async function save() {
try {
const enabled = document.getElementById('scheduled-rescan-enabled').checked;
const time = document.getElementById('scheduled-rescan-time').value;
const autoDownload = document.getElementById('auto-download-after-rescan').checked;
const interval = parseInt(document.getElementById('scheduled-rescan-interval').value) || 60;
const response = await AniWorld.ApiClient.post(API.SCHEDULER_CONFIG, {
// Get current config
const configResponse = await AniWorld.ApiClient.get(AniWorld.Constants.API.CONFIG);
if (!configResponse) return;
const config = await configResponse.json();
// Update scheduler settings
config.scheduler = {
enabled: enabled,
time: time,
auto_download_after_rescan: autoDownload
});
interval_minutes: interval
};
// Save updated config
const response = await AniWorld.ApiClient.put(AniWorld.Constants.API.CONFIG, config);
if (!response) return;
const data = await response.json();
if (data.success) {
AniWorld.UI.showToast('Scheduler configuration saved successfully', 'success');
await load();
} else {
AniWorld.UI.showToast('Failed to save config: ' + data.error, 'error');
}
AniWorld.UI.showToast('Scheduler configuration saved successfully', 'success');
await load();
} catch (error) {
console.error('Error saving scheduler config:', error);
AniWorld.UI.showToast('Failed to save scheduler configuration', 'error');

View File

@@ -186,6 +186,23 @@
</button>
</div>
<div class="modal-body">
<!-- General Settings -->
<div class="config-section">
<h4 data-text="general-settings">General Settings</h4>
<div class="config-item">
<label for="app-name-input" data-text="app-name">Application Name:</label>
<input type="text" id="app-name-input" class="input-field"
placeholder="Aniworld">
</div>
<div class="config-item">
<label for="data-dir-input" data-text="data-directory">Data Directory:</label>
<input type="text" id="data-dir-input" class="input-field"
placeholder="data">
</div>
</div>
<div class="config-item">
<label for="anime-directory-input" data-text="anime-directory">Anime Directory:</label>
<div class="input-group">
@@ -233,10 +250,18 @@
<label class="checkbox-label">
<input type="checkbox" id="scheduled-rescan-enabled">
<span class="checkbox-custom"></span>
<span data-text="enable-scheduled-rescan">Enable Daily Rescan</span>
<span data-text="enable-scheduled-rescan">Enable Scheduler</span>
</label>
</div>
<div class="config-item" id="rescan-interval-config">
<label for="scheduled-rescan-interval" data-text="rescan-interval">Check Interval (minutes):</label>
<input type="number" id="scheduled-rescan-interval" value="60" min="1" class="input-field">
<small class="config-hint" data-text="rescan-interval-hint">
How often to check for new episodes (minimum 1 minute)
</small>
</div>
<div class="config-item" id="rescan-time-config">
<label for="scheduled-rescan-time" data-text="rescan-time">Rescan Time (24h format):</label>
<input type="time" id="scheduled-rescan-time" value="03:00" class="input-field">
@@ -295,6 +320,30 @@
</select>
</div>
<div class="config-item">
<label for="log-file" data-text="log-file">Log File Path (optional):</label>
<input type="text" id="log-file" placeholder="logs/app.log" class="input-field">
<small class="config-hint" data-text="log-file-hint">
Leave empty to log to console only
</small>
</div>
<div class="config-item">
<label for="log-max-bytes" data-text="log-max-bytes">Max File Size (bytes):</label>
<input type="number" id="log-max-bytes" placeholder="10485760" min="0" class="input-field">
<small class="config-hint" data-text="log-max-bytes-hint">
Maximum size before log rotation (leave empty for no rotation)
</small>
</div>
<div class="config-item">
<label for="log-backup-count" data-text="log-backup-count">Backup Count:</label>
<input type="number" id="log-backup-count" value="3" min="0" class="input-field">
<small class="config-hint" data-text="log-backup-count-hint">
Number of rotated log files to keep
</small>
</div>
<div class="config-item">
<div class="checkbox-container">
<input type="checkbox" id="enable-console-logging">
@@ -428,6 +477,45 @@
</div>
</div>
<!-- Backup Configuration -->
<div class="config-section">
<h4 data-text="backup-config">Backup Configuration</h4>
<div class="config-item">
<label class="checkbox-label">
<input type="checkbox" id="backup-enabled">
<span class="checkbox-custom"></span>
<span data-text="backup-enabled">Enable Automatic Backups</span>
</label>
<small class="config-hint" data-text="backup-enabled-hint">
Automatically backup configuration files
</small>
</div>
<div class="config-item">
<label for="backup-path" data-text="backup-path">Backup Path:</label>
<input type="text" id="backup-path" value="data/backups" class="input-field">
<small class="config-hint" data-text="backup-path-hint">
Directory where backups will be stored
</small>
</div>
<div class="config-item">
<label for="backup-keep-days" data-text="backup-keep-days">Keep Backups (days):</label>
<input type="number" id="backup-keep-days" value="30" min="0" class="input-field">
<small class="config-hint" data-text="backup-keep-days-hint">
How many days to keep old backups
</small>
</div>
<div class="config-actions">
<button id="save-backup-config" class="btn btn-primary">
<i class="fas fa-save"></i>
<span data-text="save-backup-config">Save Backup Settings</span>
</button>
</div>
</div>
<!-- Configuration Management -->
<div class="config-section">
<h4 data-text="config-management">Configuration Management</h4>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,561 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AniWorld Manager - Setup</title>
<link rel="stylesheet" href="/static/css/styles.css">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
.setup-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--color-primary-light) 0%, var(--color-primary) 100%);
padding: 1rem;
}
.setup-card {
background: var(--color-surface);
border-radius: 16px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 500px;
border: 1px solid var(--color-border);
}
.setup-header {
text-align: center;
margin-bottom: 2rem;
}
.setup-header .logo {
font-size: 3rem;
color: var(--color-primary);
margin-bottom: 0.5rem;
}
.setup-header h1 {
margin: 0;
color: var(--color-text);
font-size: 1.8rem;
font-weight: 600;
}
.setup-header p {
margin: 1rem 0 0 0;
color: var(--color-text-secondary);
font-size: 1rem;
line-height: 1.5;
}
.setup-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-label {
font-weight: 500;
color: var(--color-text);
font-size: 0.9rem;
}
.form-input {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid var(--color-border);
border-radius: 8px;
font-size: 1rem;
background: var(--color-background);
color: var(--color-text);
transition: all 0.2s ease;
box-sizing: border-box;
}
.form-input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb), 0.1);
}
.password-input-group {
position: relative;
}
.password-input {
padding-right: 3rem;
}
.password-toggle {
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
padding: 0.25rem;
border-radius: 4px;
transition: color 0.2s ease;
}
.password-toggle:hover {
color: var(--color-primary);
}
.password-strength {
display: flex;
gap: 0.25rem;
margin-top: 0.5rem;
}
.strength-bar {
flex: 1;
height: 4px;
background: var(--color-border);
border-radius: 2px;
transition: background-color 0.2s ease;
}
.strength-bar.active.weak {
background: var(--color-error);
}
.strength-bar.active.fair {
background: var(--color-warning);
}
.strength-bar.active.good {
background: var(--color-info);
}
.strength-bar.active.strong {
background: var(--color-success);
}
.strength-text {
font-size: 0.8rem;
color: var(--color-text-secondary);
margin-top: 0.25rem;
}
.form-help {
font-size: 0.8rem;
color: var(--color-text-secondary);
line-height: 1.4;
}
.setup-button {
width: 100%;
padding: 0.75rem;
background: var(--color-primary);
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.setup-button:hover:not(:disabled) {
background: var(--color-primary-dark);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(var(--color-primary-rgb), 0.3);
}
.setup-button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.error-message {
background: var(--color-error-light);
color: var(--color-error);
padding: 0.75rem;
border-radius: 8px;
border: 1px solid var(--color-error);
font-size: 0.9rem;
}
.success-message {
background: var(--color-success-light);
color: var(--color-success);
padding: 0.75rem;
border-radius: 8px;
border: 1px solid var(--color-success);
font-size: 0.9rem;
}
.security-tips {
margin-top: 1.5rem;
padding: 1rem;
background: var(--color-info-light);
border: 1px solid var(--color-info);
border-radius: 8px;
font-size: 0.85rem;
color: var(--color-text-secondary);
}
.security-tips h4 {
margin: 0 0 0.5rem 0;
color: var(--color-info);
font-size: 0.9rem;
}
.security-tips ul {
margin: 0;
padding-left: 1.2rem;
line-height: 1.4;
}
.theme-toggle {
position: absolute;
top: 1rem;
right: 1rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
padding: 0.5rem;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
}
.theme-toggle:hover {
background: rgba(255, 255, 255, 0.2);
transform: scale(1.1);
}
.loading-spinner {
width: 1rem;
height: 1rem;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<div class="setup-container">
<button class="theme-toggle" id="theme-toggle" title="Toggle theme">
<i class="fas fa-moon"></i>
</button>
<div class="setup-card">
<div class="setup-header">
<div class="logo">
<i class="fas fa-play-circle"></i>
</div>
<h1>Welcome to AniWorld Manager</h1>
<p>Let's set up your master password to secure your anime collection.</p>
</div>
<form class="setup-form" id="setup-form">
<div class="form-group">
<label for="directory" class="form-label">Anime Directory</label>
<input type="text" id="directory" name="directory" class="form-input" placeholder="C:\Anime"
value="{{ current_directory }}" required>
<div class="form-help">
The directory where your anime series are stored. This can be changed later in settings.
</div>
</div>
<div class="form-group">
<label for="password" class="form-label">Master Password</label>
<div class="password-input-group">
<input type="password" id="password" name="password" class="form-input password-input"
placeholder="Create a strong password" required minlength="8">
<button type="button" class="password-toggle" id="password-toggle" tabindex="-1">
<i class="fas fa-eye"></i>
</button>
</div>
<div class="password-strength">
<div class="strength-bar" id="strength-1"></div>
<div class="strength-bar" id="strength-2"></div>
<div class="strength-bar" id="strength-3"></div>
<div class="strength-bar" id="strength-4"></div>
</div>
<div class="strength-text" id="strength-text">Password strength will be shown here</div>
</div>
<div class="form-group">
<label for="confirm-password" class="form-label">Confirm Password</label>
<div class="password-input-group">
<input type="password" id="confirm-password" name="confirm-password"
class="form-input password-input" placeholder="Confirm your password" required
minlength="8">
<button type="button" class="password-toggle" id="confirm-password-toggle" tabindex="-1">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
<div id="message-container"></div>
<button type="submit" class="setup-button" id="setup-button">
<i class="fas fa-check"></i>
<span>Complete Setup</span>
</button>
</form>
<div class="security-tips">
<h4><i class="fas fa-shield-alt"></i> Security Tips</h4>
<ul>
<li>Use a password with at least 12 characters</li>
<li>Include uppercase, lowercase, numbers, and symbols</li>
<li>Don't use personal information or common words</li>
<li>Consider using a password manager</li>
</ul>
</div>
</div>
</div>
<script>
// Theme toggle functionality
const themeToggle = document.getElementById('theme-toggle');
const htmlElement = document.documentElement;
const savedTheme = localStorage.getItem('theme') || 'light';
htmlElement.setAttribute('data-theme', savedTheme);
updateThemeIcon(savedTheme);
themeToggle.addEventListener('click', () => {
const currentTheme = htmlElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
htmlElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeIcon(newTheme);
});
function updateThemeIcon(theme) {
const icon = themeToggle.querySelector('i');
icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
}
// Password visibility toggles
document.querySelectorAll('.password-toggle').forEach(toggle => {
toggle.addEventListener('click', () => {
const input = toggle.parentElement.querySelector('input');
const type = input.getAttribute('type');
const newType = type === 'password' ? 'text' : 'password';
const icon = toggle.querySelector('i');
input.setAttribute('type', newType);
icon.className = newType === 'password' ? 'fas fa-eye' : 'fas fa-eye-slash';
});
});
// Password strength checker
const passwordInput = document.getElementById('password');
const strengthBars = document.querySelectorAll('.strength-bar');
const strengthText = document.getElementById('strength-text');
passwordInput.addEventListener('input', () => {
const password = passwordInput.value;
const strength = calculatePasswordStrength(password);
updatePasswordStrength(strength);
});
function calculatePasswordStrength(password) {
let score = 0;
let feedback = [];
// Length check
if (password.length >= 8) score++;
if (password.length >= 12) score++;
// Character variety
if (/[a-z]/.test(password)) score++;
if (/[A-Z]/.test(password)) score++;
if (/[0-9]/.test(password)) score++;
if (/[^A-Za-z0-9]/.test(password)) score++;
// Penalties
if (password.length < 8) {
feedback.push('Too short');
score = Math.max(0, score - 2);
}
if (!/[A-Z]/.test(password)) feedback.push('Add uppercase');
if (!/[0-9]/.test(password)) feedback.push('Add numbers');
if (!/[^A-Za-z0-9]/.test(password)) feedback.push('Add symbols');
const strengthLevels = ['Very Weak', 'Weak', 'Fair', 'Good', 'Strong', 'Very Strong'];
const strengthLevel = Math.min(Math.floor(score / 1.2), 5);
return {
score: Math.min(score, 6),
level: strengthLevel,
text: strengthLevels[strengthLevel],
feedback
};
}
function updatePasswordStrength(strength) {
const colors = ['weak', 'weak', 'fair', 'good', 'strong', 'strong'];
const color = colors[strength.level];
strengthBars.forEach((bar, index) => {
bar.className = 'strength-bar';
if (index < strength.score) {
bar.classList.add('active', color);
}
});
if (passwordInput.value) {
let text = `Password strength: ${strength.text}`;
if (strength.feedback.length > 0) {
text += ` (${strength.feedback.join(', ')})`;
}
strengthText.textContent = text;
strengthText.style.color = strength.level >= 3 ? 'var(--color-success)' : 'var(--color-warning)';
} else {
strengthText.textContent = 'Password strength will be shown here';
strengthText.style.color = 'var(--color-text-secondary)';
}
}
// Form submission
const setupForm = document.getElementById('setup-form');
const setupButton = document.getElementById('setup-button');
const messageContainer = document.getElementById('message-container');
const confirmPasswordInput = document.getElementById('confirm-password');
const directoryInput = document.getElementById('directory');
// Real-time password confirmation
confirmPasswordInput.addEventListener('input', validatePasswordMatch);
passwordInput.addEventListener('input', validatePasswordMatch);
function validatePasswordMatch() {
const password = passwordInput.value;
const confirmPassword = confirmPasswordInput.value;
if (confirmPassword && password !== confirmPassword) {
confirmPasswordInput.setCustomValidity('Passwords do not match');
confirmPasswordInput.style.borderColor = 'var(--color-error)';
} else {
confirmPasswordInput.setCustomValidity('');
confirmPasswordInput.style.borderColor = 'var(--color-border)';
}
}
setupForm.addEventListener('submit', async (e) => {
e.preventDefault();
const password = passwordInput.value;
const confirmPassword = confirmPasswordInput.value;
const directory = directoryInput.value.trim();
if (password !== confirmPassword) {
showMessage('Passwords do not match', 'error');
return;
}
const strength = calculatePasswordStrength(password);
if (strength.level < 2) {
showMessage('Password is too weak. Please use a stronger password.', 'error');
return;
}
if (!directory) {
showMessage('Please enter a valid anime directory', 'error');
return;
}
setLoading(true);
try {
const response = await fetch('/api/auth/setup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
master_password: password,
anime_directory: directory
})
});
const data = await response.json();
if (response.ok && data.status === 'ok') {
showMessage('Setup completed successfully! Redirecting to login...', 'success');
setTimeout(() => {
window.location.href = '/login';
}, 2000);
} else {
const errorMessage = data.detail || data.message || 'Setup failed';
showMessage(errorMessage, 'error');
}
} catch (error) {
showMessage('Setup failed. Please try again.', 'error');
console.error('Setup error:', error);
} finally {
setLoading(false);
}
});
function showMessage(message, type) {
messageContainer.innerHTML = `
<div class="${type}-message">
${message}
</div>
`;
}
function setLoading(loading) {
setupButton.disabled = loading;
const buttonText = setupButton.querySelector('span');
const buttonIcon = setupButton.querySelector('i');
if (loading) {
buttonIcon.className = 'loading-spinner';
buttonText.textContent = 'Setting up...';
} else {
buttonIcon.className = 'fas fa-check';
buttonText.textContent = 'Complete Setup';
}
}
// Clear message on input
[passwordInput, confirmPasswordInput, directoryInput].forEach(input => {
input.addEventListener('input', () => {
messageContainer.innerHTML = '';
});
});
</script>
</body>
</html>