changes to the processing state
This commit is contained in:
+12
-6
@@ -2517,27 +2517,33 @@ def booking_payment():
|
|||||||
|
|
||||||
app_obj = current_app._get_current_object()
|
app_obj = current_app._get_current_object()
|
||||||
user_id = session.get("user_id")
|
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")
|
user_email = session.get("email") or form_data.get("contact_email")
|
||||||
|
|
||||||
# Startet den asynchronen Provisioning-Thread
|
# Startet den asynchronen Provisioning-Thread
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
target=background_booking_provisioning,
|
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()
|
thread.start()
|
||||||
print(f"[INFO] Background Provisioning Trigger gestartet für Subdomain: {subdomain}")
|
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:
|
if is_json_request:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": True,
|
"success": True,
|
||||||
"slug": subdomain,
|
"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
|
}), 200
|
||||||
|
|
||||||
flash(f"Buchung für {selected_package} wurde erfasst. Die Instanz wird jetzt eingerichtet.", "success")
|
# Falls es KEIN AJAX-Request war:
|
||||||
return redirect(url_for("my_instance_management"))
|
# 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:
|
except Exception as e:
|
||||||
print(f"[FATAL ERROR] In booking_payment Route:")
|
print(f"[FATAL ERROR] In booking_payment Route:")
|
||||||
|
|||||||
@@ -163,7 +163,7 @@
|
|||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loading State UI */
|
/* NEU: Loading State & Fortschrittsbalken UI */
|
||||||
#setup-loading-state,
|
#setup-loading-state,
|
||||||
#setup-success-state {
|
#setup-success-state {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -171,23 +171,44 @@
|
|||||||
padding: 3rem 1rem;
|
padding: 3rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner {
|
.progress-wrapper {
|
||||||
width: 60px;
|
width: 100%;
|
||||||
height: 60px;
|
max-width: 480px;
|
||||||
border: 5px solid #e8f4f4;
|
margin: 2.5rem auto 1rem;
|
||||||
border-top-color: #166271;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
margin: 0 auto 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 {
|
.status-messages {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
color: #49626b;
|
color: #5e757d;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
transition: color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Success State UI */
|
/* Success State UI */
|
||||||
@@ -274,7 +295,6 @@
|
|||||||
<span class="booking-review__label">Paket</span>
|
<span class="booking-review__label">Paket</span>
|
||||||
<div class="booking-review__value">{{ package }}</div>
|
<div class="booking-review__value">{{ package }}</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- OPTIMIERT: Eindeutiger Hinweis auf Rechnungsstellung -->
|
|
||||||
<div class="booking-review__card" style="border-left: 4px solid #1b8a3e;">
|
<div class="booking-review__card" style="border-left: 4px solid #1b8a3e;">
|
||||||
<span class="booking-review__label">Zahlungsart</span>
|
<span class="booking-review__label">Zahlungsart</span>
|
||||||
<div class="booking-review__value" style="color: #1b8a3e;">Kauf auf Rechnung</div>
|
<div class="booking-review__value" style="color: #1b8a3e;">Kauf auf Rechnung</div>
|
||||||
@@ -308,7 +328,6 @@
|
|||||||
<input id="contact_email" name="contact_email" type="email" required value="{{ form_data.contact_email }}">
|
<input id="contact_email" name="contact_email" type="email" required value="{{ form_data.contact_email }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- OPTIMIERT: Rechnungsdaten inkl. Kassenzeichen -->
|
|
||||||
<div class="booking-review__section-title">2. Rechnungsadresse (Schulträger)</div>
|
<div class="booking-review__section-title">2. Rechnungsadresse (Schulträger)</div>
|
||||||
<div class="booking-review__field booking-review__field--full">
|
<div class="booking-review__field booking-review__field--full">
|
||||||
<label for="billing_organization">Einrichtung / Amt *</label>
|
<label for="billing_organization">Einrichtung / Amt *</label>
|
||||||
@@ -353,7 +372,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fehleranzeige, falls der Server beim Submit meckert -->
|
|
||||||
<div id="form-error-alert" style="display:none; color:#b02a37; margin-top:1rem; font-weight:bold;"></div>
|
<div id="form-error-alert" style="display:none; color:#b02a37; margin-top:1rem; font-weight:bold;"></div>
|
||||||
|
|
||||||
<div class="booking-review__actions">
|
<div class="booking-review__actions">
|
||||||
@@ -367,10 +385,18 @@
|
|||||||
<!-- ========================================== -->
|
<!-- ========================================== -->
|
||||||
<div id="setup-loading-state">
|
<div id="setup-loading-state">
|
||||||
<h2>Instanz wird bereitgestellt</h2>
|
<h2>Instanz wird bereitgestellt</h2>
|
||||||
<p>Ihre Bestellung wird verarbeitet. Die Rechnung wird zeitgleich generiert.</p>
|
<p>Ihre Umgebung wird isoliert aufgesetzt und Zertifikate werden generiert.</p>
|
||||||
<div class="spinner"></div>
|
|
||||||
|
<!-- NEU: Fortschrittsbalken -->
|
||||||
|
<div class="progress-wrapper">
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div id="provisioning-progress-bar" class="progress-bar-fill"></div>
|
||||||
|
</div>
|
||||||
|
<div id="provisioning-percent" class="progress-percent">0%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="status-messages" id="loading-status-text">Validierung der Daten...</div>
|
<div class="status-messages" id="loading-status-text">Validierung der Daten...</div>
|
||||||
<p style="margin-top: 2rem; color: #5e757d; font-size: 0.9rem;">Bitte schließen Sie dieses Fenster nicht. Dies dauert in der Regel 15-30 Sekunden.</p>
|
<p style="margin-top: 1.5rem; color: #5e757d; font-size: 0.9rem;">Bitte schließen Sie dieses Fenster nicht. Dies dauert etwa 30 bis 45 Sekunden.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ========================================== -->
|
<!-- ========================================== -->
|
||||||
@@ -391,7 +417,6 @@
|
|||||||
<p><strong>Initial-Passwort:</strong> <code id="success-pass">-</code></p>
|
<p><strong>Initial-Passwort:</strong> <code id="success-pass">-</code></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- OPTIMIERT: Rechnungs-Download, Weiterleitung und Drucken -->
|
|
||||||
<div class="booking-review__actions" style="justify-content: center; margin-top: 2rem;">
|
<div class="booking-review__actions" style="justify-content: center; margin-top: 2rem;">
|
||||||
<a id="btn-download-invoice" href="#" class="btn success-btn" target="_blank" style="display: none;">
|
<a id="btn-download-invoice" href="#" class="btn success-btn" target="_blank" style="display: none;">
|
||||||
📄 Rechnung (PDF) herunterladen
|
📄 Rechnung (PDF) herunterladen
|
||||||
@@ -416,7 +441,10 @@
|
|||||||
const form = document.getElementById('checkout-form');
|
const form = document.getElementById('checkout-form');
|
||||||
const errorAlert = document.getElementById('form-error-alert');
|
const errorAlert = document.getElementById('form-error-alert');
|
||||||
const statusText = document.getElementById('loading-status-text');
|
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
|
// Subdomain Live-Prüfung
|
||||||
const slugInput = document.getElementById('tenant_slug');
|
const slugInput = document.getElementById('tenant_slug');
|
||||||
@@ -424,12 +452,8 @@
|
|||||||
const submitBtn = document.getElementById('booking_submit');
|
const submitBtn = document.getElementById('booking_submit');
|
||||||
let slugTimer = null;
|
let slugTimer = null;
|
||||||
|
|
||||||
// OPTIMIERT: Alles im 'input'-Event (fängt auch Maus-Paste und Autocomplete)
|
|
||||||
slugInput.addEventListener('input', (e) => {
|
slugInput.addEventListener('input', (e) => {
|
||||||
// 1. Sanitization
|
|
||||||
e.target.value = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '');
|
e.target.value = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '');
|
||||||
|
|
||||||
// 2. Debounced API Check
|
|
||||||
clearTimeout(slugTimer);
|
clearTimeout(slugTimer);
|
||||||
slugTimer = setTimeout(() => checkSubdomain(e.target.value), 600);
|
slugTimer = setTimeout(() => checkSubdomain(e.target.value), 600);
|
||||||
});
|
});
|
||||||
@@ -495,12 +519,13 @@
|
|||||||
throw new Error(result.error || 'Es gab ein Problem bei der Buchung.');
|
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';
|
formState.style.display = 'none';
|
||||||
loadingState.style.display = 'block';
|
loadingState.style.display = 'block';
|
||||||
|
|
||||||
// Polling starten, um auf Fertigstellung der Instanz zu warten
|
// Backend-Schätzung für die Dauer nutzen, Default 45 Sekunden
|
||||||
startPolling(tenantSlug);
|
const estimatedSec = result.estimated_seconds || 45;
|
||||||
|
startPolling(tenantSlug, estimatedSec);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorAlert.textContent = error.message;
|
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 = [
|
const statusMessages = [
|
||||||
"Registriere Schuldatenbank...",
|
"Erstelle Systemkonfiguration...",
|
||||||
"Erstelle Auftragsverarbeitungsvertrag...",
|
"Registriere dedizierte Schuldatenbank...",
|
||||||
"Generiere Rechnung für Schulträger...",
|
"Generiere Rechnung & Auftragsverarbeitungsvertrag...",
|
||||||
"Konfiguriere Nginx & Zertifikate...",
|
"Konfiguriere SSL-Zertifikate & Webserver...",
|
||||||
"Führe finale Health-Checks durch..."
|
"Führe finale Health-Checks durch...",
|
||||||
|
"Fast geschafft..."
|
||||||
];
|
];
|
||||||
|
|
||||||
let msgIndex = 0;
|
let msgIndex = 0;
|
||||||
@@ -528,17 +555,30 @@
|
|||||||
statusText.textContent = statusMessages[msgIndex];
|
statusText.textContent = statusMessages[msgIndex];
|
||||||
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;
|
let retryCount = 0;
|
||||||
const maxRetries = 30; // OPTIMIERT: Max 90 Sekunden Polling (30 * 3s)
|
const maxRetries = 40; // Max (40 * 3s) = 120 Sekunden
|
||||||
|
|
||||||
const checkStatus = async () => {
|
const checkStatus = async () => {
|
||||||
retryCount++;
|
retryCount++;
|
||||||
|
|
||||||
// Timeout-Abbruch
|
|
||||||
if (retryCount > maxRetries) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,36 +588,50 @@
|
|||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.status === 'ready') {
|
if (data.status === 'ready' || data.status === 'active') {
|
||||||
// SETUP FERTIG!
|
// SETUP ERFOLGREICH!
|
||||||
clearInterval(msgInterval);
|
clearInterval(msgInterval);
|
||||||
showSuccessState(data.url, data.username, data.password, data.invoice_url);
|
clearInterval(progressInterval);
|
||||||
} else if (data.status === 'failed') {
|
|
||||||
|
// 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
|
// SETUP FEHLGESCHLAGEN
|
||||||
handlePollingError("Fehler bei der Bereitstellung. Bitte kontaktieren Sie unseren Support.", msgInterval);
|
handleError("Fehler bei der Bereitstellung. Bitte kontaktieren Sie unseren Support.");
|
||||||
} else {
|
} else {
|
||||||
// SETUP LÄUFT NOCH -> in 3 Sekunden erneut prüfen
|
// SETUP LÄUFT NOCH -> in 3 Sekunden erneut prüfen
|
||||||
setTimeout(checkStatus, 3000);
|
setTimeout(checkStatus, 3000);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Bei Netzwerkfehler weiter versuchen, solange maxRetries nicht erreicht ist
|
// Bei kurzem Netzwerk-Schluckauf einfach weiter pollig
|
||||||
setTimeout(checkStatus, 3000);
|
setTimeout(checkStatus, 3000);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Starte den ersten Check nach 2 Sekunden
|
function handleError(message) {
|
||||||
setTimeout(checkStatus, 2000);
|
clearInterval(msgInterval);
|
||||||
}
|
clearInterval(progressInterval);
|
||||||
|
statusText.textContent = message;
|
||||||
|
statusText.style.color = '#b02a37';
|
||||||
|
progressBar.style.background = '#b02a37'; // Balken wird rot
|
||||||
|
|
||||||
// OPTIMIERT: Hilfsfunktion für Polling-Fehler (verhindert Sackgassen-UI)
|
if (!document.getElementById('retry-btn')) {
|
||||||
function handlePollingError(message, intervalId) {
|
statusText.insertAdjacentHTML('afterend', '<br><button id="retry-btn" onclick="window.location.reload();" class="btn secondary" style="margin-top:1.5rem;">Seite neu laden</button>');
|
||||||
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
|
// Starte den ersten Status-Check nach 3 Sekunden
|
||||||
statusText.insertAdjacentHTML('afterend', '<br><a href="#" onclick="window.location.reload(); return false;" class="btn secondary" style="margin-top:1rem;">Seite neu laden</a>');
|
setTimeout(checkStatus, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -593,7 +647,6 @@
|
|||||||
document.getElementById('success-pass').textContent = pass;
|
document.getElementById('success-pass').textContent = pass;
|
||||||
document.getElementById('btn-go-to-tenant').href = url;
|
document.getElementById('btn-go-to-tenant').href = url;
|
||||||
|
|
||||||
// Rechnungs-Button einblenden, falls URL vorhanden
|
|
||||||
const invoiceBtn = document.getElementById('btn-download-invoice');
|
const invoiceBtn = document.getElementById('btn-download-invoice');
|
||||||
if (invoiceUrl) {
|
if (invoiceUrl) {
|
||||||
invoiceBtn.href = invoiceUrl;
|
invoiceBtn.href = invoiceUrl;
|
||||||
|
|||||||
Reference in New Issue
Block a user