commit 3678efed076f2a52f00cb81c749ae833a4eec868 Author: sevi-kun Date: Thu Oct 30 13:33:08 2025 +0100 Commit All diff --git a/.containerignore b/.containerignore new file mode 100644 index 0000000..d44c567 --- /dev/null +++ b/.containerignore @@ -0,0 +1,70 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +.pytest_cache/ +.mypy_cache/ +.coverage +htmlcov/ +.tox/ +.hypothesis/ + +# Virtual environments +.venv/ +venv/ +ENV/ +env/ +.env + +# UV/uv +.uv/ +uv.lock + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Git +.git/ +.gitignore +.github/ + +# Documentation +README.md +docs/ +*.md + +# Test files +test_*.py +tests/ +test_*.html + +# Cache files (will be created in container) +calendar_cache.json +.cache/ + +# CI/CD +.gitlab-ci.yml +.travis.yml +Jenkinsfile + +# Docker files (not needed in build context) +docker-compose*.yml +Dockerfile.* + +# Development files +Makefile +run.sh +setup.py + +# Temporary files +*.tmp +*.log +*.pid \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d44c567 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,70 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +.pytest_cache/ +.mypy_cache/ +.coverage +htmlcov/ +.tox/ +.hypothesis/ + +# Virtual environments +.venv/ +venv/ +ENV/ +env/ +.env + +# UV/uv +.uv/ +uv.lock + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Git +.git/ +.gitignore +.github/ + +# Documentation +README.md +docs/ +*.md + +# Test files +test_*.py +tests/ +test_*.html + +# Cache files (will be created in container) +calendar_cache.json +.cache/ + +# CI/CD +.gitlab-ci.yml +.travis.yml +Jenkinsfile + +# Docker files (not needed in build context) +docker-compose*.yml +Dockerfile.* + +# Development files +Makefile +run.sh +setup.py + +# Temporary files +*.tmp +*.log +*.pid \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..73c3689 --- /dev/null +++ b/.env.example @@ -0,0 +1,25 @@ +# Calendar Configuration +CALENDAR_URL=https://outlook.live.com/owa/calendar/ef9138c2-c803-4689-a53e-fe7d0cb90124/d12c4ed3-dfa2-461f-bcd8-9442bea1903b/cid-CD3289D19EBD3DA4/calendar.ics + +# Server Configuration +HOST=0.0.0.0 +PORT=8000 + +# Timezone (e.g., Europe/Berlin, America/New_York, UTC) +TIMEZONE=Europe/Berlin + +# Update Schedule (in hours) +UPDATE_INTERVAL=4 + +# Cache Settings +CACHE_FILE=calendar_cache.json + +# Display Settings +DAYS_TO_SHOW=30 + +# Optional: Basic Auth (uncomment to enable) +# BASIC_AUTH_USERNAME=admin +# BASIC_AUTH_PASSWORD=secure_password + +# Optional: CORS Settings (for API access from other domains) +# CORS_ORIGINS=["http://localhost:3000", "https://yourdomain.com"] \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f61f767 --- /dev/null +++ b/.gitignore @@ -0,0 +1,173 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +# VS Code +.vscode/ + +# Calendar cache file +calendar_cache.json + +# UV/uv +.uv/ +uv.lock + +# macOS +.DS_Store \ No newline at end of file diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..0adc67a --- /dev/null +++ b/Containerfile @@ -0,0 +1,36 @@ +FROM python:3.13-slim + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + TZ=Europe/Berlin + +# Install minimal dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + tzdata \ + && rm -rf /var/lib/apt/lists/* \ + && ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ + && echo $TZ > /etc/timezone + +# Set working directory +WORKDIR /app + +# Copy dependency files +COPY requirements.txt ./ + +# Install Python dependencies using pip (simpler for container) +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application files +COPY main.py . +COPY Vektor-Logo.svg ./ + +# Create static directory and copy logo +RUN mkdir -p static && \ + cp Vektor-Logo.svg static/logo.svg + +# Expose port +EXPOSE 8000 + +# Run the application +CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/DEPLOY_README.md b/DEPLOY_README.md new file mode 100644 index 0000000..c8d81b1 --- /dev/null +++ b/DEPLOY_README.md @@ -0,0 +1,422 @@ +# πŸš€ Raspberry Pi Deployment Guide - Turmli Bar Calendar + +This guide provides complete instructions for deploying the Turmli Bar Calendar application on a Raspberry Pi (optimized for Pi Zero) as a systemd service without Docker. + +## πŸ“‹ Table of Contents +- [Why No Docker?](#-why-no-docker) +- [Prerequisites](#-prerequisites) +- [Installation Steps](#-installation-steps) +- [Service Management](#-service-management) +- [Monitoring & Maintenance](#-monitoring--maintenance) +- [Troubleshooting](#-troubleshooting) +- [Performance Optimization](#-performance-optimization) + +## 🎯 Why No Docker? + +On a Raspberry Pi Zero (512MB RAM, single-core ARM CPU), running without Docker provides: +- **50-100MB less RAM usage** (no Docker daemon) +- **Faster startup times** (no container overhead) +- **Better performance** (direct execution) +- **Simpler debugging** (direct access to processes) +- **Native systemd integration** (better logging and management) + +## πŸ“‹ Prerequisites + +### Hardware +- Raspberry Pi Zero W or better +- 8GB+ SD card (Class 10 recommended) +- Stable 5V power supply +- Network connection (WiFi or Ethernet) + +### Software +- Raspberry Pi OS Lite (32-bit) +- Python 3.9+ +- 150MB free RAM +- 200MB free storage + +## πŸ“¦ Installation Steps + +### Step 1: Prepare Your Raspberry Pi + +```bash +# Update the system +sudo apt-get update && sudo apt-get upgrade -y + +# Install Git (if not present) +sudo apt-get install git -y + +# Create a working directory +mkdir -p ~/projects +cd ~/projects +``` + +### Step 2: Get the Application + +```bash +# Clone the repository +git clone turmli-calendar +cd turmli-calendar + +# OR copy files from your development machine +# From your dev machine: +scp -r /home/belar/Code/turmli-bar-calendar-tool/* pi@:~/projects/turmli-calendar/ +``` + +### Step 3: Run the Deployment Script + +```bash +# Make the script executable +chmod +x deploy_rpi.sh + +# Run the installation +sudo ./deploy_rpi.sh install +``` + +The script will: +1. Check system requirements +2. Install Python dependencies +3. Create `/opt/turmli-calendar` directory +4. Set up Python virtual environment +5. Install application dependencies +6. Create systemd service +7. Start the application + +### Step 4: Verify Installation + +```bash +# Check service status +sudo systemctl status turmli-calendar + +# Test the API +curl http://localhost:8000/api/events + +# Check the web interface +# Open in browser: http://:8000 +``` + +## πŸ› οΈ Service Management + +### Basic Commands + +```bash +# Start the service +sudo ./deploy_rpi.sh start +# OR +sudo systemctl start turmli-calendar + +# Stop the service +sudo ./deploy_rpi.sh stop +# OR +sudo systemctl stop turmli-calendar + +# Restart the service +sudo ./deploy_rpi.sh restart +# OR +sudo systemctl restart turmli-calendar + +# Check status +sudo ./deploy_rpi.sh status + +# View logs +sudo ./deploy_rpi.sh logs +# OR +sudo journalctl -u turmli-calendar -f +``` + +### Update Application + +```bash +# Pull latest changes (if using git) +git pull + +# Update the deployment +sudo ./deploy_rpi.sh update +``` + +### Uninstall + +```bash +# Complete removal +sudo ./deploy_rpi.sh uninstall +``` + +## πŸ“Š Monitoring & Maintenance + +### Using the Monitor Script + +```bash +# Quick health check +./deployment/monitor.sh status + +# Full system report +./deployment/monitor.sh full + +# Continuous monitoring (30s refresh) +./deployment/monitor.sh monitor + +# Check resources only +./deployment/monitor.sh resources +``` + +### Manual Monitoring + +```bash +# Memory usage +free -h + +# CPU temperature (Pi specific) +vcgencmd measure_temp + +# Check for throttling +vcgencmd get_throttled + +# Disk usage +df -h + +# Process info +ps aux | grep turmli +``` + +### Log Management + +```bash +# View last 50 log entries +sudo journalctl -u turmli-calendar -n 50 + +# Follow logs in real-time +sudo journalctl -u turmli-calendar -f + +# Export logs +sudo journalctl -u turmli-calendar > turmli-logs.txt + +# Clear old logs (if needed) +sudo journalctl --vacuum-time=7d +``` + +## πŸ› Troubleshooting + +### Service Won't Start + +```bash +# Check for Python errors +sudo -u pi /opt/turmli-calendar/venv/bin/python /opt/turmli-calendar/main.py + +# Check permissions +ls -la /opt/turmli-calendar/ + +# Check port availability +sudo netstat -tlnp | grep 8000 + +# Review detailed logs +sudo journalctl -u turmli-calendar -n 100 --no-pager +``` + +### High Memory Usage + +```bash +# Restart the service +sudo systemctl restart turmli-calendar + +# Check for memory leaks +watch -n 1 'free -h' + +# Clear system cache +sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches' +``` + +### Application Not Responding + +```bash +# Test locally +curl -v http://localhost:8000/api/events + +# Check network +ip addr show +ping -c 4 google.com + +# Restart networking +sudo systemctl restart networking +``` + +### Calendar Not Updating + +```bash +# Check cache file +ls -la /opt/turmli-calendar/calendar_cache.json + +# Manually trigger update +curl -X POST http://localhost:8000/api/refresh + +# Check external connectivity +curl -I https://outlook.live.com +``` + +## ⚑ Performance Optimization + +### For Raspberry Pi Zero + +1. **Increase Swap Space** +```bash +sudo nano /etc/dphys-swapfile +# Set: CONF_SWAPSIZE=256 +sudo dphys-swapfile setup +sudo dphys-swapfile swapon +``` + +2. **Disable Unnecessary Services** +```bash +# Check what's running +systemctl list-units --type=service --state=running + +# Disable unused services +sudo systemctl disable --now bluetooth +sudo systemctl disable --now cups +sudo systemctl disable --now avahi-daemon +``` + +3. **Optimize Boot Configuration** +```bash +sudo nano /boot/config.txt +# Add: +gpu_mem=16 # Minimize GPU memory +boot_delay=0 # Faster boot +disable_splash=1 # No splash screen +``` + +4. **Set CPU Governor** +```bash +# Check current governor +cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor + +# Set to performance (optional, uses more power) +echo performance | sudo tee /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor +``` + +### Network Optimization + +```bash +# Use static IP to reduce DHCP overhead +sudo nano /etc/dhcpcd.conf + +# Add (adjust for your network): +interface wlan0 +static ip_address=192.168.1.100/24 +static routers=192.168.1.1 +static domain_name_servers=192.168.1.1 8.8.8.8 + +# Restart networking +sudo systemctl restart dhcpcd +``` + +## πŸ“ˆ Expected Resource Usage + +| Component | Idle | Active | Peak | +|-----------|------|--------|------| +| RAM | ~80MB | ~120MB | ~150MB | +| CPU | 2-5% | 10-20% | 40% | +| Disk | ~200MB | ~210MB | ~250MB | +| Network | <1KB/s | 5-10KB/s | 50KB/s | + +## πŸ”’ Security Recommendations + +1. **Change Default Password** +```bash +passwd +``` + +2. **Configure Firewall** +```bash +sudo apt-get install ufw +sudo ufw allow 22/tcp # SSH +sudo ufw allow 8000/tcp # Application +sudo ufw enable +``` + +3. **SSH Key Authentication** +```bash +# On your dev machine +ssh-copy-id pi@ + +# On Pi, disable password auth +sudo nano /etc/ssh/sshd_config +# Set: PasswordAuthentication no +sudo systemctl restart ssh +``` + +4. **Regular Updates** +```bash +# Create update script +cat > ~/update-system.sh << 'EOF' +#!/bin/bash +sudo apt-get update +sudo apt-get upgrade -y +sudo apt-get autoremove -y +sudo apt-get autoclean +EOF +chmod +x ~/update-system.sh + +# Run monthly +crontab -e +# Add: 0 2 1 * * /home/pi/update-system.sh +``` + +## πŸ“ File Locations + +| Component | Path | +|-----------|------| +| Application | `/opt/turmli-calendar/` | +| Service File | `/etc/systemd/system/turmli-calendar.service` | +| Cache File | `/opt/turmli-calendar/calendar_cache.json` | +| Static Files | `/opt/turmli-calendar/static/` | +| Virtual Env | `/opt/turmli-calendar/venv/` | +| Logs | Journal (use `journalctl`) | + +## πŸ†˜ Quick Reference + +```bash +# Service control +sudo systemctl {start|stop|restart|status} turmli-calendar + +# Logs +sudo journalctl -u turmli-calendar -f + +# Test API +curl http://localhost:8000/api/events | python3 -m json.tool + +# Check port +sudo netstat -tlnp | grep 8000 + +# Python packages +/opt/turmli-calendar/venv/bin/pip list + +# Manual run (for debugging) +cd /opt/turmli-calendar +sudo -u pi ./venv/bin/python -m uvicorn main:app --host 0.0.0.0 --port 8000 + +# System resources +htop # Install with: sudo apt-get install htop +``` + +## πŸ“ Notes + +- The application runs on port 8000 by default +- Service automatically starts on boot +- Logs are managed by systemd journal +- Cache persists in `/opt/turmli-calendar/calendar_cache.json` +- The service runs as user `pi` for security +- Memory is limited to 256MB to prevent system crashes +- CPU is limited to 75% to keep system responsive + +## 🀝 Support + +If you encounter issues: + +1. Check the logs first: `sudo journalctl -u turmli-calendar -n 100` +2. Run the monitor script: `./deployment/monitor.sh full` +3. Try manual startup to see errors: `cd /opt/turmli-calendar && sudo -u pi ./venv/bin/python main.py` +4. Check system resources: `free -h && df -h` +5. Verify network connectivity: `ping -c 4 google.com` + +## πŸ“„ License + +MIT License - See main project repository for details. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0adc67a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +FROM python:3.13-slim + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + TZ=Europe/Berlin + +# Install minimal dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + tzdata \ + && rm -rf /var/lib/apt/lists/* \ + && ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ + && echo $TZ > /etc/timezone + +# Set working directory +WORKDIR /app + +# Copy dependency files +COPY requirements.txt ./ + +# Install Python dependencies using pip (simpler for container) +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application files +COPY main.py . +COPY Vektor-Logo.svg ./ + +# Create static directory and copy logo +RUN mkdir -p static && \ + cp Vektor-Logo.svg static/logo.svg + +# Expose port +EXPOSE 8000 + +# Run the application +CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/FIX_INSTALLATION.md b/FIX_INSTALLATION.md new file mode 100644 index 0000000..ff49930 --- /dev/null +++ b/FIX_INSTALLATION.md @@ -0,0 +1,241 @@ +# πŸ”§ Fix Installation Issues on Raspberry Pi + +This guide helps fix the "No space left on device" error when installing the Turmli Calendar on Raspberry Pi. + +## 🚨 The Problem + +When running `pip install` on Raspberry Pi, you're getting: +``` +ERROR: Could not install packages due to an OSError: [Errno 28] No space left on device +``` + +This happens because: +- `/tmp` is in RAM (tmpfs) and limited to ~214MB on your Pi +- The `watchfiles` package requires compilation with Rust +- The build process needs more space than available in `/tmp` + +## βœ… Quick Fix + +### Option 1: Use the Fix Script (Recommended) + +1. Copy the fix script to your Pi: +```bash +# From your development machine +scp fix_install_rpi.sh pi@turmli-pi:/tmp/turmli-calendar/ +scp requirements-rpi.txt pi@turmli-pi:/tmp/turmli-calendar/ +``` + +2. Run the fix script: +```bash +cd /tmp/turmli-calendar +sudo ./fix_install_rpi.sh +``` + +### Option 2: Manual Fix + +1. **Clean up temp space:** +```bash +# Clean pip cache +rm -rf /tmp/pip-* +sudo apt-get clean +``` + +2. **Create a custom temp directory:** +```bash +sudo mkdir -p /opt/turmli-calendar/tmp +sudo chown pi:pi /opt/turmli-calendar/tmp +export TMPDIR=/opt/turmli-calendar/tmp +``` + +3. **Use the optimized requirements file:** +```bash +cd /opt/turmli-calendar + +# Create optimized requirements without problematic packages +cat > requirements-rpi.txt << 'EOF' +fastapi>=0.104.0 +uvicorn>=0.24.0 +httpx>=0.25.0 +icalendar>=5.0.0 +jinja2>=3.1.0 +python-multipart>=0.0.6 +apscheduler>=3.10.0 +pytz>=2023.3 +EOF +``` + +4. **Install packages with the custom temp directory:** +```bash +# Activate virtual environment +source /opt/turmli-calendar/venv/bin/activate + +# Upgrade pip first +TMPDIR=/opt/turmli-calendar/tmp pip install --upgrade pip wheel setuptools + +# Install packages +TMPDIR=/opt/turmli-calendar/tmp pip install \ + --no-cache-dir \ + --prefer-binary \ + --no-build-isolation \ + -r requirements-rpi.txt +``` + +5. **Clean up:** +```bash +rm -rf /opt/turmli-calendar/tmp +deactivate +``` + +## 🎯 Alternative: Install Packages One by One + +If the above doesn't work, install packages individually: + +```bash +cd /opt/turmli-calendar +source venv/bin/activate + +# Set temp directory +export TMPDIR=/opt/turmli-calendar/tmp +mkdir -p $TMPDIR + +# Install each package separately +pip install --no-cache-dir fastapi +pip install --no-cache-dir uvicorn # Without [standard] extras +pip install --no-cache-dir httpx +pip install --no-cache-dir icalendar +pip install --no-cache-dir jinja2 +pip install --no-cache-dir python-multipart +pip install --no-cache-dir apscheduler +pip install --no-cache-dir pytz + +# Clean up +rm -rf $TMPDIR +deactivate +``` + +## πŸš€ Start the Application + +After fixing the installation: + +### Using systemd service: +```bash +sudo systemctl start turmli-calendar +sudo systemctl status turmli-calendar +``` + +### Or manually with minimal resources: +```bash +cd /opt/turmli-calendar +./start_minimal.sh +``` + +### Or directly: +```bash +cd /opt/turmli-calendar +source venv/bin/activate +python -m uvicorn main:app --host 0.0.0.0 --port 8000 --workers 1 --log-level warning +``` + +## πŸ’Ύ Increase System Resources (Optional) + +### 1. Increase Swap Space +```bash +sudo nano /etc/dphys-swapfile +# Change: CONF_SWAPSIZE=512 +sudo dphys-swapfile setup +sudo dphys-swapfile swapon +``` + +### 2. Use Different Temp Location +```bash +# Edit /etc/fstab to mount /tmp on disk instead of RAM +sudo nano /etc/fstab +# Add: tmpfs /tmp tmpfs defaults,noatime,nosuid,size=512m 0 0 +``` + +### 3. Free Up Disk Space +```bash +# Remove unnecessary packages +sudo apt-get autoremove +sudo apt-get clean + +# Clear journal logs +sudo journalctl --vacuum-time=7d + +# Remove old kernels (if any) +sudo apt-get autoremove --purge +``` + +## ⚠️ What We're Skipping + +The optimized installation skips these optional packages that require compilation: +- `watchfiles` - File watching for auto-reload (not needed in production) +- `websockets` - WebSocket support (not used by this app) +- `httptools` - Faster HTTP parsing (marginal improvement) +- `uvloop` - Faster event loop (nice to have, but not essential) + +The application will work perfectly fine without these packages! + +## πŸ” Verify Installation + +Test that everything is working: + +```bash +cd /opt/turmli-calendar +source venv/bin/activate + +# Test imports +python -c "import fastapi, uvicorn, httpx, icalendar, jinja2, apscheduler, pytz; print('βœ“ All modules OK')" + +# Test the application +python -c "from main import app; print('βœ“ Application loads OK')" + +deactivate +``` + +## πŸ“Š Check System Resources + +Monitor your system during installation: + +```bash +# In another terminal, watch resources +watch -n 1 'free -h; echo; df -h /tmp /opt' +``` + +## πŸ†˜ Still Having Issues? + +1. **Check available space:** + ```bash + df -h + ``` + +2. **Check memory:** + ```bash + free -h + ``` + +3. **Try rebooting:** + ```bash + sudo reboot + ``` + +4. **Use a bigger SD card** (16GB+ recommended) + +5. **Consider Raspberry Pi OS Lite** (uses less resources) + +## πŸ“ Notes + +- The application uses about 80-120MB RAM when running +- Installation needs about 200-300MB free disk space +- Compilation can use up to 500MB temp space +- Using pre-built wheels (--prefer-binary) avoids most compilation + +## ✨ Success! + +Once installed, the application will: +- Start automatically on boot +- Restart if it crashes +- Use minimal resources (limited to 256MB RAM) +- Be accessible at `http://your-pi-ip:8000` + +Good luck with your installation! πŸš€ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..17d80ad --- /dev/null +++ b/Makefile @@ -0,0 +1,84 @@ +# Makefile for Turmli Bar Calendar Tool +.PHONY: help install run test clean docker-build docker-run docker-stop update dev + +# Default target +help: + @echo "Turmli Bar Calendar Tool - Available commands:" + @echo " make install - Install dependencies with uv" + @echo " make run - Run the server locally" + @echo " make dev - Run the server in development mode with reload" + @echo " make test - Run the test suite" + @echo " make clean - Remove cache files and Python artifacts" + @echo " make update - Update all dependencies" + @echo " make docker-build - Build Docker container" + @echo " make docker-run - Run Docker container" + @echo " make docker-stop - Stop Docker container" + +# Install dependencies +install: + @echo "Installing dependencies with uv..." + uv sync + +# Run the server +run: + @echo "Starting Calendar Server..." + @echo "Access at: http://localhost:8000" + uv run uvicorn main:app --host 0.0.0.0 --port 8000 + +# Run in development mode with auto-reload +dev: + @echo "Starting Calendar Server in development mode..." + @echo "Access at: http://localhost:8000" + uv run uvicorn main:app --host 0.0.0.0 --port 8000 --reload + +# Run tests +test: + @echo "Running test suite..." + @uv run python test_server.py + +# Clean cache and temporary files +clean: + @echo "Cleaning cache and temporary files..." + rm -f calendar_cache.json + rm -rf __pycache__ + rm -rf .pytest_cache + rm -rf .mypy_cache + find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true + find . -type f -name "*.pyc" -delete 2>/dev/null || true + find . -type f -name "*.pyo" -delete 2>/dev/null || true + find . -type f -name "*~" -delete 2>/dev/null || true + @echo "Clean complete!" + +# Update dependencies +update: + @echo "Updating dependencies..." + uv sync --upgrade + +# Docker commands +docker-build: + @echo "Building Docker image..." + docker build -t turmli-calendar:latest . + +docker-run: + @echo "Running Docker container..." + docker-compose up -d + @echo "Container started! Access at: http://localhost:8000" + +docker-stop: + @echo "Stopping Docker container..." + docker-compose down + +# Check if server is running +check: + @curl -s http://localhost:8000/api/events > /dev/null && \ + echo "βœ“ Server is running" || \ + echo "βœ— Server is not running" + +# Show logs (for Docker) +logs: + docker-compose logs -f + +# Refresh calendar manually +refresh: + @echo "Refreshing calendar..." + @curl -X POST http://localhost:8000/api/refresh -s | python3 -m json.tool \ No newline at end of file diff --git a/PODMAN_README.md b/PODMAN_README.md new file mode 100644 index 0000000..77a854f --- /dev/null +++ b/PODMAN_README.md @@ -0,0 +1,359 @@ +# πŸš€ Podman Deployment Guide - Turmli Bar Calendar + +This guide provides instructions for deploying the Turmli Bar Calendar application using Podman, a daemonless container engine that's a drop-in replacement for Docker. + +## πŸ“‹ Table of Contents +- [Why Podman?](#-why-podman) +- [Prerequisites](#-prerequisites) +- [Quick Start](#-quick-start) +- [Deployment Options](#-deployment-options) +- [Commands Reference](#-commands-reference) +- [Rootless Podman](#-rootless-podman) +- [Systemd Integration](#-systemd-integration) +- [Differences from Docker](#-differences-from-docker) +- [Troubleshooting](#-troubleshooting) + +## 🎯 Why Podman? + +Podman offers several advantages over Docker: +- **Daemonless**: No background service consuming resources +- **Rootless**: Can run containers without root privileges +- **Systemd Integration**: Native systemd service generation +- **Docker Compatible**: Uses the same commands and image format +- **Better Security**: Each container runs in its own user namespace +- **Lower Resource Usage**: No daemon means less memory overhead + +## πŸ“¦ Prerequisites + +### Install Podman + +#### On Fedora/RHEL/CentOS: +```bash +sudo dnf install podman +``` + +#### On Ubuntu/Debian: +```bash +sudo apt-get update +sudo apt-get install podman +``` + +#### On Arch Linux: +```bash +sudo pacman -S podman +``` + +### Optional: Install podman-compose +```bash +pip install podman-compose +# or +sudo dnf install podman-compose # Fedora +sudo apt-get install podman-compose # Ubuntu/Debian +``` + +## πŸš€ Quick Start + +1. **Clone the repository**: +```bash +git clone https://github.com/yourusername/turmli-bar-calendar-tool.git +cd turmli-bar-calendar-tool +``` + +2. **Deploy with the Podman script**: +```bash +./deploy-podman.sh start +``` + +3. **Access the application**: +Open http://localhost:8000 in your browser + +## πŸ› οΈ Deployment Options + +### Option 1: Using the Deploy Script (Recommended) + +The `deploy-podman.sh` script automatically detects whether you have Podman or Docker installed and uses the appropriate tool. + +```bash +# Start the application (builds and runs) +./deploy-podman.sh start + +# Check status +./deploy-podman.sh status + +# View logs +./deploy-podman.sh logs + +# Stop the application +./deploy-podman.sh stop + +# Restart the application +./deploy-podman.sh restart + +# Clean up everything +./deploy-podman.sh clean +``` + +### Option 2: Using podman-compose + +If you have `podman-compose` installed: + +```bash +# Start with podman-compose +podman-compose -f podman-compose.yml up -d + +# Stop with podman-compose +podman-compose -f podman-compose.yml down + +# View logs +podman-compose -f podman-compose.yml logs -f +``` + +### Option 3: Direct Podman Commands + +```bash +# Build the image +podman build -f Containerfile -t turmli-calendar . + +# Run the container +podman run -d \ + --name turmli-calendar \ + -p 8000:8000 \ + -e TZ=Europe/Berlin \ + -v ./calendar_cache.json:/app/calendar_cache.json:Z \ + --restart unless-stopped \ + turmli-calendar + +# Check logs +podman logs -f turmli-calendar + +# Stop and remove +podman stop turmli-calendar +podman rm turmli-calendar +``` + +## πŸ“‹ Commands Reference + +### Environment Variables + +- `PORT`: Port to expose (default: 8000) +- `TZ`: Timezone (default: Europe/Berlin) + +Example: +```bash +PORT=8080 TZ=America/New_York ./deploy-podman.sh start +``` + +### deploy-podman.sh Commands + +| Command | Description | +|---------|------------| +| `build` | Build the container image | +| `start` | Build and start the application | +| `stop` | Stop the application | +| `restart` | Restart the application | +| `logs` | Show application logs (follow mode) | +| `status` | Check application status and health | +| `clean` | Remove containers and images | +| `systemd` | Generate systemd service (Podman only) | +| `help` | Show help message | + +## πŸ‘€ Rootless Podman + +Podman can run containers without root privileges, which is more secure. + +### Setup Rootless Podman + +1. **Enable lingering for your user** (allows services to run without being logged in): +```bash +loginctl enable-linger $USER +``` + +2. **Configure subuid and subgid** (usually already configured): +```bash +# Check if already configured +grep $USER /etc/subuid /etc/subgid +``` + +3. **Run containers as your regular user**: +```bash +# No sudo needed! +podman run -d --name test alpine sleep 1000 +podman ps +podman stop test +``` + +## πŸ”§ Systemd Integration + +Podman can generate systemd service files for automatic startup. + +### Generate and Install Systemd Service + +1. **Start the container first**: +```bash +./deploy-podman.sh start +``` + +2. **Generate systemd service**: +```bash +./deploy-podman.sh systemd +``` + +3. **Enable and start the service**: +```bash +# For user service (rootless) +systemctl --user daemon-reload +systemctl --user enable container-turmli-calendar.service +systemctl --user start container-turmli-calendar.service + +# Check status +systemctl --user status container-turmli-calendar.service +``` + +### For System-wide Service (requires root) + +```bash +# Generate for system +sudo podman generate systemd --name --files --new turmli-calendar + +# Move to system directory +sudo mv container-turmli-calendar.service /etc/systemd/system/ + +# Enable and start +sudo systemctl daemon-reload +sudo systemctl enable container-turmli-calendar.service +sudo systemctl start container-turmli-calendar.service +``` + +## πŸ”„ Differences from Docker + +### Key Differences + +1. **No Daemon**: Podman doesn't require a background service +2. **Rootless by Default**: Can run without root privileges +3. **Systemd Integration**: Native systemd service generation +4. **SELinux Labels**: Use `:Z` flag for volumes with SELinux +5. **User Namespaces**: Better isolation between containers + +### Command Compatibility + +Most Docker commands work with Podman: +```bash +# Docker command +docker build -t myapp . +docker run -d -p 8000:8000 myapp + +# Podman equivalent (same syntax!) +podman build -t myapp . +podman run -d -p 8000:8000 myapp +``` + +### Compose Differences + +The `podman-compose.yml` includes Podman-specific options: +- `userns_mode: keep-id` - Maintains user ID in container +- Volume `:Z` flag - SELinux relabeling +- Modified healthcheck - Uses Python instead of curl + +## πŸ› Troubleshooting + +### Common Issues and Solutions + +#### 1. Permission Denied on Volume Mount +```bash +# Add :Z flag for SELinux systems +-v ./calendar_cache.json:/app/calendar_cache.json:Z +``` + +#### 2. Container Can't Bind to Port +```bash +# Check if port is already in use +podman port turmli-calendar +ss -tlnp | grep 8000 + +# Use a different port +PORT=8080 ./deploy-podman.sh start +``` + +#### 3. Rootless Podman Can't Bind to Privileged Ports (< 1024) +```bash +# Allow binding to port 80 (example) +sudo sysctl net.ipv4.ip_unprivileged_port_start=80 +``` + +#### 4. Container Not Starting After Reboot +```bash +# Enable lingering for rootless containers +loginctl enable-linger $USER + +# Use systemd service +./deploy-podman.sh systemd +systemctl --user enable container-turmli-calendar.service +``` + +#### 5. DNS Issues in Container +```bash +# Check Podman's DNS configuration +podman run --rm alpine cat /etc/resolv.conf + +# Use custom DNS if needed +podman run --dns 8.8.8.8 --dns 8.8.4.4 ... +``` + +### Debugging Commands + +```bash +# Inspect container +podman inspect turmli-calendar + +# Check container processes +podman top turmli-calendar + +# Execute command in running container +podman exec -it turmli-calendar /bin/bash + +# Check Podman system info +podman system info + +# Clean up unused resources +podman system prune -a +``` + +## πŸ“ Additional Notes + +### Security Benefits + +- **No Root Daemon**: Unlike Docker, Podman doesn't require a root daemon +- **User Namespaces**: Each container runs in isolated user namespace +- **Seccomp Profiles**: Default security profiles for system calls +- **SELinux Support**: Better integration with SELinux policies + +### Resource Usage + +Podman typically uses less resources than Docker: +- No daemon = ~50-100MB less RAM +- Faster container startup +- Lower CPU overhead + +### Migration from Docker + +To migrate from Docker to Podman: +1. Install Podman +2. Use the same Dockerfile/Containerfile +3. Replace `docker` with `podman` in commands +4. Adjust volume mounts (add `:Z` for SELinux) +5. Use `podman-compose` instead of `docker-compose` + +## πŸ“š Resources + +- [Podman Documentation](https://docs.podman.io/) +- [Podman vs Docker](https://podman.io/whatis.html) +- [Rootless Podman Tutorial](https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md) +- [Podman Compose](https://github.com/containers/podman-compose) + +## 🀝 Support + +If you encounter any issues specific to Podman deployment, please: +1. Check the troubleshooting section above +2. Review Podman logs: `podman logs turmli-calendar` +3. Check Podman events: `podman events --since 1h` +4. Open an issue with the error details \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7379a66 --- /dev/null +++ b/README.md @@ -0,0 +1,313 @@ +# πŸ“… Turmli Bar Calendar Tool + +A modern, mobile-friendly web application that fetches and displays calendar events from an ICS (iCalendar) feed. Built with Python, FastAPI, and a responsive web interface. + +## ✨ Features + +- **Automatic Daily Updates**: Fetches calendar data automatically every day and every 4 hours +- **Mobile-Responsive Design**: Warm, inviting interface optimized for all devices +- **Bar-Themed Colors**: Cozy atmosphere with rich browns and warm earth tones +- **Custom Branding**: Integrated Turmli Bar logo +- **Event Grouping**: Events organized by date for easy viewing +- **Caching**: Local caching ensures calendar is available even if the source is temporarily unavailable +- **API Endpoints**: JSON API for programmatic access to calendar data +- **Real-time Updates**: + - Manual refresh button triggers actual ICS file fetch + - Auto-refresh with countdown timer (5 minutes) + - Status messages showing fetch progress and results +- **Container Support**: Docker and Podman compatible for easy deployment + +## πŸš€ Quick Start + +### Option 1: Container Deployment (Docker/Podman) + +#### Using Docker + +```bash +# Clone the repository +git clone +cd turmli-bar-calendar-tool + +# Deploy with Docker +./deploy.sh start + +# Or deploy with Podman (rootless containers) +./deploy-podman.sh start +``` + +The application will be available at `http://localhost:8000` + +### Option 2: Local Development + +### Prerequisites + +- Python 3.13 or higher +- [uv](https://github.com/astral-sh/uv) package manager (for local development only) + +### Installation + +1. Clone the repository: +```bash +git clone +cd turmli-bar-calendar-tool +``` + +2. Install dependencies using uv: +```bash +uv sync +``` + +3. Run the server: +```bash +./run.sh +``` + +Or directly with uv: +```bash +uv run uvicorn main:app --host 0.0.0.0 --port 8000 --reload +``` + +4. Open your browser and navigate to: +``` +http://localhost:8000 +``` + +## πŸ“± Features & Interface + +### Web Interface +- **Responsive Design**: Optimized for mobile phones, tablets, and desktops +- **Visual Calendar**: Warm, inviting display with time, location, and all-day indicators +- **Bar Atmosphere**: Rich brown and tan colors that evoke a cozy bar environment +- **Branded Experience**: Features the Turmli Bar logo with subtle sepia toning +- **Smart Refresh**: + - Manual refresh button that fetches new data from the ICS source + - Auto-refresh countdown timer (5 minutes) + - Visual feedback showing fetch progress and results + - Displays whether data has changed since last update + +### Event Display +- Events grouped by date +- Shows next 30 days of events +- Clear time indicators for scheduled events +- "All Day" badges for full-day events +- Location information with map pin icons +- Smooth animations and hover effects + +## πŸ”§ Configuration + +### Timezone +By default, the application uses `Europe/Berlin` timezone. To change it, modify the timezone in `main.py`: + +```python +tz = pytz.timezone('Europe/Berlin') # Change to your timezone +``` + +### Calendar URL +The calendar ICS URL is configured in `main.py`. To use a different calendar: + +```python +CALENDAR_URL = "your-calendar-ics-url-here" +``` + +### Update Schedule +The calendar updates: +- Daily at 2:00 AM +- Every 4 hours throughout the day + +You can modify the schedule in the `startup_event` function in `main.py`. + +## 🌐 API Endpoints + +### `GET /` +Returns the HTML interface with calendar events + +### `GET /api/events` +Returns calendar events in JSON format: +```json +{ + "events": [ + { + "title": "Meeting", + "location": "Conference Room", + "start": "2024-01-15T10:00:00", + "end": "2024-01-15T11:00:00", + "all_day": false + } + ], + "last_updated": "2024-01-15T08:00:00" +} +``` + +### `POST /api/refresh` +Manually triggers a calendar refresh from the ICS source: +```json +{ + "status": "success", + "message": "Calendar refreshed successfully", + "events_count": 15, + "previous_count": 14, + "data_changed": true, + "last_updated": "2024-01-15T08:00:00" +} +``` + +The refresh button in the UI: +- Shows a loading spinner while fetching +- Displays success/error status +- Shows if data has changed +- Automatically reloads the page after successful fetch +- Resets the auto-refresh countdown timer + +## 🎨 Customization + +### Logo +The application displays the Turmli Bar logo (`Vektor-Logo.svg`) in the header. To use a different logo: +1. Place your logo file in the project root or `static/` directory +2. Update the logo path in `main.py` if needed + +### Styling +The interface uses warm, bar-appropriate colors with rich browns (#2c1810, #8b4513), warm tans (#f4e4d4, #d4a574), and cream accents. The design creates a cozy, inviting atmosphere while maintaining excellent readability. Colors are easy on the eyes with subtle gradients and soft shadows. You can customize the appearance by modifying the CSS in the `HTML_TEMPLATE` variable in `main.py`. + +### Event Display Period +By default, shows events for the next 30 days. Change this in the `fetch_calendar` function: +```python +cutoff = now + timedelta(days=30) # Modify the number of days +``` + +## 🚒 Deployment + +### 🐳 Container Deployment (Docker/Podman) + +#### Quick Start + +```bash +# Deploy the application +./deploy.sh start # Start on default port 8000 +PORT=8080 ./deploy.sh start # Start on custom port + +# Manage the application +./deploy.sh status # Check container status +./deploy.sh logs # View application logs +./deploy.sh stop # Stop container +./deploy.sh restart # Restart container +./deploy.sh update # Pull updates and restart +``` + +#### Container Files Included + +- **Dockerfile/Containerfile**: Python 3.13 container (uses pip for simplicity) +- **docker-compose.yml**: Docker Compose configuration +- **podman-compose.yml**: Podman Compose configuration +- **deploy.sh**: Docker deployment script +- **deploy-podman.sh**: Podman deployment script (supports rootless) + +**Note**: The container uses `pip` for dependency installation to keep things simple. Local development still uses `uv` for better package management. + +##### Podman Support + +Podman is a daemonless, rootless container engine that's a drop-in replacement for Docker: + +```bash +# Using Podman deployment script +./deploy-podman.sh start + +# Generate systemd service (Podman only) +./deploy-podman.sh systemd +``` + +See [PODMAN_README.md](PODMAN_README.md) for detailed Podman instructions. + +#### Container Features + +- **Simple Build**: Straightforward Python container +- **Health Checks**: Built-in monitoring endpoint +- **Volume Persistence**: Calendar cache persists between restarts +- **Auto-restart**: Container restarts automatically on failure + +#### Environment Variables + +Set the port if needed (default is 8000): +```bash +PORT=8080 ./deploy.sh start +``` + +Or use compose directly: +```bash +# Docker Compose +PORT=8080 docker-compose up -d + +# Podman Compose +PORT=8080 podman-compose -f podman-compose.yml up -d +``` + +#### Simple Container Commands + +```bash +# Using the deployment script (recommended) +./deploy.sh start # Build and start +./deploy.sh stop # Stop container +./deploy.sh logs # View logs +./deploy.sh status # Check health (Docker) +./deploy-podman.sh status # Check health (Podman) + +# Or use container commands directly +# Docker: +docker build -t turmli-calendar . +docker run -d -p 8000:8000 -v $(pwd)/calendar_cache.json:/app/calendar_cache.json turmli-calendar + +# Podman (rootless): +podman build -t turmli-calendar . +podman run -d -p 8000:8000 -v $(pwd)/calendar_cache.json:/app/calendar_cache.json:Z turmli-calendar +``` + +### Traditional Deployment + +For non-container deployments, use systemd: + +```ini +[Unit] +Description=Turmli Bar Calendar Tool +After=network.target + +[Service] +Type=simple +User=youruser +WorkingDirectory=/path/to/turmli-bar-calendar-tool +ExecStart=/usr/bin/uv run uvicorn main:app --host 0.0.0.0 --port 8000 +Restart=on-failure + +[Install] +WantedBy=multi-user.target +``` + +### Notes for Internal Use + +- The application runs on a single port (default 8000) +- No SSL/HTTPS needed for internal network +- Simple Python server is sufficient for ~30 users +- Calendar cache persists in `calendar_cache.json` +- Automatic refresh every 5 minutes + +## πŸ“ License + +MIT License - feel free to use and modify as needed. + +## 🀝 Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## πŸ› Troubleshooting + +### Calendar not updating +- Check the console logs for any error messages +- Verify the ICS URL is accessible +- Check the `calendar_cache.json` file for cached data + +### Events not showing +- Ensure the calendar has events in the next 30 days +- Check timezone settings match your location +- Verify the ICS file format is valid + +### Port already in use +- Change the port in `run.sh` or when running uvicorn directly +- Check for other processes using port 8000: `lsof -i :8000` diff --git a/Vektor-Logo.svg b/Vektor-Logo.svg new file mode 100644 index 0000000..0354eb9 --- /dev/null +++ b/Vektor-Logo.svg @@ -0,0 +1,179 @@ + + + + +]> + + + + + + + + + + + + + + ΓΌ + r + m + l + i + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + T + + B + a + r + * + * + + diff --git a/deploy-podman.sh b/deploy-podman.sh new file mode 100755 index 0000000..ea68ead --- /dev/null +++ b/deploy-podman.sh @@ -0,0 +1,294 @@ +#!/bin/bash + +# Turmli Bar Calendar Tool - Podman Deployment Script +# Compatible with Podman and Podman-compose + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +IMAGE_NAME="turmli-calendar" +CONTAINER_NAME="turmli-calendar" +DEFAULT_PORT=8000 +CACHE_FILE="calendar_cache.json" + +# Print functions +print_error() { + echo -e "${RED}❌ $1${NC}" +} + +print_success() { + echo -e "${GREEN}βœ… $1${NC}" +} + +print_info() { + echo -e "${BLUE}ℹ️ $1${NC}" +} + +print_warning() { + echo -e "${YELLOW}⚠️ $1${NC}" +} + +# Check for podman or docker +check_container_runtime() { + if command -v podman &> /dev/null; then + RUNTIME="podman" + print_success "Podman is installed" + elif command -v docker &> /dev/null; then + RUNTIME="docker" + print_warning "Podman not found, using Docker instead" + else + print_error "Neither Podman nor Docker is installed. Please install Podman or Docker first." + exit 1 + fi +} + +# Check for podman-compose or docker-compose +check_compose() { + if command -v podman-compose &> /dev/null; then + COMPOSE="podman-compose" + COMPOSE_FILE="podman-compose.yml" + print_success "podman-compose is installed" + elif command -v docker-compose &> /dev/null; then + COMPOSE="docker-compose" + COMPOSE_FILE="docker-compose.yml" + print_warning "podman-compose not found, using docker-compose instead" + else + print_warning "Compose tool not found. Will use standalone container commands." + COMPOSE="" + fi +} + +build() { + print_info "Building container image..." + + # Use Containerfile if it exists, otherwise fall back to Dockerfile + if [ -f "Containerfile" ]; then + BUILD_FILE="Containerfile" + else + BUILD_FILE="Dockerfile" + fi + + ${RUNTIME} build -f ${BUILD_FILE} -t ${IMAGE_NAME} . || { + print_error "Failed to build container image" + exit 1 + } + print_success "Container image built successfully" +} + +start() { + print_info "Starting calendar application..." + + # Create cache file if it doesn't exist + if [ ! -f "${CACHE_FILE}" ]; then + echo "{}" > ${CACHE_FILE} + print_info "Created cache file: ${CACHE_FILE}" + fi + + if [ -n "$COMPOSE" ] && [ -f "$COMPOSE_FILE" ]; then + # Use compose if available + print_info "Starting with ${COMPOSE}..." + ${COMPOSE} -f ${COMPOSE_FILE} up -d || { + print_error "Failed to start application with compose" + exit 1 + } + else + # Use standalone container command + print_info "Starting with ${RUNTIME}..." + + # Check if container already exists + if ${RUNTIME} ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"; then + print_info "Container already exists, removing it..." + ${RUNTIME} rm -f ${CONTAINER_NAME} + fi + + # Run container with Podman-specific options + ${RUNTIME} run -d \ + --name ${CONTAINER_NAME} \ + -p ${PORT:-$DEFAULT_PORT}:8000 \ + -e TZ=${TZ:-Europe/Berlin} \ + -e PYTHONUNBUFFERED=1 \ + -v $(pwd)/${CACHE_FILE}:/app/${CACHE_FILE}:Z \ + --restart unless-stopped \ + ${IMAGE_NAME} || { + print_error "Failed to start container" + exit 1 + } + fi + + print_success "Application started successfully" + print_info "Access the calendar at: http://localhost:${PORT:-$DEFAULT_PORT}" +} + +stop() { + print_info "Stopping calendar application..." + + if [ -n "$COMPOSE" ] && [ -f "$COMPOSE_FILE" ]; then + ${COMPOSE} -f ${COMPOSE_FILE} down || true + else + ${RUNTIME} stop ${CONTAINER_NAME} 2>/dev/null || true + ${RUNTIME} rm ${CONTAINER_NAME} 2>/dev/null || true + fi + + print_success "Application stopped" +} + +restart() { + stop + start +} + +logs() { + if [ -n "$COMPOSE" ] && [ -f "$COMPOSE_FILE" ]; then + ${COMPOSE} -f ${COMPOSE_FILE} logs -f + else + ${RUNTIME} logs -f ${CONTAINER_NAME} + fi +} + +status() { + echo -e "${BLUE}Container Status:${NC}" + + if [ -n "$COMPOSE" ] && [ -f "$COMPOSE_FILE" ]; then + ${COMPOSE} -f ${COMPOSE_FILE} ps + else + if ${RUNTIME} ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -q ${CONTAINER_NAME}; then + ${RUNTIME} ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "NAMES|${CONTAINER_NAME}" + else + print_info "Container is not running" + return + fi + fi + + echo "" + print_info "Testing application..." + if curl -s -f http://localhost:${PORT:-$DEFAULT_PORT}/api/events > /dev/null 2>&1; then + print_success "Application is healthy and responding" + else + print_error "Application is not responding" + fi +} + +clean() { + print_info "Cleaning up..." + stop + + # Remove image + ${RUNTIME} rmi ${IMAGE_NAME} 2>/dev/null || true + + # Clean up build cache (Podman specific) + if [ "$RUNTIME" = "podman" ]; then + ${RUNTIME} system prune -f 2>/dev/null || true + fi + + print_success "Cleanup complete" +} + +# Generate systemd service for rootless Podman +generate_systemd() { + if [ "$RUNTIME" != "podman" ]; then + print_error "Systemd generation is only available for Podman" + exit 1 + fi + + print_info "Generating systemd service for rootless Podman..." + + # Check if container is running + if ! ${RUNTIME} ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + print_error "Container must be running to generate systemd service" + print_info "Run './deploy-podman.sh start' first" + exit 1 + fi + + # Create user systemd directory if it doesn't exist + mkdir -p ~/.config/systemd/user/ + + # Generate systemd files + ${RUNTIME} generate systemd --name --files --new ${CONTAINER_NAME} + + # Move to user systemd directory + mv container-${CONTAINER_NAME}.service ~/.config/systemd/user/ + + print_success "Systemd service generated" + print_info "To enable the service:" + echo " systemctl --user daemon-reload" + echo " systemctl --user enable container-${CONTAINER_NAME}.service" + echo " systemctl --user start container-${CONTAINER_NAME}.service" +} + +# Show help +show_help() { + echo "Turmli Bar Calendar - Podman Deployment Script" + echo "" + echo "Usage: $0 {build|start|stop|restart|logs|status|clean|systemd|help}" + echo "" + echo "Commands:" + echo " build - Build the container image" + echo " start - Build and start the application" + echo " stop - Stop the application" + echo " restart - Restart the application" + echo " logs - Show application logs" + echo " status - Check application status" + echo " clean - Remove containers and images" + echo " systemd - Generate systemd service (Podman only)" + echo " help - Show this help message" + echo "" + echo "Environment Variables:" + echo " PORT - Port to expose (default: 8000)" + echo " TZ - Timezone (default: Europe/Berlin)" +} + +# Main script +case "$1" in + build) + check_container_runtime + build + ;; + start) + check_container_runtime + check_compose + build + start + ;; + stop) + check_container_runtime + check_compose + stop + ;; + restart) + check_container_runtime + check_compose + restart + ;; + logs) + check_container_runtime + check_compose + logs + ;; + status) + check_container_runtime + check_compose + status + ;; + clean) + check_container_runtime + clean + ;; + systemd) + check_container_runtime + generate_systemd + ;; + help) + show_help + ;; + *) + show_help + exit 1 + ;; +esac \ No newline at end of file diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..de60b7f --- /dev/null +++ b/deploy.sh @@ -0,0 +1,180 @@ +#!/bin/bash + +# Turmli Bar Calendar Tool - Simple Docker Deployment Script +# For internal use - no nginx, no SSL, just the calendar app + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +APP_NAME="turmli-calendar" +IMAGE_NAME="turmli-calendar:latest" +CONTAINER_NAME="turmli-calendar" +DEFAULT_PORT=8000 + +# Functions +print_header() { + echo -e "${BLUE}================================${NC}" + echo -e "${BLUE} Turmli Bar Calendar Deployment${NC}" + echo -e "${BLUE}================================${NC}" + echo "" +} + +print_success() { + echo -e "${GREEN}βœ“${NC} $1" +} + +print_error() { + echo -e "${RED}βœ—${NC} $1" +} + +print_info() { + echo -e "${BLUE}β„Ή${NC} $1" +} + +check_docker() { + if ! command -v docker &> /dev/null; then + print_error "Docker is not installed. Please install Docker first." + exit 1 + fi + print_success "Docker is installed" +} + +build() { + print_info "Building Docker image..." + docker build -t ${IMAGE_NAME} . || { + print_error "Failed to build Docker image" + exit 1 + } + print_success "Docker image built successfully" +} + +start() { + print_info "Starting calendar application..." + + # Check if container already exists + if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + print_info "Container already exists, removing it..." + docker rm -f ${CONTAINER_NAME} + fi + + # Get port from environment or use default + PORT="${PORT:-$DEFAULT_PORT}" + + # Run container + docker run -d \ + --name ${CONTAINER_NAME} \ + -p ${PORT}:8000 \ + -v $(pwd)/calendar_cache.json:/app/calendar_cache.json \ + -e TZ=Europe/Berlin \ + --restart unless-stopped \ + ${IMAGE_NAME} + + print_success "Application started on port ${PORT}" + print_info "Access the calendar at: http://localhost:${PORT}" +} + +stop() { + print_info "Stopping calendar application..." + docker stop ${CONTAINER_NAME} 2>/dev/null || true + docker rm ${CONTAINER_NAME} 2>/dev/null || true + print_success "Application stopped" +} + +restart() { + stop + start +} + +logs() { + docker logs -f ${CONTAINER_NAME} +} + +status() { + echo -e "${BLUE}Container Status:${NC}" + if docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -q ${CONTAINER_NAME}; then + docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "NAMES|${CONTAINER_NAME}" + echo "" + print_info "Testing application..." + if curl -s -f http://localhost:${PORT:-$DEFAULT_PORT}/api/events > /dev/null 2>&1; then + print_success "Application is healthy and responding" + else + print_error "Application is not responding" + fi + else + print_info "Container is not running" + fi +} + +update() { + print_info "Updating application..." + + # Pull latest changes if using git + if [ -d .git ]; then + print_info "Pulling latest changes..." + git pull + fi + + # Rebuild and restart + build + restart + print_success "Application updated" +} + +# Parse command +print_header + +case "$1" in + build) + check_docker + build + ;; + start) + check_docker + build + start + ;; + stop) + stop + ;; + restart) + check_docker + restart + ;; + logs) + logs + ;; + status) + status + ;; + update) + check_docker + update + ;; + *) + echo "Usage: $0 {build|start|stop|restart|logs|status|update}" + echo "" + echo "Commands:" + echo " build - Build Docker image" + echo " start - Build and start the application" + echo " stop - Stop the application" + echo " restart - Restart the application" + echo " logs - View application logs" + echo " status - Check application status" + echo " update - Update and restart application" + echo "" + echo "Environment variables:" + echo " PORT - Port to expose (default: 8000)" + echo "" + echo "Example:" + echo " $0 start # Start on port 8000" + echo " PORT=8080 $0 start # Start on port 8080" + exit 1 + ;; +esac \ No newline at end of file diff --git a/deploy_rpi.sh b/deploy_rpi.sh new file mode 100755 index 0000000..f38d3f9 --- /dev/null +++ b/deploy_rpi.sh @@ -0,0 +1,674 @@ +#!/bin/bash + +# Turmli Bar Calendar Tool - Raspberry Pi Zero Deployment Script +# Deploys the application as a systemd service without Docker +# Optimized for low resource usage on Raspberry Pi Zero + +set -e # Exit on error + +# ============================================================================ +# Configuration +# ============================================================================ + +APP_NAME="turmli-calendar" +APP_DIR="/opt/turmli-calendar" +SERVICE_NAME="turmli-calendar.service" +SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}" +APP_USER="pi" +APP_PORT="8000" +PYTHON_VERSION="3" +REPO_DIR="$(dirname "$(readlink -f "$0")")" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +MAGENTA='\033[0;35m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# ============================================================================ +# Helper Functions +# ============================================================================ + +print_header() { + echo + echo -e "${MAGENTA}╔════════════════════════════════════════════╗${NC}" + echo -e "${MAGENTA}β•‘ Turmli Calendar - Raspberry Pi Deployment β•‘${NC}" + echo -e "${MAGENTA}β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•${NC}" + echo +} + +print_section() { + echo + echo -e "${CYAN}━━━ $1 ━━━${NC}" + echo +} + +print_success() { + echo -e "${GREEN}βœ“${NC} $1" +} + +print_error() { + echo -e "${RED}βœ—${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}⚠${NC} $1" +} + +print_info() { + echo -e "${BLUE}β„Ή${NC} $1" +} + +check_root() { + if [[ $EUID -ne 0 ]]; then + print_error "This script must be run as root (use sudo)" + exit 1 + fi +} + +check_os() { + if ! grep -q "Raspbian\|Raspberry Pi OS" /etc/os-release 2>/dev/null; then + print_warning "This script is optimized for Raspberry Pi OS" + read -p "Continue anyway? (y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi + fi +} + +# ============================================================================ +# System Checks and Prerequisites +# ============================================================================ + +check_system() { + print_section "System Check" + + # Check if running as root + check_root + + # Check OS + check_os + + # Check available memory + local mem_total=$(grep MemTotal /proc/meminfo | awk '{print $2}') + if [ "$mem_total" -lt 400000 ]; then + print_warning "Low memory detected ($(($mem_total/1024))MB). This is expected for Pi Zero." + fi + + # Check available disk space + local disk_free=$(df /opt 2>/dev/null | tail -1 | awk '{print $4}') + if [ "$disk_free" -lt 500000 ]; then + print_warning "Low disk space available. Need at least 500MB free." + print_info "Current free space: $(($disk_free/1024))MB" + fi + + # Check /tmp space + local tmp_free=$(df /tmp 2>/dev/null | tail -1 | awk '{print $4}') + if [ "$tmp_free" -lt 200000 ]; then + print_warning "/tmp has limited space. Will use alternative temp directory." + fi + + # Check Python + if ! command -v python${PYTHON_VERSION} &> /dev/null; then + print_error "Python ${PYTHON_VERSION} is not installed" + return 1 + fi + print_success "Python $(python${PYTHON_VERSION} --version 2>&1 | cut -d' ' -f2) installed" + + # Check pip + if ! python${PYTHON_VERSION} -m pip --version &> /dev/null; then + print_warning "pip not found, installing..." + apt-get update + apt-get install -y python3-pip + fi + print_success "pip installed" + + # Check git (optional, for updates) + if command -v git &> /dev/null; then + print_success "git installed (optional)" + else + print_info "git not installed (optional, needed for updates)" + fi + + return 0 +} + +install_dependencies() { + print_section "Installing System Dependencies" + + # Update package list + print_info "Updating package list..." + apt-get update + + # Install system dependencies + print_info "Installing system packages..." + apt-get install -y \ + python3-dev \ + python3-venv \ + python3-pip \ + build-essential \ + libffi-dev \ + libssl-dev \ + libxml2-dev \ + libxslt1-dev \ + curl \ + systemd + + # Clean apt cache to free up space + apt-get clean + apt-get autoremove -y + + print_success "System dependencies installed" +} + +# ============================================================================ +# Application Setup +# ============================================================================ + +create_app_directory() { + print_section "Creating Application Directory" + + # Create directory + if [ -d "$APP_DIR" ]; then + print_warning "Directory $APP_DIR already exists" + read -p "Remove existing installation? (y/N): " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + print_info "Removing existing installation..." + systemctl stop ${SERVICE_NAME} 2>/dev/null || true + rm -rf "$APP_DIR" + else + print_error "Installation cancelled" + exit 1 + fi + fi + + mkdir -p "$APP_DIR" + print_success "Created $APP_DIR" +} + +copy_application_files() { + print_section "Copying Application Files" + + # Copy necessary files + print_info "Copying application files..." + cp -v "$REPO_DIR/main.py" "$APP_DIR/" + + # Use RPi-optimized requirements if available, otherwise fallback to standard + if [ -f "$REPO_DIR/requirements-rpi.txt" ]; then + print_info "Using Raspberry Pi optimized requirements" + cp -v "$REPO_DIR/requirements-rpi.txt" "$APP_DIR/requirements.txt" + else + cp -v "$REPO_DIR/requirements.txt" "$APP_DIR/" + fi + + # Copy logo if exists + if [ -f "$REPO_DIR/Vektor-Logo.svg" ]; then + cp -v "$REPO_DIR/Vektor-Logo.svg" "$APP_DIR/" + fi + + # Create static directory + mkdir -p "$APP_DIR/static" + if [ -d "$REPO_DIR/static" ]; then + cp -r "$REPO_DIR/static/"* "$APP_DIR/static/" 2>/dev/null || true + fi + + # Copy logo to static directory + if [ -f "$APP_DIR/Vektor-Logo.svg" ]; then + cp "$APP_DIR/Vektor-Logo.svg" "$APP_DIR/static/logo.svg" + fi + + # Create empty cache file + touch "$APP_DIR/calendar_cache.json" + + # Set ownership + chown -R ${APP_USER}:${APP_USER} "$APP_DIR" + + print_success "Application files copied" +} + +setup_python_environment() { + print_section "Setting Up Python Environment" + + # Create virtual environment to isolate dependencies + print_info "Creating Python virtual environment..." + sudo -u ${APP_USER} python${PYTHON_VERSION} -m venv "$APP_DIR/venv" + + # Upgrade pip in virtual environment + print_info "Upgrading pip..." + sudo -u ${APP_USER} "$APP_DIR/venv/bin/pip" install --upgrade pip wheel setuptools + + # Set temporary directory to avoid filling up /tmp (which is in RAM on Pi) + export TMPDIR="$APP_DIR/tmp" + mkdir -p "$TMPDIR" + chown ${APP_USER}:${APP_USER} "$TMPDIR" + + # Install requirements + print_info "Installing Python dependencies (this may take a while on Pi Zero)..." + print_warning "This process might take 10-20 minutes on a Raspberry Pi Zero" + + # Install with optimizations for low memory and ARM architecture + sudo -u ${APP_USER} TMPDIR="$TMPDIR" "$APP_DIR/venv/bin/pip" install \ + --no-cache-dir \ + --prefer-binary \ + --no-build-isolation \ + --no-deps \ + -r "$APP_DIR/requirements.txt" + + # Install dependencies separately to handle failures better + sudo -u ${APP_USER} TMPDIR="$TMPDIR" "$APP_DIR/venv/bin/pip" install \ + --no-cache-dir \ + --prefer-binary \ + --no-build-isolation \ + -r "$APP_DIR/requirements.txt" + + # Clean up temp directory + rm -rf "$TMPDIR" + + print_success "Python environment set up" +} + +# ============================================================================ +# Systemd Service Setup +# ============================================================================ + +create_systemd_service() { + print_section "Creating Systemd Service" + + # Create service file + cat > "$SERVICE_FILE" << EOF +[Unit] +Description=Turmli Bar Calendar Tool +Documentation=https://github.com/turmli/bar-calendar-tool +After=network.target network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=${APP_USER} +Group=${APP_USER} +WorkingDirectory=${APP_DIR} +Environment="PATH=/opt/turmli-calendar/venv/bin:/home/turmli/.local/bin:/usr/local/bin:/usr/bin:/bin" +Environment="PYTHONPATH=${APP_DIR}" +Environment="TZ=Europe/Berlin" + +# Use virtual environment Python with uvicorn +ExecStart=${APP_DIR}/venv/bin/python -m uvicorn main:app --host 0.0.0.0 --port ${APP_PORT} --workers 1 --log-level info + +# Restart policy +Restart=always +RestartSec=10 +StartLimitInterval=200 +StartLimitBurst=5 + +# Resource limits for Raspberry Pi Zero +MemoryMax=256M +CPUQuota=75% + +# Security hardening +PrivateTmp=true +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=${APP_DIR}/calendar_cache.json ${APP_DIR}/static + +# Logging +StandardOutput=journal +StandardError=journal +SyslogIdentifier=${APP_NAME} + +[Install] +WantedBy=multi-user.target +EOF + + print_success "Service file created at $SERVICE_FILE" + + # Reload systemd + systemctl daemon-reload + print_success "Systemd configuration reloaded" +} + +# ============================================================================ +# Service Management Functions +# ============================================================================ + +start_service() { + print_section "Starting Service" + + systemctl enable ${SERVICE_NAME} + systemctl start ${SERVICE_NAME} + + # Wait for service to start + sleep 3 + + if systemctl is-active --quiet ${SERVICE_NAME}; then + print_success "Service started successfully" + + # Test if application is responding + sleep 2 + if curl -s -f "http://localhost:${APP_PORT}/api/events" > /dev/null 2>&1; then + print_success "Application is responding on port ${APP_PORT}" + else + print_warning "Service is running but not responding yet (may still be starting)" + fi + else + print_error "Failed to start service" + print_info "Check logs with: journalctl -u ${SERVICE_NAME} -n 50" + return 1 + fi +} + +stop_service() { + print_section "Stopping Service" + + if systemctl is-active --quiet ${SERVICE_NAME}; then + systemctl stop ${SERVICE_NAME} + print_success "Service stopped" + else + print_info "Service is not running" + fi +} + +restart_service() { + print_section "Restarting Service" + + systemctl restart ${SERVICE_NAME} + sleep 2 + + if systemctl is-active --quiet ${SERVICE_NAME}; then + print_success "Service restarted successfully" + else + print_error "Failed to restart service" + return 1 + fi +} + +# ============================================================================ +# Status and Monitoring +# ============================================================================ + +show_status() { + print_section "Service Status" + + # Show service status + systemctl status ${SERVICE_NAME} --no-pager + + echo + print_section "Application Health" + + # Check if responding + if curl -s -f "http://localhost:${APP_PORT}/api/events" > /dev/null 2>&1; then + print_success "Application is healthy and responding" + + # Get event count + local events=$(curl -s "http://localhost:${APP_PORT}/api/events" | python3 -c "import sys, json; data=json.load(sys.stdin); print(len(data.get('events', [])))" 2>/dev/null || echo "unknown") + print_info "Calendar has $events events" + else + print_error "Application is not responding" + fi + + # Show resource usage + echo + print_section "Resource Usage" + + local pid=$(systemctl show ${SERVICE_NAME} -p MainPID --value) + if [ "$pid" != "0" ]; then + if [ -f "/proc/$pid/status" ]; then + local mem_usage=$(grep VmRSS /proc/$pid/status | awk '{print $2/1024 " MB"}') + print_info "Memory usage: $mem_usage" + fi + fi + + # Show recent logs + echo + print_section "Recent Logs" + journalctl -u ${SERVICE_NAME} -n 20 --no-pager +} + +show_logs() { + print_section "Application Logs" + journalctl -u ${SERVICE_NAME} -f +} + +# ============================================================================ +# Update Function +# ============================================================================ + +update_application() { + print_section "Updating Application" + + # Stop service + stop_service + + # Backup current installation + print_info "Creating backup..." + cp -r "$APP_DIR" "${APP_DIR}.backup.$(date +%Y%m%d_%H%M%S)" + + # Copy new files + print_info "Copying updated files..." + cp -v "$REPO_DIR/main.py" "$APP_DIR/" + + # Use RPi-optimized requirements if available + if [ -f "$REPO_DIR/requirements-rpi.txt" ]; then + cp -v "$REPO_DIR/requirements-rpi.txt" "$APP_DIR/requirements.txt" + else + cp -v "$REPO_DIR/requirements.txt" "$APP_DIR/" + fi + + if [ -f "$REPO_DIR/Vektor-Logo.svg" ]; then + cp -v "$REPO_DIR/Vektor-Logo.svg" "$APP_DIR/" + cp "$APP_DIR/Vektor-Logo.svg" "$APP_DIR/static/logo.svg" + fi + + # Update dependencies with temp directory + print_info "Updating Python dependencies..." + export TMPDIR="$APP_DIR/tmp" + mkdir -p "$TMPDIR" + chown ${APP_USER}:${APP_USER} "$TMPDIR" + + sudo -u ${APP_USER} TMPDIR="$TMPDIR" "$APP_DIR/venv/bin/pip" install \ + --no-cache-dir \ + --prefer-binary \ + --no-build-isolation \ + -r "$APP_DIR/requirements.txt" + + # Clean up temp directory + rm -rf "$TMPDIR" + + # Set ownership + chown -R ${APP_USER}:${APP_USER} "$APP_DIR" + + # Restart service + start_service + + print_success "Application updated successfully" +} + +# ============================================================================ +# Uninstall Function +# ============================================================================ + +uninstall() { + print_section "Uninstalling Application" + + print_warning "This will remove the Turmli Calendar application" + read -p "Are you sure? (y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + print_info "Uninstall cancelled" + return + fi + + # Stop and disable service + systemctl stop ${SERVICE_NAME} 2>/dev/null || true + systemctl disable ${SERVICE_NAME} 2>/dev/null || true + + # Remove service file + rm -f "$SERVICE_FILE" + systemctl daemon-reload + + # Remove application directory + rm -rf "$APP_DIR" + + print_success "Application uninstalled" +} + +# ============================================================================ +# Performance Optimization for Pi Zero +# ============================================================================ + +optimize_for_pi_zero() { + print_section "Optimizing for Raspberry Pi Zero" + + # Disable unnecessary services to free up resources + print_info "Checking for unnecessary services..." + + local services_to_check="bluetooth cups avahi-daemon triggerhappy" + for service in $services_to_check; do + if systemctl is-enabled --quiet "$service" 2>/dev/null; then + print_info "Consider disabling $service to free resources" + fi + done + + # Configure swap if needed + local swap_total=$(grep SwapTotal /proc/meminfo | awk '{print $2}') + if [ "$swap_total" -lt 100000 ]; then + print_warning "Low swap space detected. Consider increasing swap size." + print_info "Edit /etc/dphys-swapfile and set CONF_SWAPSIZE=256" + fi + + # Set CPU governor for better performance + if [ -f /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor ]; then + local governor=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor) + if [ "$governor" != "performance" ]; then + print_info "Current CPU governor: $governor" + print_info "Consider setting to 'performance' for better response times" + fi + fi + + print_success "Optimization check complete" +} + +# ============================================================================ +# Main Installation Function +# ============================================================================ + +install() { + print_header + + # Run checks + check_system || exit 1 + + # Install dependencies + install_dependencies + + # Setup application + create_app_directory + copy_application_files + setup_python_environment + + # Setup service + create_systemd_service + + # Optimize for Pi Zero + optimize_for_pi_zero + + # Start service + start_service + + # Show final status + print_section "Installation Complete!" + print_success "Turmli Calendar is now running" + echo + print_info "Access the calendar at:" + print_info " http://$(hostname -I | cut -d' ' -f1):${APP_PORT}" + print_info " http://localhost:${APP_PORT}" + echo + print_info "Useful commands:" + print_info " sudo systemctl status ${SERVICE_NAME} - Check status" + print_info " sudo systemctl restart ${SERVICE_NAME} - Restart service" + print_info " sudo journalctl -u ${SERVICE_NAME} -f - View logs" + print_info " sudo $0 status - Show detailed status" + echo +} + +# ============================================================================ +# Command Line Interface +# ============================================================================ + +print_usage() { + echo "Usage: sudo $0 {install|start|stop|restart|status|logs|update|uninstall|optimize}" + echo + echo "Commands:" + echo " install - Full installation (first time setup)" + echo " start - Start the service" + echo " stop - Stop the service" + echo " restart - Restart the service" + echo " status - Show service status and health" + echo " logs - Follow application logs" + echo " update - Update application from current directory" + echo " uninstall - Remove application completely" + echo " optimize - Check and suggest optimizations for Pi Zero" + echo + echo "Examples:" + echo " sudo $0 install # Initial installation" + echo " sudo $0 status # Check if running properly" + echo " sudo $0 logs # View real-time logs" + echo + echo "Configuration:" + echo " Port: ${APP_PORT}" + echo " Directory: ${APP_DIR}" + echo " Service: ${SERVICE_NAME}" +} + +# ============================================================================ +# Main Script Entry Point +# ============================================================================ + +case "$1" in + install) + check_root + install + ;; + start) + check_root + start_service + ;; + stop) + check_root + stop_service + ;; + restart) + check_root + restart_service + ;; + status) + show_status + ;; + logs) + show_logs + ;; + update) + check_root + update_application + ;; + uninstall) + check_root + uninstall + ;; + optimize) + check_root + optimize_for_pi_zero + ;; + *) + print_header + print_usage + exit 1 + ;; +esac + +exit 0 diff --git a/deployment/README.md b/deployment/README.md new file mode 100644 index 0000000..12e9f00 --- /dev/null +++ b/deployment/README.md @@ -0,0 +1,51 @@ +# πŸ“… Turmli Calendar - Raspberry Pi Deployment Guide + +This guide provides instructions for deploying the Turmli Bar Calendar Tool on a Raspberry Pi Zero (or other Raspberry Pi models) as a lightweight systemd service without Docker. + +## 🎯 Why No Docker? + +On resource-constrained devices like the Raspberry Pi Zero (512MB RAM, single-core CPU), Docker adds unnecessary overhead: +- Docker daemon uses ~50-100MB RAM +- Container layer adds ~20-30MB overhead +- Additional CPU usage for containerization +- Slower startup times + +Running directly as a systemd service provides: +- βœ… Minimal resource usage +- βœ… Faster startup +- βœ… Direct hardware access +- βœ… Simple management +- βœ… Native systemd integration + +## πŸ“‹ Prerequisites + +### Hardware Requirements +- **Raspberry Pi Zero/Zero W** (minimum) or any Raspberry Pi model +- **SD Card**: 8GB minimum (Class 10 or better recommended) +- **Network**: Ethernet adapter or WiFi +- **Power**: Stable 5V power supply + +### Software Requirements +- **OS**: Raspberry Pi OS Lite (32-bit recommended) +- **Python**: 3.9 or newer +- **Memory**: ~100-150MB free RAM +- **Storage**: ~200MB free space + +## πŸš€ Quick Installation + +```bash +# 1. Copy the deployment script to your Pi +scp deploy_rpi.sh pi@raspberrypi:/home/pi/ + +# 2. SSH into your Pi +ssh pi@raspberrypi + +# 3. Clone or copy the application +git clone turmli-calendar +cd turmli-calendar + +# 4. Run the deployment script +sudo ./deploy_rpi.sh install +``` + +The application will be available at `http://:8000` diff --git a/deployment/monitor.sh b/deployment/monitor.sh new file mode 100755 index 0000000..fbe13d4 --- /dev/null +++ b/deployment/monitor.sh @@ -0,0 +1,389 @@ +#!/bin/bash + +# Turmli Calendar - Raspberry Pi Monitoring Script +# Monitors the health and performance of the deployed application + +set -e + +# Configuration +SERVICE_NAME="turmli-calendar" +APP_PORT="8000" +LOG_FILE="/var/log/turmli-calendar/monitor.log" +ALERT_THRESHOLD_MEM=200 # MB +ALERT_THRESHOLD_CPU=80 # Percentage + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +# Helper functions +print_header() { + echo -e "${CYAN}════════════════════════════════════════════════${NC}" + echo -e "${CYAN} Turmli Calendar - System Monitor${NC}" + echo -e "${CYAN} $(date '+%Y-%m-%d %H:%M:%S')${NC}" + echo -e "${CYAN}════════════════════════════════════════════════${NC}" + echo +} + +print_section() { + echo -e "${BLUE}━━━ $1 ━━━${NC}" +} + +print_ok() { + echo -e "${GREEN}βœ“${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}⚠${NC} $1" +} + +print_error() { + echo -e "${RED}βœ—${NC} $1" +} + +# System information +show_system_info() { + print_section "System Information" + + # Hostname and OS + echo "Hostname: $(hostname)" + echo "OS: $(cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)" + echo "Kernel: $(uname -r)" + echo "Uptime: $(uptime -p)" + + # CPU info + echo "CPU: $(cat /proc/cpuinfo | grep 'model name' | head -1 | cut -d':' -f2 | xargs)" + echo "Cores: $(nproc)" + + # Temperature + if command -v vcgencmd &> /dev/null; then + local temp=$(vcgencmd measure_temp | cut -d'=' -f2) + echo "Temperature: $temp" + + # Check throttling + local throttled=$(vcgencmd get_throttled | cut -d'=' -f2) + if [ "$throttled" != "0x0" ]; then + print_warning "CPU throttling detected: $throttled" + fi + fi + echo +} + +# Service status +check_service_status() { + print_section "Service Status" + + if systemctl is-active --quiet ${SERVICE_NAME}; then + print_ok "Service is running" + + # Get PID and uptime + local pid=$(systemctl show ${SERVICE_NAME} -p MainPID --value) + if [ "$pid" != "0" ]; then + echo "PID: $pid" + + # Process uptime + if [ -f "/proc/$pid/stat" ]; then + local start_time=$(stat -c %Y /proc/$pid) + local current_time=$(date +%s) + local uptime=$((current_time - start_time)) + echo "Process uptime: $((uptime / 3600))h $((uptime % 3600 / 60))m" + fi + fi + else + print_error "Service is not running" + echo "Last exit status: $(systemctl show ${SERVICE_NAME} -p ExecMainStatus --value)" + fi + + # Show recent restarts + local restarts=$(systemctl show ${SERVICE_NAME} -p NRestarts --value) + if [ "$restarts" -gt 0 ]; then + print_warning "Service has restarted $restarts times" + fi + echo +} + +# Application health check +check_application_health() { + print_section "Application Health" + + # Test API endpoint + if curl -s -f -m 5 "http://localhost:${APP_PORT}/api/events" > /dev/null 2>&1; then + print_ok "API endpoint is responding" + + # Get event count + local response=$(curl -s "http://localhost:${APP_PORT}/api/events" 2>/dev/null) + if [ ! -z "$response" ]; then + local events=$(echo "$response" | python3 -c "import sys, json; data=json.load(sys.stdin); print(len(data.get('events', [])))" 2>/dev/null || echo "unknown") + echo "Calendar events: $events" + + # Check last update time + local last_updated=$(echo "$response" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('last_updated', 'unknown'))" 2>/dev/null || echo "unknown") + echo "Last updated: $last_updated" + fi + else + print_error "API endpoint not responding" + fi + + # Test web interface + if curl -s -f -m 5 "http://localhost:${APP_PORT}/" > /dev/null 2>&1; then + print_ok "Web interface is accessible" + else + print_error "Web interface not accessible" + fi + echo +} + +# Resource usage +show_resource_usage() { + print_section "Resource Usage" + + # Memory usage + local mem_total=$(free -m | grep Mem | awk '{print $2}') + local mem_used=$(free -m | grep Mem | awk '{print $3}') + local mem_percent=$((mem_used * 100 / mem_total)) + echo "System Memory: ${mem_used}/${mem_total} MB (${mem_percent}%)" + + if [ "$mem_percent" -gt 90 ]; then + print_warning "High memory usage detected" + fi + + # Swap usage + local swap_total=$(free -m | grep Swap | awk '{print $2}') + local swap_used=$(free -m | grep Swap | awk '{print $3}') + if [ "$swap_total" -gt 0 ]; then + local swap_percent=$((swap_used * 100 / swap_total)) + echo "Swap: ${swap_used}/${swap_total} MB (${swap_percent}%)" + + if [ "$swap_percent" -gt 50 ]; then + print_warning "High swap usage - performance may be degraded" + fi + fi + + # Process-specific memory + local pid=$(systemctl show ${SERVICE_NAME} -p MainPID --value) + if [ "$pid" != "0" ] && [ -f "/proc/$pid/status" ]; then + local proc_mem=$(grep VmRSS /proc/$pid/status | awk '{print $2/1024}') + printf "Process Memory: %.1f MB\n" "$proc_mem" + + if (( $(echo "$proc_mem > $ALERT_THRESHOLD_MEM" | bc -l) )); then + print_warning "Process memory exceeds threshold (${ALERT_THRESHOLD_MEM} MB)" + fi + fi + + # CPU usage + echo + echo "CPU Load:" + local load=$(uptime | awk -F'load average:' '{print $2}') + echo " Load average: $load" + + # Process CPU usage (rough estimate) + if [ "$pid" != "0" ] && [ -f "/proc/$pid/stat" ]; then + local cpu_usage=$(ps -p $pid -o %cpu= | tr -d ' ') + printf " Process CPU: %.1f%%\n" "$cpu_usage" + + if (( $(echo "$cpu_usage > $ALERT_THRESHOLD_CPU" | bc -l) )); then + print_warning "High CPU usage detected" + fi + fi + echo +} + +# Disk usage +show_disk_usage() { + print_section "Disk Usage" + + # Root filesystem + local disk_usage=$(df -h / | tail -1) + echo "Root filesystem:" + echo " $disk_usage" + + local disk_percent=$(df / | tail -1 | awk '{print $5}' | tr -d '%') + if [ "$disk_percent" -gt 80 ]; then + print_warning "Disk usage above 80%" + fi + + # Application directory + if [ -d "/opt/turmli-calendar" ]; then + local app_size=$(du -sh /opt/turmli-calendar 2>/dev/null | cut -f1) + echo "Application directory: $app_size" + fi + + # Log directory + if [ -d "/var/log/turmli-calendar" ]; then + local log_size=$(du -sh /var/log/turmli-calendar 2>/dev/null | cut -f1) + echo "Log directory: $log_size" + fi + echo +} + +# Network statistics +show_network_stats() { + print_section "Network Statistics" + + # Network interfaces + local interfaces=$(ip -brief link show | grep UP | awk '{print $1}') + for iface in $interfaces; do + if [[ "$iface" != "lo" ]]; then + echo "Interface: $iface" + local ip=$(ip -brief addr show $iface | awk '{print $3}') + echo " IP: $ip" + + # Connection count + local connections=$(ss -tan | grep :${APP_PORT} | grep ESTAB | wc -l) + echo " Active connections on port ${APP_PORT}: $connections" + fi + done + echo +} + +# Recent errors +show_recent_errors() { + print_section "Recent Errors (last 10)" + + journalctl -u ${SERVICE_NAME} -p err -n 10 --no-pager 2>/dev/null || echo "No recent errors" + echo +} + +# Performance summary +show_performance_summary() { + print_section "Performance Summary" + + local status="HEALTHY" + local issues=0 + + # Check service + if ! systemctl is-active --quiet ${SERVICE_NAME}; then + status="CRITICAL" + ((issues++)) + print_error "Service not running" + fi + + # Check API + if ! curl -s -f -m 5 "http://localhost:${APP_PORT}/api/events" > /dev/null 2>&1; then + status="DEGRADED" + ((issues++)) + print_warning "API not responding" + fi + + # Check memory + local mem_percent=$(free -m | grep Mem | awk '{print ($3*100)/$2}' | cut -d'.' -f1) + if [ "$mem_percent" -gt 90 ]; then + status="DEGRADED" + ((issues++)) + print_warning "High memory usage" + fi + + # Check disk + local disk_percent=$(df / | tail -1 | awk '{print $5}' | tr -d '%') + if [ "$disk_percent" -gt 90 ]; then + status="DEGRADED" + ((issues++)) + print_warning "High disk usage" + fi + + echo + if [ "$issues" -eq 0 ]; then + print_ok "System Status: $status" + elif [ "$issues" -lt 2 ]; then + print_warning "System Status: $status ($issues issue)" + else + print_error "System Status: $status ($issues issues)" + fi + echo +} + +# Continuous monitoring mode +monitor_continuous() { + while true; do + clear + print_header + check_service_status + check_application_health + show_resource_usage + show_performance_summary + + echo "Press Ctrl+C to exit" + echo "Refreshing in 30 seconds..." + sleep 30 + done +} + +# Log monitoring data +log_metrics() { + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + local pid=$(systemctl show ${SERVICE_NAME} -p MainPID --value) + local mem_used=$(free -m | grep Mem | awk '{print $3}') + local cpu_load=$(uptime | awk -F'load average:' '{print $2}' | cut -d',' -f1) + local api_status="DOWN" + + if curl -s -f -m 5 "http://localhost:${APP_PORT}/api/events" > /dev/null 2>&1; then + api_status="UP" + fi + + echo "$timestamp,mem=$mem_used,cpu=$cpu_load,api=$api_status" >> "$LOG_FILE" +} + +# Command line interface +case "$1" in + status) + print_header + check_service_status + check_application_health + show_performance_summary + ;; + full) + print_header + show_system_info + check_service_status + check_application_health + show_resource_usage + show_disk_usage + show_network_stats + show_recent_errors + show_performance_summary + ;; + monitor) + monitor_continuous + ;; + resources) + print_header + show_resource_usage + show_disk_usage + ;; + health) + print_header + check_application_health + ;; + errors) + print_header + show_recent_errors + ;; + log) + log_metrics + echo "Metrics logged to $LOG_FILE" + ;; + *) + echo "Usage: $0 {status|full|monitor|resources|health|errors|log}" + echo + echo "Commands:" + echo " status - Quick status check" + echo " full - Complete system analysis" + echo " monitor - Continuous monitoring (30s refresh)" + echo " resources - Resource usage details" + echo " health - Application health check" + echo " errors - Show recent errors" + echo " log - Log metrics to file" + echo + echo "Examples:" + echo " $0 status # Quick status" + echo " $0 monitor # Live monitoring" + echo " $0 full # Full report" + exit 1 + ;; +esac + +exit 0 \ No newline at end of file diff --git a/deployment/turmli-calendar.service b/deployment/turmli-calendar.service new file mode 100644 index 0000000..9c6119b --- /dev/null +++ b/deployment/turmli-calendar.service @@ -0,0 +1,42 @@ +[Unit] +Description=Turmli Bar Calendar Tool +Documentation=https://github.com/turmli/bar-calendar-tool +After=network.target network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=pi +Group=pi +WorkingDirectory=/opt/turmli-calendar +Environment="PATH=/opt/turmli-calendar/venv/bin:/usr/local/bin:/usr/bin:/bin" +Environment="PYTHONPATH=/opt/turmli-calendar" +Environment="TZ=Europe/Berlin" + +# Use virtual environment Python with uvicorn +ExecStart=/opt/turmli-calendar/venv/bin/python -m uvicorn main:app --host 0.0.0.0 --port 8000 --workers 1 --log-level info + +# Restart policy +Restart=always +RestartSec=10 +StartLimitInterval=200 +StartLimitBurst=5 + +# Resource limits for Raspberry Pi Zero +MemoryMax=256M +CPUQuota=75% + +# Security hardening +PrivateTmp=true +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/opt/turmli-calendar/calendar_cache.json /opt/turmli-calendar/static + +# Logging +StandardOutput=journal +StandardError=journal +SyslogIdentifier=turmli-calendar + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..37a9f70 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +version: '3.8' + +services: + calendar: + build: . + container_name: turmli-calendar + ports: + - "${PORT:-8000}:8000" + environment: + - TZ=${TZ:-Europe/Berlin} + - PYTHONUNBUFFERED=1 + volumes: + # Persist calendar cache between restarts + - ./calendar_cache.json:/app/calendar_cache.json + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/api/events"] + interval: 30s + timeout: 10s + retries: 3 \ No newline at end of file diff --git a/fix_install_rpi.sh b/fix_install_rpi.sh new file mode 100755 index 0000000..2008a10 --- /dev/null +++ b/fix_install_rpi.sh @@ -0,0 +1,240 @@ +#!/bin/bash + +# Fix script for Raspberry Pi installation issues +# Specifically handles the "No space left on device" error during pip install + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +print_header() { + echo + echo -e "${BLUE}════════════════════════════════════════════════${NC}" + echo -e "${BLUE} Turmli Calendar - Installation Fix for RPi${NC}" + echo -e "${BLUE}════════════════════════════════════════════════${NC}" + echo +} + +print_success() { + echo -e "${GREEN}βœ“${NC} $1" +} + +print_error() { + echo -e "${RED}βœ—${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}⚠${NC} $1" +} + +print_info() { + echo -e "${BLUE}β„Ή${NC} $1" +} + +# Configuration +APP_DIR="/opt/turmli-calendar" +APP_USER="pi" + +print_header + +# Check if running as root +if [[ $EUID -ne 0 ]]; then + print_error "This script must be run as root (use sudo)" + exit 1 +fi + +# Step 1: Clean up temporary files +print_info "Cleaning up temporary files..." +rm -rf /tmp/pip-* +rm -rf /tmp/tmp* +apt-get clean +apt-get autoremove -y +print_success "Temporary files cleaned" + +# Step 2: Check available space +print_info "Checking available space..." +echo "Disk usage:" +df -h / /tmp /opt +echo + +# Step 3: Create alternative temp directory +print_info "Creating alternative temp directory..." +CUSTOM_TEMP="$APP_DIR/tmp" +mkdir -p "$CUSTOM_TEMP" +chown ${APP_USER}:${APP_USER} "$CUSTOM_TEMP" +print_success "Temp directory created at $CUSTOM_TEMP" + +# Step 4: Create optimized requirements file +print_info "Creating optimized requirements file..." +cat > "$APP_DIR/requirements-rpi.txt" << 'EOF' +# Optimized requirements for Raspberry Pi +# Avoids packages that require compilation + +fastapi>=0.104.0 +# Use uvicorn without extras to avoid watchfiles compilation +uvicorn>=0.24.0 +httpx>=0.25.0 +icalendar>=5.0.0 +jinja2>=3.1.0 +python-multipart>=0.0.6 +apscheduler>=3.10.0 +pytz>=2023.3 + +# These are the problematic packages we're excluding: +# - watchfiles (requires Rust compilation) +# - websockets with speedups +# - httptools (requires C compilation) +# - python-dotenv (not needed for production) +# - uvloop (requires C compilation) +EOF +print_success "Optimized requirements file created" + +# Step 5: Install Python packages with custom temp directory +print_info "Installing Python dependencies..." +print_warning "This may take 10-20 minutes on Raspberry Pi Zero" + +# First, upgrade pip and basic tools +sudo -u ${APP_USER} TMPDIR="$CUSTOM_TEMP" "$APP_DIR/venv/bin/pip" install \ + --no-cache-dir \ + --upgrade pip wheel setuptools + +# Install packages one by one to handle failures better +PACKAGES=( + "fastapi>=0.104.0" + "uvicorn>=0.24.0" + "httpx>=0.25.0" + "icalendar>=5.0.0" + "jinja2>=3.1.0" + "python-multipart>=0.0.6" + "apscheduler>=3.10.0" + "pytz>=2023.3" +) + +for package in "${PACKAGES[@]}"; do + print_info "Installing $package..." + sudo -u ${APP_USER} TMPDIR="$CUSTOM_TEMP" "$APP_DIR/venv/bin/pip" install \ + --no-cache-dir \ + --prefer-binary \ + --no-build-isolation \ + "$package" || { + print_warning "Failed to install $package, trying without isolation..." + sudo -u ${APP_USER} TMPDIR="$CUSTOM_TEMP" "$APP_DIR/venv/bin/pip" install \ + --no-cache-dir \ + --no-deps \ + "$package" || print_error "Failed to install $package" + } +done + +print_success "Dependencies installed" + +# Step 6: Clean up +print_info "Cleaning up..." +rm -rf "$CUSTOM_TEMP" +print_success "Cleanup complete" + +# Step 7: Test the installation +print_info "Testing Python installation..." +if sudo -u ${APP_USER} "$APP_DIR/venv/bin/python" -c "import fastapi, uvicorn, httpx, icalendar, jinja2, apscheduler, pytz; print('All modules imported successfully')"; then + print_success "All required Python modules are installed" +else + print_error "Some modules failed to import" + exit 1 +fi + +# Step 8: Create a test script +print_info "Creating test script..." +cat > "$APP_DIR/test_import.py" << 'EOF' +#!/usr/bin/env python3 +import sys +print("Python version:", sys.version) +print("\nTrying to import modules...") + +try: + import fastapi + print("βœ“ fastapi:", fastapi.__version__) +except ImportError as e: + print("βœ— fastapi:", e) + +try: + import uvicorn + print("βœ“ uvicorn:", uvicorn.__version__) +except ImportError as e: + print("βœ— uvicorn:", e) + +try: + import httpx + print("βœ“ httpx:", httpx.__version__) +except ImportError as e: + print("βœ— httpx:", e) + +try: + import icalendar + print("βœ“ icalendar:", icalendar.__version__) +except ImportError as e: + print("βœ— icalendar:", e) + +try: + import jinja2 + print("βœ“ jinja2:", jinja2.__version__) +except ImportError as e: + print("βœ— jinja2:", e) + +try: + import apscheduler + print("βœ“ apscheduler:", apscheduler.__version__) +except ImportError as e: + print("βœ— apscheduler:", e) + +try: + import pytz + print("βœ“ pytz:", pytz.__version__) +except ImportError as e: + print("βœ— pytz:", e) + +print("\nAll critical modules checked!") +EOF + +chmod +x "$APP_DIR/test_import.py" +print_success "Test script created" + +# Step 9: Run the test +print_info "Running import test..." +sudo -u ${APP_USER} "$APP_DIR/venv/bin/python" "$APP_DIR/test_import.py" + +# Step 10: Try to start the application +print_info "Attempting to start the application..." +cd "$APP_DIR" + +# Test if the application can start +timeout 10 sudo -u ${APP_USER} "$APP_DIR/venv/bin/python" -m uvicorn main:app --host 0.0.0.0 --port 8000 & +PID=$! +sleep 5 + +if kill -0 $PID 2>/dev/null; then + print_success "Application started successfully!" + kill $PID +else + print_warning "Application may have issues starting" +fi + +# Final summary +echo +echo -e "${GREEN}════════════════════════════════════════════════${NC}" +echo -e "${GREEN} Installation Fix Complete!${NC}" +echo -e "${GREEN}════════════════════════════════════════════════${NC}" +echo +print_info "Next steps:" +print_info "1. Start the service: sudo systemctl start turmli-calendar" +print_info "2. Check status: sudo systemctl status turmli-calendar" +print_info "3. View logs: sudo journalctl -u turmli-calendar -f" +echo +print_info "If you still have issues, try:" +print_info "1. Increase swap: sudo nano /etc/dphys-swapfile (set CONF_SWAPSIZE=512)" +print_info "2. Reboot: sudo reboot" +print_info "3. Manual test: cd $APP_DIR && sudo -u pi ./venv/bin/python main.py" +echo \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..7f3e894 --- /dev/null +++ b/main.py @@ -0,0 +1,747 @@ +import asyncio +import logging +from contextlib import asynccontextmanager +from datetime import datetime, timedelta +from pathlib import Path +from typing import List, Optional +import json +import os + +import httpx +import pytz +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from fastapi import FastAPI, Request +from fastapi.responses import HTMLResponse +from fastapi.staticfiles import StaticFiles +from icalendar import Calendar, Event +from jinja2 import Template + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Global variables +CALENDAR_URL = "https://outlook.live.com/owa/calendar/ef9138c2-c803-4689-a53e-fe7d0cb90124/d12c4ed3-dfa2-461f-bcd8-9442bea1903b/cid-CD3289D19EBD3DA4/calendar.ics" +CACHE_FILE = Path("calendar_cache.json") +calendar_data = [] +last_fetch_time = None +scheduler = AsyncIOScheduler() + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Manage application lifespan events.""" + # Startup + logger.info("Starting up...") + + # Fetch calendar immediately + await fetch_calendar() + + # Schedule daily updates at 2 AM + scheduler.add_job( + fetch_calendar, + 'cron', + hour=2, + minute=0, + id='daily_calendar_fetch' + ) + + # Also fetch every 4 hours for more frequent updates + scheduler.add_job( + fetch_calendar, + 'interval', + hours=4, + id='periodic_calendar_fetch' + ) + + scheduler.start() + logger.info("Scheduler started") + + yield + + # Shutdown + logger.info("Shutting down...") + scheduler.shutdown() + logger.info("Scheduler stopped") + + +# Initialize FastAPI app with lifespan +app = FastAPI(title="Calendar Viewer", version="1.0.0", lifespan=lifespan) + +# Mount static files directory if it exists +static_dir = Path("static") +if not static_dir.exists(): + static_dir.mkdir(exist_ok=True) + logger.info("Created static directory") + +# Ensure logo file exists in static directory +logo_source = Path("Vektor-Logo.svg") +logo_dest = static_dir / "logo.svg" +if logo_source.exists() and not logo_dest.exists(): + import shutil + shutil.copy2(logo_source, logo_dest) + logger.info("Copied logo to static directory") + +app.mount("/static", StaticFiles(directory="static"), name="static") + +# HTML template for the calendar view +HTML_TEMPLATE = """ + + + + + + Turmli Bar - Calendar Events + + + + + + + +
+
+
+ +
+

