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,305 @@
<?php
declare(strict_types=1);
namespace App\Framework\Vite;
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\PathProvider;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Process\Services\TcpPortCheckService;
use App\Framework\Vite\Exceptions\ViteManifestException;
use App\Framework\Vite\ValueObjects\ViteConfig;
use App\Framework\Vite\ValueObjects\ViteEntry;
use App\Framework\Vite\ValueObjects\ViteManifest;
/**
* Vite Service for Asset Management
*
* Manages Vite assets for both development (with HMR) and production (from manifest).
* Automatically detects dev server availability and generates appropriate HTML tags.
*/
final readonly class ViteService
{
private const DEV_SERVER_TIMEOUT = 1;
private const MANIFEST_CACHE_KEY = 'vite:manifest';
private const MANIFEST_CACHE_TTL_SECONDS = 3600;
public function __construct(
private ViteConfig $config,
private PathProvider $pathProvider,
private Cache $cache,
private TcpPortCheckService $portCheck
) {}
/**
* Check if Vite dev server is running
*/
public function isDevServerRunning(): bool
{
if (! $this->config->hotReload) {
return false;
}
// Parse dev server URL to get host and port
$url = parse_url($this->config->devServerUrl);
if ($url === false || ! isset($url['host'])) {
return false;
}
$host = $url['host'];
$port = $url['port'] ?? ($url['scheme'] === 'https' ? 443 : 80);
$isHttps = ($url['scheme'] ?? 'http') === 'https';
// Use TcpPortCheckService for port checking (no external dependencies)
if ($isHttps) {
return $this->portCheck->isSslPortOpen($host, $port, self::DEV_SERVER_TIMEOUT);
}
return $this->portCheck->isPortOpen($host, $port, self::DEV_SERVER_TIMEOUT);
}
/**
* Get Vite manifest (cached in production)
*/
public function getManifest(): ?ViteManifest
{
// In dev mode, skip manifest
if ($this->isDevServerRunning()) {
return null;
}
// Try cache first
$cacheKey = CacheKey::fromString(self::MANIFEST_CACHE_KEY);
$cached = $this->cache->get($cacheKey);
if ($cached !== null && $cached->value instanceof ViteManifest) {
return $cached->value;
}
// Load from file
$publicPath = $this->pathProvider->resolvePath('/public');
$manifestPath = $this->config->getManifestPath($publicPath);
try {
$manifest = ViteManifest::fromFile($manifestPath);
// Cache for 1 hour
$cacheItem = CacheItem::forSet(
key: $cacheKey,
value: $manifest,
ttl: Duration::fromSeconds(self::MANIFEST_CACHE_TTL_SECONDS)
);
$this->cache->set($cacheItem);
return $manifest;
} catch (ViteManifestException) {
return null;
}
}
/**
* Generate HTML tags for given entrypoints
*
* @param array<string> $entrypoints
*/
public function getTags(array $entrypoints = []): string
{
$entries = ! empty($entrypoints) ? $entrypoints : $this->config->entrypoints;
if ($this->isDevServerRunning()) {
return $this->getDevTags($entries);
}
return $this->getProductionTags($entries);
}
/**
* Generate tags for development mode
*
* @param array<string> $entrypoints
*/
private function getDevTags(array $entrypoints): string
{
$tags = [];
$nonce = $this->getNonceAttribute();
// Vite client for HMR
$tags[] = sprintf(
'<script type="module" src="%s"%s></script>',
$this->config->getViteClientUrl(),
$nonce
);
// Entrypoint scripts
foreach ($entrypoints as $entry) {
$entryPath = 'resources/js/' . $entry . '.js';
$url = $this->config->getDevServerEntry($entryPath);
$tags[] = sprintf(
'<script type="module" src="%s"%s></script>',
$url,
$nonce
);
}
return implode("\n", $tags);
}
/**
* Generate tags for production mode
*
* @param array<string> $entrypoints
*/
private function getProductionTags(array $entrypoints): string
{
$manifest = $this->getManifest();
if ($manifest === null) {
return '<!-- Vite manifest not found. Run "npm run build" -->';
}
$tags = [];
$processedEntries = [];
foreach ($entrypoints as $entryName) {
$entryPath = 'resources/js/' . $entryName . '.js';
$entries = $manifest->getEntryWithDependencies($entryPath);
foreach ($entries as $entry) {
// Avoid duplicates
if (in_array($entry->file, $processedEntries, true)) {
continue;
}
$processedEntries[] = $entry->file;
// Generate CSS tags
foreach ($entry->css as $cssFile) {
$tags[] = $this->generateCssTag($cssFile);
}
// Generate script tag
$tags[] = $this->generateScriptTag($entry);
}
}
return implode("\n", $tags);
}
/**
* Generate CSS link tag
*/
private function generateCssTag(string $cssFile): string
{
$url = '/' . ltrim($cssFile, '/');
return sprintf(
'<link rel="stylesheet" href="%s">',
htmlspecialchars($url, ENT_QUOTES, 'UTF-8')
);
}
/**
* Generate script tag
*/
private function generateScriptTag(ViteEntry $entry): string
{
$url = '/' . ltrim($entry->file, '/');
$nonce = $this->getNonceAttribute();
$integrity = $entry->integrity ? sprintf(' integrity="%s"', $entry->integrity) : '';
return sprintf(
'<script type="module" src="%s"%s%s></script>',
htmlspecialchars($url, ENT_QUOTES, 'UTF-8'),
$integrity,
$nonce
);
}
/**
* Get CSP nonce attribute if configured
*/
private function getNonceAttribute(): string
{
if ($this->config->nonce === null) {
return '';
}
return sprintf(' nonce="%s"', htmlspecialchars($this->config->nonce, ENT_QUOTES, 'UTF-8'));
}
/**
* Preload assets for given entrypoints
*
* @param array<string> $entrypoints
*/
public function getPreloadTags(array $entrypoints = []): string
{
// Preloading only makes sense in production
if ($this->isDevServerRunning()) {
return '';
}
$entries = ! empty($entrypoints) ? $entrypoints : $this->config->entrypoints;
$manifest = $this->getManifest();
if ($manifest === null) {
return '';
}
$tags = [];
$processedFiles = [];
foreach ($entries as $entryName) {
$entryPath = 'resources/js/' . $entryName . '.js';
$mainEntry = $manifest->getEntry($entryPath);
if ($mainEntry === null) {
continue;
}
// Preload main JS
if (! in_array($mainEntry->file, $processedFiles, true)) {
$processedFiles[] = $mainEntry->file;
$tags[] = $this->generatePreloadTag($mainEntry->file, 'script');
}
// Preload CSS
foreach ($mainEntry->css as $cssFile) {
if (! in_array($cssFile, $processedFiles, true)) {
$processedFiles[] = $cssFile;
$tags[] = $this->generatePreloadTag($cssFile, 'style');
}
}
}
return implode("\n", $tags);
}
/**
* Generate preload link tag
*/
private function generatePreloadTag(string $file, string $as): string
{
$url = '/' . ltrim($file, '/');
return sprintf(
'<link rel="preload" href="%s" as="%s">',
htmlspecialchars($url, ENT_QUOTES, 'UTF-8'),
$as
);
}
/**
* Clear manifest cache
*/
public function clearManifestCache(): bool
{
$cacheKey = CacheKey::fromString(self::MANIFEST_CACHE_KEY);
return $this->cache->forget($cacheKey);
}
}