Files
aliasvault/apps/server/AliasVault.Client/wwwroot/index.template.html

392 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<!--
╔═════════════════════════════════════════════════╗
║ _ _ _ __ __ _ _ ║
║ / \ | (_) __ _ __\ \ / /_ _ _ _| | |_ ║
║ / _ \ | | |/ _` / __\ \ / / _` | | | | | __| ║
║ / ___ \| | | (_| \__ \\ V / (_| | |_| | | |_ ║
║ /_/ \_\_|_|\__,_|___/ \_/ \__,_|\__,_|_|\__| ║
║ ║
╚═════════════════════════════════════════════════╝
AliasVault - Privacy-first password manager
https://github.com/aliasvault/aliasvault | AGPLv3
Build: @BuildVersion
-->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
<title>AliasVault</title>
<base href="/" />
<link rel="stylesheet" href="css/app.css?v=@CacheBuster" />
<link rel="icon" type="image/png" href="favicon.png" />
<link href="css/tailwind.css?v=@CacheBuster" rel="stylesheet">
<link rel="manifest" href="manifest.json?v=@CacheBuster" />
<link rel="apple-touch-icon" sizes="500x500" href="img/icon-500.png" />
<link rel="apple-touch-icon" sizes="192x192" href="img/icon-192.png" />
<!-- Import map for .NET 10 fingerprinted static assets -->
<script type="importmap"></script>
<link rel="prefetch" href="/wasm/aliasvault_core_bg.wasm?v=@CacheBuster" as="fetch" type="application/wasm" crossorigin="anonymous" />
<link rel="modulepreload" href="/wasm/aliasvault_core.js?v=@CacheBuster" />
<style>
.index-aliasvault-inline-spinner {
height: 51px;
width: 112px;
display: flex;
justify-content: center;
align-items: center;
}
.index-cloud-shape-inline {
border: 6px solid #eabf69;
border-radius: 9999px;
padding: 13px 26px;
display: flex;
gap: 10px;
align-items: center;
background-color: transparent;
box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.1);
}
.index-dot-inline {
width: 10px;
height: 10px;
border-radius: 9999px;
background-color: #eabf69;
animation: index-pulse-inverted 1.4s infinite ease-in-out;
}
.index-delay-1 { animation-delay: 0s; }
.index-delay-2 { animation-delay: 0.2s; }
.index-delay-3 { animation-delay: 0.4s; }
.index-delay-4 { animation-delay: 0.6s; }
@keyframes index-pulse-inverted {
0%, 100% { opacity: 0.3; transform: scale(1); }
50% { opacity: 1; transform: scale(1.3); }
}
</style>
</head>
<body class="bg-gray-100 dark:bg-gray-900" av-disable="true">
<div id="loading-screen">
<div class="fixed inset-0 flex items-center justify-center px-6 pt-8 pb-8">
<div class="w-full max-w-md space-y-4">
<div class="p-6 sm:p-8 bg-white rounded-lg shadow dark:bg-gray-800">
<div class="text-center">
<div class="inner">
<div class="index-aliasvault-inline-spinner mx-auto">
<div class="index-cloud-shape-inline">
<div class="index-dot-inline index-delay-1"></div>
<div class="index-dot-inline index-delay-2"></div>
<div class="index-dot-inline index-delay-3"></div>
<div class="index-dot-inline index-delay-4"></div>
</div>
</div>
<h2 id="loading-title" class="mt-4 text-xl font-semibold text-gray-900 dark:text-white">AliasVault is loading</h2>
<p id="loading-message" class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Initializing secure environment. AliasVault prioritizes your privacy by running entirely in your browser. The first load might take a short while.
</p>
<div class="loading-progress-text text-sm font-medium text-gray-700 dark:text-gray-300 mt-4"></div>
<div class="mt-4 text-center">
<p id="security-quote" class="text-sm text-primary-600 italic"></p>
</div>
<div id="error-message" class="hidden text-red-600 dark:text-red-400 mt-4"></div>
</div>
</div>
</div>
<div id="refresh-button" class="text-center w-full mt-4 p-6 sm:p-8 bg-white rounded-lg shadow dark:bg-gray-800 hidden">
<p id="refresh-text" class="text-sm text-gray-600 dark:text-gray-400 mb-2">If loading seems stuck, you can click the button below to refresh the page.</p>
<a href="javascript:window.location.reload(true)" class="inline-block px-4 py-2 bg-primary-600 text-white text-sm font-medium rounded-md hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 transition duration-150 ease-in-out">
<span id="refresh-button-text">Refresh Page</span>
</a>
</div>
</div>
</div>
</div>
<div id="app">
</div>
<div id="blazor-error-ui" class="text-white bg-red-700 dark:bg-red-900 p-6 border-t-2 border-red-500 dark:border-red-800">
<div class="container mx-auto max-w-screen-xl px-4">
<div class="flex flex-col sm:flex-row items-center justify-between gap-4">
<div class="flex items-center" role="alert">
<svg class="w-8 h-8 text-white mr-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<span id="unhandled-error-text">An unhandled error has occurred. Please try reloading the page. If the issue persists, please contact support.</span>
</div>
<div class="flex items-center gap-4">
<a href="javascript:location.reload()" class="reload flex items-center px-4 py-2 bg-red-600 hover:bg-red-700 rounded-md transition-colors duration-150">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
<span id="reload-page-text">Reload Page</span>
</a>
<button type="button" class="dismiss hover:text-red-200" aria-label="Dismiss error">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
</div>
</div>
<script>
// Cache buster value for dynamic fetches (set early for use in loadTranslations)
window.__CACHE_BUSTER__ = '@CacheBuster';
let translations = {};
function getCurrentLanguage() {
return localStorage.getItem('blazorCulture') ||
localStorage.getItem('AppLanguage') ||
'en';
}
async function loadTranslations() {
const lang = getCurrentLanguage();
try {
const response = await fetch(`/locales/${lang}.json?v=${window.__CACHE_BUSTER__}`);
if (response.ok) {
translations = await response.json();
} else {
const fallbackResponse = await fetch(`/locales/en.json?v=${window.__CACHE_BUSTER__}`);
translations = await fallbackResponse.json();
}
} catch (error) {
console.warn('Failed to load localization files, using fallback strings:', error);
// Fallback translations
translations = {
loading: {
title: 'AliasVault is loading',
message: 'Initializing secure environment. AliasVault prioritizes your privacy by running entirely in your browser. The first load might take a short while.',
refreshText: 'If loading seems stuck, you can click the button below to refresh the page.',
refreshButtonText: 'Refresh Page'
},
errors: {
unhandledError: 'An unhandled error has occurred. Please try reloading the page. If the issue persists, please contact support.',
webAssemblyError: 'AliasVault requires WebAssembly, which this browser does not support. Try using a more modern browser that supports WebAssembly.',
reloadPageText: 'Reload Page'
},
quotes: {
security: [
"Your identity is your most valuable asset. Protect it like one.",
"In the digital world, a strong password is your first line of defense.",
"Security is not a product, but a process."
]
}
};
}
}
function getTranslation(path) {
const keys = path.split('.');
let value = translations;
for (const key of keys) {
value = value?.[key];
if (!value) {
break;
}
}
return value || '';
}
function localizeContent() {
const localizations = [
{ id: 'loading-title', key: 'loading.title' },
{ id: 'loading-message', key: 'loading.message' },
{ id: 'refresh-text', key: 'loading.refreshText' },
{ id: 'refresh-button-text', key: 'loading.refreshButtonText' },
{ id: 'unhandled-error-text', key: 'errors.unhandledError' },
{ id: 'reload-page-text', key: 'errors.reloadPageText' }
];
localizations.forEach(({ id, key }) => {
const element = document.getElementById(id);
if (element) {
element.textContent = getTranslation(key);
}
});
const securityQuotes = getTranslation('quotes.security');
if (securityQuotes && securityQuotes.length > 0) {
const quoteElement = document.getElementById('security-quote');
const randomIndex = Math.floor(Math.random() * securityQuotes.length);
if (quoteElement) {
quoteElement.textContent = `"${securityQuotes[randomIndex]}"`;
}
}
}
async function initializeLocalization() {
await loadTranslations();
localizeContent();
}
// Initialize localization
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeLocalization);
} else {
initializeLocalization();
}
function manageLoadingScreen() {
const startTime = new Date().getTime();
const minDisplayTime = 1000;
const checkInterval = 500;
const refreshButtonTimeout = 300000;
const scriptsCheckTimeout = 5000;
const appElement = document.getElementById('app');
const refreshButton = document.getElementById('refresh-button');
appElement.style.visibility = 'hidden';
loadingScreen.style.display = 'flex';
// Show refresh button after 30 seconds
setTimeout(() => {
refreshButton.classList.remove('hidden');
}, refreshButtonTimeout);
// Add click event listener to refresh button
refreshButton.addEventListener('click', () => {
window.location.reload();
});
const checkContentAndTime = () => {
const elapsedTime = new Date().getTime() - startTime;
const hasContent = appElement.innerHTML.trim() !== '';
if (elapsedTime >= minDisplayTime && hasContent) {
loadingScreen.style.display = 'none';
appElement.style.removeProperty('visibility');
clearInterval(intervalId);
} else if (elapsedTime % 1000 < checkInterval) {
if (!('WebAssembly' in window)) {
showError(getTranslation('errors.webAssemblyError'));
clearInterval(intervalId);
}
}
};
const intervalId = setInterval(checkContentAndTime, checkInterval);
}
const loadingScreen = document.getElementById('loading-screen');
const errorMessageElement = document.getElementById('error-message');
const showError = (message) => {
if (errorMessageElement) {
errorMessageElement.textContent = message;
errorMessageElement.classList.remove('hidden');
}
const progressText = document.querySelector('.loading-progress-text');
if (progressText) progressText.classList.add('hidden');
};
// Boot failure recovery for stale service worker cache or network issues
// Use sessionStorage to prevent redirect loops - flag persists across page reloads within session
const BOOT_FAILURE_KEY = 'aliasvault_boot_failure_handled';
let bootFailureHandled = sessionStorage.getItem(BOOT_FAILURE_KEY) === 'true';
async function handleBlazorBootFailure() {
if (bootFailureHandled) {
// Already attempted recovery once this session, don't loop
console.warn('AliasVault: Boot failure recovery already attempted. Showing error to user.');
showError('Failed to load application. Please try clearing your browser cache and refreshing the page.');
return;
}
bootFailureHandled = true;
sessionStorage.setItem(BOOT_FAILURE_KEY, 'true');
console.info('AliasVault: Blazor boot failure detected. Clearing caches and reloading...');
try {
// Clear all browser caches (Cache API)
if ('caches' in window) {
const cacheKeys = await caches.keys();
await Promise.all(cacheKeys.map(key => caches.delete(key)));
}
// Unregister service workers to prevent stale cache serving
if ('serviceWorker' in navigator) {
const registrations = await navigator.serviceWorker.getRegistrations();
await Promise.all(registrations.map(r => r.unregister()));
}
} catch (e) {
console.error('AliasVault: Cache clear failed:', e);
}
// Force reload bypassing cache
const url = new URL(window.location.href);
url.searchParams.set('_r', Date.now().toString());
window.location.replace(url.toString());
}
// Clear the boot failure flag on successful Blazor start
function clearBootFailureFlag() {
sessionStorage.removeItem(BOOT_FAILURE_KEY);
}
// Detect Blazor boot failures and trigger recovery
function isBlazorBootError(message) {
const bootErrorPatterns = [
'Failed to start platform',
'Argument_InvalidHandle',
'Failed to load resource',
'integrity',
'TypeError: Failed to fetch'
];
return bootErrorPatterns.some(pattern => message.includes(pattern));
}
window.addEventListener('error', function(event) {
const errorMessage = event.error?.message || event.message || '';
if (errorMessage.includes('WebAssembly')) {
showError(getTranslation('errors.webAssemblyError'));
} else if (isBlazorBootError(errorMessage)) {
event.preventDefault();
handleBlazorBootFailure();
}
});
window.addEventListener('unhandledrejection', function(event) {
const errorMessage = event.reason?.message || String(event.reason) || '';
if (errorMessage.includes('WebAssembly')) {
showError(getTranslation('errors.webAssemblyError'));
} else if (isBlazorBootError(errorMessage)) {
event.preventDefault();
handleBlazorBootFailure();
}
});
window.addEventListener('load', manageLoadingScreen);
window.blazorCulture = {
get: function() {
return localStorage.getItem('blazorCulture') ||
localStorage.getItem('AppLanguage') ||
'en';
},
set: function(culture) {
localStorage.setItem('blazorCulture', culture);
}
};
if (typeof window.rustCoreIsAvailable === 'function') {
window.rustCoreIsAvailable().catch(() => {});
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register(`service-worker.js?v=${window.__CACHE_BUSTER__}`);
}
</script>
<script src="js/bundle.min.js?v=@CacheBuster"></script>
<script src="@BlazorScript" autostart="false"></script>
<script>
Blazor.start({
configureRuntime: runtime => runtime.withConfig({
loadAllSatelliteResources: true
})
}).then(function() {
// Blazor started successfully, clear any previous set boot failure flag
clearBootFailureFlag();
});
</script>
</body>
</html>