Commit Graph

132 Commits

Author SHA1 Message Date
stellarshenson
caa8fd765e style: progress bar tooltip, fix dark mode border color
- add Bootstrap tooltip on progress track explaining idle timer
- fix dark mode border to explicit #6f757c (was rgba computing to #525457)
2026-02-09 23:42:21 +01:00
stellarshenson
d9a7a05185 style: session timer pale colors, lighter border, narrower bar
- pale muted colors: #446897 (blue), #bfa348 (yellow), #af4e56 (red)
- CSS custom properties now use hex format
- JS parseColor() handles both hex and RGB triplet formats
- progress track border changed to #6f757c
- narrowed bar from col-md-6 to col-md-4
2026-02-09 23:33:56 +01:00
stellarshenson
7ebd60aa88 feat: session timer progress bar with smooth color interpolation
Replace the large session status card on the home page with a compact
inline progress bar. Extract all session timer JS into standalone
session-timer.js module loaded via page.html.

- progress bar with bordered track showing remaining time as percentage
- smooth RGB color interpolation: blue (100%) -> yellow (30%) -> red (10%)
- colors defined as CSS custom properties for easy theming
- "Extend" button opens Bootstrap modal instead of inline controls
- 60s local countdown with 5-minute server refresh
- DOM observer calls SessionTimer.hide() on server stop
- Dockerfile copies session-timer.js to JupyterHub static/js/
- bump version to 3.8.1
2026-02-09 23:23:25 +01:00
stellarshenson
cc05843519 fix: Docker build pipeline - uv builder, disable Bake, pass CLI args
- rewrite builder stage to use uv with venv for isolated builds and tests
- disable COMPOSE_BAKE (created manifest lists instead of plain images,
  causing latest tag to point to stale cached image despite build logs
  showing successful installation)
- pass "$@" from build scripts to docker compose build so --no-cache
  and other CLI flags actually reach Docker (previously silently ignored)
- fix shared volume comment: read-write not read-only
2026-02-09 22:40:59 +01:00
stellarshenson
00c2db6518 refactor: extract stellars_hub package with declarative config
Decompose monolithic jupyterhub_config.py (694 lines) and
custom_handlers.py (1896 lines) into pip-installable stellars_hub
package, then refactor the package into a pure logic library with
zero hardcoded data.

Package structure (stellars_hub/):
- core modules: auth, branding, events, gpu, groups, hooks, services,
  volumes, docker_utils, password_cache, volume_cache
- handlers/: 14 Tornado request handlers across 8 files
- activity/: monitoring subsystem (model, monitor, helpers, sampler,
  service)
- tests/: 65 pytest tests across 8 test files
- pyproject.toml with hatchling build backend

Declarative config architecture:
- jupyterhub_config.py expanded to 306-line self-documenting file with
  5 sections: env vars, data literals, logic calls, JupyterHub config,
  services/callbacks
- every import, env var, data literal, and c.* setting documented with
  inline comments
- all function parameters explicit (no hidden env reads in modules)
- handlers read config from self.settings['stellars_config'] via
  tornado_settings instead of os.environ.get()
- eliminated constants.py and configure.py from package

Deleted files:
- conf/bin/custom_handlers.py (1896 lines)
- conf/bin/activity_sampler.py (243 lines)
- stellars_hub/constants.py (58 lines)
- stellars_hub/configure.py (188 lines)

Other changes:
- Dockerfile uses multi-stage build (builder builds wheel + runs tests)
- 01_ensure_groups.py passes group list inline
- notebook_dir commented out (redundant with lab image default)
- docs/stellars-hub-package.md documents package architecture
2026-02-09 22:21:42 +01:00
stellarshenson
83d2e448bc feat: add admin startup scripts, update branding documentation
- add JUPYTERLAB_AUX_SCRIPTS_PATH env var for admin-managed startup
  scripts in user containers, default /mnt/shared/start-platform.d
- pass JUPYTERLAB_AUX_SCRIPTS_PATH to DockerSpawner.environment
- expand README Custom Branding from logo-only to full table covering
  logo, favicon, lab main icon, and lab splash icon