Turmli Bar Calendar

+
+ {% if last_updated %} + Last updated: {{ last_updated }} + {% endif %} +
+
Auto-refresh in 5:00
+ +
+
+ +
+ {% if error %} +
+

Error

+

{{ error }}

+
+ {% elif not events %} +
+

No Upcoming Events

+

There are no events scheduled for the next 30 days.

+
+ {% else %} + {% for date, day_events in events_by_date.items() %} +
+
{{ date }}
+ {% for event in day_events %} +
+ {% if event.all_day %} +
+ All Day +
+ {% else %} +
{{ event.time }}
+ {% endif %} +
{{ event.title }}
+ {% if event.location %} +
πŸ“ {{ event.location }}
+ {% endif %} +
+ {% endfor %} +
+ {% endfor %} + {% endif %} +
+
+ + + + +""" + + +async def fetch_calendar(): + """Fetch and parse the ICS calendar file.""" + global calendar_data, last_fetch_time + + try: + logger.info(f"Fetching calendar from {CALENDAR_URL}") + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get(CALENDAR_URL) + response.raise_for_status() + + # Parse the ICS file + cal = Calendar.from_ical(response.content) + events = [] + + # Get current time and timezone + tz = pytz.timezone('Europe/Berlin') # Adjust timezone as needed + now = datetime.now(tz) + cutoff = now + timedelta(days=30) # Show events for next 30 days + + for component in cal.walk(): + if component.name == "VEVENT": + event_data = {} + + # Get event title + event_data['title'] = str(component.get('SUMMARY', 'Untitled Event')) + + # Get event location + location = component.get('LOCATION') + event_data['location'] = str(location) if location else None + + # Get event time + dtstart = component.get('DTSTART') + dtend = component.get('DTEND') + + if dtstart: + # Handle both datetime and date objects + if hasattr(dtstart.dt, 'date'): + # It's a datetime + start_dt = dtstart.dt + if not start_dt.tzinfo: + start_dt = tz.localize(start_dt) + event_data['start'] = start_dt + event_data['all_day'] = False + else: + # It's a date (all-day event) + event_data['start'] = tz.localize(datetime.combine(dtstart.dt, datetime.min.time())) + event_data['all_day'] = True + + # Only include future events within the cutoff + if event_data['start'] >= now and event_data['start'] <= cutoff: + if dtend: + if hasattr(dtend.dt, 'date'): + end_dt = dtend.dt + if not end_dt.tzinfo: + end_dt = tz.localize(end_dt) + event_data['end'] = end_dt + else: + event_data['end'] = tz.localize(datetime.combine(dtend.dt, datetime.min.time())) + + events.append(event_data) + + # Sort events by start time + events.sort(key=lambda x: x['start']) + + calendar_data = events + last_fetch_time = datetime.now() + + # Cache the data + cache_data = { + 'events': [ + { + 'title': e['title'], + 'location': e['location'], + 'start': e['start'].isoformat(), + 'end': e.get('end').isoformat() if e.get('end') else None, + 'all_day': e.get('all_day', False) + } + for e in events + ], + 'last_fetch': last_fetch_time.isoformat() + } + + with open(CACHE_FILE, 'w') as f: + json.dump(cache_data, f) + + logger.info(f"Successfully fetched {len(events)} events") + + except Exception as e: + logger.error(f"Error fetching calendar: {e}") + # Try to load from cache + if CACHE_FILE.exists(): + try: + with open(CACHE_FILE, 'r') as f: + cache_data = json.load(f) + + tz = pytz.timezone('Europe/Berlin') + calendar_data = [] + for e in cache_data['events']: + event = { + 'title': e['title'], + 'location': e['location'], + 'start': datetime.fromisoformat(e['start']), + 'all_day': e.get('all_day', False) + } + if e.get('end'): + event['end'] = datetime.fromisoformat(e['end']) + calendar_data.append(event) + + last_fetch_time = datetime.fromisoformat(cache_data['last_fetch']) + logger.info("Loaded events from cache") + except Exception as cache_error: + logger.error(f"Error loading cache: {cache_error}") + + +@app.get("/", response_class=HTMLResponse) +async def home(request: Request): + """Display the calendar events.""" + try: + # Group events by date + events_by_date = {} + + for event in calendar_data: + date_key = event['start'].strftime('%A, %B %d, %Y') + + if date_key not in events_by_date: + events_by_date[date_key] = [] + + # Format event for display + display_event = { + 'title': event['title'], + 'location': event['location'], + 'all_day': event.get('all_day', False) + } + + if not event.get('all_day'): + if event.get('end'): + display_event['time'] = f"{event['start'].strftime('%I:%M %p')} - {event['end'].strftime('%I:%M %p')}" + else: + display_event['time'] = event['start'].strftime('%I:%M %p') + + events_by_date[date_key].append(display_event) + + template = Template(HTML_TEMPLATE) + html_content = template.render( + events=calendar_data, + events_by_date=events_by_date, + last_updated=last_fetch_time.strftime('%B %d, %Y at %I:%M %p') if last_fetch_time else None, + error=None + ) + + return HTMLResponse(content=html_content) + + except Exception as e: + logger.error(f"Error rendering page: {e}") + template = Template(HTML_TEMPLATE) + html_content = template.render( + events=[], + events_by_date={}, + last_updated=None, + error=str(e) + ) + return HTMLResponse(content=html_content) + + +@app.get("/api/events") +async def get_events(): + """API endpoint to get calendar events as JSON.""" + return { + "events": [ + { + "title": e['title'], + "location": e['location'], + "start": e['start'].isoformat(), + "end": e.get('end').isoformat() if e.get('end') else None, + "all_day": e.get('all_day', False) + } + for e in calendar_data + ], + "last_updated": last_fetch_time.isoformat() if last_fetch_time else None + } + + +@app.post("/api/refresh") +async def refresh_calendar(): + """Manually refresh the calendar data.""" + try: + # Store the previous count for comparison + previous_count = len(calendar_data) + + # Fetch the latest calendar data + await fetch_calendar() + + # Get the new count + new_count = len(calendar_data) + + # Determine if data changed + data_changed = new_count != previous_count + + return { + "status": "success", + "message": "Calendar refreshed successfully", + "events_count": new_count, + "previous_count": previous_count, + "data_changed": data_changed, + "last_updated": last_fetch_time.isoformat() if last_fetch_time else None + } + except Exception as e: + logger.error(f"Error during manual refresh: {e}") + # Try to return cached data info if available + return { + "status": "error", + "message": f"Failed to refresh calendar: {str(e)}", + "events_count": len(calendar_data), + "cached_data_available": len(calendar_data) > 0, + "last_successful_update": last_fetch_time.isoformat() if last_fetch_time else None + } + + +@app.get("/logo") +async def get_logo(): + """Serve the logo SVG file with proper content type.""" + from fastapi.responses import FileResponse + logo_path = Path("static/logo.svg") + if not logo_path.exists(): + logo_path = Path("Vektor-Logo.svg") + if logo_path.exists(): + return FileResponse(logo_path, media_type="image/svg+xml") + return {"error": "Logo not found"} + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/podman-compose.yml b/podman-compose.yml new file mode 100644 index 0000000..4818a40 --- /dev/null +++ b/podman-compose.yml @@ -0,0 +1,24 @@ +version: '3.8' + +services: + calendar: + build: . + container_name: turmli-calendar + ports: + - "${PORT:-8000}:8000" + environment: + - TZ=${TZ:-Europe/Berlin} + - PYTHONUNBUFFERED=1 + volumes: + # Persist calendar cache between restarts + - ./calendar_cache.json:/app/calendar_cache.json:Z + restart: unless-stopped + # Podman-specific: Run as current user instead of root + userns_mode: keep-id + security_opt: + - label=disable + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/events')"] + interval: 30s + timeout: 10s + retries: 3 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..aefa664 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "turmli-bar-calendar-tool" +version = "0.1.0" +description = "A web server that fetches and displays ICS calendar events" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "fastapi>=0.104.0", + "uvicorn[standard]>=0.24.0", + "httpx>=0.25.0", + "icalendar>=5.0.0", + "jinja2>=3.1.0", + "python-multipart>=0.0.6", + "apscheduler>=3.10.0", + "pytz>=2023.3", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["."] \ No newline at end of file diff --git a/requirements-rpi.txt b/requirements-rpi.txt new file mode 100644 index 0000000..493e3e4 --- /dev/null +++ b/requirements-rpi.txt @@ -0,0 +1,17 @@ +# Optimized requirements for Raspberry Pi deployment +# Avoids packages that require compilation on ARM devices + +fastapi>=0.104.0 +# Use uvicorn without the standard extras to avoid watchfiles compilation +uvicorn>=0.24.0 +httpx>=0.25.0 +icalendar>=5.0.0 +jinja2>=3.1.0 +python-multipart>=0.0.6 +apscheduler>=3.10.0 +pytz>=2023.3 + +# Optional performance improvements (if pre-built wheels are available) +# Uncomment if you want to try these: +# uvloop>=0.17.0 # Faster event loop, but may need compilation +# httptools>=0.6.0 # Faster HTTP parsing, but may need compilation \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..54d8907 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +fastapi>=0.104.0 +uvicorn[standard]>=0.24.0 +httpx>=0.25.0 +icalendar>=5.0.0 +jinja2>=3.1.0 +python-multipart>=0.0.6 +apscheduler>=3.10.0 +pytz>=2023.3 \ No newline at end of file diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..2133552 --- /dev/null +++ b/run.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Activate the virtual environment if it exists +if [ -d ".venv" ]; then + source .venv/bin/activate +fi + +# Run the FastAPI server with uvicorn +echo "Starting Calendar Server..." +echo "Access the calendar at: http://localhost:8000" +echo "Press Ctrl+C to stop the server" +echo "" + +uv run uvicorn main:app --host 0.0.0.0 --port 8000 --reload \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..12a41ce --- /dev/null +++ b/setup.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +""" +Setup script to ensure all required assets and directories are in place. +Run this after cloning the repository or if assets are missing. +""" + +import shutil +from pathlib import Path +import sys + + +def setup_assets(): + """Ensure all required assets and directories are properly configured.""" + + print("πŸ”§ Setting up Turmli Bar Calendar Tool assets...") + + # Create static directory if it doesn't exist + static_dir = Path("static") + if not static_dir.exists(): + static_dir.mkdir(exist_ok=True) + print("βœ… Created static directory") + else: + print("βœ… Static directory already exists") + + # Check for logo files + logo_files = [ + "Vektor-Logo.svg", + "Vector-Logo.svg", # Alternative spelling + "logo.svg" + ] + + logo_found = False + for logo_name in logo_files: + logo_source = Path(logo_name) + if logo_source.exists(): + logo_dest = static_dir / "logo.svg" + if not logo_dest.exists(): + shutil.copy2(logo_source, logo_dest) + print(f"βœ… Copied {logo_name} to static/logo.svg") + else: + print(f"βœ… Logo already exists in static directory") + logo_found = True + break + + if not logo_found: + print("⚠️ Warning: No logo file found. The application will work but won't display a logo.") + print(" To add a logo, place one of the following files in the project root:") + print(" - Vektor-Logo.svg") + print(" - Vector-Logo.svg") + print(" - logo.svg") + + # Create cache directory structure + cache_dir = Path(".cache") + if not cache_dir.exists(): + cache_dir.mkdir(exist_ok=True) + print("βœ… Created cache directory") + + # Check for required Python version + if sys.version_info < (3, 13): + print(f"⚠️ Warning: Python {sys.version_info.major}.{sys.version_info.minor} detected.") + print(" This application requires Python 3.13 or higher for optimal performance.") + else: + print(f"βœ… Python {sys.version_info.major}.{sys.version_info.minor} is compatible") + + # Check for UV installation + uv_installed = shutil.which("uv") is not None + if uv_installed: + print("βœ… UV package manager is installed") + else: + print("⚠️ Warning: UV package manager not found.") + print(" Install UV from: https://github.com/astral-sh/uv") + print(" Or use pip with: pip install -r requirements.txt") + + # Create .env file from template if it doesn't exist + env_file = Path(".env") + env_example = Path(".env.example") + if not env_file.exists() and env_example.exists(): + shutil.copy2(env_example, env_file) + print("βœ… Created .env file from template") + print(" Please edit .env to configure your settings") + elif env_file.exists(): + print("βœ… .env file already exists") + + print("\nπŸŽ‰ Setup complete!") + print("\nTo start the server, run:") + if uv_installed: + print(" ./run.sh") + print(" or") + print(" make run") + else: + print(" python main.py") + print("\nThen open your browser to: http://localhost:8000") + + +if __name__ == "__main__": + try: + setup_assets() + except Exception as e: + print(f"❌ Error during setup: {e}") + sys.exit(1) \ No newline at end of file diff --git a/start_minimal.sh b/start_minimal.sh new file mode 100755 index 0000000..c6bf6b4 --- /dev/null +++ b/start_minimal.sh @@ -0,0 +1,108 @@ +#!/bin/bash + +# Minimal startup script for Turmli Calendar on Raspberry Pi Zero +# Uses minimal resources and avoids problematic dependencies + +set -e + +# Configuration +APP_DIR="/opt/turmli-calendar" +APP_USER="pi" +APP_PORT="8000" +VENV_PATH="$APP_DIR/venv" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +print_info() { + echo -e "${BLUE}β„Ή${NC} $1" +} + +print_success() { + echo -e "${GREEN}βœ“${NC} $1" +} + +print_error() { + echo -e "${RED}βœ—${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}⚠${NC} $1" +} + +# Check if running as correct user +if [ "$EUID" -eq 0 ]; then + print_error "Don't run this script as root. Use: ./start_minimal.sh" + exit 1 +fi + +# Check if application directory exists +if [ ! -d "$APP_DIR" ]; then + print_error "Application directory not found: $APP_DIR" + print_info "Please run the deployment script first: sudo ./deploy_rpi.sh install" + exit 1 +fi + +# Check if virtual environment exists +if [ ! -d "$VENV_PATH" ]; then + print_error "Virtual environment not found: $VENV_PATH" + print_info "Please run the deployment script first: sudo ./deploy_rpi.sh install" + exit 1 +fi + +# Change to application directory +cd "$APP_DIR" + +# Show system resources before starting +print_info "System resources before startup:" +echo " Memory: $(free -m | grep Mem | awk '{printf "Used: %dMB / Total: %dMB (%.1f%%)", $3, $2, $3*100/$2}')" +echo " Swap: $(free -m | grep Swap | awk '{printf "Used: %dMB / Total: %dMB", $3, $2}')" +echo " CPU Load: $(uptime | awk -F'load average:' '{print $2}')" +echo " Disk: $(df -h / | tail -1 | awk '{printf "Used: %s / Total: %s (%s)", $3, $2, $5}')" +echo + +# Set environment variables for minimal resource usage +export PYTHONUNBUFFERED=1 +export PYTHONDONTWRITEBYTECODE=1 +export MALLOC_ARENA_MAX=2 # Limit memory fragmentation +export MALLOC_MMAP_THRESHOLD_=131072 +export MALLOC_TRIM_THRESHOLD_=131072 +export MALLOC_MMAP_MAX_=65536 + +# Set a custom temp directory to avoid filling /tmp +export TMPDIR="$APP_DIR/tmp" +mkdir -p "$TMPDIR" + +# Clear Python cache to free memory +find "$APP_DIR" -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true +find "$APP_DIR" -name "*.pyc" -delete 2>/dev/null || true + +print_info "Starting Turmli Calendar with minimal resources..." +print_info "Access the application at: http://$(hostname -I | cut -d' ' -f1):${APP_PORT}" +print_info "Press Ctrl+C to stop" +echo + +# Start uvicorn with minimal configuration +# --workers 1: Single worker process +# --loop asyncio: Standard event loop (no uvloop) +# --no-access-log: Disable access logging to save resources +# --log-level warning: Only log warnings and errors +# --limit-concurrency 10: Limit concurrent connections +# --timeout-keep-alive 5: Short keepalive timeout +# --backlog 32: Smaller connection backlog + +exec "$VENV_PATH/bin/python" -m uvicorn main:app \ + --host 0.0.0.0 \ + --port ${APP_PORT} \ + --workers 1 \ + --loop asyncio \ + --no-access-log \ + --log-level warning \ + --limit-concurrency 10 \ + --timeout-keep-alive 5 \ + --backlog 32 \ + --no-use-colors \ No newline at end of file diff --git a/static/logo.svg b/static/logo.svg new file mode 100644 index 0000000..0354eb9 --- /dev/null +++ b/static/logo.svg @@ -0,0 +1,179 @@ + + + + +]> + + + + + + + + + + + + + + ΓΌ + r + m + l + i + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + T + + B + a + r + * + * + + diff --git a/test_logo.html b/test_logo.html new file mode 100644 index 0000000..275c8da --- /dev/null +++ b/test_logo.html @@ -0,0 +1,229 @@ + + + + + + Logo Display Test + + + +
+

Turmli Bar Logo Display Tests

+ +
+

Direct File Access Tests

+ +
+ Logo from root + Direct from root +
+ +
+ Logo from static + From static folder +
+ +
+ Logo with absolute path + Absolute /static/ path +
+
+ +
+

Display Style Tests

+ +
+ With white background + White background + padding +
+ +
+ Object-fit contain + Object-fit: contain +
+ +
+ With drop shadow + Drop shadow filter +
+
+ +
+

Container Tests

+ +
+
+ In circle +
+ Circle container +
+ +
+
+ In square +
+ Square container +
+
+ +
+

Inline SVG Test

+
+ + SVG as object failed + +
+ Using <object> tag +
+ +
+

Debug Information

+
    +
  • Open this file directly in your browser (file:// protocol)
  • +
  • Check which display methods work
  • +
  • Check browser console for any errors
  • +
  • The SVG has a gray background (#7F7F7F) which might be part of the issue
  • +
+
+
+ + + + \ No newline at end of file diff --git a/test_server.py b/test_server.py new file mode 100755 index 0000000..1b96950 --- /dev/null +++ b/test_server.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +""" +Test script to verify the calendar server functionality. +Run with: python test_server.py +""" + +import asyncio +import sys +from datetime import datetime +import httpx +import json +from pathlib import Path + +# ANSI color codes for terminal output +GREEN = '\033[92m' +RED = '\033[91m' +YELLOW = '\033[93m' +BLUE = '\033[94m' +RESET = '\033[0m' +BOLD = '\033[1m' + + +def print_status(status: str, message: str): + """Print colored status messages.""" + if status == "success": + print(f"{GREEN}βœ“{RESET} {message}") + elif status == "error": + print(f"{RED}βœ—{RESET} {message}") + elif status == "warning": + print(f"{YELLOW}⚠{RESET} {message}") + elif status == "info": + print(f"{BLUE}β„Ή{RESET} {message}") + + +async def test_server(): + """Run tests against the calendar server.""" + base_url = "http://localhost:8000" + + print(f"\n{BOLD}Calendar Server Test Suite{RESET}") + print("=" * 50) + + # Test 1: Check if server is running + print(f"\n{BOLD}1. Server Connectivity Test{RESET}") + try: + async with httpx.AsyncClient(timeout=5.0) as client: + response = await client.get(f"{base_url}/") + if response.status_code == 200: + print_status("success", f"Server is running at {base_url}") + else: + print_status("error", f"Server returned status code: {response.status_code}") + return False + except httpx.ConnectError: + print_status("error", "Cannot connect to server. Please ensure it's running with: ./run.sh") + return False + except Exception as e: + print_status("error", f"Connection error: {e}") + return False + + # Test 2: Check HTML interface + print(f"\n{BOLD}2. HTML Interface Test{RESET}") + try: + async with httpx.AsyncClient(timeout=5.0) as client: + response = await client.get(f"{base_url}/") + content = response.text + + if "Turmli Bar Calendar" in content: + print_status("success", "HTML interface is rendering correctly") + else: + print_status("warning", "HTML interface might not be rendering correctly") + + if "Last updated:" in content: + print_status("success", "Calendar update timestamp is displayed") + else: + print_status("warning", "Update timestamp not found") + except Exception as e: + print_status("error", f"HTML interface test failed: {e}") + + # Test 3: Check API endpoints + print(f"\n{BOLD}3. API Endpoints Test{RESET}") + + # Test GET /api/events + try: + async with httpx.AsyncClient(timeout=5.0) as client: + response = await client.get(f"{base_url}/api/events") + if response.status_code == 200: + data = response.json() + print_status("success", "GET /api/events endpoint is working") + + if "events" in data: + event_count = len(data["events"]) + print_status("info", f"Found {event_count} event(s) in calendar") + + if event_count > 0: + # Display first event + first_event = data["events"][0] + print_status("info", f"Next event: {first_event.get('title', 'N/A')}") + if first_event.get('start'): + start_time = datetime.fromisoformat(first_event['start'].replace('Z', '+00:00')) + print_status("info", f"Date: {start_time.strftime('%Y-%m-%d %H:%M')}") + else: + print_status("warning", "Events data structure not found") + + if "last_updated" in data and data["last_updated"]: + print_status("success", f"Last updated: {data['last_updated']}") + else: + print_status("error", f"API returned status code: {response.status_code}") + except Exception as e: + print_status("error", f"API events test failed: {e}") + + # Test POST /api/refresh + print(f"\n{BOLD}4. Manual Refresh Test{RESET}") + try: + async with httpx.AsyncClient(timeout=30.0) as client: + print_status("info", "Triggering manual calendar refresh...") + response = await client.post(f"{base_url}/api/refresh") + if response.status_code == 200: + data = response.json() + if data.get("status") == "success": + print_status("success", f"Calendar refreshed successfully") + print_status("info", f"Total events: {data.get('events_count', 0)}") + else: + print_status("warning", "Refresh completed with unexpected status") + else: + print_status("error", f"Refresh returned status code: {response.status_code}") + except Exception as e: + print_status("error", f"Manual refresh test failed: {e}") + + # Test 5: Check cache file + print(f"\n{BOLD}5. Cache File Test{RESET}") + cache_file = Path("calendar_cache.json") + if cache_file.exists(): + print_status("success", "Cache file exists") + try: + with open(cache_file, 'r') as f: + cache_data = json.load(f) + if "events" in cache_data: + print_status("success", f"Cache contains {len(cache_data['events'])} event(s)") + if "last_fetch" in cache_data: + last_fetch = datetime.fromisoformat(cache_data['last_fetch']) + print_status("info", f"Cache last updated: {last_fetch.strftime('%Y-%m-%d %H:%M:%S')}") + except Exception as e: + print_status("warning", f"Could not read cache file: {e}") + else: + print_status("warning", "Cache file not found (will be created on first fetch)") + + # Test 6: Mobile responsiveness check + print(f"\n{BOLD}6. Mobile Responsiveness Test{RESET}") + try: + async with httpx.AsyncClient(timeout=5.0) as client: + # Simulate mobile user agent + headers = { + "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15" + } + response = await client.get(f"{base_url}/", headers=headers) + content = response.text + + if 'viewport' in content and 'width=device-width' in content: + print_status("success", "Mobile viewport meta tag found") + else: + print_status("warning", "Mobile viewport configuration might be missing") + + if 'background: #f5f5f5' in content: + print_status("success", "Simplified design detected") + else: + print_status("warning", "Design might not be using simplified styles") + except Exception as e: + print_status("error", f"Mobile responsiveness test failed: {e}") + + print(f"\n{BOLD}Test Summary{RESET}") + print("=" * 50) + print_status("info", "All basic tests completed") + print_status("info", f"Server URL: {base_url}") + print_status("info", "You can now access the calendar in your browser") + + return True + + +async def main(): + """Main function to run tests.""" + success = await test_server() + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("\n\nTests interrupted by user") + sys.exit(0) \ No newline at end of file