Version 1.1

This commit is contained in:
2026-01-28 15:31:49 +01:00
parent b5b11b5826
commit 105bcc8d7a
10 changed files with 553 additions and 4 deletions

16
.env.example Normal file
View File

@@ -0,0 +1,16 @@
# Copy to .env and adjust as needed.
# Port exposed on the host
WEB_PORT=5000
# Flask environment (development/production)
FLASK_ENV=production
# These are read by the app (see app.py) and are also used by docker-compose.yml.
SECRET_KEY=change-me
# Store the SQLite DB in the Flask instance folder
SQLALCHEMY_DATABASE_URI=sqlite:////instance/liturgie.db
# Default admin bootstrap (created only if the user does not exist yet)
ADMIN_USERNAME=admin
ADMIN_PASSWORD=admin

33
.gitignore vendored Normal file
View File

@@ -0,0 +1,33 @@
### Python
__pycache__/
*.py[cod]
*$py.class
*.pyo
*.pyd
.Python
*.egg-info/
.eggs/
dist/
build/
### Virtual environments
.venv/
venv/
ENV/
### IDEs
.vscode/
.idea/
### OS
Thumbs.db
Desktop.ini
.DS_Store
### Secrets / local env
.env
### App data (dont commit runtime db/uploads)
instance/*.db
static/uploads/*
!static/uploads/readme.txt

View File

@@ -0,0 +1,82 @@
# Psalmbord Online
## Run with Docker Compose
This project includes a `Dockerfile` + `docker-compose.yml` to run the Flask app behind gunicorn.
### Configure environment (optional)
Copy the example env file:
```bash
cp .env.example .env
```
### Start
```bash
docker compose up -d --build
```
Open:
- http://localhost:5000 (or `WEB_PORT`)
### Persisted data
Docker compose bind-mounts the following so data survives container rebuilds/recreates:
- `./instance/` -> SQLite database file (stored at `instance/liturgie.db`)
- `./static/uploads/` -> uploaded backgrounds/logos
### Default admin user
On startup, `init_db.py` ensures DB/tables exist and creates an admin user **only if it does not already exist**.
Defaults:
- Username: `admin`
- Password: `admin`
Override via `.env`:
```env
ADMIN_USERNAME=admin
ADMIN_PASSWORD=change-me
```
## Release / publish (git + docker push)
This repo includes helper scripts that:
1. Prompt for a version (e.g. `1.2.3`)
2. Create a git commit with message `Version <version>`
3. Push to: `https://git.alphen.cloud/bramval/PsalmbordOnlineCE`
4. Build + push Docker image to:
- `git.alphen.cloud/bramval/psalmbordonlinece:<version>`
- `git.alphen.cloud/bramval/psalmbordonlinece:latest`
### Recommended (cross-platform Python)
```bash
python release.py
```
### Windows (PowerShell)
```powershell
./release.ps1
```
### Linux / macOS (bash)
```bash
chmod +x ./release.sh
./release.sh
```
If Docker push fails due to authentication, run:
```bash
docker login git.alphen.cloud
```

20
app.py
View File

@@ -12,8 +12,18 @@ UPLOAD_FOLDER = 'static/uploads'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your_secret_key_here'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///liturgie.db'
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'your_secret_key_here')
# Prefer an env override, otherwise store the SQLite DB in Flask's `instance/` folder.
# This keeps the DB out of the repo root and makes Docker persistence easier.
default_db_path = os.path.join(app.instance_path, 'liturgie.db')
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get(
'SQLALCHEMY_DATABASE_URI',
f"sqlite:///{default_db_path}",
)
# Ensure the instance folder exists so SQLite can create/open the DB file.
os.makedirs(app.instance_path, exist_ok=True)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
@@ -672,7 +682,11 @@ def change_password():
return render_template('change_password.html', msg=msg)
if __name__ == '__main__':
if not os.path.exists('liturgie.db'):
# Ensure instance folder exists when running without Docker.
os.makedirs(app.instance_path, exist_ok=True)
# Create DB on first run when using the default instance DB path.
if app.config['SQLALCHEMY_DATABASE_URI'].startswith('sqlite:///') and not os.path.exists(default_db_path):
with app.app_context():
db.create_all()
# Create initial admin user if not exists

35
docker-compose.yml Normal file
View File

@@ -0,0 +1,35 @@
services:
web:
build:
context: .
container_name: psalmbordonline-web
ports:
# Host:Container
- "${WEB_PORT:-5000}:5000"
environment:
# Flask
- FLASK_ENV=${FLASK_ENV:-production}
# The app reads these from env (with sensible defaults in app.py)
- SECRET_KEY=${SECRET_KEY:-change-me}
# Use an absolute path inside the container. Note the 4 slashes for sqlite.
- SQLALCHEMY_DATABASE_URI=${SQLALCHEMY_DATABASE_URI:-sqlite:////instance/liturgie.db}
# Default admin bootstrap (only created if not existing)
- ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin}
command:
# Ensure the SQLite DB + tables exist when running under gunicorn (the __main__
# block in app.py does not run under gunicorn).
- sh
- -c
- "python init_db.py && gunicorn wsgi:app -b 0.0.0.0:5000"
volumes:
# Persist SQLite DB (stored under Flask's instance folder)
- ./instance:/instance
# Persist uploaded images/backgrounds
- ./static/uploads:/static/uploads
restart: unless-stopped

42
init_db.py Normal file
View File

@@ -0,0 +1,42 @@
"""Container startup helper.
When the app is started via gunicorn, the `if __name__ == '__main__'` block in
app.py is not executed, so the SQLite DB/tables/admin user may not be created.
This script makes startup idempotent by ensuring tables exist and creating a
default admin user if missing.
"""
import os
from werkzeug.security import generate_password_hash
from app import app, db, User, Church
def main() -> None:
admin_username = os.environ.get("ADMIN_USERNAME", "admin")
admin_password = os.environ.get("ADMIN_PASSWORD", "admin")
with app.app_context():
db.create_all()
if not User.query.filter_by(username=admin_username).first():
admin_church = Church.query.filter_by(name="Admin").first()
if not admin_church:
admin_church = Church(name="Admin")
db.session.add(admin_church)
db.session.commit()
admin_user = User(
username=admin_username,
password=generate_password_hash(admin_password),
church_id=admin_church.id,
is_admin=True,
)
db.session.add(admin_user)
db.session.commit()
if __name__ == "__main__":
main()

87
release.ps1 Normal file
View File

@@ -0,0 +1,87 @@
$ErrorActionPreference = 'Stop'
function Ensure-Success($exitCode, $what) {
if ($exitCode -ne 0) {
throw "Failed: $what (exit code $exitCode)"
}
}
function Get-VersionFromUser {
$version = Read-Host "Enter version (e.g. 1.2.3)"
$version = $version.Trim()
if ([string]::IsNullOrWhiteSpace($version)) {
throw "Version cannot be empty"
}
# Basic docker tag safety: allow only common characters
if ($version -notmatch '^[0-9A-Za-z][0-9A-Za-z._-]{0,127}$') {
throw "Invalid version '$version'. Use only letters/numbers and . _ - (max 128 chars)"
}
return $version
}
$TargetGitUrl = "https://git.alphen.cloud/bramval/PsalmbordOnlineCE.git"
$DockerImage = "git.alphen.cloud/bramval/psalmbordonlinece"
$gitCmd = Get-Command git -ErrorAction SilentlyContinue
if (-not $gitCmd) { throw "git is not available in PATH" }
$dockerCmd = Get-Command docker -ErrorAction SilentlyContinue
if (-not $dockerCmd) { throw "docker is not available in PATH" }
$version = Get-VersionFromUser
$commitMessage = "Version $version"
$branch = (git branch --show-current).Trim()
if ($branch -ne 'main') {
Write-Host "You are on branch '$branch' (expected 'main')." -ForegroundColor Yellow
$ans = Read-Host "Continue and push 'main' anyway? (y/N)"
if ($ans.ToLower() -ne 'y') { throw "Aborted by user" }
}
Write-Host "\n== Git: ensuring remote 'origin' points to $TargetGitUrl ==" -ForegroundColor Cyan
$originUrl = (git remote get-url origin 2>$null)
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($originUrl)) {
Write-Host "Remote 'origin' not found; adding it." -ForegroundColor Yellow
git remote add origin $TargetGitUrl
Ensure-Success $LASTEXITCODE "git remote add"
} elseif ($originUrl -ne $TargetGitUrl) {
Write-Host "Remote 'origin' is '$originUrl' -> updating to '$TargetGitUrl'" -ForegroundColor Yellow
git remote set-url origin $TargetGitUrl
Ensure-Success $LASTEXITCODE "git remote set-url"
}
Write-Host "\n== Git: status ==" -ForegroundColor Cyan
git status -sb
$confirm = Read-Host "Proceed with commit+push and docker push? (y/N)"
if ($confirm.ToLower() -ne 'y') { throw "Aborted by user" }
Write-Host "\n== Git: add/commit/push ==" -ForegroundColor Cyan
git add -A
Ensure-Success $LASTEXITCODE "git add"
# Commit might fail if nothing to commit; handle gracefully
git commit -m $commitMessage
if ($LASTEXITCODE -ne 0) {
Write-Host "No changes to commit (or commit failed). Continuing to push anyway..." -ForegroundColor Yellow
} else {
Write-Host "Committed: $commitMessage" -ForegroundColor Green
}
git push -u origin main
Ensure-Success $LASTEXITCODE "git push"
Write-Host "\n== Docker: build/tag/push ==" -ForegroundColor Cyan
Write-Host "Docker image: $DockerImage" -ForegroundColor Gray
Write-Host "Tags: $version, latest" -ForegroundColor Gray
docker build -t "${DockerImage}:${version}" -t "${DockerImage}:latest" .
Ensure-Success $LASTEXITCODE "docker build"
docker push "${DockerImage}:${version}"
Ensure-Success $LASTEXITCODE "docker push (version)"
docker push "${DockerImage}:latest"
Ensure-Success $LASTEXITCODE "docker push (latest)"
Write-Host "\nDone." -ForegroundColor Green
Write-Host "If docker push failed due to auth, run: docker login git.alphen.cloud" -ForegroundColor Yellow

153
release.py Normal file
View File

@@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""Release helper: git commit/push + docker build/push.
Flow:
1) Prompt for version
2) git add -A
3) git commit -m "Version <version>" (skips if nothing to commit)
4) git push -u origin main (ensures origin points to target repo)
5) docker build + push tags: <version> and latest
"""
from __future__ import annotations
import re
import shutil
import subprocess
from dataclasses import dataclass
@dataclass(frozen=True)
class Config:
target_git_url: str = "https://git.alphen.cloud/bramval/PsalmbordOnlineCE.git"
docker_image: str = "git.alphen.cloud/bramval/psalmbordonlinece"
branch: str = "main"
VERSION_RE = re.compile(r"^[0-9A-Za-z][0-9A-Za-z._-]{0,127}$")
def run(cmd: list[str], *, check: bool = True) -> subprocess.CompletedProcess[str]:
"""Run command and stream output; return CompletedProcess."""
print(f"\n$ {' '.join(cmd)}")
return subprocess.run(cmd, text=True, check=check)
def capture(cmd: list[str]) -> str:
"""Run command and capture stdout."""
return subprocess.check_output(cmd, text=True).strip()
def ensure_tools() -> None:
for tool in ("git", "docker"):
if shutil.which(tool) is None:
raise SystemExit(f"Error: '{tool}' is not available in PATH")
def prompt_version() -> str:
version = input("Enter version (e.g. 1.2.3): ").strip()
if not version:
raise SystemExit("Error: version cannot be empty")
if not VERSION_RE.match(version):
raise SystemExit(
"Error: invalid version. Use only letters/numbers and . _ - (max 128 chars)"
)
return version
def prompt_yes_no(question: str, default_no: bool = True) -> bool:
prompt = "(y/N)" if default_no else "(Y/n)"
ans = input(f"{question} {prompt}: ").strip().lower()
if not ans:
return not default_no
return ans in ("y", "yes")
def ensure_origin(config: Config) -> None:
try:
origin_url = capture(["git", "remote", "get-url", "origin"])
except subprocess.CalledProcessError:
origin_url = ""
if not origin_url:
print(f"Remote 'origin' not found; adding {config.target_git_url}")
run(["git", "remote", "add", "origin", config.target_git_url])
return
if origin_url != config.target_git_url:
print(
f"Remote 'origin' is '{origin_url}' -> updating to '{config.target_git_url}'"
)
run(["git", "remote", "set-url", "origin", config.target_git_url])
def main() -> int:
config = Config()
ensure_tools()
version = prompt_version()
commit_message = f"Version {version}"
# Branch warning
try:
current_branch = capture(["git", "branch", "--show-current"])
except subprocess.CalledProcessError:
current_branch = ""
if current_branch and current_branch != config.branch:
print(
f"Warning: you are on branch '{current_branch}' (expected '{config.branch}')."
)
if not prompt_yes_no(f"Continue and push '{config.branch}' anyway?", default_no=True):
return 1
ensure_origin(config)
print("\n== Git: status ==")
run(["git", "status", "-sb"], check=False)
if not prompt_yes_no("Proceed with commit+push and docker push?", default_no=True):
print("Aborted.")
return 1
print("\n== Git: add/commit/push ==")
run(["git", "add", "-A"])
# commit may fail if nothing to commit
try:
run(["git", "commit", "-m", commit_message])
print(f"Committed: {commit_message}")
except subprocess.CalledProcessError:
print("No changes to commit (or commit failed). Continuing...")
run(["git", "push", "-u", "origin", config.branch])
print("\n== Docker: build/tag/push ==")
print(f"Docker image: {config.docker_image}")
print(f"Tags: {version}, latest")
run(
[
"docker",
"build",
"-t",
f"{config.docker_image}:{version}",
"-t",
f"{config.docker_image}:latest",
".",
]
)
run(["docker", "push", f"{config.docker_image}:{version}"])
run(["docker", "push", f"{config.docker_image}:latest"])
print("\nDone.")
print("If docker push failed due to auth, run: docker login git.alphen.cloud")
return 0
if __name__ == "__main__":
# Make Ctrl+C exit nicely
try:
raise SystemExit(main())
except KeyboardInterrupt:
print("\nAborted (Ctrl+C).")
raise SystemExit(130)

86
release.sh Normal file
View File

@@ -0,0 +1,86 @@
#!/usr/bin/env bash
set -euo pipefail
TARGET_GIT_URL="https://git.alphen.cloud/bramval/PsalmbordOnlineCE.git"
DOCKER_IMAGE="git.alphen.cloud/bramval/psalmbordonlinece"
BRANCH="main"
command -v git >/dev/null 2>&1 || { echo "git is not available in PATH" >&2; exit 1; }
command -v docker >/dev/null 2>&1 || { echo "docker is not available in PATH" >&2; exit 1; }
read -r -p "Enter version (e.g. 1.2.3): " version
version="${version//[[:space:]]/}"
if [[ -z "$version" ]]; then
echo "Version cannot be empty" >&2
exit 1
fi
# Basic docker tag safety
if [[ ! "$version" =~ ^[0-9A-Za-z][0-9A-Za-z._-]{0,127}$ ]]; then
echo "Invalid version '$version'. Use only letters/numbers and . _ - (max 128 chars)" >&2
exit 1
fi
commit_message="Version ${version}"
current_branch="$(git branch --show-current)"
if [[ "$current_branch" != "$BRANCH" ]]; then
echo "You are on branch '$current_branch' (expected '$BRANCH')." >&2
read -r -p "Continue and push '$BRANCH' anyway? (y/N): " ans
if [[ "${ans,,}" != "y" ]]; then
echo "Aborted by user" >&2
exit 1
fi
fi
echo
echo "== Git: ensuring remote 'origin' points to ${TARGET_GIT_URL} =="
if git remote get-url origin >/dev/null 2>&1; then
origin_url="$(git remote get-url origin)"
if [[ "$origin_url" != "$TARGET_GIT_URL" ]]; then
echo "Remote 'origin' is '$origin_url' -> updating to '$TARGET_GIT_URL'"
git remote set-url origin "$TARGET_GIT_URL"
fi
else
echo "Remote 'origin' not found; adding it."
git remote add origin "$TARGET_GIT_URL"
fi
echo
echo "== Git: status =="
git status -sb
read -r -p "Proceed with commit+push and docker push? (y/N): " confirm
if [[ "${confirm,,}" != "y" ]]; then
echo "Aborted by user" >&2
exit 1
fi
echo
echo "== Git: add/commit/push =="
git add -A
set +e
git commit -m "$commit_message"
commit_ec=$?
set -e
if [[ $commit_ec -ne 0 ]]; then
echo "No changes to commit (or commit failed). Continuing to push anyway..." >&2
else
echo "Committed: $commit_message"
fi
git push -u origin "$BRANCH"
echo
echo "== Docker: build/tag/push =="
echo "Docker image: $DOCKER_IMAGE"
echo "Tags: $version, latest"
docker build -t "${DOCKER_IMAGE}:${version}" -t "${DOCKER_IMAGE}:latest" .
docker push "${DOCKER_IMAGE}:${version}"
docker push "${DOCKER_IMAGE}:latest"
echo
echo "Done."
echo "If docker push failed due to auth, run: docker login git.alphen.cloud" >&2

View File

@@ -263,7 +263,7 @@
</div>
</div>
<!-- Contact form at BOTTOM -->
<!-- Contact form at BOTTOM
<div class="contact-section" style="max-width:none; margin:3.8rem 0 0 0; width:100%;">
<h2>Contacteer ons</h2>
<form method="POST" action="/contact" autocomplete="off">
@@ -334,4 +334,5 @@
}
</style>
</div>
-->
{% endblock %}