Commit Graph

332 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
4261a60884 fix: always create Docker tag in make push regardless of git tag state
docker tag was inside the git tag else-branch, so when git tag already
existed (e.g., rebuild without version bump), Docker tag was never
created and make push failed with "tag does not exist"
2026-02-09 19:08:01 +01:00
stellarshenson
3512ad1023 fix: register favicon CHP routes in extra_routes to prevent stale deletion
check_routes() runs every ~5min and deletes any CHP route not in its
good_routes set. Favicon routes added only via add_route() were treated
as stale and removed after ~5 minutes. Now also registered in
app.proxy.extra_routes so check_routes() recognizes them as legitimate.
Applied to both pre_spawn_hook and startup callback for surviving servers.
FIX_FAVICON_CHP_STALE_ROUTE
2026-02-09 17:17:51 +01:00
stellarshenson
107764474d fix: resolve lab icon URIs at runtime in pre_spawn_hook
file:// icons were resolved to bare paths at config load time, but
JupyterLab extensions interpret bare paths as filesystem locations.
Moved URI resolution to pre_spawn_hook where app.hub.url provides the
hub origin at runtime, constructing fully qualified http:// URLs
(e.g., http://jupyterhub:8080/hub/static/lab-main-icon.svg).
2026-02-09 16:51:21 +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
da6cb127d8 docs: update custom branding with startup callback for surviving servers 2026-02-09 13:40:38 +01:00
stellarshenson
4bd130b11e fix: register favicon CHP routes for servers surviving hub restart
pre_spawn_hook only fires on new spawns. Servers already running when
JupyterHub restarts never trigger it, leaving their favicon CHP routes
missing. Add IOLoop.current().add_callback() one-shot startup callback
that iterates all active servers and registers their favicon routes
immediately after the event loop starts.
2026-02-09 13:39:56 +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
2d3e1e7949 chore: bump version to 3.7.20
Reflects JUPYTERHUB_FAVICON_URI feature and branding defaults cleanup.
2026-02-06 17:43:49 +01:00
stellarshenson
38b4ebb821 fix: add missing sys import for favicon static path resolution
sys.prefix used in favicon copy path but sys was not imported at
module level - only imported later in a different block.
2026-02-06 17:37:26 +01:00
stellarshenson
6e5115e153 fix: use correct JupyterHub static files path for favicon copy
Static files are served from {sys.prefix}/share/jupyterhub/static/,
not from the Python module directory. os.path.dirname(jupyterhub.__file__)
resolves to site-packages/jupyterhub/ which has no static/ subdirectory.
2026-02-06 17:03:17 +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
ca9d9cf3f4 docs: document JUPYTERHUB_LOGO_URI and JUPYTERHUB_FAVICON_URI env vars
Add branding env variables to CLAUDE.md environment variables reference.
Both follow file:// (local) or URL (external) pattern.
2026-02-06 16:36:50 +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
7e988b1ee4 chore: updated with the new way of localhost routing 2026-02-06 11:53:30 +01:00
stellarshenson
4ce1967481 updated with JUPYTERHUB_HOSTNAME to allow single-chunk routing 2026-02-06 09:23:45 +01:00
stellarshenson
232bb23f88 updated with .env copy 2026-02-06 08:54:38 +01:00
stellarshenson
1f6110e84f feat: upgraded certs generation 2026-02-06 08:52:57 +01:00
stellarshenson
fa9336bd71 chore: updated with different env name 2026-02-06 08:47:50 +01:00
stellarshenson
134034a203 docs: updated with better documentaiton 2026-02-06 08:29:39 +01:00
stellarshenson
f334d4b960 updated default halflife to 48 2026-02-02 14:53:31 +01:00
stellarshenson
3f06fb3399 docs: update journal with volume button positioning fix 2026-01-27 00:28:47 +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
ea814b70f8 docs: update journal with README documentation changes 2026-01-26 13:38:08 +01:00
stellarshenson
920c8b82d5 docs: add volume sizes and resource tracking to README
- Add volume sizes to Activity Monitor feature description
- Add Volume sizes feature with per-volume breakdown tooltip
- Add JUPYTERHUB_ACTIVITYMON_VOLUMES_UPDATE_INTERVAL config option
2026-01-26 13:37:32 +01:00
stellarshenson
0905d5bd07 docs: add admin volume management to README
- 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
2026-01-26 13:37:01 +01:00
stellarshenson
2a533f9140 docs: update journal with admin volume button feature
- 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
2026-01-26 13:35:26 +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
4d1f4d1407 upraded latest version 2026-01-25 23:23:38 +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
FIX_VOLUME_REFRESHER_3.7.16
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
3.7.16_cuda-13.0.2_jh-5.4.2
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
3343c568a7 docs: clarify configured vs effective half-life
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.
2026-01-25 12:09:12 +01:00
stellarshenson
b7b3f0e87c docs: add half-life simulation tables for different work patterns
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%).
2026-01-25 11:54:54 +01:00
stellarshenson
cdf1e5eaa4 docs: add rationale for 72-hour half-life choice
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.
2026-01-25 11:51:20 +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