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>
276 lines
16 KiB
HTML
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 %}
|