- add README Admin Startup Scripts section
- add User Environment category in settings_dictionary.yml
- add JUPYTERLAB_AUX_SCRIPTS_PATH to Dockerfile defaults
2026-02-09 14:24:02 +01:00
stellarshenson
92b9b179d9 feat: add JupyterLab icon env vars, fix spawner env names
- add JUPYTERHUB_LAB_MAIN_ICON_URI and JUPYTERHUB_LAB_SPLASH_ICON_URI
  for custom JupyterLab main toolbar logo and splash screen icon
- file:// URIs copy to hub static dir and resolve to hub static URL,
  external URLs passed through as-is
- resolved URIs injected conditionally into DockerSpawner.environment
  as JUPYTERLAB_MAIN_ICON_URI and JUPYTERLAB_SPLASH_ICON_URI
- fix spawner env var names to match lab image expectations:
  JUPYTERHUB_SERVICE_* -> ENABLE_SERVICE_* (MLflow, Resources Monitor,
  TensorBoard), remove duplicate JUPYTERHUB_GPU_ENABLED
- update custom-branding.md with JupyterLab Icons section
2026-02-09 14:19:02 +01:00
stellarshenson
0846cf26eb feat: serve hub favicon to JupyterLab via CHP proxy routes
JupyterLab sessions show their own default favicon because requests to
/user/{username}/static/favicons/favicon.ico route through CHP directly
to user containers, bypassing the hub.

Exploit CHP's trie-based longest-prefix-match: pre_spawn_hook registers
a per-user CHP route (/user/{username}/static/favicons/) pointing back
to the hub. CHP prefers this over the generic /user/{username}/ route.

FaviconRedirectHandler changed from BaseHandler to RequestHandler and
injected into Tornado's wildcard_router (not extra_handlers which
auto-prefixes /hub/). Handler 302-redirects to hub's static favicon.

- CHP target uses host:port only (no path) to avoid CHP path rewriting
- Tornado handler inserted at position 0 in wildcard_router rules
- Conditional on JUPYTERHUB_FAVICON_URI being non-empty
- CHP routes added idempotently per spawn, no cleanup needed
2026-02-06 21:20:15 +01:00
stellarshenson
97f9070424 refactor: default branding env vars to empty for stock assets
JUPYTERHUB_LOGO_URI previously defaulted to file:///srv/jupyterhub/logo.svg
which doesn't exist in the image - the os.path.exists() check silently
fell through to stock JupyterHub logo anyway. Changed default to empty
across Dockerfile, config, compose.yml, and settings_dictionary.yml.
Both JUPYTERHUB_LOGO_URI and JUPYTERHUB_FAVICON_URI now consistently
default to empty, meaning stock JupyterHub assets are used unless
explicitly configured.
2026-02-06 16:59:46 +01:00
stellarshenson
264a2af6e5 feat: add JUPYTERHUB_FAVICON_URI for deployment-specific favicons
Mirrors the existing JUPYTERHUB_LOGO_URI pattern:
- file:// URIs copy the file to JupyterHub static dir at startup
- http(s):// URLs passed directly to template as link href
- empty/unset falls back to default static_url('favicon.ico')

Enables visual differentiation between dev/staging/prod deployments.
2026-02-06 16:30:03 +01:00
stellarshenson
6aaae9939c feat: add immediate loading spinners for admin user operations
- show spinner immediately on "Add Users" click before POST completes,
  then transition message to "Generating credentials..." after response
- add spinner for user rename (PATCH) with "Renaming user..." message
- add error path cleanup hiding spinner on failed creation, empty
  response, or parse errors
