new: [app:user_activity] Added user activity chart
This commit is contained in:
parent
bc78e2f2cb
commit
58da718c5d
7 changed files with 223 additions and 14 deletions
30
db.py
30
db.py
|
@ -19,9 +19,16 @@ NOTIFICATION_MESSAGES = collections.deque([], NOTIFICATION_BUFFER_SIZE)
|
|||
NOTIFICATION_HISTORY_BUFFER_RESOLUTION_PER_MIN = 12
|
||||
NOTIFICATION_HISTORY_BUFFER_TIMESPAN_MIN = 20
|
||||
NOTIFICATION_HISTORY_FREQUENCY = 60 / NOTIFICATION_HISTORY_BUFFER_RESOLUTION_PER_MIN
|
||||
notification_history_size = NOTIFICATION_HISTORY_BUFFER_RESOLUTION_PER_MIN * NOTIFICATION_HISTORY_BUFFER_TIMESPAN_MIN
|
||||
NOTIFICATION_HISTORY = collections.deque([], notification_history_size)
|
||||
NOTIFICATION_HISTORY.extend([0] * notification_history_size)
|
||||
notification_history_buffer_size = NOTIFICATION_HISTORY_BUFFER_RESOLUTION_PER_MIN * NOTIFICATION_HISTORY_BUFFER_TIMESPAN_MIN
|
||||
NOTIFICATION_HISTORY = collections.deque([], notification_history_buffer_size)
|
||||
NOTIFICATION_HISTORY.extend([0] * notification_history_buffer_size)
|
||||
|
||||
USER_ACTIVITY_BUFFER_RESOLUTION_PER_MIN = 2
|
||||
USER_ACTIVITY_TIMESPAN_MIN = 20
|
||||
USER_ACTIVITY_FREQUENCY = 60 / USER_ACTIVITY_BUFFER_RESOLUTION_PER_MIN
|
||||
USER_ACTIVITY = {}
|
||||
user_activity_buffer_size = USER_ACTIVITY_BUFFER_RESOLUTION_PER_MIN * USER_ACTIVITY_TIMESPAN_MIN
|
||||
|
||||
|
||||
def resetNotificationMessage():
|
||||
global NOTIFICATION_MESSAGES
|
||||
|
@ -29,5 +36,18 @@ def resetNotificationMessage():
|
|||
|
||||
def resetNotificationHistory():
|
||||
global NOTIFICATION_HISTORY
|
||||
NOTIFICATION_HISTORY = collections.deque([], notification_history_size)
|
||||
NOTIFICATION_HISTORY.extend([0] * notification_history_size)
|
||||
NOTIFICATION_HISTORY = collections.deque([], notification_history_buffer_size)
|
||||
NOTIFICATION_HISTORY.extend([0] * notification_history_buffer_size)
|
||||
|
||||
def addUserActivity(user_id: int, count: int):
|
||||
global USER_ACTIVITY, USER_ACTIVITY_TIMESPAN_MIN
|
||||
|
||||
if user_id not in USER_ACTIVITY:
|
||||
USER_ACTIVITY[user_id] = collections.deque([], user_activity_buffer_size)
|
||||
USER_ACTIVITY[user_id].extend([0] * user_activity_buffer_size)
|
||||
USER_ACTIVITY[user_id].append(count)
|
||||
|
||||
def resetUserActivity():
|
||||
for user_id in USER_ACTIVITY.keys():
|
||||
USER_ACTIVITY[user_id] = collections.deque([], user_activity_buffer_size)
|
||||
USER_ACTIVITY[user_id].extend([0] * user_activity_buffer_size)
|
|
@ -27,14 +27,26 @@ def get_notifications() -> list[dict]:
|
|||
return list(db.NOTIFICATION_MESSAGES)
|
||||
|
||||
|
||||
def get_notifications_history() -> list[dict]:
|
||||
def get_notifications_history() -> dict:
|
||||
return {
|
||||
'history': list(db.NOTIFICATION_HISTORY),
|
||||
'config': {
|
||||
'buffer_resolution_per_minute': db.NOTIFICATION_HISTORY_BUFFER_RESOLUTION_PER_MIN,
|
||||
'buffer_timestamp_min': db.NOTIFICATION_HISTORY_BUFFER_TIMESPAN_MIN,
|
||||
'frequency': db.NOTIFICATION_HISTORY_FREQUENCY,
|
||||
'notification_history_size': db.notification_history_size,
|
||||
'notification_history_size': db.notification_history_buffer_size,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_users_activity() -> dict:
|
||||
return {
|
||||
'activity': {user_id: list(activity) for user_id, activity in db.USER_ACTIVITY.items()},
|
||||
'config': {
|
||||
'timestamp_min': db.USER_ACTIVITY_TIMESPAN_MIN,
|
||||
'buffer_resolution_per_minute': db.USER_ACTIVITY_BUFFER_RESOLUTION_PER_MIN,
|
||||
'frequency': db.USER_ACTIVITY_FREQUENCY,
|
||||
'activity_buffer_size': db.user_activity_buffer_size,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -51,6 +63,10 @@ def record_notification_history(message_count: int):
|
|||
db.NOTIFICATION_HISTORY.append(message_count)
|
||||
|
||||
|
||||
def record_user_activity(user_id: int, count: int):
|
||||
db.addUserActivity(user_id, count)
|
||||
|
||||
|
||||
def get_user_id(data: dict):
|
||||
if 'user_id' in data:
|
||||
return int(data['user_id'])
|
||||
|
|
22
server.py
22
server.py
|
@ -1,5 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import collections
|
||||
import functools
|
||||
import json
|
||||
import sys
|
||||
|
@ -20,6 +21,7 @@ import misp_api
|
|||
ZMQ_MESSAGE_COUNT_LAST_TIMESPAN = 0
|
||||
ZMQ_MESSAGE_COUNT = 0
|
||||
ZMQ_LAST_TIME = None
|
||||
USER_ACTIVITY = collections.defaultdict(int)
|
||||
|
||||
|
||||
def debounce(debounce_seconds: int = 1):
|
||||
|
@ -109,6 +111,10 @@ async def reset_notifications(sid):
|
|||
async def get_diagnostic(sid):
|
||||
return await getDiagnostic()
|
||||
|
||||
@sio.event
|
||||
async def get_users_activity(sid):
|
||||
return notification_model.get_users_activity()
|
||||
|
||||
@sio.event
|
||||
async def toggle_verbose_mode(sid, payload):
|
||||
return notification_model.set_verbose_mode(payload['verbose'])
|
||||
|
@ -144,6 +150,9 @@ async def handleMessage(topic, s, message):
|
|||
if notification_model.is_accepted_notification(notification):
|
||||
notification_model.record_notification(notification)
|
||||
ZMQ_MESSAGE_COUNT_LAST_TIMESPAN += 1
|
||||
user_id = notification_model.get_user_id(data)
|
||||
if user_id is not None:
|
||||
USER_ACTIVITY[user_id] += 1
|
||||
await sio.emit('notification', notification)
|
||||
|
||||
user_id = notification_model.get_user_id(data)
|
||||
|
@ -201,6 +210,18 @@ async def notification_history():
|
|||
await sio.emit('update_notification_history', payload)
|
||||
|
||||
|
||||
async def record_users_activity():
|
||||
global USER_ACTIVITY
|
||||
|
||||
while True:
|
||||
await sio.sleep(db.USER_ACTIVITY_FREQUENCY)
|
||||
for user_id, activity in USER_ACTIVITY.items():
|
||||
notification_model.record_user_activity(user_id, activity)
|
||||
USER_ACTIVITY[user_id] = 0
|
||||
payload = notification_model.get_users_activity()
|
||||
await sio.emit('update_users_activity', payload)
|
||||
|
||||
|
||||
async def keepalive():
|
||||
global ZMQ_LAST_TIME
|
||||
while True:
|
||||
|
@ -237,6 +258,7 @@ async def init_app():
|
|||
sio.start_background_task(forward_zmq_to_socketio)
|
||||
sio.start_background_task(keepalive)
|
||||
sio.start_background_task(notification_history)
|
||||
sio.start_background_task(record_users_activity)
|
||||
sio.start_background_task(backup_exercises_progress)
|
||||
return app
|
||||
|
||||
|
|
129
src/components/LiveLogsUserActivityGraph.vue
Normal file
129
src/components/LiveLogsUserActivityGraph.vue
Normal file
|
@ -0,0 +1,129 @@
|
|||
<script setup>
|
||||
import { ref, watch, computed } from "vue"
|
||||
import { userActivity, userActivityConfig } from "@/socket";
|
||||
import { darkModeEnabled } from "@/settings.js"
|
||||
|
||||
const props = defineProps(['user_id'])
|
||||
|
||||
const theChart = ref(null)
|
||||
const bufferSize = computed(() => userActivityConfig.value.activity_buffer_size)
|
||||
const bufferSizeMin = computed(() => userActivityConfig.value.timestamp_min)
|
||||
const chartInitSeries = Array.from(Array(bufferSize.value)).map(() => 0)
|
||||
|
||||
const hasActivity = computed(() => userActivity.value.length != 0)
|
||||
const chartSeries = computed(() => {
|
||||
return !hasActivity.value ? chartInitSeries : activitySeries.value
|
||||
})
|
||||
|
||||
const activitySeries = computed(() => {
|
||||
const data = userActivity.value[props.user_id] === undefined ? chartInitSeries : userActivity.value[props.user_id]
|
||||
return [{data: Array.from(data)}]
|
||||
})
|
||||
const colorRanges = [1, 3, 5, 7, 9, 1000]
|
||||
|
||||
const chartOptions = computed(() => {
|
||||
return {
|
||||
chart: {
|
||||
height: 20,
|
||||
width: 208,
|
||||
type: 'heatmap',
|
||||
sparkline: {
|
||||
enabled: true
|
||||
},
|
||||
animations: {
|
||||
enabled: false,
|
||||
easing: 'easeinout',
|
||||
speed: 200,
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
style: {
|
||||
fontSize: '10px',
|
||||
fontWeight: '400',
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
heatmap: {
|
||||
radius: 2,
|
||||
enableShades: !true,
|
||||
shadeIntensity: 0.5,
|
||||
reverseNegativeShade: true,
|
||||
distributed: false,
|
||||
useFillColorAsStroke: false,
|
||||
colorScale: {
|
||||
ranges: [
|
||||
{
|
||||
from: 0,
|
||||
to: colorRanges[0],
|
||||
color: darkModeEnabled.value ? '#172554' : '#bfdbfe',
|
||||
},
|
||||
{
|
||||
from: colorRanges[0] + 1,
|
||||
to: colorRanges[1],
|
||||
color: darkModeEnabled.value ? '#1e40af' : '#93c5fd',
|
||||
},
|
||||
{
|
||||
from: colorRanges[1] + 1,
|
||||
to: colorRanges[2],
|
||||
color: darkModeEnabled.value ? '#2563eb' : '#60a5fa',
|
||||
},
|
||||
{
|
||||
from: colorRanges[2] + 1,
|
||||
to: colorRanges[3],
|
||||
color: darkModeEnabled.value ? '#3b82f6' : '#3b82f6',
|
||||
},
|
||||
{
|
||||
from: colorRanges[3] + 1,
|
||||
to: colorRanges[4],
|
||||
color: darkModeEnabled.value ? '#60a5fa' : '#2563eb',
|
||||
},
|
||||
{
|
||||
from: colorRanges[4] + 1,
|
||||
to: colorRanges[5],
|
||||
color: darkModeEnabled.value ? '#93c5fd' : '#1d4ed8',
|
||||
},
|
||||
],
|
||||
// inverse: false,
|
||||
min: 0,
|
||||
max: 1000
|
||||
},
|
||||
},
|
||||
},
|
||||
states: {
|
||||
hover: {
|
||||
filter: {
|
||||
type: 'none',
|
||||
}
|
||||
},
|
||||
active: {
|
||||
filter: {
|
||||
type: 'none',
|
||||
}
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
show: false,
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
},
|
||||
stroke: {
|
||||
width: 0,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="h-3 w-52"
|
||||
:title="`Activity over ${bufferSizeMin}min`"
|
||||
>
|
||||
<apexchart type="heatmap" height="12" width="208" :options="chartOptions" :series="chartSeries"></apexchart>
|
||||
</span>
|
||||
</template>
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
const theChart = ref(null)
|
||||
const chartInitSeries = [
|
||||
// {data: Array.apply(null, {length: 240}).map(Function.call, Math.random)}
|
||||
{data: Array.from(Array(12*20)).map(()=> 0)}
|
||||
]
|
||||
const hasActivity = computed(() => notificationHistory.value.length > 0)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { active_exercises as exercises, progresses, setCompletedState } from "@/socket";
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { faCheck, faTimes, faGraduationCap, faMedal, faHourglassHalf } from '@fortawesome/free-solid-svg-icons'
|
||||
import LiveLogsUserActivityGraph from "./LiveLogsUserActivityGraph.vue"
|
||||
|
||||
const collapsed_panels = ref([])
|
||||
|
||||
|
@ -85,12 +86,15 @@
|
|||
</tr>
|
||||
<template v-else>
|
||||
<tr v-for="(progress, user_id) in progresses" :key="user_id" class="bg-slate-100 dark:bg-slate-900">
|
||||
<td class="border-b border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-3 pl-6">
|
||||
<span :title="user_id" class="text-nowrap">
|
||||
<td class="border-b border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-0 pl-2 relative">
|
||||
<span class="flex flex-col">
|
||||
<span :title="user_id" class="text-nowrap inline-block leading-5">
|
||||
<FontAwesomeIcon v-if="progress.exercises[exercise.uuid].score / progress.exercises[exercise.uuid].max_score == 1" :icon="faMedal" class="mr-1 text-amber-300"></FontAwesomeIcon>
|
||||
<span class="text-lg font-bold font-mono">{{ progress.email.split('@')[0] }}</span>
|
||||
<span class="text-lg font-bold font-mono leading-5">{{ progress.email.split('@')[0] }}</span>
|
||||
<span class="text-xs font-mono">@{{ progress.email.split('@')[1] }}</span>
|
||||
</span>
|
||||
<LiveLogsUserActivityGraph :user_id="user_id"></LiveLogsUserActivityGraph>
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
v-for="(task, task_index) in exercise.tasks"
|
||||
|
|
|
@ -3,7 +3,7 @@ import { io } from "socket.io-client";
|
|||
import debounce from 'lodash.debounce'
|
||||
|
||||
// "undefined" means the URL will be computed from the `window.location` object
|
||||
const URL = process.env.NODE_ENV === "production" ? undefined : "http://localhost:4000";
|
||||
const URL = process.env.NODE_ENV === "production" ? undefined : "http://localhost:40001";
|
||||
const MAX_LIVE_LOG = 30
|
||||
|
||||
const initial_state = {
|
||||
|
@ -12,6 +12,8 @@ const initial_state = {
|
|||
notificationAPICounter: 0,
|
||||
notificationHistory: [],
|
||||
notificationHistoryConfig: {},
|
||||
userActivity: {},
|
||||
userActivityConfig: {},
|
||||
exercises: [],
|
||||
selected_exercises: [],
|
||||
progresses: {},
|
||||
|
@ -44,6 +46,8 @@ export const userCount = computed(() => Object.keys(state.progresses).length)
|
|||
export const diagnostic = computed(() => state.diagnostic)
|
||||
export const notificationHistory = computed(() => state.notificationHistory)
|
||||
export const notificationHistoryConfig = computed(() => state.notificationHistoryConfig)
|
||||
export const userActivity = computed(() => state.userActivity)
|
||||
export const userActivityConfig = computed(() => state.userActivityConfig)
|
||||
export const socketConnected = computed(() => connectionState.connected)
|
||||
export const zmqLastTime = computed(() => connectionState.zmq_last_time)
|
||||
|
||||
|
@ -56,6 +60,7 @@ export function fullReload() {
|
|||
getSelectedExercises()
|
||||
getNotifications()
|
||||
getProgress()
|
||||
getUsersActivity()
|
||||
}
|
||||
|
||||
export function setCompletedState(completed, user_id, exec_uuid, task_uuid) {
|
||||
|
@ -122,6 +127,15 @@ function getProgress() {
|
|||
})
|
||||
}
|
||||
|
||||
function getUsersActivity() {
|
||||
socket.emit("get_users_activity", (user_activity_bundle) => {
|
||||
console.log(user_activity_bundle);
|
||||
state.userActivity = user_activity_bundle.activity
|
||||
state.userActivityConfig = user_activity_bundle.config
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function getDiangostic() {
|
||||
state.diagnostic = {}
|
||||
socket.emit("get_diagnostic", (diagnostic) => {
|
||||
|
@ -203,6 +217,11 @@ socket.on("update_notification_history", (notification_history_bundle) => {
|
|||
state.notificationHistoryConfig = notification_history_bundle.config
|
||||
});
|
||||
|
||||
socket.on("update_users_activity", (user_activity_bundle) => {
|
||||
state.userActivity = user_activity_bundle.activity
|
||||
state.userActivityConfig = user_activity_bundle.config
|
||||
});
|
||||
|
||||
function addLimited(target, message, maxCount) {
|
||||
target.unshift(message)
|
||||
if (target.length > maxCount) {
|
||||
|
|
Loading…
Reference in a new issue