- Admin panel shows spinner with "Deleting user {username}..." during deletion
- Refactored loading modal to accept dynamic message text
- New users get initial activity sample recorded automatically
- New users show 0% activity bar instead of '--' in Activity Monitor
- Activity tooltip shows "Not enough data (Nh of 24h collected)" until
sufficient samples collected (144 samples at 10-min intervals)
- Progress bar still renders to show emerging trend, tooltip clarifies
percentage not yet reliable
- Added activitymon_sample_interval to template_vars for frontend access
- Rewrote docs/activity-tracking-methodology.md as comprehensive
implementation specification covering data collection, scoring formulas,
UI components, API endpoints, and design rationale
Added JUPYTERHUB_ACTIVITYMON_TARGET_HOURS env var (default 8) to
normalize activity scores based on expected daily work hours.
- Raw score (% of sampled time active) normalized to target
- 8h/day worker with 33% raw score -> 100% normalized
- Progress bar capped at 5 segments (100%)
- Tooltip shows real % with "(>8h/day)" indicator if over 100%
Files: Dockerfile, settings_dictionary.yml, jupyterhub_config.py,
activity.html
Activity tracking now runs as a JupyterHub managed service that starts
on boot, independent of page views. Uses REST API to fetch user data.
Changes:
- Add activity_sampler.py standalone service script
- Configure as JupyterHub service with roles/scopes in config
- Add aiohttp dependency for async HTTP requests
- Remove lazy-start from ActivityDataHandler (now independent)
- Rename env var to JUPYTERHUB_ACTIVITYMON_SAMPLE_INTERVAL
- Update settings_dictionary.yml
Volume size refresher still lazy-starts on Activity page view.
post_init_hook is not a valid JupyterHub config option - sampler never
started. Changed to lazy initialization: ActivityDataHandler starts
the sampler when admin first visits the Activity page.
Add ActivitySampler singleton class that automatically samples activity
for ALL users (active, idle, offline) at configurable interval using
Tornado's PeriodicCallback for non-blocking execution.
- Uses JUPYTERHUB_ACTIVITYMON_ACTIVITY_UPDATE_INTERVAL (default 600s)
- Starts via post_init_hook after JupyterHub initialization
- Runs first sample immediately on startup
- Logs tick statistics after each sample cycle
- Fixes: missing parenthesis and stray code from previous edit
- Add ActivitySample SQLAlchemy model for database persistence
- Add ActivityMonitor singleton with scoring, reset, lifecycle methods
- Add JUPYTERHUB_ACTIVITYMON_INACTIVE_AFTER env var (default 60 min)
- Update defaults: SAMPLE_INTERVAL=600s, RETENTION_DAYS=7
- Fix score calculation to use measured samples only (not theoretical max)
- Add 3-state status: green (active), yellow (inactive), red (offline)
- Add recently_active field in API response
- Add Reset button with confirmation dialog
- Fix green color (explicit #28a745 instead of text-success)
- Add ThreadPoolExecutor for non-blocking Docker stats
- Remove old background sampler code (on-demand sampling now)
- Bump version to 3.7.0
Session Extension Feature:
- Add JUPYTERHUB_IDLE_CULLER_MAX_EXTENSION env var (default 24h)
- Add SessionInfoHandler and ExtendSessionHandler API endpoints
- Add Session Status card to home page with countdown timer
- Extension tracking in spawner state (resets on server restart)
- Color-coded warnings (yellow <1h, red <30min)
Environment Settings Harmonization:
- Add all ENV defaults to Dockerfile
- Add idle culler settings to compose.yml
- Standardize logo as JUPYTERHUB_LOGO_URI with file:// prefix
- Update NVIDIA_IMAGE to nvidia/cuda:13.0.2-base-ubuntu24.04
HomeHandler doesn't pass server_version to templates (only AdminHandler does).
Added jupyterhub.__version__ to c.JupyterHub.template_vars for global access.
- Move settings_dictionary.yml to services/jupyterhub/conf/
- Replace hardcoded word list with xkcdpass library
- Add JUPYTERHUB_AUTOGENERATED_PASSWORD_WORDS (default: 4)
- Add JUPYTERHUB_AUTOGENERATED_PASSWORD_DELIMITER (default: "-")
- Fix ENABLE_SIGNUP to JUPYTERHUB_SIGNUP_ENABLED in Dockerfile
- Add config/settings_dictionary.yml with category-based structure
- Categories as top-level keys (JupyterHub Core, Docker Spawner, etc.)
- Each setting has name, description, default, optional empty_display
- Update SettingsPageHandler to load from YAML file
- Add pyyaml to Dockerfile pip dependencies
- Refactor pip install to heredoc format with echo message
Add ability to send notifications to selected servers instead of all.
- New ActiveServersHandler at GET /api/notifications/active-servers
- BroadcastNotificationHandler accepts optional recipients array
- UI with "Send to all" checkbox and server selection list
- Select All/Deselect All buttons, dynamic button text
- Backward compatible - sends to all if recipients not specified
- Add JUPYTERHUB_LOGO_FILE config for custom logo (served at /hub/logo)
- Fix CustomAuthorizationAreaHandler with @needs_scope decorator
- Import orm inside get() method to fix NameError
- Simplify page.html logo block (always use base_url/logo)
- Add JUPYTERHUB_BASE_URL_PREFIX that handles root path case
- When BASE_URL is '/', '', or None, prefix becomes empty string
- Prevents //hub/home redirect issue when using root path routing
- docker-sock: mounts /var/run/docker.sock (container orchestration)
- docker-privileged: runs with --privileged flag (hardware access)
Updated pre_spawn_hook to check both groups and set spawner.volumes
or spawner.privileged accordingly. Documentation updated.
Fixed NameError when custom_handlers.py imports from jupyterhub_config.py
by protecting all c.* assignments with 'if c is not None:' checks.
Changes:
- Wrapped c = get_config() in try-except to handle module import context
- Defined DOCKER_SPAWNER_VOLUMES as module-level constant
- Modified get_user_volume_suffixes() to accept volumes dict parameter
- USER_VOLUME_SUFFIXES calculated from constant (importable without c defined)
- Protected all c.* configuration assignments with 'if c is not None:' guards
This allows custom_handlers.py to import USER_VOLUME_SUFFIXES without
triggering "NameError: name 'get_config' is not defined" when the config
file is imported as a module rather than loaded by JupyterHub.
Volume management now works correctly - configuration can be safely imported
by handlers to validate volume names against USER_VOLUME_SUFFIXES.
Updated volume management to dynamically read volume names from
DockerSpawner.volumes configuration instead of hardcoding them:
- Added get_user_volume_suffixes() function to extract volume suffixes from
config matching jupyterlab-{username}_<suffix> pattern
- Exposed USER_VOLUME_SUFFIXES via c.JupyterHub.template_vars for templates
- Updated home.html to use Jinja2 loop generating checkboxes dynamically
from user_volume_suffixes variable
- Modified ManageVolumesHandler to validate against configured volumes
instead of hardcoded {'home', 'workspace', 'cache'}
- Removed volume descriptions (e.g., "Contains: User home directory...") to
keep UI truly agnostic
Now works correctly if volumes are renamed, added, or removed in configuration
without requiring template or handler changes. Volume names displayed exactly
as defined in config (home, workspace, cache by default).
Implemented comprehensive notification broadcast functionality allowing
administrators to send notifications to all active user JupyterLab servers
simultaneously through a dedicated admin panel.
Core Features:
- Admin-only notification panel accessible at /hub/notifications
- Concurrent delivery to all active servers using asyncio with 5s timeout
- Temporary API token generation (5-minute expiry) for authentication
- Support for 6 notification types: default, info, success, warning, error, in-progress
- 140-character message limit with live character counter
- Auto-close toggle and dismiss button in notifications
- Dynamic endpoint URL construction using spawner.server.base_url
- Comprehensive error handling with user-friendly messages
- One-line logging per server with message preview and outcome
Technical Implementation:
- Created BroadcastNotificationHandler in custom_handlers.py
- Created NotificationsPageHandler for admin UI rendering
- Added notifications.html template with Bootstrap 5 form
- Registered handlers in jupyterhub_config.py extra_handlers
- Sends to /jupyterlab-notifications-extension/ingest endpoint
- Payload includes type, message, autoClose, and actions array
- Navigation link added to home.html for admin access
Integration:
- Requires jupyterlab_notifications_extension installed on JupyterLab servers
- Uses correct payload format (type field, not variant)
- Includes Dismiss action button for manual notification closure
Documentation:
- Updated .claude/CLAUDE.md with complete feature documentation
- Updated README.md Features section with notification broadcast details
- Updated .claude/JOURNAL.md with implementation summary
- Removed obsolete FEATURE_PLAN.md
Version: 3.2.0 (bumped from 3.1.2)
Implemented built-in protected group system enabling admins to grant trusted users read-write Docker socket access within their JupyterLab containers. Groups are managed through admin panel and cannot be permanently deleted.
- Add /srv/jupyterhub to sys.path before importing custom_handlers
- Separate shell scripts and Python files in Dockerfile COPY commands
- Resolves ModuleNotFoundError for custom_handlers module
- JupyterHub now starts successfully with custom API handlers
- Add custom API handlers for volume reset and server restart
- Create custom home.html template with self-service buttons and modals
- Register handlers in jupyterhub_config.py with @admin_or_self permissions
- Update Dockerfile to copy templates and handlers
- Add custom templates path to JupyterHub configuration
- Update .claude/CLAUDE.md with feature documentation
- Reset Home Volume: DELETE /hub/api/users/{username}/reset-home-volume
- Restart Server: POST /hub/api/users/{username}/restart-server
- Both features use Docker API directly via /var/run/docker.sock