i18 drin
mail drin
fehlende routen drin
prod-reife erreicht.
TODOS:
Deine aktuellen offenen TODOs
1. Rate-Limiting (Spam-Schutz VOR dem reCAPTCHA)
Das reCAPTCHA hält zwar Bots ab, aber wenn jemand böswillig ein Script schreibt, das 1.000 POST-Requests pro Sekunde an /api/v1/submissions sendet, muss dein Backend jedes Mal Google kontaktieren und die Datenbank befragen. Das frisst Server-Ressourcen.
Todo: Einbinden von Flask-Limiter und Absichern der API-Route (z.B. max. 10 Submissions pro Minute pro IP-Adresse oder Tenant).
2. Billing & Feature-Enforcement (Limits pro Tenant durchsetzen)
Die Mandanten-Architektur und das config-Feld im JSON existieren bereits. Aber wir erzwingen diese Limits aktuell nicht im Backend.
Todo: Im Dashboard (templates/admin/tenants/view.html) UI-Controls einbauen, um Feature-Flags für einen Tenant festzulegen:
max_forms (z.B. 1 für Free-User, 10 für Pro-User).
allow_instant_mail (Dürfen E-Mails an den Tenant gesendet werden?).
allow_autoresponder (Darf der Autoresponder für Endkunden genutzt werden?).
Todo: Diese Limits hart im Backend (beim Erstellen von Forms und beim Mailversand) kontrollieren. (Der Mailer prüft das zwar schon halbwegs, das UI zum Pflegen fehlt aber noch komplett).
3. E-Mail Templates aufwerten (HTML statt Plain-Text)
Bisher versendet der Service E-Mails als reinen Text (plain). Für eine professionelle SaaS-Lösung sollten diese Mails ansprechend aussehen.
Todo: Im services/mailer.py den MIMEMultipart so erweitern, dass er saubere HTML-Mails aus einem Jinja2-Template (templates/emails/...) generiert, inklusive Branding/Logo deines Services.
4. Frontend-Snippet "Graceful Degradation"
Das embed.js arbeitet aktuell sehr gut. Aber was ist, wenn das Formeleon-Backend mal für 5 Minuten down ist?
Todo: Einen sauberen Timeout im fetch einbauen und dem Endkunden auf der Webseite eine schöne, fallback HTML-Meldung anzeigen ("Entschuldigung, das Formular ist momentan nicht erreichbar"), anstatt dass sich das Script lautlos in der Console mit einem CORS/Network-Error verabschiedet.
5. CI/CD Pipeline (Deployment)
Wenn der Code fertig ist, willst du ihn sicher nicht immer händisch per git pull auf deinen Produktionsserver schieben und dort docker compose up tippen.
Todo: Eine simple Pipeline (Github Actions / Gitea Actions) anlegen, die den Code testet und automatisch auf dem Zielserver ausrollt.
This commit is contained in:
1 parent
7f81932fa5
commit
5fff17531d
31 files changed
+2683
-550
No files matched your search
@@ -21,45 +21,51 @@
|
||||
{# Statistics Cards #}
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3 col-6 mb-3">
|
||||
<div class="card bg-primary text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-buildings display-6"></i>
|
||||
<div class="ms-3">
|
||||
<h3 class="mb-0">{{ tenant_count }}</h3>
|
||||
<p class="mb-0">Tenants</p>
|
||||
<a href="{{ url_for('admin.list_tenants') }}" class="text-decoration-none">
|
||||
<div class="card bg-primary text-white h-100 hover-shadow">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-buildings display-6"></i>
|
||||
<div class="ms-3">
|
||||
<h3 class="mb-0">{{ tenant_count }}</h3>
|
||||
<p class="mb-0">Tenants</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 col-6 mb-3">
|
||||
<div class="card bg-success text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-file-earmark-text display-6"></i>
|
||||
<div class="ms-3">
|
||||
<h3 class="mb-0">{{ form_count }}</h3>
|
||||
<p class="mb-0">Forms</p>
|
||||
<a href="{{ url_for('admin.list_forms') }}" class="text-decoration-none">
|
||||
<div class="card bg-success text-white h-100 hover-shadow">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-file-earmark-text display-6"></i>
|
||||
<div class="ms-3">
|
||||
<h3 class="mb-0">{{ form_count }}</h3>
|
||||
<p class="mb-0">Forms</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 col-6 mb-3">
|
||||
<div class="card bg-info text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-inbox display-6"></i>
|
||||
<div class="ms-3">
|
||||
<h3 class="mb-0">{{ submission_count }}</h3>
|
||||
<p class="mb-0">Submissions</p>
|
||||
<a href="{{ url_for('admin.list_submissions') }}" class="text-decoration-none">
|
||||
<div class="card bg-info text-white h-100 hover-shadow">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-inbox display-6"></i>
|
||||
<div class="ms-3">
|
||||
<h3 class="mb-0">{{ submission_count }}</h3>
|
||||
<p class="mb-0">Submissions</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 col-6 mb-3">
|
||||
@@ -79,38 +85,46 @@
|
||||
|
||||
<div class="row">
|
||||
{# Recent Submissions #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card">
|
||||
<div class="col-12 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h5><i class="bi bi-inbox"></i> Recent Submissions</h5>
|
||||
<h5 class="mb-0"><i class="bi bi-inbox me-2"></i> Recent Submissions</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if recent_submissions %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
|
||||
<table class="table table-hover mb-0 align-middle">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Form</th>
|
||||
<th>Tenant</th>
|
||||
<th>Date</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for submission in recent_submissions %}
|
||||
<tr>
|
||||
<td>{{ submission.id[:12] }}...</td>
|
||||
<td>{{ submission.form_config.name if submission.form_config else 'N/A' }}</td>
|
||||
<td><small class="font-monospace text-muted">{{ submission.id[:12] }}...</small></td>
|
||||
<td><strong>{{ submission.form_config.name if submission.form_config else 'N/A' }}</strong></td>
|
||||
<td>{{ submission.tenant.name if submission.tenant else 'N/A' }}</td>
|
||||
<td>{{ submission.created_at.strftime('%Y-%m-%d %H:%M') if submission.created_at else 'N/A' }}</td>
|
||||
<td>
|
||||
<span class="badge bg-{{ 'success' if submission.status.value == 'processed' else 'warning' if submission.status.value == 'pending' else 'danger' }}">
|
||||
{{ submission.status.value }}
|
||||
</span>
|
||||
{% if submission.status.value == 'processed' %}
|
||||
<span class="badge bg-success">Processed</span>
|
||||
{% elif submission.status.value == 'pending' %}
|
||||
<span class="badge bg-warning text-dark">Pending</span>
|
||||
{% elif submission.status.value == 'spam' %}
|
||||
<span class="badge bg-danger">Spam</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ submission.status.value }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('admin.view_submission', submission_id=submission.id) }}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-eye"></i>
|
||||
<td class="text-end">
|
||||
<a href="{{ url_for('admin.view_submission', submission_id=submission.id) }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-eye"></i> View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -119,13 +133,13 @@
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-4 text-center text-muted">
|
||||
<i class="bi bi-inbox display-4"></i>
|
||||
<p>No recent submissions</p>
|
||||
<div class="p-5 text-center text-muted">
|
||||
<i class="bi bi-inbox display-4 d-block mb-3"></i>
|
||||
<p>No active recent submissions found.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-footer bg-light">
|
||||
<div class="card-footer bg-light text-end">
|
||||
<a href="{{ url_for('admin.list_submissions') }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-list"></i> View All Submissions
|
||||
</a>
|
||||
@@ -134,38 +148,42 @@
|
||||
</div>
|
||||
|
||||
{# Recent Forms #}
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card">
|
||||
<div class="col-12 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h5><i class="bi bi-file-earmark-text"></i> Recent Forms</h5>
|
||||
<h5 class="mb-0"><i class="bi bi-file-earmark-text me-2"></i> Recent Forms</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if recent_forms %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
|
||||
<table class="table table-hover mb-0 align-middle">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Tenant</th>
|
||||
<th>Fields</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for form in recent_forms %}
|
||||
<tr>
|
||||
<td>{{ form.name }}</td>
|
||||
<td><strong>{{ form.name }}</strong></td>
|
||||
<td>{{ form.tenant.name if form.tenant else 'N/A' }}</td>
|
||||
<td><span class="badge bg-info text-dark">{{ form.get_field_definitions()|length }}</span></td>
|
||||
<td>
|
||||
<span class="badge bg-{{ 'success' if form.is_active else 'secondary' }}">
|
||||
{{ 'Active' if form.is_active else 'Inactive' }}
|
||||
</span>
|
||||
{% if form.is_active %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Inactive</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ form.created_at.strftime('%Y-%m-%d') if form.created_at else 'N/A' }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('admin.view_form', form_id=form.id) }}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-eye"></i>
|
||||
<td class="text-end">
|
||||
<a href="{{ url_for('admin.view_form', form_id=form.id) }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -174,19 +192,19 @@
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-4 text-center text-muted">
|
||||
<i class="bi bi-file-earmark-text display-4"></i>
|
||||
<p>No recent forms</p>
|
||||
<div class="p-5 text-center text-muted">
|
||||
<i class="bi bi-file-earmark-text display-4 d-block mb-3"></i>
|
||||
<p>No forms have been created yet.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-footer bg-light">
|
||||
<div class="card-footer bg-light d-flex justify-content-between">
|
||||
<a href="{{ url_for('admin.create_form') }}" class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-plus"></i> Create New Form
|
||||
</a>
|
||||
<a href="{{ url_for('admin.list_forms') }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-list"></i> View All Forms
|
||||
</a>
|
||||
<a href="{{ url_for('admin.create_form') }}" class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-plus"></i> Create Form
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -278,5 +296,13 @@
|
||||
.display-6 {
|
||||
font-size: calc(1.2525rem + 0.975vw);
|
||||
}
|
||||
|
||||
.hover-shadow {
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
.hover-shadow:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user