diff --git a/Website/main.py b/Website/main.py
index ae40c35..750e746 100644
--- a/Website/main.py
+++ b/Website/main.py
@@ -2517,27 +2517,33 @@ def booking_payment():
app_obj = current_app._get_current_object()
user_id = session.get("user_id")
- username = session.get("username") # Hole Username für Übergabe
+ username = session.get("username")
user_email = session.get("email") or form_data.get("contact_email")
# Startet den asynchronen Provisioning-Thread
thread = threading.Thread(
target=background_booking_provisioning,
- args=(app_obj, prov_id, subdomain, package_list, user_id, form_data["school_name"], user_email, username) # Username am Ende hinzugefügt
+ args=(app_obj, prov_id, subdomain, package_list, user_id, form_data["school_name"], user_email, username)
)
thread.start()
print(f"[INFO] Background Provisioning Trigger gestartet für Subdomain: {subdomain}")
- # Antworte AJAX-Aufrufen direkt mit JSON
+ # ANPASSUNG FÜR BESSERE WARTEZEIT:
+ # Wir geben Metadaten mit, wie lange das Setup ca. dauern wird.
if is_json_request:
return jsonify({
"success": True,
"slug": subdomain,
- "message": "Provisioning gestartet."
+ "message": "Instanz wird eingerichtet...",
+ "estimated_seconds": 45, # Erwartete Dauer in Sekunden (anpassbar)
+ "status_poll_interval_ms": 3000 # Wie oft soll das Frontend fragen? (3 Sek)
}), 200
- flash(f"Buchung für {selected_package} wurde erfasst. Die Instanz wird jetzt eingerichtet.", "success")
- return redirect(url_for("my_instance_management"))
+ # Falls es KEIN AJAX-Request war:
+ # Wir hängen den Parameter "wait_for_slug" an, damit die "my_instance_management"-Seite weiß,
+ # dass sie sofort ein Lade-Overlay für diesen spezifischen Tenant öffnen muss.
+ flash(f"Buchung für {selected_package} wurde erfasst. Deine Instanz wird jetzt automatisch eingerichtet.", "success")
+ return redirect(url_for("my_instance_management", wait_for_slug=subdomain))
except Exception as e:
print(f"[FATAL ERROR] In booking_payment Route:")
diff --git a/Website/templates/schnell_buchung.html b/Website/templates/schnell_buchung.html
index c11d188..78353eb 100644
--- a/Website/templates/schnell_buchung.html
+++ b/Website/templates/schnell_buchung.html
@@ -163,7 +163,7 @@
margin-top: 1.5rem;
}
- /* Loading State UI */
+ /* NEU: Loading State & Fortschrittsbalken UI */
#setup-loading-state,
#setup-success-state {
display: none;
@@ -171,23 +171,44 @@
padding: 3rem 1rem;
}
- .spinner {
- width: 60px;
- height: 60px;
- border: 5px solid #e8f4f4;
- border-top-color: #166271;
- border-radius: 50%;
- animation: spin 1s linear infinite;
- margin: 0 auto 2rem;
+ .progress-wrapper {
+ width: 100%;
+ max-width: 480px;
+ margin: 2.5rem auto 1rem;
}
- @keyframes spin { 100% { transform: rotate(360deg); } }
+ .progress-bar-container {
+ width: 100%;
+ height: 16px;
+ background: #e8f4f4;
+ border-radius: 999px;
+ overflow: hidden;
+ box-shadow: inset 0 2px 4px rgba(0,0,0,0.05);
+ }
+
+ .progress-bar-fill {
+ height: 100%;
+ width: 0%;
+ background: linear-gradient(90deg, #166271, #1b8a3e);
+ border-radius: 999px;
+ transition: width 0.5s ease-out, background-color 0.3s;
+ }
+
+ .progress-percent {
+ font-size: 1.5rem;
+ font-weight: 800;
+ color: #173a42;
+ margin-top: 0.8rem;
+ font-variant-numeric: tabular-nums; /* Sorgt dafür, dass die Zahlen beim Hochzählen nicht wackeln */
+ }
.status-messages {
font-size: 1.1rem;
- color: #49626b;
+ color: #5e757d;
font-weight: 600;
height: 2rem;
+ margin-top: 1.5rem;
+ transition: color 0.3s;
}
/* Success State UI */
@@ -274,7 +295,6 @@
Paket
{{ package }}
-
-
2. Rechnungsadresse (Schulträger)
@@ -353,7 +372,6 @@
-
@@ -367,10 +385,18 @@
Instanz wird bereitgestellt
-
Ihre Bestellung wird verarbeitet. Die Rechnung wird zeitgleich generiert.
-
+
Ihre Umgebung wird isoliert aufgesetzt und Zertifikate werden generiert.
+
+
+
+
Validierung der Daten...
-
Bitte schließen Sie dieses Fenster nicht. Dies dauert in der Regel 15-30 Sekunden.
+
Bitte schließen Sie dieses Fenster nicht. Dies dauert etwa 30 bis 45 Sekunden.
@@ -391,7 +417,6 @@
Initial-Passwort: -
-
📄 Rechnung (PDF) herunterladen
@@ -416,7 +441,10 @@
const form = document.getElementById('checkout-form');
const errorAlert = document.getElementById('form-error-alert');
const statusText = document.getElementById('loading-status-text');
- const spinner = document.querySelector('.spinner'); // Referenz auf den Ladekreis
+
+ // Fortschritts-Elemente
+ const progressBar = document.getElementById('provisioning-progress-bar');
+ const progressPercent = document.getElementById('provisioning-percent');
// Subdomain Live-Prüfung
const slugInput = document.getElementById('tenant_slug');
@@ -424,12 +452,8 @@
const submitBtn = document.getElementById('booking_submit');
let slugTimer = null;
- // OPTIMIERT: Alles im 'input'-Event (fängt auch Maus-Paste und Autocomplete)
slugInput.addEventListener('input', (e) => {
- // 1. Sanitization
e.target.value = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '');
-
- // 2. Debounced API Check
clearTimeout(slugTimer);
slugTimer = setTimeout(() => checkSubdomain(e.target.value), 600);
});
@@ -495,12 +519,13 @@
throw new Error(result.error || 'Es gab ein Problem bei der Buchung.');
}
- // === HIER WAR DER FEHLER: JETZT WIRD AUF LOADING GEWECHSELT UND GEPOLLT ===
+ // Wechsel auf den Loading-Screen
formState.style.display = 'none';
loadingState.style.display = 'block';
- // Polling starten, um auf Fertigstellung der Instanz zu warten
- startPolling(tenantSlug);
+ // Backend-Schätzung für die Dauer nutzen, Default 45 Sekunden
+ const estimatedSec = result.estimated_seconds || 45;
+ startPolling(tenantSlug, estimatedSec);
} catch (error) {
errorAlert.textContent = error.message;
@@ -511,15 +536,17 @@
});
// ==========================================
- // Polling Logik (Warten auf den Server)
+ // Polling Logik mit Fortschrittsbalken
// ==========================================
- async function startPolling(slug) {
+ async function startPolling(slug, estimatedSeconds) {
+ // --- 1. Text-Nachrichten rotieren ---
const statusMessages = [
- "Registriere Schuldatenbank...",
- "Erstelle Auftragsverarbeitungsvertrag...",
- "Generiere Rechnung für Schulträger...",
- "Konfiguriere Nginx & Zertifikate...",
- "Führe finale Health-Checks durch..."
+ "Erstelle Systemkonfiguration...",
+ "Registriere dedizierte Schuldatenbank...",
+ "Generiere Rechnung & Auftragsverarbeitungsvertrag...",
+ "Konfiguriere SSL-Zertifikate & Webserver...",
+ "Führe finale Health-Checks durch...",
+ "Fast geschafft..."
];
let msgIndex = 0;
@@ -528,17 +555,30 @@
statusText.textContent = statusMessages[msgIndex];
msgIndex++;
}
- }, 3500);
+ }, Math.floor((estimatedSeconds * 1000) / statusMessages.length));
+ // --- 2. Fortschrittsbalken Animation ---
+ let elapsed = 0;
+ let currentPercent = 0;
+ const estimatedMs = estimatedSeconds * 1000;
+
+ const progressInterval = setInterval(() => {
+ elapsed += 250; // Jede Viertelsekunde updaten für flüssigen Lauf
+ // Kurve flacht gegen Ende ab (asymptotisch bis 99%)
+ currentPercent = Math.min(99, Math.floor((elapsed / estimatedMs) * 100));
+ progressBar.style.width = currentPercent + '%';
+ progressPercent.textContent = currentPercent + '%';
+ }, 250);
+
+ // --- 3. Backend-Polling ---
let retryCount = 0;
- const maxRetries = 30; // OPTIMIERT: Max 90 Sekunden Polling (30 * 3s)
+ const maxRetries = 40; // Max (40 * 3s) = 120 Sekunden
const checkStatus = async () => {
retryCount++;
- // Timeout-Abbruch
if (retryCount > maxRetries) {
- handlePollingError("Zeitüberschreitung. Die Bereitstellung dauert ungewöhnlich lange. Sie werden per E-Mail informiert, sobald das System bereit ist.", msgInterval);
+ handleError("Die Bereitstellung dauert ungewöhnlich lange. Sie erhalten die Zugangsdaten per E-Mail, sobald das System bereit ist.");
return;
}
@@ -548,36 +588,50 @@
const data = await res.json();
- if (data.status === 'ready') {
- // SETUP FERTIG!
+ if (data.status === 'ready' || data.status === 'active') {
+ // SETUP ERFOLGREICH!
clearInterval(msgInterval);
- showSuccessState(data.url, data.username, data.password, data.invoice_url);
- } else if (data.status === 'failed') {
+ clearInterval(progressInterval);
+
+ // Schöne End-Animation
+ progressBar.style.width = '100%';
+ progressBar.style.background = '#1b8a3e'; // Direkt auf massiv-grün wechseln
+ progressPercent.textContent = '100%';
+ statusText.textContent = "Einrichtung erfolgreich abgeschlossen!";
+ statusText.style.color = '#1b8a3e';
+
+ // Lass den Nutzer kurz die 100% sehen, bevor der Screen wechselt
+ setTimeout(() => {
+ showSuccessState(data.url, data.username, data.password, data.invoice_url);
+ }, 1200);
+
+ } else if (data.status === 'failed' || data.status === 'error') {
// SETUP FEHLGESCHLAGEN
- handlePollingError("Fehler bei der Bereitstellung. Bitte kontaktieren Sie unseren Support.", msgInterval);
+ handleError("Fehler bei der Bereitstellung. Bitte kontaktieren Sie unseren Support.");
} else {
// SETUP LÄUFT NOCH -> in 3 Sekunden erneut prüfen
setTimeout(checkStatus, 3000);
}
} catch (err) {
- // Bei Netzwerkfehler weiter versuchen, solange maxRetries nicht erreicht ist
+ // Bei kurzem Netzwerk-Schluckauf einfach weiter pollig
setTimeout(checkStatus, 3000);
}
};
- // Starte den ersten Check nach 2 Sekunden
- setTimeout(checkStatus, 2000);
- }
-
- // OPTIMIERT: Hilfsfunktion für Polling-Fehler (verhindert Sackgassen-UI)
- function handlePollingError(message, intervalId) {
- clearInterval(intervalId);
- statusText.textContent = message;
- statusText.style.color = '#b02a37';
- if (spinner) spinner.style.display = 'none'; // Spinner ausblenden
-
- // Optional: Einen Zurück/Support-Button einfügen
- statusText.insertAdjacentHTML('afterend', '
Seite neu laden');
+ function handleError(message) {
+ clearInterval(msgInterval);
+ clearInterval(progressInterval);
+ statusText.textContent = message;
+ statusText.style.color = '#b02a37';
+ progressBar.style.background = '#b02a37'; // Balken wird rot
+
+ if (!document.getElementById('retry-btn')) {
+ statusText.insertAdjacentHTML('afterend', '
');
+ }
+ }
+
+ // Starte den ersten Status-Check nach 3 Sekunden
+ setTimeout(checkStatus, 3000);
}
// ==========================================
@@ -593,7 +647,6 @@
document.getElementById('success-pass').textContent = pass;
document.getElementById('btn-go-to-tenant').href = url;
- // Rechnungs-Button einblenden, falls URL vorhanden
const invoiceBtn = document.getElementById('btn-download-invoice');
if (invoiceUrl) {
invoiceBtn.href = invoiceUrl;