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;
}