Commit All
This commit is contained in:
70
.containerignore
Normal file
70
.containerignore
Normal 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
70
.dockerignore
Normal 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
25
.env.example
Normal 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
173
.gitignore
vendored
Normal 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
36
Containerfile
Normal 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
422
DEPLOY_README.md
Normal 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
36
Dockerfile
Normal 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
241
FIX_INSTALLATION.md
Normal 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
84
Makefile
Normal 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
359
PODMAN_README.md
Normal 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
313
README.md
Normal 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
179
Vektor-Logo.svg
Normal 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
294
deploy-podman.sh
Executable 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
180
deploy.sh
Executable 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
674
deploy_rpi.sh
Executable 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
51
deployment/README.md
Normal 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
389
deployment/monitor.sh
Executable 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
|
||||
42
deployment/turmli-calendar.service
Normal file
42
deployment/turmli-calendar.service
Normal 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
20
docker-compose.yml
Normal 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
240
fix_install_rpi.sh
Executable 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
747
main.py
Normal 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
24
podman-compose.yml
Normal 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
23
pyproject.toml
Normal 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
17
requirements-rpi.txt
Normal 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
8
requirements.txt
Normal 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
14
run.sh
Executable 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
100
setup.py
Executable 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
108
start_minimal.sh
Executable 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
179
static/logo.svg
Normal 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
229
test_logo.html
Normal 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 <object> 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
189
test_server.py
Executable 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)
|
||||
Reference in New Issue
Block a user