From f075cdf8712521a965575b9f47759d2eb3576496 Mon Sep 17 00:00:00 2001 From: ske087 Date: Wed, 16 Jul 2025 16:08:40 +0300 Subject: [PATCH] updated pages --- app/models/__init__.py | 3 +- app/models/scheduled_task.py | 46 ++ app/models/server_log.py | 10 +- app/routes/admin.py | 261 ++++++- app/routes/api.py | 49 ++ app/templates/admin/index.html | 1015 ++++++++++++++++++++++++++ app/templates/player/fullscreen.html | 7 +- app/templates/player/index.html | 337 +++++++++ app/templates/player/view.html | 95 ++- app/utils/logger.py | 4 + cookies.txt | 5 + main.py | 2 +- 12 files changed, 1808 insertions(+), 26 deletions(-) create mode 100644 app/models/scheduled_task.py create mode 100644 app/templates/admin/index.html create mode 100644 app/templates/player/index.html create mode 100644 cookies.txt diff --git a/app/models/__init__.py b/app/models/__init__.py index fb1f615..4aecccc 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -7,5 +7,6 @@ from .player import Player from .content import Content from .group import Group, group_player from .server_log import ServerLog +from .scheduled_task import ScheduledTask -__all__ = ['User', 'Player', 'Content', 'Group', 'ServerLog', 'group_player'] +__all__ = ['User', 'Player', 'Content', 'Group', 'group_player', 'ServerLog', 'ScheduledTask'] diff --git a/app/models/scheduled_task.py b/app/models/scheduled_task.py new file mode 100644 index 0000000..87cf5e6 --- /dev/null +++ b/app/models/scheduled_task.py @@ -0,0 +1,46 @@ +""" +Scheduled Task Model +""" + +from app.extensions import db +from datetime import datetime +import pytz + +# Get local timezone +LOCAL_TZ = pytz.timezone('Europe/Bucharest') # Adjust this to your local timezone + +def get_local_time(): + """Get current local time""" + return datetime.now(LOCAL_TZ).replace(tzinfo=None) + +class ScheduledTask(db.Model): + """Model for scheduled maintenance tasks""" + + __tablename__ = 'scheduled_task' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + task_type = db.Column(db.String(50), nullable=False) # cleanup_files, clear_logs, optimize_db, backup_db + schedule = db.Column(db.String(100), nullable=False) # Cron expression + enabled = db.Column(db.Boolean, default=True, nullable=False) + created_at = db.Column(db.DateTime, default=get_local_time, nullable=False) + last_run = db.Column(db.DateTime, nullable=True) + next_run = db.Column(db.DateTime, nullable=True) + run_count = db.Column(db.Integer, default=0, nullable=False) + + def __repr__(self): + return f'' + + def to_dict(self): + """Convert to dictionary for JSON serialization""" + return { + 'id': self.id, + 'name': self.name, + 'task_type': self.task_type, + 'schedule': self.schedule, + 'enabled': self.enabled, + 'created_at': self.created_at.isoformat() if self.created_at else None, + 'last_run': self.last_run.isoformat() if self.last_run else None, + 'next_run': self.next_run.isoformat() if self.next_run else None, + 'run_count': self.run_count + } diff --git a/app/models/server_log.py b/app/models/server_log.py index 505b078..478788a 100644 --- a/app/models/server_log.py +++ b/app/models/server_log.py @@ -4,6 +4,14 @@ Server log model for audit trail from app.extensions import db import datetime +import pytz + +# Get local timezone +LOCAL_TZ = pytz.timezone('Europe/Bucharest') # Adjust this to your local timezone + +def get_local_time(): + """Get current local time""" + return datetime.datetime.now(LOCAL_TZ).replace(tzinfo=None) class ServerLog(db.Model): """Server log model for tracking system actions""" @@ -13,7 +21,7 @@ class ServerLog(db.Model): user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) ip_address = db.Column(db.String(45), nullable=True) # Support IPv6 user_agent = db.Column(db.Text, nullable=True) - timestamp = db.Column(db.DateTime, default=datetime.datetime.utcnow, index=True) + timestamp = db.Column(db.DateTime, default=get_local_time, index=True) level = db.Column(db.String(20), default='INFO') # INFO, WARNING, ERROR, DEBUG # Relationships diff --git a/app/routes/admin.py b/app/routes/admin.py index ca4a389..068b81e 100644 --- a/app/routes/admin.py +++ b/app/routes/admin.py @@ -89,13 +89,18 @@ def create_user(): return redirect(url_for('admin.index')) -@bp.route('/delete_user/', methods=['POST']) +@bp.route('/delete_user', methods=['POST']) @login_required @admin_required -def delete_user(user_id): - """Delete a user""" +def delete_user(): + """Delete a user using POST form data""" + user_id = request.form.get('user_id') + if not user_id: + flash('User ID is required.', 'danger') + return redirect(url_for('admin.index')) + # Prevent self-deletion - if user_id == current_user.id: + if int(user_id) == current_user.id: flash('You cannot delete your own account.', 'danger') return redirect(url_for('admin.index')) @@ -206,8 +211,8 @@ def upload_assets(): @login_required @admin_required def clean_unused_files(): - """Clean unused files from uploads folder""" - from flask import current_app + """Clean unused files from uploads folder - API endpoint""" + from flask import current_app, jsonify from app.models.content import Content try: @@ -217,6 +222,7 @@ def clean_unused_files(): content_files = {content.file_name for content in Content.query.all()} # Get all files in upload folder + deleted_count = 0 if os.path.exists(upload_folder): all_files = set(os.listdir(upload_folder)) @@ -224,7 +230,6 @@ def clean_unused_files(): unused_files = all_files - content_files # Delete unused files - deleted_count = 0 for file_name in unused_files: file_path = os.path.join(upload_folder, file_name) if os.path.isfile(file_path): @@ -233,13 +238,243 @@ def clean_unused_files(): deleted_count += 1 except Exception as e: print(f"Error deleting {file_path}: {e}") - - flash(f'Cleaned {deleted_count} unused files.', 'success') - log_action(f'Cleaned {deleted_count} unused files') - else: - flash('Upload folder does not exist.', 'info') + + log_action(f'Cleaned {deleted_count} unused files') + return jsonify({'success': True, 'deleted_count': deleted_count}) except Exception as e: - flash(f'Error cleaning files: {str(e)}', 'danger') + return jsonify({'success': False, 'message': str(e)}) + +@bp.route('/optimize_database', methods=['POST']) +@login_required +@admin_required +def optimize_database(): + """Optimize database performance""" + from flask import jsonify + + try: + # SQLite optimization commands + db.session.execute(db.text('VACUUM;')) + db.session.execute(db.text('ANALYZE;')) + db.session.commit() + + log_action('Database optimized') + return jsonify({'success': True, 'message': 'Database optimized successfully'}) + + except Exception as e: + db.session.rollback() + return jsonify({'success': False, 'message': str(e)}) + +@bp.route('/scheduled_tasks', methods=['GET']) +@login_required +@admin_required +def get_scheduled_tasks(): + """Get all scheduled tasks""" + from flask import jsonify + from app.models.scheduled_task import ScheduledTask + + try: + tasks = ScheduledTask.query.order_by(ScheduledTask.created_at.desc()).all() + tasks_data = [] + + for task in tasks: + task_dict = task.to_dict() + # Format next run time + if task.next_run: + task_dict['next_run'] = task.next_run.strftime('%Y-%m-%d %H:%M') + else: + task_dict['next_run'] = calculate_next_run(task.schedule) + + tasks_data.append(task_dict) + + return jsonify({'success': True, 'tasks': tasks_data}) + + except Exception as e: + return jsonify({'success': False, 'message': str(e)}) + +@bp.route('/create_scheduled_task', methods=['POST']) +@login_required +@admin_required +def create_scheduled_task(): + """Create a new scheduled task""" + from app.models.scheduled_task import ScheduledTask + + name = request.form.get('name', '').strip() + task_type = request.form.get('task_type', '').strip() + schedule = request.form.get('schedule', '').strip() + enabled = 'enabled' in request.form or request.form.get('enabled') == 'true' + + # Handle time and frequency form data for quick setup + time_str = request.form.get('time') + frequency = request.form.get('frequency') + + if time_str and frequency: + # Convert time and frequency to cron expression + hour, minute = time_str.split(':') + if frequency == 'daily': + schedule = f"{minute} {hour} * * *" + elif frequency == 'weekly': + schedule = f"{minute} {hour} * * 0" # Sunday + elif frequency == 'monthly': + schedule = f"{minute} {hour} 1 * *" # 1st of month + + # Generate name if not provided + if not name: + name = f"{task_type.replace('_', ' ').title()} - {frequency.title()}" + + # Validation + if not task_type or not schedule: + flash('Task type and schedule are required.', 'danger') + return redirect(url_for('admin.index')) + + if not name: + name = f"{task_type.replace('_', ' ').title()} Task" + + try: + # Create new scheduled task + task = ScheduledTask( + name=name, + task_type=task_type, + schedule=schedule, + enabled=enabled + ) + + # Calculate next run time + task.next_run = calculate_next_run_datetime(schedule) + + db.session.add(task) + db.session.commit() + + log_action(f"Scheduled task '{name}' created") + flash(f'Scheduled task "{name}" created successfully.', 'success') + + except Exception as e: + db.session.rollback() + flash(f'Error creating scheduled task: {str(e)}', 'danger') return redirect(url_for('admin.index')) + +@bp.route('/toggle_task/', methods=['POST']) +@login_required +@admin_required +def toggle_task(task_id): + """Toggle a scheduled task on/off""" + from flask import jsonify + from app.models.scheduled_task import ScheduledTask + + try: + task = ScheduledTask.query.get_or_404(task_id) + task.enabled = not task.enabled + db.session.commit() + + status = 'enabled' if task.enabled else 'disabled' + log_action(f"Scheduled task '{task.name}' {status}") + return jsonify({'success': True, 'enabled': task.enabled}) + + except Exception as e: + db.session.rollback() + return jsonify({'success': False, 'message': str(e)}) + +@bp.route('/delete_task/', methods=['DELETE']) +@login_required +@admin_required +def delete_task(task_id): + """Delete a scheduled task""" + from flask import jsonify + from app.models.scheduled_task import ScheduledTask + + try: + task = ScheduledTask.query.get_or_404(task_id) + task_name = task.name + db.session.delete(task) + db.session.commit() + + log_action(f"Scheduled task '{task_name}' deleted") + return jsonify({'success': True}) + + except Exception as e: + db.session.rollback() + return jsonify({'success': False, 'message': str(e)}) + +@bp.route('/edit_user', methods=['POST']) +@login_required +@admin_required +def edit_user(): + """Edit a user""" + user_id = request.form.get('user_id') + if not user_id: + flash('User ID is required.', 'danger') + return redirect(url_for('admin.index')) + + # Prevent self-editing + if int(user_id) == current_user.id: + flash('You cannot edit your own account.', 'danger') + return redirect(url_for('admin.index')) + + user = User.query.get_or_404(user_id) + + # Get form data + role = request.form.get('role', 'user') + is_active = 'is_active' in request.form + password = request.form.get('password', '').strip() + + if role not in ['user', 'admin']: + flash('Invalid role specified.', 'danger') + return redirect(url_for('admin.index')) + + try: + # Update user + user.role = role + user.is_active_user = is_active + + # Update password if provided + if password: + if len(password) < 6: + flash('Password must be at least 6 characters long.', 'danger') + return redirect(url_for('admin.index')) + user.set_password(password) + + db.session.commit() + + log_action(f"User '{user.username}' updated - Role: {role}, Active: {is_active}") + flash(f'User "{user.username}" updated successfully.', 'success') + + except Exception as e: + db.session.rollback() + flash(f'Error updating user: {str(e)}', 'danger') + + return redirect(url_for('admin.index')) + +def calculate_next_run(cron_expression): + """Calculate next run time from cron expression (simplified)""" + try: + parts = cron_expression.split() + if len(parts) >= 2: + minute, hour = parts[0], parts[1] + return f"Next run: {hour}:{minute.zfill(2)}" + return "Invalid schedule" + except: + return "Invalid schedule" + +def calculate_next_run_datetime(cron_expression): + """Calculate next run datetime from cron expression (basic implementation)""" + from datetime import datetime, timedelta + + try: + parts = cron_expression.split() + if len(parts) >= 2: + minute = int(parts[0]) + hour = int(parts[1]) + + now = datetime.now() + next_run = now.replace(hour=hour, minute=minute, second=0, microsecond=0) + + # If the time has passed today, schedule for tomorrow + if next_run <= now: + next_run += timedelta(days=1) + + return next_run + except: + pass + + return None diff --git a/app/routes/api.py b/app/routes/api.py index abf2abd..7800bf1 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -207,6 +207,55 @@ def get_player_content_status(player_id): except Exception as e: return jsonify({'error': f'Failed to get content status: {str(e)}'}), 500 +@bp.route('/content//edit', methods=['POST']) +def edit_content_duration(content_id): + """Edit content duration""" + from flask_login import current_user + + # Require authentication for this operation + if not current_user.is_authenticated: + return jsonify({'error': 'Authentication required'}), 401 + + data = request.get_json() + if not data or 'duration' not in data: + return jsonify({'error': 'Duration required'}), 400 + + new_duration = data.get('duration') + + # Validate duration + try: + new_duration = int(new_duration) + if new_duration < 1 or new_duration > 300: + return jsonify({'error': 'Duration must be between 1 and 300 seconds'}), 400 + except (ValueError, TypeError): + return jsonify({'error': 'Invalid duration value'}), 400 + + # Find the content item + content = Content.query.get(content_id) + if not content: + return jsonify({'error': 'Content not found'}), 404 + + # Update the content duration + try: + old_duration = content.duration + content.duration = new_duration + + # Update player's playlist version to trigger refresh + player = Player.query.get(content.player_id) + if player: + player.increment_playlist_version() + + db.session.commit() + + return jsonify({ + 'success': True, + 'message': f'Content duration updated from {old_duration}s to {new_duration}s', + 'new_duration': new_duration + }) + except Exception as e: + db.session.rollback() + return jsonify({'error': f'Failed to update content: {str(e)}'}), 500 + @bp.errorhandler(404) def api_not_found(error): """API 404 handler""" diff --git a/app/templates/admin/index.html b/app/templates/admin/index.html new file mode 100644 index 0000000..fa69822 --- /dev/null +++ b/app/templates/admin/index.html @@ -0,0 +1,1015 @@ +{% extends "base.html" %} + +{% block title %}Admin Panel - SKE Digital Signage{% endblock %} + +{% block content %} +
+ +
+
+

