Added Nbgrader
This commit is contained in:
		
							
								
								
									
										22
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
# --- This Project ---#
 | 
			
		||||
data/
 | 
			
		||||
logs/
 | 
			
		||||
.env
 | 
			
		||||
 | 
			
		||||
# Docker secrets
 | 
			
		||||
/secrets/
 | 
			
		||||
*.secret
 | 
			
		||||
*.key
 | 
			
		||||
*.pem
 | 
			
		||||
*.crt
 | 
			
		||||
 | 
			
		||||
# --- OS & Temp Files ---
 | 
			
		||||
.DS_Store
 | 
			
		||||
Thumbs.db
 | 
			
		||||
*.swp
 | 
			
		||||
*.swo
 | 
			
		||||
*.tmp
 | 
			
		||||
*~
 | 
			
		||||
~$*
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										39
									
								
								compose.override.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								compose.override.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
services:
 | 
			
		||||
  jupyterhub:
 | 
			
		||||
    container_name: ${COMPOSE_PROJECT_NAME}-jupyterhub
 | 
			
		||||
    environment:
 | 
			
		||||
      - POSTGRES_HOST=postgres 
 | 
			
		||||
      - POSTGRES_DB=${POSTGRES_DB}
 | 
			
		||||
      - POSTGRES_USER=${POSTGRES_USER}
 | 
			
		||||
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
 | 
			
		||||
        
 | 
			
		||||
      - JUPYTERHUB_CRYPT_KEY=${JUPYTERHUB_CRYPT_KEY}
 | 
			
		||||
      - DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}
 | 
			
		||||
      - NBGRADER_DB_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${NBGRADER_DB}
 | 
			
		||||
    ports:
 | 
			
		||||
      - ${JUPYTERHUB_PORT:-8000}:8000
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./data/jupyter:/srv/jupyterhub/data
 | 
			
		||||
    networks:
 | 
			
		||||
      - jupyterhub-network
 | 
			
		||||
    depends_on:
 | 
			
		||||
      - postgres 
 | 
			
		||||
 | 
			
		||||
  postgres:
 | 
			
		||||
    container_name: ${COMPOSE_PROJECT_NAME}-postgres
 | 
			
		||||
    ports:
 | 
			
		||||
      - ${POSTGRES_PORT:-5432}:5432
 | 
			
		||||
    volumes:
 | 
			
		||||
      - postgres_data:/var/lib/postgresql/data
 | 
			
		||||
    networks:
 | 
			
		||||
      - jupyterhub-network
 | 
			
		||||
  
 | 
			
		||||
  nginx:
 | 
			
		||||
    container_name: ${COMPOSE_PROJECT_NAME}-nginx
 | 
			
		||||
    ports:
 | 
			
		||||
      - ${NGINX_HTTP_PORT:-80}:80 
 | 
			
		||||
      - ${NGINX_HTTPS_PORT:-443}:443
 | 
			
		||||
    networks:
 | 
			
		||||
      - jupyterhub-network
 | 
			
		||||
    depends_on:
 | 
			
		||||
      - jupyterhub 
 | 
			
		||||
							
								
								
									
										10
									
								
								compose.yml
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								compose.yml
									
									
									
									
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
include: 
 | 
			
		||||
  - jupyterhub/compose.yml
 | 
			
		||||
  - postgres/compose.yml
 | 
			
		||||
  - nginx/compose.yml
 | 
			
		||||
 | 
			
		||||
