feat(Production): Complete production deployment infrastructure

- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,453 @@
/**
* Accessibility Manager for LiveComponents
*
* Manages accessibility features for dynamic content updates:
* - ARIA live regions for screen reader announcements
* - Focus management with data-lc-keep-focus attribute
* - Keyboard navigation preservation
* - Screen reader-friendly update notifications
*
* WCAG 2.1 Compliance:
* - 4.1.3 Status Messages (Level AA)
* - 2.4.3 Focus Order (Level A)
* - 2.1.1 Keyboard (Level A)
*/
export class AccessibilityManager {
constructor() {
/**
* ARIA live region element for announcements
* @type {HTMLElement|null}
*/
this.liveRegion = null;
/**
* Component-specific live regions
* Map<componentId, HTMLElement>
*/
this.componentLiveRegions = new Map();
/**
* Focus tracking for restoration
* Map<componentId, FocusState>
*/
this.focusStates = new Map();
/**
* Announcement queue for throttling
* @type {Array<{message: string, priority: string}>}
*/
this.announcementQueue = [];
/**
* Announcement throttle timer
* @type {number|null}
*/
this.announceTimer = null;
/**
* Throttle delay in milliseconds
* @type {number}
*/
this.throttleDelay = 500;
}
/**
* Initialize accessibility features
*
* Creates global live region and sets up initial state.
*/
initialize() {
// Create global live region if not exists
if (!this.liveRegion) {
this.liveRegion = this.createLiveRegion('livecomponent-announcer', 'polite');
document.body.appendChild(this.liveRegion);
}
console.log('[AccessibilityManager] Initialized with ARIA live regions');
}
/**
* Create ARIA live region element
*
* @param {string} id - Element ID
* @param {string} politeness - ARIA politeness level (polite|assertive)
* @returns {HTMLElement} Live region element
*/
createLiveRegion(id, politeness = 'polite') {
const region = document.createElement('div');
region.id = id;
region.setAttribute('role', 'status');
region.setAttribute('aria-live', politeness);
region.setAttribute('aria-atomic', 'true');
region.className = 'sr-only'; // Screen reader only styling
// Add screen reader only styles
region.style.position = 'absolute';
region.style.left = '-10000px';
region.style.width = '1px';
region.style.height = '1px';
region.style.overflow = 'hidden';
return region;
}
/**
* Create component-specific live region
*
* Each component can have its own live region for isolated announcements.
*
* @param {string} componentId - Component identifier
* @param {HTMLElement} container - Component container element
* @param {string} politeness - ARIA politeness level
* @returns {HTMLElement} Component live region
*/
createComponentLiveRegion(componentId, container, politeness = 'polite') {
// Check if already exists
let liveRegion = this.componentLiveRegions.get(componentId);
if (!liveRegion) {
liveRegion = this.createLiveRegion(`livecomponent-${componentId}-announcer`, politeness);
container.appendChild(liveRegion);
this.componentLiveRegions.set(componentId, liveRegion);
}
return liveRegion;
}
/**
* Announce update to screen readers
*
* Uses ARIA live regions to announce updates without stealing focus.
* Supports throttling to prevent announcement spam.
*
* @param {string} message - Message to announce
* @param {string} priority - Priority level (polite|assertive)
* @param {string|null} componentId - Optional component-specific announcement
*/
announce(message, priority = 'polite', componentId = null) {
// Add to queue
this.announcementQueue.push({ message, priority, componentId });
// Throttle announcements
if (this.announceTimer) {
clearTimeout(this.announceTimer);
}
this.announceTimer = setTimeout(() => {
this.flushAnnouncements();
}, this.throttleDelay);
}
/**
* Flush announcement queue
*
* Processes queued announcements and clears the queue.
*/
flushAnnouncements() {
if (this.announcementQueue.length === 0) return;
// Get most recent announcement (others are outdated)
const announcement = this.announcementQueue[this.announcementQueue.length - 1];
this.announcementQueue = [];
// Determine target live region
let liveRegion = this.liveRegion;
if (announcement.componentId) {
const componentRegion = this.componentLiveRegions.get(announcement.componentId);
if (componentRegion) {
liveRegion = componentRegion;
}
}
if (!liveRegion) {
console.warn('[AccessibilityManager] No live region available for announcement');
return;
}
// Update politeness if needed
if (announcement.priority === 'assertive') {
liveRegion.setAttribute('aria-live', 'assertive');
} else {
liveRegion.setAttribute('aria-live', 'polite');
}
// Clear and set new message
liveRegion.textContent = '';
// Use setTimeout to ensure screen reader picks up the change
setTimeout(() => {
liveRegion.textContent = announcement.message;
}, 100);
console.log(`[AccessibilityManager] Announced: "${announcement.message}" (${announcement.priority})`);
}
/**
* Capture focus state before update
*
* Saves current focus state for potential restoration after update.
*
* @param {string} componentId - Component identifier
* @param {HTMLElement} container - Component container element
* @returns {FocusState} Focus state object
*/
captureFocusState(componentId, container) {
const activeElement = document.activeElement;
// Check if focus is within container
if (!activeElement || !container.contains(activeElement)) {
return null;
}
const focusState = {
selector: this.getElementSelector(activeElement, container),
tagName: activeElement.tagName,
name: activeElement.name || null,
id: activeElement.id || null,
selectionStart: activeElement.selectionStart || null,
selectionEnd: activeElement.selectionEnd || null,
scrollTop: activeElement.scrollTop || 0,
scrollLeft: activeElement.scrollLeft || 0,
keepFocus: activeElement.hasAttribute('data-lc-keep-focus')
};
this.focusStates.set(componentId, focusState);
console.log(`[AccessibilityManager] Captured focus state for ${componentId}`, focusState);
return focusState;
}
/**
* Restore focus after update
*
* Restores focus to element marked with data-lc-keep-focus or previously focused element.
*
* @param {string} componentId - Component identifier
* @param {HTMLElement} container - Component container element
* @returns {boolean} True if focus was restored
*/
restoreFocus(componentId, container) {
const focusState = this.focusStates.get(componentId);
if (!focusState) {
return false;
}
try {
// Find element to focus
let elementToFocus = null;
// Priority 1: Element with data-lc-keep-focus attribute
if (focusState.keepFocus) {
elementToFocus = container.querySelector('[data-lc-keep-focus]');
}
// Priority 2: Element matching saved selector
if (!elementToFocus && focusState.selector) {
elementToFocus = container.querySelector(focusState.selector);
}
// Priority 3: Element with same ID
if (!elementToFocus && focusState.id) {
elementToFocus = container.querySelector(`#${focusState.id}`);
}
// Priority 4: Element with same name
if (!elementToFocus && focusState.name) {
elementToFocus = container.querySelector(`[name="${focusState.name}"]`);
}
if (elementToFocus && elementToFocus.focus) {
elementToFocus.focus();
// Restore selection for inputs/textareas
if (elementToFocus.setSelectionRange &&
focusState.selectionStart !== null &&
focusState.selectionEnd !== null) {
elementToFocus.setSelectionRange(
focusState.selectionStart,
focusState.selectionEnd
);
}
// Restore scroll position
if (focusState.scrollTop > 0) {
elementToFocus.scrollTop = focusState.scrollTop;
}
if (focusState.scrollLeft > 0) {
elementToFocus.scrollLeft = focusState.scrollLeft;
}
console.log(`[AccessibilityManager] Restored focus to ${focusState.selector}`);
return true;
}
} catch (error) {
console.debug('[AccessibilityManager] Could not restore focus:', error);
} finally {
// Clean up focus state
this.focusStates.delete(componentId);
}
return false;
}
/**
* Get CSS selector for an element within a container
*
* @param {HTMLElement} element - Element to get selector for
* @param {HTMLElement} container - Container element
* @returns {string|null} CSS selector or null
*/
getElementSelector(element, container) {
// Try ID (most specific)
if (element.id) {
return `#${element.id}`;
}
// Try name attribute
if (element.name) {
return `[name="${element.name}"]`;
}
// Try data-lc-key
const lcKey = element.getAttribute('data-lc-key');
if (lcKey) {
return `[data-lc-key="${lcKey}"]`;
}
// Try to build a path from container
const path = [];
let current = element;
while (current && current !== container && current !== document.body) {
let selector = current.tagName.toLowerCase();
// Add class if available
if (current.className && typeof current.className === 'string') {
const classes = current.className.split(' ').filter(c => c.trim());
if (classes.length > 0) {
selector += '.' + classes.join('.');
}
}
// Add nth-child if needed for specificity
if (current.parentElement) {
const siblings = Array.from(current.parentElement.children);
const index = siblings.indexOf(current) + 1;
if (siblings.length > 1) {
selector += `:nth-child(${index})`;
}
}
path.unshift(selector);
current = current.parentElement;
}
return path.length > 0 ? path.join(' > ') : null;
}
/**
* Announce component update
*
* Convenience method for announcing component updates.
*
* @param {string} componentId - Component identifier
* @param {string} updateType - Type of update (fragment|full|action)
* @param {Object} metadata - Additional metadata
*/
announceUpdate(componentId, updateType, metadata = {}) {
let message = '';
switch (updateType) {
case 'fragment':
message = `Updated ${metadata.fragmentName || 'content'}`;
break;
case 'full':
message = 'Component updated';
break;
case 'action':
message = metadata.actionMessage || 'Action completed';
break;
default:
message = 'Content updated';
}
this.announce(message, 'polite', componentId);
}
/**
* Check if element should preserve keyboard navigation
*
* @param {HTMLElement} element - Element to check
* @returns {boolean} True if keyboard navigation should be preserved
*/
shouldPreserveKeyboardNav(element) {
// Interactive elements that should always preserve keyboard nav
const interactiveTags = ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON', 'A'];
if (interactiveTags.includes(element.tagName)) {
return true;
}
// Elements with tabindex
if (element.hasAttribute('tabindex')) {
return true;
}
// Elements with role
const interactiveRoles = [
'button', 'link', 'textbox', 'searchbox',
'combobox', 'listbox', 'option', 'tab'
];
const role = element.getAttribute('role');
if (role && interactiveRoles.includes(role)) {
return true;
}
return false;
}
/**
* Cleanup component accessibility features
*
* @param {string} componentId - Component identifier
*/
cleanup(componentId) {
// Remove component live region
const liveRegion = this.componentLiveRegions.get(componentId);
if (liveRegion && liveRegion.parentElement) {
liveRegion.parentElement.removeChild(liveRegion);
}
this.componentLiveRegions.delete(componentId);
// Clear focus state
this.focusStates.delete(componentId);
console.log(`[AccessibilityManager] Cleaned up accessibility for ${componentId}`);
}
/**
* Get accessibility stats
*
* @returns {Object} Statistics about accessibility manager state
*/
getStats() {
return {
has_global_live_region: this.liveRegion !== null,
component_live_regions: this.componentLiveRegions.size,
tracked_focus_states: this.focusStates.size,
pending_announcements: this.announcementQueue.length
};
}
}
// Create singleton instance
export const accessibilityManager = new AccessibilityManager();
export default accessibilityManager;