add new statistic page

This commit is contained in:
Adrian Zürcher
2026-02-08 08:39:17 +01:00
parent db6168b84c
commit edc2190581
7 changed files with 236 additions and 1 deletions

View File

@@ -4,6 +4,7 @@ import (
"backend/env" "backend/env"
"backend/models" "backend/models"
"backend/server" "backend/server"
"backend/stats"
"backend/utils" "backend/utils"
"flag" "flag"
"fmt" "fmt"
@@ -119,6 +120,10 @@ func main() {
})) }))
//set logger for AuthMiddleware //set logger for AuthMiddleware
s.Routes.Use(func(c *gin.Context) {
c.Set("logger", logger)
c.Next()
})
accessHandler.SetMiddlewareLogger(s.Routes) accessHandler.SetMiddlewareLogger(s.Routes)
api := s.Routes.Group("/api") api := s.Routes.Group("/api")
//set routes //set routes
@@ -136,6 +141,7 @@ func main() {
role.GET("/members", dbHandler.GetMember) role.GET("/members", dbHandler.GetMember)
auth.GET("/events", dbHandler.GetEvent) auth.GET("/events", dbHandler.GetEvent)
auth.GET("/groups", dbHandler.GetGroup) auth.GET("/groups", dbHandler.GetGroup)
auth.POST("/stats", stats.GetStats)
auth.GET("/users", accessHandler.GetUser) auth.GET("/users", accessHandler.GetUser)
auth.GET("/roles", accessHandler.GetRole) auth.GET("/roles", accessHandler.GetRole)

54
backend/stats/stats.go Normal file
View File

@@ -0,0 +1,54 @@
package stats
import (
"backend/utils"
"net/http"
"os"
"gitea.tecamino.com/paadi/tecamino-logger/logging"
"github.com/gin-gonic/gin"
)
type Stats struct {
Database string `json:"database,omitempty"`
DatabaseSize int64 `json:"databaseSize,omitempty"`
}
func GetStats(c *gin.Context) {
middlewareData, err := utils.GetMiddlewareData(c, "logger")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
}
logger, ok := middlewareData.(*logging.Logger)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "middleware logger for state not defined",
})
return
}
var stats Stats
err = c.BindJSON(&stats)
if err != nil {
logger.Error("GetStats", err)
c.JSON(http.StatusInternalServerError, nil)
return
}
f, err := os.Stat(stats.Database)
if err != nil {
logger.Error("GetStats", err)
c.JSON(http.StatusInternalServerError, nil)
return
}
var returnStats = Stats{
DatabaseSize: f.Size(),
}
c.JSON(http.StatusOK, gin.H{
"data": returnStats,
})
}

View File

@@ -0,0 +1,28 @@
package utils
import (
"fmt"
"log"
"net/http"
"github.com/gin-gonic/gin"
)
func SetMiddlewareData(r *gin.Engine, key string, data any) {
//set logger for middleware
r.Use(func(c *gin.Context) {
c.Set(key, data)
c.Next()
})
}
func GetMiddlewareData(c *gin.Context, key string) (any, error) {
// Retrieve logger from Gin context
data, ok := c.Get("logger")
if !ok {
log.Fatal("middleware logger not set — use SetMiddlewareLogger first")
c.AbortWithStatusJSON(http.StatusInternalServerError, http.StatusInternalServerError)
return nil, fmt.Errorf("middleware key '%s'not set ", key)
}
return data, nil
}

View File

@@ -63,6 +63,22 @@
> >
<q-item-section>{{ $t('groups') }}</q-item-section> <q-item-section>{{ $t('groups') }}</q-item-section>
</q-item> </q-item>
<q-item v-if="!autorized" to="/login" exact clickable v-ripple @click="closeDrawer">
<q-item-section>{{ $t('login') }}</q-item-section>
</q-item>
<!-- <q-item
v-if="autorized || user.isPermittedTo('members', 'read')"
to="/report"
exact
clickable
v-ripple
@click="closeDrawer"
>
<q-item-section> {{ $t('report') }}</q-item-section>
</q-item> -->
<q-item v-if="autorized" to="/stats" exact clickable v-ripple @click="closeDrawer">
<q-item-section> {{ $t('stats') }}</q-item-section>
</q-item>
</q-list> </q-list>
</q-drawer> </q-drawer>
<q-page-container> <q-page-container>

116
src/pages/StatsPage.vue Normal file
View File

