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