445 Zeilen
15 KiB
HTML
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 %} |