- Volume management button now only appears for users with stopped servers
- Button dynamically removed when server starts running
- Button positioned as last element in action cell (after Edit User)
- MutationObserver handles React re-renders
- Add feature bullet for admin volume management button
- Add note about admin capability in User Self-Service Workflow section
- Admins can manage any user's volumes via database icon in admin panel
- Document final implementation using tr.user-row selector
- Username extracted from span[data-testid="user-name-div-{username}"]
- Button matches Edit User button styling (btn btn-light btn-xs)
- Positioned before Edit User button in actions cell
- Find user rows via tr.user-row selector
- Get username from span[data-testid="user-name-div-{username}"]
- Find Edit User button by text content (no title attribute)
- Insert volume button before Edit User button
- Copies exact classes from Edit User button (btn btn-light btn-xs)
- Find Edit buttons directly instead of looking for td.actions
- Inject button before each Edit User button found
- Works with JupyterHub React admin panel structure
- Walks up DOM to find user container and extract username
- Find username via multiple methods (admin link, server link, text)
- Copy exact class from Edit button for consistent styling
- Insert button before Edit button (between server actions and edit)
- Works for all users, not just those with running servers
- 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
- Increased .col-auth width from 4em to 5em to prevent truncation
- Changed Volumes tooltip to generic "hover for breakdown" since
volume names are autodiscovered
- Added explanatory tooltips to all 9 column headers in Activity Monitor
- Added sortable Auth column showing NativeAuthenticator authorization status
- Green checkmark for authorized users, red X for not authorized
- Backend queries users_info table with graceful fallback
- Updated documentation with new column and API schema
- 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
Rewrote simulation section explaining why effective half-life differs
from configured half-life:
1. Decay is CONTINUOUS (24/7 in calendar time)
2. Work is SPARSE (only during work hours)
3. Decay during BREAKS (overnight with no new work)
Added single table showing effective half-life for work patterns
(12h, 10h, 8h, 6h, 4h, 2h) vs configured half-lives (24h, 48h, 72h).
Key insight: 72h configured = 18h effective for 8h/day worker.
Added detailed simulation results showing how calendar half-life
translates to effective working-time decay:
- 10h/day (intensive): 72h -> 28.5 work hours at 50%
- 8h/day (typical): 72h -> 22.8 work hours at 50%
- 4h/day (part-time): 72h -> 11.5 work hours at 50%
Key finding: 72h calendar half-life consistently yields ~2.9 work
days at the 50% point, regardless of daily work hours. Activity
scores correctly reflect work fraction (8h/24h = 33.3%).
Explains why 72h calendar half-life was chosen:
- Decay applies to wall-clock time, not working time
- Users work ~8h/day (1/3 of 24h period)
- 72h calendar = ~24h of working time = one full workday
- Prevents overnight breaks from penalizing scores
This calibrates decay to actual engagement patterns rather
than raw calendar time.
Changed JUPYTERHUB_ACTIVITYMON_HALF_LIFE default from 48h to 72h
for more stable activity scores. Activity from 3 days ago now has
50% weight, better suited for users with irregular schedules.
Updated: Dockerfile, custom_handlers.py, activity_sampler.py,
settings_dictionary.yml, README.md, docs/activity-tracking-methodology.md
Changed JUPYTERHUB_ACTIVITYMON_HALF_LIFE default from 24h to 48h
for smoother decay in activity scoring. A sample from 48 hours ago
now has 50% weight (was 50% at 24h).
Research document covering industry approaches for activity tracking:
- Exponential Moving Average (EMA) with half-life decay
- Hubstaff's hour-based approach
- Daily target method (8h=100%)
- GitHub contribution graph methodology
Validates current EMA implementation aligns with industry standards.
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.
- VolumeSizeRefresher class using Tornado PeriodicCallback
- Refreshes volume sizes every hour (JUPYTERHUB_ACTIVITYMON_VOLUMES_UPDATE_INTERVAL)
- Lazy start on first Activity page access (like ActivitySampler)
- Runs first refresh immediately, then at configured interval
- Add BUILD_OPTS variable with --no-version-increment support
- Add maybe_increment_version conditional target
- Filter out custom opts before passing to docker
- Usage: make build BUILD_OPTS='--no-version-increment'
- Update journal with activity sampler fixes and color changes
The sampler was expecting an app object with db and users attributes,
but handler settings don't provide that. Changed to pass handler's
self.db and self.find_user directly to the sampler.
Also added flush=True to print statements for immediate log output.
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
Threshold-based coloring (green/yellow/red) doesn't work for
multi-core systems where CPU usage can exceed 100% (e.g., 6400%
on 64-core machines). Now displays plain text values.
- Activity Monitor now shows all users with historical activity, not just active servers
- Badge changed from "X active servers" to "X users (Y active)" format
- Status column now sortable (green > amber > red priority)
- Default sort changed to status descending with secondary sort by username
- Moved inline table styles to CSS classes using em units
- Added .activity-table, .settings-table, .notifications-table classes
- Simplified Activity page subtitle
- Add sampling interval config (default 600s / 10min)
- Separate from RESOURCES_UPDATE_INTERVAL (10s for UI refresh)
- Sampling interval controls how often activity samples are recorded
- Added to Dockerfile, settings_dictionary.yml, and ActivityMonitor class
- Remove "Measured X ago" timer display (adds no value)
- Change auto-refresh interval from 30s to 10s for real-time monitoring
- Add JUPYTERHUB_ACTIVITYMON_RESOURCES_UPDATE_INTERVAL env var (default 10s)
- Resource refresh updates status, CPU, memory, timers only
- Activity sampling is separate (controlled by background process)
- Remove formatTimeAgo function and related timestamp tracking
- Add clickable column sorting for User, CPU, Memory, Time Left, Last Active
- Sorting cycles: descending -> ascending -> none (default)
- Sort icons show current direction
- Last Active now shows full words ("8 minutes ago", "4 months ago")
- Proper singular/plural handling for time units