@@ -0,0 +1,116 @@
<template>
<site-title :title="$t('stats')" />
<q-card class="q-ma-md">
<q-card-section>
<div class="row q-ma-md q-gutter-md">
<q-card class="row col-auto">
<div class="col">
<h5 class="text-primary q-ma-md">{{ i18n.global.t('database') }}</h5>
<q-card-section>
<div class="column q-gutter-y-sm">
<div class="row items-center">
<p class="text-bold" style="min-width: 180px">
{{ i18n.global.t('databaseSize') }}:
</p>
<p class="q-ml-md">{{ stats }}</p>
</div>
<div class="row items-center">
<p class="text-bold" style="min-width: 180px">
{{ i18n.global.t('numberOfMembers') }}:
</p>
<p class="q-ml-md">{{ amounts?.members }}</p>
</div>
<div class="row items-center">
<p class="text-bold" style="min-width: 180px">
{{ i18n.global.t('numberOfEvents') }}:
</p>
<p class="q-ml-md">{{ amounts?.events }}</p>
</div>
<div class="row items-center">
<p class="text-bold" style="min-width: 180px">
{{ i18n.global.t('numberOfResponsibles') }}:
</p>
<p class="q-ml-md">{{ amounts?.responsibles }}</p>
</div>
<div class="row items-center">
<p class="text-bold" style="min-width: 180px">
{{ i18n.global.t('numberOfGroups') }}:
</p>
<p class="q-ml-md">{{ amounts?.groups }}</p>
</div>
</div>
</q-card-section>
</div>
</q-card>
</div>
</q-card-section>
</q-card>
</template>
<script setup lang="ts">
import { appApi } from 'src/boot/axios';
import { i18n } from 'src/boot/lang';
import SiteTitle from 'src/vueLib/general/SiteTitle.vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import { databaseName } from 'src/vueLib/models/settings';
import { onMounted, ref } from 'vue';
const stats = ref();
const amounts = ref<{
members: number;
events: number;
responsibles: number;
groups: number;
}>({
members: 0,
events: 0,
responsibles: 0,
groups: 0,
});
const { NotifyResponse } = useNotify();
onMounted(async () => {
stats.value = await appApi
.post('/stats', { database: databaseName.value })
.then((resp) => {
if ((resp.data.databaseSize as number) >= 1000000000) {
return (resp.data.data.databaseSize / 1000000000).toFixed(2) + ' GB';
} else if ((resp.data.data.databaseSize as number) >= 1000000) {
return (resp.data.data.databaseSize / 1000000).toFixed(2) + ' MB';
} else if ((resp.data.data.databaseSize as number) >= 1000) {
return (resp.data.data.databaseSize / 1000).toFixed(2) + ' kB';
}
return resp.data.data.databaseSize + ' B';
})
.catch((err) => NotifyResponse(err, 'error'));
amounts.value.members = await appApi
.get('/members')
.then((resp) => {
return resp.data.length;
})
.catch((err) => NotifyResponse(err, 'error'));
amounts.value.events = await appApi
.get('/events')
.then((resp) => {
return resp.data.length;
})
.catch((err) => NotifyResponse(err, 'error'));
amounts.value.responsibles = await appApi
.get('responsible')
.then((resp) => {
return resp.data.length;
})
.catch((err) => NotifyResponse(err, 'error'));
amounts.value.groups = await appApi
.get('/groups')
.then((resp) => {
return resp.data.length;
})
.catch((err) => NotifyResponse(err, 'error'));
});
</script>

View File

@@ -40,6 +40,7 @@ export default defineRouter(function (/* { store, ssrContext } */) {
Router.beforeEach((to, from, next) => { Router.beforeEach((to, from, next) => {
const userStore = useUserStore(); const userStore = useUserStore();
const isLoggedIn = userStore.isAuthenticated; const isLoggedIn = userStore.isAuthenticated;
if (!userStore.$state.firstLogin && to.path === '/firstlogin') { if (!userStore.$state.firstLogin && to.path === '/firstlogin') {
next('/login'); next('/login');
@@ -51,7 +52,11 @@ export default defineRouter(function (/* { store, ssrContext } */) {
to.meta.requiresAdmin && to.meta.requiresAdmin &&
!userStore.isPermittedTo(to.path.replace('/', ''), 'read') !userStore.isPermittedTo(to.path.replace('/', ''), 'read')
) { ) {
next('/'); if (to.meta.noBackendAdmin) {
next();
} else {
next('/');
}
} else { } else {
next(); next();
} }

View File

@@ -37,6 +37,16 @@ const routes: RouteRecordRaw[] = [
component: () => import('src/pages/GroupTable.vue'), component: () => import('src/pages/GroupTable.vue'),
meta: { requiresAuth: true, requiresAdmin: true }, meta: { requiresAuth: true, requiresAdmin: true },
}, },
{
path: 'report',
component: () => import('src/pages/ReportPage.vue'),
meta: { requiresAuth: true, requiresAdmin: false },
},
{
path: 'stats',
component: () => import('src/pages/StatsPage.vue'),
meta: { requiresAuth: true, requiresAdmin: true, noBackendAdmin: true },
},
{ {
path: 'settings', path: 'settings',
component: () => import('pages/SettingsPage.vue'), component: () => import('pages/SettingsPage.vue'),