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,277 @@
/**
* LiveComponents - Zero-Dependency Interactive Component System
* Part of Custom PHP Framework
*/
class LiveComponentManager {
constructor() {
this.components = new Map();
this.pollingIntervals = new Map();
this.init();
}
init() {
// Find all live components
document.querySelectorAll('[data-live-component]').forEach(el => {
const id = el.dataset.liveComponent;
const state = JSON.parse(el.dataset.componentState || '{}');
this.components.set(id, {
element: el,
state: state
});
// Setup event listeners
this.setupListeners(el, id);
});
}
setupListeners(element, componentId) {
// Action buttons and links
element.querySelectorAll('[data-live-action]').forEach(btn => {
btn.addEventListener('click', async (e) => {
const action = btn.dataset.liveAction;
const params = this.getActionParams(btn);
if (btn.dataset.livePrevent !== undefined) {
e.preventDefault();
}
await this.callAction(componentId, action, params);
});
});
// Forms with data-live-action
element.querySelectorAll('form[data-live-action]').forEach(form => {
form.addEventListener('submit', async (e) => {
e.preventDefault();
const action = form.dataset.liveAction;
const formData = new FormData(form);
const params = Object.fromEntries(formData);
await this.callAction(componentId, action, params);
});
});
// Upload forms
element.querySelectorAll('form[data-live-upload]').forEach(form => {
form.addEventListener('submit', async (e) => {
e.preventDefault();
const action = form.dataset.liveUpload;
const fileInput = form.querySelector('input[type="file"]');
const file = fileInput?.files[0];
if (!file) return;
// Validate file size
const maxSize = parseInt(fileInput.dataset.maxSize || '0');
if (maxSize > 0 && file.size > maxSize) {
alert(`File too large. Max ${(maxSize / 1024 / 1024).toFixed(2)}MB`);
return;
}
await this.uploadFile(componentId, action, file, form);
});
});
// Setup polling if interval is set
const pollInterval = element.dataset.pollInterval;
if (pollInterval) {
this.startPolling(componentId, parseInt(pollInterval));
}
}
async callAction(componentId, method, params = {}) {
const component = this.components.get(componentId);
if (!component) return;
try {
// URL-encode the component ID to handle special characters (backslashes, colons)
const encodedId = encodeURIComponent(componentId);
const response = await fetch(`/live-component/${encodedId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
component_id: componentId,
method: method,
params: params,
state: component.state.data || {}
})
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
// Update DOM
component.element.innerHTML = data.html;
// Update state
if (data.state) {
component.state = JSON.parse(data.state);
component.element.dataset.componentState = data.state;
}
// Re-setup listeners
this.setupListeners(component.element, componentId);
// Dispatch custom events
data.events?.forEach(event => {
document.dispatchEvent(new CustomEvent(event.name, {
detail: event.data
}));
});
} catch (error) {
console.error('LiveComponent error:', error);
}
}
async uploadFile(componentId, action, file, form) {
const component = this.components.get(componentId);
if (!component) return;
// Create FormData
const formData = new FormData();
formData.append('file', file);
formData.append('component_id', componentId);
formData.append('method', action);
formData.append('state', JSON.stringify(component.state.data || {}));
try {
// Show upload progress
this.updateUploadProgress(componentId, 0);
const xhr = new XMLHttpRequest();
// Track upload progress
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentage = (e.loaded / e.total) * 100;
this.updateUploadProgress(componentId, percentage);
}
});
// Handle completion
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
// Update component
component.element.innerHTML = data.html;
if (data.state) {
component.state = JSON.parse(data.state);
component.element.dataset.componentState = data.state;
}
this.setupListeners(component.element, componentId);
this.updateUploadProgress(componentId, 100);
// Reset form
form.reset();
} else {
alert('Upload failed');
}
});
xhr.addEventListener('error', () => {
alert('Upload error');
});
// URL-encode the component ID to handle special characters
const encodedId = encodeURIComponent(componentId);
xhr.open('POST', `/live-component/${encodedId}/upload`);
xhr.send(formData);
} catch (error) {
console.error('Upload error:', error);
alert('Upload failed');
}
}
updateUploadProgress(componentId, percentage) {
const component = this.components.get(componentId);
if (!component) return;
const progressBar = component.element.querySelector('.progress-bar');
const progressText = component.element.querySelector('.upload-progress span');
if (progressBar) {
progressBar.style.width = `${percentage}%`;
}
if (progressText) {
progressText.textContent = `${Math.round(percentage)}%`;
}
}
startPolling(componentId, interval) {
// Clear existing interval
this.stopPolling(componentId);
// Start new interval
const intervalId = setInterval(async () => {
await this.callAction(componentId, 'poll', {});
}, interval);
this.pollingIntervals.set(componentId, intervalId);
}
stopPolling(componentId) {
const intervalId = this.pollingIntervals.get(componentId);
if (intervalId) {
clearInterval(intervalId);
this.pollingIntervals.delete(componentId);
}
}
removeComponent(componentId) {
this.stopPolling(componentId);
this.components.delete(componentId);
}
getActionParams(element) {
// Extract params from data attributes
const params = {};
Object.keys(element.dataset).forEach(key => {
if (key.startsWith('param')) {
const paramName = key.replace('param', '').toLowerCase();
params[paramName] = element.dataset[key];
}
});
return params;
}
// Debounce utility
debounce(func, wait) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
}
// Auto-init with error handling
try {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
window.liveComponents = new LiveComponentManager();
console.log('LiveComponentManager initialized (DOMContentLoaded)');
});
} else {
window.liveComponents = new LiveComponentManager();
console.log('LiveComponentManager initialized (immediate)');
}
} catch (error) {
console.error('Failed to initialize LiveComponentManager:', error);
window.liveComponents = null;
}

174
public/js/sse-client.js Normal file
View File

@@ -0,0 +1,174 @@
/**
* SSE Client - Server-Sent Events Manager
* Zero-Dependency EventSource wrapper with auto-reconnect
*/
class SSEManager {
constructor() {
this.connections = new Map();
}
/**
* Connect to SSE endpoint
* @param {string} url - SSE endpoint URL
* @param {Object} handlers - Event handlers
* @param {Function} [handlers.onOpen] - Connection opened callback
* @param {Function} [handlers.onError] - Error callback
* @param {Function} [handlers.onMessage] - Default message handler
* @param {Object} [handlers.events] - Named event handlers
* @returns {EventSource}
*/
connect(url, handlers = {}) {
// Close existing connection
this.disconnect(url);
const eventSource = new EventSource(url);
// Default handlers
eventSource.onopen = () => {
console.log('SSE connected:', url);
handlers.onOpen?.();
};
eventSource.onerror = (error) => {
console.error('SSE error:', error);
handlers.onError?.(error);
// Auto-reconnect after 5 seconds
setTimeout(() => {
console.log('SSE reconnecting:', url);
this.connect(url, handlers);
}, 5000);
};
// Custom event handlers
if (handlers.events) {
Object.entries(handlers.events).forEach(([event, handler]) => {
eventSource.addEventListener(event, (e) => {
try {
const data = JSON.parse(e.data);
handler(data, e);
} catch {
handler(e.data, e);
}
});
});
}
// Default message handler
eventSource.onmessage = (e) => {
try {
const data = JSON.parse(e.data);
handlers.onMessage?.(data, e);
} catch {
handlers.onMessage?.(e.data, e);
}
};
this.connections.set(url, eventSource);
return eventSource;
}
/**
* Disconnect from SSE endpoint
* @param {string} url - SSE endpoint URL
*/
disconnect(url) {
const connection = this.connections.get(url);
if (connection) {
connection.close();
this.connections.delete(url);
console.log('SSE disconnected:', url);
}
}
/**
* Disconnect all SSE connections
*/
disconnectAll() {
this.connections.forEach((conn, url) => {
conn.close();
console.log('SSE disconnected:', url);
});
this.connections.clear();
}
/**
* Check if connected to URL
* @param {string} url
* @returns {boolean}
*/
isConnected(url) {
return this.connections.has(url);
}
/**
* Get connection by URL
* @param {string} url
* @returns {EventSource|undefined}
*/
getConnection(url) {
return this.connections.get(url);
}
}
// Global instance
window.sseManager = new SSEManager();
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
window.sseManager.disconnectAll();
});
/**
* Usage Examples:
*
* // 1. Notification Stream
* window.sseManager.connect('/notifications/stream', {
* events: {
* 'notification': (data) => {
* showNotification(data.message, data.type);
* },
* 'heartbeat': (data) => {
* console.log('Server alive:', data.status);
* }
* },
* onError: () => {
* console.log('Reconnecting to notifications...');
* }
* });
*
* // 2. Live Component Updates via SSE
* window.sseManager.connect('/live-component/UserCardComponent:123/stream', {
* events: {
* 'component-update': (data) => {
* const component = window.liveComponents.components.get('UserCardComponent:123');
* if (component) {
* component.element.innerHTML = data.html;
* component.state = JSON.parse(data.state);
* window.liveComponents.setupListeners(component.element, 'UserCardComponent:123');
* }
* }
* }
* });
*
* // 3. Job Progress Tracking
* function trackJobProgress(jobId) {
* const progressBar = document.getElementById('progress-bar');
* const progressText = document.getElementById('progress-text');
*
* window.sseManager.connect(`/jobs/${jobId}/progress`, {
* events: {
* 'progress': (data) => {
* progressBar.style.width = `${data.percentage}%`;
* progressText.textContent = data.message;
*
* if (data.status === 'completed') {
* window.sseManager.disconnect(`/jobs/${jobId}/progress`);
* alert('Job completed!');
* }
* }
* }
* });
* }
*/

123
public/js/sw-push.js Normal file
View File

@@ -0,0 +1,123 @@
/**
* Service Worker for Web Push Notifications
*
* Handles incoming push notifications and displays them to the user.
*/
// Listen for push events
self.addEventListener('push', function(event) {
console.log('[Service Worker] Push received', event);
if (!event.data) {
console.log('[Service Worker] Push event but no data');
return;
}
let notificationData;
try {
notificationData = event.data.json();
} catch (e) {
console.error('[Service Worker] Failed to parse push data', e);
notificationData = {
title: 'Notification',
body: event.data.text()
};
}
const title = notificationData.title || 'Notification';
const options = {
body: notificationData.body || '',
icon: notificationData.icon || '/assets/icons/notification-icon.png',
badge: notificationData.badge || '/assets/icons/notification-badge.png',
image: notificationData.image,
tag: notificationData.tag || 'default-tag',
requireInteraction: notificationData.requireInteraction || false,
silent: notificationData.silent || false,
actions: notificationData.actions || [],
data: notificationData.data || {}
};
event.waitUntil(
self.registration.showNotification(title, options)
);
});
// Listen for notification clicks
self.addEventListener('notificationclick', function(event) {
console.log('[Service Worker] Notification clicked', event);
event.notification.close();
// Handle action button clicks
if (event.action) {
console.log('[Service Worker] Action clicked:', event.action);
// You can handle different actions here
switch (event.action) {
case 'open':
event.waitUntil(clients.openWindow('/'));
break;
case 'dismiss':
// Just close, no action
break;
default:
event.waitUntil(clients.openWindow('/'));
}
} else {
// Regular notification click (not action button)
const urlToOpen = event.notification.data?.url || '/';
event.waitUntil(
clients.matchAll({
type: 'window',
includeUncontrolled: true
})
.then(function(windowClients) {
// Check if there is already a window/tab open
for (let i = 0; i < windowClients.length; i++) {
const client = windowClients[i];
if (client.url === urlToOpen && 'focus' in client) {
return client.focus();
}
}
// If not, open new window/tab
if (clients.openWindow) {
return clients.openWindow(urlToOpen);
}
})
);
}
});
// Listen for notification close
self.addEventListener('notificationclose', function(event) {
console.log('[Service Worker] Notification closed', event);
// You can track notification dismissals here
// e.g., send analytics event
});
// Service Worker Installation
self.addEventListener('install', function(event) {
console.log('[Service Worker] Installing');
self.skipWaiting(); // Activate immediately
});
// Service Worker Activation
self.addEventListener('activate', function(event) {
console.log('[Service Worker] Activating');
event.waitUntil(clients.claim()); // Take control of all pages
});
// Optional: Handle background sync for failed pushes
self.addEventListener('sync', function(event) {
console.log('[Service Worker] Background sync', event);
if (event.tag === 'push-sync') {
event.waitUntil(
// Retry failed push operations
Promise.resolve()
);
}
});