Files
orion/app/modules/hosting/templates/hosting/admin/dashboard.html
Samir Boulahtit 2287f4597d
All checks were successful
CI / ruff (push) Successful in 10s
CI / pytest (push) Successful in 48m48s
CI / validate (push) Successful in 24s
CI / dependency-scanning (push) Successful in 29s
CI / docs (push) Successful in 38s
CI / deploy (push) Successful in 51s
feat(hosting,prospecting): add hosting unit tests and fix template bugs
- Add 55 unit tests for hosting module (hosted site service, client
  service service, stats service) with full fixture setup
- Fix table_empty_state macro: add x_message param for dynamic Alpine.js
  expressions rendered via x-text instead of server-side Jinja
- Fix hosting templates (sites, clients) using message= with Alpine
  expressions that rendered as literal text
- Fix prospecting templates (leads, scan-jobs, prospects) using
  nonexistent subtitle= param, migrated to x_message=
- Align hosting and prospecting admin templates with shared design system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 06:18:26 +01:00

152 lines
7.9 KiB
HTML

{% extends "admin/base.html" %}
{% from 'shared/macros/headers.html' import page_header, section_header %}
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
{% block title %}Hosting Dashboard{% endblock %}
{% block alpine_data %}hostingDashboard(){% endblock %}
{% block content %}
{{ page_header('Hosting Dashboard') }}
{{ loading_state('Loading dashboard...') }}
{{ error_state('Error loading dashboard') }}
<div x-show="!loading && !error" class="space-y-6">
<!-- KPI Cards -->
<div class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-teal-500 bg-teal-100 rounded-full dark:text-teal-100 dark:bg-teal-500">
<span x-html="$icon('globe', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Total Sites</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.total_sites || 0"></p>
</div>
</div>
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-100 dark:bg-green-500">
<span x-html="$icon('check-circle', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Live Sites</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.live_sites || 0"></p>
</div>
</div>
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-orange-500 bg-orange-100 rounded-full dark:text-orange-100 dark:bg-orange-500">
<span x-html="$icon('eye', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">POC Sites</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.poc_sites || 0"></p>
</div>
</div>
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-red-500 bg-red-100 rounded-full dark:text-red-100 dark:bg-red-500">
<span x-html="$icon('clock', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Upcoming Renewals</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.upcoming_renewals || 0"></p>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
{{ section_header('Quick Actions', icon='cursor-click') }}
<div class="flex flex-wrap gap-3">
<a href="/admin/hosting/sites/new"
class="inline-flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-teal-600 border border-transparent rounded-lg hover:bg-teal-700 focus:outline-none">
<span x-html="$icon('plus', 'w-4 h-4 mr-2')"></span>
New Site
</a>
<a href="/admin/hosting/sites"
class="inline-flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 dark:text-gray-300 transition-colors duration-150 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none">
<span x-html="$icon('globe', 'w-4 h-4 mr-2')"></span>
All Sites
</a>
<a href="/admin/hosting/clients"
class="inline-flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 dark:text-gray-300 transition-colors duration-150 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none">
<span x-html="$icon('cube', 'w-4 h-4 mr-2')"></span>
All Services
</a>
</div>
</div>
<!-- Sites by Status + Active Services -->
<div class="grid gap-6 md:grid-cols-2">
<div class="p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
{{ section_header('Sites by Status', icon='globe') }}
<div class="space-y-3">
<template x-for="status in ['draft', 'poc_ready', 'proposal_sent', 'accepted', 'live', 'suspended', 'cancelled']" :key="status">
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600 dark:text-gray-400 capitalize" x-text="status.replace('_', ' ')"></span>
<span class="px-2.5 py-0.5 text-xs font-medium rounded-full"
:class="statusBadgeClass(status)"
x-text="stats.sites_by_status?.[status] || 0"></span>
</div>
</template>
</div>
</div>
<div class="p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
{{ section_header('Active Services', icon='cube') }}
<div class="space-y-3">
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600 dark:text-gray-400">Total Active</span>
<span class="text-sm font-semibold text-gray-700 dark:text-gray-200" x-text="stats.active_services || 0"></span>
</div>
<template x-for="[type, count] in Object.entries(stats.services_by_type || {})" :key="type">
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600 dark:text-gray-400 capitalize" x-text="type.replace('_', ' ')"></span>
<span class="text-sm font-semibold text-gray-700 dark:text-gray-200" x-text="count"></span>
</div>
</template>
</div>
<div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-600 dark:text-gray-400">Monthly Revenue</span>
<span class="text-lg font-semibold text-green-600 dark:text-green-400"
x-text="'€' + ((stats.monthly_revenue_cents || 0) / 100).toFixed(2)"></span>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
function hostingDashboard() {
return {
...data(),
currentPage: 'hosting-dashboard',
loading: true,
error: null,
stats: {},
async init() {
try {
this.stats = await apiClient.get('/admin/hosting/stats/dashboard');
} catch (e) {
this.error = e.message;
} finally {
this.loading = false;
}
},
statusBadgeClass(status) {
const classes = {
draft: 'text-gray-700 bg-gray-100 dark:text-gray-300 dark:bg-gray-700',
poc_ready: 'text-blue-700 bg-blue-100 dark:text-blue-100 dark:bg-blue-700',
proposal_sent: 'text-yellow-700 bg-yellow-100 dark:text-yellow-100 dark:bg-yellow-700',
accepted: 'text-purple-700 bg-purple-100 dark:text-purple-100 dark:bg-purple-700',
live: 'text-green-700 bg-green-100 dark:text-green-100 dark:bg-green-700',
suspended: 'text-red-700 bg-red-100 dark:text-red-100 dark:bg-red-700',
cancelled: 'text-gray-500 bg-gray-200 dark:text-gray-400 dark:bg-gray-600',
};
return classes[status] || 'text-gray-700 bg-gray-100';
},
};
}
</script>
{% endblock %}