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,81 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Framework\Development\HotReload;
use App\Framework\Development\HotReload\FileChangeEvent;
use App\Framework\Development\HotReload\FileChangeType;
describe('FileChangeEvent', function () {
it('creates event for file creation', function () {
$event = FileChangeEvent::created('/path/to/file.php');
expect($event->getPath())->toBe('/path/to/file.php');
expect($event->getType())->toBe(FileChangeType::CREATED);
expect($event->getTimestamp())->not->toBeNull();
});
it('creates event for file modification', function () {
$event = FileChangeEvent::modified('/path/to/file.php', 'abc123');
expect($event->getPath())->toBe('/path/to/file.php');
expect($event->getType())->toBe(FileChangeType::MODIFIED);
expect($event->getHash())->toBe('abc123');
});
it('creates event for file deletion', function () {
$event = FileChangeEvent::deleted('/path/to/file.php');
expect($event->getPath())->toBe('/path/to/file.php');
expect($event->getType())->toBe(FileChangeType::DELETED);
});
it('identifies PHP files', function () {
$event = FileChangeEvent::created('/path/to/file.php');
expect($event->isPhpFile())->toBeTrue();
$event2 = FileChangeEvent::created('/path/to/file.js');
expect($event2->isPhpFile())->toBeFalse();
});
it('identifies view files', function () {
$event = FileChangeEvent::created('/path/views/template.view.php');
expect($event->isViewFile())->toBeTrue();
$event2 = FileChangeEvent::created('/src/Controller.php');
expect($event2->isViewFile())->toBeFalse();
});
it('identifies config files', function () {
$event = FileChangeEvent::created('/config/app.php');
expect($event->isConfigFile())->toBeTrue();
$event2 = FileChangeEvent::created('/src/Service.php');
expect($event2->isConfigFile())->toBeFalse();
});
it('identifies asset files', function () {
expect(FileChangeEvent::created('/styles/app.css')->isAssetFile())->toBeTrue();
expect(FileChangeEvent::created('/scripts/app.js')->isAssetFile())->toBeTrue();
expect(FileChangeEvent::created('/src/app.ts')->isAssetFile())->toBeTrue();
expect(FileChangeEvent::created('/styles/main.scss')->isAssetFile())->toBeTrue();
expect(FileChangeEvent::created('/src/Controller.php')->isAssetFile())->toBeFalse();
});
it('gets relative path from base path', function () {
$event = FileChangeEvent::created('/var/www/html/src/Controller.php');
$relative = $event->getRelativePath('/var/www/html');
expect($relative)->toBe('src/Controller.php');
});
it('returns full path when base path does not match', function () {
$event = FileChangeEvent::created('/var/www/html/src/Controller.php');
$relative = $event->getRelativePath('/other/path');
expect($relative)->toBe('/var/www/html/src/Controller.php');
});
});

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Framework\Development\HotReload;
use App\Framework\Development\HotReload\FileChangeType;
describe('FileChangeType', function () {
it('has CREATED change type', function () {
expect(FileChangeType::CREATED->value)->toBe('created');
});
it('has MODIFIED change type', function () {
expect(FileChangeType::MODIFIED->value)->toBe('modified');
});
it('has DELETED change type', function () {
expect(FileChangeType::DELETED->value)->toBe('deleted');
});
it('has RENAMED change type', function () {
expect(FileChangeType::RENAMED->value)->toBe('renamed');
});
it('can be created from cases', function () {
expect(FileChangeType::cases())->toHaveCount(4);
expect(FileChangeType::cases())->toContain(FileChangeType::CREATED);
expect(FileChangeType::cases())->toContain(FileChangeType::MODIFIED);
expect(FileChangeType::cases())->toContain(FileChangeType::DELETED);
expect(FileChangeType::cases())->toContain(FileChangeType::RENAMED);
});
it('can be compared', function () {
expect(FileChangeType::CREATED)->toBe(FileChangeType::CREATED);
expect(FileChangeType::MODIFIED)->not->toBe(FileChangeType::CREATED);
});
});

View File

