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

@@ -7,8 +7,7 @@ namespace App\Application\Api\Images;
use App\Domain\Media\ImageRepository;
use App\Framework\Attributes\Route;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
use App\Framework\Filesystem\FilePath;
use App\Framework\Filesystem\ValueObjects\FilePath;
use App\Framework\Http\Exception\NotFound;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\Method;
@@ -29,7 +28,7 @@ final readonly class DeleteImageController
// Find image by ULID
$image = $this->imageRepository->findByUlid($ulid);
if (!$image) {
if (! $image) {
throw NotFound::create(
ErrorCode::ENTITY_NOT_FOUND,
"Image with ULID {$ulid} not found"
@@ -96,4 +95,4 @@ final readonly class DeleteImageController
}
}
}
}
}

View File

@@ -7,7 +7,6 @@ namespace App\Application\Api\Images;
use App\Domain\Media\ImageRepository;
use App\Framework\Attributes\Route;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
use App\Framework\Http\Exception\NotFound;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\Method;
@@ -27,7 +26,7 @@ final readonly class GetImageController
// Find image by ULID
$image = $this->imageRepository->findByUlid($ulid);
if (!$image) {
if (! $image) {
throw NotFound::create(
ErrorCode::ENTITY_NOT_FOUND,
"Image with ULID {$ulid} not found"
@@ -64,4 +63,4 @@ final readonly class GetImageController
], $image->variants ?? []),
]);
}
}
}

View File

@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace App\Application\Api\Images;
use App\Application\Security\Services\FileUploadSecurityService;
use App\Domain\Media\Image;
use App\Domain\Media\ImageProcessor;
use App\Domain\Media\ImageRepository;
use App\Domain\Media\ImageVariantRepository;
@@ -18,8 +17,6 @@ use App\Framework\Http\Exception\NotFound;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\Method;
use App\Framework\Http\Responses\JsonResponse;
use App\Framework\Http\Status;
use App\Framework\Http\UploadedFile;
use App\Framework\Router\Result\FileResult;
use App\Framework\Ulid\UlidGenerator;
@@ -191,7 +188,7 @@ final readonly class ImageApiController
// The path already contains the full file path
$file = $image->path->toString();
if (!file_exists($file)) {
if (! file_exists($file)) {
throw FrameworkException::create(
ErrorCode::ENTITY_NOT_FOUND,
"Image file not found on filesystem: {$file}"

View File

@@ -164,4 +164,4 @@ final readonly class ListImagesController
], $image->variants ?? []),
];
}
}
}

View File

@@ -28,7 +28,7 @@ final readonly class UpdateImageController
// Find image by ULID
$image = $this->imageRepository->findByUlid($ulid);
if (!$image) {
if (! $image) {
throw NotFound::create(
ErrorCode::ENTITY_NOT_FOUND,
"Image with ULID {$ulid} not found"
@@ -125,7 +125,7 @@ final readonly class UpdateImageController
)->withData([
'field' => 'filename',
'value' => $filename,
'invalid_chars' => ['/', '\\', ':', '*', '?', '"', '<', '>', '|']
'invalid_chars' => ['/', '\\', ':', '*', '?', '"', '<', '>', '|'],
]);
}
@@ -137,8 +137,8 @@ final readonly class UpdateImageController
)->withData([
'field' => 'filename',
'length' => strlen($filename),
'max_length' => 255
'max_length' => 255,
]);
}
}
}
}

View File

@@ -11,14 +11,13 @@ use App\Domain\Media\ImageRepository;
use App\Framework\Attributes\Route;
use App\Framework\Core\ValueObjects\FileSize;
use App\Framework\Core\ValueObjects\Hash;
use App\Framework\Core\ValueObjects\HashAlgorithm;
use App\Framework\DateTime\Clock;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
use App\Framework\Filesystem\FilePath;
use App\Framework\Http\Request;
use App\Framework\Filesystem\ValueObjects\FilePath;
use App\Framework\Http\Method;
use App\Framework\Http\MimeType;
use App\Framework\Http\Request;
use App\Framework\Http\Responses\JsonResponse;
use App\Framework\Http\Status;
use App\Framework\Http\UploadedFile;
@@ -40,19 +39,19 @@ final readonly class UploadImageController
// Validate uploaded file
$uploadedFiles = $request->files;
if ($uploadedFiles->isEmpty() || !$uploadedFiles->has('image')) {
if ($uploadedFiles->isEmpty() || ! $uploadedFiles->has('image')) {
throw FrameworkException::create(
ErrorCode::VAL_REQUIRED_FIELD_MISSING,
'No image file uploaded'
)->withData([
'field' => 'image',
'files_empty' => $uploadedFiles->isEmpty(),
'available_fields' => $uploadedFiles->keys()
'available_fields' => $uploadedFiles->keys(),
]);
}
$uploadedFile = $uploadedFiles->get('image');
if (!($uploadedFile instanceof UploadedFile)) {
if (! ($uploadedFile instanceof UploadedFile)) {
throw FrameworkException::create(
ErrorCode::VAL_INVALID_FORMAT,
'Invalid uploaded file'
@@ -62,7 +61,7 @@ final readonly class UploadImageController
// Security validation
try {
$validationResult = $this->uploadSecurityService->validateUpload($uploadedFile);
if (!$validationResult) {
if (! $validationResult) {
throw FrameworkException::create(
ErrorCode::SEC_FILE_UPLOAD_REJECTED,
'File upload security validation failed'
@@ -74,7 +73,7 @@ final readonly class UploadImageController
// Validate MIME type
$detectedMimeType = MimeType::fromFilePath($uploadedFile->name);
if (!$detectedMimeType || !$detectedMimeType->isImage()) {
if (! $detectedMimeType || ! $detectedMimeType->isImage()) {
throw FrameworkException::create(
ErrorCode::VAL_INVALID_FORMAT,
'Uploaded file is not a valid image'
@@ -85,7 +84,7 @@ final readonly class UploadImageController
$ulid = new Ulid($this->clock);
// Calculate file hash first (needed for filename)
if (!is_file($uploadedFile->tmpName)) {
if (! is_file($uploadedFile->tmpName)) {
throw FrameworkException::create(
ErrorCode::VAL_INVALID_FORMAT,
'Temporary file is not a valid file'
@@ -93,7 +92,7 @@ final readonly class UploadImageController
'tmp_name' => $uploadedFile->tmpName,
'is_file' => is_file($uploadedFile->tmpName),
'is_dir' => is_dir($uploadedFile->tmpName),
'exists' => file_exists($uploadedFile->tmpName)
'exists' => file_exists($uploadedFile->tmpName),
]);
}
@@ -202,5 +201,4 @@ final readonly class UploadImageController
'created_at' => $image->ulid->getDateTime()->format('c'),
], Status::CREATED);
}
}
}