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,189 @@
import { test, expect } from '@playwright/test';
import {
waitForLiveComponent,
fillAndSubmitForm,
assertValidationError,
assertNoValidationErrors,
waitForSuccessMessage
} from '../support/test-helpers';
/**
* LiveComponents: Form Validation Tests
*
* Tests real-time form validation with LiveComponents
*/
test.describe('LiveComponents Form Validation', () => {
test.beforeEach(async ({ page }) => {
// Navigate to a page with LiveComponent form
// Adjust URL based on your actual routes
await page.goto('/');
});
test('should show validation errors on invalid input', async ({ page }) => {
// Skip if no form is present on homepage
const formCount = await page.locator('form').count();
if (formCount === 0) {
test.skip();
return;
}
const form = page.locator('form').first();
// Try to submit empty form
await form.locator('button[type="submit"]').click();
// Wait a bit for validation to trigger
await page.waitForTimeout(500);
// Check if any validation messages appear
const errorMessages = page.locator('.error-message, [class*="error-"], .invalid-feedback');
const errorCount = await errorMessages.count();
// If there are required fields, there should be error messages
const requiredFields = await form.locator('[required]').count();
if (requiredFields > 0) {
expect(errorCount).toBeGreaterThan(0);
}
});
test('should validate email format in real-time', async ({ page }) => {
// Look for email input
const emailInput = page.locator('input[type="email"]').first();
const emailCount = await emailInput.count();
if (emailCount === 0) {
test.skip();
return;
}
// Enter invalid email
await emailInput.fill('invalid-email');
await emailInput.blur();
// Wait for validation
await page.waitForTimeout(500);
// Check for error message (if client-side validation is present)
// Note: HTML5 validation or custom validation might trigger
const validity = await emailInput.evaluate((el: HTMLInputElement) => el.validity.valid);
expect(validity).toBe(false);
});
test('should clear validation errors on valid input', async ({ page }) => {
const emailInput = page.locator('input[type="email"]').first();
const emailCount = await emailInput.count();
if (emailCount === 0) {
test.skip();
return;
}
// Enter invalid email first
await emailInput.fill('invalid');
await emailInput.blur();
await page.waitForTimeout(300);
// Now enter valid email
await emailInput.fill('valid@example.com');
await emailInput.blur();
await page.waitForTimeout(300);
// Check validity
const validity = await emailInput.evaluate((el: HTMLInputElement) => el.validity.valid);
expect(validity).toBe(true);
});
test('should prevent submission with validation errors', async ({ page }) => {
const formCount = await page.locator('form').count();
if (formCount === 0) {
test.skip();
return;
}
const form = page.locator('form').first();
const currentUrl = page.url();
// Try to submit form with invalid data
const submitButton = form.locator('button[type="submit"]');
await submitButton.click();
// Wait a bit
await page.waitForTimeout(1000);
// URL should not change if validation failed
// (assuming no AJAX submission that stays on page)
const newUrl = page.url();
// Form should still be visible (not submitted)
await expect(form).toBeVisible();
});
test('should handle required fields', async ({ page }) => {
const requiredInputs = page.locator('input[required], textarea[required], select[required]');
const requiredCount = await requiredInputs.count();
if (requiredCount === 0) {
test.skip();
return;
}
// Check first required field
const firstRequired = requiredInputs.first();
// Should have required attribute
await expect(firstRequired).toHaveAttribute('required');
// Should have appropriate ARIA attributes
const ariaRequired = await firstRequired.getAttribute('aria-required');
expect(['true', null]).toContain(ariaRequired);
});
test('should handle max length validation', async ({ page }) => {
const maxLengthInputs = page.locator('input[maxlength], textarea[maxlength]');
const count = await maxLengthInputs.count();
if (count === 0) {
test.skip();
return;
}
const input = maxLengthInputs.first();
const maxLength = await input.getAttribute('maxlength');
if (maxLength) {
const maxLengthNum = parseInt(maxLength, 10);
// Try to enter text longer than maxlength
const longText = 'a'.repeat(maxLengthNum + 10);
await input.fill(longText);
// Value should be truncated to maxlength
const value = await input.inputValue();
expect(value.length).toBeLessThanOrEqual(maxLengthNum);
}
});
test('should show character counter for textarea (if present)', async ({ page }) => {
const textarea = page.locator('textarea[maxlength]').first();
const count = await textarea.count();
if (count === 0) {
test.skip();
return;
}
// Look for character counter
const counter = page.locator('[data-char-counter], .char-counter, .character-count');
const counterCount = await counter.count();
// Enter some text
await textarea.fill('Hello World');
// If counter exists, it should update
if (counterCount > 0) {
await page.waitForTimeout(500);
const counterText = await counter.first().textContent();
expect(counterText).toBeTruthy();
}
});
});

