1183 lines
56 KiB
HTML
1183 lines
56 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Admin Panel - SKE Digital Signage{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.clickable-row:hover {
|
|
background-color: #f8f9fa !important;
|
|
}
|
|
|
|
.expand-icon {
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.expand-icon.rotated {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
.edit-row {
|
|
background-color: #f8f9fa;
|
|
border-left: 4px solid #007bff;
|
|
}
|
|
|
|
.edit-row td {
|
|
padding: 0;
|
|
}
|
|
|
|
.clickable-row {
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid py-4">
|
|
<!-- Page Header -->
|
|
<div class="row mb-4">
|
|
<div class="col">
|
|
<h1><i class="bi bi-gear-fill"></i> Admin Panel
|
|
{% if current_user.is_super_admin %}
|
|
<span class="badge bg-danger ms-2">Super Admin</span>
|
|
{% elif current_user.is_admin %}
|
|
<span class="badge bg-warning ms-2">Admin</span>
|
|
{% endif %}
|
|
</h1>
|
|
<p class="text-muted">System administration and user management</p>
|
|
</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 Users</h5>
|
|
<h2>{{ users|length }}</h2>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="bi bi-people" 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 Users</h5>
|
|
<h2>{{ users|selectattr('is_active_user')|list|length }}</h2>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="bi bi-person-check" 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">Admin Users</h5>
|
|
<h2>{{ users|selectattr('role', 'equalto', 'admin')|list|length }}</h2>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="bi bi-shield-check" 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">Regular Users</h5>
|
|
<h2>{{ users|selectattr('role', 'equalto', 'user')|list|length }}</h2>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="bi bi-person" style="font-size: 2rem;"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Navigation Tabs -->
|
|
<ul class="nav nav-tabs mb-4" id="adminTabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" id="users-tab" data-bs-toggle="tab" data-bs-target="#users" type="button" role="tab">
|
|
<i class="bi bi-people"></i> User Management
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="maintenance-tab" data-bs-toggle="tab" data-bs-target="#maintenance" type="button" role="tab">
|
|
<i class="bi bi-tools"></i> System Maintenance
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="scheduler-tab" data-bs-toggle="tab" data-bs-target="#scheduler" type="button" role="tab">
|
|
<i class="bi bi-clock"></i> Task Scheduler
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<!-- Tab Content -->
|
|
<div class="tab-content" id="adminTabContent">
|
|
<!-- User Management Tab -->
|
|
<div class="tab-pane fade show active" id="users" role="tabpanel">
|
|
<div class="row">
|
|
<!-- User Management -->
|
|
<div class="col-md-8">
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h5><i class="bi bi-people"></i> User Management</h5>
|
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createUserModal">
|
|
<i class="bi bi-plus"></i> Add User
|
|
</button>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if users %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Username</th>
|
|
<th>Role</th>
|
|
<th>Status</th>
|
|
<th>Created</th>
|
|
<th>Last Login</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for user in users %}
|
|
<!-- Main user row (clickable) -->
|
|
<tr class="user-row {% if user.username != current_user.username and user.role != 'sadmin' %}clickable-row{% endif %}" data-user-id="{{ user.id }}" {% if user.username != current_user.username and user.role != 'sadmin' %}style="cursor: pointer;"{% endif %}>
|
|
<td>
|
|
<strong>{{ user.username }}</strong>
|
|
{% if user.username == current_user.username %}
|
|
<span class="badge bg-secondary">You</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if user.role == 'sadmin' %}
|
|
<span class="badge bg-danger">Super Admin</span>
|
|
{% elif user.role == 'admin' %}
|
|
<span class="badge bg-warning">Admin</span>
|
|
{% else %}
|
|
<span class="badge bg-primary">User</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if user.is_active_user %}
|
|
<span class="badge bg-success">Active</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">Inactive</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ user.created_at.strftime('%Y-%m-%d') if user.created_at else 'N/A' }}</td>
|
|
<td>{{ user.last_login.strftime('%Y-%m-%d %H:%M') if user.last_login else 'Never' }}</td>
|
|
<td>
|
|
{% if user.username != current_user.username and user.role != 'sadmin' %}
|
|
<i class="bi bi-chevron-down expand-icon"></i>
|
|
<small class="text-muted">Click to edit</small>
|
|
{% elif user.role == 'sadmin' %}
|
|
<span class="badge bg-danger">Protected</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">Current User</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
|
|
<!-- Expandable edit row (hidden by default) -->
|
|
{% if user.username != current_user.username and user.role != 'sadmin' %}
|
|
<tr class="edit-row" id="edit-row-{{ user.id }}" style="display: none;">
|
|
<td colspan="6" class="bg-light">
|
|
<div class="row p-3">
|
|
<div class="col-md-8">
|
|
<h6><i class="bi bi-pencil"></i> Edit User: {{ user.username }}</h6>
|
|
<form method="POST" action="{{ url_for('admin.edit_user') }}" class="row g-3">
|
|
<input type="hidden" name="user_id" value="{{ user.id }}">
|
|
|
|
<div class="col-md-4">
|
|
<label class="form-label">Username</label>
|
|
<input type="text" class="form-control form-control-sm" name="username" value="{{ user.username }}" required>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<label class="form-label">New Password</label>
|
|
<input type="password" class="form-control form-control-sm" name="password" placeholder="Leave blank to keep current">
|
|
</div>
|
|
|
|
<div class="col-md-2">
|
|
<label class="form-label">Role</label>
|
|
<select name="role" class="form-select form-select-sm">
|
|
<option value="user" {% if user.role == 'user' %}selected{% endif %}>User</option>
|
|
<option value="admin" {% if user.role == 'admin' %}selected{% endif %}>Admin</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-md-2">
|
|
<label class="form-label">Status</label>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" name="is_active" {% if user.is_active_user %}checked{% endif %}>
|
|
<label class="form-check-label">Active</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<button type="submit" class="btn btn-primary btn-sm me-2">
|
|
<i class="bi bi-check-lg"></i> Save Changes
|
|
</button>
|
|
<button type="button" class="btn btn-secondary btn-sm collapse-row" data-user-id="{{ user.id }}">
|
|
<i class="bi bi-x-lg"></i> Cancel
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
{% if current_user.is_super_admin %}
|
|
<h6 class="text-danger"><i class="bi bi-trash"></i> Delete User</h6>
|
|
<p class="small text-muted">This action cannot be undone.</p>
|
|
<form method="POST" action="{{ url_for('admin.delete_user') }}" onsubmit="return confirm('Are you sure you want to delete user {{ user.username }}? This action cannot be undone!');">
|
|
<input type="hidden" name="user_id" value="{{ user.id }}">
|
|
<button type="submit" class="btn btn-danger btn-sm">
|
|
<i class="bi bi-trash"></i> Delete User
|
|
</button>
|
|
</form>
|
|
{% else %}
|
|
<h6 class="text-muted"><i class="bi bi-shield-lock"></i> Delete User</h6>
|
|
<p class="small text-muted">Super admin access required.</p>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" disabled>
|
|
<i class="bi bi-shield-lock"></i> Restricted
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-4">
|
|
<i class="bi bi-people text-muted" style="font-size: 3rem;"></i>
|
|
<h5 class="text-muted mt-2">No Users Found</h5>
|
|
<p class="text-muted">Create your first user to get started.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- System Info -->
|
|
<div class="col-md-4">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5><i class="bi bi-info-circle"></i> System Assets</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="mb-3">
|
|
<h6>Logo Asset</h6>
|
|
{% if logo_exists %}
|
|
<span class="badge bg-success">Available</span>
|
|
<div class="mt-2">
|
|
<img src="{{ url_for('static', filename='assets/logo.png') }}" alt="Logo" class="img-thumbnail" style="max-height: 100px;">
|
|
</div>
|
|
{% else %}
|
|
<span class="badge bg-warning">Missing</span>
|
|
<p class="text-muted small mt-1">Upload logo.png to static/assets/</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<h6>Login Picture</h6>
|
|
{% if login_picture_exists %}
|
|
<span class="badge bg-success">Available</span>
|
|
<div class="mt-2">
|
|
<img src="{{ url_for('static', filename='assets/login_picture.png') }}" alt="Login Picture" class="img-thumbnail" style="max-height: 100px;">
|
|
</div>
|
|
{% else %}
|
|
<span class="badge bg-warning">Missing</span>
|
|
<p class="text-muted small mt-1">Upload login_picture.png to static/assets/</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mt-3">
|
|
<div class="card-header">
|
|
<h5><i class="bi bi-tools"></i> Quick Actions</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="d-grid gap-2">
|
|
<a href="{{ url_for('dashboard.index') }}" class="btn btn-outline-primary">
|
|
<i class="bi bi-speedometer2"></i> Dashboard
|
|
</a>
|
|
<a href="{{ url_for('player.index') }}" class="btn btn-outline-info">
|
|
<i class="bi bi-display"></i> Manage Players
|
|
</a>
|
|
<a href="{{ url_for('content.upload') }}" class="btn btn-outline-success">
|
|
<i class="bi bi-upload"></i> Upload Content
|
|
</a>
|
|
<button type="button" class="btn btn-outline-warning" onclick="clearLogs()">
|
|
<i class="bi bi-trash"></i> Clear Logs
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- System Maintenance Tab -->
|
|
<div class="tab-pane fade" id="maintenance" role="tabpanel">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5><i class="bi bi-trash"></i> File Management</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="mb-4">
|
|
<h6><i class="bi bi-trash"></i> Clean Unused Files</h6>
|
|
<p class="text-muted small">Remove media files from uploads folder that are not referenced by any players. This helps free up storage space by removing orphaned files.</p>
|
|
<div class="d-flex gap-2">
|
|
<button type="button" class="btn btn-warning" onclick="cleanUnusedFiles()">
|
|
<i class="bi bi-play-circle"></i> Run Now
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary" onclick="scheduleCleanupTask()">
|
|
<i class="bi bi-clock"></i> Schedule
|
|
</button>
|
|
</div>
|
|
<div id="cleanupStatus" class="mt-2"></div>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<h6><i class="bi bi-journal-x"></i> Clear Server Logs</h6>
|
|
<p class="text-muted small">Remove all server log entries from the database to free up space.</p>
|
|
<div class="d-flex gap-2">
|
|
<button type="button" class="btn btn-outline-warning" onclick="clearLogs()">
|
|
<i class="bi bi-play-circle"></i> Clear Now
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary" onclick="scheduleLogClearTask()">
|
|
<i class="bi bi-clock"></i> Schedule
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<h6><i class="bi bi-database"></i> Database Maintenance</h6>
|
|
<p class="text-muted small">Optimize database performance and clean up orphaned records.</p>
|
|
<div class="d-flex gap-2">
|
|
<button type="button" class="btn btn-outline-info" onclick="optimizeDatabase()">
|
|
<i class="bi bi-play-circle"></i> Optimize Now
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary" onclick="scheduleDatabaseTask()">
|
|
<i class="bi bi-clock"></i> Schedule
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5><i class="bi bi-info-circle"></i> Maintenance Status</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="maintenanceStatus">
|
|
<div class="alert alert-info">
|
|
<i class="bi bi-info-circle"></i> Ready to perform maintenance tasks.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mt-3">
|
|
<div class="card-header">
|
|
<h5><i class="bi bi-upload"></i> Asset Upload</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="POST" action="{{ url_for('admin.upload_assets') }}" enctype="multipart/form-data">
|
|
<div class="mb-3">
|
|
<label for="logo" class="form-label">Logo (PNG format)</label>
|
|
<input type="file" class="form-control" id="logo" name="logo" accept=".png">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="login_picture" class="form-label">Login Picture (PNG format)</label>
|
|
<input type="file" class="form-control" id="login_picture" name="login_picture" accept=".png">
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="bi bi-upload"></i> Upload Assets
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Task Scheduler Tab -->
|
|
<div class="tab-pane fade" id="scheduler" role="tabpanel">
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h5><i class="bi bi-clock"></i> Scheduled Tasks</h5>
|
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createTaskModal">
|
|
<i class="bi bi-plus"></i> Add Task
|
|
</button>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="scheduledTasks">
|
|
<!-- Tasks will be loaded here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5><i class="bi bi-gear"></i> Quick Setup</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<h6>Daily File Cleanup</h6>
|
|
<p class="text-muted small">Automatically clean unused files daily at 1:00 AM</p>
|
|
<form method="POST" action="{{ url_for('admin.create_scheduled_task') }}">
|
|
<input type="hidden" name="task_type" value="cleanup_files">
|
|
<input type="hidden" name="schedule" value="0 1 * * *">
|
|
<input type="hidden" name="enabled" value="true">
|
|
<button type="submit" class="btn btn-success btn-sm w-100">
|
|
<i class="bi bi-clock"></i> Enable Daily Cleanup
|
|
</button>
|
|
</form>
|
|
|
|
<hr>
|
|
|
|
<h6>Custom Schedule</h6>
|
|
<form method="POST" action="{{ url_for('admin.create_scheduled_task') }}">
|
|
<div class="mb-2">
|
|
<label class="form-label">Task Type</label>
|
|
<select class="form-select form-select-sm" name="task_type" required>
|
|
<option value="cleanup_files">Clean Unused Files</option>
|
|
<option value="clear_logs">Clear Server Logs</option>
|
|
<option value="optimize_db">Optimize Database</option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label">Time</label>
|
|
<input type="time" class="form-control form-control-sm" name="time" value="01:00" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Frequency</label>
|
|
<select class="form-select form-select-sm" name="frequency" required>
|
|
<option value="daily">Daily</option>
|
|
<option value="weekly">Weekly</option>
|
|
<option value="monthly">Monthly</option>
|
|
</select>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary btn-sm w-100">
|
|
<i class="bi bi-plus"></i> Create Task
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create Scheduled Task Modal -->
|
|
<div class="modal fade" id="createTaskModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Create Scheduled Task</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST" action="{{ url_for('admin.create_scheduled_task') }}">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label for="taskName" class="form-label">Task Name *</label>
|
|
<input type="text" class="form-control" id="taskName" name="name" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="taskType" class="form-label">Task Type *</label>
|
|
<select class="form-select" id="taskType" name="task_type" required>
|
|
<option value="cleanup_files">Clean Unused Files</option>
|
|
<option value="clear_logs">Clear Server Logs</option>
|
|
<option value="optimize_db">Optimize Database</option>
|
|
<option value="backup_db">Backup Database</option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="cronExpression" class="form-label">Cron Expression *</label>
|
|
<input type="text" class="form-control" id="cronExpression" name="schedule"
|
|
placeholder="0 1 * * * (daily at 1:00 AM)" required>
|
|
<div class="form-text">
|
|
Examples: <br>
|
|
• <code>0 1 * * *</code> - Daily at 1:00 AM<br>
|
|
• <code>0 0 * * 0</code> - Weekly on Sunday at midnight<br>
|
|
• <code>0 2 1 * *</code> - Monthly on 1st at 2:00 AM
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="taskEnabled" name="enabled" checked>
|
|
<label class="form-check-label" for="taskEnabled">
|
|
Enable Task
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Create Task</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create User Modal -->
|
|
<div class="modal fade" id="createUserModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Create New User</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST" action="{{ url_for('admin.create_user') }}">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label for="username" class="form-label">Username *</label>
|
|
<input type="text" class="form-control" id="username" name="username" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="password" class="form-label">Password *</label>
|
|
<input type="password" class="form-control" id="password" name="password" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="role" class="form-label">Role</label>
|
|
<select class="form-select" id="role" name="role">
|
|
<option value="user">User</option>
|
|
<option value="admin">Admin</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Create User</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Schedule Task Modal -->
|
|
<div class="modal fade" id="quickScheduleModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="quickScheduleTitle">Schedule Maintenance Task</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST" action="{{ url_for('admin.create_scheduled_task') }}" id="quickScheduleForm">
|
|
<input type="hidden" id="quickTaskType" name="task_type">
|
|
<input type="hidden" name="enabled" value="true">
|
|
<div class="modal-body">
|
|
<div class="alert alert-info">
|
|
<i class="bi bi-info-circle"></i> <span id="quickScheduleDescription"></span>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="quickTaskName" class="form-label">Task Name</label>
|
|
<input type="text" class="form-control" id="quickTaskName" name="name" required>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<label for="quickTime" class="form-label">Time</label>
|
|
<input type="time" class="form-control" id="quickTime" name="time" value="01:00" required>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label for="quickFrequency" class="form-label">Frequency</label>
|
|
<select class="form-select" id="quickFrequency" name="frequency" required>
|
|
<option value="daily">Daily</option>
|
|
<option value="weekly">Weekly (Sunday)</option>
|
|
<option value="monthly">Monthly (1st day)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-3">
|
|
<small class="text-muted">
|
|
<strong>Preview:</strong> <span id="schedulePreview">Daily at 01:00</span>
|
|
</small>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="bi bi-clock"></i> Schedule Task
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function clearLogs() {
|
|
if (confirm('Are you sure you want to clear all server logs? This action cannot be undone.')) {
|
|
fetch('/admin/clear_logs', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showMaintenanceStatus('success', 'Server logs cleared successfully');
|
|
} else {
|
|
showMaintenanceStatus('danger', 'Error clearing logs: ' + data.message);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showMaintenanceStatus('danger', 'Error clearing logs');
|
|
});
|
|
}
|
|
}
|
|
|
|
function cleanUnusedFiles() {
|
|
if (confirm('Are you sure you want to clean unused files? This will remove files from uploads folder that are not referenced in the database.')) {
|
|
showCleanupStatus('info', 'Scanning for unused files...');
|
|
showMaintenanceStatus('info', 'Cleaning unused files...');
|
|
|
|
fetch('/admin/clean_unused_files', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
const message = `Successfully cleaned ${data.deleted_count} unused files`;
|
|
showCleanupStatus('success', message);
|
|
showMaintenanceStatus('success', message);
|
|
} else {
|
|
const errorMsg = 'Error cleaning files: ' + data.message;
|
|
showCleanupStatus('danger', errorMsg);
|
|
showMaintenanceStatus('danger', errorMsg);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
const errorMsg = 'Error cleaning files';
|
|
showCleanupStatus('danger', errorMsg);
|
|
showMaintenanceStatus('danger', errorMsg);
|
|
});
|
|
}
|
|
}
|
|
|
|
function showCleanupStatus(type, message) {
|
|
const statusDiv = document.getElementById('cleanupStatus');
|
|
if (statusDiv) {
|
|
const alertClass = type === 'success' ? 'alert-success' :
|
|
type === 'danger' ? 'alert-danger' :
|
|
type === 'warning' ? 'alert-warning' : 'alert-info';
|
|
|
|
const icon = type === 'success' ? 'check-circle' :
|
|
type === 'danger' ? 'exclamation-triangle' :
|
|
type === 'warning' ? 'exclamation-triangle' : 'info-circle';
|
|
|
|
statusDiv.innerHTML = `
|
|
<div class="alert ${alertClass} alert-sm">
|
|
<i class="bi bi-${icon}"></i> ${message}
|
|
</div>
|
|
`;
|
|
|
|
// Auto-hide success messages after 5 seconds
|
|
if (type === 'success') {
|
|
setTimeout(() => {
|
|
statusDiv.innerHTML = '';
|
|
}, 5000);
|
|
}
|
|
}
|
|
}
|
|
|
|
function optimizeDatabase() {
|
|
if (confirm('Are you sure you want to optimize the database? This may take a few moments.')) {
|
|
showMaintenanceStatus('info', 'Optimizing database...');
|
|
|
|
fetch('/admin/optimize_database', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showMaintenanceStatus('success', 'Database optimized successfully');
|
|
} else {
|
|
showMaintenanceStatus('danger', 'Error optimizing database: ' + data.message);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showMaintenanceStatus('danger', 'Error optimizing database');
|
|
});
|
|
}
|
|
}
|
|
|
|
function showMaintenanceStatus(type, message) {
|
|
const statusDiv = document.getElementById('maintenanceStatus');
|
|
const alertClass = type === 'success' ? 'alert-success' :
|
|
type === 'danger' ? 'alert-danger' :
|
|
type === 'warning' ? 'alert-warning' : 'alert-info';
|
|
|
|
const icon = type === 'success' ? 'check-circle' :
|
|
type === 'danger' ? 'exclamation-triangle' :
|
|
type === 'warning' ? 'exclamation-triangle' : 'info-circle';
|
|
|
|
statusDiv.innerHTML = `
|
|
<div class="alert ${alertClass}">
|
|
<i class="bi bi-${icon}"></i> ${message}
|
|
</div>
|
|
`;
|
|
|
|
// Auto-hide success messages after 5 seconds
|
|
if (type === 'success') {
|
|
setTimeout(() => {
|
|
statusDiv.innerHTML = `
|
|
<div class="alert alert-info">
|
|
<i class="bi bi-info-circle"></i> Ready to perform maintenance tasks.
|
|
</div>
|
|
`;
|
|
}, 5000);
|
|
}
|
|
}
|
|
|
|
function loadScheduledTasks() {
|
|
fetch('/admin/scheduled_tasks')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const tasksDiv = document.getElementById('scheduledTasks');
|
|
|
|
if (data.tasks && data.tasks.length > 0) {
|
|
let tasksHtml = '<div class="list-group">';
|
|
|
|
data.tasks.forEach(task => {
|
|
const statusBadge = task.enabled ?
|
|
'<span class="badge bg-success">Enabled</span>' :
|
|
'<span class="badge bg-secondary">Disabled</span>';
|
|
|
|
tasksHtml += `
|
|
<div class="list-group-item d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="mb-1">${task.name}</h6>
|
|
<p class="mb-1 text-muted">${task.task_type} - ${task.schedule}</p>
|
|
<small class="text-muted">Next run: ${task.next_run || 'Not scheduled'}</small>
|
|
</div>
|
|
<div>
|
|
${statusBadge}
|
|
<button class="btn btn-sm btn-outline-warning ms-2" onclick="toggleTask(${task.id})">
|
|
<i class="bi bi-${task.enabled ? 'pause' : 'play'}"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-danger ms-1" onclick="deleteTask(${task.id})">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
tasksHtml += '</div>';
|
|
tasksDiv.innerHTML = tasksHtml;
|
|
} else {
|
|
tasksDiv.innerHTML = `
|
|
<div class="text-center py-4">
|
|
<i class="bi bi-clock text-muted" style="font-size: 3rem;"></i>
|
|
<h5 class="text-muted mt-2">No Scheduled Tasks</h5>
|
|
<p class="text-muted">Create your first scheduled task to automate maintenance.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading tasks:', error);
|
|
document.getElementById('scheduledTasks').innerHTML = `
|
|
<div class="alert alert-danger">
|
|
<i class="bi bi-exclamation-triangle"></i> Error loading scheduled tasks.
|
|
</div>
|
|
`;
|
|
});
|
|
}
|
|
|
|
function toggleTask(taskId) {
|
|
fetch(`/admin/toggle_task/${taskId}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
loadScheduledTasks(); // Reload tasks
|
|
} else {
|
|
alert('Error toggling task: ' + data.message);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('Error toggling task');
|
|
});
|
|
}
|
|
|
|
function deleteTask(taskId) {
|
|
if (confirm('Are you sure you want to delete this scheduled task?')) {
|
|
fetch(`/admin/delete_task/${taskId}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
loadScheduledTasks(); // Reload tasks
|
|
} else {
|
|
alert('Error deleting task: ' + data.message);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('Error deleting task');
|
|
});
|
|
}
|
|
}
|
|
|
|
// Consolidated DOMContentLoaded event
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
console.log('Admin page loaded - initializing...');
|
|
|
|
// Debug: Check how many clickable rows we found
|
|
const clickableRows = document.querySelectorAll('.clickable-row');
|
|
console.log('Found clickable rows:', clickableRows.length);
|
|
|
|
if (clickableRows.length === 0) {
|
|
console.warn('No clickable rows found! Check if users are loaded and rendered.');
|
|
// Check if we're on the right tab
|
|
setTimeout(function() {
|
|
const activeTab = document.querySelector('.nav-link.active');
|
|
console.log('Active tab:', activeTab ? activeTab.textContent : 'none');
|
|
const userTable = document.querySelector('#users table');
|
|
console.log('User table found:', !!userTable);
|
|
if (userTable) {
|
|
const rows = userTable.querySelectorAll('tbody tr');
|
|
console.log('Total table rows found:', rows.length);
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
// Add event listeners for expandable user rows
|
|
clickableRows.forEach((row, index) => {
|
|
console.log(`Setting up click handler for row ${index}:`, row);
|
|
|
|
row.addEventListener('click', function(e) {
|
|
console.log('Row clicked!', e.target);
|
|
|
|
// Don't expand if clicking on a form element
|
|
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') {
|
|
console.log('Clicked on form element, ignoring');
|
|
return;
|
|
}
|
|
|
|
const userId = this.getAttribute('data-user-id');
|
|
console.log('User ID:', userId);
|
|
|
|
const editRow = document.getElementById(`edit-row-${userId}`);
|
|
console.log('Edit row found:', !!editRow);
|
|
|
|
const expandIcon = this.querySelector('.expand-icon');
|
|
console.log('Expand icon found:', !!expandIcon);
|
|
|
|
if (editRow) {
|
|
if (editRow.style.display === 'none' || editRow.style.display === '') {
|
|
// Close all other edit rows first
|
|
document.querySelectorAll('.edit-row').forEach(row => {
|
|
row.style.display = 'none';
|
|
});
|
|
document.querySelectorAll('.expand-icon').forEach(icon => {
|
|
icon.classList.remove('rotated');
|
|
});
|
|
|
|
// Open this edit row
|
|
editRow.style.display = 'table-row';
|
|
if (expandIcon) expandIcon.classList.add('rotated');
|
|
console.log('Opened edit row for user:', userId);
|
|
} else {
|
|
// Close this edit row
|
|
editRow.style.display = 'none';
|
|
if (expandIcon) expandIcon.classList.remove('rotated');
|
|
console.log('Closed edit row for user:', userId);
|
|
}
|
|
} else {
|
|
console.error('Edit row not found for user:', userId);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Add event listeners for collapse buttons
|
|
document.querySelectorAll('.collapse-row').forEach(button => {
|
|
button.addEventListener('click', function() {
|
|
const userId = this.getAttribute('data-user-id');
|
|
const editRow = document.getElementById(`edit-row-${userId}`);
|
|
const userRow = document.querySelector(`.clickable-row[data-user-id="${userId}"]`);
|
|
const expandIcon = userRow ? userRow.querySelector('.expand-icon') : null;
|
|
|
|
if (editRow) {
|
|
editRow.style.display = 'none';
|
|
if (expandIcon) expandIcon.classList.remove('rotated');
|
|
console.log('Collapsed edit row for user:', userId);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Setup click handlers for user rows (also call this when tabs change)
|
|
function setupUserRowClickHandlers() {
|
|
console.log('Setting up user row click handlers...');
|
|
|
|
// Remove any existing handlers to avoid duplicates
|
|
document.querySelectorAll('.clickable-row').forEach(row => {
|
|
row.replaceWith(row.cloneNode(true));
|
|
});
|
|
|
|
// Add fresh handlers
|
|
document.querySelectorAll('.clickable-row').forEach((row, index) => {
|
|
console.log(`Setting up click handler for row ${index}:`, row);
|
|
|
|
row.addEventListener('click', function(e) {
|
|
console.log('Row clicked!', e.target, 'Row:', this);
|
|
|
|
// Don't expand if clicking on a form element
|
|
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') {
|
|
console.log('Clicked on form element, ignoring');
|
|
return;
|
|
}
|
|
|
|
const userId = this.getAttribute('data-user-id');
|
|
console.log('User ID:', userId);
|
|
|
|
const editRow = document.getElementById(`edit-row-${userId}`);
|
|
console.log('Edit row found:', !!editRow);
|
|
|
|
const expandIcon = this.querySelector('.expand-icon');
|
|
console.log('Expand icon found:', !!expandIcon);
|
|
|
|
if (editRow) {
|
|
if (editRow.style.display === 'none' || editRow.style.display === '') {
|
|
// Close all other edit rows first
|
|
document.querySelectorAll('.edit-row').forEach(row => {
|
|
row.style.display = 'none';
|
|
});
|
|
document.querySelectorAll('.expand-icon').forEach(icon => {
|
|
icon.classList.remove('rotated');
|
|
});
|
|
|
|
// Open this edit row
|
|
editRow.style.display = 'table-row';
|
|
if (expandIcon) expandIcon.classList.add('rotated');
|
|
console.log('Opened edit row for user:', userId);
|
|
} else {
|
|
// Close this edit row
|
|
editRow.style.display = 'none';
|
|
if (expandIcon) expandIcon.classList.remove('rotated');
|
|
console.log('Closed edit row for user:', userId);
|
|
}
|
|
} else {
|
|
console.error('Edit row not found for user:', userId);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Call setup initially
|
|
setupUserRowClickHandlers();
|
|
|
|
// Re-setup handlers when users tab is shown
|
|
const usersTab = document.getElementById('users-tab');
|
|
if (usersTab) {
|
|
usersTab.addEventListener('shown.bs.tab', function() {
|
|
console.log('Users tab shown, re-setting up click handlers...');
|
|
setTimeout(setupUserRowClickHandlers, 100); // Small delay to ensure content is rendered
|
|
});
|
|
}
|
|
|
|
// Check for successful user operations and handle refresh
|
|
checkForUserOperationSuccess();
|
|
|
|
// Load scheduled tasks immediately on page load
|
|
loadScheduledTasks();
|
|
|
|
// Load scheduled tasks when scheduler tab is shown
|
|
const schedulerTab = document.getElementById('scheduler-tab');
|
|
if (schedulerTab) {
|
|
schedulerTab.addEventListener('shown.bs.tab', function() {
|
|
console.log('Loading scheduled tasks...');
|
|
loadScheduledTasks();
|
|
});
|
|
}
|
|
|
|
// Add event listeners for the schedule preview
|
|
const timeInput = document.getElementById('quickTime');
|
|
const frequencySelect = document.getElementById('quickFrequency');
|
|
|
|
if (timeInput && frequencySelect) {
|
|
timeInput.addEventListener('change', updateSchedulePreview);
|
|
frequencySelect.addEventListener('change', updateSchedulePreview);
|
|
}
|
|
|
|
// Debug: Check if tab content is visible
|
|
const maintenanceContent = document.getElementById('maintenance');
|
|
const schedulerContent = document.getElementById('scheduler');
|
|
|
|
console.log('Maintenance tab content found:', !!maintenanceContent);
|
|
console.log('Scheduler tab content found:', !!schedulerContent);
|
|
});
|
|
|
|
// Frequency change handler for schedule preview
|
|
document.getElementById('quickFrequency').addEventListener('change', function() {
|
|
const frequency = this.value;
|
|
const timeInput = document.getElementById('quickTime');
|
|
const previewText = document.getElementById('schedulePreview');
|
|
|
|
let defaultTime = '01:00';
|
|
let description = '';
|
|
|
|
switch (frequency) {
|
|
case 'daily':
|
|
description = 'Runs daily at the specified time.';
|
|
break;
|
|
case 'weekly':
|
|
description = 'Runs weekly on Sunday at the specified time.';
|
|
defaultTime = '01:00';
|
|
break;
|
|
case 'monthly':
|
|
description = 'Runs monthly on the 1st day at the specified time.';
|
|
defaultTime = '01:00';
|
|
break;
|
|
}
|
|
|
|
timeInput.value = defaultTime;
|
|
previewText.textContent = description;
|
|
});
|
|
|
|
// Open quick schedule modal
|
|
function openQuickScheduleModal(taskType) {
|
|
const title = taskType === 'cleanup_files' ? 'Quick Schedule: Daily File Cleanup' :
|
|
taskType === 'clear_logs' ? 'Quick Schedule: Weekly Log Clearance' :
|
|
'Quick Schedule: Monthly Database Optimization';
|
|
|
|
const description = taskType === 'cleanup_files' ? 'This task will clean unused files daily at 1:00 AM.' :
|
|
taskType === 'clear_logs' ? 'This task will clear server logs weekly on Sunday at 1:00 AM.' :
|
|
'This task will optimize the database monthly on the 1st day at 1:00 AM.';
|
|
|
|
document.getElementById('quickScheduleTitle').textContent = title;
|
|
document.getElementById('quickScheduleDescription').innerHTML = `<i class="bi bi-info-circle"></i> ${description}`;
|
|
document.getElementById('quickTaskType').value = taskType;
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('quickScheduleModal'));
|
|
modal.show();
|
|
}
|
|
|
|
function scheduleCleanupTask() {
|
|
showQuickScheduleModal('cleanup_files', 'Clean Unused Files', 'Automatically remove unused media files from uploads folder');
|
|
}
|
|
|
|
function scheduleLogClearTask() {
|
|
showQuickScheduleModal('clear_logs', 'Clear Server Logs', 'Automatically clear server log entries to free up database space');
|
|
}
|
|
|
|
function scheduleDatabaseTask() {
|
|
showQuickScheduleModal('optimize_db', 'Optimize Database', 'Automatically optimize database performance and clean up orphaned records');
|
|
}
|
|
|
|
function showQuickScheduleModal(taskType, taskName, description) {
|
|
document.getElementById('quickTaskType').value = taskType;
|
|
document.getElementById('quickTaskName').value = taskName;
|
|
document.getElementById('quickScheduleTitle').textContent = `Schedule ${taskName}`;
|
|
document.getElementById('quickScheduleDescription').textContent = description;
|
|
|
|
// Update preview
|
|
updateSchedulePreview();
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('quickScheduleModal'));
|
|
modal.show();
|
|
}
|
|
|
|
function updateSchedulePreview() {
|
|
const time = document.getElementById('quickTime').value;
|
|
const frequency = document.getElementById('quickFrequency').value;
|
|
const preview = document.getElementById('schedulePreview');
|
|
|
|
let frequencyText = frequency.charAt(0).toUpperCase() + frequency.slice(1);
|
|
if (frequency === 'weekly') {
|
|
frequencyText = 'Weekly (Sunday)';
|
|
} else if (frequency === 'monthly') {
|
|
frequencyText = 'Monthly (1st day)';
|
|
}
|
|
|
|
preview.textContent = `${frequencyText} at ${time}`;
|
|
}
|
|
|
|
// Check for flash messages indicating user operations and refresh if needed
|
|
function checkForUserOperationSuccess() {
|
|
// Check if there are any flash messages indicating successful user operations
|
|
const flashMessages = document.querySelectorAll('.alert');
|
|
let shouldRefresh = false;
|
|
|
|
flashMessages.forEach(function(alert) {
|
|
const text = alert.textContent.toLowerCase();
|
|
if (text.includes('created successfully') ||
|
|
text.includes('updated successfully') ||
|
|
text.includes('deleted successfully')) {
|
|
shouldRefresh = true;
|
|
}
|
|
});
|
|
|
|
// If we just completed a user operation, we might need to refresh to show updated data
|
|
if (shouldRefresh && window.performance && window.performance.navigation.type === 1) {
|
|
// This is a refresh after a form submission, ensure we show the latest data
|
|
setTimeout(function() {
|
|
if (document.querySelector('.nav-link.active').getAttribute('data-bs-target') === '#users') {
|
|
// We're on the users tab, reload to show updated user list
|
|
window.location.reload();
|
|
}
|
|
}, 2000); // Wait 2 seconds to let user read the success message
|
|
}
|
|
}
|
|
|
|
</script>
|
|
{% endblock %}
|