networks:
 | 
			
		||||
  jupyterhub-network:
 | 
			
		||||
    name: ${DOCKER_NETWORK_NAME:-jupyterhub-network}
 | 
			
		||||
    driver: bridge
 | 
			
		||||
    attachable: true
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										61
									
								
								flake.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								flake.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
{
 | 
			
		||||
  "nodes": {
 | 
			
		||||
    "flake-utils": {
 | 
			
		||||
      "inputs": {
 | 
			
		||||
        "systems": "systems"
 | 
			
		||||
      },
 | 
			
		||||
      "locked": {
 | 
			
		||||
        "lastModified": 1731533236,
 | 
			
		||||
        "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
 | 
			
		||||
        "owner": "numtide",
 | 
			
		||||
        "repo": "flake-utils",
 | 
			
		||||
        "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      },
 | 
			
		||||
      "original": {
 | 
			
		||||
        "owner": "numtide",
 | 
			
		||||
        "repo": "flake-utils",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "nixpkgs": {
 | 
			
		||||
      "locked": {
 | 
			
		||||
        "lastModified": 1750776420,
 | 
			
		||||
        "narHash": "sha256-/CG+w0o0oJ5itVklOoLbdn2dGB0wbZVOoDm4np6w09A=",
 | 
			
		||||
        "owner": "NixOS",
 | 
			
		||||
        "repo": "nixpkgs",
 | 
			
		||||
        "rev": "30a61f056ac492e3b7cdcb69c1e6abdcf00e39cf",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      },
 | 
			
		||||
      "original": {
 | 
			
		||||
        "owner": "NixOS",
 | 
			
		||||
        "ref": "nixos-unstable",
 | 
			
		||||
        "repo": "nixpkgs",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "root": {
 | 
			
		||||
      "inputs": {
 | 
			
		||||
        "flake-utils": "flake-utils",
 | 
			
		||||
        "nixpkgs": "nixpkgs"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "systems": {
 | 
			
		||||
      "locked": {
 | 
			
		||||
        "lastModified": 1681028828,
 | 
			
		||||
        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
 | 
			
		||||
        "owner": "nix-systems",
 | 
			
		||||
        "repo": "default",
 | 
			
		||||
        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      },
 | 
			
		||||
      "original": {
 | 
			
		||||
        "owner": "nix-systems",
 | 
			
		||||
        "repo": "default",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "root": "root",
 | 
			
		||||
  "version": 7
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										161
									
								
								flake.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								flake.nix
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,161 @@
 | 
			
		||||
{
 | 
			
		||||
    description = "Docker JupyterHub development environment";
 | 
			
		||||
 | 
			
		||||
    inputs = {
 | 
			
		||||
        nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
 | 
			
		||||
        flake-utils.url = "github:numtide/flake-utils";
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    outputs = { self, nixpkgs, flake-utils, ... }:
 | 
			
		||||
        flake-utils.lib.eachDefaultSystem (system:
 | 
			
		||||
        let
 | 
			
		||||
            pkgs = nixpkgs.legacyPackages.${system};
 | 
			
		||||
 | 
			
		||||
            app = {
 | 
			
		||||
                setup-dev = pkgs.writeShellScriptBin "setup-dev" ''
 | 
			
		||||
                    echo "Setting up basic Project Structure"
 | 
			
		||||
                    echo "----------------------------------"
 | 
			
		||||
                    echo "Setting up logs directory"
 | 
			
		||||
                    mkdir -p logs
 | 
			
		||||
                    mkdir -p logs/nginx 
 | 
			
		||||
                    mkdir -p logs/jupyterhub 
 | 
			
		||||
                    mkdir -p logs/postgres
 | 
			
		||||
                    echo "Logs Directory:"
 | 
			
		||||
                    tree logs/
 | 
			
		||||
                    echo ""
 | 
			
		||||
                    echo "Setting up jupyter-data"
 | 
			
		||||
                    mkdir -p jupyter-data/nbgrader/courses/example-course/{autograded,feedback,release,source/assigment1,submitted}
 | 
			
		||||
                    mkdir -p jupyter-data/nbgrader/{exchange,users}
 | 
			
		||||
                    cat > jupyter-data/nbgrader/courses/example-course/source/assigment1/sample.ipynb << 'EOL' 
 | 
			
		||||
                    {
 | 
			
		||||
                        "cells": [
 | 
			
		||||
                            {
 | 
			
		||||
                                "cell_type": "markdown",
 | 
			
		||||
                                "metadata": {},
 | 
			
		||||
                                "source": [
 | 
			
		||||
                                    "# Sample Notebook\n",
 | 
			
		||||
                                    "## Created with Bash\n",
 | 
			
		||||
                                    "\n",
 | 
			
		||||
                                    "This Jupyter notebook was generated automatically using a bash script."
 | 
			
		||||
                                ]
 | 
			
		||||
                            },
 | 
			
		||||
                            {
 | 
			
		||||
                                "cell_type": "code",
 | 
			
		||||
                                "execution_count": null,
 | 
			
		||||
                                "metadata": {},
 | 
			
		||||
                                "outputs": [],
 | 
			
		||||
                                "source": [
 | 
			
		||||
                                    "# Python code example\n",
 | 
			
		||||
                                    "import numpy as np\n",
 | 
			
		||||
                                    "import matplotlib.pyplot as plt\n",
 | 
			
		||||
                                    "\n",
 | 
			
		||||
                                    "x = np.linspace(0, 10, 100)\n",
 | 
			
		||||
                                    "y = np.sin(x)\n",
 | 
			
		||||
                                    "\n",
 | 
			
		||||
                                    "plt.plot(x, y)\n",
 | 
			
		||||
                                    "plt.title('Sine Wave')\n",
 | 
			
		||||
                                    "plt.show()"
 | 
			
		||||
                                ]
 | 
			
		||||
                            },
 | 
			
		||||
                            {
 | 
			
		||||
                                "cell_type": "markdown",
 | 
			
		||||
                                "metadata": {},
 | 
			
		||||
                                "source": [
 | 
			
		||||
                                    "## Next Steps\n",
 | 
			
		||||
                                    "\n",
 | 
			
		||||
                                    "- Run the code cells\n",
 | 
			
		||||
                                    "- Add more cells\n",
 | 
			
		||||
                                    "- Experiment with different Python libraries"
 | 
			
		||||
                                ]
 | 
			
		||||
                            }
 | 
			
		||||
                        ],
 | 
			
		||||
                        "metadata": {
 | 
			
		||||
                            "kernelspec": {
 | 
			
		||||
                                "display_name": "Python 3",
 | 
			
		||||
                                "language": "python",
 | 
			
		||||
                                "name": "python3"
 | 
			
		||||
                            },
 | 
			
		||||
                            "language_info": {
 | 
			
		||||
                                "codemirror_mode": {
 | 
			
		||||
                                    "name": "ipython",
 | 
			
		||||
                                    "version": 3
 | 
			
		||||
                                },
 | 
			
		||||
                            "file_extension": ".py",
 | 
			
		||||
                            "mimetype": "text/x-python",
 | 
			
		||||
                            "name": "python",
 | 
			
		||||
                            "nbconvert_exporter": "python",
 | 
			
		||||
                            "pygments_lexer": "ipython3",
 | 
			
		||||
                            "version": "3.8.5"
 | 
			
		||||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
                        "nbformat": 4,
 | 
			
		||||
                        "nbformat_minor": 4
 | 
			
		||||
                    }
 | 
			
		||||
                    EOL
 | 
			
		||||
                    echo "jupyter-data Directory:"
 | 
			
		||||
                    tree jupyter-data
 | 
			
		||||
                '';
 | 
			
		||||
                
 | 
			
		||||
                storage = pkgs.writeShellScriptBin "storage" ''
 | 
			
		||||
                    echo "Creating storage"
 | 
			
		||||
                    mkdir -p storage/{jupyter,postgres}
 | 
			
		||||
                    chmod -R 644 storage/
 | 
			
		||||
                '';
 | 
			
		||||
 | 
			
		||||
                build = pkgs.writeShellScriptBin "build" ''
 | 
			
		||||
                    echo "Building all Containers"
 | 
			
		||||
                '';
 | 
			
		||||
 | 
			
		||||
                clean = pkgs.writeShellScriptBin "clean" ''
 | 
			
		||||
                    echo "Cleaning Project"
 | 
			
		||||
                    echo "----------------------------------"
 | 
			
		||||
                    echo "Removing logs"
 | 
			
		||||
                    rm -rf logs/
 | 
			
		||||
                    echo "Removing jupyter-data"
 | 
			
		||||
                    rm -rf jupyter-data/
 | 
			
		||||
                '';
 | 
			
		||||
 | 
			
		||||
                reset = pkgs.writeShellScriptBin "reset" ''
 | 
			
		||||
                   ${app.clean}/bin/clean
 | 
			
		||||
                   ${app.setup-dev}/bin/setup-dev
 | 
			
		||||
                '';
 | 
			
		||||
            };
 | 
			
		||||
        in
 | 
			
		||||
        {
 | 
			
		||||
        devShells.default = pkgs.mkShell {
 | 
			
		||||
            buildInputs = with pkgs; [
 | 
			
		||||
                # For deployment/testing
 | 
			
		||||
                docker
 | 
			
		||||
                docker-compose
 | 
			
		||||
 | 
			
		||||
                # Terminal Tools
 | 
			
		||||
                tree
 | 
			
		||||
            ] ++ (builtins.attrValues app);
 | 
			
		||||
 | 
			
		||||
            shellHook = ''
 | 
			
		||||
                echo "JupyterHub dev shell"
 | 
			
		||||
                echo "------------------------------------"
 | 
			
		||||
                echo "$(docker --version)"
 | 
			
		||||
                echo "------------------------------------"
 | 
			
		||||
                echo "Available commands:"
 | 
			
		||||
                echo "  setup-dev - Set Up dev environment"
 | 
			
		||||
                echo "  clean - Clean the Project"
 | 
			
		||||
                echo "  reset - Reset project completly (Use with caution it whipes all generated Data)"
 | 
			
		||||
            '';
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        apps = {
 | 
			
		||||
            setup-dev = {
 | 
			
		||||
                type = "app";
 | 
			
		||||
                program = "${app.setup-dev}/bin/setup-dev";
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            build = {
 | 
			
		||||
                type = "app";
 | 
			
		||||
                program = "${app.build}/bin/build";
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
            default = self.apps.${system}.dev;
 | 
			
		||||
        };
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,17 +0,0 @@
 | 
			
		||||
services:
 | 
			
		||||
  db:
 | 
			
		||||
    image: postgres
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment:
 | 
			
		||||
      POSTRGES_PASSWORD: 123456
 | 
			
		||||
    volumes:
 | 
			
		||||
      - pgdata:/var/lib/postgresql/data
 | 
			
		||||
 | 
			
		||||
  adminer:
 | 
			
		||||
    image: adminer
 | 
			
		||||
    restart: always 
 | 
			
		||||
    ports:
 | 
			
		||||
      - 8080:8080 
 | 
			
		||||
 | 
			
		||||
volumes:
 | 
			
		||||
  pgdata:
 | 
			
		||||
							
								
								
									
										48
									
								
								jupyterhub/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								jupyterhub/Dockerfile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
FROM quay.io/jupyterhub/jupyterhub:latest
 | 
			
		||||
 | 
			
		||||
# Install system dependencies
 | 
			
		||||
USER root
 | 
			
		||||
RUN apt-get update && apt-get install -y \
 | 
			
		||||
    curl \
 | 
			
		||||
    gnupg \
 | 
			
		||||
    iproute2 \
 | 
			
		||||
    iputils-ping \
 | 
			
		||||
    netcat \
 | 
			
		||||
    ca-certificates \
 | 
			
		||||
    && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
# Install Docker CLI (needed for DockerSpawner)
 | 
			
		||||
RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \
 | 
			
		||||
    && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu focal stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \
 | 
			
		||||
    && apt-get update \
 | 
			
		||||
    && apt-get install -y docker-ce-cli \
 | 
			
		||||
    && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
# Install Required packages
 | 
			
		||||
RUN pip install --no-cache-dir \
 | 
			
		||||
        dockerspawner \
 | 
			
		||||
        jupyterhub-nativeauthenticator \
 | 
			
		||||
        jupyterhub-dummyauthenticator \
 | 
			
		||||
        nbgrader \
 | 
			
		||||
        psycopg2-binary
 | 
			
		||||
 | 
			
		||||
RUN mkdir -p /srv/nbgrader/exchange
 | 
			
		||||
RUN chmod -R 777 /srv/nbgrader/exchange
 | 
			
		||||
RUN mkdir -p /srv/nbgrader/courses
 | 
			
		||||
RUN chown -R 1000:100 /srv/nbgrader 
 | 
			
		||||
 | 
			
		||||
#RUN jupyter nbextension install --sys-prefix --py nbgrader --overwrite
 | 
			
		||||
#RUN jupyter nbextension enable --sys-prefix --py nbgrader
 | 
			
		||||
#RUN jupyter serverextension enable --sys-prefix --py nbgrader
 | 
			
		||||
 | 
			
		||||
# Set Workdir
 | 
			
		||||
WORKDIR /srv/jupyterhub
 | 
			
		||||
 | 
			
		||||
# Generate Cookie Secret
 | 
			
		||||
RUN openssl rand -hex 32 > /srv/jupyterhub/cookie_secret
 | 
			
		||||
RUN chmod 600 /srv/jupyterhub/cookie_secret
 | 
			
		||||
#ENV JUPYTERHUB_AUTH_TOKEN=$(openssl rand -hex 32)
 | 
			
		||||
 | 
			
		||||
EXPOSE 8000
 | 
			
		||||
 | 
			
		||||
CMD ["jupyterhub", "-f", "/srv/jupyterhub/jupyterhub_config.py"]
 | 
			
		||||
							
								
								
									
										15
									
								
								jupyterhub/compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								jupyterhub/compose.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
services:
 | 
			
		||||
  jupyterhub:
 | 
			
		||||
    build: .
 | 
			
		||||
    container_name: jupyterhub
 | 
			
		||||
    restart: unless-stopped 
 | 
			
		||||
    ports:
 | 
			
		||||
      - 8000:8000
 | 
			
		||||
    environment:
 | 
			
		||||
      - JUPYTERHUB_LOG_LEVEL=DEBUG
 | 
			
		||||
    volumes:
 | 
			
		||||
      - /var/run/docker.sock:/var/run/docker.sock
 | 
			
		||||
      - ./templates:/srv/jupyterhub/templates:ro
 | 
			
		||||
      - ./jupyterhub_config.py:/srv/jupyterhub/jupyterhub_config.py
 | 
			
		||||
      - ./nbgrader_config.py:/srv/nbgrader/courses/nbgrader_config.py
 | 
			
		||||
      - ../logs/jupyterhub:/var/log/jupyterhub
 | 
			
		||||
							
								
								
									
										9
									
								
								jupyterhub/conf_test.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								jupyterhub/conf_test.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
c = get_config()
 | 
			
		||||
 | 
			
		||||
c.JupyterHub.authenticator_class = "dummy"
 | 
			
		||||
c.JupyterHub.spawner_class = "docker"
 | 
			
		||||
c.JupyterHub.hub_ip = '0.0.0.0'
 | 
			
		||||
c.JupyterHub.hub_connect_ip = 'jupyterhub'
 | 
			
		||||
c.DockerSpawner.image = 'jupyter/base-notebook'
 | 
			
		||||
c.DockerSpawner.network_name = 'stack_default'
 | 
			
		||||
c.DockerSpawner.remove = True
 | 
			
		||||
							
								
								
									
										227
									
								
								jupyterhub/jupyterhub_config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										227
									
								
								jupyterhub/jupyterhub_config.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,227 @@
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
from dockerspawner import DockerSpawner
 | 
			
		||||
from jupyterhub.utils import random_port
 | 
			
		||||
from nativeauthenticator import NativeAuthenticator
 | 
			
		||||
#from nbgrader.auth import JupyterHubAuthPlugin
 | 
			
		||||
from traitlets import Unicode, Bool, Int, Float
 | 
			
		||||
 | 
			
		||||
import secrets
 | 
			
		||||
import string
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
def generate_api_token(
 | 
			
		||||
    length: int = 32,
 | 
			
		||||
    prefix: Optional[str] = None,
 | 
			
		||||
) -> dict:
 | 
			
		||||
    """
 | 
			
		||||
    Generate a secure random API token with optional prefix and expiration.
 | 
			
		||||
    
 | 
			
		||||
    Args:
 | 
			
		||||
        length: Length of the token (default: 32)
 | 
			
		||||
        prefix: Optional prefix for the token (e.g., 'hub_')
 | 
			
		||||
        expiration_days: Optional expiration in days from now
 | 
			
		||||
        
 | 
			
		||||
    Returns:
 | 
			
		||||
        - token: The generated token
 | 
			
		||||
    """
 | 
			
		||||
    # Generate cryptographically secure random string
 | 
			
		||||
    alphabet = string.ascii_letters + string.digits
 | 
			
		||||
    token = ''.join(secrets.choice(alphabet) for _ in range(length))
 | 
			
		||||
    
 | 
			
		||||
    # Add prefix if specified
 | 
			
		||||
    if prefix:
 | 
			
		||||
        token = f"{prefix}{token}"
 | 
			
		||||
    
 | 
			
		||||
    return token 
 | 
			
		||||
 | 
			
		||||
import subprocess
 | 
			
		||||
 | 
			
		||||
def is_service_available_cmd(host, port):
 | 
			
		||||
    """Check service using system commands"""
 | 
			
		||||
    try:
 | 
			
		||||
        subprocess.run(
 | 
			
		||||
            ['nc', '-z', host, str(port)],
 | 
			
		||||
            check=True,
 | 
			
		||||
            timeout=3,
 | 
			
		||||
            stdout=subprocess.DEVNULL,
 | 
			
		||||
            stderr=subprocess.DEVNULL
 | 
			
		||||
        )
 | 
			
		||||
        return True
 | 
			
		||||
    except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
# Configuration file for JupyterHub
 | 
			
		||||
c = get_config()
 | 
			
		||||
 | 
			
		||||
# Base URL configuration (The new way, dont use ip/port/bind_url attributes it throws a warning and is deprecated)
 | 
			
		||||
port = int(os.environ.get('JUPYTERHUB_PORT', '8000'))
 | 
			
		||||
base_url = os.environ.get('JUPYTERHUB_BASE_URL', '/jupyter/')
 | 
			
		||||
c.JupyterHub.bind_url = f"http://0.0.0.0:{port}{base_url}"
 | 
			
		||||
 | 
			
		||||
# Database configuration
 | 
			
		||||
c.JupyterHub.db_url = f"postgresql://{os.environ['POSTGRES_USER']}:{os.environ['POSTGRES_PASSWORD']}@stack-{os.environ['POSTGRES_HOST']}:5432/{os.environ['POSTGRES_DB']}"
 | 
			
		||||
#c.JupyterHub.db_url = 'sqlite:///jupyterhub.sqlite' 
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Authenticator configuration - NativeAuthenticator
 | 
			
		||||
c.JupyterHub.authenticator_class = NativeAuthenticator
 | 
			
		||||
 | 
			
		||||
# Native Authenticator settings
 | 
			
		||||
#c.NativeAuthenticator.create_system_users = True
 | 
			
		||||
c.NativeAuthenticator.minimum_password_length = int(os.environ.get('NATIVE_AUTH_MIN_PASSWORD_LENGTH', '8'))
 | 
			
		||||
c.NativeAuthenticator.check_common_password = True
 | 
			
		||||
c.NativeAuthenticator.enable_signup = True
 | 
			
		||||
c.NativeAuthenticator.ask_email_on_signup = False
 | 
			
		||||
c.NativeAuthenticator.allow_2fa = False
 | 
			
		||||
 | 
			
		||||
# Admin configuration
 | 
			
		||||
admin_user = os.environ.get('JUPYTERHUB_ADMIN_USER', 'admin')
 | 
			
		||||
c.Authenticator.admin_users = {admin_user, 'DerGrumpf'}
 | 
			
		||||
c.Authenticator.any_allow_config = True
 | 
			
		||||
c.Authenticator.allow_all = True
 | 
			
		||||
 | 
			
		||||
# Custom logo and templates
 | 
			
		||||
c.JupyterHub.logo_file = '/srv/jupyterhub/templates/logo.png'
 | 
			
		||||
c.JupyterHub.template_paths = ['/etc/jupyterhub/templates']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Hub environment for containers
 | 
			
		||||
c.JupyterHub.spawner_class = DockerSpawner
 | 
			
		||||
c.Spawner.ip = '0.0.0.0'
 | 
			
		||||
c.DockerSpawner.image = os.environ.get('NOTEBOOK_IMAGE', 'jupyter/scipy-notebook:latest')
 | 
			
		||||
'''network_name = os.environ.get('DOCKER_NETWORK_NAME', 'stack_default')
 | 
			
		||||
c.DockerSpawner.network_name = network_name 
 | 
			
		||||
c.DockerSpawner.extra_host_config = {
 | 
			
		||||
    'network_mode': network_name
 | 
			
		||||
}
 | 
			
		||||
c.DockerSpawner.remove = True 
 | 
			
		||||
c.DockerSpawner.debug = True
 | 
			
		||||
c.DockerSpawner.default_url = os.environ.get('SPAWNER_DEFAULT_URL', '/lab')
 | 
			
		||||
c.DockerSpawner.use_internal_ip = True
 | 
			
		||||
 | 
			
		||||
# Resources
 | 
			
		||||
c.DockerSpawner.mem_limit = os.environ.get('NOTEBOOK_MEMORY_LIMIT', '500M')
 | 
			
		||||
c.DockerSpawner.cpu_limit = float(os.environ.get('NOTEBOOK_CPU_LIMIT', '1.0'))
 | 
			
		||||
 | 
			
		||||
# Volume
 | 
			
		||||
c.DockerSpawner.volumes = {
 | 
			
		||||
     '/var/run/docker.sock': '/var/run/docker.sock'
 | 
			
		||||
#    './data/jupyter/users/{username}': '/home/jovyan/work',
 | 
			
		||||
#    './data/jupyter/nbgrader/exchange': '/srv/nbgrader/exchange',
 | 
			
		||||
#    './data/jupyter/nbgrader/courses': '/srv/nbgrader/courses',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
c.DockerSpawner.notebook_dir = '/home/jovyan/work'
 | 
			
		||||
 | 
			
		||||
# Container Environment 
 | 
			
		||||
c.DockerSpawner.environment = {
 | 
			
		||||
    'GRANT_SUDO': '0',
 | 
			
		||||
    'JUPYTER_ENABLE_LAB': os.environ.get('ENABLE_LAB', '1'),
 | 
			
		||||
    'JUPYTERHUB_SINGLEUSER_APP': 'jupyter_server.serverapp.ServerApp'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#c.DockerSpawner.hub_ip_connect = os.environ.get('HUB_IP', 'jupyterhub')
 | 
			
		||||
#c.DockerSpawner.hub_port_connect = 8081
 | 
			
		||||
 | 
			
		||||
def pre_spawn_hook(spawner):
 | 
			
		||||
    """Create user directories before spawning"""
 | 
			
		||||
    username = spawner.user.name 
 | 
			
		||||
    user_dir = f"./data/jupyter/users/{username}"
 | 
			
		||||
 | 
			
		||||
    import os 
 | 
			
		||||
    import stat 
 | 
			
		||||
 | 
			
		||||
    if not os.path.exists(user_dir):
 | 
			
		||||
        os.makedirs(user_dir, mode=0o755, exist_ok=True)
 | 
			
		||||
        os.chown(user_dir, 1000, 1000)
 | 
			
		||||
 | 
			
		||||
    # TODO Nbgrader dirs 
 | 
			
		||||
 | 
			
		||||
c.DockerSpawner.pre_spawn_hook = pre_spawn_hook
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
# Services configuration for NBGrader
 | 
			
		||||
c.JupyterHub.services = [
 | 
			
		||||
    {
 | 
			
		||||
        'name': 'nbgrader-formgrader',
 | 
			
		||||
        'url': 'http://127.0.0.1:9999',
 | 
			
		||||
        'command': [
 | 
			
		||||
            'jupyter-nbgrader',
 | 
			
		||||
            '--port=9999',
 | 
			
		||||
            '--no-browser',
 | 
			
		||||
            '--log-level=INFO'
 | 
			
		||||
        ],
 | 
			
		||||
        'cwd': '/srv/jupyterhub/courses',
 | 
			
		||||
        'user': 'root',
 | 
			
		||||
        'environment': {
 | 
			
		||||
            'PYTHONPATH': '/srv/nbgrader',
 | 
			
		||||
            'NBGRADER_CONFIG_FILE': '/srv/jupyterhub/nbgrader/nbgrader_config.py'
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
]
 | 
			
		||||
'''
 | 
			
		||||
'''
 | 
			
		||||
# NBGrader configuration
 | 
			
		||||
c.JupyterHub.load_groups = {
 | 
			
		||||
    'nbgrader-instructors': ['instructor'],
 | 
			
		||||
    'nbgrader-students': []
 | 
			
		||||
}
 | 
			
		||||
'''
 | 
			
		||||
'''
 | 
			
		||||
# NBGrader Config 
 | 
			
		||||
c.JupyterHub.services = [
 | 
			
		||||
    {
 | 
			
		||||
        'name': 'nbgrader-formgrader',
 | 
			
		||||
        'url': 'http://127.0.0.1:9999',
 | 
			
		||||
        'api_token': generate_api_token(prefix='nbgrader'),
 | 
			
		||||
        'command': [
 | 
			
		||||
            'python', '-m', 'nbgrader', 'formgrader',
 | 
			
		||||
            #'--port=9999',
 | 
			
		||||
            #'--no-browser',
 | 
			
		||||
            '--log-level=INFO',
 | 
			
		||||
            #'--base_url=/jupyter/services/nbgrader-formgrader',
 | 
			
		||||
            '--debug'
 | 
			
		||||
        ],
 | 
			
		||||
        'environment': {
 | 
			
		||||
            'JUPYTERHUB_SERVICE_URL': 'http://127.0.0.1:8081/jupyter/hub/api',
 | 
			
		||||
            'NBGRADER_CONFIG_FILE': '/srv/nbgrader/nbgrader_config.py',
 | 
			
		||||
            'PYTHONPATH': '/srv/nbgrader'
 | 
			
		||||
        },
 | 
			
		||||
        'cwd': '/srv/nbgrader/courses',
 | 
			
		||||
        'user': 'root'
 | 
			
		||||
    }
 | 
			
		||||
]
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
# Security settings
 | 
			
		||||
c.JupyterHub.cookie_secret_file = '/srv/jupyterhub/cookie_secret'
 | 
			
		||||
c.ConfigurableHTTPProxy.auth_token = os.environ.get('JUPYTERHUB_AUTH_TOKEN', '')
 | 
			
		||||
 | 
			
		||||
# Logging
 | 
			
		||||
c.JupyterHub.log_level = os.environ.get('LOG_LEVEL', 'INFO')
 | 
			
		||||
 | 
			
		||||
# Shutdown settings
 | 
			
		||||
c.JupyterHub.shutdown_on_logout = False
 | 
			
		||||
c.JupyterHub.cleanup_servers = True
 | 
			
		||||
 | 
			
		||||
# Timeout settings
 | 
			
		||||
c.JupyterHub.active_server_limit = 0
 | 
			
		||||
c.JupyterHub.concurrent_spawn_limit = 10
 | 
			
		||||
 | 
			
		||||
# Allow named servers
 | 
			
		||||
c.JupyterHub.allow_named_servers = True
 | 
			
		||||
c.JupyterHub.named_server_limit_per_user = 3
 | 
			
		||||
 | 
			
		||||
print("JupyterHub configuration loaded successfully!")
 | 
			
		||||
print(f"Base URL: {c.JupyterHub.base_url}")
 | 
			
		||||
print(f"Bind URL: {c.JupyterHub.bind_url}")
 | 
			
		||||
print(f"Database URL: {c.JupyterHub.db_url}")
 | 
			
		||||
print(f"Docker Network: {c.DockerSpawner.network_name}")
 | 
			
		||||
print(f"Docker Image: {c.DockerSpawner.image}")
 | 
			
		||||
print("Postgres available", is_service_available_cmd("postgres", 5432))
 | 
			
		||||
 | 
			
		||||
with open("/srv/jupyterhub/cookie_secret") as f:
 | 
			
		||||
    print(f.readlines())
 | 
			
		||||
							
								
								
									
										30
									
								
								jupyterhub/nbgrader/nbgrader_config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								jupyterhub/nbgrader/nbgrader_config.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
# /srv/nbgrader/nbgrader_config.py
 | 
			
		||||
c = get_config()
 | 
			
		||||
 | 
			
		||||
# 1. Course Configuration
 | 
			
		||||
c.CourseDirectory.course_id = "intro-to-python"  # Change to your course name
 | 
			
		||||
c.CourseDirectory.root = '/srv/nbgrader/courses'
 | 
			
		||||
 | 
			
		||||
# 2. Exchange Directory (for submitting/collecting assignments)
 | 
			
		||||
c.Exchange.root = '/srv/nbgrader/exchange'
 | 
			
		||||
c.Exchange.path_includes_course = True
 | 
			
		||||
 | 
			
		||||
# 3. JupyterHub Integration
 | 
			
		||||
#c.NbGrader.hub_url = 'http://jupyterhub:8081/hub/api'
 | 
			
		||||
#c.NbGrader.hubapi_token = 'your-hub-token'  # Generate with: openssl rand -hex 32
 | 
			
		||||
 | 
			
		||||
# 4. Database Configuration
 | 
			
		||||
c.StudentAssignmentNotebook.database_url = 'sqlite:////srv/nbgrader/nbgrader.sqlite'
 | 
			
		||||
 | 
			
		||||
# 5. Assignment Policies
 | 
			
		||||
c.ExecutePreprocessor.timeout = 300  # 5 minutes timeout per cell
 | 
			
		||||
c.ClearSolutions.code_stub = "# YOUR CODE HERE"
 | 
			
		||||
c.ClearSolutions.text_stub = "YOUR ANSWER HERE"
 | 
			
		||||
 | 
			
		||||
# 6. Formgrader UI Settings
 | 
			
		||||
c.FormgradeApp.port = 9999
 | 
			
		||||
c.FormgradeApp.authenticator_class = 'nbgrader.auth.hubauth.HubAuth'
 | 
			
		||||
c.FormgradeApp.ip = '0.0.0.0'
 | 
			
		||||
 | 
			
		||||
# 7. Permissions
 | 
			
		||||
c.CourseDirectory.groupshared = True
 | 
			
		||||
							
								
								
									
										30
									
								
								jupyterhub/nbgrader_config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								jupyterhub/nbgrader_config.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
# /srv/nbgrader/nbgrader_config.py
 | 
			
		||||
c = get_config()
 | 
			
		||||
 | 
			
		||||
# 1. Course Configuration
 | 
			
		||||
c.CourseDirectory.course_id = "intro-to-python"  # Change to your course name
 | 
			
		||||
c.CourseDirectory.root = '/srv/nbgrader/courses'
 | 
			
		||||
 | 
			
		||||
# 2. Exchange Directory (for submitting/collecting assignments)
 | 
			
		||||
c.Exchange.root = '/srv/nbgrader/exchange'
 | 
			
		||||
c.Exchange.path_includes_course = True
 | 
			
		||||
 | 
			
		||||
# 3. JupyterHub Integration
 | 
			
		||||
#c.NbGrader.hub_url = 'http://jupyterhub:8081/hub/api'
 | 
			
		||||
#c.NbGrader.hubapi_token = 'your-hub-token'  # Generate with: openssl rand -hex 32
 | 
			
		||||
 | 
			
		||||
# 4. Database Configuration
 | 
			
		||||
c.StudentAssignmentNotebook.database_url = 'sqlite:////srv/nbgrader/nbgrader.sqlite'
 | 
			
		||||
 | 
			
		||||
# 5. Assignment Policies
 | 
			
		||||
c.ExecutePreprocessor.timeout = 300  # 5 minutes timeout per cell
 | 
			
		||||
c.ClearSolutions.code_stub = "# YOUR CODE HERE"
 | 
			
		||||
c.ClearSolutions.text_stub = "YOUR ANSWER HERE"
 | 
			
		||||
 | 
			
		||||
# 6. Formgrader UI Settings
 | 
			
		||||
c.FormgradeApp.port = 9999
 | 
			
		||||
c.FormgradeApp.authenticator_class = 'nbgrader.auth.hubauth.HubAuth'
 | 
			
		||||
c.FormgradeApp.ip = '0.0.0.0'
 | 
			
		||||
 | 
			
		||||
# 7. Permissions
 | 
			
		||||
c.CourseDirectory.groupshared = True
 | 
			
		||||
							
								
								
									
										74
									
								
								jupyterhub/templates/login.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								jupyterhub/templates/login.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
{% extends "templates/page.html" %}
 | 
			
		||||
 | 
			
		||||
{% block login %}
 | 
			
		||||
<div class="container login-container">
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-md-6 col-md-offset-3">
 | 
			
		||||
            <div class="login-panel panel panel-default">
 | 
			
		||||
                <div class="panel-heading">
 | 
			
		||||
                    <h3 class="panel-title">Welcome to JupyterHub</h3>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="panel-body">
 | 
			
		||||
                    <form action="{{authenticator_login_url}}" method="post" role="form">
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="username_input">User</label>
 | 
			
		||||
                            <input
 | 
			
		||||
                                id="username_input"
 | 
			
		||||
                                type="text"
 | 
			
		||||
                                class="form-control"
 | 
			
		||||
                                name="username"
 | 
			
		||||
                                autocapitalize="off"
 | 
			
		||||
                                autocorrect="off"
 | 
			
		||||
                                required
 | 
			
		||||
                            />
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="password_input">Password</label>
 | 
			
		||||
                            <input
 | 
			
		||||
                                id="password_input"
 | 
			
		||||
                                type="password"
 | 
			
		||||
                                class="form-control"
 | 
			
		||||
                                name="password"
 | 
			
		||||
                                required
 | 
			
		||||
                            />
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <input
 | 
			
		||||
                                type="submit"
 | 
			
		||||
                                id="login_submit"
 | 
			
		||||
                                class="btn btn-jupyter btn-lg btn-block"
 | 
			
		||||
                                value="Sign In"
 | 
			
		||||
                            />
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {% if login_error %}
 | 
			
		||||
                        <div class="alert alert-danger" role="alert">
 | 
			
		||||
                            {{login_error}}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </form>
 | 
			
		||||
                    {% if authenticator.enable_signup %}
 | 
			
		||||
                    <div class="text-center">
 | 
			
		||||
                        <p>Don't have an account? <a href="{{authenticator_signup_url}}">Sign up</a></p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock login %}
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
.btn-jupyter {
 | 
			
		||||
    background-color: #FFFFFF;
 | 
			
		||||
    color: white;
 | 
			
		||||
    border-color: #F37626;
 | 
			
		||||
}
 | 
			
		||||
.btn-jupyter:hover {
 | 
			
		||||
    background-color: #D85F1C;
 | 
			
		||||
    border-color: #D85F1C;
 | 
			
		||||
}
 | 
			
		||||
.login-container {
 | 
			
		||||
    margin-top: 50px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								jupyterhub/templates/logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								jupyterhub/templates/logo.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 11 KiB  | 
							
								
								
									
										61
									
								
								jupyterhub/templates/page.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								jupyterhub/templates/page.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en" data-bs-theme="light">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="utf-8">
 | 
			
		||||
    <title>JupyterHub</title>
 | 
			
		||||
    <meta http-equiv="X-UA-Compatible" content="chrome=1">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
    
 | 
			
		||||
      <link rel="stylesheet" href="/jupyter/hub/static/css/style.min.css?v=90495ca2cd6745c4b19a42dfd4b244ac5ca697ae76bf6f58a465da54045d2e0032f25207e2ebe4df838e4d7bd40c183228f28bbacc2456fe706797438809f749" type="text/css">
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
      <link rel="icon" href="/jupyter/hub/static/favicon.ico?v=fde5757cd3892b979919d3b1faa88a410f28829feb5ba22b6cf069f2c6c98675fceef90f932e49b510e74d65c681d5846b943e7f7cc1b41867422f0481085c1f" type="image/x-icon">
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
      <script src="/jupyter/hub/static/components/bootstrap/dist/js/bootstrap.bundle.min.js?v=ecf8bfa2d7656db091f8b9d6f85ecfc057120c93ae5090773b1b441db838bd232fcef26375ee0fa35bf8051f4675cf5a5cd50d155518f922b9d70593f161741a" type="text/javascript" charset="utf-8"></script>
 | 
			
		||||
      <script src="/jupyter/hub/static/components/requirejs/require.js?v=1ff44af658602d913b22fca97c78f98945f47e76dacf9800f32f35350f05e9acda6dc710b8501579076f3980de02f02c97f5994ce1a9864c21865a42262d79ec" type="text/javascript" charset="utf-8"></script>
 | 
			
		||||
      <script src="/jupyter/hub/static/components/jquery/dist/jquery.min.js?v=bf6089ed4698cb8270a8b0c8ad9508ff886a7a842278e98064d5c1790ca3a36d5d69d9f047ef196882554fc104da2c88eb5395f1ee8cf0f3f6ff8869408350fe" type="text/javascript" charset="utf-8"></script>
 | 
			
		||||
      <script src="/jupyter/hub/static/js/darkmode.js?v=2fd9a7d11ad78df9351fed40ab35eab52e1e6a3d516f188b652120e6faf57b8e387a30aae8f52a6fb51563d06d04545c7005da0b77a98c21b0bd28f6d1cdfa11" type="text/javascript" charset="utf-8"></script>
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
    <script type="text/javascript">
 | 
			
		||||
      require.config({
 | 
			
		||||
        
 | 
			
		||||
        urlArgs: "v=20250711124117",
 | 
			
		||||
        
 | 
			
		||||
        baseUrl: '/jupyter/hub/static/js',
 | 
			
		||||
        paths: {
 | 
			
		||||
          components: '../components',
 | 
			
		||||
          jquery: '../components/jquery/dist/jquery.min',
 | 
			
		||||
          moment: "../components/moment/moment",
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      window.jhdata = {
 | 
			
		||||
        base_url: "/jupyter/hub/",
 | 
			
		||||
        prefix: "/jupyter/",
 | 
			
		||||
        
 | 
			
		||||
        
 | 
			
		||||
        admin_access: false,
 | 
			
		||||
        
 | 
			
		||||
        
 | 
			
		||||
        options_form: false,
 | 
			
		||||
        
 | 
			
		||||
        xsrf_token: "MnwxOjB8MTA6MTc1MjIzNzY4Nnw1Ol94c3JmfDY4OlRtOXVaVHA1TFdsUGJHbHhVa1pSTlZCbWFrbEdjWGhYU1dKUFNVSnVURTlVVkdsTllUQkJYMXBJWmpadGJFUjNQUT09fDlkOTc3NzE2YjVkNGVhZjZjYzE5NWFmMGI3MTE3ZjNjNzVlM2IzYmNmM2M3YWU4YjQ3ZTQ5YmVlZTk5MjYxZWQ",
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
      <meta name="description" content="JupyterHub">
 | 
			
		||||
      <meta name="keywords" content="Jupyter, JupyterHub">
 | 
			
		||||
    
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
    <noscript>
 | 
			
		||||
        <div id ='noscript'>Jupyterhub requires JavaScript.</div>   
 | 
			
		||||
    </noscript>
 | 
			
		||||
    Hello
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										82
									
								
								jupyterhub/templates/signup.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								jupyterhub/templates/signup.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
			
		||||
{% extends "templates/page.html" %}
 | 
			
		||||
 | 
			
		||||
{% block login %}
 | 
			
		||||
<div class="container login-container">
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-md-6 col-md-offset-3">
 | 
			
		||||
            <div class="login-panel panel panel-default">
 | 
			
		||||
                <div class="panel-heading">
 | 
			
		||||
                    <h3 class="panel-title">Create Account</h3>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="panel-body">
 | 
			
		||||
                    <form action="{{authenticator_signup_url}}" method="post" role="form">
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="username_input">Username</label>
 | 
			
		||||
                            <input
 | 
			
		||||
                                id="username_input"
 | 
			
		||||
                                type="text"
 | 
			
		||||
                                class="form-control"
 | 
			
		||||
                                name="username"
 | 
			
		||||
                                autocapitalize="off"
 | 
			
		||||
                                autocorrect="off"
 | 
			
		||||
                                required
 | 
			
		||||
                            />
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="password_input">Password (min. 8 characters)</label>
 | 
			
		||||
                            <input
 | 
			
		||||
                                id="password_input"
 | 
			
		||||
                                type="password"
 | 
			
		||||
                                class="form-control"
 | 
			
		||||
                                name="password"
 | 
			
		||||
                                required
 | 
			
		||||
                            />
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="password_confirm">Confirm Password</label>
 | 
			
		||||
                            <input
 | 
			
		||||
                                id="password_confirm"
 | 
			
		||||
                                type="password"
 | 
			
		||||
                                class="form-control"
 | 
			
		||||
                                name="password_confirmation"
 | 
			
		||||
                                required
 | 
			
		||||
                            />
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <input
 | 
			
		||||
                                type="submit"
 | 
			
		||||
                                id="signup_submit"
 | 
			
		||||
                                class="btn btn-jupyter btn-lg btn-block"
 | 
			
		||||
                                value="Create Account"
 | 
			
		||||
                            />
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {% if signup_error %}
 | 
			
		||||
                        <div class="alert alert-danger" role="alert">
 | 
			
		||||
                            {{signup_error}}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </form>
 | 
			
		||||
                    <div class="text-center">
 | 
			
		||||
                        <p>Already have an account? <a href="{{authenticator_login_url}}">Sign in</a></p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock login %}
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
.btn-jupyter {
 | 
			
		||||
    background-color: #F37626;
 | 
			
		||||
    color: white;
 | 
			
		||||
    border-color: #F37626;
 | 
			
		||||
}
 | 
			
		||||
.btn-jupyter:hover {
 | 
			
		||||
    background-color: #D85F1C;
 | 
			
		||||
    border-color: #D85F1C;
 | 
			
		||||
}
 | 
			
		||||
.login-container {
 | 
			
		||||
    margin-top: 50px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										15
									
								
								nginx/compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								nginx/compose.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
services:
 | 
			
		||||
  nginx:
 | 
			
		||||
    image: nginx:alpine
 | 
			
		||||
    container_name: nginx
 | 
			
		||||
    restart: unless-stopped 
 | 
			
		||||
    ports:
 | 
			
		||||
      - 8080:80
 | 
			
		||||
      - 8443:443
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
 | 
			
		||||
      - ./conf.d:/etc/nginx/conf.d:ro
 | 
			
		||||
      - ./snippets:/etc/nginx/snippets:ro
 | 
			
		||||
      - ./ssl:/etc/nginx/ssl:ro
 | 
			
		||||
      - ../logs/nginx:/etc/log/nginx
 | 
			
		||||
      - ./discord.html:/var/www/static/index.html:ro
 | 
			
		||||
							
								
								
									
										43
									
								
								nginx/conf.d/jupyterhub.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								nginx/conf.d/jupyterhub.conf
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
# Shared proxy headers
 | 
			
		||||
include /etc/nginx/snippets/proxy-headers.conf;
 | 
			
		||||
 | 
			
		||||
server {
 | 
			
		||||
    listen 80 default_server;
 | 
			
		||||
    server_name _;
 | 
			
		||||
 | 
			
		||||
    location /jupyter/ {
 | 
			
		||||
        proxy_pass http://jupyterhub:8000/;  # Docker service name
 | 
			
		||||
        
 | 
			
		||||
        # Apply shared proxy headers
 | 
			
		||||
        include /etc/nginx/snippets/proxy-headers.conf;
 | 
			
		||||
        
 | 
			
		||||
        # Special WebSocket timeout
 | 
			
		||||
        proxy_read_timeout 86400;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    # Redirect /jupyter to /jupyter/
 | 
			
		||||
    location = /jupyter {
 | 
			
		||||
        return 302 /jupyter/;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    # Stativ site conf
 | 
			
		||||
    root /var/www/static;
 | 
			
		||||
    index index.html;
 | 
			
		||||
 | 
			
		||||
    location / {
 | 
			
		||||
        try_files $uri $uri/ =404;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    # Cache static assets
 | 
			
		||||
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
 | 
			
		||||
        expires 30d;
 | 
			
		||||
        add_header Cache-Control "public, no-transform";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    # Deny access to hidden files
 | 
			
		||||
    location ~ /\. {
 | 
			
		||||
        deny all;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										360
									
								
								nginx/discord.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										360
									
								
								nginx/discord.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,360 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
    <title>Discord Mod Simulator 2025</title>
 | 
			
		||||
    <style>
 | 
			
		||||
        body {
 | 
			
		||||
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 | 
			
		||||
            background-color: #36393f;
 | 
			
		||||
            color: #dcddde;
 | 
			
		||||
            max-width: 800px;
 | 
			
		||||
            margin: 0 auto;
 | 
			
		||||
            padding: 20px;
 | 
			
		||||
        }
 | 
			
		||||
        #game-container {
 | 
			
		||||
            background-color: #2f3136;
 | 
			
		||||
            border-radius: 8px;
 | 
			
		||||
            padding: 20px;
 | 
			
		||||
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
 | 
			
		||||
        }
 | 
			
		||||
        #status-bar {
 | 
			
		||||
            display: flex;
 | 
			
		||||
            justify-content: space-between;
 | 
			
		||||
            margin-bottom: 20px;
 | 
			
		||||
            padding: 10px;
 | 
			
		||||
            background-color: #202225;
 | 
			
		||||
            border-radius: 5px;
 | 
			
		||||
        }
 | 
			
		||||
        .stat {
 | 
			
		||||
            display: flex;
 | 
			
		||||
            flex-direction: column;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
        }
 | 
			
		||||
        #event-display {
 | 
			
		||||
            min-height: 150px;
 | 
			
		||||
            padding: 15px;
 | 
			
		||||
            background-color: #40444b;
 | 
			
		||||
            border-radius: 5px;
 | 
			
		||||
            margin-bottom: 20px;
 | 
			
		||||
            white-space: pre-wrap;
 | 
			
		||||
        }
 | 
			
		||||
        #actions {
 | 
			
		||||
            display: grid;
 | 
			
		||||
            grid-template-columns: 1fr 1fr;
 | 
			
		||||
            gap: 10px;
 | 
			
		||||
        }
 | 
			
		||||
        button {
 | 
			
		||||
            background-color: #5865f2;
 | 
			
		||||
            color: white;
 | 
			
		||||
            border: none;
 | 
			
		||||
            padding: 10px;
 | 
			
		||||
            border-radius: 5px;
 | 
			
		||||
            cursor: pointer;
 | 
			
		||||
            transition: background-color 0.2s;
 | 
			
		||||
        }
 | 
			
		||||
        button:hover {
 | 
			
		||||
            background-color: #4752c4;
 | 
			
		||||
        }
 | 
			
		||||
        button:disabled {
 | 
			
		||||
            background-color: #4f545c;
 | 
			
		||||
            cursor: not-allowed;
 | 
			
		||||
        }
 | 
			
		||||
        #log {
 | 
			
		||||
            margin-top: 20px;
 | 
			
		||||
            max-height: 200px;
 | 
			
		||||
            overflow-y: auto;
 | 
			
		||||
            padding: 10px;
 | 
			
		||||
            background-color: #40444b;
 | 
			
		||||
            border-radius: 5px;
 | 
			
		||||
            font-size: 0.9em;
 | 
			
		||||
        }
 | 
			
		||||
        .log-entry {
 | 
			
		||||
            margin-bottom: 5px;
 | 
			
		||||
            border-bottom: 1px solid #4f545c;
 | 
			
		||||
            padding-bottom: 5px;
 | 
			
		||||
        }
 | 
			
		||||
        .positive {
 | 
			
		||||
            color: #3ba55c;
 | 
			
		||||
        }
 | 
			
		||||
        .negative {
 | 
			
		||||
            color: #ed4245;
 | 
			
		||||
        }
 | 
			
		||||
        .warning {
 | 
			
		||||
            color: #faa61a;
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
    <div id="game-container">
 | 
			
		||||
        <h1>Discord Mod Simulator 2025</h1>
 | 
			
		||||
        <div id="status-bar">
 | 
			
		||||
            <div class="stat">
 | 
			
		||||
                <span>Members</span>
 | 
			
		||||
                <span id="members">1000</span>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="stat">
 | 
			
		||||
                <span>Activity</span>
 | 
			
		||||
                <span id="activity">50%</span>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="stat">
 | 
			
		||||
                <span>Reputation</span>
 | 
			
		||||
                <span id="reputation">50%</span>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="stat">
 | 
			
		||||
                <span>Day</span>
 | 
			
		||||
                <span id="day">1</span>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <div id="event-display">
 | 
			
		||||
            Welcome to Discord Mod Simulator 2025! As a moderator, you must balance keeping the server active while maintaining order. Make your decisions carefully!
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <div id="actions">
 | 
			
		||||
            <!-- Buttons will be generated by JavaScript -->
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <div id="log">
 | 
			
		||||
            <div class="log-entry">Game started. Good luck!</div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <script>
 | 
			
		||||
        // Game state
 | 
			
		||||
        const gameState = {
 | 
			
		||||
            members: 1000,
 | 
			
		||||
            activity: 50,
 | 
			
		||||
            reputation: 50,
 | 
			
		||||
            day: 1,
 | 
			
		||||
            powerUps: {
 | 
			
		||||
                adminBackup: 3,
 | 
			
		||||
                muteAll: 2,
 | 
			
		||||
                customRole: 3
 | 
			
		||||
            },
 | 
			
		||||
            gameOver: false
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Event database
 | 
			
		||||
        const events = [
 | 
			
		||||
            {
 | 
			
		||||
                id: 'spam',
 | 
			
		||||
                text: "User xX_SpamLord_Xx is posting rapid-fire memes in #general. What do you do?",
 | 
			
		||||
                options: [
 | 
			
		||||
                    { text: "Delete messages and warn", members: 0, activity: -5, reputation: 5 },
 | 
			
		||||
                    { text: "Timeout for 1 hour", members: -10, activity: -10, reputation: 10 },
 | 
			
		||||
                    { text: "Ban the user", members: -30, activity: -15, reputation: 15 },
 | 
			
		||||
                    { text: "Ignore it", members: 5, activity: 10, reputation: -10 }
 | 
			
		||||
                ]
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                id: 'argument',
 | 
			
		||||
                text: "Two users are having a heated argument about which year had the best memes. It's getting personal.",
 | 
			
		||||
                options: [
 | 
			
		||||
                    { text: "Warn both users", members: 0, activity: -5, reputation: 5 },
 | 
			
		||||
                    { text: "Timeout both for 30 minutes", members: -15, activity: -15, reputation: 10 },
 | 
			
		||||
                    { text: "Create #debate channel and move them", members: 10, activity: 5, reputation: 15 },
 | 
			
		||||
                    { text: "Let them fight", members: -20, activity: 20, reputation: -20 }
 | 
			
		||||
                ]
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                id: 'raid',
 | 
			
		||||
                text: "OH NO! The server is being raided by another community! Spam and offensive content is flooding in!",
 | 
			
		||||
                options: [
 | 
			
		||||
                    { text: "Use Mute All power-up (stops raid but lowers activity)", powerUp: 'muteAll', members: -50, activity: -30, reputation: 20 },
 | 
			
		||||
                    { text: "Call Admin Backup", powerUp: 'adminBackup', members: -20, activity: -10, reputation: 10 },
 | 
			
		||||
                    { text: "Ban raiders manually", members: -100, activity: -40, reputation: 5 },
 | 
			
		||||
                    { text: "Do nothing (risky!)", members: -150, activity: -50, reputation: -30 }
 | 
			
		||||
                ]
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                id: 'feature',
 | 
			
		||||
                text: "A user suggests a great feature for the server that would require some moderator work to implement.",
 | 
			
		||||
                options: [
 | 
			
		||||
                    { text: "Implement the feature", members: 20, activity: 15, reputation: 10 },
 | 
			
		||||
                    { text: "Say you'll consider it", members: 5, activity: 5, reputation: 5 },
 | 
			
		||||
                    { text: "Reject the idea", members: -10, activity: -5, reputation: -5 },
 | 
			
		||||
                    { text: "Create a poll for the community", members: 10, activity: 20, reputation: 15 }
 | 
			
		||||
                ]
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                id: 'bot',
 | 
			
		||||
                text: "The moderation bot is malfunctioning! It's randomly timing out users and missing actual rule violations.",
 | 
			
		||||
                options: [
 | 
			
		||||
                    { text: "Fix the bot (takes time)", members: -10, activity: -10, reputation: 20 },
 | 
			
		||||
                    { text: "Disable the bot temporarily", members: 0, activity: 10, reputation: -10 },
 | 
			
		||||
                    { text: "Blame the bot developer", members: -20, activity: -20, reputation: -20 },
 | 
			
		||||
                    { text: "Use Custom Roles to help moderate", powerUp: 'customRole', members: 0, activity: 0, reputation: 15 }
 | 
			
		||||
                ]
 | 
			
		||||
            }
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        // DOM elements
 | 
			
		||||
        const membersEl = document.getElementById('members');
 | 
			
		||||
        const activityEl = document.getElementById('activity');
 | 
			
		||||
        const reputationEl = document.getElementById('reputation');
 | 
			
		||||
        const dayEl = document.getElementById('day');
 | 
			
		||||
        const eventDisplayEl = document.getElementById('event-display');
 | 
			
		||||
        const actionsEl = document.getElementById('actions');
 | 
			
		||||
        const logEl = document.getElementById('log');
 | 
			
		||||
 | 
			
		||||
        // Update the UI with current game state
 | 
			
		||||
        function updateUI() {
 | 
			
		||||
            membersEl.textContent = gameState.members;
 | 
			
		||||
            activityEl.textContent = `${gameState.activity}%`;
 | 
			
		||||
            reputationEl.textContent = `${gameState.reputation}%`;
 | 
			
		||||
            dayEl.textContent = gameState.day;
 | 
			
		||||
            
 | 
			
		||||
            // Update colors based on values
 | 
			
		||||
            activityEl.className = gameState.activity < 30 ? 'negative' : gameState.activity > 70 ? 'positive' : '';
 | 
			
		||||
            reputationEl.className = gameState.reputation < 30 ? 'negative' : gameState.reputation > 70 ? 'positive' : '';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Add a message to the log
 | 
			
		||||
        function addLog(message, type = '') {
 | 
			
		||||
            const entry = document.createElement('div');
 | 
			
		||||
            entry.className = `log-entry ${type}`;
 | 
			
		||||
            entry.textContent = `Day ${gameState.day}: ${message}`;
 | 
			
		||||
            logEl.appendChild(entry);
 | 
			
		||||
            logEl.scrollTop = logEl.scrollHeight;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Present a random event to the player
 | 
			
		||||
        function presentEvent() {
 | 
			
		||||
            if (gameState.gameOver) return;
 | 
			
		||||
            
 | 
			
		||||
            const event = events[Math.floor(Math.random() * events.length)];
 | 
			
		||||
            eventDisplayEl.textContent = event.text;
 | 
			
		||||
            
 | 
			
		||||
            // Clear previous buttons
 | 
			
		||||
            actionsEl.innerHTML = '';
 | 
			
		||||
            
 | 
			
		||||
            // Create new buttons for each option
 | 
			
		||||
            event.options.forEach((option, index) => {
 | 
			
		||||
                const button = document.createElement('button');
 | 
			
		||||
                button.textContent = option.text;
 | 
			
		||||
                
 | 
			
		||||
                // Check if this option requires a power-up we don't have
 | 
			
		||||
                if (option.powerUp && gameState.powerUps[option.powerUp] <= 0) {
 | 
			
		||||
                    button.disabled = true;
 | 
			
		||||
                    button.title = "No more uses left!";
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                button.addEventListener('click', () => {
 | 
			
		||||
                    handleChoice(option, event);
 | 
			
		||||
                });
 | 
			
		||||
                
 | 
			
		||||
                actionsEl.appendChild(button);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Handle player's choice
 | 
			
		||||
        function handleChoice(option, event) {
 | 
			
		||||
            // Use power-up if needed
 | 
			
		||||
            if (option.powerUp) {
 | 
			
		||||
                gameState.powerUps[option.powerUp]--;
 | 
			
		||||
                addLog(`Used ${option.powerUp} power-up.`, 'warning');
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // Update game state
 | 
			
		||||
            gameState.members += option.members || 0;
 | 
			
		||||
            gameState.activity += option.activity || 0;
 | 
			
		||||
            gameState.reputation += option.reputation || 0;
 | 
			
		||||
            
 | 
			
		||||
            // Ensure values stay within bounds
 | 
			
		||||
            gameState.members = Math.max(0, gameState.members);
 | 
			
		||||
            gameState.activity = Math.max(0, Math.min(100, gameState.activity));
 | 
			
		||||
            gameState.reputation = Math.max(0, Math.min(100, gameState.reputation));
 | 
			
		||||
            
 | 
			
		||||
            // Log the result
 | 
			
		||||
            let resultMessage = `You chose: ${option.text}. `;
 | 
			
		||||
            if (option.members !== 0) {
 | 
			
		||||
                resultMessage += `Members ${option.members > 0 ? '+' : ''}${option.members}. `;
 | 
			
		||||
            }
 | 
			
		||||
            if (option.activity !== 0) {
 | 
			
		||||
                resultMessage += `Activity ${option.activity > 0 ? '+' : ''}${option.activity}%. `;
 | 
			
		||||
            }
 | 
			
		||||
            if (option.reputation !== 0) {
 | 
			
		||||
                resultMessage += `Reputation ${option.reputation > 0 ? '+' : ''}${option.reputation}%. `;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            addLog(resultMessage.trim());
 | 
			
		||||
            
 | 
			
		||||
            // Advance to next day
 | 
			
		||||
            gameState.day++;
 | 
			
		||||
            
 | 
			
		||||
            // Check win/lose conditions
 | 
			
		||||
            checkGameState();
 | 
			
		||||
            
 | 
			
		||||
            // Update UI and present new event
 | 
			
		||||
            updateUI();
 | 
			
		||||
            
 | 
			
		||||
            if (!gameState.gameOver) {
 | 
			
		||||
                setTimeout(presentEvent, 1000);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check win/lose conditions
 | 
			
		||||
        function checkGameState() {
 | 
			
		||||
            if (gameState.members >= 10000 && gameState.reputation >= 75) {
 | 
			
		||||
                eventDisplayEl.textContent = "CONGRATULATIONS! You've grown the server to 10,000 members with great reputation! You win!";
 | 
			
		||||
                gameState.gameOver = true;
 | 
			
		||||
                actionsEl.innerHTML = '<button onclick="location.reload()">Play Again</button>';
 | 
			
		||||
                addLog("YOU WIN! Server is thriving!", 'positive');
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            if (gameState.members <= 100) {
 | 
			
		||||
                eventDisplayEl.textContent = "GAME OVER! The server has died from lack of members. Maybe you were too strict?";
 | 
			
		||||
                gameState.gameOver = true;
 | 
			
		||||
                actionsEl.innerHTML = '<button onclick="location.reload()">Play Again</button>';
 | 
			
		||||
                addLog("Game over - server died.", 'negative');
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            if (gameState.reputation <= 20) {
 | 
			
		||||
                eventDisplayEl.textContent = "GAME OVER! The community revolted against your moderation and you were demoted.";
 | 
			
		||||
                gameState.gameOver = true;
 | 
			
		||||
                actionsEl.innerHTML = '<button onclick="location.reload()">Play Again</button>';
 | 
			
		||||
                addLog("Game over - you were demoted.", 'negative');
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            if (gameState.reputation >= 90 && Math.random() < 0.3) {
 | 
			
		||||
                addLog("The server owner promoted you to Admin for your great work!", 'positive');
 | 
			
		||||
                gameState.powerUps.adminBackup += 2;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // Random member growth based on reputation and activity
 | 
			
		||||
            const growthFactor = (gameState.reputation / 100) * (gameState.activity / 100);
 | 
			
		||||
            const randomGrowth = Math.floor(Math.random() * 20 * growthFactor);
 | 
			
		||||
            if (randomGrowth > 0) {
 | 
			
		||||
                gameState.members += randomGrowth;
 | 
			
		||||
                addLog(`Server grew by ${randomGrowth} members organically!`, 'positive');
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // Random events
 | 
			
		||||
            if (Math.random() < 0.2) {
 | 
			
		||||
                const randomEvent = Math.random();
 | 
			
		||||
                if (randomEvent < 0.3) {
 | 
			
		||||
                    // Good event
 | 
			
		||||
                    gameState.reputation += 5;
 | 
			
		||||
                    addLog("A user complimented your moderation style in #feedback!", 'positive');
 | 
			
		||||
                } else if (randomEvent < 0.6) {
 | 
			
		||||
                    // Bad event
 | 
			
		||||
                    gameState.activity -= 10;
 | 
			
		||||
                    addLog("A popular meme channel went quiet for a day.", 'negative');
 | 
			
		||||
                } else {
 | 
			
		||||
                    // Neutral event
 | 
			
		||||
                    addLog("Nothing particularly interesting happened today.");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Initialize the game
 | 
			
		||||
        updateUI();
 | 
			
		||||
        presentEvent();
 | 
			
		||||
    </script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										30
									
								
								nginx/nginx.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								nginx/nginx.conf
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
user  nginx;
 | 
			
		||||
worker_processes  auto;
 | 
			
		||||
 | 
			
		||||
error_log  /var/log/nginx/error.log warn;
 | 
			
		||||
pid        /var/run/nginx.pid;
 | 
			
		||||
 | 
			
		||||
events {
 | 
			
		||||
    worker_connections  1024;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
http {
 | 
			
		||||
    include       /etc/nginx/mime.types;
 | 
			
		||||
    default_type  application/octet-stream;
 | 
			
		||||
 | 
			
		||||
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
 | 
			
		||||
                      '$status $body_bytes_sent "$http_referer" '
 | 
			
		||||
                      '"$http_user_agent" "$http_x_forwarded_for"';
 | 
			
		||||
 | 
			
		||||
    access_log  /var/log/nginx/access.log  main;
 | 
			
		||||
 | 
			
		||||
    sendfile        on;
 | 
			
		||||
    keepalive_timeout  65;
 | 
			
		||||
 | 
			
		||||
    # Gzip compression
 | 
			
		||||
    gzip  on;
 | 
			
		||||
    gzip_types text/plain text/css application/json application/javascript text/xml;
 | 
			
		||||
 | 
			
		||||
    # Include modular configs
 | 
			
		||||
    include /etc/nginx/conf.d/*.conf;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								nginx/snippets/proxy-headers.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								nginx/snippets/proxy-headers.conf
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
proxy_set_header Host $host;
 | 
			
		||||
proxy_set_header X-Real-IP $remote_addr;
 | 
			
		||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 | 
			
		||||
proxy_set_header X-Forwarded-Proto $scheme;
 | 
			
		||||
 | 
			
		||||
# WebSocket support
 | 
			
		||||
proxy_http_version 1.1;
 | 
			
		||||
proxy_set_header Upgrade $http_upgrade;
 | 
			
		||||
proxy_set_header Connection "upgrade";
 | 
			
		||||
							
								
								
									
										22
									
								
								postgres/compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								postgres/compose.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
services:
 | 
			
		||||
  postgres:
 | 
			
		||||
    image: postgres:15-alpine 
 | 
			
		||||
    container_name: postgres
 | 
			
		||||
    environment:
 | 
			
		||||
      POSTGRES_HOST: postgres
 | 
			
		||||
      POSTGRES_USER: ${POSTGRES_USER:-postgres}
 | 
			
		||||
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}  
 | 
			
		||||
      POSTGRES_DB: ${POSTGRES_DB:-postgres}        
 | 
			
		||||
    ports:
 | 
			
		||||
      - 5432:5432  
 | 
			
		||||
    volumes:
 | 
			
		||||
      - postgres_data:/var/lib/postgresql/data  
 | 
			
		||||
    restart: unless-stopped  
 | 
			
		||||
    #healthcheck:
 | 
			
		||||
    #  test: ["CMD-SHELL", "pg_isready -U postgres"]
 | 
			
		||||
    #  interval: 5s
 | 
			
		||||
    #  timeout: 5s
 | 
			
		||||
    #  retries: 5
 | 
			
		||||
 | 
			
		||||
volumes:
 | 
			
		||||
  postgres_data:  # Named volume for data persistence
 | 
			
		||||
							
								
								
									
										0
									
								
								postgres/init-multiple-db.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								postgres/init-multiple-db.sh
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										6
									
								
								postgres/postgresql.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								postgres/postgresql.conf
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
log_destination = 'stderr'
 | 
			
		||||
logging_collector = on
 | 
			
		||||
log_directory = '/var/log/postgres'
 | 
			
		||||
log_filename = 'postgresql-%Y-%m-%d.log'
 | 
			
		||||
log_statement = 'all'
 | 
			
		||||
log_rotation_age = 1d
 | 
			
		||||
		Reference in New Issue
	
	Block a user