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

@@ -4,8 +4,11 @@ declare(strict_types=1);
namespace App\Application\Admin;
use App\Domain\PreSave\PreSaveCampaign;
use App\Domain\PreSave\PreSaveCampaignRepository;
use App\Domain\PreSave\Services\PreSaveCampaignService;
use App\Domain\PreSave\ValueObjects\CampaignStatus;
use App\Domain\PreSave\ValueObjects\TrackUrl;
use App\Framework\Admin\AdminPageRenderer;
use App\Framework\Admin\Attributes\AdminResource;
use App\Framework\Admin\Factories\AdminFormFactory;
@@ -13,18 +16,14 @@ use App\Framework\Admin\Factories\AdminTableFactory;
use App\Framework\Admin\ValueObjects\AdminFormConfig;
use App\Framework\Admin\ValueObjects\AdminTableConfig;
use App\Framework\Attributes\Route;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\Method;
use App\Framework\Router\Result\JsonResult;
use App\Framework\Router\Result\Redirect;
use App\Framework\Router\Result\ViewResult;
use App\Domain\PreSave\PreSaveCampaign;
use App\Domain\PreSave\ValueObjects\CampaignStatus;
use App\Domain\PreSave\ValueObjects\StreamingPlatform;
use App\Domain\PreSave\ValueObjects\TrackUrl;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Exception\FrameworkException;
use App\Framework\Exception\ErrorCode;
/**
* Pre-Save Campaign Admin Controller
@@ -47,7 +46,8 @@ final readonly class PreSaveCampaignAdminController
private AdminFormFactory $formFactory,
private PreSaveCampaignRepository $repository,
private PreSaveCampaignService $service,
) {}
) {
}
/**
* List all campaigns
@@ -94,7 +94,7 @@ final readonly class PreSaveCampaignAdminController
$campaigns = $this->repository->findAll();
$campaignData = array_map(
fn($campaign) => [
fn ($campaign) => [
...$campaign->toArray(),
'release_date' => $campaign->releaseDate->format('Y-m-d H:i'),
'created_at' => $campaign->createdAt->format('Y-m-d H:i'),
@@ -163,7 +163,7 @@ final readonly class PreSaveCampaignAdminController
$campaigns = $this->repository->findAll();
$items = array_map(
fn($campaign) => [
fn ($campaign) => [
...$campaign->toArray(),
'release_date' => $campaign->releaseDate->format('Y-m-d H:i'),
'created_at' => $campaign->createdAt->format('Y-m-d H:i'),
@@ -293,13 +293,13 @@ final readonly class PreSaveCampaignAdminController
// Parse track URLs
$trackUrls = [];
if (!empty($data['track_url_spotify'])) {
if (! empty($data['track_url_spotify'])) {
$trackUrls[] = TrackUrl::fromUrl($data['track_url_spotify']);
}
if (!empty($data['track_url_tidal'])) {
if (! empty($data['track_url_tidal'])) {
$trackUrls[] = TrackUrl::fromUrl($data['track_url_tidal']);
}
if (!empty($data['track_url_apple_music'])) {
if (! empty($data['track_url_apple_music'])) {
$trackUrls[] = TrackUrl::fromUrl($data['track_url_apple_music']);
}
@@ -319,8 +319,8 @@ final readonly class PreSaveCampaignAdminController
coverImageUrl: $data['cover_image_url'],
releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable($data['release_date'])),
trackUrls: $trackUrls,
description: !empty($data['description']) ? $data['description'] : null,
startDate: !empty($data['start_date']) ? Timestamp::fromDateTime(new \DateTimeImmutable($data['start_date'])) : null
description: ! empty($data['description']) ? $data['description'] : null,
startDate: ! empty($data['start_date']) ? Timestamp::fromDateTime(new \DateTimeImmutable($data['start_date'])) : null
);
// Save campaign
@@ -461,13 +461,13 @@ final readonly class PreSaveCampaignAdminController
// Parse track URLs
$trackUrls = [];
if (!empty($data['track_url_spotify'])) {
if (! empty($data['track_url_spotify'])) {
$trackUrls[] = TrackUrl::fromUrl($data['track_url_spotify']);
}
if (!empty($data['track_url_tidal'])) {
if (! empty($data['track_url_tidal'])) {
$trackUrls[] = TrackUrl::fromUrl($data['track_url_tidal']);
}
if (!empty($data['track_url_apple_music'])) {
if (! empty($data['track_url_apple_music'])) {
$trackUrls[] = TrackUrl::fromUrl($data['track_url_apple_music']);
}
@@ -490,8 +490,8 @@ final readonly class PreSaveCampaignAdminController
status: CampaignStatus::from($data['status']),
createdAt: $campaign->createdAt,
updatedAt: Timestamp::now(),
description: !empty($data['description']) ? $data['description'] : null,
startDate: !empty($data['start_date']) ? Timestamp::fromDateTime(new \DateTimeImmutable($data['start_date'])) : null
description: ! empty($data['description']) ? $data['description'] : null,
startDate: ! empty($data['start_date']) ? Timestamp::fromDateTime(new \DateTimeImmutable($data['start_date'])) : null
);
$this->repository->save($updatedCampaign);
@@ -540,7 +540,7 @@ final readonly class PreSaveCampaignAdminController
'Only draft campaigns can be published'
)->withData([
'campaign_id' => $id,
'current_status' => $campaign->status->value
'current_status' => $campaign->status->value,
]);
}
@@ -574,13 +574,13 @@ final readonly class PreSaveCampaignAdminController
)->withData(['campaign_id' => $id]);
}
if (!in_array($campaign->status, [CampaignStatus::DRAFT, CampaignStatus::SCHEDULED, CampaignStatus::ACTIVE], true)) {
if (! in_array($campaign->status, [CampaignStatus::DRAFT, CampaignStatus::SCHEDULED, CampaignStatus::ACTIVE], true)) {
throw FrameworkException::create(
ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
'Campaign cannot be cancelled in current status'
)->withData([
'campaign_id' => $id,
'current_status' => $campaign->status->value
'current_status' => $campaign->status->value,
]);
}