diff --git a/.DS_Store b/.DS_Store index 0503454..7f4c47e 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.env.example b/.env.example index b0919f5..9f5a7cf 100644 --- a/.env.example +++ b/.env.example @@ -45,9 +45,9 @@ BOOKSTACK_APP_KEY=base64:YOUR_BOOKSTACK_KEY_HERE # ======================================== DB_HOST=bookstack-mariadb DB_PORT=3306 -DB_DATABASE=bookstack -DB_USERNAME=bookstack -DB_PASSWORD=your_secure_database_password_here +BOOKSTACK_DB_DATABASE=bookstack +BOOKSTACK_DB_USERNAME=bookstack +BOOKSTACK_DB_PASSWORD=your_secure_database_password_here # MariaDB root password MARIADB_ROOT_PASSWORD=your_secure_root_password_here diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index df0c438..dfc408c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -71,6 +71,13 @@ BOOKSTACK_PORT=6875 # BookStack/DB configs (see docker-compose.yml for full list) ``` +Note: this repository also contains a local `.env` with example credentials (do NOT commit real secrets). Current repository `.env` contents: +```properties +MATRIX_USER=einszwovier_bot +MATRIX_PASS=einszwo4 +``` +Prefer injecting secrets via CI environment variables or a secure vault for production. Add `MATRIX_ROOM` and/or `MATRIX_HOMESERVER` when testing Matrix flows. + ### Debugging Matrix Integration Use `get_room_id.py` to discover Matrix room IDs: ```python diff --git a/.gitignore b/.gitignore index e3cb230..f5e2f12 100644 --- a/.gitignore +++ b/.gitignore @@ -103,3 +103,5 @@ dmypy.json *.db-wal *.sqlite *.sqlite3 +.github +.venv diff --git a/PORTAINER_QUICKREF.md b/PORTAINER_QUICKREF.md deleted file mode 100644 index c1d8d9c..0000000 --- a/PORTAINER_QUICKREF.md +++ /dev/null @@ -1,231 +0,0 @@ -# Portainer Quick Reference - -## 🚀 Quick Start - -### Access Portainer -``` -http://localhost:9000 -or -http://einszwovier.local:9000 -``` - -### First Login -1. Create admin account -2. Select "Local" environment -3. Done! - -## 📊 Stack Overview: studio-einszwovier - -| Service | Port | Health Check | Purpose | -|---------|------|--------------|---------| -| web | 80 | ✅ | PDF Cost Calculator | -| bookstack | 6875 | ✅ | Documentation Wiki | -| bookstack-mariadb | - | ✅ | Database | -| synapse | 8008 | ✅ | Matrix Server | -| ollama | 11434 | ✅ | Local LLM | -| open-webui | 8080 | ✅ | LLM Interface | -| watchtower | - | ✅ | Auto-Updates | -| portainer | 9000 | ✅ | Management UI | - -## 🎯 Common Tasks - -### View All Services -**Portainer**: Stacks → studio-einszwovier -**CLI**: `docker-compose ps` - -### Restart a Service -**Portainer**: Containers → [service] → Restart -**CLI**: `docker-compose restart [service]` - -### View Logs -**Portainer**: Containers → [service] → Logs -**CLI**: `docker-compose logs -f [service]` - -### Check Health Status -**Portainer**: Containers → Look for 🟢/🔴 indicator -**CLI**: `docker-compose ps` (shows (healthy) or (unhealthy)) - -### Stop Everything -**Portainer**: Stacks → studio-einszwovier → Stop -**CLI**: `docker-compose stop` - -### Start Everything -**Portainer**: Stacks → studio-einszwovier → Start -**CLI**: `docker-compose start` - -### Update a Service -**Portainer**: Containers → [service] → Recreate -**CLI**: `docker-compose pull [service] && docker-compose up -d [service]` - -## 🔍 Troubleshooting - -### Service Shows 🔴 Unhealthy -1. Click container → Logs -2. Look for errors -3. Check health check output in Inspect tab -4. Restart if needed - -### Can't Access Portainer -```bash -docker-compose restart portainer -# Wait 30 seconds -# Try again at http://localhost:9000 -``` - -### Service Won't Start -1. Check Portainer logs for the service -2. Look for dependency issues (red containers) -3. Check resource limits (Stats tab) -4. Verify environment variables in Container → Env - -## 📋 Health Check Endpoints - -Test manually if needed: - -```bash -# Web App -curl http://localhost/ - -# BookStack -curl http://localhost:6875/ - -# Matrix Synapse -curl http://localhost:8008/health - -# Ollama -curl http://localhost:11434/api/tags - -# Open WebUI -curl http://localhost:8080/ - -# Portainer -curl http://localhost:9000/api/system/status -``` - -## 🏷️ Labels in Portainer - -All services are tagged with: -- **description**: What it does -- **maintainer**: Studio EinsZwoVier -- **watchtower.enable**: Auto-update enabled - -Filter by labels in Portainer UI! - -## 🔄 Auto-Updates (Watchtower) - -- **Schedule**: Every 24 hours -- **Action**: Pulls latest images and recreates containers -- **Which services**: Only those with `watchtower.enable=true` label -- **View updates**: Containers → watchtower → Logs - -### Disable Auto-Update for a Service -Edit docker-compose.yml, remove: -```yaml -labels: - - "com.centurylinklabs.watchtower.enable=true" -``` - -## 📈 Resource Monitoring - -**Portainer**: Container → Stats -Shows: -- CPU usage (%) -- Memory usage (MB / GB) -- Network I/O -- Block I/O - -**Limits Set**: -- web: 1GB RAM, 1 CPU -- ollama: 8GB RAM, 4 CPU - -## 🌐 Network: einszwovier_network - -All services communicate on this network. - -**View**: Networks → einszwovier_network → Connected containers - -## 💾 Volumes - -| Volume | Used By | Purpose | -|--------|---------|---------| -| portainer_data | portainer | Portainer config | -| ./data/uploads | web | PDF uploads | -| ./bookstack/* | bookstack | Wiki data | -| ./matrix/data | synapse | Matrix data | -| ./ollama | ollama | LLM models | -| ./open-webui | open-webui | Chat history | - -**Backup**: See `backup.sh` script - -## ⚙️ Useful Portainer Features - -### Execute Shell Commands -1. Containers → [service] → Console -2. Select `/bin/bash` or `/bin/sh` -3. Click Connect -4. Run commands - -### View Container Config -1. Containers → [service] → Inspect -2. See full JSON configuration -3. Copy environment variables -4. Check volume mounts - -### Duplicate Container -1. Containers → [service] → Duplicate/Edit -2. Modify settings -3. Deploy as new container - -### Container Template -1. App Templates → Custom Templates -2. Create from existing container -3. Reuse configuration - -## 🎨 Status Icons - -| Icon | Meaning | Action | -|------|---------|--------| -| 🟢 | Healthy | None needed | -| 🟡 | Starting | Wait (within start_period) | -| 🔴 | Unhealthy | Check logs | -| ⚫ | Stopped | Start container | -| 🔄 | Restarting | Wait or investigate | - -## 📞 Quick Help - -### "I can't find my containers!" -- Go to **Home** -- Select **Local** environment -- Go to **Stacks** → studio-einszwovier - -### "Health checks keep failing" -- Increase `start_period` in docker-compose.yml -- Check service logs for actual errors -- Verify network connectivity - -### "Portainer shows wrong status" -- Refresh page (F5) -- Check "Last Updated" timestamp -- Restart Portainer if stale - -## 🔐 Security Notes - -- Portainer admin password is set on first login (save it!) -- Docker socket mounted = full control (use carefully) -- HTTPS available on port 9443 (configure in Portainer settings) - -## 📱 Mobile Access - -Portainer works great on mobile: -1. Open browser on phone -2. Navigate to `http://[server-ip]:9000` -3. Same features as desktop! - -## ✅ Daily Checklist - -- [ ] All services showing 🟢 healthy -- [ ] No unusual CPU/memory spikes -- [ ] Recent watchtower update logs look good -- [ ] No red error logs in critical services - -**Portainer makes this a 30-second check!** diff --git a/README.md b/README.md index 2465c2b..0c88c01 100644 --- a/README.md +++ b/README.md @@ -11,17 +11,21 @@ ## 🎯 Features ### Core Application + - **📄 PDF Cost Calculator**: Automated ink coverage analysis with color/B&W detection - **🖼️ Course Management**: Dynamic course list with image gallery and modal viewer - **📊 Cost Estimation**: Per-page breakdown with configurable rates (€/m²) - **💬 Matrix Integration**: Automatic order submission to Matrix room ### Integrated Services + - **📚 BookStack Wiki**: Documentation and knowledge base (port 6875) - **🤖 Ollama + Open WebUI**: Local LLM chatbot interface (port 8080) - **📨 Matrix Synapse**: Print order collection and payment tracking (port 8008) - **🐳 Portainer**: Container management dashboard (port 9000) -- **🔄 Watchtower**: Automatic container updates every 24 hours +- **� JupyterHub**: Multi-user Jupyter notebook server for drone programming (port 8001) +- **🦊 Forgejo**: Self-hosted Git service for code collaboration (port 3003) +- **�🔄 Watchtower**: Automatic container updates every 24 hours --- @@ -74,35 +78,55 @@ graph TB OpenWebUI[💭 Open WebUI
:8080] Portainer[🐳 Portainer
:9000] Synapse[📨 Matrix Synapse
:8008] + JupyterHub[📓 JupyterHub
:8001
Drone Programming] + Forgejo[🦊 Forgejo Git
:3003
Code Collaboration] LP -.-> BookStack LP -.-> OpenWebUI LP -.-> Portainer + LP -.-> JupyterHub + LP -.-> Forgejo OpenWebUI --> Ollama MatrixRoom --> Synapse end + subgraph "Educational Stack" + Notebooks[📒 Jupyter Notebooks
Python Drone Code] + GitRepos[📦 Git Repositories
Project Source Code] + DronePackages[🚁 djitellopy
Tello SDK] + + JupyterHub --> Notebooks + Forgejo --> GitRepos + Notebooks -.-> DronePackages + end + subgraph "Data Persistence" Uploads[📁 data/uploads/
PDF Files] Courses[📋 data/courses.csv
Course List] MatrixDB[(🗄️ Matrix DB
homeserver.db)] BookDB[(🗄️ BookStack DB
MariaDB)] + JupyterVolumes[(💾 Jupyter Volumes
User Workspaces)] + ForgejoData[(📚 Forgejo Data
Git Repos)] Upload --> Uploads CSV --> Courses Synapse --> MatrixDB BookStack --> BookDB + JupyterHub --> JupyterVolumes + Forgejo --> ForgejoData end classDef webapp fill:#e3f2fd,stroke:#1976d2,stroke-width:2px classDef service fill:#fff3e0,stroke:#f57c00,stroke-width:2px classDef data fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px classDef matrix fill:#e8f5e9,stroke:#388e3c,stroke-width:2px + classDef edu fill:#fce4ec,stroke:#c2185b,stroke-width:2px class LP,About,Cost,Upload,Analysis,Quote webapp - class BookStack,Ollama,OpenWebUI,Portainer,Synapse service - class Uploads,Courses,MatrixDB,BookDB data + class BookStack,Ollama,OpenWebUI,Portainer,Synapse,JupyterHub,Forgejo service + class Uploads,Courses,MatrixDB,BookDB,JupyterVolumes,ForgejoData data class MatrixRoom,PDF_Upload,Summary,CSV,Images,Modal matrix + class Notebooks,GitRepos,DronePackages edu ``` --- @@ -113,47 +137,55 @@ graph TB - **Docker** & **Docker Compose** installed - **Poppler** (for pdf2image - included in Docker) -- **Port availability**: 80, 8008, 8080, 6875, 9000, 11434 +- **Port availability**: 80, 3003, 6875, 8001, 8008, 8080, 9000, 11434 ### Installation 1. **Clone the repository:** + ```bash git clone https://github.com/arontaupe/124-webapp.git cd 124-webapp ``` 2. **Configure environment:** + ```bash cp .env.example .env nano .env # Update SERVER_HOSTNAME and passwords ``` 3. **Start the stack:** + ```bash docker compose up -d --build ``` 4. **Access services:** - - Web App: http://localhost (or http://your-server) - - BookStack: http://localhost:6875 - - Open WebUI: http://localhost:8080 - - Portainer: http://localhost:9000 - - Matrix: http://localhost:8008 + - Web App: (or ) + - BookStack: + - Open WebUI: + - Portainer: + - Matrix: + - JupyterHub: + - Forgejo: ### First-Time Setup 1. **Get Matrix Room ID:** + ```bash python get_room_id.py ``` 2. **Update .env with room ID:** + ```bash MATRIX_ROOM="!YourRoomID:${SERVER_HOSTNAME}" ``` 3. **Restart web container:** + ```bash docker compose restart web ``` @@ -167,12 +199,14 @@ Courses are managed via CSV file with optional images: ### Add a Course 1. **Edit `data/courses.csv`:** + ```csv title,description,dates,offen_fuer,image My Course,Learn cool stuff,"Oct '25",Grade 10,/static/images/courses/my-course.jpg ``` 2. **Add course image (optional):** + ```bash cp my-image.jpg static/images/courses/my-course.jpg ``` @@ -180,6 +214,7 @@ Courses are managed via CSV file with optional images: 3. **Changes apply immediately** (no restart needed) ### Course Image Features + - **Thumbnail view**: 80x80px next to course info - **Hover zoom**: Expands to 200x200px on hover - **Click to enlarge**: Opens full-size modal viewer @@ -215,6 +250,11 @@ OLLAMA_PORT=11434 OPENWEBUI_PORT=8080 BOOKSTACK_PORT=6875 PORTAINER_PORT=9000 +JUPYTER_PORT=8001 +FORGEJO_PORT=3003 + +# JupyterHub Authentication +JUPYTER_PASSWORD=einszwo4 ``` See [.env.example](.env.example) for full configuration. @@ -231,12 +271,15 @@ See [.env.example](.env.example) for full configuration. | **open-webui** | ghcr.io/open-webui/open-webui | 8080 | LLM chat UI | 2 CPU, 2GB RAM | | **bookstack** | lscr.io/linuxserver/bookstack | 6875 | Documentation wiki | Default | | **bookstack-mariadb** | lscr.io/linuxserver/mariadb | 3306 | Database for BookStack | Default | +| **jupyterhub** | Custom (Python 3.13) | 8001 | Multi-user Jupyter notebooks | 2 CPU, 2GB RAM | +| **forgejo** | codeberg.org/forgejo/forgejo:11 | 3003, 222 | Git service (web, SSH) | Default | | **portainer** | portainer/portainer-ce | 9000, 9443 | Container management | Default | | **watchtower** | containrrr/watchtower | - | Auto-updates (24h) | Default | ### Health Checks All services have health checks configured: + - **web**: `curl http://localhost:8000/` - **synapse**: `curl http://localhost:8008/_matrix/static/` - **ollama**: `curl http://localhost:11434/` @@ -283,6 +326,7 @@ See [SECURITY.md](SECURITY.md) for comprehensive security guidelines. ``` Creates timestamped backups of: + - BookStack database & files - Matrix homeserver data - PDF uploads @@ -302,11 +346,13 @@ Creates timestamped backups of: This application is designed to be **fully portable**. To move to a new server: 1. **On old server:** + ```bash ./backup.sh ``` 2. **On new server:** + ```bash git clone cd 124-webapp @@ -393,7 +439,7 @@ This project is part of Studio EinsZwoVier educational initiative. Gabriele-von-Bülow-Gymnasium Tile-Brügge-Weg 63, 13509 Berlin (Tegel) -- **Email**: einszwovier@gvb-gymnasium.de +- **Email**: - **Team**: Aron Petau & Friedrich Weber - **Hours**: Tuesday - Thursday, 11:00 - 16:00 - **Location**: Room 124 diff --git a/backup.sh b/backup.sh index 1aa0355..e2404b8 100755 --- a/backup.sh +++ b/backup.sh @@ -19,9 +19,9 @@ echo "Working directory: $SCRIPT_DIR" # 1. Backup BookStack database echo "Backing up BookStack database..." docker exec bookstack-mariadb mariadb-dump \ - -u"${DB_USERNAME:-bookstack}" \ - -p"${DB_PASSWORD}" \ - "${DB_DATABASE:-bookstack}" \ + -u"${BOOKSTACK_DB_USERNAME:-bookstack}" \ + -p"${BOOKSTACK_DB_PASSWORD}" \ + "${BOOKSTACK_DB_DATABASE:-bookstack}" \ | gzip > "$BACKUP_DIR/bookstack_db_$DATE.sql.gz" # 2. Backup BookStack uploads and config diff --git a/data/.DS_Store b/data/.DS_Store index 645b942..53ad94d 100644 Binary files a/data/.DS_Store and b/data/.DS_Store differ diff --git a/data/Tello_Drone_Setup.ipynb b/data/Tello_Drone_Setup.ipynb new file mode 100644 index 0000000..fe66d06 --- /dev/null +++ b/data/Tello_Drone_Setup.ipynb @@ -0,0 +1,177 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "49a243e8", + "metadata": {}, + "source": [ + "# Tello Drone Programming - Setup\n", + "\n", + "Dieses Notebook hilft dir, die Programmierung des DJI Tello Drohne einzurichten.\n", + "\n", + "## Schritt 1: Pakete installieren\n", + "\n", + "Führe die folgende Zelle aus, um die benötigten Python-Pakete zu installieren:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4804023e", + "metadata": {}, + "outputs": [], + "source": [ + "# Installiere djitellopy und Abhängigkeiten\n", + "!pip install djitellopy opencv-python pillow" + ] + }, + { + "cell_type": "markdown", + "id": "a480f600", + "metadata": {}, + "source": [ + "## Schritt 2: Import und Verbindungstest\n", + "\n", + "Nach der Installation, importiere die Bibliothek:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7bcda678", + "metadata": {}, + "outputs": [], + "source": [ + "from djitellopy import Tello\n", + "import time\n", + "\n", + "print(\"✅ djitellopy erfolgreich importiert!\")" + ] + }, + { + "cell_type": "markdown", + "id": "165d7797", + "metadata": {}, + "source": [ + "## Schritt 3: Drohne verbinden (nur ausführen wenn Drohne eingeschaltet ist!)\n", + "\n", + "**⚠️ WICHTIG:** Stelle sicher, dass:\n", + "1. Die Tello Drohne eingeschaltet ist\n", + "2. Dein Computer mit dem WiFi der Drohne verbunden ist (TELLO-XXXXXX)\n", + "3. Du genug Platz hast (mindestens 2x2 Meter freier Raum)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6acedf15", + "metadata": {}, + "outputs": [], + "source": [ + "# Drohne initialisieren\n", + "drone = Tello()\n", + "\n", + "# Verbindung aufbauen\n", + "drone.connect()\n", + "\n", + "# Batterie-Status prüfen\n", + "battery = drone.get_battery()\n", + "print(f\"🔋 Batterie: {battery}%\")\n", + "\n", + "if battery < 20:\n", + " print(\"⚠️ Warnung: Batterie niedrig! Bitte aufladen.\")\n", + "else:\n", + " print(\"✅ Drohne bereit!\")" + ] + }, + { + "cell_type": "markdown", + "id": "c4c90848", + "metadata": {}, + "source": [ + "## Schritt 4: Einfacher Testflug\n", + "\n", + "**⚠️ SICHERHEIT ZUERST:**\n", + "- Freier Raum um dich herum\n", + "- Keine Personen in der Nähe\n", + "- Drohne auf ebener Fläche\n", + "- Bereit, die Drohne zu fangen falls nötig" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ce22798", + "metadata": {}, + "outputs": [], + "source": [ + "# Starten und einfache Bewegung\n", + "drone.takeoff() # Abheben\n", + "time.sleep(3) # 3 Sekunden schweben\n", + "\n", + "drone.move_up(30) # 30cm nach oben\n", + "time.sleep(2)\n", + "\n", + "drone.rotate_clockwise(90) # 90° drehen\n", + "time.sleep(2)\n", + "\n", + "drone.land() # Landen\n", + "print(\"✅ Testflug abgeschlossen!\")" + ] + }, + { + "cell_type": "markdown", + "id": "3b616df2", + "metadata": {}, + "source": [ + "## Nützliche Befehle\n", + "\n", + "Hier sind einige grundlegende Befehle:\n", + "\n", + "```python\n", + "# Bewegungen\n", + "drone.takeoff() # Abheben\n", + "drone.land() # Landen\n", + "drone.move_up(x) # x cm nach oben\n", + "drone.move_down(x) # x cm nach unten\n", + "drone.move_forward(x) # x cm vorwärts\n", + "drone.move_back(x) # x cm rückwärts\n", + "drone.move_left(x) # x cm links\n", + "drone.move_right(x) # x cm rechts\n", + "drone.rotate_clockwise(x) # x° im Uhrzeigersinn\n", + "drone.rotate_counter_clockwise(x) # x° gegen Uhrzeigersinn\n", + "\n", + "# Informationen\n", + "drone.get_battery() # Batterie-Status\n", + "drone.get_height() # Aktuelle Höhe\n", + "drone.get_speed() # Geschwindigkeit\n", + "\n", + "# Notfall\n", + "drone.emergency() # NOTLANDUNG (sofort stoppen!)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "441ebe1d", + "metadata": {}, + "source": [ + "## Weiterführende Ressourcen\n", + "\n", + "- [DJITelloPy Dokumentation](https://djitellopy.readthedocs.io/)\n", + "- [Beispiele auf GitHub](https://github.com/damiafuentes/DJITelloPy/tree/master/examples)\n", + "\n", + "---\n", + "\n", + "**Viel Spaß beim Programmieren! 🚁**" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docker-compose.yml b/docker-compose.yml index 231028c..d5fcf01 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -108,10 +108,10 @@ services: image: lscr.io/linuxserver/bookstack:latest container_name: bookstack environment: - - APP_KEY=${BOOKSTACK_APP_KEY} - - APP_URL=${BOOKSTACK_APP_URL} - env_file: - - .env + - MYSQL_ROOT_PASSWORD=${BOOKSTACK_DB_PASSWORD} + - MYSQL_DATABASE=${BOOKSTACK_DB_DATABASE} + - MYSQL_USER=${BOOKSTACK_DB_USERNAME} + - MYSQL_PASSWORD=${BOOKSTACK_DB_PASSWORD} volumes: - ./bookstack/bookstack_app_data:/config ports: @@ -121,7 +121,11 @@ services: bookstack-mariadb: condition: service_healthy healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"] + test: + [ + "CMD-SHELL", + "mariadb -u${BOOKSTACK_DB_USERNAME} -p${BOOKSTACK_DB_PASSWORD} -e 'SELECT 1' || exit 1", + ] interval: 30s timeout: 10s retries: 3 @@ -135,10 +139,10 @@ services: image: lscr.io/linuxserver/mariadb:latest container_name: bookstack-mariadb environment: - - MYSQL_ROOT_PASSWORD=${DB_PASSWORD} - - MYSQL_DATABASE=${DB_DATABASE} - - MYSQL_USER=${DB_USERNAME} - - MYSQL_PASSWORD=${DB_PASSWORD} + - MYSQL_ROOT_PASSWORD=${BOOKSTACK_DB_PASSWORD} + - MYSQL_DATABASE=${BOOKSTACK_DB_DATABASE} + - MYSQL_USER=${BOOKSTACK_DB_USERNAME} + - MYSQL_PASSWORD=${BOOKSTACK_DB_PASSWORD} - PUID=1000 - PGID=1000 - TZ=Europe/Berlin @@ -151,7 +155,7 @@ services: test: [ "CMD-SHELL", - "mariadb -u${DB_USERNAME} -p${DB_PASSWORD} -e 'SELECT 1' || exit 1", + "mariadb -u${BOOKSTACK_DB_USERNAME} -p${BOOKSTACK_DB_PASSWORD} -e 'SELECT 1' || exit 1", ] interval: 10s timeout: 5s @@ -207,6 +211,59 @@ services: - "description=Portainer Container Management UI" - "maintainer=Studio EinsZwoVier" + jupyterhub: + build: ./jupyterhub + container_name: jupyterhub + ports: + - "8001:8001" + - "8081:8081" + volumes: + - ./data:/home + - ./jupyterhub/jupyterhub_config.py:/srv/jupyterhub/jupyterhub_config.py:ro + - ./static/images/logo.png:/srv/jupyterhub/logo.png:ro + - /var/run/docker.sock:/var/run/docker.sock # Docker socket for spawning containers + env_file: + - .env + environment: + - JUPYTERHUB_SINGLEUSER_APP=jupyterlab + restart: unless-stopped + mem_limit: 2g + cpus: 1.0 + depends_on: + - web + labels: + - "com.centurylinklabs.watchtower.enable=true" + - "description=JupyterHub for interactive notebooks" + - "maintainer=Studio EinsZwoVier" + + forgejo: + image: codeberg.org/forgejo/forgejo:11 + container_name: forgejo + environment: + - USER_UID=1000 + - USER_GID=1000 + - TZ=Europe/Berlin + - FORGEJO__repository__ENABLE_PUSH_CREATE_USER=true + restart: unless-stopped + ports: + - "${FORGEJO_PORT:-3003}:3000" + - "222:22" + volumes: + - ./forgejo/data:/data + mem_limit: 2g + cpus: 2.0 + mem_reservation: 512m + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3000/ || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + labels: + - "com.centurylinklabs.watchtower.enable=true" + - "description=Forgejo Git Server" + - "maintainer=Studio EinsZwoVier" + volumes: portainer_data: driver: local diff --git a/forgejo/data/git/.ssh/environment b/forgejo/data/git/.ssh/environment new file mode 100644 index 0000000..f86169b --- /dev/null +++ b/forgejo/data/git/.ssh/environment @@ -0,0 +1 @@ +GITEA_CUSTOM=/data/gitea diff --git a/forgejo/data/gitea/conf/app.ini b/forgejo/data/gitea/conf/app.ini new file mode 100644 index 0000000..afecad9 --- /dev/null +++ b/forgejo/data/gitea/conf/app.ini @@ -0,0 +1,63 @@ +APP_NAME = Forgejo: Beyond coding. We forge. +RUN_MODE = prod + +[repository] +ROOT = /data/git/repositories +ENABLE_PUSH_CREATE_USER = true + +[repository.local] +LOCAL_COPY_PATH = /data/gitea/tmp/local-repo + +[repository.upload] +TEMP_PATH = /data/gitea/uploads + +[server] +APP_DATA_PATH = /data/gitea +DOMAIN = localhost +SSH_DOMAIN = localhost +HTTP_PORT = 3000 +ROOT_URL = +DISABLE_SSH = false +SSH_PORT = 22 +SSH_LISTEN_PORT = 22 +LFS_START_SERVER = false + +[database] +PATH = /data/gitea/gitea.db +DB_TYPE = sqlite3 +HOST = localhost:3306 +NAME = gitea +USER = root +PASSWD = +LOG_SQL = false + +[indexer] +ISSUE_INDEXER_PATH = /data/gitea/indexers/issues.bleve + +[session] +PROVIDER_CONFIG = /data/gitea/sessions + +[picture] +AVATAR_UPLOAD_PATH = /data/gitea/avatars +REPOSITORY_AVATAR_UPLOAD_PATH = /data/gitea/repo-avatars + +[attachment] +PATH = /data/gitea/attachments + +[log] +MODE = console +LEVEL = info +ROOT_PATH = /data/gitea/log + +[security] +INSTALL_LOCK = false +SECRET_KEY = +REVERSE_PROXY_LIMIT = 1 +REVERSE_PROXY_TRUSTED_PROXIES = * + +[service] +DISABLE_REGISTRATION = false +REQUIRE_SIGNIN_VIEW = false + +[lfs] +PATH = /data/git/lfs diff --git a/forgejo/data/ssh/ssh_host_ecdsa_key b/forgejo/data/ssh/ssh_host_ecdsa_key new file mode 100644 index 0000000..7e07159 --- /dev/null +++ b/forgejo/data/ssh/ssh_host_ecdsa_key @@ -0,0 +1,9 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS +1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQQ02EGaaw86ZKkpWSL8gktKug0A/XNF +TgcY469G/5ecX9H/AMxrPlOKCU0K+3eWGSCISQGlVLBRNpHK5VM9ga13AAAAsJrl+aya5f +msAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBDTYQZprDzpkqSlZ +IvyCS0q6DQD9c0VOBxjjr0b/l5xf0f8AzGs+U4oJTQr7d5YZIIhJAaVUsFE2kcrlUz2BrX +cAAAAhAPrnsLBtojpOaaAt0yDgy/lCpqRnfbGBkV5FMiFYvm+fAAAAEXJvb3RAMmQ4Yjk4 +Nzg0OWMyAQIDBAUG +-----END OPENSSH PRIVATE KEY----- diff --git a/forgejo/data/ssh/ssh_host_ecdsa_key.pub b/forgejo/data/ssh/ssh_host_ecdsa_key.pub new file mode 100644 index 0000000..9e57846 --- /dev/null +++ b/forgejo/data/ssh/ssh_host_ecdsa_key.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBDTYQZprDzpkqSlZIvyCS0q6DQD9c0VOBxjjr0b/l5xf0f8AzGs+U4oJTQr7d5YZIIhJAaVUsFE2kcrlUz2BrXc= root@2d8b987849c2 diff --git a/forgejo/data/ssh/ssh_host_ed25519_key b/forgejo/data/ssh/ssh_host_ed25519_key new file mode 100644 index 0000000..340221a --- /dev/null +++ b/forgejo/data/ssh/ssh_host_ed25519_key @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACD7RYGr6qRspA4rgtvxhXJiS4a7z/hXm8q2XS9J8iqncQAAAJit3hcbrd4X +GwAAAAtzc2gtZWQyNTUxOQAAACD7RYGr6qRspA4rgtvxhXJiS4a7z/hXm8q2XS9J8iqncQ +AAAEB2I1zsRzNSHt6lqkavsXdj1fi2cDcnSCImC4Kpaqi3APtFgavqpGykDiuC2/GFcmJL +hrvP+FebyrZdL0nyKqdxAAAAEXJvb3RAMmQ4Yjk4Nzg0OWMyAQIDBA== +-----END OPENSSH PRIVATE KEY----- diff --git a/forgejo/data/ssh/ssh_host_ed25519_key.pub b/forgejo/data/ssh/ssh_host_ed25519_key.pub new file mode 100644 index 0000000..5a48f35 --- /dev/null +++ b/forgejo/data/ssh/ssh_host_ed25519_key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPtFgavqpGykDiuC2/GFcmJLhrvP+FebyrZdL0nyKqdx root@2d8b987849c2 diff --git a/forgejo/data/ssh/ssh_host_rsa_key b/forgejo/data/ssh/ssh_host_rsa_key new file mode 100644 index 0000000..f60fa3a --- /dev/null +++ b/forgejo/data/ssh/ssh_host_rsa_key @@ -0,0 +1,38 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY----- diff --git a/forgejo/data/ssh/ssh_host_rsa_key.pub b/forgejo/data/ssh/ssh_host_rsa_key.pub new file mode 100644 index 0000000..1442d1c --- /dev/null +++ b/forgejo/data/ssh/ssh_host_rsa_key.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC2FfioI52Oa6bL480x/ZFQiJqS4F2pBcxEh/OkozGyNIy7F/TpiyphP59FHoQ52JtTMwcmX0GoD8bPDYi26FIb2ugpW+TBtijJN+m3q03qrEnKxMHrMsZQoZT+bO05WAUvlpO2qO+i7f5QSfQEi+heXrG7WC2MYli3ZC3+5PEpK3ey9wbhc7nnoTtHZW1Tu/2cnqu2AkLHnkirnTNGDHM73adyESet+wps6sbyr7qO/ieOmwGoSIelc2Kk/zBQdzAAMvpbE8FyzrK5qcJaDBDx5grLJCT+ugtxIPtXRhcEf4LGQ/0/1ntBd19qID6Dai7yUS47C1mxE2IWTkb2Xu2tk6D0T1kh9vhjIVUD31qQotAzi+kHpuHz7tKLfIxRjvD2YUjiy93ygnFOxofTW3RO1irbOLLYc7WPDBp5Ee3IPZUDu363ciViuF0w1BJO/v3VkfIpfFwJW/dm3SAheW8UtOf5expa33TCSy3tSKENQdhp4fgKRXVzv6fV3xQP75s= root@2d8b987849c2 diff --git a/jupyterhub/Dockerfile b/jupyterhub/Dockerfile new file mode 100644 index 0000000..08181ba --- /dev/null +++ b/jupyterhub/Dockerfile @@ -0,0 +1,28 @@ +FROM python:3.13-slim + +# Install system deps +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + nodejs npm \ + git \ + sudo \ + && rm -rf /var/lib/apt/lists/* + +# Install JupyterHub, JupyterLab, notebook (required for singleuser), and DockerSpawner +RUN pip install --no-cache-dir jupyterhub jupyterlab notebook dockerspawner + +# Install configurable-http-proxy (required by JupyterHub) +RUN npm install -g configurable-http-proxy@^4.0.0 + +# Ensure npm global binaries are available in PATH +ENV PATH="/usr/local/bin:${PATH}" + +# Create directories +RUN mkdir -p /srv/jupyterhub /srv/jupyterhub/data +WORKDIR /srv/jupyterhub + +COPY jupyterhub_config.py /srv/jupyterhub/ + +EXPOSE 8001 + +CMD ["jupyterhub", "-f", "/srv/jupyterhub/jupyterhub_config.py"] diff --git a/jupyterhub/jupyterhub_config.py b/jupyterhub/jupyterhub_config.py new file mode 100644 index 0000000..d25ecc6 --- /dev/null +++ b/jupyterhub/jupyterhub_config.py @@ -0,0 +1,79 @@ +from traitlets.config import get_config +import os + +# Minimal JupyterHub config for local multi-user usage inside docker-compose. +# This config is intentionally small — customize for auth, spawners, and TLS in production. + +c = get_config() + +# Hub internal URL (where the hub process listens for /hub requests) +c.JupyterHub.bind_url = 'http://:8081/hub/' + +# Hub IP for spawned containers to connect back (use container name on Docker network) +c.JupyterHub.hub_connect_ip = 'jupyterhub' + +# Proxy configuration +# - Proxy public address (what users access): default is port 8000, we want 8001 +# - Proxy API address (hub talks to proxy control): separate port 8002 +c.JupyterHub.port = 8001 # Public proxy port +c.ConfigurableHTTPProxy.api_url = 'http://127.0.0.1:8002' + +# Lightweight built-in authenticator: no external package required. This is +# intended only for local development/testing. It accepts any username as long +# as the password equals the value of JUPYTER_PASSWORD (default: einszwo4). +from jupyterhub.auth import Authenticator + +class SimpleEnvPasswordAuthenticator(Authenticator): + async def authenticate(self, handler, data): + """Authenticate any username when the shared env password matches. + + Dev-only: returns the username on success, otherwise None. + """ + username = data.get('username') + password = data.get('password') + if not username or not password: + return None + if password == os.environ.get('JUPYTER_PASSWORD', 'einszwo4'): + return username + return None + +# Use the in-file authenticator for dev/testing +c.JupyterHub.authenticator_class = SimpleEnvPasswordAuthenticator + +# Allow any authenticated user to access (suppress the warning) +c.Authenticator.allow_all = True + +# Use DockerSpawner for production-ready containerized user servers +from dockerspawner import DockerSpawner +c.JupyterHub.spawner_class = DockerSpawner + +# Docker image for single-user notebook servers +c.DockerSpawner.image = 'jupyter/scipy-notebook:latest' + +# Connect to Docker socket +c.DockerSpawner.use_internal_ip = True +c.DockerSpawner.network_name = 'einszwovier_network' + +# Remove containers after they stop +c.DockerSpawner.remove = True + +# Mount the data directory for persistent storage +# Use the data directory we already have mounted in docker-compose +c.DockerSpawner.volumes = { + 'jupyterhub-user-{username}': {'bind': '/home/jovyan/work', 'mode': 'rw'} +} + +# Increase spawn timeout +c.Spawner.start_timeout = 120 +c.Spawner.http_timeout = 120 + +# Set notebook directory and default interface +c.Spawner.notebook_dir = '/home/jovyan/work' +c.Spawner.default_url = '/lab' + +# UI and data locations +c.JupyterHub.logo_file = '/srv/jupyterhub/logo.png' +c.JupyterHub.cookie_secret_file = '/srv/jupyterhub/jupyterhub_cookie_secret' + +# Note: DockerSpawner creates isolated containers for each user. +# Each user gets their own containerized Jupyter environment with persistent storage. diff --git a/main.py b/main.py index fa3327c..89a5b61 100644 --- a/main.py +++ b/main.py @@ -18,6 +18,7 @@ SERVER_HOSTNAME = os.environ.get("SERVER_HOSTNAME", "einszwovier.local") BOOKSTACK_PORT = os.environ.get("BOOKSTACK_PORT", "6875") OPENWEBUI_PORT = os.environ.get("OPENWEBUI_PORT", "8080") PORTAINER_PORT = os.environ.get("PORTAINER_PORT", "9000") +FORGEJO_PORT = os.environ.get("FORGEJO_PORT", "3003") # Courses CSV path COURSES_CSV = Path("data/courses.csv") @@ -84,6 +85,7 @@ async def welcome(request: Request): "bookstack_port": BOOKSTACK_PORT, "openwebui_port": OPENWEBUI_PORT, "portainer_port": PORTAINER_PORT, + "forgejo_port": FORGEJO_PORT, }, ) diff --git a/restore.sh b/restore.sh index a766bf5..5e230f1 100755 --- a/restore.sh +++ b/restore.sh @@ -34,9 +34,9 @@ sleep 10 # Wait for MariaDB to start zcat "$BACKUP_DIR/bookstack_db_$DATE.sql.gz" | \ docker exec -i bookstack-mariadb mariadb \ - -u"${DB_USERNAME:-bookstack}" \ - -p"${DB_PASSWORD}" \ - "${DB_DATABASE:-bookstack}" + -u"${BOOKSTACK_DB_USERNAME:-bookstack}" \ + -p"${BOOKSTACK_DB_PASSWORD}" \ + "${BOOKSTACK_DB_DATABASE:-bookstack}" # 2. Restore BookStack files echo "Restoring BookStack files..." diff --git a/templates/landing.html b/templates/landing.html index 25f4275..43bdb87 100644 --- a/templates/landing.html +++ b/templates/landing.html @@ -60,6 +60,18 @@
Teste unsere schulischen Large Language Models direkt über die Weboberfläche.
+ +
JupyterHub
+
Interaktive Python-Notebooks für Datenanalyse, Visualisierung und kollaboratives + Programmieren. Schreib uns eine E-Mail für Login-Daten.
+
+ + +
Forgejo Git Server
+
Versionskontrolle und Code-Hosting für kollaborative Softwareprojekte. Ideal für gemeinsame + Entwicklung und Projektmanagement.
+
+
Kontakt
Schreibe uns direkt an: einszwovier@gvb-gymnasium.de
@@ -70,7 +82,8 @@