View File

@@ -0,0 +1,200 @@
import { test, expect } from '@playwright/test';
/**
* LiveComponents: Real-time Updates Tests
*
* Tests SSE (Server-Sent Events) and real-time component updates
*/
test.describe('LiveComponents Real-time Updates', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('should establish SSE connection (if present)', async ({ page }) => {
// Listen for EventSource connections
const sseConnections: string[] = [];
page.on('request', (request) => {
if (request.url().includes('/sse') || request.headers()['accept']?.includes('text/event-stream')) {
sseConnections.push(request.url());
}
});
// Wait for potential SSE connections
await page.waitForTimeout(2000);
// If SSE is implemented, we should have connections
// This is informational - not all pages may have SSE
console.log('SSE Connections:', sseConnections);
});
test('should handle component updates via HTMX', async ({ page }) => {
// Look for HTMX-enabled elements
const htmxElements = page.locator('[hx-get], [hx-post], [hx-trigger]');
const count = await htmxElements.count();
if (count === 0) {
test.skip();
return;
}
// Track HTMX requests
let htmxRequestMade = false;
page.on('request', (request) => {
const headers = request.headers();
if (headers['hx-request'] === 'true') {
htmxRequestMade = true;
}
});
// Trigger first HTMX element
const firstElement = htmxElements.first();
await firstElement.click();
// Wait for request
await page.waitForTimeout(1000);
// HTMX request should have been made
expect(htmxRequestMade).toBe(true);
});
test('should update DOM without full page reload', async ({ page }) => {
const htmxElements = page.locator('[hx-get], [hx-post]');
const count = await htmxElements.count();
if (count === 0) {
test.skip();
return;
}
// Track page loads
let pageReloaded = false;
page.on('load', () => {
pageReloaded = true;
});
// Get initial page state
const initialUrl = page.url();
// Trigger HTMX action
await htmxElements.first().click();
await page.waitForTimeout(1500);
// URL should not change (unless it's a navigation)
const currentUrl = page.url();
// Page should not have reloaded for simple updates
// (this is a soft check as some actions might navigate)
if (currentUrl === initialUrl) {
expect(pageReloaded).toBe(false);
}
});
test('should handle loading states during updates', async ({ page }) => {
// Look for loading indicators
const loadingIndicators = page.locator('[hx-indicator], .htmx-request, [data-loading]');
// Trigger update if HTMX elements exist
const htmxElements = page.locator('[hx-get], [hx-post]');
const count = await htmxElements.count();
if (count === 0) {
test.skip();
return;
}
// Trigger action and check for loading state
await htmxElements.first().click();
// During request, loading indicator might be visible
await page.waitForTimeout(100);
// After request, loading should be hidden
await page.waitForTimeout(2000);
const loadingCount = await loadingIndicators.count();
console.log('Loading indicators found:', loadingCount);
});
test('should preserve form data during partial updates', async ({ page }) => {
const forms = page.locator('form');
const formCount = await forms.count();
if (formCount === 0) {
test.skip();
return;
}
const form = forms.first();
const inputs = form.locator('input[type="text"], input[type="email"]');
const inputCount = await inputs.count();
if (inputCount === 0) {
test.skip();
return;
}
// Fill some data
const testValue = 'test-value-123';
await inputs.first().fill(testValue);
// Wait a bit
await page.waitForTimeout(500);
// Value should still be present
const currentValue = await inputs.first().inputValue();
expect(currentValue).toBe(testValue);
});
test('should handle WebSocket connections (if present)', async ({ page }) => {
// Track WebSocket connections
const wsConnections: string[] = [];
page.on('websocket', (ws) => {
wsConnections.push(ws.url());
ws.on('framesent', (event) => {
console.log('WebSocket sent:', event.payload);
});
ws.on('framereceived', (event) => {
console.log('WebSocket received:', event.payload);
});
});
// Wait for potential WebSocket connections
await page.waitForTimeout(2000);
// Log WebSocket connections (informational)
console.log('WebSocket Connections:', wsConnections);
// If WebSocket is used, we should have connections
if (wsConnections.length > 0) {
expect(wsConnections.length).toBeGreaterThan(0);
}
});
test('should handle connection errors gracefully', async ({ page }) => {
// Monitor console errors
const errors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
// Navigate and wait
await page.goto('/');
await page.waitForTimeout(3000);
// Filter out known/expected errors
const criticalErrors = errors.filter(
(error) => !error.includes('favicon') && !error.includes('DevTools')
);
// Should not have critical JavaScript errors
expect(criticalErrors.length).toBe(0);
});
});