diff --git a/project.env b/project.env
index ea8f688..cfca250 100644
--- a/project.env
+++ b/project.env
@@ -3,7 +3,7 @@ PROJECT_NAME="stellars-jupyterhub-ds"
PROJECT_DESCRIPTION="Multi-user JupyterHub 4 deployment platform with data science stack, GPU auto-detection, NativeAuthenticator, and isolated per-user environments spawned via DockerSpawner"
# Version
-VERSION="3.7.0_cuda-13.0.2_jh-5.4.2"
+VERSION="3.7.1_cuda-13.0.2_jh-5.4.2"
VERSION_COMMENT="Activity Monitor: admin page with 3-state status, activity scoring, reset functionality"
RELEASE_TAG="RELEASE_3.2.11"
RELEASE_DATE="2025-11-09"
diff --git a/services/jupyterhub/html_templates_enhanced/activity.html b/services/jupyterhub/html_templates_enhanced/activity.html
index fb96488..c02215d 100644
--- a/services/jupyterhub/html_templates_enhanced/activity.html
+++ b/services/jupyterhub/html_templates_enhanced/activity.html
@@ -71,7 +71,11 @@ require(["jquery"], function($) {
// Manual refresh button
$('#refresh-btn').on('click', function() {
- fetchActivityData();
+ var btn = $(this);
+ btn.prop('disabled', true).html('');
+ fetchActivityData(function() {
+ btn.prop('disabled', false).html(' Refresh');
+ });
});
// Reset button
@@ -98,7 +102,7 @@ require(["jquery"], function($) {
});
});
- function fetchActivityData() {
+ function fetchActivityData(callback) {
$.ajax({
url: '{{ base_url }}api/activity',
method: 'GET',
@@ -107,6 +111,7 @@ require(["jquery"], function($) {
},
success: function(data) {
renderActivityTable(data);
+ if (callback) callback();
},
error: function(xhr) {
$('#loading-indicator').hide();
@@ -115,6 +120,7 @@ require(["jquery"], function($) {
.removeClass('alert-info')
.addClass('alert-danger')
.html(' Failed to load activity data');
+ if (callback) callback();
}
});
}
@@ -137,7 +143,7 @@ require(["jquery"], function($) {
// Update header info
$('#active-count').text(users.length + ' active server' + (users.length !== 1 ? 's' : ''));
- $('#last-updated').text('Measured: ' + formatTimestamp(timestamp));
+ $('#last-updated').text('Measured ' + formatTimeAgo(timestamp));
// Show sampling status if available
if (samplingStatus) {
@@ -255,9 +261,24 @@ require(["jquery"], function($) {
return html;
}
- function formatTimestamp(isoString) {
+ function formatTimeAgo(isoString) {
var date = new Date(isoString);
- return date.toLocaleTimeString();
+ var now = new Date();
+ var diffMs = now - date;
+ var diffSec = Math.floor(diffMs / 1000);
+ var diffMin = Math.floor(diffSec / 60);
+ var diffHour = Math.floor(diffMin / 60);
+
+ if (diffMin < 1) {
+ return 'just now';
+ } else if (diffMin < 60) {
+ return diffMin + 'min ago';
+ } else if (diffHour < 24) {
+ return diffHour + 'h ago';
+ } else {
+ var diffDay = Math.floor(diffHour / 24);
+ return diffDay + 'd ago';
+ }
}
function getCookie(name) {