Initial commit: enterprise digital platform with portal SSO, DigiServer, IT Assets, NetworkView, Server Monitor

This commit is contained in:
ske087
2026-05-10 21:07:50 +03:00
commit 8d9df56b0b
364 changed files with 73655 additions and 0 deletions
@@ -0,0 +1,256 @@
{% extends "base.html" %}
{% block title %}Admin Panel - DigiServer v2{% endblock %}
{% block content %}
<h1>Admin Panel</h1>
<div class="dashboard-grid">
<!-- System Overview Card -->
<div class="card">
<h2>📊 System Overview</h2>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-icon">👥</div>
<div class="stat-content">
<span class="stat-label">Total Users</span>
<span class="stat-value">{{ total_users or 0 }}</span>
</div>
</div>
<div class="stat-item">
<div class="stat-icon">🖥️</div>
<div class="stat-content">
<span class="stat-label">Total Players</span>
<span class="stat-value">{{ total_players or 0 }}</span>
</div>
</div>
<div class="stat-item">
<div class="stat-icon">📋</div>
<div class="stat-content">
<span class="stat-label">Total Playlists</span>
<span class="stat-value">{{ total_playlists or 0 }}</span>
</div>
</div>
<div class="stat-item">
<div class="stat-icon">📁</div>
<div class="stat-content">
<span class="stat-label">Media Files</span>
<span class="stat-value">{{ total_content or 0 }}</span>
</div>
</div>
<div class="stat-item">
<div class="stat-icon">💾</div>
<div class="stat-content">
<span class="stat-label">Storage Used</span>
<span class="stat-value">{{ storage_mb or 0 }} MB</span>
</div>
</div>
</div>
</div>
{% if current_user.is_admin %}
<!-- User Management Card (Admin Only) -->
<div class="card management-card">
<h2>👥 User Management</h2>
<p>Manage application users, roles and permissions</p>
<div class="card-actions">
<a href="{{ url_for('admin.user_management') }}" class="btn btn-primary">
Manage Users
</a>
</div>
</div>
{% endif %}
<!-- Editing Users Card -->
<div class="card management-card">
<h2>✏️ Editing Users</h2>
<p>Manage user codes from players that edit images on-screen</p>
<div class="card-actions">
<a href="{{ url_for('admin.manage_editing_users') }}" class="btn btn-primary">
Manage Editing Users
</a>
</div>
</div>
<!-- Leftover Media Management Card -->
<div class="card management-card">
<h2>🗑️ Manage Leftover Media</h2>
<p>Clean up media files not assigned to any playlist</p>
<div class="card-actions">
<a href="{{ url_for('admin.leftover_media') }}" class="btn btn-warning">
Manage Leftover Files
</a>
</div>
</div>
{% if current_user.is_admin %}
<!-- System Dependencies Card (Admin Only) -->
<div class="card management-card" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
<h2>🔧 System Dependencies</h2>
<p>Check and install required software dependencies</p>
<div class="card-actions">
<a href="{{ url_for('admin.dependencies') }}" class="btn btn-primary">
View Dependencies
</a>
</div>
</div>
<!-- Logo Customization Card (Admin Only) -->
<div class="card management-card" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);">
<h2>🎨 Logo Customization</h2>
<p>Upload custom logos for header and login page</p>
<div class="card-actions">
<a href="{{ url_for('admin.customize_logos') }}" class="btn btn-primary">
Customize Logos
</a>
</div>
</div>
<!-- HTTPS Configuration Card (Admin Only) -->
<div class="card management-card" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<h2>🔒 HTTPS Configuration</h2>
<p>Manage SSL/HTTPS settings, domain, and access points</p>
<div class="card-actions">
<a href="{{ url_for('admin.https_config') }}" class="btn btn-primary">
Configure HTTPS
</a>
</div>
</div>
{% endif %}
<!-- Quick Actions Card -->
<div class="card">
<h2>⚡ Quick Actions</h2>
<div class="quick-actions">
<a href="{{ url_for('players.list') }}" class="btn btn-secondary">🖥️ View Players</a>
<a href="{{ url_for('content.content_list') }}" class="btn btn-secondary">📋 View Playlists</a>
<a href="{{ url_for('content.content_list') }}" class="btn btn-secondary">📁 View Media Library</a>
</div>
</div>
</div>
<style>
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.stats-grid {
display: grid;
gap: 10px;
margin-top: 15px;
}
.stat-item {
display: flex;
align-items: center;
gap: 12px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e2e8f0;
transition: all 0.2s;
}
.stat-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
body.dark-mode .stat-item {
background: #1a202c;
border-color: #4a5568;
}
body.dark-mode .stat-item:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.stat-icon {
font-size: 2rem;
line-height: 1;
}
.stat-content {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.stat-label {
font-weight: 500;
font-size: 0.85rem;
color: #666;
}
body.dark-mode .stat-label {
color: #a0aec0;
}
.stat-value {
font-weight: bold;
font-size: 1.5rem;
color: #2c3e50;
}
body.dark-mode .stat-value {
color: #e2e8f0;
}
.management-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.management-card h2 {
color: white;
}
.management-card p {
color: rgba(255, 255, 255, 0.9);
}
.card-actions {
margin-top: 20px;
}
.quick-actions {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 15px;
}
.btn {
padding: 10px 20px;
text-decoration: none;
border-radius: 4px;
text-align: center;
font-weight: 500;
display: inline-block;
border: none;
cursor: pointer;
}
.btn-primary {
background: white;
color: #667eea;
}
.btn-primary:hover {
background: #f0f0f0;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
</style>
{% endblock %}
@@ -0,0 +1,101 @@
{% extends "base.html" %}
{% block title %}Logo Customization - DigiServer{% endblock %}
{% block content %}
<div class="container" style="max-width: 900px;">
<h1 style="margin-bottom: 25px;">🎨 Logo Customization</h1>
<div class="card" style="margin-bottom: 20px;">
<h2 style="margin-bottom: 20px;">📸 Upload Custom Logos</h2>
<!-- Header Logo -->
<div style="margin-bottom: 30px; padding: 20px; background: #f8f9fa; border-radius: 8px;">
<h3 style="margin-bottom: 15px;">Header Logo (Small)</h3>
<p style="color: #666; margin-bottom: 15px;">
This logo appears in the top header next to "DigiServer" text.<br>
<strong>Recommended:</strong> 150x40 pixels (or similar aspect ratio), transparent background PNG
</p>
<div style="display: flex; align-items: center; gap: 20px; margin-bottom: 15px;">
<div style="flex: 1;">
<img src="{{ url_for('static', filename='uploads/header_logo.png') }}?v={{ version }}"
alt="Current Header Logo"
style="max-height: 50px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 10px; border-radius: 4px;"
onerror="this.src='{{ url_for('static', filename='icons/monitor.svg') }}'; this.style.filter='brightness(0) invert(1)'; this.style.maxWidth='50px';">
<p style="margin-top: 5px; font-size: 0.9rem; color: #888;">Current Header Logo</p>
</div>
</div>
<form method="POST" action="{{ url_for('admin.upload_header_logo') }}" enctype="multipart/form-data">
<div style="margin-bottom: 10px;">
<input type="file" name="header_logo" accept="image/png,image/jpeg,image/svg+xml" required
style="padding: 10px; border: 2px solid #ddd; border-radius: 4px; width: 100%;">
</div>
<button type="submit" class="btn btn-primary">📤 Upload Header Logo</button>
</form>
</div>
<!-- Login Logo -->
<div style="margin-bottom: 30px; padding: 20px; background: #f8f9fa; border-radius: 8px;">
<h3 style="margin-bottom: 15px;">Login Page Logo (Large)</h3>
<p style="color: #666; margin-bottom: 15px;">
This logo appears on the left side of the login page (2/3 of screen).<br>
<strong>Recommended:</strong> 800x600 pixels (or similar), transparent background PNG
</p>
<div style="display: flex; align-items: center; gap: 20px; margin-bottom: 15px;">
<div style="flex: 1;">
<img src="{{ url_for('static', filename='uploads/login_logo.png') }}?v={{ version }}"
alt="Current Login Logo"
style="max-width: 300px; max-height: 200px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 8px;"
onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
<p style="margin-top: 10px; font-size: 0.9rem; color: #888; display: none;">No login logo uploaded yet</p>
</div>
</div>
<form method="POST" action="{{ url_for('admin.upload_login_logo') }}" enctype="multipart/form-data">
<div style="margin-bottom: 10px;">
<input type="file" name="login_logo" accept="image/png,image/jpeg,image/svg+xml" required
style="padding: 10px; border: 2px solid #ddd; border-radius: 4px; width: 100%;">
</div>
<button type="submit" class="btn btn-primary">📤 Upload Login Logo</button>
</form>
</div>
<div style="padding: 15px; background: #e7f3ff; border-radius: 8px; border-left: 4px solid #0066cc;">
<h4 style="margin: 0 0 10px 0;">️ Logo Guidelines</h4>
<ul style="margin: 5px 0; padding-left: 25px; color: #555;">
<li><strong>Header Logo:</strong> Keep it simple and small (max 200px width recommended)</li>
<li><strong>Login Logo:</strong> Can be larger and more detailed (800x600px works great)</li>
<li><strong>Format:</strong> PNG with transparent background recommended, or JPG/SVG</li>
<li><strong>File Size:</strong> Keep under 2MB for optimal performance</li>
<li>Logos are cached - clear browser cache if changes don't appear immediately</li>
</ul>
</div>
</div>
<div style="margin-top: 20px;">
<a href="{{ url_for('admin.admin_panel') }}" class="btn btn-secondary">
← Back to Admin Panel
</a>
</div>
</div>
<style>
body.dark-mode div[style*="background: #f8f9fa"] {
background: #2d3748 !important;
}
body.dark-mode div[style*="background: #e7f3ff"] {
background: #1e3a5f !important;
border-left-color: #64b5f6 !important;
}
body.dark-mode p[style*="color: #666"],
body.dark-mode p[style*="color: #888"],
body.dark-mode ul[style*="color: #555"] {
color: #cbd5e0 !important;
}
</style>
{% endblock %}
@@ -0,0 +1,176 @@
{% extends "base.html" %}
{% block title %}System Dependencies - DigiServer v2{% endblock %}
{% block content %}
<div class="container" style="max-width: 1000px;">
<h1 style="margin-bottom: 25px;">🔧 System Dependencies</h1>
<div class="card" style="margin-bottom: 20px;">
<h2 style="margin-bottom: 20px;">📦 Installed Dependencies</h2>
<!-- LibreOffice -->
<div class="dependency-card" style="background: {% if libreoffice_installed %}#d4edda{% else %}#f8d7da{% endif %}; padding: 20px; border-radius: 8px; margin-bottom: 15px; border-left: 4px solid {% if libreoffice_installed %}#28a745{% else %}#dc3545{% endif %};">
<div style="display: flex; justify-content: space-between; align-items: start;">
<div style="flex: 1;">
<h3 style="margin: 0 0 10px 0; display: flex; align-items: center; gap: 10px;">
{% if libreoffice_installed %}
<span style="font-size: 24px;"></span>
{% else %}
<span style="font-size: 24px;"></span>
{% endif %}
LibreOffice
</h3>
<p style="margin: 5px 0; color: #555;">
<strong>Purpose:</strong> Required for PowerPoint (PPTX/PPT) to image conversion
</p>
<p style="margin: 5px 0; color: #555;">
<strong>Status:</strong> {{ libreoffice_version }}
</p>
{% if not libreoffice_installed %}
<p style="margin: 10px 0 0 0; color: #721c24;">
⚠️ Without LibreOffice, you cannot upload or convert PowerPoint presentations.
</p>
{% endif %}
</div>
{% if not libreoffice_installed %}
<form method="POST" action="{{ url_for('admin.install_libreoffice') }}" style="margin-left: 20px;">
<button type="submit" class="btn btn-success" onclick="return confirm('Install LibreOffice? This may take 2-5 minutes.');">
📥 Install LibreOffice
</button>
</form>
{% endif %}
</div>
</div>
<!-- Poppler Utils -->
<div class="dependency-card" style="background: {% if poppler_installed %}#d4edda{% else %}#fff3cd{% endif %}; padding: 20px; border-radius: 8px; margin-bottom: 15px; border-left: 4px solid {% if poppler_installed %}#28a745{% else %}#ffc107{% endif %};">
<div style="display: flex; justify-content: space-between; align-items: start;">
<div style="flex: 1;">
<h3 style="margin: 0 0 10px 0; display: flex; align-items: center; gap: 10px;">
{% if poppler_installed %}
<span style="font-size: 24px;"></span>
{% else %}
<span style="font-size: 24px;">⚠️</span>
{% endif %}
Poppler Utils
</h3>
<p style="margin: 5px 0; color: #555;">
<strong>Purpose:</strong> Required for PDF to image conversion
</p>
<p style="margin: 5px 0; color: #555;">
<strong>Status:</strong> {{ poppler_version }}
</p>
</div>
</div>
</div>
<!-- FFmpeg -->
<div class="dependency-card" style="background: {% if ffmpeg_installed %}#d4edda{% else %}#fff3cd{% endif %}; padding: 20px; border-radius: 8px; margin-bottom: 15px; border-left: 4px solid {% if ffmpeg_installed %}#28a745{% else %}#ffc107{% endif %};">
<div style="display: flex; justify-content: space-between; align-items: start;">
<div style="flex: 1;">
<h3 style="margin: 0 0 10px 0; display: flex; align-items: center; gap: 10px;">
{% if ffmpeg_installed %}
<span style="font-size: 24px;"></span>
{% else %}
<span style="font-size: 24px;">⚠️</span>
{% endif %}
FFmpeg
</h3>
<p style="margin: 5px 0; color: #555;">
<strong>Purpose:</strong> Required for video processing and validation
</p>
<p style="margin: 5px 0; color: #555;">
<strong>Status:</strong> {{ ffmpeg_version }}
</p>
</div>
</div>
</div>
<!-- Emoji Fonts -->
<div class="dependency-card" style="background: {% if emoji_installed %}#d4edda{% else %}#fff3cd{% endif %}; padding: 20px; border-radius: 8px; margin-bottom: 15px; border-left: 4px solid {% if emoji_installed %}#28a745{% else %}#ffc107{% endif %};">
<div style="display: flex; justify-content: space-between; align-items: start;">
<div style="flex: 1;">
<h3 style="margin: 0 0 10px 0; display: flex; align-items: center; gap: 10px;">
{% if emoji_installed %}
<span style="font-size: 24px;"></span>
{% else %}
<span style="font-size: 24px;">⚠️</span>
{% endif %}
Emoji Fonts
</h3>
<p style="margin: 5px 0; color: #555;">
<strong>Purpose:</strong> Better emoji display in UI (optional, mainly for Raspberry Pi)
</p>
<p style="margin: 5px 0; color: #555;">
<strong>Status:</strong> {{ emoji_version }}
</p>
{% if not emoji_installed %}
<p style="margin: 10px 0 0 0; color: #856404;">
️ Optional: Improves emoji rendering on systems without native emoji support.
</p>
{% endif %}
</div>
{% if not emoji_installed %}
<form method="POST" action="{{ url_for('admin.install_emoji_fonts') }}" style="margin-left: 20px;">
<button type="submit" class="btn btn-warning" onclick="return confirm('Install emoji fonts? This may take 1-2 minutes.');">
📥 Install Emoji Fonts
</button>
</form>
{% endif %}
</div>
</div>
<div style="margin-top: 25px; padding: 15px; background: #e7f3ff; border-radius: 8px; border-left: 4px solid #0066cc;">
<h4 style="margin: 0 0 10px 0;">️ Installation Notes</h4>
<ul style="margin: 5px 0; padding-left: 25px; color: #555;">
<li>LibreOffice can be installed using the button above (requires sudo access)</li>
<li>Emoji fonts improve UI display, especially on Raspberry Pi systems</li>
<li>Installation may take 1-5 minutes depending on your internet connection</li>
<li>After installation, refresh this page to verify the status</li>
<li>Docker containers may require rebuilding to include dependencies</li>
</ul>
</div>
</div>
<div style="margin-top: 20px;">
<a href="{{ url_for('admin.admin_panel') }}" class="btn btn-secondary">
← Back to Admin Panel
</a>
</div>
</div>
<style>
body.dark-mode .dependency-card {
color: #e2e8f0 !important;
}
body.dark-mode .dependency-card p {
color: #cbd5e0 !important;
}
body.dark-mode .dependency-card[style*="#d4edda"] {
background: #1e4620 !important;
border-left-color: #48bb78 !important;
}
body.dark-mode .dependency-card[style*="#f8d7da"] {
background: #5a1e1e !important;
border-left-color: #ef5350 !important;
}
body.dark-mode .dependency-card[style*="#fff3cd"] {
background: #5a4a1e !important;
border-left-color: #ffc107 !important;
}
body.dark-mode div[style*="#e7f3ff"] {
background: #1e3a5f !important;
border-left-color: #64b5f6 !important;
}
body.dark-mode div[style*="#e7f3ff"] ul {
color: #cbd5e0 !important;
}
</style>
{% endblock %}
@@ -0,0 +1,105 @@
{% extends "base.html" %}
{% block title %}Manage Editing Users{% endblock %}
{% block content %}
<div style="margin-bottom: 2rem;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
<div style="display: flex; align-items: center; gap: 1rem;">
<a href="{{ url_for('admin.admin_panel') }}"
class="btn"
style="background: #6c757d; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 6px;">
← Back to Admin
</a>
<h1 style="margin: 0;">👤 Manage Editing Users</h1>
</div>
</div>
<p style="color: #6c757d;">Manage users who edit images on players. User codes are automatically created from player metadata.</p>
</div>
{% if users %}
<div class="card">
<div class="card-header">
<h3 style="margin: 0;">Editing Users ({{ users|length }})</h3>
</div>
<div class="card-body" style="padding: 0;">
<table class="table" style="margin: 0;">
<thead>
<tr>
<th style="width: 30%;">User Code</th>
<th style="width: 30%;">Display Name</th>
<th style="width: 15%;">Edits Count</th>
<th style="width: 15%;">Created</th>
<th style="width: 10%;">Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td style="font-family: monospace; font-weight: 600;">{{ user.user_code }}</td>
<td>
<form method="POST" action="{{ url_for('admin.update_editing_user', user_id=user.id) }}" style="display: flex; gap: 0.5rem; align-items: center;">
<input type="text"
name="user_name"
value="{{ user.user_name or '' }}"
placeholder="Enter display name"
class="form-control"
style="flex: 1;">
<button type="submit" class="btn btn-sm btn-primary">Save</button>
</form>
</td>
<td>
<span class="badge badge-info">{{ user_stats.get(user.user_code, 0) }} edits</span>
</td>
<td>{{ user.created_at | localtime('%Y-%m-%d %H:%M') }}</td>
<td>
<form method="POST"
action="{{ url_for('admin.delete_editing_user', user_id=user.id) }}"
style="display: inline;"
onsubmit="return confirm('Are you sure you want to delete this user? This will not delete their edit history.');">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<div class="card" style="text-align: center; padding: 4rem 2rem;">
<div style="font-size: 4rem; margin-bottom: 1rem; opacity: 0.5;">👤</div>
<h2 style="color: #6c757d; margin-bottom: 1rem;">No Editing Users Yet</h2>
<p style="color: #6c757d; font-size: 1.1rem;">
User codes will appear here automatically when players edit media files.
</p>
</div>
{% endif %}
<style>
body.dark-mode .table {
color: #e2e8f0;
}
body.dark-mode .table thead th {
background: #2d3748;
color: #e2e8f0;
border-color: #4a5568;
}
body.dark-mode .table tbody tr {
border-color: #4a5568;
}
body.dark-mode .table tbody tr:hover {
background: #2d3748;
}
body.dark-mode .form-control {
background: #2d3748;
color: #e2e8f0;
border-color: #4a5568;
}
</style>
{% endblock %}
@@ -0,0 +1,654 @@
{% extends "base.html" %}
{% block title %}HTTPS Configuration - DigiServer v2{% endblock %}
{% block content %}
<div class="container">
<div class="page-header">
<a href="{{ url_for('admin.admin_panel') }}" class="back-link">← Back to Admin Panel</a>
<h1>🔒 HTTPS Configuration</h1>
</div>
<div class="https-config-container">
<!-- Status Display -->
<div class="card status-card">
<h2>Current Status</h2>
<!-- Real-time HTTPS detection -->
<div class="status-detection">
<p class="detection-info">
<strong>🔍 Detected Connection:</strong>
{% if is_https_active %}
<span class="badge badge-success">🔒 HTTPS ({{ request.scheme.upper() }})</span>
{% else %}
<span class="badge badge-warning">🔓 HTTP</span>
{% endif %}
<br>
<small>Current host: <code>{{ current_host }}</code> via {{ request.host }}</small>
</p>
</div>
{% if config and config.https_enabled %}
<div class="status-enabled">
<span class="status-badge">✅ HTTPS ENABLED</span>
<div class="status-details">
<p><strong>Domain:</strong> {{ config.domain }}</p>
<p><strong>Hostname:</strong> {{ config.hostname }}</p>
<p><strong>Email:</strong> {{ config.email }}</p>
<p><strong>IP Address:</strong> {{ config.ip_address }}</p>
<p><strong>Port:</strong> {{ config.port }}</p>
<p><strong>Access URL:</strong> <code>https://{{ config.domain }}</code></p>
<p><strong>Last Updated:</strong> {{ config.updated_at.strftime('%Y-%m-%d %H:%M:%S') }} by {{ config.updated_by }}</p>
</div>
</div>
{% else %}
<div class="status-disabled">
<span class="status-badge-inactive">⚠️ HTTPS DISABLED</span>
{% if is_https_active %}
<p style="color: #156b2e; background: #d1f0e0; padding: 10px; border-radius: 4px; margin: 10px 0;">
<strong>Note:</strong> You are currently accessing this page via HTTPS, but the configuration shows as disabled.
This configuration will be automatically updated. Please refresh the page.
</p>
{% else %}
<p>The application is currently running on HTTP only (port 80)</p>
<p>Enable HTTPS below to secure your application.</p>
{% endif %}
</div>
{% endif %}
</div>
<!-- Configuration Form -->
<div class="card config-card">
<h2>Configure HTTPS Settings</h2>
<p class="info-text">
💡 <strong>Workflow:</strong> First, the app runs on HTTP (port 80). After you configure the HTTPS settings below,
the application will be available over HTTPS (port 443) using the domain and hostname you specify.
</p>
<form method="POST" action="{{ url_for('admin.update_https_config') }}" class="https-form">
<!-- Enable HTTPS Toggle -->
<div class="form-group">
<label class="toggle-label">
<input type="checkbox" name="https_enabled" id="https_enabled"
{% if config and config.https_enabled %}checked{% endif %}
class="toggle-input">
<span class="toggle-slider"></span>
<span class="toggle-text">Enable HTTPS</span>
</label>
<p class="form-hint">Check this box to enable HTTPS/SSL for your application</p>
</div>
<!-- Hostname Field -->
<div class="form-group">
<label for="hostname">Hostname <span class="required">*</span></label>
<input type="text" id="hostname" name="hostname"
value="{{ config.hostname or 'digiserver' }}"
placeholder="e.g., digiserver"
class="form-input"
required>
<p class="form-hint">Short name for your server (e.g., 'digiserver')</p>
</div>
<!-- Domain Field -->
<div class="form-group">
<label for="domain">Full Domain Name <span class="required">*</span></label>
<input type="text" id="domain" name="domain"
value="{{ config.domain or 'digiserver.sibiusb.harting.intra' }}"
placeholder="e.g., digiserver.sibiusb.harting.intra"
class="form-input"
required>
<p class="form-hint">Complete domain name (e.g., digiserver.sibiusb.harting.intra)</p>
</div>
<!-- IP Address Field -->
<div class="form-group">
<label for="ip_address">IP Address <span class="required">*</span></label>
<input type="text" id="ip_address" name="ip_address"
value="{{ config.ip_address or '10.76.152.164' }}"
placeholder="e.g., 10.76.152.164"
class="form-input"
required>
<p class="form-hint">Server's IP address for direct access (e.g., 10.76.152.164)</p>
</div>
<!-- Email Field -->
<div class="form-group">
<label for="email">Email Address <span class="required">*</span></label>
<input type="email" id="email" name="email"
value="{{ config.email or '' }}"
placeholder="e.g., admin@example.com"
class="form-input"
required>
<p class="form-hint">Email address for SSL certificate notifications and Let's Encrypt communications</p>
</div>
<!-- Port Field -->
<div class="form-group">
<label for="port">HTTPS Port</label>
<input type="number" id="port" name="port"
value="{{ config.port or 443 }}"
placeholder="443"
min="1" max="65535"
class="form-input">
<p class="form-hint">Port for HTTPS connections (default: 443)</p>
</div>
<!-- Preview Section -->
<div class="preview-section">
<h3>Access Points After Configuration:</h3>
<ul class="access-points">
<li>
<strong>HTTPS (Recommended):</strong>
<code>https://<span id="preview-domain">digiserver.sibiusb.harting.intra</span></code>
</li>
<li>
<strong>HTTP (Fallback):</strong>
<code>http://<span id="preview-ip">10.76.152.164</span></code>
</li>
</ul>
</div>
<!-- Form Actions -->
<div class="form-actions">
<button type="submit" class="btn btn-primary btn-lg">
💾 Save HTTPS Configuration
</button>
<a href="{{ url_for('admin.admin_panel') }}" class="btn btn-secondary">
Cancel
</a>
</div>
</form>
</div>
<!-- Nginx Status Card -->
<div class="card nginx-status-card">
<h2>🔧 Nginx Reverse Proxy Status</h2>
{% if nginx_status.available %}
<div class="nginx-status-content">
<div class="status-item">
<strong>Status:</strong>
<span class="badge badge-success">✅ Nginx Configured</span>
</div>
<div class="status-item">
<strong>Configuration Path:</strong>
<code>{{ nginx_status.path }}</code>
</div>
{% if nginx_status.ssl_enabled %}
<div class="status-item">
<strong>SSL/TLS:</strong>
<span class="badge badge-success">🔒 Enabled</span>
</div>
{% else %}
<div class="status-item">
<strong>SSL/TLS:</strong>
<span class="badge badge-warning">⚠️ Not Configured</span>
</div>
{% endif %}
{% if nginx_status.http_ports %}
<div class="status-item">
<strong>HTTP Ports:</strong>
<code>{{ nginx_status.http_ports|join(', ') }}</code>
</div>
{% endif %}
{% if nginx_status.https_ports %}
<div class="status-item">
<strong>HTTPS Ports:</strong>
<code>{{ nginx_status.https_ports|join(', ') }}</code>
</div>
{% endif %}
{% if nginx_status.server_names %}
<div class="status-item">
<strong>Server Names:</strong>
{% for name in nginx_status.server_names %}
<code>{{ name }}</code>{% if not loop.last %}<br>{% endif %}
{% endfor %}
</div>
{% endif %}
{% if nginx_status.upstream_servers %}
<div class="status-item">
<strong>Upstream Servers:</strong>
{% for server in nginx_status.upstream_servers %}
<code>{{ server }}</code>{% if not loop.last %}<br>{% endif %}
{% endfor %}
</div>
{% endif %}
{% if nginx_status.ssl_protocols %}
<div class="status-item">
<strong>SSL Protocols:</strong>
<code>{{ nginx_status.ssl_protocols|join(', ') }}</code>
</div>
{% endif %}
{% if nginx_status.client_max_body_size %}
<div class="status-item">
<strong>Max Body Size:</strong>
<code>{{ nginx_status.client_max_body_size }}</code>
</div>
{% endif %}
{% if nginx_status.gzip_enabled %}
<div class="status-item">
<strong>Gzip Compression:</strong>
<span class="badge badge-success">✅ Enabled</span>
</div>
{% endif %}
</div>
{% else %}
<div class="status-disabled">
<p>⚠️ <strong>Nginx configuration not accessible</strong></p>
<p>Error: {{ nginx_status.error|default('Unknown error') }}</p>
<p style="font-size: 12px; color: #666;">Path checked: {{ nginx_status.path }}</p>
</div>
{% endif %}
</div>
<!-- Information Section -->
<div class="card info-card">
<h2>️ Important Information</h2>
<div class="info-sections">
<div class="info-section">
<h3>📝 Before You Start</h3>
<ul>
<li>Ensure your DNS is configured to resolve the domain to your server</li>
<li>Verify the IP address matches your server's actual network interface</li>
<li>Check that ports 80, 443, and 443/UDP are open for traffic</li>
</ul>
</div>
<div class="info-section">
<h3>🔐 HTTPS Setup</h3>
<ul>
<li>SSL certificates are automatically managed by Caddy</li>
<li>Certificates are obtained from Let's Encrypt</li>
<li>Automatic renewal is handled by the system</li>
</ul>
</div>
<div class="info-section">
<h3>✅ After Configuration</h3>
<ul>
<li>Your app will restart with the new settings</li>
<li>Both HTTP and HTTPS access points will be available</li>
<li>HTTP requests will be redirected to HTTPS</li>
<li>Check the status above for current configuration</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<style>
.https-config-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.page-header {
margin-bottom: 30px;
}
.back-link {
display: inline-block;
margin-bottom: 15px;
color: #0066cc;
text-decoration: none;
font-weight: 500;
}
.back-link:hover {
text-decoration: underline;
}
.status-detection {
background: #f0f7ff;
border-left: 4px solid #0066cc;
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
}
.detection-info {
margin: 0;
font-size: 14px;
line-height: 1.6;
}
.badge {
display: inline-block;
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
margin-left: 8px;
}
.badge-success {
background: #d1f0e0;
color: #156b2e;
}
.badge-warning {
background: #fff3cd;
color: #856404;
}
.status-card {
margin-bottom: 30px;
border-left: 5px solid #ddd;
}
.status-enabled {
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
border-radius: 8px;
padding: 20px;
border-left: 5px solid #28a745;
}
.status-disabled {
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
border-radius: 8px;
padding: 20px;
border-left: 5px solid #ffc107;
}
.status-badge {
display: inline-block;
background: #28a745;
color: white;
padding: 8px 16px;
border-radius: 20px;
font-weight: bold;
margin-bottom: 15px;
font-size: 14px;
}
.status-badge-inactive {
display: inline-block;
background: #ffc107;
color: #333;
padding: 8px 16px;
border-radius: 20px;
font-weight: bold;
margin-bottom: 15px;
font-size: 14px;
}
.status-details {
margin-top: 15px;
}
.status-details p {
margin: 8px 0;
font-size: 14px;
}
.status-details code {
background: rgba(0,0,0,0.1);
padding: 4px 8px;
border-radius: 4px;
font-family: 'Courier New', monospace;
}
.config-card {
margin-bottom: 30px;
}
.info-text {
background: #e7f3ff;
border-left: 4px solid #0066cc;
padding: 12px 16px;
border-radius: 4px;
margin-bottom: 25px;
font-size: 14px;
}
.https-form {
padding: 20px 0;
}
.form-group {
margin-bottom: 25px;
}
.form-group label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #333;
}
.required {
color: #dc3545;
}
.form-input {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
font-family: inherit;
transition: all 0.2s;
}
.form-input:focus {
outline: none;
border-color: #0066cc;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
}
.form-hint {
font-size: 13px;
color: #666;
margin-top: 6px;
}
/* Toggle Switch Styling */
.toggle-label {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
user-select: none;
}
.toggle-input {
display: none;
}
.toggle-slider {
display: inline-block;
width: 50px;
height: 28px;
background: #ccc;
border-radius: 14px;
position: relative;
transition: all 0.3s;
}
.toggle-slider::after {
content: '';
position: absolute;
width: 24px;
height: 24px;
background: white;
border-radius: 50%;
top: 2px;
left: 2px;
transition: all 0.3s;
}
.toggle-input:checked + .toggle-slider {
background: #28a745;
}
.toggle-input:checked + .toggle-slider::after {
left: 24px;
}
.toggle-text {
font-weight: 600;
color: #333;
}
.preview-section {
background: #f8f9fa;
border: 2px dashed #0066cc;
border-radius: 8px;
padding: 20px;
margin: 25px 0;
}
.preview-section h3 {
margin-top: 0;
color: #0066cc;
}
.access-points {
list-style: none;
padding: 0;
margin: 0;
}
.access-points li {
padding: 10px;
background: white;
border-radius: 4px;
margin-bottom: 8px;
border-left: 4px solid #0066cc;
}
.access-points code {
background: #e7f3ff;
padding: 6px 10px;
border-radius: 4px;
font-family: 'Courier New', monospace;
color: #0066cc;
}
.form-actions {
display: flex;
gap: 10px;
margin-top: 30px;
padding-top: 25px;
border-top: 1px solid #ddd;
}
.btn-lg {
padding: 12px 30px;
font-size: 16px;
font-weight: 600;
}
.info-card {
background: linear-gradient(135deg, #e7f3ff 0%, #f0f7ff 100%);
}
.info-card h2 {
color: #0066cc;
}
.nginx-status-card {
background: linear-gradient(135deg, #f0f7ff 0%, #e7f3ff 100%);
border-left: 5px solid #0066cc;
margin-bottom: 30px;
}
.nginx-status-card h2 {
color: #0066cc;
margin-top: 0;
}
.nginx-status-content {
padding: 10px 0;
}
.status-item {
padding: 12px;
background: white;
border-radius: 4px;
margin-bottom: 10px;
border-left: 3px solid #0066cc;
font-size: 14px;
}
.status-item strong {
display: inline-block;
min-width: 150px;
color: #333;
}
.status-item code {
background: #f0f7ff;
padding: 4px 8px;
border-radius: 3px;
font-family: 'Courier New', monospace;
color: #0066cc;
word-break: break-all;
}
.info-sections {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-top: 20px;
}
.info-section h3 {
color: #0066cc;
margin-top: 0;
font-size: 16px;
}
.info-section ul {
padding-left: 20px;
margin: 0;
}
.info-section li {
margin-bottom: 8px;
font-size: 14px;
color: #555;
}
@media (max-width: 768px) {
.https-config-container {
padding: 10px;
}
.info-sections {
grid-template-columns: 1fr;
}
.form-actions {
flex-direction: column;
}
.btn-lg {
width: 100%;
}
}
</style>
<script>
// Update preview in real-time
document.getElementById('domain').addEventListener('input', function() {
document.getElementById('preview-domain').textContent = this.value || 'digiserver.sibiusb.harting.intra';
});
document.getElementById('ip_address').addEventListener('input', function() {
document.getElementById('preview-ip').textContent = this.value || '10.76.152.164';
});
// Load initial preview
document.getElementById('preview-domain').textContent = document.getElementById('domain').value || 'digiserver.sibiusb.harting.intra';
document.getElementById('preview-ip').textContent = document.getElementById('ip_address').value || '10.76.152.164';
</script>
{% endblock %}
@@ -0,0 +1,215 @@
{% extends "base.html" %}
{% block title %}Leftover Media - Admin - DigiServer v2{% endblock %}
{% block content %}
<div style="max-width: 1400px; margin: 0 auto;">
<div style="margin-bottom: 30px; display: flex; justify-content: space-between; align-items: center;">
<h1 style="display: flex; align-items: center; gap: 0.5rem;">
🗑️ Manage Leftover Media
</h1>
<a href="{{ url_for('admin.admin_panel') }}" class="btn btn-secondary">
← Back to Admin
</a>
</div>
<!-- Overview Stats -->
<div class="card" style="margin-bottom: 30px;">
<h2>📊 Overview</h2>
<p style="color: #6c757d; margin-bottom: 20px;">
Media files that are not assigned to any playlist. These can be safely deleted to free up storage.
</p>
<div class="stats-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
<div class="stat-item" style="background: #f8f9fa; padding: 15px; border-radius: 8px;">
<div style="font-size: 12px; color: #6c757d; margin-bottom: 5px;">Total Leftover Files</div>
<div style="font-size: 24px; font-weight: bold;">{{ total_leftover }}</div>
</div>
<div class="stat-item" style="background: #f8f9fa; padding: 15px; border-radius: 8px;">
<div style="font-size: 12px; color: #6c757d; margin-bottom: 5px;">Total Size</div>
<div style="font-size: 24px; font-weight: bold;">{{ "%.2f"|format(total_leftover_size_mb) }} MB</div>
</div>
<div class="stat-item" style="background: #f8f9fa; padding: 15px; border-radius: 8px;">
<div style="font-size: 12px; color: #6c757d; margin-bottom: 5px;">Images</div>
<div style="font-size: 24px; font-weight: bold;">{{ leftover_images|length }} ({{ "%.2f"|format(images_size_mb) }} MB)</div>
</div>
<div class="stat-item" style="background: #f8f9fa; padding: 15px; border-radius: 8px;">
<div style="font-size: 12px; color: #6c757d; margin-bottom: 5px;">Videos</div>
<div style="font-size: 24px; font-weight: bold;">{{ leftover_videos|length }} ({{ "%.2f"|format(videos_size_mb) }} MB)</div>
</div>
</div>
</div>
<!-- Images Section -->
<div class="card" style="margin-bottom: 30px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2>📷 Leftover Images ({{ leftover_images|length }})</h2>
{% if leftover_images %}
<form method="POST" action="{{ url_for('admin.delete_leftover_images') }}"
onsubmit="return confirm('Are you sure you want to delete ALL {{ leftover_images|length }} leftover images? This cannot be undone!');"
style="display: inline;">
<button type="submit" class="btn btn-danger">
🗑️ Delete All Images
</button>
</form>
{% endif %}
</div>
{% if leftover_images %}
<div style="max-height: 400px; overflow-y: auto;">
<table class="table" style="width: 100%; border-collapse: collapse;">
<thead style="position: sticky; top: 0; background: #f8f9fa;">
<tr>
<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Filename</th>
<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Size</th>
<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Duration</th>
<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Uploaded</th>
<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6; width: 80px;">Action</th>
</tr>
</thead>
<tbody>
{% for img in leftover_images %}
<tr style="border-bottom: 1px solid #dee2e6;">
<td style="padding: 10px;">📷 {{ img.filename }}</td>
<td style="padding: 10px;">{{ "%.2f"|format(img.file_size_mb) }} MB</td>
<td style="padding: 10px;">{{ img.duration }}s</td>
<td style="padding: 10px;">{{ img.uploaded_at | localtime if img.uploaded_at else 'N/A' }}</td>
<td style="padding: 10px;">
<form method="POST" action="{{ url_for('admin.delete_single_leftover', content_id=img.id) }}" style="display: inline;" onsubmit="return confirm('Delete this image?');">
<button type="submit" class="btn btn-danger btn-sm" style="padding: 4px 8px; font-size: 12px;">🗑️</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div style="text-align: center; padding: 40px; color: #999;">
<div style="font-size: 48px; margin-bottom: 15px;"></div>
<p>No leftover images found. All images are assigned to playlists!</p>
</div>
{% endif %}
</div>
<!-- Videos Section -->
<div class="card" style="margin-bottom: 30px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h2 style="margin: 0;">🎥 Leftover Videos ({{ leftover_videos|length }})</h2>
{% if leftover_videos %}
<form method="POST" action="{{ url_for('admin.delete_leftover_videos') }}" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete ALL {{ leftover_videos|length }} leftover videos? This action cannot be undone!');">
<button type="submit" class="btn btn-danger" style="padding: 8px 16px; font-size: 14px;">
🗑️ Delete All Videos
</button>
</form>
{% endif %}
</div>
{% if leftover_videos %}
<div style="max-height: 400px; overflow-y: auto; margin-top: 20px;">
<table class="table" style="width: 100%; border-collapse: collapse;">
<thead style="position: sticky; top: 0; background: #f8f9fa;">
<tr>
<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Filename</th>
<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Size</th>
<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Duration</th>
<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Uploaded</th>
<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6; width: 80px;">Action</th>
</tr>
</thead>
<tbody>
{% for video in leftover_videos %}
<tr style="border-bottom: 1px solid #dee2e6;">
<td style="padding: 10px;">🎥 {{ video.filename }}</td>
<td style="padding: 10px;">{{ "%.2f"|format(video.file_size_mb) }} MB</td>
<td style="padding: 10px;">{{ video.duration }}s</td>
<td style="padding: 10px;">{{ video.uploaded_at | localtime if video.uploaded_at else 'N/A' }}</td>
<td style="padding: 10px;">
<form method="POST" action="{{ url_for('admin.delete_single_leftover', content_id=video.id) }}" style="display: inline;" onsubmit="return confirm('Delete this video?');">
<button type="submit" class="btn btn-danger btn-sm" style="padding: 4px 8px; font-size: 12px;">🗑️</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div style="text-align: center; padding: 40px; color: #999;">
<div style="font-size: 48px; margin-bottom: 15px;"></div>
<p>No leftover videos found. All videos are assigned to playlists!</p>
</div>
{% endif %}
</div>
<!-- PDFs Section -->
<div class="card" style="margin-bottom: 30px;">
<h2>📄 Leftover PDFs ({{ leftover_pdfs|length }})</h2>
{% if leftover_pdfs %}
<div style="max-height: 400px; overflow-y: auto; margin-top: 20px;">
<table class="table" style="width: 100%; border-collapse: collapse;">
<thead style="position: sticky; top: 0; background: #f8f9fa;">
<tr>
<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Filename</th>
<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Size</th>
<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Duration</th>
<th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Uploaded</th>
</tr>
</thead>
<tbody>
{% for pdf in leftover_pdfs %}
<tr style="border-bottom: 1px solid #dee2e6;">
<td style="padding: 10px;">📄 {{ pdf.filename }}</td>
<td style="padding: 10px;">{{ "%.2f"|format(pdf.file_size_mb) }} MB</td>
<td style="padding: 10px;">{{ pdf.duration }}s</td>
<td style="padding: 10px;">{{ pdf.uploaded_at | localtime if pdf.uploaded_at else 'N/A' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div style="text-align: center; padding: 40px; color: #999;">
<div style="font-size: 48px; margin-bottom: 15px;"></div>
<p>No leftover PDFs found. All PDFs are assigned to playlists!</p>
</div>
{% endif %}
</div>
</div>
<style>
body.dark-mode .card {
background: #2d3748;
color: #e2e8f0;
}
body.dark-mode h1,
body.dark-mode h2 {
color: #e2e8f0;
}
body.dark-mode .stat-item {
background: #1a202c !important;
color: #e2e8f0;
}
body.dark-mode .table thead {
background: #1a202c !important;
}
body.dark-mode .table th,
body.dark-mode .table td {
color: #e2e8f0;
border-color: #4a5568 !important;
}
body.dark-mode .table tr {
border-color: #4a5568 !important;
}
body.dark-mode .table tr:hover {
background: #1a202c;
}
</style>
{% endblock %}
@@ -0,0 +1,524 @@
{% extends "base.html" %}
{% block title %}User Management - DigiServer v2{% endblock %}
{% block content %}
<div class="page-header">
<h1>👥 User Management</h1>
<button class="btn btn-primary" onclick="showCreateUserModal()">+ Create New User</button>
</div>
<!-- Users Table -->
<div class="card">
<h2>All Users</h2>
<div class="table-responsive">
<table class="user-table">
<thead>
<tr>
<th>Username</th>
<th>Role</th>
<th>Created At</th>
<th>Last Login</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% if users %}
{% for user in users %}
<tr>
<td>
<strong>{{ user.username }}</strong>
{% if user.id == current_user.id %}
<span class="badge badge-info">You</span>
{% endif %}
</td>
<td>
<span class="badge badge-{{ 'success' if user.role == 'admin' else 'secondary' }}">
{{ user.role|capitalize }}
</span>
</td>
<td>{{ user.created_at | localtime if user.created_at else 'N/A' }}</td>
<td>{{ user.last_login | localtime if user.last_login else 'Never' }}</td>
<td class="actions">
{% if user.id != current_user.id %}
<button class="btn btn-sm btn-warning" onclick="showEditUserModal({{ user.id }}, '{{ user.username }}', '{{ user.role }}')">
Edit Role
</button>
<button class="btn btn-sm btn-secondary" onclick="showResetPasswordModal({{ user.id }}, '{{ user.username }}')">
Reset Password
</button>
<button class="btn btn-sm btn-danger" onclick="confirmDeleteUser({{ user.id }}, '{{ user.username }}')">
Delete
</button>
{% else %}
<span class="text-muted">Current User</span>
{% endif %}
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="5" class="text-center">No users found</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<!-- Role Descriptions -->
<div class="card">
<h2>📖 Role Descriptions</h2>
<div class="role-descriptions">
<div class="role-item">
<h3>👤 Normal User</h3>
<ul>
<li>Upload media content</li>
<li>Add/remove media from playlists</li>
<li>Edit media in playlists</li>
<li>Set display time for media items</li>
<li>View players and groups</li>
</ul>
</div>
<div class="role-item">
<h3>👑 Admin User</h3>
<ul>
<li>All normal user permissions</li>
<li>Create and manage users</li>
<li>Manage players and groups</li>
<li>Delete content</li>
<li>Access system settings</li>
<li>View system logs</li>
</ul>
</div>
</div>
</div>
<!-- Create User Modal -->
<div id="createUserModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Create New User</h2>
<span class="close" onclick="closeModal('createUserModal')">&times;</span>
</div>
<form method="POST" action="{{ url_for('admin.create_user') }}">
<div class="form-group">
<label for="username">Username *</label>
<input type="text" id="username" name="username" required minlength="3"
placeholder="Enter username" class="form-control">
<small>Minimum 3 characters</small>
</div>
<div class="form-group">
<label for="password">Password *</label>
<input type="password" id="password" name="password" required minlength="6"
placeholder="Enter password" class="form-control">
<small>Minimum 6 characters</small>
</div>
<div class="form-group">
<label for="role">Role *</label>
<select id="role" name="role" required class="form-control">
<option value="user">Normal User</option>
<option value="admin">Admin User</option>
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeModal('createUserModal')">Cancel</button>
<button type="submit" class="btn btn-primary">Create User</button>
</div>
</form>
</div>
</div>
<!-- Edit Role Modal -->
<div id="editRoleModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Edit User Role</h2>
<span class="close" onclick="closeModal('editRoleModal')">&times;</span>
</div>
<form method="POST" id="editRoleForm">
<div class="form-group">
<label>Username</label>
<input type="text" id="edit_username" readonly class="form-control">
</div>
<div class="form-group">
<label for="edit_role">New Role *</label>
<select id="edit_role" name="role" required class="form-control">
<option value="user">Normal User</option>
<option value="admin">Admin User</option>
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeModal('editRoleModal')">Cancel</button>
<button type="submit" class="btn btn-primary">Update Role</button>
</div>
</form>
</div>
</div>
<!-- Reset Password Modal -->
<div id="resetPasswordModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Reset User Password</h2>
<span class="close" onclick="closeModal('resetPasswordModal')">&times;</span>
</div>
<form method="POST" id="resetPasswordForm">
<div class="form-group">
<label>Username</label>
<input type="text" id="reset_username" readonly class="form-control">
</div>
<div class="form-group">
<label for="new_password">New Password *</label>
<input type="password" id="new_password" name="password" required minlength="6"
placeholder="Enter new password" class="form-control">
<small>Minimum 6 characters</small>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeModal('resetPasswordModal')">Cancel</button>
<button type="submit" class="btn btn-primary">Reset Password</button>
</div>
</form>
</div>
</div>
<style>
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.page-header .btn-primary {
background: #667eea;
color: white;
border: none;
}
.page-header .btn-primary:hover {
background: #5568d3;
}
body.dark-mode .page-header .btn-primary {
background: #7c3aed;
}
body.dark-mode .page-header .btn-primary:hover {
background: #6d28d9;
}
.table-responsive {
overflow-x: auto;
}
.user-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
.user-table th,
.user-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
.user-table th {
background: #f8f9fa;
font-weight: 600;
color: #495057;
}
.user-table tbody tr:hover {
background: #f8f9fa;
}
body.dark-mode .user-table th,
body.dark-mode .user-table td {
border-bottom: 1px solid #4a5568;
}
body.dark-mode .user-table th {
background: #1a202c;
color: #e2e8f0;
}
body.dark-mode .user-table tbody tr:hover {
background: #1a202c;
}
.badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
display: inline-block;
}
.badge-success {
background: #28a745;
color: white;
}
.badge-secondary {
background: #6c757d;
color: white;
}
.badge-info {
background: #17a2b8;
color: white;
margin-left: 8px;
}
.actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.btn-sm {
padding: 6px 12px;
font-size: 14px;
}
.btn-warning {
background: #ffc107;
color: #000;
}
.btn-warning:hover {
background: #e0a800;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
.role-descriptions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 15px;
}
.role-item {
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
.role-item h3 {
margin-bottom: 10px;
color: #495057;
}
.role-item ul {
list-style-position: inside;
padding-left: 0;
}
.role-item li {
padding: 5px 0;
color: #666;
}
body.dark-mode .role-item {
background: #1a202c;
border: 1px solid #4a5568;
}
body.dark-mode .role-item h3 {
color: #e2e8f0;
}
body.dark-mode .role-item li {
color: #a0aec0;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: #fff;
margin: 5% auto;
padding: 0;
border-radius: 8px;
width: 90%;
max-width: 500px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
body.dark-mode .modal-content {
background-color: #2d3748;
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #e0e0e0;
}
body.dark-mode .modal-header {
border-bottom: 1px solid #4a5568;
}
.modal-header h2 {
margin: 0;
font-size: 20px;
}
body.dark-mode .modal-header h2 {
color: #e2e8f0;
}
.close {
font-size: 28px;
font-weight: bold;
color: #aaa;
cursor: pointer;
}
.close:hover {
color: #000;
}
.modal form {
padding: 20px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #495057;
}
body.dark-mode .form-group label {
color: #e2e8f0;
}
.form-control {
width: 100%;
padding: 10px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 14px;
background: white;
color: #2d3748;
}
.form-control:focus {
outline: none;
border-color: #667eea;
}
body.dark-mode .form-control {
background: #1a202c;
border-color: #4a5568;
color: #e2e8f0;
}
body.dark-mode .form-control:focus {
border-color: #7c3aed;
}
.form-group small {
display: block;
margin-top: 5px;
color: #6c757d;
font-size: 12px;
}
body.dark-mode .form-group small {
color: #a0aec0;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding-top: 20px;
border-top: 1px solid #e0e0e0;
margin-top: 20px;
}
body.dark-mode .modal-footer {
border-top: 1px solid #4a5568;
}
.text-muted {
color: #6c757d;
}
.text-center {
text-align: center;
}
</style>
<script>
function showCreateUserModal() {
document.getElementById('createUserModal').style.display = 'block';
}
function showEditUserModal(userId, username, currentRole) {
document.getElementById('edit_username').value = username;
document.getElementById('edit_role').value = currentRole;
document.getElementById('editRoleForm').action = `/admin/user/${userId}/role`;
document.getElementById('editRoleModal').style.display = 'block';
}
function showResetPasswordModal(userId, username) {
document.getElementById('reset_username').value = username;
document.getElementById('resetPasswordForm').action = `/admin/user/${userId}/password`;
document.getElementById('resetPasswordModal').style.display = 'block';
}
function closeModal(modalId) {
document.getElementById(modalId).style.display = 'none';
}
function confirmDeleteUser(userId, username) {
if (confirm(`Are you sure you want to delete user "${username}"? This action cannot be undone.`)) {
const form = document.createElement('form');
form.method = 'POST';
form.action = `/admin/user/${userId}/delete`;
document.body.appendChild(form);
form.submit();
}
}
// Close modal when clicking outside
window.onclick = function(event) {
const modals = document.getElementsByClassName('modal');
for (let modal of modals) {
if (event.target === modal) {
modal.style.display = 'none';
}
}
}
</script>
{% endblock %}