- 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.
278 lines
8.9 KiB
JavaScript
278 lines
8.9 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|