feat(Deployment): Integrate Ansible deployment via PHP deployment pipeline

- Create AnsibleDeployStage using framework's Process module for secure command execution
- Integrate AnsibleDeployStage into DeploymentPipelineCommands for production deployments
- Add force_deploy flag support in Ansible playbook to override stale locks
- Use PHP deployment module as orchestrator (php console.php deploy:production)
- Fix ErrorAggregationInitializer to use Environment class instead of $_ENV superglobal

Architecture:
- BuildStage → AnsibleDeployStage → HealthCheckStage for production
- Process module provides timeout, error handling, and output capture
- Ansible playbook supports rollback via rollback-git-based.yml
- Zero-downtime deployments with health checks
This commit is contained in:
2025-10-26 14:08:07 +01:00
parent a90263d3be
commit 3b623e7afb
170 changed files with 19888 additions and 575 deletions

View File

@@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
namespace App\Application\Admin\MachineLearning;
use App\Application\Admin\Service\AdminLayoutProcessor;
use App\Framework\Attributes\Route;
use App\Framework\Auth\Auth;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\Method;
use App\Framework\MachineLearning\ModelManagement\ModelPerformanceMonitor;
use App\Framework\MachineLearning\ModelManagement\ModelRegistry;
use App\Framework\Meta\MetaData;
use App\Framework\Router\AdminRoutes;
use App\Framework\Router\Result\ViewResult;
final readonly class MLDashboardAdminController
{
public function __construct(
private ModelRegistry $registry,
private ModelPerformanceMonitor $performanceMonitor,
private AdminLayoutProcessor $layoutProcessor
) {}
#[Auth]
#[Route(path: '/admin/ml/dashboard', method: Method::GET, name: AdminRoutes::ML_DASHBOARD)]
public function dashboard(HttpRequest $request): ViewResult
{
$timeWindowHours = (int) ($request->queryParameters['timeWindow'] ?? 24);
$timeWindow = Duration::fromHours($timeWindowHours);
// Get all models
$allModels = $this->getAllModels();
// Collect performance overview
$performanceOverview = [];
$totalPredictions = 0;
$accuracySum = 0.0;
$healthyCount = 0;
$degradedCount = 0;
$criticalCount = 0;
foreach ($allModels as $metadata) {
$metrics = $this->performanceMonitor->getCurrentMetrics(
$metadata->modelName,
$metadata->version,
$timeWindow
);
$accuracy = $metrics['accuracy'];
$isHealthy = $accuracy >= 0.85;
$isCritical = $accuracy < 0.7;
if ($isHealthy) {
$healthyCount++;
} elseif ($isCritical) {
$criticalCount++;
} else {
$degradedCount++;
}
$performanceOverview[] = [
'model_name' => $metadata->modelName,
'version' => $metadata->version->toString(),
'type' => $metadata->modelType->value,
'accuracy' => round($accuracy * 100, 2),
'precision' => isset($metrics['precision']) ? round($metrics['precision'] * 100, 2) : null,
'recall' => isset($metrics['recall']) ? round($metrics['recall'] * 100, 2) : null,
'f1_score' => isset($metrics['f1_score']) ? round($metrics['f1_score'] * 100, 2) : null,
'total_predictions' => number_format($metrics['total_predictions']),
'average_confidence' => isset($metrics['average_confidence']) ? round($metrics['average_confidence'] * 100, 2) : null,
'threshold' => $metadata->configuration['threshold'] ?? null,
'status' => $isHealthy ? 'healthy' : ($isCritical ? 'critical' : 'degraded'),
'status_badge' => $isHealthy ? 'success' : ($isCritical ? 'danger' : 'warning'),
];
$totalPredictions += $metrics['total_predictions'];
$accuracySum += $accuracy;
}
// Calculate degradation alerts
$degradationAlerts = [];
foreach ($performanceOverview as $model) {
if ($model['status'] !== 'healthy') {
$degradationAlerts[] = [
'model_name' => $model['model_name'],
'version' => $model['version'],
'current_accuracy' => $model['accuracy'],
'threshold' => 85.0,
'severity' => $model['status'],
'severity_badge' => $model['status_badge'],
'recommendation' => 'Consider retraining or rolling back to previous version',
];
}
}
// Calculate health indicators
$modelCount = count($allModels);
$averageAccuracy = $modelCount > 0 ? ($accuracySum / $modelCount) * 100 : 0.0;
$healthPercentage = $modelCount > 0 ? ($healthyCount / $modelCount) * 100 : 0.0;
$overallStatus = $criticalCount > 0 ? 'critical' : ($degradedCount > $modelCount / 2 ? 'warning' : 'healthy');
$overallBadge = $criticalCount > 0 ? 'danger' : ($degradedCount > $modelCount / 2 ? 'warning' : 'success');
// Count by type
$byType = [
'supervised' => 0,
'unsupervised' => 0,
'reinforcement' => 0,
];
foreach ($allModels as $metadata) {
$typeName = strtolower($metadata->modelType->value);
$byType[$typeName] = ($byType[$typeName] ?? 0) + 1;
}
$data = [
'title' => 'ML Model Dashboard',
'page_title' => 'Machine Learning Model Dashboard',
'current_path' => '/admin/ml/dashboard',
'time_window_hours' => $timeWindowHours,
// Summary stats
'total_models' => $modelCount,
'healthy_models' => $healthyCount,
'degraded_models' => $degradedCount,
'critical_models' => $criticalCount,
'total_predictions' => number_format($totalPredictions),
'average_accuracy' => round($averageAccuracy, 2),
'health_percentage' => round($healthPercentage, 2),
'overall_status' => ucfirst($overallStatus),
'overall_badge' => $overallBadge,
// Type distribution
'supervised_count' => $byType['supervised'],
'unsupervised_count' => $byType['unsupervised'],
'reinforcement_count' => $byType['reinforcement'],
// Models and alerts
'models' => $performanceOverview,
'alerts' => $degradationAlerts,
'has_alerts' => count($degradationAlerts) > 0,
'alert_count' => count($degradationAlerts),
// Links
'api_dashboard_url' => '/api/ml/dashboard',
'api_health_url' => '/api/ml/dashboard/health',
];
$finalData = $this->layoutProcessor->processLayoutFromArray($data);
return new ViewResult(
template: 'ml-dashboard',
metaData: new MetaData('ML Dashboard', 'Machine Learning Model Monitoring and Performance'),
data: $finalData
);
}
/**
* Get all models from registry (all names and all versions)
*/
private function getAllModels(): array
{
$modelNames = $this->registry->getAllModelNames();
$allModels = [];
foreach ($modelNames as $modelName) {
$versions = $this->registry->getAll($modelName);
$allModels = array_merge($allModels, $versions);
}
return $allModels;
}
}

