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:
@@ -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;
|
||||
}
|
||||
}
|
||||
253
src/Application/Admin/templates/ml-dashboard.view.php
Normal file
253
src/Application/Admin/templates/ml-dashboard.view.php
Normal 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>
|
||||
Reference in New Issue
Block a user