fixed layout - rename functions for user to sender

This commit is contained in:
nahakubuilde
2025-06-10 01:50:35 +01:00
parent a0dfe8a535
commit f07b9c2150
15 changed files with 352 additions and 238 deletions

View File

@@ -5,7 +5,7 @@ This module provides the main dashboard view and overview functionality.
"""
from flask import render_template
from email_server.models import Session, Domain, User, DKIMKey, EmailLog, AuthLog
from email_server.models import Session, Domain, Sender, DKIMKey, EmailLog, AuthLog
from email_server.tool_box import get_logger
from .routes import email_bp
@@ -19,7 +19,7 @@ def dashboard():
try:
# Get counts
domain_count = session.query(Domain).filter_by(is_active=True).count()
user_count = session.query(User).filter_by(is_active=True).count()
sender_count = session.query(Sender).filter_by(is_active=True).count()
dkim_count = session.query(DKIMKey).filter_by(is_active=True).count()
# Get recent email logs
@@ -30,7 +30,7 @@ def dashboard():
return render_template('dashboard.html',
domain_count=domain_count,
user_count=user_count,
sender_count=sender_count,
dkim_count=dkim_count,
recent_emails=recent_emails,
recent_auths=recent_auths)

View File

@@ -432,11 +432,22 @@ def check_spf_dns():
spf_record = record
break
spf_valid_for_server = False
spf_check_message = ''
public_ip = get_public_ip()
ip_mechanism = f'ip4:{public_ip}'
if spf_record:
result['spf_record'] = spf_record
if ip_mechanism in spf_record:
spf_valid_for_server = True
spf_check_message = f'SPF is valid for this server (contains {ip_mechanism})'
else:
spf_check_message = f'SPF is missing this server\'s IP ({ip_mechanism})'
result['message'] = 'SPF record found'
else:
result['success'] = False
result['message'] = 'No SPF record found'
result['spf_valid_for_server'] = spf_valid_for_server
result['spf_check_message'] = spf_check_message
result['public_ip'] = public_ip
return jsonify(result)

View File

@@ -10,7 +10,7 @@ This module provides domain management functionality including:
"""
from flask import render_template, request, redirect, url_for, flash
from email_server.models import Session, Domain, User, WhitelistedIP, DKIMKey, CustomHeader
from email_server.models import Session, Domain, Sender, WhitelistedIP, DKIMKey, CustomHeader
from email_server.dkim_manager import DKIMManager
from email_server.tool_box import get_logger
from sqlalchemy.orm import joinedload
@@ -188,12 +188,12 @@ def remove_domain(domain_id: int):
domain_name = domain.domain_name
# Count associated records
user_count = session.query(User).filter_by(domain_id=domain_id).count()
sender_count = session.query(Sender).filter_by(domain_id=domain_id).count()
ip_count = session.query(WhitelistedIP).filter_by(domain_id=domain_id).count()
dkim_count = session.query(DKIMKey).filter_by(domain_id=domain_id).count()
# Delete associated records
session.query(User).filter_by(domain_id=domain_id).delete()
session.query(Sender).filter_by(domain_id=domain_id).delete()
session.query(WhitelistedIP).filter_by(domain_id=domain_id).delete()
session.query(DKIMKey).filter_by(domain_id=domain_id).delete()
session.query(CustomHeader).filter_by(domain_id=domain_id).delete()
@@ -202,7 +202,7 @@ def remove_domain(domain_id: int):
session.delete(domain)
session.commit()
flash(f'Domain {domain_name} and all associated data permanently removed ({user_count} users, {ip_count} IPs, {dkim_count} DKIM keys)', 'success')
flash(f'Domain {domain_name} and all associated data permanently removed ({sender_count} senders, {ip_count} IPs, {dkim_count} DKIM keys)', 'success')
return redirect(url_for('email.domains_list'))
except Exception as e:

View File

