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,\
 | 
				
			||||||
 | 
					T1RUTwADACAAAQAQQ0ZGIF3BGscAAACgAAAKZEdQT1Ovhb56AAALBAAAAG5jbWFwAu8BNQAAADwA\
 | 
				
			||||||
 | 
					AABkAAAAAQAAAAMAAAAMAAQAWAAAABIAEAADAAIAKgBCAFQAYQBpAG0AcgD8//8AAAAqAEIAVABh\
 | 
				
			||||||
 | 
					AGkAbAByAPz////X/8H/tP+h/5v/mf+V/w0AAQAAAAAAAAAAAAAAAAAAAAAAAAEABAIAAQEBCkNh\
 | 
				
			||||||
 | 
					c3RlbGxhcgABAQE5+BsB+BgE+BwMFfwF/M8cCaQcB1YFHqAASIKBJf+Lix6gAEiCgSX/i4sMB/cx\
 | 
				
			||||||
 | 
					D4wQ90QRkhwKXRIAAgEBOkdDYXN0ZWxsYXIgaXMgYSB0cmFkZW1hcmsgb2YgVGhlIE1vbm90eXBl\
 | 
				
			||||||
 | 
					IENvcnBvcmF0aW9uIHBsYy4vRlNUeXBlIDggZGVmAAAAAAsAIgAjACoALQAuADMANQDDAAoCAAEA\
 | 
				
			||||||
 | 
					HgD7Aa0DfAPuBHUFqQdaCAsJlfv/HAVVBPnwHPqr/fAGtBwFLBUc+v35nhwFAwcO/H74XRwFnBVN\
 | 
				
			||||||
 | 
					BqI+nDOWKgiWBvsB97wV928GTypp+wWD+xb3D8fn08rf1ftpGJRgX5BfG2poiIZkH2SGaIRtguYl\
 | 
				
			||||||
 | 
					5kXoZvtA+xkYgPZi9wJG9wVTJGj7An37CftO9xMY7rjn0+DvCJlETJJVG01XhX5hH8v3Z7Vet2a4\
 | 
				
			||||||
 | 
					bRm4bb1ww3KJxILFesR6xHXAbrwI+Cr7iRX7jDeNfuCk45foiRn86PcFFXlN94w8j5s2qT21RMAZ\
 | 
				
			||||||
 | 
					98v7VRWBg8A0sTyjQhm8rwX7bfdhFYGTYFtpaHN0GXN0bHJmccVkGA73qvmkHAUCFfibHPslBc4G\
 | 
				
			||||||
 | 
					/L0cBS8FWvs8FXloc1duR21HcEtzUHNQckxxSHFIel6CdrKGuYfAiQiJv72Kuhv3CeqQlNYfctZg\
 | 
				
			||||||
 | 
					9wBO9x9O9x9U9wdZ5wj1960V+EX+ncP7G8z7Gdb7GRn3OmgFgf1WlQf3pK6BzXnTcNkZcNhtz2rE\
 | 
				
			||||||
 | 
					CP0HBnBIdlN7YHpgfF58XHxcgWSGbPeaaBiB/MOVB/dMrrjYtOGw6Bn4YxwEmgUOdqKVFfePsJbC\
 | 
				
			||||||
 | 
					lOmS9xkZkvcZjvLVGvfWB+aJ4obeHobehcyDvPuYrhiV+XsH9t96acofymi3YKRYCKRYmFVSGiZp\
 | 
				
			||||||
 | 
					O0hQHkdQOWkqggiHB++G4HfSZtFmwFyvUgivUZ1NSRpTfVJwUB5vUFlZQmEIYEIpdvsPG/24BvkM\
 | 
				
			||||||
 | 
					HAWBFVhkh4NxH3ogg/sS+yYaII1Hj20ef6izhb0bzsaXpL8fv6O0r6m7CKm6msbRGsGAvHS4HnS3\
 | 
				
			||||||
 | 
					Z65apgilWkyYPxuN/TAVSl2GgnEfhlqIPPsCGvsXkPsRlPsMHrF2rXyqgwiDqbCHuBvSyJekwB/A\
 | 
				
			||||||
 | 
					o7SvqbwIqLyaxtEa0XrJasEeacFZtkqqCKpJPJouG/vR+UIVHPqU0RwFbAf3pJgVgwfUicl8vnC+\
 | 
				
			||||||
 | 
					b7FlpFwIpFuYVE0aUIBYdV8edF5vaGhyaHJneWaCCIAH6JjTsL/ICL7IpdPfGr+DuXq0Hnq0cq1s\
 | 
				
			||||||
 | 
					qGunZ6BimgiZYl2SVxt+eIqIcB/3A/08FYMHxoDCd75svmy1YapXCKpWmk1CGlB+U3JYHnFXYWBS\
 | 
				
			||||||
 | 
					aghqUUN6NBuDB4iWnomoG9/TnK3HH8esuLepwgiowprFyBrUe8xsxB5sxFy4TqxNrEGcNI4IDvfo\
 | 
				
			||||||
 | 
					HAWRFRz6ltEcBWoH+BEc+m8V/YiVBveIsJbelPCR9woZkPcJjvcH9wUa9zwH93mB90139yMe+4yu\
 | 
				
			||||||
 | 
					BZX5iIEH+45ohEqFUYhXGYhWiEyIQQiIQIpFShr7cQdijUmOLx6OLo89kEuQS49bkGv3jGgYDiD3\
 | 
				
			||||||
 | 
					6BwFkxUc+pTRHAVsB/wJphWV+YiBB/uMaIJKhDSF+wEZhfsCiCs5GvxCBySQ+wCW+wQeZ/cG9wp5\
 | 
				
			||||||
 | 
					9w0bxsGOkrwfvJKukZ+R8/elGJoGXPwBBRz7XpUG94Ouk6uSxJHdGZDdkOCO4giO4o3Jrxr3kAfC\
 | 
				
			||||||
 | 
					isuI0x6I0ojKiMGHwYa3ha4IDvmT96gcBZEV+Wcc+smuz/1EHATzBRwFa0oVUfsJ4xz7TAXTBikc\
 | 
				
			||||||
 | 
					BSkFHPlS8xX4QgavO6pKpFikWKpQr0f3rfydGND7EcUtukyyysj3BN/3NfeU+HUY1Pcfw/cMsvAI\
 | 
				
			||||||
 | 
					+ECBBvueZgWHUIlRUBpOkfsSl/tRHpb7Upn7RZz7N5z7OJoqmW73TGgYgf1ClQf3gq4FkL6Oz+Ea\
 | 
				
			||||||
 | 
					uonJiNgeiNeG44TuhO6C5oHfgd+CxIKobFlqUmlK/Bn9bRhO+wtiNXZYCH0GfapzvGnOac5wwHax\
 | 
				
			||||||
 | 
					+9r48BhU8l3dZ8iGcIRPgiyCLIIlg/sDgvsDhS2HPQiGPYlOXhpsjF2OTx73UmYFgfx/lQf3UrCU\
 | 
				
			||||||
 | 
					p5jnm/cxGZv3MZn3NZf3OgiX9zmR9wzVGqWKs4jCHnizZbZUufs+sBgO91T3yfjoFffJB9qI5oby\
 | 
				
			||||||
 | 
					HobxhNuDxvuErhiV+SUH9xHuclrWH9VZvlKoTAioS5lUXRpMfVJuWh5uWWZjXW5cblx5W4YIhwe4\
 | 
				
			||||||
 | 
					ebFzqW2idKZqqWGpYLNRvEK8QrNRqWKpYqdppHEIU8PCb8IbgftCByo9qMZSH3SidaZ1qnSqaL1b\
 | 
				
			||||||
 | 
					0lrSY8VquWq5cKx4nm6ocZ50lQiUc2iQXRthaomGdB+HZ4ljXxr7TQc9kySc+xQe941oBYH9e5UH\
 | 
				
			||||||
 | 
					93+ulc6T6pH3DhmR9w6O5MMa91v4XRX7OAdRjVWPWR6HpbGJvBvg0JqowB+/qLCyoroIobqWvsMa\
 | 
				
			||||||
 | 
					woC+droedbpvsmmsaKtlpGGdCJxgYpRiG2RtiIZ1H4VkhlSGQwiGQ4hPWxr3/PyEFfdt+8+/QblW\
 | 
				
			||||||
 | 
					tGwZtGu7e8KKCJUHVppIzzj3DPtC944YZsJotGqnaqdjnlyUh4MYsHOvZaxYCPyq+fgVHPqW0xwF\
 | 
				
			||||||
 | 
					agf3XpwVgQfmidh1yWHIYLhXp04Ip06ZTk8a+xlRKvsIUB6Rh8+cw7C4xBm3xKHO2hrIf8NzvR5z\
 | 
				
			||||||
 | 
					vGu1YqxirF2lWJwInFhYlFgbDuH5OBwFkxUc+pTOHAVsB/yHsBX5+AbAu42Otx+3jsSP0JKi+9gY\
 | 
				
			||||||
 | 
					fwZB94AFlk5MkEkbUliIhVwfXIVZgVV9hk2HT4hSiFGJVopaCIpaiml5GvvgBzSOKZH7AB6R+wGS\
 | 
				
			||||||
 | 
					NZRK94tmGIH9h5UH94ewBaD3J5X3R/dlGvf+B9aJ1IjUHojUhdCDzGSWXpNYkliRWo9djAhCRIV/\
 | 
				
			||||||
 | 
					RB9D+4IFfQah99yfiqiIsYYZhrGqiKQbDvgV+l+kFX4H2pDPmMSgxKC7qLGxsLCnup7ECJ7ElMzW\
 | 
				
			||||||
 | 
					GvpKRf5IB/sZbCFOPh5OPihd+x18CPsXfhWYBy2QOZ5FrESrVLtkywhjynfZ5xr6XEX+fwf7D7Yp\
 | 
				
			||||||
 | 
					4kQe4UP3HWP3UIMI/M74MBX4wQf3EIP3J3v3PR77ZawFlfltgQf7nGqEXIZTh0oZh0qIRYhBCIhB\
 | 
				
			||||||
 | 
					iVBeGvwvBymfPLNQHrJPwGHMcghyzNJ/2hv3GvStz9gf18+x9wL3LBr4QAfXiOWG8x6G8oTag8H7\
 | 
				
			||||||
 | 
					m6wYlflpgQf7YGqCYIRDhiYZhiaINkYa/HkHQIJLeVYeeVZvXmVlcXFwdW55bnlne19+X35bgVeF\
 | 
				
			||||||
 | 
					CIVXS4g+G/sR+wGZpy4fLqdDulbMCFbMcOH0GvhDHATdFZqYhoCXH5aAkX17GnuFfYCAHoCAfYV8\
 | 
				
			||||||
 | 
					G3p9kJaAH4CWhpmcGpyRmZaWHpWWmJCcG/f4FpuZhYCWH5Z/kH58GnqGfoGAHoCAfYV6G3p+kZaA\
 | 
				
			||||||
 | 
					H4CWhpmbGpmRmJaXHpeWmZGaGw75qBQcBWsVAAEAAAAKAB4ALAABREZMVAAIAAQAAAAA//8AAQAA\
 | 
				
			||||||
 | 
					AAFrZXJuAAgAAAABAAAAAQAEAAIAAAABAAgAAQAqAAQAAAAEABIAGAAeACQAAQAI/30AAQAI/5oA\
 | 
				
			||||||
 | 
					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,\
 | 
				
			||||||
 | 
					T1RUTwADACAAAQAQQ0ZGIF3BGscAAACgAAAKZEdQT1Ovhb56AAALBAAAAG5jbWFwAu8BNQAAADwA\
 | 
				
			||||||
 | 
					AABkAAAAAQAAAAMAAAAMAAQAWAAAABIAEAADAAIAKgBCAFQAYQBpAG0AcgD8//8AAAAqAEIAVABh\
 | 
				
			||||||
 | 
					AGkAbAByAPz////X/8H/tP+h/5v/mf+V/w0AAQAAAAAAAAAAAAAAAAAAAAAAAAEABAIAAQEBCkNh\
 | 
				
			||||||
 | 
					c3RlbGxhcgABAQE5+BsB+BgE+BwMFfwF/M8cCaQcB1YFHqAASIKBJf+Lix6gAEiCgSX/i4sMB/cx\
 | 
				
			||||||
 | 
					D4wQ90QRkhwKXRIAAgEBOkdDYXN0ZWxsYXIgaXMgYSB0cmFkZW1hcmsgb2YgVGhlIE1vbm90eXBl\
 | 
				
			||||||
 | 
					IENvcnBvcmF0aW9uIHBsYy4vRlNUeXBlIDggZGVmAAAAAAsAIgAjACoALQAuADMANQDDAAoCAAEA\
 | 
				
			||||||
 | 
					HgD7Aa0DfAPuBHUFqQdaCAsJlfv/HAVVBPnwHPqr/fAGtBwFLBUc+v35nhwFAwcO/H74XRwFnBVN\
 | 
				
			||||||
 | 
					BqI+nDOWKgiWBvsB97wV928GTypp+wWD+xb3D8fn08rf1ftpGJRgX5BfG2poiIZkH2SGaIRtguYl\
 | 
				
			||||||
 | 
					5kXoZvtA+xkYgPZi9wJG9wVTJGj7An37CftO9xMY7rjn0+DvCJlETJJVG01XhX5hH8v3Z7Vet2a4\
 | 
				
			||||||
 | 
					bRm4bb1ww3KJxILFesR6xHXAbrwI+Cr7iRX7jDeNfuCk45foiRn86PcFFXlN94w8j5s2qT21RMAZ\
 | 
				
			||||||
 | 
					98v7VRWBg8A0sTyjQhm8rwX7bfdhFYGTYFtpaHN0GXN0bHJmccVkGA73qvmkHAUCFfibHPslBc4G\
 | 
				
			||||||
 | 
					/L0cBS8FWvs8FXloc1duR21HcEtzUHNQckxxSHFIel6CdrKGuYfAiQiJv72Kuhv3CeqQlNYfctZg\
 | 
				
			||||||
 | 
					9wBO9x9O9x9U9wdZ5wj1960V+EX+ncP7G8z7Gdb7GRn3OmgFgf1WlQf3pK6BzXnTcNkZcNhtz2rE\
 | 
				
			||||||
 | 
					CP0HBnBIdlN7YHpgfF58XHxcgWSGbPeaaBiB/MOVB/dMrrjYtOGw6Bn4YxwEmgUOdqKVFfePsJbC\
 | 
				
			||||||
 | 
					lOmS9xkZkvcZjvLVGvfWB+aJ4obeHobehcyDvPuYrhiV+XsH9t96acofymi3YKRYCKRYmFVSGiZp\
 | 
				
			||||||
 | 
					O0hQHkdQOWkqggiHB++G4HfSZtFmwFyvUgivUZ1NSRpTfVJwUB5vUFlZQmEIYEIpdvsPG/24BvkM\
 | 
				
			||||||
 | 
					HAWBFVhkh4NxH3ogg/sS+yYaII1Hj20ef6izhb0bzsaXpL8fv6O0r6m7CKm6msbRGsGAvHS4HnS3\
 | 
				
			||||||
 | 
					Z65apgilWkyYPxuN/TAVSl2GgnEfhlqIPPsCGvsXkPsRlPsMHrF2rXyqgwiDqbCHuBvSyJekwB/A\
 | 
				
			||||||
 | 
					o7SvqbwIqLyaxtEa0XrJasEeacFZtkqqCKpJPJouG/vR+UIVHPqU0RwFbAf3pJgVgwfUicl8vnC+\
 | 
				
			||||||
 | 
					b7FlpFwIpFuYVE0aUIBYdV8edF5vaGhyaHJneWaCCIAH6JjTsL/ICL7IpdPfGr+DuXq0Hnq0cq1s\
 | 
				
			||||||
 | 
					qGunZ6BimgiZYl2SVxt+eIqIcB/3A/08FYMHxoDCd75svmy1YapXCKpWmk1CGlB+U3JYHnFXYWBS\
 | 
				
			||||||
 | 
					aghqUUN6NBuDB4iWnomoG9/TnK3HH8esuLepwgiowprFyBrUe8xsxB5sxFy4TqxNrEGcNI4IDvfo\
 | 
				
			||||||
 | 
					HAWRFRz6ltEcBWoH+BEc+m8V/YiVBveIsJbelPCR9woZkPcJjvcH9wUa9zwH93mB90139yMe+4yu\
 | 
				
			||||||
 | 
					BZX5iIEH+45ohEqFUYhXGYhWiEyIQQiIQIpFShr7cQdijUmOLx6OLo89kEuQS49bkGv3jGgYDiD3\
 | 
				
			||||||
 | 
					6BwFkxUc+pTRHAVsB/wJphWV+YiBB/uMaIJKhDSF+wEZhfsCiCs5GvxCBySQ+wCW+wQeZ/cG9wp5\
 | 
				
			||||||
 | 
					9w0bxsGOkrwfvJKukZ+R8/elGJoGXPwBBRz7XpUG94Ouk6uSxJHdGZDdkOCO4giO4o3Jrxr3kAfC\
 | 
				
			||||||
 | 
					isuI0x6I0ojKiMGHwYa3ha4IDvmT96gcBZEV+Wcc+smuz/1EHATzBRwFa0oVUfsJ4xz7TAXTBikc\
 | 
				
			||||||
 | 
					BSkFHPlS8xX4QgavO6pKpFikWKpQr0f3rfydGND7EcUtukyyysj3BN/3NfeU+HUY1Pcfw/cMsvAI\
 | 
				
			||||||
 | 
					+ECBBvueZgWHUIlRUBpOkfsSl/tRHpb7Upn7RZz7N5z7OJoqmW73TGgYgf1ClQf3gq4FkL6Oz+Ea\
 | 
				
			||||||
 | 
					uonJiNgeiNeG44TuhO6C5oHfgd+CxIKobFlqUmlK/Bn9bRhO+wtiNXZYCH0GfapzvGnOac5wwHax\
 | 
				
			||||||
 | 
					+9r48BhU8l3dZ8iGcIRPgiyCLIIlg/sDgvsDhS2HPQiGPYlOXhpsjF2OTx73UmYFgfx/lQf3UrCU\
 | 
				
			||||||
 | 
					p5jnm/cxGZv3MZn3NZf3OgiX9zmR9wzVGqWKs4jCHnizZbZUufs+sBgO91T3yfjoFffJB9qI5oby\
 | 
				
			||||||
 | 
					HobxhNuDxvuErhiV+SUH9xHuclrWH9VZvlKoTAioS5lUXRpMfVJuWh5uWWZjXW5cblx5W4YIhwe4\
 | 
				
			||||||
 | 
					ebFzqW2idKZqqWGpYLNRvEK8QrNRqWKpYqdppHEIU8PCb8IbgftCByo9qMZSH3SidaZ1qnSqaL1b\
 | 
				
			||||||
 | 
					0lrSY8VquWq5cKx4nm6ocZ50lQiUc2iQXRthaomGdB+HZ4ljXxr7TQc9kySc+xQe941oBYH9e5UH\
 | 
				
			||||||
 | 
					93+ulc6T6pH3DhmR9w6O5MMa91v4XRX7OAdRjVWPWR6HpbGJvBvg0JqowB+/qLCyoroIobqWvsMa\
 | 
				
			||||||
 | 
					woC+droedbpvsmmsaKtlpGGdCJxgYpRiG2RtiIZ1H4VkhlSGQwiGQ4hPWxr3/PyEFfdt+8+/QblW\
 | 
				
			||||||
 | 
					tGwZtGu7e8KKCJUHVppIzzj3DPtC944YZsJotGqnaqdjnlyUh4MYsHOvZaxYCPyq+fgVHPqW0xwF\
 | 
				
			||||||
 | 
					agf3XpwVgQfmidh1yWHIYLhXp04Ip06ZTk8a+xlRKvsIUB6Rh8+cw7C4xBm3xKHO2hrIf8NzvR5z\
 | 
				
			||||||
 | 
					vGu1YqxirF2lWJwInFhYlFgbDuH5OBwFkxUc+pTOHAVsB/yHsBX5+AbAu42Otx+3jsSP0JKi+9gY\
 | 
				
			||||||
 | 
					fwZB94AFlk5MkEkbUliIhVwfXIVZgVV9hk2HT4hSiFGJVopaCIpaiml5GvvgBzSOKZH7AB6R+wGS\
 | 
				
			||||||
 | 
					NZRK94tmGIH9h5UH94ewBaD3J5X3R/dlGvf+B9aJ1IjUHojUhdCDzGSWXpNYkliRWo9djAhCRIV/\
 | 
				
			||||||
 | 
					RB9D+4IFfQah99yfiqiIsYYZhrGqiKQbDvgV+l+kFX4H2pDPmMSgxKC7qLGxsLCnup7ECJ7ElMzW\
 | 
				
			||||||
 | 
					GvpKRf5IB/sZbCFOPh5OPihd+x18CPsXfhWYBy2QOZ5FrESrVLtkywhjynfZ5xr6XEX+fwf7D7Yp\
 | 
				
			||||||
 | 
					4kQe4UP3HWP3UIMI/M74MBX4wQf3EIP3J3v3PR77ZawFlfltgQf7nGqEXIZTh0oZh0qIRYhBCIhB\
 | 
				
			||||||
 | 
					iVBeGvwvBymfPLNQHrJPwGHMcghyzNJ/2hv3GvStz9gf18+x9wL3LBr4QAfXiOWG8x6G8oTag8H7\
 | 
				
			||||||
 | 
					m6wYlflpgQf7YGqCYIRDhiYZhiaINkYa/HkHQIJLeVYeeVZvXmVlcXFwdW55bnlne19+X35bgVeF\
 | 
				
			||||||
 | 
					CIVXS4g+G/sR+wGZpy4fLqdDulbMCFbMcOH0GvhDHATdFZqYhoCXH5aAkX17GnuFfYCAHoCAfYV8\
 | 
				
			||||||
 | 
					G3p9kJaAH4CWhpmcGpyRmZaWHpWWmJCcG/f4FpuZhYCWH5Z/kH58GnqGfoGAHoCAfYV6G3p+kZaA\
 | 
				
			||||||
 | 
					H4CWhpmbGpmRmJaXHpeWmZGaGw75qBQcBWsVAAEAAAAKAB4ALAABREZMVAAIAAQAAAAA//8AAQAA\
 | 
				
			||||||
 | 
					AAFrZXJuAAgAAAABAAAAAQAEAAIAAAABAAgAAQAqAAQAAAAEABIAGAAeACQAAQAI/30AAQAI/5oA\
 | 
				
			||||||
 | 
					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