From 4b2fc084bfa73ad61ba41385cd10db8ffe65ff46 Mon Sep 17 00:00:00 2001 From: stellarshenson Date: Tue, 20 Jan 2026 19:04:39 +0100 Subject: [PATCH] fix: use separate SQLite database for activity monitor - ActivityMonitor now uses /data/activity_samples.sqlite instead of JupyterHub's main database to avoid SQLite locking conflicts - Fixes "database is locked" errors that prevented login when both JupyterHub and ActivityMonitor wrote simultaneously - Added "Last Active" column to activity table showing relative time --- .claude/JOURNAL.md | 3 +++ project.env | 2 +- .../jupyterhub/conf/bin/custom_handlers.py | 9 +++++-- .../html_templates_enhanced/activity.html | 26 +++++++++++++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/.claude/JOURNAL.md b/.claude/JOURNAL.md index 99722e0..a7c71fc 100644 --- a/.claude/JOURNAL.md +++ b/.claude/JOURNAL.md @@ -189,3 +189,6 @@ This journal tracks substantive work on documents, diagrams, and documentation c 62. **Task - Activity Monitor UI polish and sample cleanup fix**: Improved activity bar design and fixed sample retention enforcement
**Result**: Fixed inconsistent timestamp text (initial "Last updated: --" vs loaded "Measured X ago" - removed initial text so only "Measured X ago" shows). Redesigned activity bar from separate 16x16 boxes to thin continuous bar (80px wide, 8px tall) with subtle 1px dividers between 5 segments - visually distinct from day-based layouts. Fixed critical sample cleanup bug - cleanup only ran when new sample was INSERT-ed, not when existing sample was UPDATE-d, causing old samples to persist indefinitely when refreshes kept updating the last sample. Moved cleanup to run on every record_sample() call regardless of update vs insert, ensuring samples older than RETENTION_DAYS are always pruned + +63. **Task - Fix SQLite database locking**: Resolved database lock errors preventing login
+ **Result**: ActivityMonitor was using JupyterHub's main SQLite database (`/data/jupyterhub.sqlite`) via separate SQLAlchemy connection, causing `sqlite3.OperationalError: database is locked` when both tried to write simultaneously (login + activity sampling). Fixed by moving ActivityMonitor to separate database file `/data/activity_samples.sqlite`, completely avoiding lock contention. Added "Last Active" column to activity table showing relative time since user's last activity (e.g., "5min", "2h 14min") diff --git a/project.env b/project.env index 9d4b3a6..0aa7ad6 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.3_cuda-13.0.2_jh-5.4.2" +VERSION="3.7.5_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/conf/bin/custom_handlers.py b/services/jupyterhub/conf/bin/custom_handlers.py index 6b47332..78e5481 100755 --- a/services/jupyterhub/conf/bin/custom_handlers.py +++ b/services/jupyterhub/conf/bin/custom_handlers.py @@ -132,11 +132,16 @@ class ActivityMonitor: return default def _get_db(self): - """Get or create database session""" + """Get or create database session. + + Uses a SEPARATE database file to avoid SQLite locking conflicts + with JupyterHub's main database. + """ if self._db_session is not None: return self._db_session - db_url = os.environ.get('JUPYTERHUB_DB_URL', 'sqlite:////data/jupyterhub.sqlite') + # Use separate database file to avoid locking conflicts with JupyterHub + db_url = 'sqlite:////data/activity_samples.sqlite' try: self._engine = create_engine(db_url) diff --git a/services/jupyterhub/html_templates_enhanced/activity.html b/services/jupyterhub/html_templates_enhanced/activity.html index 76c4794..92a8d96 100644 --- a/services/jupyterhub/html_templates_enhanced/activity.html +++ b/services/jupyterhub/html_templates_enhanced/activity.html @@ -46,6 +46,7 @@ CPU Memory Time Left + Last Active Activity (7 days) @@ -166,6 +167,7 @@ require(["jquery"], function($) { '' + formatCpu(user.cpu_percent) + '' + '' + formatMemory(user.memory_mb, user.memory_percent) + '' + '' + formatTimeRemaining(user.time_remaining_seconds) + '' + + '' + formatLastActive(user.last_activity) + '' + '' + renderActivityBar(user.activity_score) + '' + ''; tbody.append(row); @@ -229,6 +231,30 @@ require(["jquery"], function($) { } } + function formatLastActive(isoString) { + if (!isoString) { + return '--'; + } + var date = new Date(isoString); + 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); + var remainingMin = diffMin % 60; + + if (diffMin < 1) { + return 'now'; + } else if (diffMin < 60) { + return diffMin + 'min'; + } else if (diffHour < 24) { + return diffHour + 'h ' + remainingMin + 'min'; + } else { + var diffDay = Math.floor(diffHour / 24); + return diffDay + 'd ' + (diffHour % 24) + 'h'; + } + } + function renderActivityBar(score) { if (score === null || score === undefined) { return '--';