diff --git a/allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html b/allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html
new file mode 100644
index 00000000..99ad0be0
--- /dev/null
+++ b/allianceauth/services/modules/mumble/templates/services/mumble/mumble_connection_history.html
@@ -0,0 +1,185 @@
+{% extends "allianceauth/base-bs5.html" %}
+{% load i18n %}
+{% load humanize %}
+
+{% block page_title %}
+ {% translate "Mumble" %}
+{% endblock page_title %}
+
+{% block header_nav_brand %}
+ {% trans "Mumble History" %} - {{ mumble_url }}
+{% endblock header_nav_brand %}
+{% block header_nav_collapse_left %}
+{% endblock header_nav_collapse_left %}
+
+{% block header_nav_collapse_right %}
+{% endblock header_nav_collapse_right %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+ {% translate "User" %} |
+ {% translate "Display_Name" %} |
+ {% translate "Release" %} |
+ {% translate "Version" %} |
+ {% translate "Last Connection" %} |
+ {% translate "Last Disconnection" %} |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% translate "Version" %} |
+ {% translate "Number" %} |
+
+
+
+
+
+
+
+
+{% endblock content %}
+{% block extra_javascript %}
+ {% include "bundles/datatables-js-bs5.html" %}
+ {% include "bundles/filterdropdown-js.html" %}
+ {% include "bundles/chart-js.html" %}
+ {% include "bundles/moment-js.html" with locale=True %}
+
+{% endblock extra_javascript %}
+
+{% block extra_css %}
+ {% include "bundles/datatables-css-bs5.html" %}
+{% endblock extra_css %}
+{% block extra_script %}
+$(document).ready(function () {
+ $("#table-mumble-connection-history").DataTable({
+ ajax: {
+ url: "{% url 'mumble:connection_history_data' %}",
+ dataSrc: "connection_history_data",
+ },
+ columns: [
+ { data: "user" },
+ { data: "display_name" },
+ { data: "release" },
+ { data: "version" },
+ { data: "last_connect" },
+ { data: "last_connect" },
+ ],
+ order: [[4, "desc"]],
+ processing: true,
+ stateSave: true,
+ stateDuration: 0,
+ filterDropDown: {
+ columns: [
+ {
+ idx: 2,
+ },
+ {
+ idx: 3,
+ },
+ ],
+ bootstrap: true,
+ bootstrap_version: 5,
+ },
+ });
+
+ $("#table-mumble-connection-stats").DataTable({
+ ajax: {
+ url: "{% url 'mumble:release_counts_data' %}",
+ dataSrc: "release_counts_data",
+ },
+ columns: [{ data: "release" }, { data: "user_count" }],
+ order: [[1, "desc"]],
+ processing: true,
+ stateSave: true,
+ stateDuration: 0,
+ });
+
+ // Initialize empty Pie chart
+ var ctx = document.getElementById("pieChart").getContext("2d");
+ var pieChart = new Chart(ctx, {
+ type: "pie",
+ data: {
+ labels: [], // Initially empty
+ datasets: [
+ {
+ label: "Server Connection Breakdown",
+ data: [], // Initially empty
+ backgroundColor: [
+ "rgba(255, 99, 132, 0.2)",
+ "rgba(54, 162, 235, 0.2)",
+ "rgba(255, 206, 86, 0.2)",
+ ],
+ borderColor: [
+ "rgba(255, 99, 132, 1)",
+ "rgba(54, 162, 235, 1)",
+ "rgba(255, 206, 86, 1)",
+ ],
+ borderWidth: 1,
+ },
+ ],
+ },
+ options: {
+ responsive: true,
+ plugins: {
+ legend: {
+ position: "top",
+ },
+ },
+ },
+ });
+
+ // AJAX call to dynamically update the chart
+ $.ajax({
+ url: "{% url 'mumble:release_pie_chart_data' %}", // Your Django view URL that returns chart data
+ method: "GET",
+ success: function (data) {
+ // Replace chart data with the data from the AJAX response
+ pieChart.data.labels = data.labels; // Set the new labels
+ pieChart.data.datasets[0].data = data.values; // Set the new values
+
+ // Update the chart to reflect the new data
+ pieChart.update();
+ },
+ error: function (xhr, status, error) {
+ console.error("Error fetching pie chart data:", status, error);
+ },
+ });
+});
+
+{% endblock extra_script %}
diff --git a/allianceauth/services/modules/mumble/templates/services/mumble/mumble_service_ctrl.html b/allianceauth/services/modules/mumble/templates/services/mumble/mumble_service_ctrl.html
index e140ee7c..cf122157 100644
--- a/allianceauth/services/modules/mumble/templates/services/mumble/mumble_service_ctrl.html
+++ b/allianceauth/services/modules/mumble/templates/services/mumble/mumble_service_ctrl.html
@@ -48,4 +48,9 @@
{% endif %}
{% endif %}
+ {% if request.user.is_superuser %}
+
+ History
+
+{% endif %}
{% endblock %}
diff --git a/allianceauth/services/modules/mumble/urls.py b/allianceauth/services/modules/mumble/urls.py
index ef7ff699..d3276e0b 100644
--- a/allianceauth/services/modules/mumble/urls.py
+++ b/allianceauth/services/modules/mumble/urls.py
@@ -10,6 +10,10 @@ module_urls = [
path('deactivate/', views.DeleteMumbleView.as_view(), name='deactivate'),
path('reset_password/', views.ResetPasswordMumbleView.as_view(), name='reset_password'),
path('set_password/', views.SetPasswordMumbleView.as_view(), name='set_password'),
+ path('connection_history/', views.connection_history, name="connection_history"),
+ path('ajax/connection_history_data', views.connection_history_data, name="connection_history_data"),
+ path('ajax/release_counts_data', views.release_counts_data, name="release_counts_data"),
+ path('ajax/release_pie_chart_data', views.release_pie_chart_data, name="release_pie_chart_data"),
]
urlpatterns = [
diff --git a/allianceauth/services/modules/mumble/views.py b/allianceauth/services/modules/mumble/views.py
index ba8904fd..7dd07c82 100644
--- a/allianceauth/services/modules/mumble/views.py
+++ b/allianceauth/services/modules/mumble/views.py
@@ -3,6 +3,11 @@ import logging
from allianceauth.services.forms import ServicePasswordModelForm
from allianceauth.services.abstract import BaseCreatePasswordServiceAccountView, BaseDeactivateServiceAccountView, \
BaseResetPasswordServiceAccountView, BaseSetPasswordServiceAccountView
+from django.conf import settings
+from django.contrib.auth.decorators import login_required, permission_required
+from django.db.models import Count
+from django.http import HttpResponse, JsonResponse
+from django.shortcuts import render
from .models import MumbleUser
@@ -35,3 +40,51 @@ class ResetPasswordMumbleView(MumbleViewMixin, BaseResetPasswordServiceAccountVi
class SetPasswordMumbleView(MumbleViewMixin, BaseSetPasswordServiceAccountView):
form_class = MumblePasswordForm
+
+
+@login_required
+@permission_required('mumble.view_connection_history')
+def connection_history(request) -> HttpResponse:
+
+ context = {
+ "mumble_url": settings.MUMBLE_URL,
+ }
+
+ return render(request, 'services/mumble/mumble_connection_history.html', context)
+
+
+@login_required
+@permission_required("mumble.view_connection_history")
+def connection_history_data(request) -> JsonResponse:
+ connection_history_data = MumbleUser.objects.all(
+ ).values(
+ 'user',
+ 'display_name',
+ 'release',
+ 'version',
+ 'last_connect',
+ 'last_disconnect',
+ )
+
+ return JsonResponse({"connection_history_data": list(connection_history_data)})
+
+
+@login_required
+@permission_required("mumble.view_connection_history")
+def release_counts_data(request) -> JsonResponse:
+ release_counts_data = MumbleUser.objects.values('release').annotate(user_count=Count('user_id')).order_by('release')
+
+ return JsonResponse({
+ "release_counts_data": list(release_counts_data),
+ })
+
+
+@login_required
+@permission_required("mumble.view_connection_history")
+def release_pie_chart_data(request) -> JsonResponse:
+ release_counts = MumbleUser.objects.values('release').annotate(user_count=Count('user_id')).order_by('release')
+
+ return JsonResponse({
+ "labels": list(release_counts.values_list("release", flat=True)),
+ "values": list(release_counts.values_list("user_count", flat=True)),
+ })