2026-02-06 15:37:04 +01:00
stellarshenson
f334d4b960 updated default halflife to 48 2026-02-02 14:53:31 +01:00
stellarshenson
2c0bcc676c fix: reposition volume button on each MutationObserver tick
Button now moves to end of actions cell if React renders additional
buttons after it, ensuring consistent rightmost positioning
2026-01-27 00:28:23 +01:00
stellarshenson
d3913b4d69 fix: admin volume button only for stopped servers, positioned last
- 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
2026-01-26 23:50:36 +01:00
stellarshenson
32d645182d fix: admin volume button now appears for all users
- 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)
2026-01-26 13:32:16 +01:00
stellarshenson
a67af7a090 fix: admin volume button for all users
- 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
2026-01-26 13:31:30 +01:00
stellarshenson
a7ec100878 fix: admin volume button positioning and styling
- 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
2026-01-26 13:28:54 +01:00
stellarshenson
f63de6454c feat: add admin volume management button
- Add Manage Volumes modal to admin panel with volume checkboxes
- MutationObserver injects database icon button into each user row
- Button uses btn-outline-secondary matching other admin buttons
- Calls existing DELETE /api/users/{username}/manage-volumes endpoint
- Admins can reset any user's volumes directly from admin panel
2026-01-26 13:25:39 +01:00
stellarshenson
9162de622f fix: volume sizes logging and tooltip cleanup
- Add _get_logger() helper for JupyterHub application logger integration
- Module-level logger doesn't inherit JupyterHub handlers, causing
  invisible logs for VolumeSizeRefresher and Volume Sizes operations
- Update all volume-related functions to use _get_logger()
- Add cache validation to prevent empty results overwriting valid cache
- Remove "(>8h/day)" suffix from activity tooltip when score > 100%
2026-01-26 12:57:32 +01:00
stellarshenson
6ced633450 feat: add deletion spinner and initialize activity for new users
- 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
2026-01-25 23:18:30 +01:00
stellarshenson
5678af11f8 fix: add volume refresher tick logging for diagnostics
- Added tick completion log showing user count and total MB after each refresh
- Helps diagnose if VolumeSizeRefresher is running or died silently
2026-01-25 21:50:25 +01:00
stellarshenson
d2a93a08f2 fix: Auth column width and Volumes tooltip
- Increased .col-auth width from 4em to 5em to prevent truncation
- Changed Volumes tooltip to generic "hover for breakdown" since
  volume names are autodiscovered
2026-01-25 12:50:06 +01:00
stellarshenson
845d458d75 feat: add column tooltips and authorization status column
- 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
2026-01-25 12:44:11 +01:00
stellarshenson
ad1d37b16f feat: add 24h minimum data requirement for activity score
- 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
2026-01-25 12:25:38 +01:00
stellarshenson
05c99624d9 feat: add activity score normalization with TARGET_HOURS
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
2026-01-25 12:17:26 +01:00
stellarshenson
a76c99d6ab feat: increase activity monitor half-life to 72 hours (3 days)
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
2026-01-25 11:50:19 +01:00
stellarshenson
8980e552cf refactor: simplify Activity column header
Removed "(7 days)" suffix from Activity column header in activity.html.
The retention period is a configuration detail, not needed in the UI.
2026-01-25 11:48:49 +01:00
stellarshenson
ea5e8b730a feat: add volume size tooltip with per-volume breakdown
Activity Monitor now shows hover tooltip on volume sizes displaying
individual volume sizes (home, workspace, cache) instead of just total.

Backend changes:
- _fetch_volume_sizes() returns {total, volumes: {suffix: size}}
- ActivityDataHandler passes volume_breakdown to frontend

Frontend changes:
- formatVolumeSize() displays tooltip with sorted volume list
- Dotted underline indicates tooltip availability