Admin Panel

+

System administration and user management

+
+
+ + +
+
+
+
+
+
+
Total Users
+

{{ users|length }}

+
+
+ +
+
+
+
+
+
+
+
+
+
+
Active Users
+

{{ users|selectattr('is_active_user')|list|length }}

+
+
+ +
+
+
+
+
+
+
+
+
+
+
Admin Users
+

{{ users|selectattr('role', 'equalto', 'admin')|list|length }}

+
+
+ +
+
+
+
+
+
+
+
+
+
+
Regular Users
+

{{ users|selectattr('role', 'equalto', 'user')|list|length }}

+
+
+ +
+
+
+
+
+
+ + + + + +
+ +
+
+ +
+
+
+
User Management
+ +
+
+ {% if users %} +
+ + + + + + + + + + + + + {% for user in users %} + + + + + + + + + {% endfor %} + +
UsernameRoleStatusCreatedLast LoginActions
+ {{ user.username }} + {% if user.username == current_user.username %} + You + {% endif %} + + {% if user.role == 'admin' %} + Admin + {% else %} + User + {% endif %} + + {% if user.is_active_user %} + Active + {% else %} + Inactive + {% endif %} + {{ user.created_at.strftime('%Y-%m-%d') if user.created_at else 'N/A' }}{{ user.last_login.strftime('%Y-%m-%d %H:%M') if user.last_login else 'Never' }} +
+ {% if user.username != current_user.username %} + + + {% else %} + + {% endif %} +
+
+
+ {% else %} +
+ +
No Users Found
+

