Enable Discovery debug logging for production troubleshooting

- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,489 @@
import { Logger } from '../../core/logger.js';
export class SPARouter {
constructor(options = {}) {
this.options = {
containerSelector: 'main',
linkSelector: 'a[href^="/"]', // All internal links
loadingClass: 'spa-loading',
excludeSelector: '[data-spa="false"], [download], [target="_blank"], [href^="mailto:"], [href^="tel:"], [href^="#"]',
enableTransitions: true,
transitionDuration: 100, // Beschleunigt von 300ms auf 100ms
skeletonTemplate: this.createSkeletonTemplate(),
...options
};
this.container = null;
this.isLoading = false;
this.currentUrl = window.location.href;
this.abortController = null;
// Bind event handlers to preserve context for removal
this.handleLinkClick = this.handleLinkClick.bind(this);
this.handlePopState = this.handlePopState.bind(this);
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.init();
}
static create(options = {}) {
return new SPARouter(options);
}
init() {
this.container = document.querySelector(this.options.containerSelector);
if (!this.container) {
Logger.error(`[SPARouter] Container "${this.options.containerSelector}" not found`);
return;
}
this.bindEvents();
this.setupStyles();
// Handle initial page load for history
this.updateHistoryState(window.location.href, document.title);
Logger.info('[SPARouter] Initialized');
}
bindEvents() {
// Intercept link clicks
document.addEventListener('click', this.handleLinkClick);
// Handle browser back/forward
window.addEventListener('popstate', this.handlePopState);
// Handle form submissions that might redirect
document.addEventListener('submit', this.handleFormSubmit);
}
handleLinkClick(event) {
const link = event.target.closest(this.options.linkSelector);
if (!link) return;
// Check for exclusions
if (link.matches(this.options.excludeSelector)) return;
// Check for modifier keys (Ctrl, Cmd, etc.)
if (event.ctrlKey || event.metaKey || event.shiftKey) return;
event.preventDefault();
const href = link.href;
const title = link.title || link.textContent.trim();
this.navigate(href, title);
}
handlePopState(event) {
const url = window.location.href;
if (url !== this.currentUrl) {
this.loadContent(url, false); // Don't update history on popstate
}
}
handleFormSubmit(event) {
const form = event.target;
// Only handle forms that might redirect (non-AJAX forms)
if (form.hasAttribute('data-spa') && form.getAttribute('data-spa') === 'false') return;
if (form._moduleInstance) return; // Skip forms with form-handling module
// For now, let forms submit normally
// Could be enhanced later to handle form submissions via SPA
}
async navigate(url, title = '') {
if (this.isLoading) {
Logger.warn(`[SPARouter] Already loading, aborting previous request`);
this.abortController?.abort();
}
if (url === this.currentUrl) {
Logger.info(`[SPARouter] Already at ${url}, skipping navigation`);
return;
}
Logger.info(`[SPARouter] Navigating to: ${url}`);
try {
await this.loadContent(url, true, title);
} catch (error) {
if (error.name !== 'AbortError') {
Logger.error('[SPARouter] Navigation failed:', error);
// Fallback to normal navigation
window.location.href = url;
}
}
}
async loadContent(url, updateHistory = true, title = '') {
// Prevent loading the same URL that's already current
if (url === this.currentUrl && !updateHistory) {
return;
}
if (this.isLoading) {
this.abortController?.abort();
}
this.isLoading = true;
this.abortController = new AbortController();
try {
// Show loading state
this.showLoadingState();
// Request only the main content
const response = await fetch(url, {
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-SPA-Request': 'true', // Signal to backend this is an SPA request
'Accept': 'application/json, text/html'
},
signal: this.abortController.signal
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// Check content type to determine how to parse response
const contentType = response.headers.get('content-type');
let newContent, newTitle;
if (contentType?.includes('application/json')) {
// Backend sent JSON SPA response
const jsonData = await response.json();
newContent = jsonData.html;
newTitle = jsonData.title || title;
// Update meta tags if provided
if (jsonData.meta) {
this.updateMetaTags(jsonData.meta);
}
} else {
// Backend sent full HTML response - extract content
const html = await response.text();
newContent = this.extractMainContent(html);
newTitle = this.extractTitle(html) || title;
}
// Update content
await this.updateContent(newContent, newTitle);
// Update browser history
if (updateHistory) {
this.updateHistoryState(url, newTitle);
}
this.currentUrl = url;
Logger.info(`[SPARouter] Successfully loaded: ${url}`);
} catch (error) {
if (error.name !== 'AbortError') {
this.hideLoadingState();
throw error;
}
} finally {
this.isLoading = false;
this.abortController = null;
}
}
extractMainContent(html) {
// Create a temporary DOM to parse the response
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const mainElement = doc.querySelector('main');
if (mainElement) {
return mainElement.innerHTML;
}
// Fallback: try to find content in common containers
const fallbackSelectors = ['[role="main"]', '.main-content', '#main', '.content'];
for (const selector of fallbackSelectors) {
const element = doc.querySelector(selector);
if (element) {
Logger.warn(`[SPARouter] Using fallback selector: ${selector}`);
return element.innerHTML;
}
}
// Last resort: use the entire body
Logger.warn('[SPARouter] No main element found, using entire body');
return doc.body.innerHTML;
}
extractTitle(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const titleElement = doc.querySelector('title');
return titleElement ? titleElement.textContent.trim() : '';
}
async updateContent(newContent, newTitle) {
// Update page title
if (newTitle) {
document.title = newTitle;
}
// Smooth transition
if (this.options.enableTransitions) {
await this.transitionOut();
}
// Update content
this.container.innerHTML = newContent;
// Re-initialize modules for new content
this.reinitializeModules();
// Smooth transition in
if (this.options.enableTransitions) {
await this.transitionIn();
}
this.hideLoadingState();
// Scroll to top
window.scrollTo({ top: 0, behavior: 'smooth' });
// Trigger custom event
this.triggerNavigationEvent();
}
showLoadingState() {
document.body.classList.add(this.options.loadingClass);
// Show skeleton loader
if (this.options.enableTransitions) {
this.container.classList.add('spa-transitioning-out');
}
}
hideLoadingState() {
document.body.classList.remove(this.options.loadingClass);
}
async transitionOut() {
return new Promise(resolve => {
// Verwende schnellere cubic-bezier für snappigere Animation
this.container.style.transition = `opacity ${this.options.transitionDuration}ms cubic-bezier(0.4, 0, 1, 1)`;
this.container.style.opacity = '0';
setTimeout(() => {
resolve();
}, this.options.transitionDuration);
});
}
async transitionIn() {
return new Promise(resolve => {
this.container.style.opacity = '0';
// Reduziere Verzögerung von 50ms auf 10ms
setTimeout(() => {
// Verwende schnellere cubic-bezier für snappigere Animation
this.container.style.transition = `opacity ${this.options.transitionDuration}ms cubic-bezier(0, 0, 0.2, 1)`;
this.container.style.opacity = '1';
setTimeout(() => {
this.container.style.transition = '';
this.container.classList.remove('spa-transitioning-out');
resolve();
}, this.options.transitionDuration);
}, 10); // Von 50ms auf 10ms reduziert
});
}
updateHistoryState(url, title) {
const state = { url, title, timestamp: Date.now() };
if (url !== window.location.href) {
history.pushState(state, title, url);
} else {
history.replaceState(state, title, url);
}
}
reinitializeModules() {
// Re-run form auto-enhancement for new content
if (window.initAutoFormHandling) {
window.initAutoFormHandling();
}
// Re-initialize any data-module elements in new content
const moduleElements = this.container.querySelectorAll('[data-module]');
moduleElements.forEach(element => {
const moduleName = element.dataset.module;
Logger.info(`[SPARouter] Re-initializing module "${moduleName}" on new content`);
// Trigger module initialization (would need access to module system)
const event = new CustomEvent('spa:reinit-module', {
detail: { element, moduleName },
bubbles: true
});
element.dispatchEvent(event);
});
}
createSkeletonTemplate() {
return `
<div class="spa-skeleton">
<div class="spa-skeleton-header"></div>
<div class="spa-skeleton-content">
<div class="spa-skeleton-line"></div>
<div class="spa-skeleton-line"></div>
<div class="spa-skeleton-line short"></div>
</div>
</div>
`;
}
setupStyles() {
if (document.getElementById('spa-router-styles')) return;
const styles = document.createElement('style');
styles.id = 'spa-router-styles';
styles.textContent = `
/* SPA Router Transitions */
.spa-loading {
cursor: progress;
}
.spa-transitioning-out {
pointer-events: none;
}
/* Skeleton Loading Styles */
.spa-skeleton {
animation: spa-pulse 1.5s ease-in-out infinite alternate;
}
.spa-skeleton-header {
height: 2rem;
background: #e5e7eb;
border-radius: 0.375rem;
margin-bottom: 1rem;
width: 60%;
}
.spa-skeleton-content {
space-y: 0.75rem;
}
.spa-skeleton-line {
height: 1rem;
background: #e5e7eb;
border-radius: 0.375rem;
margin-bottom: 0.75rem;
}
.spa-skeleton-line.short {
width: 75%;
}
@keyframes spa-pulse {
0% {
opacity: 1;
}
100% {
opacity: 0.4;
}
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.spa-skeleton-header,
.spa-skeleton-line {
background: #374151;
}
}
`;
document.head.appendChild(styles);
}
updateMetaTags(metaData) {
// Update meta description
if (metaData.description) {
let metaDesc = document.querySelector('meta[name="description"]');
if (metaDesc) {
metaDesc.content = metaData.description;
} else {
metaDesc = document.createElement('meta');
metaDesc.name = 'description';
metaDesc.content = metaData.description;
document.head.appendChild(metaDesc);
}
}
// Add other meta tag updates as needed
Object.entries(metaData).forEach(([key, value]) => {
if (key !== 'description' && value) {
let metaTag = document.querySelector(`meta[name="${key}"]`);
if (metaTag) {
metaTag.content = value;
} else {
metaTag = document.createElement('meta');
metaTag.name = key;
metaTag.content = value;
document.head.appendChild(metaTag);
}
}
});
}
triggerNavigationEvent() {
const event = new CustomEvent('spa:navigated', {
detail: {
url: this.currentUrl,
container: this.container,
timestamp: Date.now()
},
bubbles: true
});
document.dispatchEvent(event);
}
// Public API
navigateTo(url, title) {
return this.navigate(url, title);
}
getCurrentUrl() {
return this.currentUrl;
}
isNavigating() {
return this.isLoading;
}
destroy() {
this.abortController?.abort();
// Remove event listeners
document.removeEventListener('click', this.handleLinkClick);
window.removeEventListener('popstate', this.handlePopState);
document.removeEventListener('submit', this.handleFormSubmit);
// Remove styles
const styles = document.getElementById('spa-router-styles');
if (styles) {
styles.remove();
}
Logger.info('[SPARouter] Destroyed');
}
}