// Password Generator Frontend Application v4.0 // Complete refactoring with API-driven generator definitions // Supports programmatic i18n key derivation and dynamic form generation class PasswordGeneratorApp { constructor() { this.currentLang = 'en'; this.translations = {}; this.generatorDefinitions = []; // ✅ NEW: API-driven definitions this.availableWordlists = {}; this.gridResizeListener = null; this.gridState = { currentLayout: 'single', cardCount: 0 }; this.resizeTimeout = null; this.rouletteRuns = new Map(); this.defaultRouletteIntervalSeconds = 3; this.defaultLanguages = { 'en': 'English', 'de': 'Deutsch', 'de_CH': 'Schwyzerdütsch', 'fr': 'Français', 'it': 'Italiano', 'es': 'Español', 'pt': 'Portugues', 'ja': '日本語', 'zh': '中文' }; this.init(); } async init() { console.log('🚀 Initializing Password Generator App v4.0'); try { await this.detectLanguage(); await this.loadTranslations(); await this.loadGeneratorsMetadata(); // ✅ NEW: Load from API await this.loadAvailableWordlists(); this.setupEventListeners(); this.renderGeneratorCards(); // ✅ Now uses API definitions this.updateLanguageSelector(); this.switchLanguage(this.currentLang); this.hideLoadingSpinner(); this.updateFormFieldsForScreen(); this.setupGridObserver(); // Add resize listener window.addEventListener('resize', () => { clearTimeout(this.resizeTimeout); this.resizeTimeout = setTimeout(() => { this.updateFormFieldsForScreen(); }, 250); }); console.log('✅ Initialization complete'); } catch (error) { console.error('❌ Initialization failed:', error); this.showError('Failed to initialize application'); } } /** * ✅ NEW: Load generator metadata from backend API */ async loadGeneratorsMetadata() { try { console.log('🔄 Loading generator metadata from backend...'); const response = await fetch('/api/v1/passwords/generators'); if (!response.ok) { throw new Error(`API request failed: ${response.status}`); } const data = await response.json(); if (data.success && data.data.generators) { this.generatorDefinitions = data.data.generators; this.defaultRouletteIntervalSeconds = Math.max( 1, Math.min(30, parseInt(data.data.settings?.roulette_interval_seconds ?? 3, 10) || 3) ); console.log(`✅ Loaded ${this.generatorDefinitions.length} generators`); // Log generator summary this.generatorDefinitions.forEach(gen => { console.log(` 📦 ${gen.id} (v${gen.version}): ${gen.options.length} options`); }); } else { throw new Error('Invalid API response format for generators'); } } catch (error) { console.error('❌ Failed to load generator metadata:', error.message); this.generatorDefinitions = []; throw error; } } hideLoadingSpinner() { const loadingElement = document.getElementById('initial-loading'); const generatorsContainer = document.getElementById('generators-container'); if (loadingElement) { loadingElement.style.display = 'none'; } if (generatorsContainer) { generatorsContainer.style.display = 'grid'; generatorsContainer.classList.add('generator-grid'); console.log('✅ Generator grid now visible'); } else { console.error('❌ generators-container element not found in DOM'); } } async loadAvailableWordlists() { try { console.log('🔄 Loading available wordlists from backend...'); const response = await fetch('/api/v1/passwords/wordlists'); if (!response.ok) { throw new Error(`API request failed: ${response.status}`); } const data = await response.json(); if (data.success && data.data.wordlists) { this.availableWordlists = data.data.wordlists; console.log('✅ Loaded wordlists:', Object.keys(this.availableWordlists)); } else { throw new Error('Invalid API response format'); } } catch (error) { console.warn('⚠️ Failed to load wordlists from API:', error.message); console.log('🔄 Using fallback wordlist configuration'); this.availableWordlists = { 'en': { name: 'English (Fallback)', description: 'Built-in English word list', available: false }, 'de': { name: 'German (Fallback)', description: 'Built-in German word list', available: false } }; } } setupGridObserver() { if ('IntersectionObserver' in window) { const gridObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const container = entry.target; this.updateGridLayout(container); } }); }, { rootMargin: '50px' }); const container = document.getElementById('generators-container'); if (container) { gridObserver.observe(container); } } } updateGridLayout(container) { const cardCount = container.querySelectorAll('.generator-card').length; const containerWidth = container.clientWidth; let layout = 'single'; if (containerWidth >= 1600 && cardCount >= 4) { layout = 'quad'; } else if (containerWidth >= 1200 && cardCount >= 3) { layout = 'triple'; } else if (containerWidth >= 768 && cardCount >= 2) { layout = 'dual'; } if (this.gridState.currentLayout !== layout) { this.gridState.currentLayout = layout; this.gridState.cardCount = cardCount; console.log(`📐 Grid layout: ${layout} (${cardCount} cards)`); } } updateFormFieldsForScreen() { const isMobile = window.innerWidth <= 768; const formControls = document.querySelectorAll('.form-control'); formControls.forEach(control => { if (isMobile) { if (control.type !== 'checkbox' && control.type !== 'radio') { control.classList.add('form-control--full'); control.classList.remove('form-control--sm', 'form-control--md', 'form-control--lg'); } } else { control.classList.remove('form-control--full'); if (control.type === 'number') { control.classList.add('form-control--sm'); } else if (control.tagName === 'SELECT') { if (control.dataset.option === 'language' || control.dataset.option === 'wordlist') { control.classList.add('form-control--language'); } else { control.classList.add('form-control--md'); } } } }); } async detectLanguage() { const browserLang = (navigator.language || 'en').replace('-', '_'); const supportedLanguages = ['en', 'de', 'de_CH', 'fr', 'it', 'es', 'pt', 'ja', 'zh']; if (supportedLanguages.includes(browserLang)) { this.currentLang = browserLang; } else if (supportedLanguages.includes(browserLang.substring(0, 2))) { this.currentLang = browserLang.substring(0, 2); } else { this.currentLang = 'en'; } console.log(`🌍 Language detected: ${this.currentLang}`); } async loadTranslations() { try { const response = await fetch(`/api/v1/locales/${this.currentLang}`); if (response.ok) { this.translations = await response.json(); console.log(`✅ Loaded translations for ${this.currentLang}`); } else { throw new Error(`Failed to load translations: ${response.status}`); } } catch (error) { console.warn(`⚠️ Failed to load ${this.currentLang} translations:`, error.message); this.translations = this.getFallbackTranslations(); } } getFallbackTranslations() { return { "app_title": "Password Generator", "generate": "Generate", "roulette": "Roulette", "stop": "Stop", "roulette_interval": "Interval (s)", "copy": "Copy", "copied_to_clipboard": "Copied to clipboard!", "generating": "Generating...", "count": "Count", "loading_generators": "Loading password generators...", "entropy": "Entropy", "length": "Length", "details": "Details", "security_level": "Security level", "very_strong": "Very strong", "strong": "Strong", "adequate": "Adequate", "weak": "Weak", "very_weak": "Very weak", "error_loading_generators": "Failed to load generators" }; } translateSecurityLevel(level) { const normalized = String(level ?? '') .trim() .toLowerCase() .replace(/[^a-z]+/g, '_') .replace(/^_+|_+$/g, ''); const translationKeys = { 'very_strong': 'very_strong', 'strong': 'strong', 'adequate': 'adequate', 'weak': 'weak', 'very_weak': 'very_weak', }; if (translationKeys[normalized]) { return this.t(translationKeys[normalized]); } return level ? String(level) : this.t('security_level'); } t(key, params = {}) { if (!key || typeof key !== 'string') { return ''; } const candidates = [key]; if (key.includes('_choice_')) { candidates.push(key.replace('_choice_', '_list_')); } if (key.includes('add_number')) { candidates.push(key.replace('add_number', 'addNumber')); } if (key.includes('number_position')) { candidates.push(key.replace('number_position', 'numberPosition')); } if (key.includes('_options_group_')) { candidates.push(''); } let translation = ''; for (const candidate of candidates) { if (!candidate) { continue; } const value = this.translations[candidate]; if (value !== undefined && value !== null) { translation = value; break; } } if (!translation && key.includes('_options_group_')) { return ''; } if (!translation) { translation = key; } Object.keys(params).forEach(param => { translation = String(translation).replace(`{{${param}}}`, params[param]); }); return translation; } updateLanguageSelector() { const languageSelect = document.getElementById('language-select'); if (!languageSelect) { console.warn('⚠️ Language selector not found'); return; } languageSelect.innerHTML = ''; Object.entries(this.defaultLanguages).forEach(([code, name]) => { const option = document.createElement('option'); option.value = code; option.textContent = name; option.selected = code === this.currentLang; languageSelect.appendChild(option); }); console.log('✅ Language selector updated'); } setupEventListeners() { // Language selector const languageSelect = document.getElementById('language-select'); if (languageSelect) { languageSelect.addEventListener('change', (e) => { this.switchLanguage(e.target.value); }); } // Event delegation for dynamic content document.addEventListener('click', (e) => { // Generate button handler const generateBtn = e.target.closest('.generate-btn'); if (generateBtn && !generateBtn.disabled) { e.preventDefault(); e.stopPropagation(); const type = generateBtn.dataset.type; if (type) { this.generatePasswords(type); } return; } // Roulette button handler const rouletteBtn = e.target.closest('.roulette-btn'); if (rouletteBtn && !rouletteBtn.disabled) { e.preventDefault(); e.stopPropagation(); const type = rouletteBtn.dataset.type; if (type) { this.toggleRoulette(type); } return; } const intervalIncBtn = e.target.closest('.roulette-interval-inc-btn'); if (intervalIncBtn) { e.preventDefault(); e.stopPropagation(); this.adjustRouletteInterval(intervalIncBtn, +1); return; } const intervalDecBtn = e.target.closest('.roulette-interval-dec-btn'); if (intervalDecBtn) { e.preventDefault(); e.stopPropagation(); this.adjustRouletteInterval(intervalDecBtn, -1); return; } // Copy button handler const copyBtn = e.target.closest('.copy-btn'); if (copyBtn) { e.preventDefault(); e.stopPropagation(); const card = copyBtn.closest('.generator-card'); if (card?.dataset?.type) { this.stopRoulette(card.dataset.type); } const text = copyBtn.dataset.text; if (text) { this.copyToClipboard(text, copyBtn); } return; } }); // Form changes document.addEventListener('change', (e) => { if (e.target.matches('input, select')) { this.updateConditionalOptions(e.target); } }); console.log('✅ Event listeners set up'); } async switchLanguage(lang) { this.currentLang = lang; await this.loadTranslations(); this.updateUI(); console.log(`🌍 Switched to language: ${lang}`); } updateUI() { // Update translatable elements document.querySelectorAll('[data-i18n]').forEach(element => { const key = element.dataset.i18n; element.textContent = this.t(key); }); // Update placeholders document.querySelectorAll('[data-i18n-placeholder]').forEach(element => { const key = element.dataset.i18nPlaceholder; element.placeholder = this.t(key); }); // Update titles document.querySelectorAll('[data-i18n-title]').forEach(element => { const key = element.dataset.i18nTitle; element.title = this.t(key); }); // Update page title document.title = this.t('app_title'); // Re-render generator cards with new translations this.renderGeneratorCards(); } updateConditionalOptions(changedElement) { const card = changedElement.closest('.generator-card'); if (!card) return; // Generic conditional option handling based on data attributes const optionName = changedElement.dataset.option; if (!optionName) return; // Find all elements that depend on this option const dependentElements = card.querySelectorAll(`[data-depends-on="${optionName}"]`); dependentElements.forEach(dependent => { const showWhen = dependent.dataset.showWhen; let shouldShow = false; if (changedElement.type === 'checkbox') { shouldShow = showWhen === 'checked' ? changedElement.checked : !changedElement.checked; } else if (changedElement.tagName === 'SELECT' || changedElement.type === 'text') { const value = changedElement.value; if (showWhen) { shouldShow = showWhen.split(',').includes(value); } else { shouldShow = value.trim().length > 0; } } dependent.style.display = shouldShow ? 'block' : 'none'; if (shouldShow) { dependent.classList.add('option-visible'); } else { dependent.classList.remove('option-visible'); } }); } /** * ✅ NEW: Render generator cards from API definitions */ renderGeneratorCards() { const container = document.getElementById('generators-container'); if (!container) { console.error('❌ Container not found'); return; } container.innerHTML = ''; if (!this.generatorDefinitions || this.generatorDefinitions.length === 0) { container.innerHTML = `
`; return; } console.log(`🎨 Rendering ${this.generatorDefinitions.length} generator cards...`); this.generatorDefinitions.forEach(generator => { const card = this.createGeneratorCard(generator); container.appendChild(card); }); console.log('✅ Generator cards rendered'); } /** * ✅ NEW: Create generator card from API definition */ createGeneratorCard(generator) { const card = document.createElement('div'); card.className = 'generator-card'; card.dataset.type = generator.id; // Header const header = document.createElement('div'); header.className = 'generator-header'; const title = document.createElement('h3'); title.className = 'generator-title'; const translatedTitle = this.t(generator.nameKey); title.textContent = translatedTitle && translatedTitle !== generator.nameKey ? translatedTitle : (generator.name || generator.id || 'Generator'); header.appendChild(title); const description = document.createElement('p'); description.className = 'generator-description'; const translatedDescription = this.t(generator.descriptionKey); description.textContent = translatedDescription && translatedDescription !== generator.descriptionKey ? translatedDescription : (generator.description || ''); header.appendChild(description); card.appendChild(header); // Options form const optionsContainer = document.createElement('div'); optionsContainer.className = 'generator-options'; // Group options by group const groupedOptions = this.groupOptions(generator.options); Object.entries(groupedOptions).forEach(([groupName, options]) => { const groupDiv = document.createElement('div'); groupDiv.className = 'option-group'; groupDiv.dataset.group = groupName; // Group title const groupTitle = document.createElement('h4'); groupTitle.className = 'option-group-title'; if (options[0].groupTitleKey) { const groupTitleKey = options[0].groupTitleKey; const translatedGroupTitle = this.t(groupTitleKey); groupTitle.textContent = translatedGroupTitle && translatedGroupTitle !== groupTitleKey ? translatedGroupTitle : ''; } else { groupTitle.textContent = ''; } const resolvedGroupTitle = this.getGroupDisplayTitle(groupName, groupTitle.textContent); if (this.shouldCollapseGroup(groupName)) { const collapsible = document.createElement('details'); collapsible.className = 'option-group-collapsible'; const summary = document.createElement('summary'); summary.className = 'option-group-summary'; summary.textContent = resolvedGroupTitle; collapsible.appendChild(summary); options.forEach(option => { const optionElement = this.renderOption(generator.id, option); collapsible.appendChild(optionElement); }); groupDiv.appendChild(collapsible); optionsContainer.appendChild(groupDiv); return; } groupTitle.textContent = resolvedGroupTitle; groupDiv.appendChild(groupTitle); // Render each option in group options.forEach(option => { const optionElement = this.renderOption(generator.id, option); groupDiv.appendChild(optionElement); }); optionsContainer.appendChild(groupDiv); }); card.appendChild(optionsContainer); // Generate button and results const actionsDiv = document.createElement('div'); actionsDiv.className = 'generator-actions'; const generateBtn = document.createElement('button'); generateBtn.className = 'generate-btn'; generateBtn.dataset.type = generator.id; generateBtn.textContent = this.t('generate'); actionsDiv.appendChild(generateBtn); const rouletteBtn = document.createElement('button'); rouletteBtn.className = 'roulette-btn'; rouletteBtn.dataset.type = generator.id; rouletteBtn.textContent = this.t('roulette'); actionsDiv.appendChild(rouletteBtn); card.appendChild(actionsDiv); const rouletteStatusRow = document.createElement('div'); rouletteStatusRow.className = 'roulette-status-row'; rouletteStatusRow.dataset.rouletteIntervalSeconds = String(this.defaultRouletteIntervalSeconds); const rouletteProgressSlot = document.createElement('div'); rouletteProgressSlot.className = 'roulette-progress-slot'; rouletteProgressSlot.setAttribute('aria-hidden', 'true'); const rouletteProgressBar = document.createElement('div'); rouletteProgressBar.className = 'roulette-progress-bar'; rouletteProgressSlot.appendChild(rouletteProgressBar); rouletteStatusRow.appendChild(rouletteProgressSlot); const rouletteSpeedControls = document.createElement('div'); rouletteSpeedControls.className = 'roulette-speed-controls'; const incBtn = document.createElement('button'); incBtn.type = 'button'; incBtn.className = 'roulette-speed-btn roulette-interval-inc-btn'; incBtn.dataset.type = generator.id; incBtn.textContent = '🐌'; incBtn.title = this.t('roulette_slow_down'); rouletteSpeedControls.appendChild(incBtn); const decBtn = document.createElement('button'); decBtn.type = 'button'; decBtn.className = 'roulette-speed-btn roulette-interval-dec-btn'; decBtn.dataset.type = generator.id; decBtn.textContent = '🐇'; decBtn.title = this.t('roulette_speed_up'); rouletteSpeedControls.appendChild(decBtn); rouletteStatusRow.appendChild(rouletteSpeedControls); card.appendChild(rouletteStatusRow); // Results container const resultsDiv = document.createElement('div'); resultsDiv.className = 'password-results'; resultsDiv.id = `${generator.id}-results`; card.appendChild(resultsDiv); return card; } /** * ✅ NEW: Group options by their group property */ groupOptions(options) { const grouped = {}; options.forEach(option => { const group = option.group || 'basic'; if (!grouped[group]) { grouped[group] = []; } grouped[group].push(option); }); return grouped; } shouldCollapseGroup(groupName) { const collapsibleGroups = ['advanced', 'ai', 'expert']; return collapsibleGroups.includes((groupName || '').toLowerCase()); } getGroupDisplayTitle(groupName, fallbackTitle) { if (fallbackTitle && fallbackTitle.trim().length > 0) { return fallbackTitle; } const groupTitleKeys = { advanced: 'collapse_group_advanced', ai: 'collapse_group_ai', expert: 'collapse_group_expert', basic: 'collapse_group_basic', character_set: 'collapse_group_character_set' }; const titleKey = groupTitleKeys[groupName]; if (titleKey) { const translated = this.t(titleKey); if (translated && translated !== titleKey) { return translated; } } return String(groupName || 'Options') .replace(/[_-]+/g, ' ') .replace(/\b\w/g, (c) => c.toUpperCase()); } /** * ✅ NEW: Render individual option based on type */ renderOption(generatorId, option) { const wrapper = document.createElement('div'); wrapper.className = 'form-group'; wrapper.dataset.optionName = option.name; // Handle special case: count option should be prominent if (option.name === 'count') { wrapper.classList.add('form-group--count'); } let input; switch (option.type) { case 'integer': input = this.createIntegerInput(option); break; case 'boolean': input = this.createBooleanInput(option); break; case 'string': input = this.createStringInput(option); break; case 'select': input = this.createSelectInput(option); break; default: console.warn(`Unknown option type: ${option.type}`); return wrapper; } input.id = `${generatorId}-${option.name}`; input.dataset.option = option.name; // For boolean inputs, wrap differently if (option.type === 'boolean') { const checkboxWrapper = document.createElement('div'); checkboxWrapper.className = 'checkbox-wrapper'; checkboxWrapper.appendChild(input); const checkboxContent = document.createElement('div'); checkboxContent.className = 'checkbox-content'; const label = document.createElement('label'); label.htmlFor = input.id; const translatedLabel = this.t(option.labelKey); label.textContent = translatedLabel && translatedLabel !== option.labelKey ? translatedLabel : (option.label || option.name || 'Option'); checkboxContent.appendChild(label); if (option.descriptionKey) { const help = document.createElement('small'); help.className = 'form-text text-muted'; const translatedHelp = this.t(option.descriptionKey); help.textContent = translatedHelp && translatedHelp !== option.descriptionKey ? translatedHelp : (option.description || ''); checkboxContent.appendChild(help); } checkboxWrapper.appendChild(checkboxContent); wrapper.appendChild(checkboxWrapper); } else { // Label first for non-checkbox inputs const label = document.createElement('label'); label.htmlFor = input.id; const translatedLabel = this.t(option.labelKey); label.textContent = translatedLabel && translatedLabel !== option.labelKey ? translatedLabel : (option.label || option.name || 'Option'); wrapper.appendChild(label); wrapper.appendChild(input); } // Description/help text if (option.descriptionKey && option.type !== 'boolean') { const help = document.createElement('small'); help.className = 'form-text text-muted'; const translatedHelp = this.t(option.descriptionKey); help.textContent = translatedHelp && translatedHelp !== option.descriptionKey ? translatedHelp : (option.description || ''); wrapper.appendChild(help); } return wrapper; } createIntegerInput(option) { const input = document.createElement('input'); input.type = 'number'; input.className = 'form-control form-control--sm'; input.value = option.default; if ('min' in option) input.min = option.min; if ('max' in option) input.max = option.max; if (option.required) input.required = true; return input; } createBooleanInput(option) { const input = document.createElement('input'); input.type = 'checkbox'; input.className = 'form-check-input'; input.checked = !!option.default; return input; } createStringInput(option) { const input = document.createElement('input'); input.type = 'text'; input.className = 'form-control form-control--md'; input.value = option.default || ''; if ('maxLength' in option) input.maxLength = option.maxLength; if ('minLength' in option) input.minLength = option.minLength; if (option.required) input.required = true; return input; } createSelectInput(option) { const select = document.createElement('select'); select.className = 'form-control form-control--md'; if (!option.choices || option.choices.length === 0) { console.warn(`No choices for select option: ${option.name}`); return select; } option.choices.forEach(choice => { const opt = document.createElement('option'); opt.value = choice.value; // ✅ Check if choices should be translated if (option.translateChoices === false || !choice.labelKey) { // Wordlists and other non-translated choices opt.textContent = choice.label || choice.value; } else { // Translated choices const translatedChoice = this.t(choice.labelKey); opt.textContent = translatedChoice && translatedChoice !== choice.labelKey ? translatedChoice : (choice.label || choice.value); } opt.selected = choice.value === option.default; select.appendChild(opt); }); return select; } /** * Collect form options for password generation */ collectFormOptions(card, type) { const options = {}; // Get all form inputs in this card const inputs = card.querySelectorAll('input, select'); inputs.forEach(input => { const optionName = input.dataset.option; if (!optionName) return; switch (input.type) { case 'checkbox': options[optionName] = input.checked; break; case 'number': case 'range': options[optionName] = parseInt(input.value) || 0; break; case 'text': case 'select-one': options[optionName] = input.value; break; } }); return options; } async generatePasswords(type) { console.log(`🎯 Generating passwords for type: ${type}`); const card = document.querySelector(`[data-type="${type}"]`); if (!card) { console.error('Generator card not found:', type); return; } const generateBtn = card.querySelector('.generate-btn'); if (!generateBtn) { console.error('Generate button not found'); return; } // Disable button and show loading state generateBtn.disabled = true; const originalText = generateBtn.textContent; generateBtn.textContent = this.t('generating'); try { // Collect options const options = this.collectFormOptions(card, type); // Extract count separately const countInput = card.querySelector(`[data-option="count"]`); const count = countInput ? parseInt(countInput.value) || 1 : 1; console.log(`📝 Options for ${type}:`, options); console.log(`📝 Count: ${count}`); // Make API request const response = await fetch('/api/v1/passwords/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: type, count: count, options: options }) }); if (!response.ok) { throw new Error(`API request failed: ${response.status}`); } const data = await response.json(); if (data.success && data.data.passwords) { this.displayPasswords(card, data.data); } else { throw new Error(data.message || 'Unknown error'); } } catch (error) { console.error('❌ Password generation failed:', error); this.showError('Failed to generate passwords: ' + error.message); } finally { // Re-enable button generateBtn.disabled = false; generateBtn.textContent = originalText; } } async toggleRoulette(type) { if (this.rouletteRuns.has(type)) { this.stopRoulette(type); return; } await this.startRoulette(type); } async startRoulette(type) { const card = document.querySelector(`[data-type="${type}"]`); if (!card) { return; } const run = { active: true, iterations: 0, maxIterations: 12, }; this.rouletteRuns.set(type, run); this.updateRouletteButtonState(card, true); try { while (run.active && run.iterations < run.maxIterations) { run.iterations += 1; await this.generatePasswords(type); if (!run.active || run.iterations >= run.maxIterations) { break; } const intervalMs = this.getRouletteIntervalMs(card); const completed = await this.runRouletteCountdown(type, intervalMs); if (!completed) { break; } } } finally { this.stopRoulette(type); } } stopRoulette(type) { const run = this.rouletteRuns.get(type); if (run) { run.active = false; if (run.countdownFrame) { cancelAnimationFrame(run.countdownFrame); } this.rouletteRuns.delete(type); } const card = document.querySelector(`[data-type="${type}"]`); if (card) { this.updateRouletteButtonState(card, false); this.setRouletteProgress(card, 0, false); } } updateRouletteButtonState(card, isRunning) { const rouletteBtn = card.querySelector('.roulette-btn'); const statusRow = card.querySelector('.roulette-status-row'); if (!rouletteBtn) { return; } rouletteBtn.classList.toggle('is-running', isRunning); rouletteBtn.textContent = isRunning ? this.t('stop') : this.t('roulette'); if (statusRow) { statusRow.classList.toggle('is-running', isRunning); } } getRouletteIntervalMs(card) { const statusRow = card.querySelector('.roulette-status-row'); const seconds = statusRow ? parseInt(statusRow.dataset.rouletteIntervalSeconds || '3', 10) : 3; const safeSeconds = Number.isFinite(seconds) ? Math.min(30, Math.max(1, seconds)) : 3; if (statusRow) { statusRow.dataset.rouletteIntervalSeconds = String(safeSeconds); } return safeSeconds * 1000; } adjustRouletteInterval(button, deltaSeconds) { const card = button.closest('.generator-card'); const statusRow = card?.querySelector('.roulette-status-row'); if (!statusRow) { return; } const current = parseInt(statusRow.dataset.rouletteIntervalSeconds || '3', 10) || 3; const next = Math.min(30, Math.max(1, current + deltaSeconds)); statusRow.dataset.rouletteIntervalSeconds = String(next); } setRouletteProgress(card, progress, active) { const slot = card.querySelector('.roulette-progress-slot'); const bar = card.querySelector('.roulette-progress-bar'); if (!slot || !bar) { return; } const normalized = Math.max(0, Math.min(1, progress)); bar.style.width = `${normalized * 100}%`; slot.classList.toggle('is-active', !!active); } async runRouletteCountdown(type, durationMs) { const run = this.rouletteRuns.get(type); const card = document.querySelector(`[data-type="${type}"]`); if (!run || !card) { return false; } this.setRouletteProgress(card, 1, true); return await new Promise((resolve) => { const startTime = performance.now(); const step = (now) => { const currentRun = this.rouletteRuns.get(type); if (!currentRun || !currentRun.active) { resolve(false); return; } const elapsed = now - startTime; const remainingRatio = 1 - (elapsed / durationMs); this.setRouletteProgress(card, remainingRatio, true); if (elapsed >= durationMs) { resolve(true); return; } currentRun.countdownFrame = requestAnimationFrame(step); }; run.countdownFrame = requestAnimationFrame(step); }); } delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } displayPasswords(card, payload) { const resultsDiv = card.querySelector('.password-results'); if (!resultsDiv) return; resultsDiv.innerHTML = ''; const normalizedPayload = Array.isArray(payload) ? { passwords: payload, explanations: [] } : (payload || { passwords: [], explanations: [] }); const passwords = Array.isArray(normalizedPayload.passwords) ? normalizedPayload.passwords : []; const explanations = Array.isArray(normalizedPayload.explanations) ? normalizedPayload.explanations : []; const securityAssessment = typeof normalizedPayload.security_assessment === 'string' ? normalizedPayload.security_assessment : ''; const rawTechnicalDetails = normalizedPayload.raw_technical_details || normalizedPayload.security_info || null; passwords.forEach((passwordData, index) => { const passwordValue = typeof passwordData === 'string' ? passwordData : (passwordData?.password || ''); if (!passwordValue) { return; } const resultItem = document.createElement('div'); resultItem.className = 'password-result-item'; const resultContent = document.createElement('div'); resultContent.className = 'password-result-content'; const passwordText = document.createElement('div'); passwordText.className = 'password-text'; passwordText.textContent = passwordValue; resultContent.appendChild(passwordText); const securityRow = document.createElement('div'); securityRow.className = 'password-security-row'; const securityLevel = rawTechnicalDetails?.security_level || ''; const localizedSecurityLevel = this.translateSecurityLevel(securityLevel); if (securityLevel || securityAssessment.trim().length > 0) { const assessmentBadge = document.createElement('div'); assessmentBadge.className = 'password-security-badge'; assessmentBadge.textContent = localizedSecurityLevel || securityAssessment.trim(); securityRow.appendChild(assessmentBadge); } if (rawTechnicalDetails && typeof rawTechnicalDetails === 'object') { const entropyValue = rawTechnicalDetails.entropy_bits ?? normalizedPayload.entropy_bits; const detailRows = []; if (typeof entropyValue !== 'undefined') { detailRows.push({ label: this.t('entropy'), value: `${Number(entropyValue).toFixed(1)} bits` }); } if (rawTechnicalDetails.length) { detailRows.push({ label: this.t('length'), value: `${rawTechnicalDetails.length} ${this.t('chars') || 'chars'}` }); } if (rawTechnicalDetails.security_level) { detailRows.push({ label: this.t('security_level'), value: this.translateSecurityLevel(rawTechnicalDetails.security_level) }); } if (detailRows.length > 0) { const technicalToggle = document.createElement('details'); technicalToggle.className = 'password-technical-toggle'; const summary = document.createElement('summary'); summary.className = 'password-technical-summary'; summary.textContent = this.t('details'); const technicalBlock = document.createElement('div'); technicalBlock.className = 'password-technical-details'; detailRows.forEach((detail) => { const rowEl = document.createElement('div'); rowEl.className = 'password-technical-row'; const labelEl = document.createElement('span'); labelEl.className = 'password-technical-label'; labelEl.textContent = detail.label; const valueEl = document.createElement('span'); valueEl.className = 'password-technical-value'; valueEl.textContent = detail.value; rowEl.appendChild(labelEl); rowEl.appendChild(valueEl); technicalBlock.appendChild(rowEl); }); technicalToggle.appendChild(summary); technicalToggle.appendChild(technicalBlock); securityRow.appendChild(technicalToggle); } } if (securityRow.children.length > 0) { resultContent.appendChild(securityRow); } const explanation = explanations[index]; if (typeof explanation === 'string' && explanation.trim().length > 0) { const explanationText = document.createElement('div'); explanationText.className = 'password-explanation'; explanationText.textContent = explanation.trim(); resultContent.appendChild(explanationText); } resultItem.appendChild(resultContent); const copyBtn = document.createElement('button'); copyBtn.className = 'copy-btn'; copyBtn.dataset.text = passwordValue; copyBtn.textContent = this.t('copy'); resultItem.appendChild(copyBtn); resultsDiv.appendChild(resultItem); }); } async copyToClipboard(text, button = null) { try { await navigator.clipboard.writeText(text); if (button) { const originalText = button.dataset.originalText || button.textContent || this.t('copy'); button.dataset.originalText = originalText; button.textContent = this.t('copied'); button.classList.add('success'); if (button._copyFeedbackTimeout) { clearTimeout(button._copyFeedbackTimeout); } button._copyFeedbackTimeout = setTimeout(() => { button.textContent = originalText; button.classList.remove('success'); button._copyFeedbackTimeout = null; }, 1400); } this.showSuccess(this.t('copied_to_clipboard')); } catch (error) { console.error('❌ Failed to copy:', error); this.showError('Failed to copy to clipboard'); } } showSuccess(message) { console.log('✅', message); // TODO: Implement toast notification } showError(message) { console.error('❌', message); // TODO: Implement toast notification } } // Initialize app when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { new PasswordGeneratorApp(); }); } else { new PasswordGeneratorApp(); }