View File

@@ -0,0 +1,253 @@
<layout name="admin" />
<x-breadcrumbs items='[
{"label": "Admin", "url": "/admin"},
{"label": "ML Dashboard", "url": "/admin/ml/dashboard"}
]' />
<div class="admin-page">
<div class="admin-page__header">
<div class="admin-page__header-content">
<h1 class="admin-page__title">{{ $page_title }}</h1>
<p class="admin-page__subtitle">Monitor machine learning model performance and health metrics</p>
</div>
<div class="admin-page__actions">
<a href="{{ $api_dashboard_url }}" class="admin-button admin-button--secondary" target="_blank">
<svg class="admin-icon" width="16" height="16" fill="currentColor">
<path d="M8 2a6 6 0 100 12A6 6 0 008 2zm0 10a4 4 0 110-8 4 4 0 010 8z"/>
</svg>
View API
</a>
</div>
</div>
<!-- Summary Cards -->
<div class="admin-grid admin-grid--3-col">
<!-- System Health Card -->
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">System Health</h3>
</div>
<div class="admin-card__content">
<div class="admin-stat-list">
<div class="admin-stat-item">
<span class="admin-stat-item__label">Overall Status</span>
<span class="admin-stat-item__value">
<span class="admin-badge admin-badge--{{ $overall_badge }}">{{ $overall_status }}</span>
</span>
</div>
<div class="admin-stat-item">
<span class="admin-stat-item__label">Health Percentage</span>
<span class="admin-stat-item__value">{{ $health_percentage }}%</span>
</div>
<div class="admin-stat-item">
<span class="admin-stat-item__label">Average Accuracy</span>
<span class="admin-stat-item__value">{{ $average_accuracy }}%</span>
</div>
<div class="admin-stat-item">
<span class="admin-stat-item__label">Time Window</span>
<span class="admin-stat-item__value">{{ $time_window_hours }} hours</span>
</div>
</div>
</div>
</div>
<!-- Model Statistics Card -->
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">Model Statistics</h3>
</div>
<div class="admin-card__content">
<div class="admin-stat-list">
<div class="admin-stat-item">
<span class="admin-stat-item__label">Total Models</span>
<span class="admin-stat-item__value">{{ $total_models }}</span>
</div>
<div class="admin-stat-item">
<span class="admin-stat-item__label">Healthy</span>
<span class="admin-stat-item__value">
<span class="admin-badge admin-badge--success">{{ $healthy_models }}</span>
</span>
</div>
<div class="admin-stat-item">
<span class="admin-stat-item__label">Degraded</span>
<span class="admin-stat-item__value">
<span class="admin-badge admin-badge--warning">{{ $degraded_models }}</span>
</span>
</div>
<div class="admin-stat-item">
<span class="admin-stat-item__label">Critical</span>
<span class="admin-stat-item__value">
<span class="admin-badge admin-badge--danger">{{ $critical_models }}</span>
</span>
</div>
</div>
</div>
</div>
<!-- Performance Metrics Card -->
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">Performance Metrics</h3>
</div>
<div class="admin-card__content">
<div class="admin-stat-list">
<div class="admin-stat-item">
<span class="admin-stat-item__label">Total Predictions</span>
<span class="admin-stat-item__value">{{ $total_predictions }}</span>
</div>
<div class="admin-stat-item">
<span class="admin-stat-item__label">Supervised Models</span>
<span class="admin-stat-item__value">{{ $supervised_count }}</span>
</div>
<div class="admin-stat-item">
<span class="admin-stat-item__label">Unsupervised Models</span>
<span class="admin-stat-item__value">{{ $unsupervised_count }}</span>
</div>
<div class="admin-stat-item">
<span class="admin-stat-item__label">Reinforcement Models</span>
<span class="admin-stat-item__value">{{ $reinforcement_count }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Degradation Alerts Section -->
<div class="admin-card" if="{{ $has_alerts }}">
<div class="admin-card__header">
<h3 class="admin-card__title">
Degradation Alerts
<span class="admin-badge admin-badge--danger">{{ $alert_count }}</span>
</h3>
</div>
<div class="admin-card__content">
<div class="admin-table-container">
<table class="admin-table">
<thead>
<tr>
<th>Model</th>
<th>Version</th>
<th>Current Accuracy</th>
<th>Threshold</th>
<th>Severity</th>
<th>Recommendation</th>
</tr>
</thead>
<tbody>
<tr foreach="$alerts as $alert">
<td>
<strong>{{ $alert['model_name'] }}</strong>
</td>
<td>
<code>{{ $alert['version'] }}</code>
</td>
<td>
<span class="admin-badge admin-badge--{{ $alert['severity_badge'] }}">
{{ $alert['current_accuracy'] }}%
</span>
</td>
<td>{{ $alert['threshold'] }}%</td>
<td>
<span class="admin-badge admin-badge--{{ $alert['severity_badge'] }}">
{{ $alert['severity'] }}
</span>
</td>
<td>{{ $alert['recommendation'] }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Models Overview Section -->
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">Models Overview</h3>
</div>
<div class="admin-card__content">
<div class="admin-table-container">
<table class="admin-table">
<thead>
<tr>
<th>Model Name</th>
<th>Version</th>
<th>Type</th>
<th>Accuracy</th>
<th>Precision</th>
<th>Recall</th>
<th>F1 Score</th>
<th>Predictions</th>
<th>Avg Confidence</th>
<th>Threshold</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr foreach="$models as $model">
<td>
<strong>{{ $model['model_name'] }}</strong>
</td>
<td>
<code>{{ $model['version'] }}</code>
</td>
<td>
<span class="admin-badge admin-badge--info">
{{ $model['type'] }}
</span>
</td>
<td>{{ $model['accuracy'] }}%</td>
<td>
<span if="!{{ $model['precision'] }}">-</span>
<span if="{{ $model['precision'] }}">{{ $model['precision'] }}%</span>
</td>
<td>
<span if="!{{ $model['recall'] }}">-</span>
<span if="{{ $model['recall'] }}">{{ $model['recall'] }}%</span>
</td>
<td>
<span if="!{{ $model['f1_score'] }}">-</span>
<span if="{{ $model['f1_score'] }}">{{ $model['f1_score'] }}%</span>
</td>
<td>{{ $model['total_predictions'] }}</td>
<td>
<span if="!{{ $model['average_confidence'] }}">-</span>
<span if="{{ $model['average_confidence'] }}">{{ $model['average_confidence'] }}%</span>
</td>
<td>{{ $model['threshold'] }}</td>
<td>
<span class="admin-badge admin-badge--{{ $model['status_badge'] }}">
{{ $model['status'] }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- API Information Card -->
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">API Endpoints</h3>
</div>
<div class="admin-card__content">
<div class="admin-stat-list">
<div class="admin-stat-item">
<span class="admin-stat-item__label">Dashboard Data</span>
<span class="admin-stat-item__value">
<code>GET {{ $api_dashboard_url }}</code>
</span>
</div>
<div class="admin-stat-item">
<span class="admin-stat-item__label">Health Check</span>
<span class="admin-stat-item__value">
<code>GET {{ $api_health_url }}</code>
</span>
</div>
</div>
</div>
</div>
</div>