Files
orion/app/templates/admin/platform-health.html
Samir Boulahtit dc7fb5ca19 feat: add capacity planning docs, image upload system, and platform health monitoring
Documentation:
- Add comprehensive capacity planning guide (docs/architecture/capacity-planning.md)
- Add operations docs: platform-health, capacity-monitoring, image-storage
- Link pricing strategy to capacity planning documentation
- Update mkdocs.yml with new Operations section

Image Upload System:
- Add ImageService with WebP conversion and sharded directory structure
- Generate multiple size variants (original, 800px, 200px)
- Add storage stats endpoint for monitoring
- Add Pillow dependency for image processing

Platform Health Monitoring:
- Add /admin/platform-health page with real-time metrics
- Show CPU, memory, disk usage with progress bars
- Display capacity thresholds with status indicators
- Generate scaling recommendations automatically
- Determine infrastructure tier based on usage
- Add psutil dependency for system metrics

Admin UI:
- Add Capacity Monitor to Platform Health section in sidebar
- Create platform-health.html template with stats cards
- Create platform-health.js for Alpine.js state management

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-25 17:17:09 +01:00

276 lines
16 KiB
HTML

{# app/templates/admin/platform-health.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
{% from 'shared/macros/headers.html' import page_header %}
{% block title %}Platform Health{% endblock %}
{% block alpine_data %}adminPlatformHealth(){% endblock %}
{% block content %}
{% call page_header("Platform Health", subtitle="System metrics, capacity monitoring, and scaling recommendations") %}
<button
@click="refresh()"
:disabled="loading"
class="flex items-center px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50"
>
<span x-show="loading" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
<span x-show="!loading" x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
Refresh
</button>
{% endcall %}
{{ loading_state('Loading platform health...') }}
{{ error_state('Error loading platform health') }}
<!-- Main Content -->
<div x-show="!loading && !error" x-cloak class="space-y-6">
<!-- Overall Status Banner -->
<div
:class="{
'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800': health?.overall_status === 'healthy',
'bg-yellow-50 border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-800': health?.overall_status === 'degraded',
'bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800': health?.overall_status === 'critical'
}"
class="px-4 py-3 rounded-lg border flex items-center justify-between"
>
<div class="flex items-center gap-3">
<span
:class="{
'text-green-600 dark:text-green-400': health?.overall_status === 'healthy',
'text-yellow-600 dark:text-yellow-400': health?.overall_status === 'degraded',
'text-red-600 dark:text-red-400': health?.overall_status === 'critical'
}"
x-html="health?.overall_status === 'healthy' ? $icon('check-circle', 'w-6 h-6') : (health?.overall_status === 'degraded' ? $icon('exclamation', 'w-6 h-6') : $icon('x-circle', 'w-6 h-6'))"
></span>
<div>
<span class="font-semibold capitalize" x-text="health?.overall_status || 'Unknown'"></span>
<span class="text-sm text-gray-600 dark:text-gray-400 ml-2">
Infrastructure Tier: <span class="font-medium" x-text="health?.infrastructure_tier"></span>
</span>
</div>
</div>
<span class="text-xs text-gray-500 dark:text-gray-400" x-text="'Last updated: ' + formatTime(health?.timestamp)"></span>
</div>
<!-- Stats Cards -->
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<!-- Products -->
<div class="px-4 py-5 bg-white rounded-lg shadow-md dark:bg-gray-800">
<div class="flex items-center">
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-400 dark:bg-blue-900/50">
<span x-html="$icon('cube', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-1 text-sm font-medium text-gray-500 dark:text-gray-400">Products</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(health?.database?.products_count || 0)"></p>
</div>
</div>
</div>
<!-- Image Storage -->
<div class="px-4 py-5 bg-white rounded-lg shadow-md dark:bg-gray-800">
<div class="flex items-center">
<div class="p-3 mr-4 text-purple-500 bg-purple-100 rounded-full dark:text-purple-400 dark:bg-purple-900/50">
<span x-html="$icon('photograph', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-1 text-sm font-medium text-gray-500 dark:text-gray-400">Image Storage</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatStorage(health?.image_storage?.total_size_gb || 0)"></p>
</div>
</div>
</div>
<!-- Database Size -->
<div class="px-4 py-5 bg-white rounded-lg shadow-md dark:bg-gray-800">
<div class="flex items-center">
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-400 dark:bg-green-900/50">
<span x-html="$icon('database', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-1 text-sm font-medium text-gray-500 dark:text-gray-400">Database</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(health?.database?.size_mb || 0) + ' MB'"></p>
</div>
</div>
</div>
<!-- Vendors -->
<div class="px-4 py-5 bg-white rounded-lg shadow-md dark:bg-gray-800">
<div class="flex items-center">
<div class="p-3 mr-4 text-orange-500 bg-orange-100 rounded-full dark:text-orange-400 dark:bg-orange-900/50">
<span x-html="$icon('office-building', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-1 text-sm font-medium text-gray-500 dark:text-gray-400">Vendors</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="formatNumber(health?.database?.vendors_count || 0)"></p>
</div>
</div>
</div>
</div>
<!-- Two Column Layout -->
<div class="grid gap-6 lg:grid-cols-2">
<!-- System Resources -->
<div class="px-4 py-5 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">System Resources</h3>
<div class="space-y-4">
<!-- CPU -->
<div>
<div class="flex justify-between mb-1">
<span class="text-sm font-medium text-gray-600 dark:text-gray-400">CPU</span>
<span class="text-sm font-semibold" x-text="(health?.system?.cpu_percent || 0).toFixed(1) + '%'"></span>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
:class="{
'bg-green-500': (health?.system?.cpu_percent || 0) < 70,
'bg-yellow-500': (health?.system?.cpu_percent || 0) >= 70 && (health?.system?.cpu_percent || 0) < 85,
'bg-red-500': (health?.system?.cpu_percent || 0) >= 85
}"
class="h-2 rounded-full transition-all"
:style="'width: ' + Math.min(health?.system?.cpu_percent || 0, 100) + '%'"
></div>
</div>
</div>
<!-- Memory -->
<div>
<div class="flex justify-between mb-1">
<span class="text-sm font-medium text-gray-600 dark:text-gray-400">Memory</span>
<span class="text-sm">
<span class="font-semibold" x-text="(health?.system?.memory_percent || 0).toFixed(1) + '%'"></span>
<span class="text-gray-500 dark:text-gray-400" x-text="' (' + (health?.system?.memory_used_gb || 0).toFixed(1) + ' / ' + (health?.system?.memory_total_gb || 0).toFixed(1) + ' GB)'"></span>
</span>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
:class="{
'bg-green-500': (health?.system?.memory_percent || 0) < 75,
'bg-yellow-500': (health?.system?.memory_percent || 0) >= 75 && (health?.system?.memory_percent || 0) < 90,
'bg-red-500': (health?.system?.memory_percent || 0) >= 90
}"
class="h-2 rounded-full transition-all"
:style="'width: ' + Math.min(health?.system?.memory_percent || 0, 100) + '%'"
></div>
</div>
</div>
<!-- Disk -->
<div>
<div class="flex justify-between mb-1">
<span class="text-sm font-medium text-gray-600 dark:text-gray-400">Disk</span>
<span class="text-sm">
<span class="font-semibold" x-text="(health?.system?.disk_percent || 0).toFixed(1) + '%'"></span>
<span class="text-gray-500 dark:text-gray-400" x-text="' (' + (health?.system?.disk_used_gb || 0).toFixed(1) + ' / ' + (health?.system?.disk_total_gb || 0).toFixed(1) + ' GB)'"></span>
</span>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
:class="{
'bg-green-500': (health?.system?.disk_percent || 0) < 70,
'bg-yellow-500': (health?.system?.disk_percent || 0) >= 70 && (health?.system?.disk_percent || 0) < 85,
'bg-red-500': (health?.system?.disk_percent || 0) >= 85
}"
class="h-2 rounded-full transition-all"
:style="'width: ' + Math.min(health?.system?.disk_percent || 0, 100) + '%'"
></div>
</div>
</div>
</div>
</div>
<!-- Capacity Thresholds -->
<div class="px-4 py-5 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">Capacity Thresholds</h3>
<div class="space-y-3">
<template x-for="threshold in health?.thresholds || []" :key="threshold.name">
<div class="flex items-center justify-between py-2 border-b border-gray-100 dark:border-gray-700 last:border-0">
<div class="flex items-center gap-2">
<span
:class="{
'bg-green-100 text-green-600 dark:bg-green-900/50 dark:text-green-400': threshold.status === 'ok',
'bg-yellow-100 text-yellow-600 dark:bg-yellow-900/50 dark:text-yellow-400': threshold.status === 'warning',
'bg-red-100 text-red-600 dark:bg-red-900/50 dark:text-red-400': threshold.status === 'critical'
}"
class="w-2 h-2 rounded-full"
></span>
<span class="text-sm text-gray-700 dark:text-gray-300" x-text="threshold.name"></span>
</div>
<div class="text-right">
<span class="text-sm font-medium" x-text="formatNumber(threshold.current)"></span>
<span class="text-xs text-gray-500 dark:text-gray-400" x-text="' / ' + formatNumber(threshold.limit)"></span>
<span
:class="{
'text-green-600 dark:text-green-400': threshold.status === 'ok',
'text-yellow-600 dark:text-yellow-400': threshold.status === 'warning',
'text-red-600 dark:text-red-400': threshold.status === 'critical'
}"
class="text-xs ml-1"
x-text="'(' + threshold.percent_used.toFixed(0) + '%)'"
></span>
</div>
</div>
</template>
</div>
</div>
</div>
<!-- Recommendations -->
<div class="px-4 py-5 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">Scaling Recommendations</h3>
<div class="space-y-3">
<template x-for="rec in health?.recommendations || []" :key="rec.title">
<div
:class="{
'border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-900/20': rec.priority === 'info',
'border-yellow-200 bg-yellow-50 dark:border-yellow-800 dark:bg-yellow-900/20': rec.priority === 'warning',
'border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-900/20': rec.priority === 'critical'
}"
class="p-4 rounded-lg border"
>
<div class="flex items-start gap-3">
<span
:class="{
'text-blue-600 dark:text-blue-400': rec.priority === 'info',
'text-yellow-600 dark:text-yellow-400': rec.priority === 'warning',
'text-red-600 dark:text-red-400': rec.priority === 'critical'
}"
x-html="rec.priority === 'info' ? $icon('information-circle', 'w-5 h-5') : (rec.priority === 'warning' ? $icon('exclamation', 'w-5 h-5') : $icon('x-circle', 'w-5 h-5'))"
></span>
<div class="flex-1">
<p class="font-medium text-gray-800 dark:text-gray-200" x-text="rec.title"></p>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1" x-text="rec.description"></p>
<p x-show="rec.action" class="text-sm font-medium mt-2" x-text="rec.action"></p>
</div>
</div>
</div>
</template>
</div>
</div>
<!-- Quick Links -->
<div class="px-4 py-5 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">Related Resources</h3>
<div class="grid gap-4 md:grid-cols-3">
<a href="/admin/code-quality" class="flex items-center gap-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<span class="text-purple-600 dark:text-purple-400" x-html="$icon('code', 'w-5 h-5')"></span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Code Quality Dashboard</span>
</a>
<a href="/admin/settings" class="flex items-center gap-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<span class="text-gray-600 dark:text-gray-400" x-html="$icon('cog', 'w-5 h-5')"></span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Platform Settings</span>
</a>
<a href="https://docs.wizamart.com/architecture/capacity-planning/" target="_blank" class="flex items-center gap-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<span class="text-blue-600 dark:text-blue-400" x-html="$icon('book-open', 'w-5 h-5')"></span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Capacity Planning Docs</span>
</a>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('static', path='admin/js/platform-health.js') }}"></script>
{% endblock %}