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 @@