Create your first user to get started.

+
+ {% endif %} +
+
+
+ + +
+
+
+
System Assets
+
+
+
+
Logo Asset
+ {% if logo_exists %} + Available +
+ Logo +
+ {% else %} + Missing +

Upload logo.png to static/assets/

+ {% endif %} +
+ +
+
Login Picture
+ {% if login_picture_exists %} + Available +
+ Login Picture +
+ {% else %} + Missing +

Upload login_picture.png to static/assets/

+ {% endif %} +
+
+
+ +
+
+
Quick Actions
+
+ +
+
+ + +
+
+
+
+
+
File Management
+
+
+
+
Clean Unused Files
+

Remove media files from uploads folder that are not referenced by any players. This helps free up storage space by removing orphaned files.

+
+ + +
+
+
+ +
+
Clear Server Logs
+

Remove all server log entries from the database to free up space.

+
+ + +
+
+ +
+
Database Maintenance
+

Optimize database performance and clean up orphaned records.

+
+ + +
+
+
+
+
+ +
+
+
+
Maintenance Status
+
+
+
+
+ Ready to perform maintenance tasks. +
+
+
+
+ +
+
+
Asset Upload
+
+
+
+
+ + +
+
+ + +
+ +
+
+
+
+
+
+ + +
+
+
+
+
+
Scheduled Tasks
+ +
+
+
+ +
+
+
+
+ +
+
+
+
Quick Setup
+
+
+
Daily File Cleanup
+

