This commit is contained in:
jpmvaz
2026-09-13 20:09:20 +01:00
commit 9b5cc30fb4
482 changed files with 57569 additions and 0 deletions
@@ -0,0 +1,51 @@
{% extends "base.html" %}
{% block title %}Audit log{% endblock %}
{% block content %}
<div class="section-head">
<div>
<span class="section-tag">// console audit log</span>
<h1>Audit log</h1>
<p class="lead">Sign-ins, configuration changes, and alert mutations. Most-recent first.</p>
</div>
<div class="log-actions">
<a href="{{ url_for('backoffice.audit_log_export') }}" class="btn btn-ghost">⬇ Export CSV</a>
<form method="post" action="{{ url_for('backoffice.audit_log_email') }}" class="log-email-form" novalidate>
{{ log_email_form.csrf_token }}
{{ log_email_form.log_recipient(placeholder="destination@example.com") }}
{{ log_email_form.submit(class_="btn btn-primary") }}
</form>
</div>
</div>
<table class="table">
<thead>
<tr><th>Timestamp (UTC)</th><th>User</th><th>Action</th><th>Details</th><th>IP</th></tr>
</thead>
<tbody>
{% for e in pagination.items %}
<tr>
<td class="mono dim" style="font-size: .82rem; white-space: nowrap;">{{ e.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</td>
<td>{{ e.username or '—' }}</td>
<td><span class="mono" style="color: var(--accent);">{{ e.action }}</span></td>
<td class="dim">{{ e.details or '' }}</td>
<td class="mono dim">{{ e.ip_address or '' }}</td>
</tr>
{% else %}
<tr><td colspan="5" class="dim" style="text-align: center; padding: 32px;">No audit entries yet.</td></tr>
{% endfor %}
</tbody>
</table>
{% if pagination.pages > 1 %}
<div class="pagination">
{% if pagination.has_prev %}<a href="{{ url_for('backoffice.audit_log', page=pagination.prev_num) }}"> prev</a>{% endif %}
{% for p in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
{% if p %}
{% if p == pagination.page %}<span class="current">{{ p }}</span>
{% else %}<a href="{{ url_for('backoffice.audit_log', page=p) }}">{{ p }}</a>{% endif %}
{% else %}<span></span>{% endif %}
{% endfor %}
{% if pagination.has_next %}<a href="{{ url_for('backoffice.audit_log', page=pagination.next_num) }}">next </a>{% endif %}
</div>
{% endif %}
{% endblock %}
@@ -0,0 +1,77 @@
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<div class="section-head">
<div>
<span class="section-tag">// console overview</span>
<h1>Operations dashboard</h1>
<p class="lead">System status as of <span class="mono">{{ today.isoformat() }}</span>.</p>
</div>
</div>
<div class="stat-grid">
<div class="stat stat-accent">
<div class="stat-label">Active alerts</div>
<div class="stat-value">{{ active_alerts }}</div>
</div>
<div class="stat">
<div class="stat-label">Users</div>
<div class="stat-value">{{ user_count }}</div>
</div>
<div class="stat stat-warn">
<div class="stat-label">Administrators</div>
<div class="stat-value">{{ admin_count }}</div>
</div>
</div>
<div style="display:grid; grid-template-columns: 1.2fr 1fr; gap: 20px;">
<section class="panel">
<header class="panel-header">
<h3>Expiring soonest</h3>
<a href="{{ url_for('backoffice.settings') }}" class="muted mono" style="font-size: .82rem;">settings →</a>
</header>
<div class="panel-body">
{% if expiring_soon %}
<table class="table" style="border: none;">
<thead>
<tr><th>Title</th><th>Expires</th><th>Days</th></tr>
</thead>
<tbody>
{% for a in expiring_soon %}
{% set days = (a.expiration_date - today).days %}
<tr>
<td><strong>{{ a.title }}</strong>{% if a.category %} <span class="alert-cat">{{ a.category }}</span>{% endif %}</td>
<td class="mono">{{ a.expiration_date.isoformat() }}</td>
<td class="mono" style="color: {% if days < 7 %}var(--danger){% elif days < 30 %}var(--amber){% else %}var(--accent){% endif %};">{{ days }}d</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="dim">No upcoming alerts.</p>
{% endif %}
</div>
</section>
<section class="panel">
<header class="panel-header"><h3>Recent activity</h3>
<a href="{{ url_for('backoffice.audit_log') }}" class="muted mono" style="font-size: .82rem;">full log →</a>
</header>
<div class="panel-body">
{% if recent_audit %}
<ul style="list-style: none; padding: 0; margin: 0; display: grid; gap: 8px;">
{% for entry in recent_audit %}
<li class="mono" style="font-size: .82rem; display: grid; grid-template-columns: 1fr auto; gap: 8px; padding: 6px 0; border-bottom: 1px dashed var(--border);">
<span><span style="color: var(--accent);">{{ entry.action }}</span> {% if entry.username %}<span class="dim">· {{ entry.username }}</span>{% endif %}{% if entry.details %}<br><span class="dim">{{ entry.details }}</span>{% endif %}</span>
<span class="dim" style="white-space: nowrap;">{{ entry.timestamp.strftime('%m-%d %H:%M') }}</span>
</li>
{% endfor %}
</ul>
{% else %}
<p class="dim">No activity yet.</p>
{% endif %}
</div>
</section>
</div>
<style>@media (max-width: 800px) { div[style*="grid-template-columns: 1.2fr 1fr"] { grid-template-columns: 1fr !important; } }</style>
{% endblock %}
@@ -0,0 +1,90 @@
{% extends "base.html" %}
{% block title %}Mail configuration{% endblock %}
{% block content %}
<div class="section-head">
<div>
<span class="section-tag">// console mail server</span>
<h1>SMTP configuration</h1>
<p class="lead">External mail server used to dispatch alert reminders. Edits take effect immediately.</p>
</div>
</div>
<div style="display: grid; grid-template-columns: 1.4fr 1fr; gap: 20px;">
<section class="panel">
<header class="panel-header"><h3>Server settings</h3></header>
<div class="panel-body">
<form method="post" action="{{ url_for('backoffice.mail_config') }}" class="form" novalidate>
{{ form.csrf_token }}
<div class="form-grid-2">
<div style="grid-column: span 2;">
{{ form.smtp_host.label }}{{ form.smtp_host(placeholder="smtp.example.com") }}
{% for err in form.smtp_host.errors %}<div class="error">{{ err }}</div>{% endfor %}
</div>
<div>
{{ form.smtp_port.label }}{{ form.smtp_port() }}
{% for err in form.smtp_port.errors %}<div class="error">{{ err }}</div>{% endfor %}
</div>
<div>
{{ form.smtp_encryption.label }}{{ form.smtp_encryption() }}
<div class="help">587 → STARTTLS · 465 → SSL/TLS · 25 → none</div>
</div>
<div>
{{ form.smtp_username.label }}{{ form.smtp_username(autocomplete="off") }}
</div>
<div>
{{ form.smtp_password.label }}{{ form.smtp_password(autocomplete="new-password") }}
<div class="help">Leave blank to keep the current password.</div>
</div>
<div>
{{ form.smtp_from_address.label }}{{ form.smtp_from_address() }}
</div>
<div>
{{ form.smtp_from_name.label }}{{ form.smtp_from_name() }}
</div>
</div>
<hr>
<h4 style="margin: 4px 0 2px; font-family: var(--font-mono); color: var(--text-secondary);">Sent-folder copy (IMAP)</h4>
<p class="help" style="margin-top: 0;">When enabled, a copy of every message the platform sends is saved to the mailbox's Sent folder over IMAP, so it appears in your normal mail client.</p>
<div class="check-row" style="margin-bottom: 12px;">{{ form.imap_enabled() }} {{ form.imap_enabled.label }}</div>
<div class="form-grid-2">
<div style="grid-column: span 2;">
{{ form.imap_host.label }}{{ form.imap_host(placeholder="imap.purelymail.com") }}
{% for err in form.imap_host.errors %}<div class="error">{{ err }}</div>{% endfor %}
</div>
<div>
{{ form.imap_port.label }}{{ form.imap_port(placeholder="993") }}
{% for err in form.imap_port.errors %}<div class="error">{{ err }}</div>{% endfor %}
</div>
<div>
{{ form.imap_sent_folder.label }}{{ form.imap_sent_folder(placeholder="Sent") }}
<div class="help">Usually <code>Sent</code>.</div>
</div>
<div>
{{ form.imap_username.label }}{{ form.imap_username(autocomplete="off") }}
</div>
<div>
{{ form.imap_password.label }}{{ form.imap_password(autocomplete="new-password") }}
<div class="help">Blank keeps current; if never set, the SMTP password is used.</div>
</div>
</div>
{{ form.submit(class_="btn btn-primary") }}
</form>
</div>
</section>
<section class="panel">
<header class="panel-header"><h3>Send test email</h3></header>
<div class="panel-body">
<p class="muted" style="font-size: .9rem;">Confirms credentials, port, and encryption are correct.</p>
<form method="post" action="{{ url_for('backoffice.mail_test') }}" class="form" novalidate>
{{ test_form.csrf_token }}
<div>{{ test_form.test_recipient.label }}{{ test_form.test_recipient(placeholder="you@example.com") }}</div>
{{ test_form.submit(class_="btn btn-ghost btn-block") }}
</form>
</div>
</section>
</div>
<style>@media (max-width: 800px) { div[style*="grid-template-columns: 1.4fr 1fr"] { grid-template-columns: 1fr !important; } }</style>
{% endblock %}
@@ -0,0 +1,73 @@
{% extends "base.html" %}
{% block title %}Mail log{% endblock %}
{% block content %}
<div class="section-head">
<div>
<span class="section-tag">// console mail log</span>
<h1>Mail log</h1>
<p class="lead">Every message the platform has dispatched, whether a copy reached the Sent folder, and its delivery result. Most-recent first.</p>
</div>
<div class="log-actions">
<a href="{{ url_for('backoffice.mail_log_export') }}" class="btn btn-ghost">⬇ Export CSV</a>
<form method="post" action="{{ url_for('backoffice.mail_log_email') }}" class="log-email-form" novalidate>
{{ log_email_form.csrf_token }}
{{ log_email_form.log_recipient(placeholder="destination@example.com") }}
{{ log_email_form.submit(class_="btn btn-primary") }}
</form>
</div>
</div>
<table class="table">
<thead>
<tr>
<th>Timestamp (UTC)</th><th>Kind</th><th>Recipients</th><th>Subject</th>
<th>Status</th><th>Sent copy</th><th>By</th><th>Info</th>
</tr>
</thead>
<tbody>
{% for m in pagination.items %}
<tr>
<td class="mono dim" style="font-size: .82rem; white-space: nowrap;">{{ m.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</td>
<td><span class="mono" style="color: var(--info);">{{ m.kind }}</span></td>
<td class="dim" style="font-size: .85rem;">{{ m.recipients or '—' }}</td>
<td>{{ m.subject or '—' }}</td>
<td>
{% if m.status == 'sent' %}
<span class="status-badge status-active"><span class="led"></span>sent</span>
{% else %}
<span class="status-badge" style="color: var(--danger);"><span class="led" style="background: var(--danger);"></span>failed</span>
{% endif %}
</td>
<td>
{% if m.sent_copy == 'saved' %}
<span class="mono" style="color: var(--accent);">saved</span>
{% elif m.sent_copy == 'failed' %}
<span class="mono" style="color: var(--danger);">failed</span>
{% elif m.sent_copy == 'disabled' %}
<span class="mono dim">disabled</span>
{% else %}
<span class="mono dim"></span>
{% endif %}
</td>
<td class="mono dim" style="font-size: .82rem;">{{ m.triggered_by or 'system' }}</td>
<td class="dim" style="font-size: .8rem;">{{ m.info or '' }}</td>
</tr>
{% else %}
<tr><td colspan="8" class="dim" style="text-align: center; padding: 32px;">No mail has been sent yet.</td></tr>
{% endfor %}
</tbody>
</table>
{% if pagination.pages > 1 %}
<div class="pagination">
{% if pagination.has_prev %}<a href="{{ url_for('backoffice.mail_log', page=pagination.prev_num) }}"> prev</a>{% endif %}
{% for p in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
{% if p %}
{% if p == pagination.page %}<span class="current">{{ p }}</span>
{% else %}<a href="{{ url_for('backoffice.mail_log', page=p) }}">{{ p }}</a>{% endif %}
{% else %}<span></span>{% endif %}
{% endfor %}
{% if pagination.has_next %}<a href="{{ url_for('backoffice.mail_log', page=pagination.next_num) }}">next </a>{% endif %}
</div>
{% endif %}
{% endblock %}
@@ -0,0 +1,45 @@
{% extends "base.html" %}
{% block title %}Settings{% endblock %}
{% block content %}
<div class="section-head">
<div>
<span class="section-tag">// console settings</span>
<h1>Global settings</h1>
<p class="lead">Default reminder cadence and scheduler controls.</p>
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
<section class="panel">
<header class="panel-header"><h3>Default reminder schedule</h3></header>
<div class="panel-body">
<form method="post" class="form" novalidate>
{{ form.csrf_token }}
<div>
{{ form.default_reminder_days.label }}{{ form.default_reminder_days() }}
<div class="help">When creating a new alert, this is the default. Existing alerts are unaffected.</div>
{% for err in form.default_reminder_days.errors %}<div class="error">{{ err }}</div>{% endfor %}
</div>
{{ form.submit(class_="btn btn-primary") }}
</form>
</div>
</section>
<section class="panel">
<header class="panel-header"><h3>Scheduler</h3></header>
<div class="panel-body">
<p>Reminder dispatch runs every <strong class="mono" style="color: var(--accent);">{{ scheduler_interval }}</strong> minute(s).</p>
<p class="muted" style="font-size: .9rem;">Configured via the <code>SCHEDULER_INTERVAL_MINUTES</code> environment variable. Restart the container to change it.</p>
<hr>
<p>You can trigger a check immediately to verify reminders dispatch correctly.</p>
<form method="post" action="{{ url_for('backoffice.run_check_now') }}" class="inline-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-ghost" type="submit">▶ Run reminder check now</button>
</form>
</div>
</section>
</div>
<style>@media (max-width: 800px) { div[style*="grid-template-columns: 1fr 1fr"] { grid-template-columns: 1fr !important; } }</style>
{% endblock %}
@@ -0,0 +1,41 @@
{% extends "base.html" %}
{% block title %}{{ 'Edit' if user else 'New' }} user{% endblock %}
{% block content %}
<div class="section-head">
<div>
<span class="section-tag">// section 03 {{ 'edit' if user else 'new' }} user</span>
<h1>{{ 'Edit user' if user else 'New user' }}</h1>
{% if user %}<p class="lead">Created <span class="mono">{{ user.created_at.strftime('%Y-%m-%d') }}</span></p>{% endif %}
</div>
<a href="{{ url_for('backoffice.users_list') }}" class="btn btn-ghost">← Back</a>
</div>
<div class="panel" style="max-width: 640px;">
<div class="panel-body">
<form method="post" class="form" novalidate>
{{ form.csrf_token }}
<div class="form-grid-2">
<div>{{ form.username.label }}{{ form.username() }}
{% for err in form.username.errors %}<div class="error">{{ err }}</div>{% endfor %}
</div>
<div>{{ form.email.label }}{{ form.email() }}
{% for err in form.email.errors %}<div class="error">{{ err }}</div>{% endfor %}
</div>
</div>
<div class="form-grid-2">
<div>{{ form.role.label }}{{ form.role() }}</div>
<div class="check-row" style="align-self: end;">{{ form.is_active() }} {{ form.is_active.label }}</div>
</div>
<div>
{{ form.password.label }}{{ form.password(autocomplete="new-password") }}
<div class="help">{% if user %}Leave blank to keep the existing password.{% else %}Min {{ config.PASSWORD_MIN_LENGTH }} characters.{% endif %}</div>
{% for err in form.password.errors %}<div class="error">{{ err }}</div>{% endfor %}
</div>
<div class="btn-row">
{{ form.submit(class_="btn btn-primary") }}
<a href="{{ url_for('backoffice.users_list') }}" class="btn btn-ghost">Cancel</a>
</div>
</form>
</div>
</div>
{% endblock %}
@@ -0,0 +1,65 @@
{% extends "base.html" %}
{% block title %}Users{% endblock %}
{% block content %}
<div class="section-head">
<div>
<span class="section-tag">// section 03 users & permissions</span>
<h1>Users</h1>
<p class="lead">Manage accounts, roles, MFA, and lockouts.</p>
</div>
<a href="{{ url_for('backoffice.user_new') }}" class="btn btn-primary">+ New user</a>
</div>
<table class="table">
<thead>
<tr>
<th>Username</th><th>Email</th><th>Role</th><th>MFA</th><th>Status</th><th>Last login</th><th class="actions">Actions</th>
</tr>
</thead>
<tbody>
{% for u in users %}
<tr>
<td><strong>{{ u.username }}</strong>{% if u.id == current_user.id %} <span class="dim mono" style="font-size: .72rem;">(you)</span>{% endif %}</td>
<td class="mono dim" style="font-size: .85rem;">{{ u.email }}</td>
<td><span class="role-badge role-{{ u.role }}">{{ u.role }}</span></td>
<td>
<span class="status-badge {{ 'status-active' if u.mfa_enabled else 'status-inactive' }}">
<span class="led"></span>{{ 'on' if u.mfa_enabled else 'off' }}
</span>
</td>
<td>
{% if u.is_locked %}
<span class="status-badge" style="color: var(--danger);"><span class="led" style="background: var(--danger);"></span>locked</span>
{% else %}
<span class="status-badge {{ 'status-active' if u.is_active else 'status-inactive' }}"><span class="led"></span>{{ 'active' if u.is_active else 'disabled' }}</span>
{% endif %}
</td>
<td class="mono dim" style="font-size: .82rem;">
{% if u.last_login_at %}{{ u.last_login_at.strftime('%Y-%m-%d %H:%M') }}{% else %}never{% endif %}
</td>
<td class="actions">
<a href="{{ url_for('backoffice.user_edit', user_id=u.id) }}" class="btn btn-ghost btn-sm">Edit</a>
{% if u.mfa_enabled %}
<form method="post" action="{{ url_for('backoffice.user_reset_mfa', user_id=u.id) }}" class="inline-form" data-confirm="Reset MFA for {{ u.username }}?">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-ghost btn-sm" type="submit">Reset MFA</button>
</form>
{% endif %}
{% if u.is_locked %}
<form method="post" action="{{ url_for('backoffice.user_unlock', user_id=u.id) }}" class="inline-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-ghost btn-sm" type="submit">Unlock</button>
</form>
{% endif %}
{% if u.id != current_user.id %}
<form method="post" action="{{ url_for('backoffice.user_delete', user_id=u.id) }}" class="inline-form" data-confirm="Permanently delete user '{{ u.username }}'?">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-danger btn-sm" type="submit">Delete</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}