Also: removed clean dependency from build targets in Makefile
2026-01-22 22:39:54 +01:00
stellarshenson
4d9a97eb82 feat: increase activity monitor half-life to 48 hours
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).
2026-01-22 01:52:01 +01:00
stellarshenson
a737058903 feat: activity sampler as independent JupyterHub service
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.
2026-01-22 01:28:31 +01:00
stellarshenson
aaca516bae feat: add VolumeSizeRefresher for independent background refresh
- 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
2026-01-22 01:23:09 +01:00
stellarshenson
ae387e7734 refactor: use logging module instead of print statements 2026-01-22 01:17:04 +01:00
stellarshenson
30a1665e23 fix: add flush=True to volume sizes logging 2026-01-22 01:12:55 +01:00
stellarshenson
a2b7a0249a fix: activity bar colors 1=red, 2-3=yellow, 4-5=green 2026-01-21 16:50:36 +01:00
stellarshenson
887a3569fb fix: pass db and find_user to activity sampler instead of app
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.
2026-01-21 15:01:13 +01:00
stellarshenson
b8ab1f6614 fix: lazy start activity sampler on first Activity page access
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.
2026-01-21 14:56:41 +01:00
stellarshenson
24c51a959c feat: background activity sampler with Tornado PeriodicCallback
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
2026-01-21 14:38:17 +01:00
stellarshenson
7f9fb3f871 feat: activity sampling for all users including offline
- Added record_samples_for_all_users() to sample ALL users
- Offline users marked inactive (activity bars decay over time)
- New API: POST /hub/api/activity/sample (admin, for cron jobs)
- Returns counts: total, active, inactive, offline
2026-01-21 13:40:23 +01:00
stellarshenson
2c561490b3 perf: background caching for volume sizes
- Volume sizes now cached and refreshed hourly in background
- Activity page loads instantly (no longer waits for docker df)
- Added JUPYTERHUB_ACTIVITYMON_VOLUMES_UPDATE_INTERVAL (default 3600s)
- First load shows empty volumes until background refresh completes
2026-01-21 08:56:36 +01:00
stellarshenson
48da028968 feat: add Volumes column to activity monitor
- Backend: get_all_user_volumes_sizes() fetches Docker volume sizes
- Frontend: sortable Volumes column with human-friendly display (GB/MB)
- Badge now shows "N users (x active, y idle, z offline)"
- CSS: .col-volumes class (8em width)
2026-01-21 08:45:27 +01:00
stellarshenson
a118bb6d7f fix: remove CPU/Memory color coding in activity monitor
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.
2026-01-21 08:37:29 +01:00
stellarshenson
3ab2106c37 style: adjust activity table column widths for better proportions
- CPU: 7.5em, Memory: 9em, Time Left: 12em
- Last Active: 13.5em, Activity: 20em
- User column takes remaining space
- table-layout: fixed with 100% width
2026-01-20 21:35:24 +01:00
stellarshenson
d691390549 feat: show all users in activity monitor and refactor table styles
- 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
2026-01-20 21:26:24 +01:00
stellarshenson
21501e0a2d feat: add JUPYTERHUB_ACTIVITYMON_SAMPLING_INTERVAL env var
- 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
2026-01-20 20:40:33 +01:00
stellarshenson
9262e30fe8 refactor: separate resource refresh from activity sampling
- 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
2026-01-20 20:39:10 +01:00
stellarshenson
094be29d6d feat: add column sorting and improved last active display
- 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
2026-01-20 20:33:23 +01:00
stellarshenson
c1cabde01c fix: hide 'Measured' text when time empty, simplify settings text
- Only show "Measured X ago" when timeAgo has value
- Remove compose.yml/compose_override.yml mention from Settings page
2026-01-20 19:34:08 +01:00
stellarshenson
04ef42ce41 refactor: remove sample interval, simplify activity sampling
- Remove JUPYTERHUB_ACTIVITYMON_SAMPLE_INTERVAL env var entirely
- Remove on-demand sampling from activity page view
- Simplify record_sample() to always insert (caller controls frequency)
- Fix formatTimeAgo to show nothing when time ≤0s
- Update Dockerfile, settings_dictionary.yml, custom_handlers.py
2026-01-20 19:29:26 +01:00
stellarshenson
064c553808 feat: dynamic refresh timer and simplified last active display
- "Measured X ago" now updates every second (shows 5s ago, 3min ago, etc.)
- Last Active column left-aligned and shows rounded times (5min, 2h, 3d)
- Removed redundant minutes/hours from time display
2026-01-20 19:06:17 +01:00
stellarshenson
4b2fc084bf 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
2026-01-20 19:04:39 +01:00