diff --git a/Dockerfile b/Dockerfile index 1c855f0..ceb7710 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,9 @@ COPY requirements.txt . # Install Python dependencies RUN pip install --no-cache-dir -r requirements.txt -# Copy application code +# Copy entire application code into container +# This includes: app/, migrations/, configs, and all scripts +# Code is immutable in the image - only data folders are mounted as volumes COPY . . # Copy and set permissions for entrypoint script diff --git a/QUICK_DEPLOYMENT.md b/QUICK_DEPLOYMENT.md index cfea467..289ac74 100644 --- a/QUICK_DEPLOYMENT.md +++ b/QUICK_DEPLOYMENT.md @@ -18,19 +18,96 @@ SQLite Database --- -## 🚀 Quick Start (One Command) +## 🚀 Complete Deployment Workflow +### **1️⃣ Clone & Setup** ```bash -cd /srv/digiserver-v2 -bash deploy.sh +# Copy the app folder from repository +git clone +cd digiserver-v2 + +# Copy environment file and modify as needed +cp .env.example .env + +# Edit .env with your configuration: +nano .env ``` -This will: -1. ✅ Start Docker containers -2. ✅ Initialize database -3. ✅ Run migrations -4. ✅ Configure HTTPS -5. ✅ Display access information +**Configure in .env:** +```env +SECRET_KEY=your-secret-key-change-this +ADMIN_USERNAME=admin +ADMIN_PASSWORD=your-secure-password +DOMAIN=your-domain.com +EMAIL=admin@your-domain.com +IP_ADDRESS=192.168.0.111 +``` + +--- + +### **2️⃣ Deploy via Script** +```bash +# Run the deployment script +./deploy.sh +``` + +This automatically: +1. ✅ Creates `data/` directories (instance, uploads, nginx-ssl, etc.) +2. ✅ Copies nginx configs from repo root to `data/` +3. ✅ Starts Docker containers +4. ✅ Initializes database +5. ✅ Runs all migrations +6. ✅ Configures HTTPS with SSL certificates +7. ✅ Displays access information + +**Output shows:** +- Access URLs (HTTP/HTTPS) +- Default credentials +- Next steps for configuration + +--- + +### **3️⃣ Network Migration (When Network Changes)** + +When moving the server to a different network with a new IP: + +```bash +# Migrate to the new network IP +./migrate_network.sh 10.55.150.160 + +# Optional: with custom hostname +./migrate_network.sh 10.55.150.160 digiserver-secured +``` + +This automatically: +1. ✅ Regenerates SSL certificates for new IP +2. ✅ Updates database HTTPS configuration +3. ✅ Restarts nginx and app containers +4. ✅ Verifies HTTPS connectivity + +--- + +### **4️⃣ Normal Operations** + +**Restart containers:** +```bash +docker compose restart +``` + +**Stop containers:** +```bash +docker compose down +``` + +**View logs:** +```bash +docker compose logs -f +``` + +**View container status:** +```bash +docker compose ps +``` --- diff --git a/app/app.py b/app/app.py index 1eec851..641b527 100644 --- a/app/app.py +++ b/app/app.py @@ -80,7 +80,6 @@ def register_blueprints(app): from app.blueprints.auth import auth_bp from app.blueprints.admin import admin_bp from app.blueprints.players import players_bp - from app.blueprints.groups import groups_bp from app.blueprints.content import content_bp from app.blueprints.playlist import playlist_bp from app.blueprints.api import api_bp @@ -90,7 +89,6 @@ def register_blueprints(app): app.register_blueprint(auth_bp) app.register_blueprint(admin_bp) app.register_blueprint(players_bp) - app.register_blueprint(groups_bp) app.register_blueprint(content_bp) app.register_blueprint(playlist_bp) app.register_blueprint(api_bp) diff --git a/app/blueprints/admin.py b/app/blueprints/admin.py index 3898fab..e754d5a 100644 --- a/app/blueprints/admin.py +++ b/app/blueprints/admin.py @@ -8,7 +8,7 @@ from datetime import datetime from typing import Optional from app.extensions import db, bcrypt -from app.models import User, Player, Group, Content, ServerLog, Playlist, HTTPSConfig +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 diff --git a/app/blueprints/api.py b/app/blueprints/api.py index 6a9bc3f..8cb77a4 100644 --- a/app/blueprints/api.py +++ b/app/blueprints/api.py @@ -7,7 +7,7 @@ import bcrypt from typing import Optional, Dict, List from app.extensions import db, cache -from app.models import Player, Group, Content, PlayerFeedback, ServerLog +from app.models import Player, Content, PlayerFeedback, ServerLog from app.utils.logger import log_action api_bp = Blueprint('api', __name__, url_prefix='/api') @@ -599,31 +599,33 @@ def system_info(): return jsonify({'error': 'Internal server error'}), 500 -@api_bp.route('/groups', methods=['GET']) -@rate_limit(max_requests=60, window=60) -def list_groups(): - """List all groups with basic information.""" - try: - groups = Group.query.order_by(Group.name).all() - - groups_data = [] - for group in groups: - groups_data.append({ - 'id': group.id, - 'name': group.name, - 'description': group.description, - 'player_count': group.players.count(), - 'content_count': group.contents.count() - }) - - return jsonify({ - 'groups': groups_data, - 'count': len(groups_data) - }) - - except Exception as e: - log_action('error', f'Error listing groups: {str(e)}') - return jsonify({'error': 'Internal server error'}), 500 + +# DEPRECATED: Groups functionality has been archived +# @api_bp.route('/groups', methods=['GET']) +# @rate_limit(max_requests=60, window=60) +# def list_groups(): +# """List all groups with basic information.""" +# try: +# groups = Group.query.order_by(Group.name).all() +# +# groups_data = [] +# for group in groups: +# groups_data.append({ +# 'id': group.id, +# 'name': group.name, +# 'description': group.description, +# 'player_count': group.players.count(), +# 'content_count': group.contents.count() +# }) +# +# return jsonify({ +# 'groups': groups_data, +# 'count': len(groups_data) +# }) +# +# except Exception as e: +# log_action('error', f'Error listing groups: {str(e)}') +# return jsonify({'error': 'Internal server error'}), 500 @api_bp.route('/content', methods=['GET']) diff --git a/app/blueprints/playlist.py b/app/blueprints/playlist.py index f652245..e5abbc2 100644 --- a/app/blueprints/playlist.py +++ b/app/blueprints/playlist.py @@ -16,25 +16,16 @@ playlist_bp = Blueprint('playlist', __name__, url_prefix='/playlist') @playlist_bp.route('/') @login_required def manage_playlist(player_id: int): - """Manage playlist for a specific player.""" + """Legacy route - redirect to new content management area.""" player = Player.query.get_or_404(player_id) - # Get content from player's assigned playlist - playlist_items = [] if player.playlist_id: - playlist = Playlist.query.get(player.playlist_id) - if playlist: - playlist_items = playlist.get_content_ordered() - - # Get available content (all content not in current playlist) - all_content = Content.query.all() - playlist_content_ids = {item.id for item in playlist_items} - available_content = [c for c in all_content if c.id not in playlist_content_ids] - - return render_template('playlist/manage_playlist.html', - player=player, - playlist_content=playlist_items, - available_content=available_content) + # Redirect to the new content management interface + return redirect(url_for('content.manage_playlist_content', playlist_id=player.playlist_id)) + else: + # Player has no playlist assigned + flash('This player has no playlist assigned.', 'warning') + return redirect(url_for('players.manage_player', player_id=player_id)) @playlist_bp.route('//add', methods=['POST']) diff --git a/app/templates/content/manage_playlist_content.html b/app/templates/content/manage_playlist_content.html index 539c012..3d3e3b8 100644 --- a/app/templates/content/manage_playlist_content.html +++ b/app/templates/content/manage_playlist_content.html @@ -97,6 +97,67 @@ user-select: none; } + /* Duration spinner control */ + .duration-spinner { + display: flex; + align-items: center; + gap: 8px; + pointer-events: auto; + } + + .duration-display { + min-width: 60px; + text-align: center; + font-weight: 500; + font-size: 16px; + padding: 6px 12px; + background: #f5f5f5; + border-radius: 4px; + border: 1px solid #ddd; + pointer-events: auto; + } + + .duration-spinner button { + width: 36px; + height: 36px; + padding: 0; + border: 1px solid #ddd; + background: white; + border-radius: 4px; + cursor: pointer; + font-size: 18px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + pointer-events: auto; + } + + .duration-spinner button:hover { + background: #f0f0f0; + border-color: #999; + } + + .duration-spinner button:active { + background: #e0e0e0; + transform: scale(0.95); + } + + .duration-spinner button.btn-increase { + color: #28a745; + } + + .duration-spinner button.btn-decrease { + color: #dc3545; + } + + .audio-toggle { + display: inline-flex; + align-items: center; + cursor: pointer; + user-select: none; + } + .audio-checkbox { display: none; } @@ -154,6 +215,36 @@ body.dark-mode .available-content { color: #e2e8f0; } + + /* Dark mode for duration spinner */ + body.dark-mode .duration-display { + background: #2d3748; + border-color: #4a5568; + color: #e2e8f0; + } + + body.dark-mode .duration-spinner button { + background: #2d3748; + border-color: #4a5568; + color: #e2e8f0; + } + + body.dark-mode .duration-spinner button:hover { + background: #4a5568; + border-color: #718096; + } + + body.dark-mode .duration-spinner button:active { + background: #5a6a78; + } + + body.dark-mode .duration-spinner button.btn-increase { + color: #48bb78; + } + + body.dark-mode .duration-spinner button.btn-decrease { + color: #f56565; + }
@@ -230,7 +321,27 @@ {% elif content.content_type == 'pdf' %}📄 PDF {% else %}📁 Other{% endif %} - {{ content._playlist_duration or content.duration }}s + +
+ +
+ {{ content._playlist_duration or content.duration }}s +
+ +
+ {% if content.content_type == 'video' %}