feat: add weblink playlists, SSH player deployment, Caddy HTTPS, build player page

- Content model: add url column + is_weblink() for web page content items
- Player model: add deployment tracking fields (status, timestamps, message)
- Content blueprint: add add_weblink and add_weblink_to_playlist routes;
  weblinks auto-deleted when removed from playlist
- Players blueprint: add SSH deploy mode in add_player; weblink-aware playlist
- API blueprint: weblink URL served directly in playlist response;
  add /api/deploy/test-ssh and /api/deploy/player endpoints
- Admin blueprint: add build-player page (clone from Gitea, write base config);
  replace nginx status card with Caddy status on HTTPS config page
- caddy_manager: rewritten to generate proper HTTPS/internal-CA Caddyfiles
  and reload Caddy via admin API (/load) for live config updates
- ssh_deploy, background_tasks, player_build: new utils for SSH deployment
- background_tasks: push Flask app context into background thread so DB
  updates after deployment complete correctly
- ssh_deploy: robust install script detection with passwordless sudo injection
  (uses SSH credentials, cleaned up after install)
- Dockerfile: add git, sshpass, openssh-client, rsync
- docker-compose: switch nginx to Caddy on ports 80/443; add port 5000 for dev
- Templates: add_player deploy mode UI, weblink form in playlist/upload pages,
  build_player admin page, Caddy status on HTTPS config page
- Migrations: add_url_to_content, add_deployment_fields_to_player
- app.py: call db.create_all() on startup for schema bootstrap
- config.py: add PLAYER_CODE_DIR and PLAYER_REPO_URL settings
This commit is contained in:
2026-07-13 16:46:19 +03:00
parent ae3b82862d
commit 2af04e1db3
24 changed files with 2571 additions and 248 deletions
+116 -2
View File
@@ -3,12 +3,126 @@ import os
from typing import Optional
from app.models.https_config import HTTPSConfig
# Shared reverse-proxy snippet used in every Caddy site block
_PROXY_SNIPPET = """\
reverse_proxy digiserver-app:5000 {
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto {scheme}
transport http {
read_timeout 300s
write_timeout 300s
}
}
request_body {
max_size 2GB
}
encode gzip
header {
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
X-XSS-Protection "1; mode=block"
}
log {
output file /var/log/caddy/access.log
}
"""
class CaddyConfigGenerator:
"""Generate Caddyfile configuration based on HTTPSConfig."""
@staticmethod
def generate_caddyfile(config: Optional[HTTPSConfig] = None) -> str:
def generate_caddyfile(config: Optional['HTTPSConfig'] = None) -> str:
"""Generate a complete Caddyfile.
Behaviour:
- HTTPS disabled / no domain → HTTP-only on port 80 (initial deploy mode).
- HTTPS enabled + real domain → Caddy auto-provisions a Let's Encrypt cert
for that domain; HTTP redirects to HTTPS automatically.
- HTTPS enabled + IP only (no domain) → TLS with Caddy's internal CA
(self-signed, trusted within the Docker network).
"""
if config is None:
config = HTTPSConfig.get_config()
email = (config.email or "admin@localhost") if config else "admin@localhost"
https_enabled = config.https_enabled if config else False
domain = (config.domain or "").strip() if config else ""
ip_address = (config.ip_address or "").strip() if config else ""
global_block = f"""{{\n admin 0.0.0.0:2019\n email {email}\n}}\n\n"""
if https_enabled and domain:
# Caddy handles Let's Encrypt + HTTP→HTTPS redirect automatically
# when a plain hostname (no scheme) is used.
caddyfile = global_block
caddyfile += f"{domain} {{\n{_PROXY_SNIPPET}}}\n"
# Also accept requests on the raw IP (HTTP only, no cert needed)
if ip_address:
caddyfile += f"\nhttp://{ip_address} {{\n{_PROXY_SNIPPET}}}\n"
elif https_enabled and ip_address:
# No public domain — use Caddy's internal CA (self-signed)
caddyfile = global_block
caddyfile += f"https://{ip_address} {{\n tls internal\n{_PROXY_SNIPPET}}}\n"
caddyfile += f"\nhttp://{ip_address} {{\n redir https://{ip_address}{{uri}} 301\n}}\n"
else:
# HTTP-only fallback (first deploy, before HTTPS is configured)
caddyfile = "{\n admin 0.0.0.0:2019\n}\n\n"
caddyfile += f":80 {{\n{_PROXY_SNIPPET}}}\n"
return caddyfile
@staticmethod
def write_caddyfile(caddyfile_content: str,
path: str = '/etc/caddy/Caddyfile') -> bool:
"""Write Caddyfile to disk.
The default path is /etc/caddy/Caddyfile — the standard location inside
the caddy:2-alpine container when a volume is mounted there.
"""
try:
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w') as f:
f.write(caddyfile_content)
return True
except Exception as e:
print(f"Error writing Caddyfile: {str(e)}")
return False
@staticmethod
def reload_caddy() -> bool:
"""Push the current Caddyfile to Caddy via its admin API (/load).
Caddy applies the new config live without dropping connections.
"""
try:
import urllib.request
caddyfile_path = '/etc/caddy/Caddyfile'
if not os.path.exists(caddyfile_path):
print(f"Caddyfile not found at {caddyfile_path}")
return False
with open(caddyfile_path, 'rb') as f:
caddyfile_bytes = f.read()
req = urllib.request.Request(
'http://caddy:2019/load',
data=caddyfile_bytes,
headers={'Content-Type': 'text/caddyfile'},
method='POST',
)
response = urllib.request.urlopen(req, timeout=10)
return response.status == 200
except Exception as e:
print(f"Caddy reload error: {str(e)}")
return False
"""Generate complete Caddyfile content.
Args: