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:
+147
-6
@@ -11,7 +11,6 @@ from app.extensions import db, bcrypt
|
||||
from app.models import User, Player, Content, ServerLog, Playlist, HTTPSConfig
|
||||
from app.utils.logger import log_action
|
||||
from app.utils.caddy_manager import CaddyConfigGenerator
|
||||
from app.utils.nginx_config_reader import get_nginx_status
|
||||
|
||||
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||
|
||||
@@ -871,14 +870,10 @@ def https_config():
|
||||
db.session.commit()
|
||||
log_action('info', f'HTTPS status auto-corrected to enabled (detected from request)')
|
||||
|
||||
# Get Nginx configuration status
|
||||
nginx_status = get_nginx_status()
|
||||
|
||||
return render_template('admin/https_config.html',
|
||||
config=config,
|
||||
is_https_active=is_https_active,
|
||||
current_host=current_host,
|
||||
nginx_status=nginx_status)
|
||||
current_host=current_host)
|
||||
except Exception as e:
|
||||
log_action('error', f'Error loading HTTPS config page: {str(e)}')
|
||||
flash('Error loading HTTPS configuration page.', 'danger')
|
||||
@@ -1015,3 +1010,149 @@ def https_config_status():
|
||||
except Exception as e:
|
||||
log_action('error', f'Error getting HTTPS status: {str(e)}')
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Player Build & Deployment Routes
|
||||
# ============================================================================
|
||||
|
||||
def _player_build_meta_path() -> str:
|
||||
"""Path to the persisted player-build settings (in the instance folder)."""
|
||||
from app.utils.player_build import BUILD_META_FILENAME
|
||||
return os.path.join(current_app.instance_path, BUILD_META_FILENAME)
|
||||
|
||||
|
||||
@admin_bp.route('/build-player', methods=['GET'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def build_player():
|
||||
"""Display the 'Build player files for deployment' admin page."""
|
||||
from app.utils.ssh_deploy import get_local_player_code_status
|
||||
from app.utils.player_build import load_build_settings
|
||||
|
||||
player_code_dir = current_app.config['PLAYER_CODE_DIR']
|
||||
settings = load_build_settings(_player_build_meta_path()) or {}
|
||||
|
||||
# Prefill server address from saved settings, else from HTTPS config.
|
||||
if not settings.get('server_ip'):
|
||||
https_cfg = HTTPSConfig.get_config()
|
||||
if https_cfg and (https_cfg.domain or https_cfg.ip_address):
|
||||
settings.setdefault('server_ip', https_cfg.domain or https_cfg.ip_address)
|
||||
settings.setdefault('port', str(https_cfg.port or 443))
|
||||
settings.setdefault('use_https', bool(https_cfg.https_enabled))
|
||||
|
||||
# Sensible defaults.
|
||||
settings.setdefault('repo_url', current_app.config.get('PLAYER_REPO_URL', ''))
|
||||
settings.setdefault('branch', 'main')
|
||||
default_host = request.host.split(':')[0]
|
||||
if default_host in ('localhost', '127.0.0.1', '') or default_host.startswith('127.'):
|
||||
from app.utils.ssh_deploy import detect_server_ip
|
||||
default_host = detect_server_ip() or default_host
|
||||
settings.setdefault('server_ip', default_host)
|
||||
settings.setdefault('port', '443')
|
||||
settings.setdefault('use_https', True)
|
||||
settings.setdefault('verify_ssl', False)
|
||||
settings.setdefault('orientation', 'Landscape')
|
||||
settings.setdefault('max_resolution', '1920x1080')
|
||||
|
||||
code_status = get_local_player_code_status(player_code_dir)
|
||||
if code_status.get('updated'):
|
||||
code_status['updated_str'] = datetime.fromtimestamp(
|
||||
code_status['updated']).strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
return render_template(
|
||||
'admin/build_player.html',
|
||||
settings=settings,
|
||||
code_status=code_status,
|
||||
player_code_dir=player_code_dir,
|
||||
)
|
||||
|
||||
|
||||
@admin_bp.route('/build-player', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def build_player_action():
|
||||
"""Build/refresh the staged player code and/or write its base config."""
|
||||
from app.utils.player_build import (
|
||||
build_player_files, write_base_config, get_short_head,
|
||||
save_build_settings, make_build_record,
|
||||
)
|
||||
|
||||
player_code_dir = current_app.config['PLAYER_CODE_DIR']
|
||||
action = request.form.get('action', 'build_and_config')
|
||||
|
||||
repo_url = request.form.get('repo_url', '').strip()
|
||||
branch = request.form.get('branch', 'main').strip() or 'main'
|
||||
server_ip = request.form.get('server_ip', '').strip()
|
||||
port = request.form.get('port', '443').strip()
|
||||
use_https = request.form.get('use_https') == 'on'
|
||||
verify_ssl = request.form.get('verify_ssl') == 'on'
|
||||
orientation = request.form.get('orientation', 'Landscape').strip() or 'Landscape'
|
||||
max_resolution = request.form.get('max_resolution', '1920x1080').strip() or '1920x1080'
|
||||
|
||||
# Validation
|
||||
errors = []
|
||||
if action in ('build_files', 'build_and_config') and not repo_url:
|
||||
errors.append('Repository URL is required to build player files.')
|
||||
if action in ('save_config', 'build_and_config') and not server_ip:
|
||||
errors.append('Server IP / domain is required for the player configuration.')
|
||||
try:
|
||||
port_num = int(port)
|
||||
if port_num < 1 or port_num > 65535:
|
||||
errors.append('Port must be between 1 and 65535.')
|
||||
except ValueError:
|
||||
errors.append('Port must be a valid number.')
|
||||
|
||||
if errors:
|
||||
for err in errors:
|
||||
flash(err, 'warning')
|
||||
return redirect(url_for('admin.build_player'))
|
||||
|
||||
messages = []
|
||||
success = True
|
||||
version = None
|
||||
|
||||
# Step 1: build/refresh files from the repository.
|
||||
if action in ('build_files', 'build_and_config'):
|
||||
result = build_player_files(player_code_dir, repo_url, branch)
|
||||
version = result.get('version')
|
||||
messages.append(result['message'])
|
||||
if not result['success']:
|
||||
success = False
|
||||
log_action('error', f'Player build failed by {current_user.username}: {result["message"]}')
|
||||
|
||||
# Step 2: write the base config (only if the previous step didn't fail).
|
||||
if success and action in ('save_config', 'build_and_config'):
|
||||
cfg_result = write_base_config(
|
||||
player_code_dir=player_code_dir,
|
||||
server_ip=server_ip,
|
||||
port=port,
|
||||
use_https=use_https,
|
||||
verify_ssl=verify_ssl,
|
||||
orientation=orientation,
|
||||
max_resolution=max_resolution,
|
||||
)
|
||||
messages.append(cfg_result['message'])
|
||||
if not cfg_result['success']:
|
||||
success = False
|
||||
|
||||
# Persist settings so deployment uses the same server address.
|
||||
if version is None:
|
||||
version = get_short_head(player_code_dir)
|
||||
save_build_settings(
|
||||
_player_build_meta_path(),
|
||||
make_build_record(
|
||||
repo_url=repo_url, branch=branch, server_ip=server_ip, port=port,
|
||||
use_https=use_https, verify_ssl=verify_ssl, orientation=orientation,
|
||||
max_resolution=max_resolution, version=version, built_by=current_user.username,
|
||||
),
|
||||
)
|
||||
|
||||
summary = ' '.join(messages) if messages else 'No action performed.'
|
||||
if success:
|
||||
log_action('info', f'Player files built by {current_user.username} (version {version})')
|
||||
flash(f'✅ {summary}', 'success')
|
||||
else:
|
||||
flash(f'⚠️ {summary}', 'danger')
|
||||
|
||||
return redirect(url_for('admin.build_player'))
|
||||
|
||||
Reference in New Issue
Block a user