i18 drin
Formeleon Deployment Pipeline / Deploy to Dev Environment (push) Successful in 1m1s
Formeleon Deployment Pipeline / Deploy to Production Environment (push) Skipped

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:
br committed 2026-09-29 05:16:01 +02:00
1 parent 7f81932fa5
commit 5fff17531d
31 files changed
+2683 -550

No files matched your search

+90 -64
View File
@@ -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 %}