fix: Gitea Traefik routing and connection pool optimization
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
This commit is contained in:
@@ -13,6 +13,27 @@ export class FormHandler {
|
||||
preventSubmitOnError: true,
|
||||
submitMethod: 'POST',
|
||||
ajaxSubmit: true,
|
||||
// Autosave options
|
||||
enableAutosave: options.enableAutosave ?? form.hasAttribute('data-autosave'),
|
||||
autosaveInterval: options.autosaveInterval || 30000, // 30 seconds
|
||||
autosaveRetentionPeriod: options.autosaveRetentionPeriod || 24 * 60 * 60 * 1000, // 24 hours
|
||||
autosaveStorageKey: options.autosaveStorageKey || null,
|
||||
autosaveStoragePrefix: options.autosaveStoragePrefix || 'form_draft_',
|
||||
autosaveVisualFeedback: options.autosaveVisualFeedback ?? true,
|
||||
autosaveExcludeFields: options.autosaveExcludeFields || [
|
||||
'input[type="password"]',
|
||||
'input[type="hidden"][name="_token"]',
|
||||
'input[type="hidden"][name="_form_id"]',
|
||||
'input[name*="password"]',
|
||||
'input[name*="confirm"]',
|
||||
'input[name*="honeypot"]'
|
||||
],
|
||||
autosaveImmediateFields: options.autosaveImmediateFields || [
|
||||
'textarea',
|
||||
'input[type="email"]',
|
||||
'input[type="text"]',
|
||||
'select'
|
||||
],
|
||||
...options
|
||||
};
|
||||
|
||||
@@ -20,6 +41,11 @@ export class FormHandler {
|
||||
this.state = FormState.create(form);
|
||||
this.isSubmitting = false;
|
||||
|
||||
// Autosave state
|
||||
this.autosaveTimer = null;
|
||||
this.lastAutosaveTime = null;
|
||||
this.autosaveStorageKey = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
@@ -31,6 +57,11 @@ export class FormHandler {
|
||||
this.bindEvents();
|
||||
this.setupErrorDisplay();
|
||||
|
||||
// Initialize autosave if enabled
|
||||
if (this.options.enableAutosave) {
|
||||
this.initAutosave();
|
||||
}
|
||||
|
||||
// Mark form as enhanced
|
||||
this.form.setAttribute('data-enhanced', 'true');
|
||||
|
||||
@@ -55,6 +86,26 @@ export class FormHandler {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Autosave events
|
||||
if (this.options.enableAutosave) {
|
||||
this.form.addEventListener('input', (e) => this.onAutosaveFieldChange(e));
|
||||
this.form.addEventListener('change', (e) => this.onAutosaveFieldChange(e));
|
||||
|
||||
// Save on page unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (this.state.isDirty()) {
|
||||
this.saveAutosaveDraft();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle page visibility changes
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden && this.state.isDirty()) {
|
||||
this.saveAutosaveDraft();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async handleSubmit(event) {
|
||||
@@ -139,6 +190,11 @@ export class FormHandler {
|
||||
handleSuccess(data) {
|
||||
Logger.info('[FormHandler] Form submitted successfully');
|
||||
|
||||
// Clear autosave draft on successful submission
|
||||
if (this.options.enableAutosave) {
|
||||
this.clearAutosaveDraft();
|
||||
}
|
||||
|
||||
// Clear form if configured
|
||||
if (data.clearForm !== false) {
|
||||
this.form.reset();
|
||||
@@ -345,7 +401,351 @@ export class FormHandler {
|
||||
this.form.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize autosave functionality
|
||||
*/
|
||||
initAutosave() {
|
||||
// Generate storage key
|
||||
this.autosaveStorageKey = this.options.autosaveStorageKey ||
|
||||
this.options.autosaveStoragePrefix + this.generateFormId();
|
||||
|
||||
// Restore draft on init
|
||||
this.restoreAutosaveDraft();
|
||||
|
||||
// Start periodic autosave
|
||||
this.startAutosave();
|
||||
|
||||
Logger.info('[FormHandler] Autosave initialized', { storageKey: this.autosaveStorageKey });
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique form identifier
|
||||
*/
|
||||
generateFormId() {
|
||||
const formId = this.form.id ||
|
||||
this.form.getAttribute('data-form-id') ||
|
||||
this.form.querySelector('input[name="_form_id"]')?.value ||
|
||||
'form_' + Date.now();
|
||||
|
||||
return formId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle field change for autosave
|
||||
*/
|
||||
onAutosaveFieldChange(event) {
|
||||
const field = event.target;
|
||||
|
||||
// Skip excluded fields
|
||||
if (this.isAutosaveFieldExcluded(field)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save immediately for important fields
|
||||
if (this.isAutosaveImmediateField(field)) {
|
||||
this.saveAutosaveDraft();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if field should be excluded from autosave
|
||||
*/
|
||||
isAutosaveFieldExcluded(field) {
|
||||
return this.options.autosaveExcludeFields.some(selector =>
|
||||
field.matches(selector)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if field should trigger immediate save
|
||||
*/
|
||||
isAutosaveImmediateField(field) {
|
||||
return this.options.autosaveImmediateFields.some(selector =>
|
||||
field.matches(selector)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic autosave
|
||||
*/
|
||||
startAutosave() {
|
||||
if (this.autosaveTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.autosaveTimer = setInterval(() => {
|
||||
if (this.state.isDirty()) {
|
||||
this.saveAutosaveDraft();
|
||||
}
|
||||
}, this.options.autosaveInterval);
|
||||
|
||||
Logger.debug('[FormHandler] Autosave started', { interval: this.options.autosaveInterval });
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop periodic autosave
|
||||
*/
|
||||
stopAutosave() {
|
||||
if (this.autosaveTimer) {
|
||||
clearInterval(this.autosaveTimer);
|
||||
this.autosaveTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save form data as draft
|
||||
*/
|
||||
saveAutosaveDraft() {
|
||||
if (!this.form || !this.state.isDirty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = this.extractAutosaveFormData();
|
||||
|
||||
const draft = {
|
||||
data: formData,
|
||||
timestamp: Date.now(),
|
||||
formId: this.generateFormId(),
|
||||
url: window.location.href,
|
||||
version: '1.0'
|
||||
};
|
||||
|
||||
localStorage.setItem(this.autosaveStorageKey, JSON.stringify(draft));
|
||||
|
||||
this.lastAutosaveTime = new Date();
|
||||
|
||||
Logger.debug('[FormHandler] Draft saved', {
|
||||
fields: Object.keys(formData).length,
|
||||
storageKey: this.autosaveStorageKey
|
||||
});
|
||||
|
||||
if (this.options.autosaveVisualFeedback) {
|
||||
this.showAutosaveStatus('Draft saved', 'success', 1500);
|
||||
}
|
||||
|
||||
// Trigger event
|
||||
this.triggerEvent('form:autosave', { draft });
|
||||
} catch (error) {
|
||||
Logger.error('[FormHandler] Failed to save draft', error);
|
||||
|
||||
if (this.options.autosaveVisualFeedback) {
|
||||
this.showAutosaveStatus('Failed to save draft', 'error', 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore draft data to form
|
||||
*/
|
||||
restoreAutosaveDraft() {
|
||||
try {
|
||||
const draftJson = localStorage.getItem(this.autosaveStorageKey);
|
||||
|
||||
if (!draftJson) {
|
||||
return;
|
||||
}
|
||||
|
||||
const draft = JSON.parse(draftJson);
|
||||
|
||||
// Check if draft is expired
|
||||
const age = Date.now() - draft.timestamp;
|
||||
if (age > this.options.autosaveRetentionPeriod) {
|
||||
localStorage.removeItem(this.autosaveStorageKey);
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore form data
|
||||
this.restoreAutosaveFormData(draft.data);
|
||||
|
||||
Logger.info('[FormHandler] Draft restored', {
|
||||
age: Math.floor(age / 1000) + 's',
|
||||
fields: Object.keys(draft.data).length
|
||||
});
|
||||
|
||||
if (this.options.autosaveVisualFeedback) {
|
||||
const ageText = this.formatDuration(age);
|
||||
this.showAutosaveStatus(`Draft restored from ${ageText} ago`, 'info', 4000);
|
||||
}
|
||||
|
||||
// Trigger event
|
||||
this.triggerEvent('form:autosave-restored', { draft });
|
||||
} catch (error) {
|
||||
Logger.error('[FormHandler] Failed to restore draft', error);
|
||||
localStorage.removeItem(this.autosaveStorageKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract form data for autosave (excluding sensitive fields)
|
||||
*/
|
||||
extractAutosaveFormData() {
|
||||
const fields = this.getFormFields();
|
||||
const data = {};
|
||||
|
||||
fields.forEach(field => {
|
||||
if (!this.isAutosaveFieldExcluded(field)) {
|
||||
const key = field.name || field.id;
|
||||
const value = this.getAutosaveFieldValue(field);
|
||||
|
||||
if (key && value !== null && value !== undefined && value !== '') {
|
||||
data[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore data to form fields
|
||||
*/
|
||||
restoreAutosaveFormData(data) {
|
||||
let restoredCount = 0;
|
||||
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
const field = this.form.querySelector(`[name="${key}"], #${key}`);
|
||||
|
||||
if (field && !this.isAutosaveFieldExcluded(field)) {
|
||||
this.setAutosaveFieldValue(field, value);
|
||||
restoredCount++;
|
||||
}
|
||||
});
|
||||
|
||||
// Update form state
|
||||
this.state.captureInitialValues();
|
||||
|
||||
return restoredCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get form fields
|
||||
*/
|
||||
getFormFields() {
|
||||
return Array.from(this.form.querySelectorAll(
|
||||
'input:not([type="submit"]):not([type="button"]):not([type="reset"]), ' +
|
||||
'textarea, select'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field value for autosave
|
||||
*/
|
||||
getAutosaveFieldValue(field) {
|
||||
switch (field.type) {
|
||||
case 'checkbox':
|
||||
return field.checked;
|
||||
case 'radio':
|
||||
return field.checked ? field.value : null;
|
||||
case 'file':
|
||||
return null; // Don't save file inputs
|
||||
default:
|
||||
return field.value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set field value for autosave
|
||||
*/
|
||||
setAutosaveFieldValue(field, value) {
|
||||
switch (field.type) {
|
||||
case 'checkbox':
|
||||
field.checked = Boolean(value);
|
||||
break;
|
||||
case 'radio':
|
||||
field.checked = (field.value === value);
|
||||
break;
|
||||
case 'file':
|
||||
// Can't restore file inputs
|
||||
break;
|
||||
default:
|
||||
field.value = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear autosave draft
|
||||
*/
|
||||
clearAutosaveDraft() {
|
||||
if (this.autosaveStorageKey) {
|
||||
localStorage.removeItem(this.autosaveStorageKey);
|
||||
}
|
||||
Logger.debug('[FormHandler] Draft cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration for human reading
|
||||
*/
|
||||
formatDuration(ms) {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m`;
|
||||
} else {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show autosave status message
|
||||
*/
|
||||
showAutosaveStatus(message, type = 'info', duration = 3000) {
|
||||
// Create or update status element
|
||||
let statusEl = document.getElementById('form-autosave-status');
|
||||
|
||||
if (!statusEl) {
|
||||
statusEl = document.createElement('div');
|
||||
statusEl.id = 'form-autosave-status';
|
||||
statusEl.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
z-index: 9999;
|
||||
max-width: 250px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
transition: opacity 0.3s ease;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
`;
|
||||
document.body.appendChild(statusEl);
|
||||
}
|
||||
|
||||
statusEl.textContent = message;
|
||||
|
||||
const styles = {
|
||||
info: 'background: #e3f2fd; color: #1565c0; border: 1px solid #bbdefb;',
|
||||
success: 'background: #e8f5e8; color: #2e7d32; border: 1px solid #c8e6c9;',
|
||||
error: 'background: #ffebee; color: #c62828; border: 1px solid #ffcdd2;'
|
||||
};
|
||||
|
||||
statusEl.style.cssText += styles[type] || styles.info;
|
||||
statusEl.style.opacity = '1';
|
||||
|
||||
setTimeout(() => {
|
||||
if (statusEl) {
|
||||
statusEl.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
if (statusEl && statusEl.parentNode) {
|
||||
statusEl.parentNode.removeChild(statusEl);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}, duration);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// Stop autosave
|
||||
if (this.options.enableAutosave) {
|
||||
this.stopAutosave();
|
||||
}
|
||||
|
||||
// Remove event listeners and clean up
|
||||
this.form.removeAttribute('data-enhanced');
|
||||
Logger.info('[FormHandler] Destroyed');
|
||||
|
||||
Reference in New Issue
Block a user