Commit All

This commit is contained in:
2025-10-30 13:33:08 +01:00
commit 3678efed07
31 changed files with 5536 additions and 0 deletions

70
.containerignore Normal file
View File

@@ -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

70
.dockerignore Normal file
View File

@@ -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

25
.env.example Normal file
View File

@@ -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"]

173
.gitignore vendored Normal file
View File

@@ -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

36
Containerfile Normal file
View File

@@ -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"]

422
DEPLOY_README.md Normal file
View File

@@ -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 <your-repository-url> 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@<pi-ip>:~/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://<pi-ip>: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@<pi-ip>
# 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.

36
Dockerfile Normal file
View File

@@ -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"]

241
FIX_INSTALLATION.md Normal file
View File

@@ -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! 🚀

84
Makefile Normal file
View File

@@ -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

359
PODMAN_README.md Normal file
View File

@@ -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

313
README.md Normal file
View File

@@ -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 <repository-url>
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 <repository-url>
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`

179
Vektor-Logo.svg Normal file
View File

@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_svg "http://www.w3.org/2000/svg">
<!ENTITY ns_xlink "http://www.w3.org/1999/xlink">
]>
<svg version="1.1" xmlns="&ns_svg;" xmlns:xlink="&ns_xlink;" width="776.79" height="758.327" viewBox="0 0 776.79 758.327"
overflow="visible" enable-background="new 0 0 776.79 758.327" xml:space="preserve">
<style type="text/css">
<![CDATA[
@font-face{font-family:'Castellar';src:url("data:;base64,\
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\
AQAI/5oAAQAC/4sAAQAEAAIABQAHAAgAAA==")}
]]>
</style>
<g id="Ebene_1">
</g>
<g id="Ebene_2">
<rect x="0.75" y="0.75" fill="#7F7F7F" stroke="#000000" stroke-width="1.5" width="775.29" height="754.697"/>
<polygon fill="#FFFFFF" stroke="#000000" stroke-width="1.5" points="520.016,506.097 716.116,442.064 685.815,531.539
756.422,580.134 606.918,629.587 "/>
<path fill="#CECECE" stroke="#000000" stroke-width="1.5" d="M244.357,176.889c0,0,0.459,59.496,2.067,68.684l-7.58,1.837
l-74.195-14.241l-18.147-33.538l21.134-30.092l62.939-9.418l14.242,1.379L244.357,176.889z"/>
<path fill="#CECECE" stroke="#000000" stroke-width="1.5" d="M517.872,238.854c9.635-10.731,21.635-24.731,30.633-36.83
c2.68-4.099,5.072-8.499,7.096-13.364c1.738-3.856,3.133-7.869,4.139-12.004c8.039-33.078,58.805,8.499,58.805,8.499l21.363,14.701
l8.729,22.512l-6.598,30.32c0,0-0.705,3.102-3.367,8.527c-10.938,14.59-22.271,28.771-34.172,42.381
c-3.969,4.537-7.998,9.01-12.1,13.41c-14.893,19.116-34.893,33.116-52.893,49.116c-3.75-26.25,2.5-51.719-3.012-77.001
c-1.102-5.057-2.275-10.105-3.533-15.149c-1.455-9.85-11.455-19.85-13.082-28.93L517.872,238.854z"/>
<path fill="#FFFFFF" stroke="#000000" stroke-width="1.5" d="M241.362,387.484c-10.074-17.857-21.979-47.162-23.811-70.515
c0,0-4.58-18.317,4.578-47.164c0,0,15.11-29.306,29.306-43.5c0,0,17.4-17.857,44.416-25.642c0,0,22.894-9.158,54.947-12.821
c0,0,19.231-4.579,74.638,0c0,0,22.438,3.205,46.248,14.652c0,0,10.531,5.494,31.137,19.231c0,0,26.1,18.316,36.174,42.585
c0,0,7.324,4.121,7.783,118.595l-25.184,16.484l-98.447-30.679l-119.512,7.785l-51.742,13.736L241.362,387.484z"/>
<path fill="#FFFFFF" stroke="#000000" stroke-width="1.5" d="M235.409,425.49l-0.916-116.305c0,0-0.916-16.026,9.158-32.053
c0,0,14.652-30.222,59.068-45.79c0,0,40.295-16.942,100.279-12.363c0,0,36.174,1.375,74.18,21.979c0,0,31.139,17.856,43.043,41.21
c0,0,5.951,11.904,5.951,31.595l0.459,102.112l-107.148-20.605l-96.616,1.832l-65.937,21.063L235.409,425.49z"/>
<path fill="#FFFFFF" stroke="#000000" stroke-width="1.5" d="M20.174,167.932c23.188-6.102,131.193-38.442,131.193-38.442
s3.242,39.534,7.153,50.918c0,0,7.491,30.238,24.577,43.663c0,0,28.68,15.256,52.478,22.578l-7.322,10.983L59.227,307.059
l32.341-91.531L20.174,167.932z"/>
<path fill="#FFFFFF" stroke="#000000" stroke-width="1.5" d="M151.368,129.489c0,0-3.631-34.305-3.827-55.121
c0,0-1.375-10.801,2.749-16.3c0,0,2.945-5.499,10.604-9.034c0,0,13.747-7.854,24.745-10.407c0,0,39.082-8.445,71.878-11.783
s108.013-5.694,167.712-4.713c0,0,53.221-0.589,112.529,11.587c0,0,30.045,4.714,93.479,26.905c0,0,14.141,4.908,22.389,11.39
c0,0,9.82,4.123,14.336,28.868c0,0,4.32,29.263-0.59,59.898c0,0-8.641,55.186-25.334,91.909c0,0,0.785-20.229-1.768-32.208
c0,0,1.168-6.23-21.885-15.394c-0.102-0.04-0.203-0.08-0.307-0.121c-23.244-9.18-29.781-10.007-46.084-13.88
c-0.088-0.021-0.176-0.042-0.264-0.063c-16.496-3.928-38.1-10.212-83.66-14.729c0,0-52.826-8.249-146.699-4.714
c0,0-76.591,3.143-100.941,5.499c0,0-32.6,2.357-46.936,4.518c0,0-14.139,2.553-16.889,6.284c0,0-8.052,5.499-9.819,13.747
c0,0-8.249-17.282-8.838-24.744C157.949,176.884,153.821,163.987,151.368,129.489z"/>
<text transform="matrix(0.9989 -0.0473 0.0473 0.9989 256.5254 158.293)" font-family="'Castellar'" font-size="96">ü</text>
<text transform="matrix(1 0 0 1 338.5337 153.6455)" font-family="'Castellar'" font-size="96">r</text>
<text transform="matrix(0.9955 0.0947 -0.0947 0.9955 416.3237 153.6987)" font-family="'Castellar'" font-size="96">m</text>
<text transform="matrix(0.9793 0.2025 -0.2025 0.9793 518.5571 163.5659)" font-family="'Castellar'" font-size="96">l</text>
<text transform="matrix(0.9766 0.2149 -0.2149 0.9766 576.2554 175.7905)" font-family="'Castellar'" font-size="144">i</text>
<path fill="#CECECE" stroke="#000000" d="M233.049,425.384c-3.468,1.734-56.878,31.908-88.438,67.629
c0,0-12.138,14.221-13.525,21.85l-3.469,27.744l13.873,33.988l45.086,17.688l38.843-4.855l13.179-23.236l-5.202-41.617
l1.388-61.387L233.049,425.384z"/>
<path fill="#CECECE" stroke="#000000" d="M515.354,504.111c0,0,17.686,1.039,33.293,5.549c0,0,21.85,5.895,40.23,15.953
c0,0,14.221,7.283,19.77,15.607c0,0,2.428,2.43,3.814,26.705l-16.301,22.889l-67.281-0.693l-9.018-4.508l-1.039-25.318
L515.354,504.111z"/>
<path fill="#FFFFFF" stroke="#000000" stroke-width="1.5" d="M227.499,591.48l0.693-104.391l-4.855-7.977l-0.347-49.594
c0,0,19.074-23.234,63.813-37.801c0,0,64.854-18.729,155.025-8.672c0,0,57.57,8.324,74.564,20.115c0,0,8.67,4.855,15.953,14.221
c0,0,3.818,4.162-1.732,18.381v19.768c0,0,6.936,12.486-0.348,18.729l0.348,109.59L227.499,591.48z"/>
<path fill="none" stroke="#000000" stroke-width="1.5" d="M223.336,457.609c0,0,9.712-19.768,28.439-29.479
c0,0,38.844-20.115,73.871-22.889c0,0,71.443-5.896,118.957-1.041c0,0,35.029,4.508,51.328,11.098c0,0,16.994,6.938,34.334,23.584"
/>
<path fill="none" stroke="#000000" stroke-width="1.5" d="M227.845,488.128c7.283-11.443,9.364-16.301,22.543-27.398
c0,0,28.092-14.221,63.467-20.115c0,0,72.833-13.176,140.46-4.854c0,0,33.639,5.896,50.98,15.953c0,0,18.033,11.098,24.623,23.93"
/>
<polygon fill="none" stroke="#000000" stroke-width="1.5" points="269.463,584.886 269.463,482.23 321.832,469.744 321.485,593.21
"/>
<polygon fill="none" stroke="#000000" stroke-width="1.5" points="388.42,574.482 388.42,461.421 453.62,463.849 453.967,574.83
"/>
<polygon fill="none" stroke="#000000" stroke-width="1.5" points="500.44,587.662 500.788,474.947 529.573,501.304 530.266,593.21
"/>
<line fill="none" stroke="#000000" stroke-width="1.5" x1="500.788" y1="566.853" x2="529.573" y2="584.193"/>
<polygon stroke="#000000" stroke-width="1.5" points="404.026,561.65 403.333,476.333 440.094,476.333 440.442,561.998 "/>
<polygon stroke="#000000" stroke-width="1.5" points="509.11,571.707 508.417,493.328 524.024,505.119 524.37,580.378 "/>
<polygon fill="#020101" stroke="#000000" stroke-width="1.5" points="280.908,575.175 280.908,488.126 309.693,482.576
309.693,568.933 "/>
<path fill="none" stroke="#FFFFFF" stroke-width="1.5" d="M315.589,495.062c-11.792,1.734-24.971,4.508-24.971,4.508v15.26
l23.236-5.549L315.589,495.062z"/>
<line fill="none" stroke="#FFFFFF" stroke-width="1.5" x1="302.757" y1="497.142" x2="302.757" y2="512.402"/>
<polyline fill="none" stroke="#FFFFFF" stroke-width="1.5" points="290.619,572.402 290.619,524.888 313.162,521.074 "/>
<line fill="none" stroke="#FFFFFF" stroke-width="1.5" x1="302.41" y1="523.501" x2="302.757" y2="570.32"/>
<line fill="none" stroke="#FFFFFF" stroke-width="1.5" x1="290.965" y1="540.494" x2="313.508" y2="537.373"/>
<line fill="none" stroke="#FFFFFF" stroke-width="1.5" x1="290.965" y1="558.529" x2="313.855" y2="556.101"/>
<path fill="none" stroke="#000000" stroke-width="1.5" d="M269.81,577.951c12.832-2.428,51.328-11.445,51.328-11.445"/>
<path fill="none" stroke="#FFFFFF" stroke-width="1.5" d="M398.428,490.8c5.686,0.277,30.506,0.139,30.506,0.139v16.5
l-29.258-0.139"/>
<line fill="none" stroke="#FFFFFF" stroke-width="1.5" x1="415.344" y1="491.632" x2="415.483" y2="507.3"/>
<polyline fill="none" stroke="#FFFFFF" stroke-width="1.5" points="399.399,512.431 428.657,512.154 428.518,561.796 "/>
<line fill="none" stroke="#FFFFFF" stroke-width="1.5" x1="415.206" y1="512.431" x2="414.79" y2="561.242"/>
<line fill="none" stroke="#FFFFFF" stroke-width="1.5" x1="428.379" y1="533.509" x2="399.538" y2="533.371"/>
<line fill="none" stroke="#FFFFFF" stroke-width="1.5" x1="428.379" y1="549.593" x2="398.012" y2="549.455"/>
<line fill="none" stroke="#000000" stroke-width="1.5" x1="388.073" y1="561.302" x2="453.274" y2="561.998"/>
<path fill="none" stroke="#000000" stroke-width="1.5" d="M245.483,398.47c0-31.594,0-78.3,0-78.3"/>
<polygon stroke="#000000" stroke-width="1.5" points="261.051,392.976 260.593,318.339 268.835,311.013 259.219,299.565
273.873,286.744 277.078,290.407 277.078,385.65 "/>
<polygon stroke="#000000" stroke-width="1.5" points="293.562,381.07 293.562,290.865 299.056,286.744 298.598,273.923
315.999,265.223 322.867,275.755 323.325,375.119 "/>
<polygon stroke="#000000" stroke-width="1.5" points="340.267,373.744 339.809,276.212 345.762,272.091 345.762,259.271
349.883,253.317 375.067,254.233 375.983,270.718 378.731,273.465 378.731,372.37 "/>
<polygon stroke="#000000" stroke-width="1.5" points="395.672,372.37 395.672,272.549 399.793,269.344 401.167,255.607
430.93,261.56 424.977,267.513 425.436,274.381 430.014,278.96 430.93,372.828 "/>
<polygon stroke="#000000" stroke-width="1.5" points="449.704,376.035 449.704,272.091 457.03,267.055 477.178,278.044
469.852,289.033 476.721,299.107 475.805,384.277 "/>
<polygon stroke="#000000" stroke-width="1.5" points="492.747,382.902 493.204,294.07 495.952,290.407 504.653,300.938
496.411,310.097 496.411,316.05 501.905,321.086 501.905,387.023 "/>
<polyline fill="none" stroke="#000000" stroke-width="1.5" points="522.051,394.808 518.389,391.603 518.846,319.713 "/>
<polyline fill="none" stroke="#FFFFFF" stroke-width="1.5" points="296.767,382.902 296.767,374.66 316.915,371.913
316.457,381.07 "/>
<polyline fill="none" stroke="#FFFFFF" stroke-width="1.5" points="346.22,376.492 345.762,366.418 369.114,366.876
369.114,376.949 "/>
<polyline fill="none" stroke="#FFFFFF" stroke-width="1.5" points="402.542,376.035 402.542,365.96 424.52,365.96 424.52,376.492
"/>
<polyline fill="none" stroke="#FFFFFF" stroke-width="1.5" points="456.116,382.445 457.03,372.828 473.973,377.865
473.973,386.566 "/>
<text transform="matrix(0.9442 -0.0882 0.0931 0.9957 168.582 166.0718)" font-family="'Castellar'" font-size="144">T</text>
<path fill="#FFFDFD" stroke="#000000" stroke-width="1.5" d="M610.063,545.832c0,0,6.861,18.869,14.293,53.457
c0,0,5.309,31.57,7.916,56.316c0.029,0.287,0.059,0.572,0.088,0.857c2.51,24.273,1.48,39.016-6.014,43.162
c-0.184,0.102-0.371,0.197-0.563,0.287c-8.004,3.717-18.01,14.58-110.627,25.729c0,0-77.186,9.434-151.223,8.576
c0,0-83.188,0.857-121.207-8.576c0,0-72.608-13.15-102.338-26.871c0,0-29.726-10.291-31.73-45.451c0,0-3.719-28.867,10.859-107.193
c0,0,0.572-9.432,11.721-32.873c0,0-21.439,43.166,96.621,61.746c0,0,67.461,10.006,166.082,8.576
c0,0,141.217,2.002,183.811-12.863C577.752,570.71,609.499,562.976,610.063,545.832z"/>
<text transform="matrix(0.9886 0.1507 -0.1507 0.9886 204.0767 695.6118)" font-family="'Castellar'" font-size="144">B</text>
<text transform="matrix(1 0 0 1 305.772 707.8472)" font-family="'Castellar'" font-size="144">a</text>
<text transform="matrix(0.9913 -0.1317 0.1317 0.9913 436.1226 709.8608)" font-family="'Castellar'" font-size="144">r</text>
<text transform="matrix(0.9996 -0.0296 0.0296 0.9996 551.2358 718.1958)" font-family="'Castellar'" font-size="144">*</text>
<text transform="matrix(1 0 0 1 125.2202 712.3237)" font-family="'Castellar'" font-size="144">*</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

294
deploy-podman.sh Executable file
View File

@@ -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

180
deploy.sh Executable file
View File

@@ -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

674
deploy_rpi.sh Executable file
View File

@@ -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

51
deployment/README.md Normal file
View File

@@ -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 <repository-url> turmli-calendar
cd turmli-calendar
# 4. Run the deployment script
sudo ./deploy_rpi.sh install
```
The application will be available at `http://<pi-ip-address>:8000`

