Added Nbgrader
This commit is contained in:
parent
a6db921bea
commit
d7e7733166
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
Normal file
61
flake.lock
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
|
Loading…
Reference in New Issue
Block a user