updated pages

This commit is contained in:
2025-07-16 16:08:40 +03:00
parent 94fad22d85
commit f075cdf871
12 changed files with 1808 additions and 26 deletions

View File

@@ -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']

View File

@@ -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'<ScheduledTask {self.name}>'
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
}

View File

@@ -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

View File

@@ -89,13 +89,18 @@ def create_user():
return redirect(url_for('admin.index'))
@bp.route('/delete_user/<int:user_id>', 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/<int:task_id>', 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/<int:task_id>', 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

View File

@@ -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/<int:content_id>/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"""

File diff suppressed because it is too large Load Diff

View File

@@ -105,13 +105,12 @@
{% if content %}
{% for item in content %}
<div class="content-item" data-duration="{{ item.duration }}" data-id="{{ item.id }}">
{% if item.content_type.startswith('image/') %}
{% if item.content_type == 'image' %}
<img src="{{ url_for('static', filename='uploads/' + item.file_name) }}"
alt="{{ item.original_name or item.file_name }}">
{% elif item.content_type.startswith('video/') %}
{% elif item.content_type == 'video' %}
<video muted autoplay>
<source src="{{ url_for('static', filename='uploads/' + item.file_name) }}"
type="{{ item.content_type }}">
<source src="{{ url_for('static', filename='uploads/' + item.file_name) }}">
</video>
{% endif %}
</div>

View File

@@ -0,0 +1,337 @@
{% extends "base.html" %}
{% block title %}Player Management - SKE Digital Signage{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Page Header -->
<div class="row mb-4">
<div class="col">
<h1><i class="bi bi-display"></i> Player Management</h1>
<p class="text-muted">Manage digital signage players and their configurations</p>
</div>
<div class="col-auto">
<a href="{{ url_for('player.add') }}" class="btn btn-primary">
<i class="bi bi-plus"></i> Add Player
</a>
</div>
</div>
<!-- Quick Stats -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card text-white bg-primary">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h5 class="card-title">Total Players</h5>
<h2>{{ players|length }}</h2>
</div>
<div class="align-self-center">
<i class="bi bi-display" style="font-size: 2rem;"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-success">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h5 class="card-title">Active Players</h5>
<h2>{{ players|selectattr('is_active')|list|length }}</h2>
</div>
<div class="align-self-center">
<i class="bi bi-play-circle" style="font-size: 2rem;"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-warning">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h5 class="card-title">Online Players</h5>
<h2>{{ players|selectattr('last_seen')|list|length }}</h2>
</div>
<div class="align-self-center">
<i class="bi bi-wifi" style="font-size: 2rem;"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-info">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h5 class="card-title">Locked Players</h5>
<h2>{{ players|selectattr('locked_to_group_id')|list|length }}</h2>
</div>
<div class="align-self-center">
<i class="bi bi-lock" style="font-size: 2rem;"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Players Table -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="bi bi-list"></i> Players</h5>
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="refreshPlayers()">
<i class="bi bi-arrow-clockwise"></i> Refresh
</button>
<button type="button" class="btn btn-outline-danger btn-sm" onclick="bulkDelete()" disabled id="bulkDeleteBtn">
<i class="bi bi-trash"></i> Delete Selected
</button>
</div>
</div>
<div class="card-body">
{% if players %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<input type="checkbox" id="selectAll" onchange="toggleSelectAll()">
</th>
<th>Player Name</th>
<th>Hostname</th>
<th>Status</th>
<th>Group</th>
<th>Last Seen</th>
<th>Version</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for player in players %}
<tr>
<td>
<input type="checkbox" class="player-checkbox" value="{{ player.id }}" onchange="updateBulkActions()">
</td>
<td>
<strong>{{ player.username }}</strong>
{% if player.locked_to_group_id %}
<span class="badge bg-warning" title="Locked to group">
<i class="bi bi-lock"></i>
</span>
{% endif %}
</td>
<td>
<code>{{ player.hostname or 'Not set' }}</code>
</td>
<td>
{% if player.is_active %}
{% if player.last_seen and (now() - player.last_seen).total_seconds() < 300 %}
<span class="badge bg-success">Online</span>
{% else %}
<span class="badge bg-warning">Active</span>
{% endif %}
{% else %}
<span class="badge bg-secondary">Inactive</span>
{% endif %}
</td>
<td>
{% if player.locked_to_group_id %}
<span class="badge bg-primary">Group {{ player.locked_to_group_id }}</span>
{% else %}
<span class="text-muted">No group</span>
{% endif %}
</td>
<td>
{% if player.last_seen %}
<span title="{{ player.last_seen }}">
{{ player.last_seen.strftime('%Y-%m-%d %H:%M') }}
</span>
{% else %}
<span class="text-muted">Never</span>
{% endif %}
</td>
<td>
<span class="badge bg-info">v{{ player.playlist_version or 0 }}</span>
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="{{ url_for('player.view', id=player.id) }}" class="btn btn-outline-primary" title="View Player">
<i class="bi bi-eye"></i>
</a>
<a href="{{ url_for('player.edit', id=player.id) }}" class="btn btn-outline-warning" title="Edit Player">
<i class="bi bi-pencil"></i>
</a>
<button type="button" class="btn btn-outline-success" onclick="openPlayerWindow('{{ player.id }}')" title="Open Player View">
<i class="bi bi-window"></i>
</button>
<button type="button" class="btn btn-outline-danger" onclick="deletePlayer('{{ player.id }}', '{{ player.username }}')" title="Delete Player">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-display text-muted" style="font-size: 4rem;"></i>
<h4 class="text-muted mt-3">No Players Found</h4>
<p class="text-muted">Get started by adding your first digital signage player.</p>
<a href="{{ url_for('player.add') }}" class="btn btn-primary">
<i class="bi bi-plus"></i> Add First Player
</a>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Delete Player Modal -->
<div class="modal fade" id="deletePlayerModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete Player</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete player <strong id="deletePlayerName"></strong>?</p>
<p class="text-danger small">This action cannot be undone and will remove all player data.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<form method="POST" action="{{ url_for('player.delete') }}" style="display:inline;">
<input type="hidden" id="deletePlayerId" name="player_id">
<button type="submit" class="btn btn-danger">Delete Player</button>
</form>
</div>
</div>
</div>
</div>
<!-- Bulk Delete Modal -->
<div class="modal fade" id="bulkDeleteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete Selected Players</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete <span id="selectedCount">0</span> selected players?</p>
<p class="text-danger small">This action cannot be undone and will remove all player data.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" onclick="confirmBulkDelete()">Delete Players</button>
</div>
</div>
</div>
</div>
<script>
function deletePlayer(playerId, playerName) {
document.getElementById('deletePlayerId').value = playerId;
document.getElementById('deletePlayerName').textContent = playerName;
const modal = new bootstrap.Modal(document.getElementById('deletePlayerModal'));
modal.show();
}
function toggleSelectAll() {
const selectAll = document.getElementById('selectAll');
const checkboxes = document.querySelectorAll('.player-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = selectAll.checked;
});
updateBulkActions();
}
function updateBulkActions() {
const selectedCheckboxes = document.querySelectorAll('.player-checkbox:checked');
const bulkDeleteBtn = document.getElementById('bulkDeleteBtn');
if (selectedCheckboxes.length > 0) {
bulkDeleteBtn.disabled = false;
bulkDeleteBtn.textContent = `Delete ${selectedCheckboxes.length} Selected`;
} else {
bulkDeleteBtn.disabled = true;
bulkDeleteBtn.innerHTML = '<i class="bi bi-trash"></i> Delete Selected';
}
}
function bulkDelete() {
const selectedCheckboxes = document.querySelectorAll('.player-checkbox:checked');
if (selectedCheckboxes.length === 0) {
return;
}
document.getElementById('selectedCount').textContent = selectedCheckboxes.length;
const modal = new bootstrap.Modal(document.getElementById('bulkDeleteModal'));
modal.show();
}
function confirmBulkDelete() {
const selectedCheckboxes = document.querySelectorAll('.player-checkbox:checked');
const playerIds = Array.from(selectedCheckboxes).map(cb => cb.value);
// Create a form and submit it
const form = document.createElement('form');
form.method = 'POST';
form.action = '{{ url_for("player.bulk_delete") }}';
playerIds.forEach(id => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'player_ids';
input.value = id;
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
}
function refreshPlayers() {
window.location.reload();
}
function openPlayerWindow(playerId) {
const url = `{{ url_for('player.fullscreen', id='PLAYER_ID') }}`.replace('PLAYER_ID', playerId);
window.open(url, '_blank', 'width=1920,height=1080,fullscreen=yes');
}
// Auto-refresh every 30 seconds
setInterval(function() {
const visibilitySupported = typeof document.hidden !== 'undefined';
if (!visibilitySupported || !document.hidden) {
// Only refresh if page is visible
fetch(window.location.href)
.then(response => response.text())
.then(html => {
// Update only the stats if different
const parser = new DOMParser();
const newDoc = parser.parseFromString(html, 'text/html');
const currentStats = document.querySelector('.row.mb-4').innerHTML;
const newStats = newDoc.querySelector('.row.mb-4').innerHTML;
if (currentStats !== newStats) {
document.querySelector('.row.mb-4').innerHTML = newStats;
}
})
.catch(error => console.log('Auto-refresh failed:', error));
}
}, 30000);
</script>
{% endblock %}

View File

@@ -130,9 +130,9 @@
{% endif %}
</td>
<td>
{% if item.content_type.startswith('image/') %}
{% if item.content_type == 'image' %}
<span class="badge bg-success"><i class="bi bi-image"></i> Image</span>
{% elif item.content_type.startswith('video/') %}
{% elif item.content_type == 'video' %}
<span class="badge bg-info"><i class="bi bi-play-circle"></i> Video</span>
{% else %}
<span class="badge bg-secondary">{{ item.content_type }}</span>
@@ -146,6 +146,10 @@
onclick="previewContent('{{ item.id }}', '{{ item.file_name }}', '{{ item.content_type }}')">
<i class="bi bi-eye"></i>
</button>
<button type="button" class="btn btn-outline-warning"
onclick="editContent('{{ item.id }}', '{{ item.file_name }}', {{ item.duration }})">
<i class="bi bi-pencil"></i>
</button>
<button type="button" class="btn btn-outline-danger"
onclick="removeContent('{{ item.id }}', '{{ item.file_name }}')">
<i class="bi bi-trash"></i>
@@ -208,6 +212,30 @@
</div>
</div>
<!-- Edit Content Modal -->
<div class="modal fade" id="editModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Content Duration</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Edit display duration for <strong id="editFilename"></strong></p>
<div class="mb-3">
<label for="editDuration" class="form-label">Duration (seconds)</label>
<input type="number" class="form-control" id="editDuration" min="1" max="300" required>
<div class="form-text">How long should this content be displayed? (1-300 seconds)</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="confirmEditContent()">Update Duration</button>
</div>
</div>
</div>
</div>
<script>
let currentContentId = null;
@@ -217,17 +245,26 @@ function refreshPlayer() {
}
function previewContent(contentId, filename, contentType) {
console.log('Preview function called:', {contentId, filename, contentType});
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
const previewDiv = document.getElementById('previewContent');
if (contentType.startsWith('image/')) {
previewDiv.innerHTML = `<img src="/static/uploads/${filename}" class="img-fluid" alt="${filename}">`;
} else if (contentType.startsWith('video/')) {
if (contentType === 'image') {
const imgSrc = `/static/uploads/${filename}`;
console.log('Loading image from:', imgSrc);
previewDiv.innerHTML = `<img src="${imgSrc}" class="img-fluid" alt="${filename}"
onload="console.log('Image loaded successfully')"
onerror="console.error('Failed to load image:', this.src)">`;
} else if (contentType === 'video') {
const videoSrc = `/static/uploads/${filename}`;
console.log('Loading video from:', videoSrc);
previewDiv.innerHTML = `<video controls class="w-100" style="max-height: 400px;">
<source src="/static/uploads/${filename}" type="${contentType}">
<source src="${videoSrc}">
Your browser does not support the video tag.
</video>`;
} else {
console.log('Unsupported content type:', contentType);
previewDiv.innerHTML = `<p>Preview not available for this content type: ${contentType}</p>`;
}
@@ -241,6 +278,52 @@ function removeContent(contentId, filename) {
modal.show();
}
function editContent(contentId, filename, currentDuration) {
currentContentId = contentId;
document.getElementById('editFilename').textContent = filename;
document.getElementById('editDuration').value = currentDuration;
const modal = new bootstrap.Modal(document.getElementById('editModal'));
modal.show();
}
function confirmEditContent() {
if (currentContentId) {
const newDuration = parseInt(document.getElementById('editDuration').value);
if (!newDuration || newDuration < 1 || newDuration > 300) {
alert('Please enter a valid duration between 1 and 300 seconds.');
return;
}
// Make API call to update content duration
fetch(`/api/content/${currentContentId}/edit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
duration: newDuration
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Error updating content: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('Error updating content');
});
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('editModal'));
modal.hide();
}
}
function confirmRemoveContent() {
if (currentContentId) {
// Make API call to remove content from player

View File

@@ -3,11 +3,15 @@ Logging utilities for server actions
"""
import datetime
import pytz
from flask import request
from flask_login import current_user
from app.extensions import db
from app.models.server_log import ServerLog
# Get local timezone
LOCAL_TZ = pytz.timezone('Europe/Bucharest') # Adjust this to your local timezone
def log_action(action, level='INFO', user_id=None, ip_address=None, user_agent=None):
"""
Log an action to the server log database

5
cookies.txt Normal file
View File

@@ -0,0 +1,5 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
#HttpOnly_localhost FALSE / FALSE 0 session .eJwlzj0OwjAMQOG7ZGZw4jixe5nKfxGsLZ0Qd6cSw1ufvk_Z15Hns2zv48pH2V9RtmKAo6OyUqXUrCBjpNEUCTBpbHVpcmgaGCii3S0koxUzQYJm12bIHqgLPQktrI8ejaP5PSXEMbuzAU83ndFFbWlzl0ia5YZcZx5_TS3fHxMgMKY.aHeCCg.7OIBpOpeNf7DdekBD8uk66K1N30

View File

@@ -8,7 +8,7 @@ import os
import sys
from app import create_app
from app.extensions import db
from app.models import User, ServerLog
from app.models import User, ServerLog, ScheduledTask
from app.utils.logger import log_action
def main():