Files
br 5fff17531d
Formeleon Deployment Pipeline / Deploy to Dev Environment (push) Successful in 1m1s
Formeleon Deployment Pipeline / Deploy to Production Environment (push) Skipped
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.
2026-09-29 05:16:01 +02:00

309 lines
14 KiB
HTML

{# ==============================================================================
Formeleon - Multi-tenant Form Generator
Author: Björn Runden <br@runden.net>
Template: admin/dashboard.html
Description: Admin dashboard overview page with statistics and quick access.
============================================================================== #}
{% extends "base.html" %}
{% block title %}Dashboard - Formeleon{% endblock %}
{% block content %}
<div class="container">
<div class="row mb-4">
<div class="col-12">
<h1><i class="bi bi-speedometer2"></i> Dashboard</h1>
<p class="text-muted">Welcome, <strong>{{ user.name }}</strong>! | Role: <span class="badge bg-{{ 'primary' if user.role.value == 'admin' else 'secondary' }}">{{ user.role.value }}</span></p>
</div>
</div>
{# Statistics Cards #}
<div class="row mb-4">
<div class="col-md-3 col-6 mb-3">
<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>
</a>
</div>
<div class="col-md-3 col-6 mb-3">
<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>
</a>
</div>
<div class="col-md-3 col-6 mb-3">
<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>
</a>
</div>
<div class="col-md-3 col-6 mb-3">
<div class="card bg-warning text-dark">
<div class="card-body">
<div class="d-flex align-items-center">
<i class="bi bi-activity display-6"></i>
<div class="ms-3">
<h3 class="mb-0">100%</h3>
<p class="mb-0">Uptime</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
{# Recent Submissions #}
<div class="col-12 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-light">
<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" 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 class="text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for submission in recent_submissions %}
<tr>
<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>
{% 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 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>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<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 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>
</div>
</div>
</div>
{# Recent Forms #}
<div class="col-12 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-light">
<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" 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 class="text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for form in recent_forms %}
<tr>
<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>
{% 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 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>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<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 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>
</div>
</div>
</div>
</div>
{% if user.is_admin() %}
{# Tenants Overview #}
<div class="row">
<div class="col-12 mb-4">
<div class="card">
<div class="card-header bg-light">
<h5><i class="bi bi-buildings"></i> Tenants Overview</h5>
</div>
<div class="card-body p-0">
{% if tenants %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Name</th>
<th>ID</th>
<th>Status</th>
<th>Forms</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for tenant in tenants %}
<tr>
<td>{{ tenant.name }}</td>
<td><code>{{ tenant.id }}</code></td>
<td>
<span class="badge bg-{{ 'success' if tenant.status.value == 'active' else 'warning' if tenant.status.value == 'suspended' else 'danger' }}">
{{ tenant.status.value }}
</span>
</td>
<td>{{ tenant.forms|length }}</td>
<td>
<a href="{{ url_for('admin.view_tenant', tenant_id=tenant.id) }}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="p-4 text-center text-muted">
<i class="bi bi-buildings display-4"></i>
<p>No tenants yet</p>
</div>
{% endif %}
</div>
<div class="card-footer bg-light">
<a href="{{ url_for('admin.list_tenants') }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-list"></i> View All Tenants
</a>
<a href="{{ url_for('admin.create_tenant') }}" class="btn btn-sm btn-primary">
<i class="bi bi-plus"></i> Create Tenant
</a>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block extra_css %}
<style>
.dev-mode-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #ffc107;
color: #000;
padding: 10px 20px;
font-size: 0.9rem;
z-index: 1000;
box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
}
.card {
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.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 %}