From d9896a7073a9a849ad98fd4bc053a96098414ccd Mon Sep 17 00:00:00 2001 From: stellarshenson Date: Tue, 4 Nov 2025 10:49:32 +0100 Subject: [PATCH] feat: add icons to buttons and auto-refresh on stop, implement GitHub Actions CI/CD UI Enhancements: - Add Font Awesome icons to all control buttons (stop, start, restart, manage volumes) - Auto-refresh page after server stop with smooth UI transitions - Hide/show appropriate buttons based on server state (Restart vs Manage Volumes) - Re-inject icons removed by JupyterHub's DOM manipulation Technical Implementation: - MutationObserver watches for JupyterHub DOM changes after stop - Immediate UI state correction before page refresh - Comprehensive console logging for debugging CI/CD: - Add GitHub Actions workflow for Dockerfile validation with hadolint - Triggers on push to main, version tags, and pull requests - Uses hadolint to ensure Dockerfile best practices --- .github/workflows/docker-build.yml | 29 +++++++++ RELEASE.md | 1 - project.env | 2 +- services/jupyterhub/templates/home.html | 79 ++++++++++++++++++------- 4 files changed, 88 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/docker-build.yml diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..1ccc80c --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,29 @@ +name: Validate Dockerfile + +on: + push: + branches: + - main + tags: + - '*_cuda-*_jh-*' + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Lint Dockerfile with hadolint + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: services/jupyterhub/Dockerfile.jupyterhub + ignore: DL3008,DL3013,DL3015,DL3059,DL3027,DL4006,SC2006,SC1008,DL3046,SC2153,DL3001,SC2016,SC2002 + + - name: Validation completed + run: echo "Dockerfile validation completed successfully" diff --git a/RELEASE.md b/RELEASE.md index 533f9bc..21d6436 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -5,7 +5,6 @@ **User Self-Service Capabilities** - **Restart Server**: Users can restart their running JupyterLab containers directly from the control panel without admin intervention - **Manage Volumes**: Selective reset of persistent volumes (home/workspace/cache) when server is stopped - users choose which volumes to reset via checkbox interface -- **Auto-refresh**: Page automatically refreshes after Stop/Manage/Restart actions for seamless UX ## Technical Improvements diff --git a/project.env b/project.env index d647603..9b97b8e 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.0.14_cuda-12.9.1_jh-5.4.2" +VERSION="3.0.20_cuda-12.9.1_jh-5.4.2" VERSION_COMMENT="Multi-user JupyterHub with GPU auto-detection, self-service volume management (home/workspace/cache), and container restart capabilities" # Author diff --git a/services/jupyterhub/templates/home.html b/services/jupyterhub/templates/home.html index 9f8639f..38c7fae 100644 --- a/services/jupyterhub/templates/home.html +++ b/services/jupyterhub/templates/home.html @@ -7,11 +7,17 @@

JupyterHub home page

- {% if default_server.active %}Stop My Server{% endif %} + {% if default_server.active %} + + + Stop My Server + + {% endif %} + {% if not default_server.active %}Start{% endif %} My Server @@ -372,27 +378,58 @@ }); // Add page refresh after Stop Server completes - $('#stop').on('click', function() { - console.log('[Stop Server] Stop button clicked, will refresh page after stop completes'); - // JupyterHub's home.js handles the actual stop - // We just need to refresh after it completes - // Monitor for when button changes state (becomes start button) - const checkInterval = setInterval(function() { - if ($('#stop').length === 0 && $('#start').length > 0) { - console.log('[Stop Server] Server stopped, waiting before refresh...'); - clearInterval(checkInterval); - setTimeout(function() { - console.log('[Stop Server] Refreshing page now'); - location.reload(); - }, 3000); // Wait 3 seconds after stop completes before refreshing - } - }, 500); + // Watch for when JupyterHub's home.js modifies the DOM after stop + console.log('[Stop Server] Setting up DOM observer'); - // Safety timeout to clear interval after 30 seconds - setTimeout(function() { - clearInterval(checkInterval); - }, 30000); - }); + const targetNode = document.querySelector('.text-center'); + if (targetNode) { + const observer = new MutationObserver(function(mutations) { + // Check if stop button was hidden (JupyterHub uses .hide() which sets display:none) + const stopButton = document.getElementById('stop'); + const startButton = document.getElementById('start'); + + // Check visibility using offsetParent (null = hidden) + const stopVisible = stopButton && stopButton.offsetParent !== null; + const startVisible = startButton && startButton.offsetParent !== null; + + console.log('[Stop Server] Mutation detected - stop visible:', stopVisible, 'start visible:', startVisible); + + if (!stopVisible && startVisible) { + // Check if start button text was changed by JupyterHub (indicates stop completed) + const startText = startButton.textContent.trim(); + console.log('[Stop Server] Start button text:', startText); + + if (startText === 'Start My Server') { + console.log('[Stop Server] JupyterHub updated DOM after stop, updating UI and refreshing...'); + observer.disconnect(); + + // Re-inject the play icon that JupyterHub removed + const iconHtml = ' Start My Server'; + $(startButton).html(iconHtml); + + // Hide Restart Server button and show Manage Volumes button + $('#restart-server-btn').hide(); + $('#manage-volumes-btn').show(); + console.log('[Stop Server] Updated button visibility - showing Manage Volumes'); + + setTimeout(function() { + console.log('[Stop Server] Refreshing now'); + location.reload(); + }, 1000); // Give user a moment to see the corrected button state + } + } + }); + + observer.observe(targetNode, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['style', 'class'], + characterData: true + }); + + console.log('[Stop Server] Observer attached to button container'); + } // Debug: Check if buttons exist console.log('[Custom Handlers] Restart button exists:', $('#restart-server-btn').length > 0);