@@ -0,0 +1,292 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Framework\Development\HotReload;
use App\Framework\Development\HotReload\FileChangeEvent;
use App\Framework\Development\HotReload\FileChangeType;
use App\Framework\Development\HotReload\HotReloadServer;
use App\Framework\Development\HotReload\ReloadType;
describe('HotReloadServer', function () {
it('determines full reload for PHP files', function () {
$server = new \ReflectionClass(HotReloadServer::class);
$method = $server->getMethod('determineReloadType');
$fileWatcher = new class extends \App\Framework\Filesystem\FileWatcher {
public function __construct() {}
};
$sseStream = new class implements \App\Framework\Http\SseStreamInterface {
public function start($headers, $status, array $initialEvents = []): void {}
public function sendEvent($event): void {}
public function send(string $data, ?string $event = null, ?string $id = null, ?int $retry = null): void {}
public function sendJson(array $data, ?string $event = null, ?string $id = null): void {}
public function sendHeartbeat(?string $id = null): void {}
public function sendRetry(int $milliseconds): void {}
public function close(?string $message = null): void {}
public function isConnectionActive(): bool { return true; }
public function isActive(): bool { return true; }
};
$hotReloadServer = new HotReloadServer($fileWatcher, $sseStream);
$event = FileChangeEvent::modified('/var/www/html/src/Controller/UserController.php');
$reloadType = $method->invoke($hotReloadServer, $event);
expect($reloadType)->toBe(ReloadType::FULL);
});
it('determines CSS reload for CSS files', function () {
$server = new \ReflectionClass(HotReloadServer::class);
$method = $server->getMethod('determineReloadType');
$fileWatcher = new class extends \App\Framework\Filesystem\FileWatcher {
public function __construct() {}
};
$sseStream = new class implements \App\Framework\Http\SseStreamInterface {
public function start($headers, $status, array $initialEvents = []): void {}
public function sendEvent($event): void {}
public function send(string $data, ?string $event = null, ?string $id = null, ?int $retry = null): void {}
public function sendJson(array $data, ?string $event = null, ?string $id = null): void {}
public function sendHeartbeat(?string $id = null): void {}
public function sendRetry(int $milliseconds): void {}
public function close(?string $message = null): void {}
public function isConnectionActive(): bool { return true; }
public function isActive(): bool { return true; }
};
$hotReloadServer = new HotReloadServer($fileWatcher, $sseStream);
$event = FileChangeEvent::modified('/var/www/html/resources/css/app.css');
$reloadType = $method->invoke($hotReloadServer, $event);
expect($reloadType)->toBe(ReloadType::CSS);
});
it('determines HMR reload for JavaScript files', function () {
$server = new \ReflectionClass(HotReloadServer::class);
$method = $server->getMethod('determineReloadType');
$fileWatcher = new class extends \App\Framework\Filesystem\FileWatcher {
public function __construct() {}
};
$sseStream = new class implements \App\Framework\Http\SseStreamInterface {
public function start($headers, $status, array $initialEvents = []): void {}
public function sendEvent($event): void {}
public function send(string $data, ?string $event = null, ?string $id = null, ?int $retry = null): void {}
public function sendJson(array $data, ?string $event = null, ?string $id = null): void {}
public function sendHeartbeat(?string $id = null): void {}
public function sendRetry(int $milliseconds): void {}
public function close(?string $message = null): void {}
public function isConnectionActive(): bool { return true; }
public function isActive(): bool { return true; }
};
$hotReloadServer = new HotReloadServer($fileWatcher, $sseStream);
$event = FileChangeEvent::modified('/var/www/html/resources/js/app.js');
$reloadType = $method->invoke($hotReloadServer, $event);
expect($reloadType)->toBe(ReloadType::HMR);
});
it('determines HMR reload for TypeScript files', function () {
$server = new \ReflectionClass(HotReloadServer::class);
$method = $server->getMethod('determineReloadType');
$fileWatcher = new class extends \App\Framework\Filesystem\FileWatcher {
public function __construct() {}
};
$sseStream = new class implements \App\Framework\Http\SseStreamInterface {
public function start($headers, $status, array $initialEvents = []): void {}
public function sendEvent($event): void {}
public function send(string $data, ?string $event = null, ?string $id = null, ?int $retry = null): void {}
public function sendJson(array $data, ?string $event = null, ?string $id = null): void {}
public function sendHeartbeat(?string $id = null): void {}
public function sendRetry(int $milliseconds): void {}
public function close(?string $message = null): void {}
public function isConnectionActive(): bool { return true; }
public function isActive(): bool { return true; }
};
$hotReloadServer = new HotReloadServer($fileWatcher, $sseStream);
$event = FileChangeEvent::modified('/var/www/html/resources/js/module.ts');
$reloadType = $method->invoke($hotReloadServer, $event);
expect($reloadType)->toBe(ReloadType::HMR);
});
it('determines full reload for view files', function () {
$server = new \ReflectionClass(HotReloadServer::class);
$method = $server->getMethod('determineReloadType');
$fileWatcher = new class extends \App\Framework\Filesystem\FileWatcher {
public function __construct() {}
};
$sseStream = new class implements \App\Framework\Http\SseStreamInterface {
public function start($headers, $status, array $initialEvents = []): void {}
public function sendEvent($event): void {}
public function send(string $data, ?string $event = null, ?string $id = null, ?int $retry = null): void {}
public function sendJson(array $data, ?string $event = null, ?string $id = null): void {}
public function sendHeartbeat(?string $id = null): void {}
public function sendRetry(int $milliseconds): void {}
public function close(?string $message = null): void {}
public function isConnectionActive(): bool { return true; }
public function isActive(): bool { return true; }
};
$hotReloadServer = new HotReloadServer($fileWatcher, $sseStream);
$event = FileChangeEvent::modified('/var/www/html/resources/views/home.view.php');
$reloadType = $method->invoke($hotReloadServer, $event);
expect($reloadType)->toBe(ReloadType::FULL);
});
it('should clear cache for config changes', function () {
$server = new \ReflectionClass(HotReloadServer::class);
$method = $server->getMethod('shouldClearCache');
$fileWatcher = new class extends \App\Framework\Filesystem\FileWatcher {
public function __construct() {}
};
$sseStream = new class implements \App\Framework\Http\SseStreamInterface {
public function start($headers, $status, array $initialEvents = []): void {}
public function sendEvent($event): void {}
public function send(string $data, ?string $event = null, ?string $id = null, ?int $retry = null): void {}
public function sendJson(array $data, ?string $event = null, ?string $id = null): void {}
public function sendHeartbeat(?string $id = null): void {}
public function sendRetry(int $milliseconds): void {}
public function close(?string $message = null): void {}
public function isConnectionActive(): bool { return true; }
public function isActive(): bool { return true; }
};
$hotReloadServer = new HotReloadServer($fileWatcher, $sseStream);
$event = FileChangeEvent::modified('/var/www/html/config/app.php');
$shouldClear = $method->invoke($hotReloadServer, $event);
expect($shouldClear)->toBeTrue();
});
it('should clear cache for controller changes', function () {
$server = new \ReflectionClass(HotReloadServer::class);
$method = $server->getMethod('shouldClearCache');
$fileWatcher = new class extends \App\Framework\Filesystem\FileWatcher {
public function __construct() {}
};
$sseStream = new class implements \App\Framework\Http\SseStreamInterface {
public function start($headers, $status, array $initialEvents = []): void {}
public function sendEvent($event): void {}
public function send(string $data, ?string $event = null, ?string $id = null, ?int $retry = null): void {}
public function sendJson(array $data, ?string $event = null, ?string $id = null): void {}
public function sendHeartbeat(?string $id = null): void {}
public function sendRetry(int $milliseconds): void {}
public function close(?string $message = null): void {}
public function isConnectionActive(): bool { return true; }
public function isActive(): bool { return true; }
};
$hotReloadServer = new HotReloadServer($fileWatcher, $sseStream);
$event = FileChangeEvent::modified('/var/www/html/src/Controller/UserController.php');
$shouldClear = $method->invoke($hotReloadServer, $event);
expect($shouldClear)->toBeTrue();
});
it('should clear cache for route changes', function () {
$server = new \ReflectionClass(HotReloadServer::class);
$method = $server->getMethod('shouldClearCache');
$fileWatcher = new class extends \App\Framework\Filesystem\FileWatcher {
public function __construct() {}
};
$sseStream = new class implements \App\Framework\Http\SseStreamInterface {
public function start($headers, $status, array $initialEvents = []): void {}
public function sendEvent($event): void {}
public function send(string $data, ?string $event = null, ?string $id = null, ?int $retry = null): void {}
public function sendJson(array $data, ?string $event = null, ?string $id = null): void {}
public function sendHeartbeat(?string $id = null): void {}
public function sendRetry(int $milliseconds): void {}
public function close(?string $message = null): void {}
public function isConnectionActive(): bool { return true; }
public function isActive(): bool { return true; }
};
$hotReloadServer = new HotReloadServer($fileWatcher, $sseStream);
$event = FileChangeEvent::modified('/var/www/html/src/Router/WebRoutes.php');
$shouldClear = $method->invoke($hotReloadServer, $event);
expect($shouldClear)->toBeTrue();
});
it('should clear cache for view changes', function () {
$server = new \ReflectionClass(HotReloadServer::class);
$method = $server->getMethod('shouldClearCache');
$fileWatcher = new class extends \App\Framework\Filesystem\FileWatcher {
public function __construct() {}
};
$sseStream = new class implements \App\Framework\Http\SseStreamInterface {
public function start($headers, $status, array $initialEvents = []): void {}
public function sendEvent($event): void {}
public function send(string $data, ?string $event = null, ?string $id = null, ?int $retry = null): void {}
public function sendJson(array $data, ?string $event = null, ?string $id = null): void {}
public function sendHeartbeat(?string $id = null): void {}
public function sendRetry(int $milliseconds): void {}
public function close(?string $message = null): void {}
public function isConnectionActive(): bool { return true; }
public function isActive(): bool { return true; }
};
$hotReloadServer = new HotReloadServer($fileWatcher, $sseStream);
$event = FileChangeEvent::modified('/var/www/html/resources/views/components/header.view.php');
$shouldClear = $method->invoke($hotReloadServer, $event);
expect($shouldClear)->toBeTrue();
});
it('should not clear cache for regular PHP files', function () {
$server = new \ReflectionClass(HotReloadServer::class);
$method = $server->getMethod('shouldClearCache');
$fileWatcher = new class extends \App\Framework\Filesystem\FileWatcher {
public function __construct() {}
};
$sseStream = new class implements \App\Framework\Http\SseStreamInterface {
public function start($headers, $status, array $initialEvents = []): void {}
public function sendEvent($event): void {}
public function send(string $data, ?string $event = null, ?string $id = null, ?int $retry = null): void {}
public function sendJson(array $data, ?string $event = null, ?string $id = null): void {}
public function sendHeartbeat(?string $id = null): void {}
public function sendRetry(int $milliseconds): void {}
public function close(?string $message = null): void {}
public function isConnectionActive(): bool { return true; }
public function isActive(): bool { return true; }
};
$hotReloadServer = new HotReloadServer($fileWatcher, $sseStream);
$event = FileChangeEvent::modified('/var/www/html/src/Service/UserService.php');
$shouldClear = $method->invoke($hotReloadServer, $event);
expect($shouldClear)->toBeFalse();
});
});

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Framework\Development\HotReload;
use App\Framework\Development\HotReload\ReloadType;
describe('ReloadType', function () {
it('has FULL reload type', function () {
expect(ReloadType::FULL->value)->toBe('full');
});
it('has CSS reload type', function () {
expect(ReloadType::CSS->value)->toBe('css');
});
it('has HMR reload type', function () {
expect(ReloadType::HMR->value)->toBe('hmr');
});
it('has PARTIAL reload type', function () {
expect(ReloadType::PARTIAL->value)->toBe('partial');
});
it('can be created from cases', function () {
expect(ReloadType::cases())->toHaveCount(4);
expect(ReloadType::cases())->toContain(ReloadType::FULL);
expect(ReloadType::cases())->toContain(ReloadType::CSS);
expect(ReloadType::cases())->toContain(ReloadType::HMR);
expect(ReloadType::cases())->toContain(ReloadType::PARTIAL);
});
it('can be compared', function () {
expect(ReloadType::FULL)->toBe(ReloadType::FULL);
expect(ReloadType::CSS)->not->toBe(ReloadType::FULL);
});
});