Automatically clean unused files daily at 1:00 AM

+
+ + + + +
+ +
+ +
Custom Schedule
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + +{% endblock %} diff --git a/app/templates/player/fullscreen.html b/app/templates/player/fullscreen.html index 5e18db5..746e7c2 100644 --- a/app/templates/player/fullscreen.html +++ b/app/templates/player/fullscreen.html @@ -105,13 +105,12 @@ {% if content %} {% for item in content %}
- {% if item.content_type.startswith('image/') %} + {% if item.content_type == 'image' %} {{ item.original_name or item.file_name }} - {% elif item.content_type.startswith('video/') %} + {% elif item.content_type == 'video' %} {% endif %}
diff --git a/app/templates/player/index.html b/app/templates/player/index.html new file mode 100644 index 0000000..e8eece7 --- /dev/null +++ b/app/templates/player/index.html @@ -0,0 +1,337 @@ +{% extends "base.html" %} + +{% block title %}Player Management - SKE Digital Signage{% endblock %} + +{% block content %} +
+ +
+
+

Player Management

+

Manage digital signage players and their configurations

+
+ +
+ + +
+
+
+
+
+
+
Total Players
+

{{ players|length }}

+
+
+ +
+
+
+
+
+
+
+
+
+
+
Active Players
+