@@ -10,27 +10,27 @@ This module provides sender management functionality including:
"""
from flask import render_template, request, redirect, url_for, flash
from email_server.models import Session, Domain, User
from email_server.models import Session, Domain, Sender
from email_server.tool_box import get_logger
import bcrypt
from .routes import email_bp
from email_server.models import Session, Domain, User, hash_password
from email_server.models import Session, Domain, Sender, hash_password
logger = get_logger()
@email_bp.route('/senders')
def senders_list():
"""List all users."""
"""List all senders."""
session = Session()
try:
users = session.query(User, Domain).join(Domain, User.domain_id == Domain.id).order_by(User.email).all()
return render_template('senders.html', users=users)
senders = session.query(Sender, Domain).join(Domain, Sender.domain_id == Domain.id).order_by(Sender.email).all()
return render_template('senders.html', senders=senders)
finally:
session.close()
@email_bp.route('/senders/add', methods=['GET', 'POST'])
def add_sender():
"""Add new user."""
"""Add new sender."""
session = Session()
try:
domains = session.query(Domain).filter_by(is_active=True).order_by(Domain.domain_name).all()
@@ -50,118 +50,118 @@ def add_sender():
flash('Invalid email format', 'error')
return redirect(url_for('email.add_sender'))
# Check if user already exists
existing = session.query(User).filter_by(email=email).first()
# Check if sender already exists
existing = session.query(Sender).filter_by(email=email).first()
if existing:
flash(f'User {email} already exists', 'error')
flash(f'Sender {email} already exists', 'error')
return redirect(url_for('email.senders_list'))
# Create user
user = User(
# Create sender
sender = Sender(
email=email,
password_hash=hash_password(password),
domain_id=domain_id,
can_send_as_domain=can_send_as_domain
)
session.add(user)
session.add(sender)
session.commit()
flash(f'User {email} added successfully', 'success')
flash(f'Sender {email} added successfully', 'success')
return redirect(url_for('email.senders_list'))
return render_template('add_sender.html', domains=domains)
except Exception as e:
session.rollback()
logger.error(f"Error adding user: {e}")
flash(f'Error adding user: {str(e)}', 'error')
logger.error(f"Error adding sender: {e}")
flash(f'Error adding sender: {str(e)}', 'error')
return redirect(url_for('email.add_sender'))
finally:
session.close()
@email_bp.route('/senders/<int:user_id>/delete', methods=['POST'])
def delete_sender(user_id: int):
"""Disable user (soft delete)."""
"""Disable sender (soft delete)."""
session = Session()
try:
user = session.query(User).get(user_id)
if not user:
flash('User not found', 'error')
sender = session.query(Sender).get(user_id)
if not sender:
flash('Sender not found', 'error')
return redirect(url_for('email.senders_list'))
user_email = user.email
user.is_active = False
sender_email = sender.email
sender.is_active = False
session.commit()
flash(f'User {user_email} disabled', 'success')
flash(f'Sender {sender_email} disabled', 'success')
return redirect(url_for('email.senders_list'))
except Exception as e:
session.rollback()
logger.error(f"Error disabling user: {e}")
flash(f'Error disabling user: {str(e)}', 'error')
logger.error(f"Error disabling sender: {e}")
flash(f'Error disabling sender: {str(e)}', 'error')
return redirect(url_for('email.senders_list'))
finally:
session.close()
@email_bp.route('/senders/<int:user_id>/enable', methods=['POST'])
def enable_sender(user_id: int):
"""Enable user."""
"""Enable sender."""
session = Session()
try:
user = session.query(User).get(user_id)
if not user:
flash('User not found', 'error')
sender = session.query(Sender).get(user_id)
if not sender:
flash('Sender not found', 'error')
return redirect(url_for('email.senders_list'))
user_email = user.email
user.is_active = True
sender_email = sender.email
sender.is_active = True
session.commit()
flash(f'User {user_email} enabled', 'success')
flash(f'Sender {sender_email} enabled', 'success')
return redirect(url_for('email.senders_list'))
except Exception as e:
session.rollback()
logger.error(f"Error enabling user: {e}")
flash(f'Error enabling user: {str(e)}', 'error')
logger.error(f"Error enabling sender: {e}")
flash(f'Error enabling sender: {str(e)}', 'error')
return redirect(url_for('email.senders_list'))
finally:
session.close()
@email_bp.route('/senders/<int:user_id>/remove', methods=['POST'])
def remove_sender(user_id: int):
"""Permanently remove user."""
"""Permanently remove sender."""
session = Session()
try:
user = session.query(User).get(user_id)
if not user:
flash('User not found', 'error')
sender = session.query(Sender).get(user_id)
if not sender:
flash('Sender not found', 'error')
return redirect(url_for('email.senders_list'))
user_email = user.email
session.delete(user)
sender_email = sender.email
session.delete(sender)
session.commit()
flash(f'User {user_email} permanently removed', 'success')
flash(f'Sender {sender_email} permanently removed', 'success')
return redirect(url_for('email.senders_list'))
except Exception as e:
session.rollback()
logger.error(f"Error removing user: {e}")
flash(f'Error removing user: {str(e)}', 'error')
logger.error(f"Error removing sender: {e}")
flash(f'Error removing sender: {str(e)}', 'error')
return redirect(url_for('email.senders_list'))
finally:
session.close()
@email_bp.route('/senders/<int:user_id>/edit', methods=['GET', 'POST'])
def edit_sender(user_id: int):
"""Edit user."""
"""Edit sender."""
session = Session()
try:
user = session.query(User).get(user_id)
if not user:
flash('User not found', 'error')
sender = session.query(Sender).get(user_id)
if not sender:
flash('Sender not found', 'error')
return redirect(url_for('email.senders_list'))
domains = session.query(Domain).filter_by(is_active=True).order_by(Domain.domain_name).all()
@@ -181,35 +181,35 @@ def edit_sender(user_id: int):
flash('Invalid email format', 'error')
return redirect(url_for('email.edit_sender', user_id=user_id))
# Check if email already exists (excluding current user)
existing = session.query(User).filter(
User.email == email,
User.id != user_id
# Check if email already exists (excluding current sender)
existing = session.query(Sender).filter(
Sender.email == email,
Sender.id != user_id
).first()
if existing:
flash(f'Email {email} already exists', 'error')
return redirect(url_for('email.edit_sender', user_id=user_id))
# Update user
user.email = email
user.domain_id = domain_id
user.can_send_as_domain = can_send_as_domain
# Update sender
sender.email = email
sender.domain_id = domain_id
sender.can_send_as_domain = can_send_as_domain
# Update password if provided
if password:
user.password_hash = hash_password(password)
sender.password_hash = hash_password(password)
session.commit()
flash(f'User {email} updated successfully', 'success')
flash(f'Sender {email} updated successfully', 'success')
return redirect(url_for('email.senders_list'))
return render_template('edit_sender.html', user=user, domains=domains)
return render_template('edit_sender.html', sender=sender, domains=domains)
except Exception as e:
session.rollback()
logger.error(f"Error editing user: {e}")
flash(f'Error editing user: {str(e)}', 'error')
logger.error(f"Error editing sender: {e}")
flash(f'Error editing sender: {str(e)}', 'error')
return redirect(url_for('email.senders_list'))
finally:
session.close()

View File

@@ -131,6 +131,20 @@
<!-- Custom SMTP Management CSS -->
<link href="{{ url_for('email.static', filename='css/smtp-management.css') }}" rel="stylesheet">
<style>
/* Ensure tooltip text is visible on dark backgrounds */
.tooltip-inner {
color: #fff !important;
background-color: #222 !important;
font-size: 1rem;
text-align: left;
}
.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,
.bs-tooltip-top .tooltip-arrow::before {
border-top-color: #222 !important;
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
@@ -337,6 +351,16 @@
<!-- Custom SMTP Management JavaScript -->
<script src="{{ url_for('email.static', filename='js/smtp-management.js') }}"></script>
<!-- Bootstrap Tooltip Initialization -->
<script>
document.addEventListener('DOMContentLoaded', function() {
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
new bootstrap.Tooltip(tooltipTriggerEl);
});
});
</script>
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@@ -7,6 +7,7 @@
<div class="row">
<!-- Statistics Cards -->
<div class="col-lg-3 col-md-6 mb-4">
<a href="{{ url_for('email.domains_list') }}" class="dashboard-card-link text-decoration-none">
<div class="card border-primary">
<div class="card-body">
<div class="d-flex align-items-center">
@@ -24,19 +25,21 @@
</div>
</div>
</div>
</a>
</div>
<div class="col-lg-3 col-md-6 mb-4">
<a href="{{ url_for('email.senders_list') }}" class="dashboard-card-link text-decoration-none">
<div class="card border-success">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h5 class="card-title text-success mb-1">
<i class="bi bi-people me-2"></i>
Users
Senders
</h5>
<h3 class="mb-0">{{ user_count }}</h3>
<small class="text-muted">Authenticated users</small>
<h3 class="mb-0">{{ sender_count }}</h3>
<small class="text-muted">Authenticated senders</small>
</div>
<div class="fs-2 text-success opacity-50">
<i class="bi bi-people"></i>
@@ -44,9 +47,11 @@
</div>
</div>
</div>
</a>
</div>
<div class="col-lg-3 col-md-6 mb-4">
<a href="{{ url_for('email.dkim_list') }}" class="dashboard-card-link text-decoration-none">
<div class="card border-warning">
<div class="card-body">
<div class="d-flex align-items-center">
@@ -64,6 +69,7 @@
</div>
</div>
</div>
</a>
</div>
<div class="col-lg-3 col-md-6 mb-4">
@@ -75,11 +81,28 @@
<i class="bi bi-activity me-2"></i>
Status
</h5>
<h6 class="text-success mb-0">
{% set health = check_health() %}
<h6 class="{% if health.status == 'healthy' %}text-success{% else %}text-warning{% endif %} mb-0 status-indicator"
data-bs-toggle="tooltip"
data-bs-html="true"
data-bs-placement="top"
title="{{ (
"<div class='text-start'><strong>Service Status:</strong><br>" +
'SMTP Server: ' + health.services.smtp_server|title + '<br>' +
'Web Frontend: ' + health.services.web_frontend|title + '<br>' +
'Database: ' + health.services.database|title + '</div>'
) | safe }}">
<i class="bi bi-circle-fill me-1" style="font-size: 0.5rem;"></i>
Online
{{ health.status|title }}
</h6>
<small class="text-muted">Server running</small>
<small class="text-muted">
{% if health.services.smtp_server == 'running' and health.services.database == 'ok' %}
All services running
{% else %}
{% if health.services.smtp_server == 'stopped' %}SMTP Server stopped{% endif %}
{% if health.services.database == 'error' %}Database error{% endif %}
{% endif %}
</small>
</div>
<div class="fs-2 text-info opacity-50">
<i class="bi bi-activity"></i>
@@ -248,7 +271,7 @@
<div class="d-grid">
<a href="{{ url_for('email.add_sender') }}" class="btn btn-outline-success">
<i class="bi bi-person-plus me-2"></i>
Add User
Add Sender
</a>
</div>
</div>
@@ -282,4 +305,31 @@
location.reload();
}, 30000);
</script>
<style>
.status-indicator {
cursor: pointer;
}
.dashboard-card-link {
cursor: pointer;
display: block;
}
.dashboard-card-link:hover .card {
box-shadow: 0 0 0 2px #0d6efd33;
filter: brightness(1.05);
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltipTriggerList.forEach(function(tooltipTriggerEl) {
new bootstrap.Tooltip(tooltipTriggerEl, {
html: true,
placement: 'top',
trigger: 'hover'
});
});
});
</script>
{% endblock %}

View File

@@ -331,6 +331,30 @@
{% block extra_js %}
<script>
// Clipboard copy function for all copy buttons
function copyToClipboard(text) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(function() {
showToast('Copied to clipboard!', 'success');
}, function(err) {
showToast('Failed to copy: ' + err, 'danger');
});
} else {
// Fallback for older browsers
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
showToast('Copied to clipboard!', 'success');
} catch (err) {
showToast('Failed to copy: ' + err, 'danger');
}
document.body.removeChild(textarea);
}
}
// Show toast notification
function showToast(message, type = 'info') {
const toastContainer = document.getElementById('toastContainer');
@@ -433,6 +457,14 @@
// Update SPF status
if (spfResult.success) {
spfStatus.innerHTML = '<span class="status-indicator status-success"></span><small class="text-success">✓ Found</small>';
// Show additional SPF check message if available
if (typeof spfResult.spf_valid_for_server !== 'undefined') {
if (spfResult.spf_valid_for_server) {
spfStatus.innerHTML += '<br><span class="text-success"><i class="bi bi-check-circle me-1"></i> SPF is valid for this server</span>';
} else {
spfStatus.innerHTML += '<br><span class="text-warning"><i class="bi bi-exclamation-triangle me-1"></i> SPF missing server IP</span>';
}
}
} else {
spfStatus.innerHTML = '<span class="status-indicator status-danger"></span><small class="text-danger">✗ Not found</small>';
}

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Edit User - SMTP Management{% endblock %}
{% block title %}Edit Sender - SMTP Management{% endblock %}
{% block content %}
<div class="row">
@@ -9,7 +9,7 @@
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-person-fill-gear me-2"></i>
Edit User
Edit Sender
</h5>
</div>
<div class="card-body">
@@ -20,7 +20,7 @@
class="form-control"
id="email"
name="email"
value="{{ user.email }}"
value="{{ sender.email }}"
required>
</div>
@@ -42,7 +42,7 @@
<option value="">Select a domain</option>
{% for domain in domains %}
<option value="{{ domain.id }}"
{% if domain.id == user.domain_id %}selected{% endif %}>
{% if domain.id == sender.domain_id %}selected{% endif %}>
{{ domain.domain_name }}
</option>
{% endfor %}
@@ -55,12 +55,12 @@
type="checkbox"
id="can_send_as_domain"
name="can_send_as_domain"
{% if user.can_send_as_domain %}checked{% endif %}>
{% if sender.can_send_as_domain %}checked{% endif %}>
<label class="form-check-label" for="can_send_as_domain">
<strong>Can send as any email from domain</strong>
</label>
<div class="form-text">
Allow this user to send emails using any address within their domain
Allow this sender to send emails using any address within their domain
</div>
</div>
</div>
@@ -68,7 +68,7 @@
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg me-1"></i>
Update User
Update Sender
</button>
<a href="{{ url_for('email.senders_list') }}" class="btn btn-secondary">
<i class="bi bi-x-lg me-1"></i>
@@ -85,26 +85,26 @@
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-info-circle me-2"></i>
Current User Details
Current Sender Details
</h6>
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-4">Email:</dt>
<dd class="col-sm-8">
<code>{{ user.email }}</code>
<code>{{ sender.email }}</code>
</dd>
<dt class="col-sm-4">Domain:</dt>
<dd class="col-sm-8">
{% for domain in domains %}
{% if domain.id == user.domain_id %}
{% if domain.id == sender.domain_id %}
<span class="badge bg-secondary">{{ domain.domain_name }}</span>
{% endif %}
{% endfor %}
</dd>
<dt class="col-sm-4">Status:</dt>
<dd class="col-sm-8">
{% if user.is_active %}
{% if sender.is_active %}
<span class="badge bg-success">
<i class="bi bi-check-circle me-1"></i>
Active
@@ -118,7 +118,7 @@
</dd>
<dt class="col-sm-4">Domain Sender:</dt>
<dd class="col-sm-8">
{% if user.can_send_as_domain %}
{% if sender.can_send_as_domain %}
<span class="badge bg-success">
<i class="bi bi-check-circle me-1"></i>
Yes
@@ -133,7 +133,7 @@
<dt class="col-sm-4">Created:</dt>
<dd class="col-sm-8">
<small class="text-muted">
{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}
{{ sender.created_at.strftime('%Y-%m-%d %H:%M') }}
</small>
</dd>
</dl>
@@ -144,7 +144,7 @@
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-shield-check me-2"></i>
User Permissions
Sender Permissions
</h6>
</div>
<div class="card-body">
@@ -154,8 +154,8 @@
Domain Sender Permission
</h6>
<ul class="mb-0 small">
<li><strong>Enabled:</strong> User can send emails using any address in their domain (e.g., admin@domain.com, support@domain.com)</li>
<li><strong>Disabled:</strong> User can only send emails from their own email address</li>
<li><strong>Enabled:</strong> Sender can send emails using any address in their domain (e.g., admin@domain.com, support@domain.com)</li>
<li><strong>Disabled:</strong> Sender can only send emails from their own email address</li>
</ul>
</div>
</div>

View File

@@ -76,7 +76,7 @@
</h5>
</div>
<div class="card-body p-0">
{% if users %}
{% if senders %}
<div class="table-responsive">
<table class="table table-dark table-hover mb-0">
<thead>
@@ -90,16 +90,16 @@
</tr>
</thead>
<tbody>
{% for user, domain in users %}
{% for sender, domain in senders %}
<tr>
<td>
<div class="fw-bold">{{ user.email }}</div>
<div class="fw-bold">{{ sender.email }}</div>
</td>
<td>
<span class="badge bg-secondary">{{ domain.domain_name }}</span>
</td>
<td>
{% if user.can_send_as_domain %}
{% if sender.can_send_as_domain %}
<span class="badge bg-warning" style="color: black;">
<i class="bi bi-star me-1"></i>
Domain Sender
@@ -112,11 +112,11 @@
Regular Sender
</span>
<br>
<small class="text-muted">Can only send as {{ user.email }}</small>
<small class="text-muted">Can only send as {{ sender.email }}</small>
{% endif %}
</td>
<td>
{% if user.is_active %}
{% if sender.is_active %}
<span class="badge bg-success">
<i class="bi bi-check-circle me-1"></i>
Active
@@ -130,45 +130,45 @@
</td>
<td>
<small class="text-muted">
{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}
{{ sender.created_at.strftime('%Y-%m-%d %H:%M') }}
</small>
</td>
<td>
<div class="btn-group" role="group">
<!-- Edit Button -->
<a href="{{ url_for('email.edit_sender', user_id=user.id) }}"
<a href="{{ url_for('email.edit_sender', user_id=sender.id) }}"
class="btn btn-outline-primary btn-sm"
title="Edit Sender">
<i class="bi bi-pencil"></i>
</a>
<!-- Enable/Disable Button -->
{% if user.is_active %}
<form method="post" action="{{ url_for('email.delete_sender', user_id=user.id) }}" class="d-inline">
{% if sender.is_active %}
<form method="post" action="{{ url_for('email.delete_sender', user_id=sender.id) }}" class="d-inline">
<button type="submit"
class="btn btn-outline-warning btn-sm"
title="Disable Sender"
onclick="return confirm('Disable user {{ user.email }}?')">
onclick="return confirm('Disable user {{ sender.email }}?')">
<i class="bi bi-pause-circle"></i>
</button>
</form>
{% else %}
<form method="post" action="{{ url_for('email.enable_sender', user_id=user.id) }}" class="d-inline">
<form method="post" action="{{ url_for('email.enable_sender', user_id=sender.id) }}" class="d-inline">
<button type="submit"
class="btn btn-outline-success btn-sm"
title="Enable Sender"
onclick="return confirm('Enable user {{ user.email }}?')">
onclick="return confirm('Enable user {{ sender.email }}?')">
<i class="bi bi-play-circle"></i>
</button>
</form>
{% endif %}
<!-- Permanent Remove Button -->
<form method="post" action="{{ url_for('email.remove_sender', user_id=user.id) }}" class="d-inline">
<form method="post" action="{{ url_for('email.remove_sender', user_id=sender.id) }}" class="d-inline">
<button type="submit"
class="btn btn-outline-danger btn-sm"
title="Permanently Remove Sender"
onclick="return confirm('Permanently remove user {{ user.email }}? This cannot be undone!')">
onclick="return confirm('Permanently remove user {{ sender.email }}? This cannot be undone!')">
<i class="bi bi-trash"></i>
</button>
</form>

View File

@@ -44,7 +44,7 @@
class="nav-link text-white {{ 'active' if request.endpoint in ['email.senders_list', 'email.add_user'] else '' }}">
<i class="bi bi-people me-2"></i>
Allowed Senders
<span class="badge bg-secondary ms-auto">{{ user_count if user_count is defined else '' }}</span>
<span class="badge bg-secondary ms-auto">{{ sender_count if sender_count is defined else '' }}</span>
</a>
</li>
@@ -122,12 +122,22 @@
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<small class="text-muted d-block">Server Status</small>
<small class="text-success">
{% set health = check_health() %}
<small class="{% if health.status == 'healthy' %}text-success{% else %}text-warning{% endif %} status-indicator"
data-bs-toggle="tooltip"
data-bs-html="true"
data-bs-placement="top"
title="{{ (
"<div class='text-start'><strong>Service Status:</strong><br>" +
'SMTP Server: ' + health.services.smtp_server|title + '<br>' +
'Web Frontend: ' + health.services.web_frontend|title + '<br>' +
'Database: ' + health.services.database|title + '</div>'
) | safe }}">
<i class="bi bi-circle-fill me-1" style="font-size: 0.5rem;"></i>
Online
{{ health.status|title }}
</small>
</div>
<button class="btn btn-outline-secondary btn-sm" title="Refresh Status">
<button class="btn btn-outline-secondary btn-sm" title="Refresh Status" onclick="location.reload()">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
@@ -170,6 +180,10 @@
font-size: 0.7rem;
}
.status-indicator {
cursor: pointer;
}
/* Responsive design */
@media (max-width: 768px) {
.sidebar {
@@ -186,3 +200,16 @@
}
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltipTriggerList.forEach(function(tooltipTriggerEl) {
new bootstrap.Tooltip(tooltipTriggerEl, {
html: true,
placement: 'top',
trigger: 'hover'
});
});
});
</script>