Passowrd_manager / templates /analyse.html
pranit144's picture
Upload 23 files
f88a7bc verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Analyse Passwords - Secure Password Manager</title>
<!-- Ensure correct path to your CSS file -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<!-- Add Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--primary-color: #2563eb;
--primary-hover: #1d4ed8;
--success-color: #10b981;
--warning-color: #f59e0b;
--danger-color: #ef4444;
--text-primary: #1f2937;
--text-secondary: #4b5563;
--bg-card: #ffffff;
--border-color: #e5e7eb;
--transition: all 0.3s ease;
}
body {
font-family: 'Inter', sans-serif;
line-height: 1.6;
color: var(--text-primary);
background: #f3f4f6;
margin: 0;
padding: 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
header {
background: var(--bg-card);
border-radius: 1rem;
padding: 1.5rem;
margin-bottom: 2rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 2rem;
font-weight: 700;
color: var(--text-primary);
margin: 0;
display: flex;
align-items: center;
gap: 1rem;
}
.llm-badge {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: white;
padding: 0.25rem 0.75rem;
border-radius: 1rem;
font-size: 0.875rem;
font-weight: 500;
}
nav ul {
display: flex;
gap: 1rem;
padding: 0;
margin: 1rem 0 0;
list-style: none;
}
nav a {
color: var(--text-secondary);
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: var(--transition);
}
nav a:hover, nav a.active {
color: var(--primary-color);
background: #f0f9ff;
}
.card {
background: var(--bg-card);
border-radius: 1rem;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
transition: var(--transition);
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 1.5rem;
position: relative;
}
input[type="password"] {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid var(--border-color);
border-radius: 0.5rem;
font-size: 1rem;
transition: var(--transition);
}
input[type="password"]:focus {
border-color: var(--primary-color);
outline: none;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.toggle-button {
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
font-size: 0.875rem;
}
.btn {
background: var(--primary-color);
color: white;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 0.5rem;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
}
.btn:hover {
background: var(--primary-hover);
}
.btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.loading-indicator {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 2rem;
}
.spinner {
width: 2.5rem;
height: 2.5rem;
border: 3px solid #f3f3f3;
border-top: 3px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.analysis-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin: 1.5rem 0;
}
.analysis-table th,
.analysis-table td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.analysis-table th {
background: #f8fafc;
font-weight: 600;
color: var(--text-secondary);
}
.strength-bar {
background: #e5e7eb;
height: 0.5rem;
border-radius: 1rem;
overflow: hidden;
}
.strength-indicator {
height: 100%;
transition: width 0.3s ease;
}
.strength-indicator.very-weak { background: var(--danger-color); }
.strength-indicator.weak { background: #f97316; }
.strength-indicator.medium { background: var(--warning-color); }
.strength-indicator.strong { background: #84cc16; }
.strength-indicator.very-strong { background: var(--success-color); }
.feedback-item {
padding: 0.75rem;
margin-bottom: 0.5rem;
border-radius: 0.5rem;
font-size: 0.875rem;
}
.feedback-item.issue {
background: #fee2e2;
color: #991b1b;
}
.feedback-item.tip {
background: #e0f2fe;
color: #075985;
}
.breach-indicator {
display: inline-flex;
align-items: center;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
}
.breach-indicator.safe {
background: #dcfce7;
color: #166534;
}
.breach-indicator.pwned {
background: #fee2e2;
color: #991b1b;
}
.breach-indicator.error {
background: #fef3c7;
color: #92400e;
}
footer {
text-align: center;
padding: 2rem;
color: var(--text-secondary);
font-size: 0.875rem;
}
.logout-link {
float: right;
color: var(--text-secondary);
text-decoration: none;
font-size: 0.875rem;
transition: var(--transition);
}
.logout-link:hover {
color: var(--primary-color);
}
/* Responsive Design */
@media (max-width: 768px) {
.container {
padding: 1rem;
}
.card {
padding: 1.5rem;
}
.analysis-table {
display: block;
overflow-x: auto;
}
nav ul {
flex-direction: column;
}
nav a {
display: block;
}
}
.header-content {
width: 100%;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.user-email {
color: var(--text-secondary);
margin-right: 0.5rem;
}
.logout-text {
color: var(--danger-color);
}
.card-header {
margin-bottom: 2rem;
}
.card-description {
color: var(--text-secondary);
margin: 0.5rem 0 0;
font-size: 0.95rem;
}
.analysis-form {
max-width: 600px;
}
.password-input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.form-actions {
display: flex;
justify-content: flex-start;
gap: 1rem;
}
.btn-primary {
background: var(--primary-color);
}
.btn-secondary {
background: #4b5563;
}
.card-actions {
margin: 1.5rem 0;
}
.result-container {
background: #f8fafc;
border-radius: 0.5rem;
padding: 1rem;
margin-top: 1rem;
}
.table-container {
overflow-x: auto;
background: #f8fafc;
border-radius: 0.5rem;
padding: 1rem;
margin-top: 1rem;
}
.password-cell {
font-family: monospace;
font-size: 1rem;
}
.assessment-cell {
font-weight: 500;
}
.feedback-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.status-message {
margin: 1rem 0;
padding: 0.75rem;
border-radius: 0.5rem;
font-size: 0.875rem;
background: #f0f9ff;
color: var(--primary-color);
}
/* Enhanced Loading Animation */
.loading-indicator {
background: rgba(255, 255, 255, 0.9);
border-radius: 0.5rem;
}
.loading-indicator p {
color: var(--text-secondary);
font-size: 0.875rem;
margin: 0;
}
/* Improved Table Styles */
.analysis-table th:first-child,
.analysis-table td:first-child {
border-top-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
}
.analysis-table th:last-child,
.analysis-table td:last-child {
border-top-right-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
}
/* Strength Bar Enhancements */
.strength-bar-table {
width: 100%;
height: 0.375rem;
background: #e5e7eb;
border-radius: 0.25rem;
overflow: hidden;
}
.strength-indicator-table {
height: 100%;
transition: width 0.3s ease, background-color 0.3s ease;
}
/* Responsive Enhancements */
@media (max-width: 768px) {
.header-top {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.logout-link {
float: none;
display: inline-block;
margin-top: 0.5rem;
}
.card {
padding: 1.25rem;
}
.form-actions {
flex-direction: column;
}
.btn {
width: 100%;
}
.analysis-table {
font-size: 0.875rem;
}
.analysis-table th,
.analysis-table td {
padding: 0.75rem;
}
}
/* Dark Mode Support (if needed) */
@media (prefers-color-scheme: dark) {
:root {
--bg-card: #1f2937;
--text-primary: #f3f4f6;
--text-secondary: #9ca3af;
--border-color: #374151;
}
body {
background: #111827;
}
.card {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.2);
}
input[type="password"] {
background: #374151;
color: #f3f4f6;
}
.analysis-table th {
background: #374151;
}
.result-container,
.table-container {
background: #1f2937;
}
.status-message {
background: #1e3a8a;
color: #93c5fd;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="header-content">
<div class="header-top">
<h1>Password Analysis <span class="llm-badge">AI Insights</span></h1>
{% if current_user.is_authenticated %}
<a href="{{ url_for('logout') }}" class="logout-link">
<span class="user-email">{{ current_user.email }}</span>
<span class="logout-text">Logout</span>
</a>
{% endif %}
</div>
<nav>
<ul>
<li><a href="{{ url_for('add_password_page') }}">Add Password</a></li>
<li><a href="{{ url_for('storage') }}">View Passwords</a></li>
<li><a href="{{ url_for('analyse') }}" class="active">Analyse Passwords</a></li>
</ul>
</nav>
</div>
</header>
<main>
<!-- Single Password Analysis Card -->
<section class="card analysis-card">
<div class="card-header">
<h2>Analyse a Single Password</h2>
<p class="card-description">Enter a password below to get an instant strength analysis using AI insights and check if it has been compromised in known data breaches. The password itself is not stored or sent after analysis.</p>
</div>
<form id="analyse-form" class="analysis-form">
<div class="form-group">
<label for="analyse-password">Password to Analyse:</label>
<div class="password-input-wrapper">
<input type="password" id="analyse-password" name="analyse-password" required autocomplete="new-password" placeholder="Enter password to analyse">
<button type="button" id="toggle-analyse-password" class="toggle-button" title="Show/hide password">Show</button>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Analyse Password</button>
</div>
</form>
<div id="single-loading" class="loading-indicator" style="display: none;">
<div class="spinner"></div>
<p>Analysing your password...</p>
</div>
<div id="single-analysis-result" class="analysis-result" style="display: none;">
<h3>Analysis Result</h3>
<div class="result-container">
<table class="analysis-table">
<thead>
<tr>
<th>Password</th>
<th>Strength</th>
<th>Assessment</th>
<th>AI Insights & Suggestions</th>
<th>Breach Status</th>
</tr>
</thead>
<tbody>
<tr>
<td id="password-value" class="password-cell">*****</td>
<td>
<div class="strength-bar">
<div id="strength-indicator" class="strength-indicator"></div>
</div>
</td>
<td id="strength-text" class="assessment-cell">Medium</td>
<td id="feedback-cell">
<div id="feedback-list" class="feedback-list"></div>
</td>
<td class="breach-cell">
<span id="single-breach-indicator" class="breach-indicator"></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- All Stored Passwords Analysis Card -->
<section class="card analysis-card">
<div class="card-header">
<h2>Analyse All Stored Credentials</h2>
<p class="card-description">Decrypt and analyse all your stored credentials to identify potential weaknesses and check for compromises across your accounts. This may take some time depending on the number of credentials.</p>
</div>
<div class="card-actions">
<button id="analyse-all-btn" class="btn btn-secondary">Load & Analyse All Stored Credentials</button>
</div>
<div id="all-passwords-loading" class="loading-indicator" style="display: none;">
<div class="spinner"></div>
<p>Loading, decrypting, and analysing all credentials...</p>
</div>
<div id="all-analysis-status" class="status-message"></div>
<div id="all-passwords-analysis" class="analysis-result" style="display: none;">
<h3>Stored Credentials Analysis</h3>
<div class="table-container">
<table class="analysis-table" id="password-analysis-table">
<thead>
<tr>
<th>Service/Website</th>
<th>Username/Email</th>
<th>Strength</th>
<th>Assessment</th>
<th>AI Insights & Suggestions</th>
<th>Breach Status</th>
</tr>
</thead>
<tbody id="password-analysis-list"></tbody>
</table>
</div>
</div>
</section>
</main>
<footer>
<p>Secure Password Manager - End-to-End Encrypted Password Management</p>
</footer>
</div>
<script src="{{ url_for('static', filename='js/crypto-helpers.js') }}"></script>
<script>
// --- E2EE Helper Functions (Assume they are loaded from crypto-helpers.js) ---
// Includes: base64UrlDecode, decryptData, base64ToArrayBuffer,
// arrayBufferToBase64, escapeHtml, sha1Hash, checkHIBPPassword
// --- Client-Side Password Analysis Helpers ---
function getPasswordCharacteristics(password) {
let comp = { lowercase: 0, uppercase: 0, digits: 0, special: 0 };
if (!password || typeof password !== 'string') return { length: 0, composition: comp };
for (let i = 0; i < password.length; i++) {
const char = password[i];
if (char >= 'a' && char <= 'z') comp.lowercase++; else if (char >= 'A' && char <= 'Z') comp.uppercase++;
else if (char >= '0' && char <= '9') comp.digits++; else if (!char.match(/^[a-zA-Z0-9\s]$/)) comp.special++;
} return { length: password.length, composition: comp };
}
function getStrengthClass(score) { // Maps 1-5 score to CSS class
if (score <= 1) return 'very-weak'; if (score === 2) return 'weak'; if (score === 3) return 'medium';
if (score === 4) return 'strong'; return 'very-strong';
}
function formatFeedbackHtml(feedbackArray) { // Formats LLM/Basic feedback
if (!feedbackArray || feedbackArray.length === 0) return '<div class="feedback-item tip">No specific issues found.</div>';
let html = '<div class="feedback-section">';
feedbackArray.forEach(fb => {
let itemClass = 'feedback-item'; let text = fb;
if (fb.toLowerCase().startsWith('issue:') || fb.toLowerCase().startsWith('warning:')) { itemClass += ' issue'; text = fb.substring(fb.indexOf(':') + 1).trim(); }
else if (fb.toLowerCase().startsWith('tip:') || fb.toLowerCase().startsWith('suggestion:')) { itemClass += ' tip'; text = fb.substring(fb.indexOf(':') + 1).trim(); }
html += `<div class="${itemClass}">${escapeHtml(text)}</div>`;
});
html += '</div>';
return html;
}
// --- DOMContentLoaded ---
document.addEventListener('DOMContentLoaded', function() {
const flaskKey = "{{ session.get('encryption_key', 'null') }}";
if (flaskKey !== 'null' && !sessionStorage.getItem('encryptionKey')) sessionStorage.setItem('encryptionKey', flaskKey);
else if (!sessionStorage.getItem('encryptionKey')) { console.warn("Key missing, redirecting."); window.location.href = "{{ url_for('login') }}"; return; }
// Single Analysis Elements
const analyseForm = document.getElementById('analyse-form');
const togglePasswordBtn = document.getElementById('toggle-analyse-password');
const passwordInput = document.getElementById('analyse-password');
const singleAnalysisResultEl = document.getElementById('single-analysis-result');
const singleLoadingEl = document.getElementById('single-loading');
const strengthIndicator = document.getElementById('strength-indicator');
const strengthText = document.getElementById('strength-text');
const feedbackList = document.getElementById('feedback-list');
const passwordValueCell = document.getElementById('password-value');
const singleBreachIndicator = document.getElementById('single-breach-indicator'); // Get breach element
const singleSubmitBtn = analyseForm.querySelector('button[type="submit"]');
// All Analysis Elements
const analyseAllBtn = document.getElementById('analyse-all-btn');
const allPasswordsLoadingEl = document.getElementById('all-passwords-loading');
const allPasswordsAnalysisEl = document.getElementById('all-passwords-analysis');
const passwordAnalysisListBody = document.getElementById('password-analysis-list');
const allAnalysisStatus = document.getElementById('all-analysis-status');
// --- Single Analysis Logic ---
togglePasswordBtn.addEventListener('click', function() {
const type = passwordInput.type === 'password' ? 'text' : 'password';
passwordInput.type = type; this.textContent = type === 'password' ? 'Show' : 'Hide';
});
analyseForm.addEventListener('submit', async function(e) {
e.preventDefault();
const password = passwordInput.value;
if (!password) return;
singleLoadingEl.style.display = 'block';
singleAnalysisResultEl.style.display = 'none';
singleSubmitBtn.disabled = true;
passwordValueCell.textContent = password.replace(/./g, '*'); // Mask password in table
// Reset previous results
strengthIndicator.style.width = '0%';
strengthIndicator.className = 'strength-indicator';
strengthText.textContent = 'Checking...';
feedbackList.innerHTML = '';
singleBreachIndicator.textContent = 'Checking...';
singleBreachIndicator.className = 'breach-indicator loading';
try {
const characteristics = getPasswordCharacteristics(password);
// --- Perform Checks Concurrently ---
const analysisPromise = fetch("{{ url_for('analyse_password_api') }}", {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ characteristics: characteristics })
});
// Ensure crypto-helpers is loaded and function exists
const breachPromise = typeof checkHIBPPassword === 'function'
? checkHIBPPassword(password)
: Promise.resolve({ error: "Breach check function not available." }); // Fallback
// --- Process Analysis Results ---
const analysisResponse = await analysisPromise;
if (!analysisResponse.ok) {
const err = await analysisResponse.json().catch(() => ({}));
console.error(`Analysis API failed: ${analysisResponse.status} ${err.error || ''}`);
strengthText.textContent = 'Error';
feedbackList.innerHTML = `<div class="feedback-item issue">Analysis Failed</div>`;
strengthIndicator.className = 'strength-indicator very-weak';
} else {
const analysisResult = await analysisResponse.json();
updateSingleAnalysisUI(analysisResult); // Update strength part of UI
}
// --- Process Breach Results ---
const breachResult = await breachPromise;
if (breachResult.error) {
singleBreachIndicator.textContent = `Error: ${escapeHtml(breachResult.error)}`;
singleBreachIndicator.className = 'breach-indicator error';
} else if (breachResult.isPwned) {
singleBreachIndicator.textContent = `Compromised! Found in ${breachResult.count} breach${breachResult.count > 1 ? 'es' : ''}.`;
singleBreachIndicator.className = 'breach-indicator pwned';
} else {
singleBreachIndicator.textContent = 'Not found in known breaches.';
singleBreachIndicator.className = 'breach-indicator safe';
}
} catch (error) {
console.error('Single analysis processing error:', error);
// Display generic error if fetch/processing fails broadly
strengthText.textContent = 'Error';
feedbackList.innerHTML = `<div class="feedback-item issue">Failed: ${escapeHtml(error.message)}</div>`;
singleBreachIndicator.textContent = 'Error checking.';
singleBreachIndicator.className = 'breach-indicator error';
strengthIndicator.className = 'strength-indicator very-weak';
} finally {
singleLoadingEl.style.display = 'none';
singleAnalysisResultEl.style.display = 'block'; // Show results table
singleSubmitBtn.disabled = false;
}
});
function updateSingleAnalysisUI(data) { // Only updates strength part now
const score = data.strength || 1;
strengthIndicator.style.width = (score / 5) * 100 + '%';
strengthIndicator.className = 'strength-indicator ' + getStrengthClass(score);
strengthText.textContent = data.assessment || 'Unknown';
if (feedbackList) feedbackList.innerHTML = formatFeedbackHtml(data.feedback);
}
// --- All Analysis Logic ---
analyseAllBtn.addEventListener('click', async function() {
allPasswordsLoadingEl.style.display = 'block'; allPasswordsAnalysisEl.style.display = 'none';
passwordAnalysisListBody.innerHTML = ''; allAnalysisStatus.textContent = ''; allAnalysisStatus.style.color = '';
analyseAllBtn.disabled = true;
try {
// ***** CORRECTED KEY RETRIEVAL *****
const keyB64Url = sessionStorage.getItem('encryptionKey');
if (!keyB64Url) {
console.error("Encryption key missing from sessionStorage.");
throw new Error("Key missing. Please login again.");
}
let encryptionKey = null;
try {
// Assuming base64UrlDecode is loaded from crypto-helpers.js
encryptionKey = base64UrlDecode(keyB64Url);
} catch (e) {
console.error("Failed to decode encryption key:", e);
throw new Error("Invalid encryption key format. Please login again.");
}
if (!encryptionKey) { // Should not happen if decode worked, but for safety
throw new Error("Key decoding failed unexpectedly. Please login again.");
}
// ***** END KEY RETRIEVAL CORRECTION *****
allAnalysisStatus.textContent = 'Fetching credentials...';
const credResponse = await fetch("{{ url_for('get_credentials') }}");
if (!credResponse.ok) {
if (credResponse.status === 401) {
throw new Error('Unauthorized (401). Session likely expired. Please re-login.');
}
throw new Error(`Failed to fetch credentials (${credResponse.status})`);
}
const encryptedCredentials = await credResponse.json();
if (!encryptedCredentials || encryptedCredentials.length === 0) {
allAnalysisStatus.textContent = 'No stored credentials found.'; allPasswordsLoadingEl.style.display = 'none'; analyseAllBtn.disabled = false; return;
}
allAnalysisStatus.textContent = `Found ${encryptedCredentials.length}. Analysing...`;
let analysedCount = 0; const totalCount = encryptedCredentials.length;
allPasswordsAnalysisEl.style.display = 'block'; // Show table header
// Use Promise.allSettled for concurrency
const analysisPromises = encryptedCredentials.map(async (cred) => {
const decrypted = await decryptData(encryptionKey, cred.encrypted_data); // Assumes decryptData is loaded
let analysisResult = { strength: 0, assessment: 'Error', feedback: ['Issue: Processing Error'] };
let breachResult = { isPwned: false, count: null, error: "Not checked" };
let serviceName = cred.service_hint || `(Hint: ${cred.id.substring(0,8)})`;
let userName = '(Unknown)';
let passwordForChecks = null;
if (decrypted) {
serviceName = decrypted.service || serviceName;
userName = decrypted.username || userName;
passwordForChecks = decrypted.password || null;
if (passwordForChecks) {
// Perform LLM/Basic Analysis (async)
const characteristics = getPasswordCharacteristics(passwordForChecks);
const analysisApiPromise = fetch("{{ url_for('analyse_password_api') }}", {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ characteristics: characteristics })
}).then(async resp => {
if (resp.ok) return await resp.json();
console.warn(`Analysis API failed for ${serviceName}: ${resp.status}`);
return { strength: 0, assessment: 'API Error', feedback: ['Issue: Analysis service failed.'] };
}).catch(apiError => {
console.error(`Fetch error for analysis API (${serviceName}):`, apiError);
return { strength: 0, assessment: 'Fetch Error', feedback: [`Issue: ${apiError.message}`] };
});
// Perform HIBP Breach Check (async)
const breachCheckPromise = (typeof checkHIBPPassword === 'function')
? checkHIBPPassword(passwordForChecks)
.catch(hibpError => {
console.error("Error during HIBP check for", serviceName, hibpError);
return { isPwned: false, count: null, error: "Check failed" };
})
: Promise.resolve({ isPwned: false, count: null, error: "Checker fn missing" });
// Await both results
[analysisResult, breachResult] = await Promise.all([analysisApiPromise, breachCheckPromise]);
} else {
analysisResult = { strength: 0, assessment: 'No Password', feedback: ['Issue: No password found.'] };
breachResult = { isPwned: false, count: null, error: "No password" };
}
} else {
analysisResult = { strength: 0, assessment: 'Decrypt Error', feedback: ['Issue: Could not decrypt data.'] };
userName = '(Decryption Failed)';
breachResult = { isPwned: false, count: null, error: "Decryption failed" };
}
analysedCount++;
allAnalysisStatus.textContent = `Analysed ${analysedCount} of ${totalCount}...`;
return { service: serviceName, username: userName, analysis: analysisResult, breach: breachResult };
}); // End of map
// Wait for all analyses to settle
const settledResults = await Promise.allSettled(analysisPromises);
const finalResults = settledResults
.filter(result => result.status === 'fulfilled')
.map(result => result.value);
settledResults.forEach((result, index) => {
if(result.status === 'rejected') { console.error(`Analysis promise rejected for index ${index}:`, result.reason); }
});
displayAllAnalysisResults(finalResults);
allAnalysisStatus.textContent = `Analysis complete for ${finalResults.length} credentials.`;
} catch (error) {
console.error('All analysis processing error:', error); // Log the full error object
allAnalysisStatus.textContent = `Error: ${error.message || 'An unknown error occurred'}`; // Display message
allAnalysisStatus.style.color = 'red';
if (error.message.includes('Unauthorized (401)')) {
setTimeout(() => window.location.href = "{{ url_for('login') }}", 3000);
}
} finally {
allPasswordsLoadingEl.style.display = 'none';
analyseAllBtn.disabled = false;
}
});
// --- Modify displayAllAnalysisResults ---
function displayAllAnalysisResults(results) {
passwordAnalysisListBody.innerHTML = '';
allPasswordsAnalysisEl.style.display = 'block';
if (results.length === 0) {
passwordAnalysisListBody.innerHTML = '<tr><td colspan="6">No results to display.</td></tr>'; // Colspan is 6
return;
}
results.forEach((item) => {
const row = passwordAnalysisListBody.insertRow();
const analysis = item.analysis || { strength: 0, assessment: 'N/A', feedback: [] };
const breach = item.breach || { isPwned: false, count: null, error: "Unknown" };
const score = analysis.strength || 0;
const strengthClass = getStrengthClass(score);
const feedbackHtml = formatFeedbackHtml(analysis.feedback);
// Breach Status Cell Content
let breachHtml = ''; let breachClass = ''; let breachTitle = '';
if (breach.error) {
breachHtml = `Error`;
breachClass = 'error';
breachTitle = escapeHtml(breach.error);
} else if (breach.isPwned) {
breachHtml = `Compromised! (${breach.count})`;
breachClass = 'pwned';
breachTitle = `Found in ${breach.count} known breach(es).`;
} else {
breachHtml = `Not Found`;
breachClass = 'safe';
breachTitle = 'Not found in known breaches.';
}
row.innerHTML = `
<td>${escapeHtml(item.service)}</td>
<td>${escapeHtml(item.username)}</td>
<td><div class="strength-bar-table" title="Strength: ${score}/5"><div class="strength-indicator-table ${strengthClass}" style="width: ${(score/5)*100}%"></div></div></td>
<td>${escapeHtml(analysis.assessment || 'N/A')}</td>
<td>${feedbackHtml}</td>
<td class="breach-cell breach-${breachClass}" title="${breachTitle}"><span>${breachHtml}</span></td> <!-- Added Breach Cell with title -->
`;
});
}
}); // End DOMContentLoaded
</script>
</body>
</html>