{{ players|selectattr('is_active')|list|length }}

+
+
+ +
+
+
+
+
+
+
+
+
+
+
Online Players
+

{{ players|selectattr('last_seen')|list|length }}

+
+
+ +
+
+
+
+
+
+
+
+
+
+
Locked Players
+

{{ players|selectattr('locked_to_group_id')|list|length }}

+
+
+ +
+
+
+
+
+
+ + +
+
+
Players
+
+ + +
+
+
+ {% if players %} +
+ + + + + + + + + + + + + + + {% for player in players %} + + + + + + + + + + + {% endfor %} + +
+ + Player NameHostnameStatusGroupLast SeenVersionActions
+ + + {{ player.username }} + {% if player.locked_to_group_id %} + + + + {% endif %} + + {{ player.hostname or 'Not set' }} + + {% if player.is_active %} + {% if player.last_seen and (now() - player.last_seen).total_seconds() < 300 %} + Online + {% else %} + Active + {% endif %} + {% else %} + Inactive + {% endif %} + + {% if player.locked_to_group_id %} + Group {{ player.locked_to_group_id }} + {% else %} + No group + {% endif %} + + {% if player.last_seen %} + + {{ player.last_seen.strftime('%Y-%m-%d %H:%M') }} + + {% else %} + Never + {% endif %} + + v{{ player.playlist_version or 0 }} + +
+ + + + + + + + +
+
+
+ {% else %} +
+ +

No Players Found

+

Get started by adding your first digital signage player.

+ + Add First Player + +
+ {% endif %} +
+
+
+ + + + + + + + +{% endblock %} diff --git a/app/templates/player/view.html b/app/templates/player/view.html index 5737355..40c86d0 100644 --- a/app/templates/player/view.html +++ b/app/templates/player/view.html @@ -130,9 +130,9 @@ {% endif %} - {% if item.content_type.startswith('image/') %} + {% if item.content_type == 'image' %} Image - {% elif item.content_type.startswith('video/') %} + {% elif item.content_type == 'video' %} Video {% else %} {{ item.content_type }} @@ -146,6 +146,10 @@ onclick="previewContent('{{ item.id }}', '{{ item.file_name }}', '{{ item.content_type }}')"> +
+ + +