Files
v2-Docker/v2_adminpanel/templates/license_analytics.html
Claude Project Manager 0d7d888502 Initial commit
2025-07-05 17:51:16 +02:00

445 Zeilen
15 KiB
HTML

{% extends "base.html" %}
{% block title %}License Analytics{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col-12">
<h1 class="h3">License Analytics</h1>
<!-- Time Range Selector -->
<div class="btn-group mb-3" role="group">
<a href="{{ url_for('admin.license_analytics', days=7) }}"
class="btn btn-sm {% if days == 7 %}btn-primary{% else %}btn-outline-primary{% endif %}">7 Tage</a>
<a href="{{ url_for('admin.license_analytics', days=30) }}"
class="btn btn-sm {% if days == 30 %}btn-primary{% else %}btn-outline-primary{% endif %}">30 Tage</a>
<a href="{{ url_for('admin.license_analytics', days=90) }}"
class="btn btn-sm {% if days == 90 %}btn-primary{% else %}btn-outline-primary{% endif %}">90 Tage</a>
</div>
</div>
</div>
<!-- Summary Cards -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">Aktive Lizenzen</h5>
<h2 class="text-primary" id="active-licenses">-</h2>
<small class="text-muted">In den letzten {{ days }} Tagen</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">Aktive Geräte</h5>
<h2 class="text-info" id="active-devices">-</h2>
<small class="text-muted">Unique Hardware IDs</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">Validierungen</h5>
<h2 class="text-success" id="total-validations">-</h2>
<small class="text-muted">Gesamt in {{ days }} Tagen</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">Churn Risk</h5>
<h2 class="text-warning" id="churn-risk">-</h2>
<small class="text-muted">Kunden mit hohem Risiko</small>
</div>
</div>
</div>
</div>
<!-- Usage Trends Chart -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Nutzungstrends</h5>
</div>
<div class="card-body">
<canvas id="usageTrendsChart" height="100"></canvas>
</div>
</div>
</div>
</div>
<!-- Performance Metrics -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Performance Metriken</h5>
</div>
<div class="card-body">
<canvas id="performanceChart" height="150"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Lizenzverteilung</h5>
</div>
<div class="card-body">
<canvas id="distributionChart" height="150"></canvas>
</div>
</div>
</div>
</div>
<!-- Revenue Analysis -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Revenue Analysis</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" id="revenue-table">
<thead>
<tr>
<th>Lizenztyp</th>
<th>Anzahl Lizenzen</th>
<th>Aktive Lizenzen</th>
<th>Gesamtumsatz</th>
<th>Aktiver Umsatz</th>
<th>Inaktiver Umsatz</th>
</tr>
</thead>
<tbody>
<!-- Wird von JavaScript gefüllt -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Top Performers -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Top 10 Aktive Lizenzen</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm" id="top-licenses">
<thead>
<tr>
<th>Lizenz</th>
<th>Kunde</th>
<th>Geräte</th>
<th>Validierungen</th>
</tr>
</thead>
<tbody>
<!-- Wird von JavaScript gefüllt -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Churn Risk Kunden</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm" id="churn-risk-table">
<thead>
<tr>
<th>Kunde</th>
<th>Lizenzen</th>
<th>Letzte Aktivität</th>
<th>Risk Level</th>
</tr>
</thead>
<tbody>
<!-- Wird von JavaScript gefüllt -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Usage Patterns Heatmap -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Nutzungsmuster (Heatmap)</h5>
</div>
<div class="card-body">
<canvas id="usagePatternsChart" height="60"></canvas>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
// Configuration
const API_BASE_URL = '/api/v1/analytics';
const DAYS = {{ days }};
// Fetch data from Analytics Service
async function fetchAnalyticsData() {
try {
// Get JWT token first (from license server auth)
const tokenResponse = await fetch('/api/admin/license/auth-token');
const tokenData = await tokenResponse.json();
const token = tokenData.token;
const headers = {
'Authorization': `Bearer ${token}`
};
// Fetch all analytics data
const [usage, performance, distribution, revenue, patterns, churnRisk] = await Promise.all([
fetch(`${API_BASE_URL}/usage?days=${DAYS}`, { headers }).then(r => r.json()),
fetch(`${API_BASE_URL}/performance?days=${DAYS}`, { headers }).then(r => r.json()),
fetch(`${API_BASE_URL}/distribution`, { headers }).then(r => r.json()),
fetch(`${API_BASE_URL}/revenue?days=${DAYS}`, { headers }).then(r => r.json()),
fetch(`${API_BASE_URL}/patterns`, { headers }).then(r => r.json()),
fetch(`${API_BASE_URL}/churn-risk`, { headers }).then(r => r.json())
]);
// Update UI with fetched data
updateSummaryCards(usage.data, distribution.data, churnRisk.data);
createUsageTrendsChart(usage.data);
createPerformanceChart(performance.data);
createDistributionChart(distribution.data);
updateRevenueTable(revenue.data);
updateChurnRiskTable(churnRisk.data);
createUsagePatternsHeatmap(patterns.data);
} catch (error) {
console.error('Error fetching analytics data:', error);
// Fallback to database data if API is not available
useFallbackData();
}
}
// Update summary cards
function updateSummaryCards(usageData, distributionData, churnData) {
if (usageData && usageData.length > 0) {
const totalValidations = usageData.reduce((sum, day) => sum + day.total_heartbeats, 0);
const uniqueLicenses = new Set(usageData.flatMap(day => day.active_licenses)).size;
const uniqueDevices = new Set(usageData.flatMap(day => day.active_devices)).size;
document.getElementById('active-licenses').textContent = uniqueLicenses.toLocaleString();
document.getElementById('active-devices').textContent = uniqueDevices.toLocaleString();
document.getElementById('total-validations').textContent = totalValidations.toLocaleString();
}
if (churnData && churnData.length > 0) {
const highRiskCount = churnData.filter(c => c.churn_risk === 'high').length;
document.getElementById('churn-risk').textContent = highRiskCount;
}
}
// Create usage trends chart
function createUsageTrendsChart(data) {
const ctx = document.getElementById('usageTrendsChart').getContext('2d');
const chartData = {
labels: data.map(d => new Date(d.date).toLocaleDateString('de-DE')),
datasets: [
{
label: 'Aktive Lizenzen',
data: data.map(d => d.active_licenses),
borderColor: 'rgb(54, 162, 235)',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
tension: 0.1
},
{
label: 'Aktive Geräte',
data: data.map(d => d.active_devices),
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.1)',
tension: 0.1
}
]
};
new Chart(ctx, {
type: 'line',
data: chartData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}
// Create performance chart
function createPerformanceChart(data) {
const ctx = document.getElementById('performanceChart').getContext('2d');
const chartData = {
labels: data.map(d => new Date(d.hour).toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
hour: '2-digit'
})),
datasets: [{
label: 'Validierungen pro Stunde',
data: data.map(d => d.validation_count),
backgroundColor: 'rgba(75, 192, 192, 0.6)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1
}]
};
new Chart(ctx, {
type: 'bar',
data: chartData,
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
}
// Create distribution chart
function createDistributionChart(data) {
const ctx = document.getElementById('distributionChart').getContext('2d');
const chartData = {
labels: data.map(d => `${d.license_type} (${d.is_test ? 'Test' : 'Prod'})`),
datasets: [{
label: 'Lizenzverteilung',
data: data.map(d => d.active_count),
backgroundColor: [
'rgba(255, 99, 132, 0.6)',
'rgba(54, 162, 235, 0.6)',
'rgba(255, 206, 86, 0.6)',
'rgba(75, 192, 192, 0.6)',
'rgba(153, 102, 255, 0.6)'
],
borderWidth: 1
}]
};
new Chart(ctx, {
type: 'doughnut',
data: chartData,
options: {
responsive: true,
maintainAspectRatio: false
}
});
}
// Update revenue table
function updateRevenueTable(data) {
const tbody = document.querySelector('#revenue-table tbody');
tbody.innerHTML = '';
data.forEach(row => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${row.license_type}</td>
<td>${row.total_licenses}</td>
<td>${row.total_licenses > 0 ? Math.round((row.active_revenue / row.total_revenue) * 100) : 0}%</td>
<td>€${(row.total_revenue || 0).toFixed(2)}</td>
<td>€${(row.active_revenue || 0).toFixed(2)}</td>
<td>€${(row.inactive_revenue || 0).toFixed(2)}</td>
`;
tbody.appendChild(tr);
});
}
// Update churn risk table
function updateChurnRiskTable(data) {
const tbody = document.querySelector('#churn-risk-table tbody');
tbody.innerHTML = '';
data.filter(d => d.churn_risk !== 'low').slice(0, 10).forEach(row => {
const tr = document.createElement('tr');
const riskClass = row.churn_risk === 'high' ? 'danger' : 'warning';
tr.innerHTML = `
<td>${row.customer_id}</td>
<td>${row.total_licenses}</td>
<td>${row.avg_days_since_activity ? Math.round(row.avg_days_since_activity) + ' Tage' : '-'}</td>
<td><span class="badge bg-${riskClass}">${row.churn_risk}</span></td>
`;
tbody.appendChild(tr);
});
}
// Create usage patterns heatmap
function createUsagePatternsHeatmap(data) {
// Implementation for heatmap would go here
// For now, just log the data
console.log('Usage patterns data:', data);
}
// Fallback to use existing database data
function useFallbackData() {
// Use the data passed from the server template
const usageTrends = {{ usage_trends | tojson | safe }};
const licenseMetrics = {{ license_metrics | tojson | safe }};
const deviceDistribution = {{ device_distribution | tojson | safe }};
const revenueAnalysis = {{ revenue_analysis | tojson | safe }};
// Create charts with fallback data
if (usageTrends) {
const ctx = document.getElementById('usageTrendsChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: usageTrends.map(d => d[0]),
datasets: [{
label: 'Aktive Lizenzen',
data: usageTrends.map(d => d[1]),
borderColor: 'rgb(54, 162, 235)',
tension: 0.1
}]
}
});
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
fetchAnalyticsData();
});
</script>
{% endblock %}