389
deployment/monitor.sh Executable file
View File

@@ -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

View File

@@ -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

20
docker-compose.yml Normal file
View File

@@ -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

240
fix_install_rpi.sh Executable file
View File

@@ -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

747
main.py Normal file
View File

@@ -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 = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Turmli Bar - Calendar Events</title>
<meta name="description" content="View upcoming events at Turmli Bar">
<meta name="theme-color" content="#667eea">
<link rel="icon" type="image/svg+xml" href="/static/logo.svg">
<link rel="apple-touch-icon" href="/static/logo.svg">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #2c1810 0%, #3d2817 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
}
.header {
text-align: center;
color: #f4e4d4;
margin-bottom: 30px;
padding: 20px;
background: rgba(139, 69, 19, 0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(139, 69, 19, 0.2);
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
}
.logo-container {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
width: 150px;
height: 150px;
margin-left: auto;
margin-right: auto;
background: rgba(244, 228, 212, 0.1);
border-radius: 10px;
padding: 10px;
}
.logo {
width: 100%;
height: 100%;
object-fit: contain;
filter: sepia(20%) saturate(0.8);
}
.header h1 {
font-size: 1.8em;
margin-bottom: 10px;
margin-top: 0;
color: #f4e4d4;
font-weight: 600;
}
.last-updated {
font-size: 0.9em;
color: #d4a574;
margin-top: 10px;
}
.refresh-btn {
background: #8b4513;
border: 1px solid #a0522d;
color: #f4e4d4;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 1em;
margin-top: 15px;
font-weight: 500;
transition: all 0.2s ease;
min-width: 120px;
position: relative;
}
.refresh-btn:disabled {
cursor: not-allowed;
opacity: 0.7;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid #f4e4d4;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.6s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
.refresh-btn:hover {
background: #a0522d;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.events-container {
display: grid;
gap: 20px;
}
.date-group {
background: rgba(244, 228, 212, 0.95);
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
border: 1px solid rgba(139, 69, 19, 0.1);
}
.date-header {
font-size: 1.2em;
font-weight: 600;
color: #5d4e37;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #d4a574;
}
.event-card {
background: rgba(255, 248, 240, 0.9);
border-radius: 6px;
padding: 15px;
margin-bottom: 15px;
border-left: 4px solid #cd853f;
transition: all 0.2s ease;
}
.event-card:hover {
background: rgba(255, 248, 240, 1);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.event-card:last-child {
margin-bottom: 0;
}
.event-time {
font-size: 0.9em;
color: #8b6914;
font-weight: 500;
margin-bottom: 8px;
}
.event-title {
font-size: 1.1em;
color: #3e2817;
font-weight: bold;
margin-bottom: 8px;
}
.event-location {
font-size: 0.9em;
color: #8b7355;
margin-top: 5px;
}
.all-day-badge {
display: inline-block;
background: #cd853f;
color: #fff8f0;
padding: 3px 10px;
border-radius: 20px;
font-size: 0.8em;
font-weight: 600;
}
.no-events {
text-align: center;
padding: 40px;
color: #5d4e37;
background: rgba(244, 228, 212, 0.95);
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
.error {
background: rgba(139, 69, 19, 0.2);
color: #ff6b6b;
padding: 20px;
border-radius: 8px;
text-align: center;
border: 1px solid rgba(139, 69, 19, 0.3);
}
.loading {
text-align: center;
color: #d4a574;
font-size: 1.1em;
padding: 40px;
}
.auto-refresh-indicator {
font-size: 0.85em;
color: #8b7355;
margin-top: 5px;
}
@media (max-width: 600px) {
body {
padding: 10px;
background: #2c1810;
}
.header {
background: rgba(139, 69, 19, 0.4);
padding: 15px;
}
.logo-container {
width: 100px;
height: 100px;
}
.header h1 {
font-size: 1.5em;
}
.date-group {
background: rgba(244, 228, 212, 0.98);
}
.date-header {
font-size: 1.1em;
color: #3e2817;
}
.event-title {
font-size: 1em;
}
.event-card {
padding: 12px;
background: rgba(255, 248, 240, 0.95);
}
.refresh-btn {
padding: 8px 16px;
font-size: 0.95em;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo-container">
<img src="/static/logo.svg" alt="Turmli Bar Logo" class="logo" onerror="this.style.display='none'">
</div>
<h1>Turmli Bar Calendar</h1>
<div class="last-updated" id="lastUpdated">
{% if last_updated %}
Last updated: {{ last_updated }}
{% endif %}
</div>
<div class="auto-refresh-indicator" id="autoRefreshIndicator">Auto-refresh in <span id="countdown">5:00</span></div>
<button class="refresh-btn" id="refreshBtn" onclick="refreshCalendar()">Refresh Calendar</button>
<div id="statusMessage" style="margin-top: 10px; font-size: 0.9em; color: #d4a574; min-height: 20px;"></div>
</div>
<div class="events-container">
{% if error %}
<div class="error">
<h2>Error</h2>
<p>{{ error }}</p>
</div>
{% elif not events %}
<div class="no-events">
<h2>No Upcoming Events</h2>
<p>There are no events scheduled for the next 30 days.</p>
</div>
{% else %}
{% for date, day_events in events_by_date.items() %}
<div class="date-group">
<div class="date-header">{{ date }}</div>
{% for event in day_events %}
<div class="event-card">
{% if event.all_day %}
<div class="event-time">
<span class="all-day-badge">All Day</span>
</div>
{% else %}
<div class="event-time">{{ event.time }}</div>
{% endif %}
<div class="event-title">{{ event.title }}</div>
{% if event.location %}
<div class="event-location">📍 {{ event.location }}</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endfor %}
{% endif %}
</div>
</div>
<script>
// Auto-refresh countdown timer
let refreshInterval = 5 * 60; // 5 minutes in seconds
let timeRemaining = refreshInterval;
function updateCountdown() {
const minutes = Math.floor(timeRemaining / 60);
const seconds = timeRemaining % 60;
const countdownEl = document.getElementById('countdown');
if (countdownEl) {
countdownEl.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
timeRemaining--;
if (timeRemaining < 0) {
// Auto-refresh
refreshCalendar();
timeRemaining = refreshInterval; // Reset countdown
}
}
// Update countdown every second
setInterval(updateCountdown, 1000);
// Function to refresh calendar data
async function refreshCalendar() {
const btn = document.getElementById('refreshBtn');
const statusMsg = document.getElementById('statusMessage');
const lastUpdated = document.getElementById('lastUpdated');
const originalText = btn.textContent;
try {
// Update button to show loading state with spinner
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span>Fetching...';
statusMsg.textContent = 'Connecting to calendar server...';
statusMsg.style.color = '#d4a574';
// Reset countdown timer when manually refreshing
timeRemaining = refreshInterval;
// Call the refresh API endpoint
const response = await fetch('/api/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
console.log('Calendar refreshed:', data);
// Show success
btn.innerHTML = '✓ Success!';
btn.style.background = '#4a7c59';
btn.style.borderColor = '#5a8c69';
const eventCount = data.events_count || 0;
const changeText = data.data_changed ? ' (data updated)' : ' (no changes)';
statusMsg.textContent = `Found ${eventCount} event${eventCount !== 1 ? 's' : ''}${changeText}. Reloading...`;
statusMsg.style.color = '#90c9a4';
// Update the last updated time
const now = new Date();
const timeStr = now.toLocaleString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true
});
lastUpdated.textContent = `Last updated: ${timeStr}`;
// Reload page after showing success
setTimeout(() => {
location.reload();
}, 1500);
} else {
throw new Error(`Server returned ${response.status}`);
}
} catch (error) {
console.error('Error refreshing calendar:', error);
// Show error
btn.innerHTML = '✗ Failed';
btn.style.background = '#c44536';
btn.style.borderColor = '#d45546';
statusMsg.textContent = 'Could not refresh calendar. Please try again.';
statusMsg.style.color = '#ff6b6b';
// Reset button after 3 seconds
setTimeout(() => {
btn.disabled = false;
btn.innerHTML = originalText;
btn.style.background = '#8b4513';
btn.style.borderColor = '#a0522d';
statusMsg.textContent = '';
}, 3000);
}
}
</script>
</body>
</html>
"""
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)

24
podman-compose.yml Normal file
View File

@@ -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

23
pyproject.toml Normal file
View File

@@ -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 = ["."]

17
requirements-rpi.txt Normal file
View File

@@ -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

8
requirements.txt Normal file
View File

@@ -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

14
run.sh Executable file
View File

@@ -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

100
setup.py Executable file
View File

@@ -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)

108
start_minimal.sh Executable file
View File

@@ -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

179
static/logo.svg Normal file
View File

@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_svg "http://www.w3.org/2000/svg">
<!ENTITY ns_xlink "http://www.w3.org/1999/xlink">
]>
<svg version="1.1" xmlns="&ns_svg;" xmlns:xlink="&ns_xlink;" width="776.79" height="758.327" viewBox="0 0 776.79 758.327"
overflow="visible" enable-background="new 0 0 776.79 758.327" xml:space="preserve">
<style type="text/css">
<![CDATA[
@font-face{font-family:'Castellar';src:url("data:;base64,\
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\
AQAI/5oAAQAC/4sAAQAEAAIABQAHAAgAAA==")}
]]>
</style>
<g id="Ebene_1">
</g>
<g id="Ebene_2">
<rect x="0.75" y="0.75" fill="#7F7F7F" stroke="#000000" stroke-width="1.5" width="775.29" height="754.697"/>
<polygon fill="#FFFFFF" stroke="#000000" stroke-width="1.5" points="520.016,506.097 716.116,442.064 685.815,531.539
756.422,580.134 606.918,629.587 "/>
<path fill="#CECECE" stroke="#000000" stroke-width="1.5" d="M244.357,176.889c0,0,0.459,59.496,2.067,68.684l-7.58,1.837
l-74.195-14.241l-18.147-33.538l21.134-30.092l62.939-9.418l14.242,1.379L244.357,176.889z"/>
<path fill="#CECECE" stroke="#000000" stroke-width="1.5" d="M517.872,238.854c9.635-10.731,21.635-24.731,30.633-36.83
c2.68-4.099,5.072-8.499,7.096-13.364c1.738-3.856,3.133-7.869,4.139-12.004c8.039-33.078,58.805,8.499,58.805,8.499l21.363,14.701
l8.729,22.512l-6.598,30.32c0,0-0.705,3.102-3.367,8.527c-10.938,14.59-22.271,28.771-34.172,42.381
c-3.969,4.537-7.998,9.01-12.1,13.41c-14.893,19.116-34.893,33.116-52.893,49.116c-3.75-26.25,2.5-51.719-3.012-77.001
c-1.102-5.057-2.275-10.105-3.533-15.149c-1.455-9.85-11.455-19.85-13.082-28.93L517.872,238.854z"/>
<path fill="#FFFFFF" stroke="#000000" stroke-width="1.5" d="M241.362,387.484c-10.074-17.857-21.979-47.162-23.811-70.515
c0,0-4.58-18.317,4.578-47.164c0,0,15.11-29.306,29.306-43.5c0,0,17.4-17.857,44.416-25.642c0,0,22.894-9.158,54.947-12.821
c0,0,19.231-4.579,74.638,0c0,0,22.438,3.205,46.248,14.652c0,0,10.531,5.494,31.137,19.231c0,0,26.1,18.316,36.174,42.585
c0,0,7.324,4.121,7.783,118.595l-25.184,16.484l-98.447-30.679l-119.512,7.785l-51.742,13.736L241.362,387.484z"/>
<path fill="#FFFFFF" stroke="#000000" stroke-width="1.5" d="M235.409,425.49l-0.916-116.305c0,0-0.916-16.026,9.158-32.053
c0,0,14.652-30.222,59.068-45.79c0,0,40.295-16.942,100.279-12.363c0,0,36.174,1.375,74.18,21.979c0,0,31.139,17.856,43.043,41.21
c0,0,5.951,11.904,5.951,31.595l0.459,102.112l-107.148-20.605l-96.616,1.832l-65.937,21.063L235.409,425.49z"/>
<path fill="#FFFFFF" stroke="#000000" stroke-width="1.5" d="M20.174,167.932c23.188-6.102,131.193-38.442,131.193-38.442
s3.242,39.534,7.153,50.918c0,0,7.491,30.238,24.577,43.663c0,0,28.68,15.256,52.478,22.578l-7.322,10.983L59.227,307.059
l32.341-91.531L20.174,167.932z"/>
<path fill="#FFFFFF" stroke="#000000" stroke-width="1.5" d="M151.368,129.489c0,0-3.631-34.305-3.827-55.121
c0,0-1.375-10.801,2.749-16.3c0,0,2.945-5.499,10.604-9.034c0,0,13.747-7.854,24.745-10.407c0,0,39.082-8.445,71.878-11.783
s108.013-5.694,167.712-4.713c0,0,53.221-0.589,112.529,11.587c0,0,30.045,4.714,93.479,26.905c0,0,14.141,4.908,22.389,11.39
c0,0,9.82,4.123,14.336,28.868c0,0,4.32,29.263-0.59,59.898c0,0-8.641,55.186-25.334,91.909c0,0,0.785-20.229-1.768-32.208
c0,0,1.168-6.23-21.885-15.394c-0.102-0.04-0.203-0.08-0.307-0.121c-23.244-9.18-29.781-10.007-46.084-13.88
c-0.088-0.021-0.176-0.042-0.264-0.063c-16.496-3.928-38.1-10.212-83.66-14.729c0,0-52.826-8.249-146.699-4.714
c0,0-76.591,3.143-100.941,5.499c0,0-32.6,2.357-46.936,4.518c0,0-14.139,2.553-16.889,6.284c0,0-8.052,5.499-9.819,13.747
c0,0-8.249-17.282-8.838-24.744C157.949,176.884,153.821,163.987,151.368,129.489z"/>
<text transform="matrix(0.9989 -0.0473 0.0473 0.9989 256.5254 158.293)" font-family="'Castellar'" font-size="96">ü</text>
<text transform="matrix(1 0 0 1 338.5337 153.6455)" font-family="'Castellar'" font-size="96">r</text>
<text transform="matrix(0.9955 0.0947 -0.0947 0.9955 416.3237 153.6987)" font-family="'Castellar'" font-size="96">m</text>
<text transform="matrix(0.9793 0.2025 -0.2025 0.9793 518.5571 163.5659)" font-family="'Castellar'" font-size="96">l</text>
<text transform="matrix(0.9766 0.2149 -0.2149 0.9766 576.2554 175.7905)" font-family="'Castellar'" font-size="144">i</text>
<path fill="#CECECE" stroke="#000000" d="M233.049,425.384c-3.468,1.734-56.878,31.908-88.438,67.629
c0,0-12.138,14.221-13.525,21.85l-3.469,27.744l13.873,33.988l45.086,17.688l38.843-4.855l13.179-23.236l-5.202-41.617
l1.388-61.387L233.049,425.384z"/>
<path fill="#CECECE" stroke="#000000" d="M515.354,504.111c0,0,17.686,1.039,33.293,5.549c0,0,21.85,5.895,40.23,15.953
c0,0,14.221,7.283,19.77,15.607c0,0,2.428,2.43,3.814,26.705l-16.301,22.889l-67.281-0.693l-9.018-4.508l-1.039-25.318
L515.354,504.111z"/>
<path fill="#FFFFFF" stroke="#000000" stroke-width="1.5" d="M227.499,591.48l0.693-104.391l-4.855-7.977l-0.347-49.594
c0,0,19.074-23.234,63.813-37.801c0,0,64.854-18.729,155.025-8.672c0,0,57.57,8.324,74.564,20.115c0,0,8.67,4.855,15.953,14.221
c0,0,3.818,4.162-1.732,18.381v19.768c0,0,6.936,12.486-0.348,18.729l0.348,109.59L227.499,591.48z"/>
<path fill="none" stroke="#000000" stroke-width="1.5" d="M223.336,457.609c0,0,9.712-19.768,28.439-29.479
c0,0,38.844-20.115,73.871-22.889c0,0,71.443-5.896,118.957-1.041c0,0,35.029,4.508,51.328,11.098c0,0,16.994,6.938,34.334,23.584"
/>
<path fill="none" stroke="#000000" stroke-width="1.5" d="M227.845,488.128c7.283-11.443,9.364-16.301,22.543-27.398
c0,0,28.092-14.221,63.467-20.115c0,0,72.833-13.176,140.46-4.854c0,0,33.639,5.896,50.98,15.953c0,0,18.033,11.098,24.623,23.93"
/>
<polygon fill="none" stroke="#000000" stroke-width="1.5" points="269.463,584.886 269.463,482.23 321.832,469.744 321.485,593.21
"/>
<polygon fill="none" stroke="#000000" stroke-width="1.5" points="388.42,574.482 388.42,461.421 453.62,463.849 453.967,574.83
"/>
<polygon fill="none" stroke="#000000" stroke-width="1.5" points="500.44,587.662 500.788,474.947 529.573,501.304 530.266,593.21
"/>
<line fill="none" stroke="#000000" stroke-width="1.5" x1="500.788" y1="566.853" x2="529.573" y2="584.193"/>
<polygon stroke="#000000" stroke-width="1.5" points="404.026,561.65 403.333,476.333 440.094,476.333 440.442,561.998 "/>
<polygon stroke="#000000" stroke-width="1.5" points="509.11,571.707 508.417,493.328 524.024,505.119 524.37,580.378 "/>
<polygon fill="#020101" stroke="#000000" stroke-width="1.5" points="280.908,575.175 280.908,488.126 309.693,482.576
309.693,568.933 "/>
<path fill="none" stroke="#FFFFFF" stroke-width="1.5" d="M315.589,495.062c-11.792,1.734-24.971,4.508-24.971,4.508v15.26
l23.236-5.549L315.589,495.062z"/>
<line fill="none" stroke="#FFFFFF" stroke-width="1.5" x1="302.757" y1="497.142" x2="302.757" y2="512.402"/>
<polyline fill="none" stroke="#FFFFFF" stroke-width="1.5" points="290.619,572.402 290.619,524.888 313.162,521.074 "/>
<line fill="none" stroke="#FFFFFF" stroke-width="1.5" x1="302.41" y1="523.501" x2="302.757" y2="570.32"/>
<line fill="none" stroke="#FFFFFF" stroke-width="1.5" x1="290.965" y1="540.494" x2="313.508" y2="537.373"/>
<line fill="none" stroke="#FFFFFF" stroke-width="1.5" x1="290.965" y1="558.529" x2="313.855" y2="556.101"/>
<path fill="none" stroke="#000000" stroke-width="1.5" d="M269.81,577.951c12.832-2.428,51.328-11.445,51.328-11.445"/>
<path fill="none" stroke="#FFFFFF" stroke-width="1.5" d="M398.428,490.8c5.686,0.277,30.506,0.139,30.506,0.139v16.5
l-29.258-0.139"/>
<line fill="none" stroke="#FFFFFF" stroke-width="1.5" x1="415.344" y1="491.632" x2="415.483" y2="507.3"/>
<polyline fill="none" stroke="#FFFFFF" stroke-width="1.5" points="399.399,512.431 428.657,512.154 428.518,561.796 "/>
<line fill="none" stroke="#FFFFFF" stroke-width="1.5" x1="415.206" y1="512.431" x2="414.79" y2="561.242"/>
<line fill="none" stroke="#FFFFFF" stroke-width="1.5" x1="428.379" y1="533.509" x2="399.538" y2="533.371"/>
<line fill="none" stroke="#FFFFFF" stroke-width="1.5" x1="428.379" y1="549.593" x2="398.012" y2="549.455"/>
<line fill="none" stroke="#000000" stroke-width="1.5" x1="388.073" y1="561.302" x2="453.274" y2="561.998"/>
<path fill="none" stroke="#000000" stroke-width="1.5" d="M245.483,398.47c0-31.594,0-78.3,0-78.3"/>
<polygon stroke="#000000" stroke-width="1.5" points="261.051,392.976 260.593,318.339 268.835,311.013 259.219,299.565
273.873,286.744 277.078,290.407 277.078,385.65 "/>
<polygon stroke="#000000" stroke-width="1.5" points="293.562,381.07 293.562,290.865 299.056,286.744 298.598,273.923
315.999,265.223 322.867,275.755 323.325,375.119 "/>
<polygon stroke="#000000" stroke-width="1.5" points="340.267,373.744 339.809,276.212 345.762,272.091 345.762,259.271
349.883,253.317 375.067,254.233 375.983,270.718 378.731,273.465 378.731,372.37 "/>
<polygon stroke="#000000" stroke-width="1.5" points="395.672,372.37 395.672,272.549 399.793,269.344 401.167,255.607
430.93,261.56 424.977,267.513 425.436,274.381 430.014,278.96 430.93,372.828 "/>
<polygon stroke="#000000" stroke-width="1.5" points="449.704,376.035 449.704,272.091 457.03,267.055 477.178,278.044
469.852,289.033 476.721,299.107 475.805,384.277 "/>
<polygon stroke="#000000" stroke-width="1.5" points="492.747,382.902 493.204,294.07 495.952,290.407 504.653,300.938
496.411,310.097 496.411,316.05 501.905,321.086 501.905,387.023 "/>
<polyline fill="none" stroke="#000000" stroke-width="1.5" points="522.051,394.808 518.389,391.603 518.846,319.713 "/>
<polyline fill="none" stroke="#FFFFFF" stroke-width="1.5" points="296.767,382.902 296.767,374.66 316.915,371.913
316.457,381.07 "/>
<polyline fill="none" stroke="#FFFFFF" stroke-width="1.5" points="346.22,376.492 345.762,366.418 369.114,366.876
369.114,376.949 "/>
<polyline fill="none" stroke="#FFFFFF" stroke-width="1.5" points="402.542,376.035 402.542,365.96 424.52,365.96 424.52,376.492
"/>
<polyline fill="none" stroke="#FFFFFF" stroke-width="1.5" points="456.116,382.445 457.03,372.828 473.973,377.865
473.973,386.566 "/>
<text transform="matrix(0.9442 -0.0882 0.0931 0.9957 168.582 166.0718)" font-family="'Castellar'" font-size="144">T</text>
<path fill="#FFFDFD" stroke="#000000" stroke-width="1.5" d="M610.063,545.832c0,0,6.861,18.869,14.293,53.457
c0,0,5.309,31.57,7.916,56.316c0.029,0.287,0.059,0.572,0.088,0.857c2.51,24.273,1.48,39.016-6.014,43.162
c-0.184,0.102-0.371,0.197-0.563,0.287c-8.004,3.717-18.01,14.58-110.627,25.729c0,0-77.186,9.434-151.223,8.576
c0,0-83.188,0.857-121.207-8.576c0,0-72.608-13.15-102.338-26.871c0,0-29.726-10.291-31.73-45.451c0,0-3.719-28.867,10.859-107.193
c0,0,0.572-9.432,11.721-32.873c0,0-21.439,43.166,96.621,61.746c0,0,67.461,10.006,166.082,8.576
c0,0,141.217,2.002,183.811-12.863C577.752,570.71,609.499,562.976,610.063,545.832z"/>
<text transform="matrix(0.9886 0.1507 -0.1507 0.9886 204.0767 695.6118)" font-family="'Castellar'" font-size="144">B</text>
<text transform="matrix(1 0 0 1 305.772 707.8472)" font-family="'Castellar'" font-size="144">a</text>
<text transform="matrix(0.9913 -0.1317 0.1317 0.9913 436.1226 709.8608)" font-family="'Castellar'" font-size="144">r</text>
<text transform="matrix(0.9996 -0.0296 0.0296 0.9996 551.2358 718.1958)" font-family="'Castellar'" font-size="144">*</text>
<text transform="matrix(1 0 0 1 125.2202 712.3237)" font-family="'Castellar'" font-size="144">*</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

229
test_logo.html Normal file
View File

@@ -0,0 +1,229 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Logo Display Test</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.test-container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 10px;
}
h2 {
color: #333;
border-bottom: 2px solid #667eea;
padding-bottom: 10px;
}
.test-section {
margin: 30px 0;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
}
.logo-test {
display: inline-block;
margin: 10px;
text-align: center;
}
.logo-test img {
display: block;
margin-bottom: 10px;
}
.logo-test span {
display: block;
font-size: 12px;
color: #666;
}
/* Test 1: Raw display */
.test1 img {
width: 150px;
height: auto;
}
/* Test 2: With white background */
.test2 img {
width: 150px;
height: auto;
background: white;
padding: 10px;
border-radius: 10px;
}
/* Test 3: With object-fit contain */
.test3 img {
width: 150px;
height: 150px;
object-fit: contain;
background: #f0f0f0;
padding: 10px;
border-radius: 10px;
}
/* Test 4: With shadow */
.test4 img {
width: 150px;
height: auto;
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.3));
}
/* Test 5: In a circle container */
.test5 {
width: 150px;
height: 150px;
background: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.test5 img {
width: 80%;
height: 80%;
object-fit: contain;
}
/* Test 6: In a square container with border */
.test6 {
width: 150px;
height: 150px;
background: white;
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
padding: 10px;
}
.test6 img {
width: 100%;
height: 100%;
object-fit: contain;
}
.error {
color: red;
font-weight: bold;
}
.success {
color: green;
font-weight: bold;
}
</style>
</head>
<body>
<div class="test-container">
<h1>Turmli Bar Logo Display Tests</h1>
<div class="test-section">
<h2>Direct File Access Tests</h2>
<div class="logo-test test1">
<img src="Vektor-Logo.svg" alt="Logo from root" onerror="this.parentElement.innerHTML+='<span class=error>Failed to load from root</span>'" onload="this.parentElement.innerHTML+='<span class=success>Loaded from root</span>'">
<span>Direct from root</span>
</div>
<div class="logo-test test1">
<img src="static/logo.svg" alt="Logo from static" onerror="this.parentElement.innerHTML+='<span class=error>Failed to load from static</span>'" onload="this.parentElement.innerHTML+='<span class=success>Loaded from static</span>'">
<span>From static folder</span>
</div>
<div class="logo-test test1">
<img src="/static/logo.svg" alt="Logo with absolute path" onerror="this.parentElement.innerHTML+='<span class=error>Failed with /static/</span>'" onload="this.parentElement.innerHTML+='<span class=success>Loaded with /static/</span>'">
<span>Absolute /static/ path</span>
</div>
</div>
<div class="test-section">
<h2>Display Style Tests</h2>
<div class="logo-test test2">
<img src="static/logo.svg" alt="With white background">
<span>White background + padding</span>
</div>
<div class="logo-test test3">
<img src="static/logo.svg" alt="Object-fit contain">
<span>Object-fit: contain</span>
</div>
<div class="logo-test test4">
<img src="static/logo.svg" alt="With drop shadow">
<span>Drop shadow filter</span>
</div>
</div>
<div class="test-section">
<h2>Container Tests</h2>
<div class="logo-test">
<div class="test5">
<img src="static/logo.svg" alt="In circle">
</div>
<span>Circle container</span>
</div>
<div class="logo-test">
<div class="test6">
<img src="static/logo.svg" alt="In square">
</div>
<span>Square container</span>
</div>
</div>
<div class="test-section">
<h2>Inline SVG Test</h2>
<div style="width: 150px; height: 150px; background: white; padding: 10px; border-radius: 10px; display: inline-block;">
<object data="static/logo.svg" type="image/svg+xml" style="width: 100%; height: 100%;">
<span class="error">SVG as object failed</span>
</object>
</div>
<span style="display: block; margin-top: 10px; font-size: 12px;">Using &lt;object&gt; tag</span>
</div>
<div class="test-section">
<h2>Debug Information</h2>
<ul>
<li>Open this file directly in your browser (file:// protocol)</li>
<li>Check which display methods work</li>
<li>Check browser console for any errors</li>
<li>The SVG has a gray background (#7F7F7F) which might be part of the issue</li>
</ul>
</div>
</div>
<script>
console.log('Logo test page loaded');
// Check if images are loading
document.querySelectorAll('img').forEach((img, index) => {
img.addEventListener('load', () => {
console.log(`Image ${index} loaded successfully:`, img.src);
});
img.addEventListener('error', () => {
console.error(`Image ${index} failed to load:`, img.src);
});
});
</script>
</body>
</html>

189
test_server.py Executable file
View File

@@ -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)