657 lines
26 KiB
HTML
657 lines
26 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Buchung prüfen & abschließen{% endblock %}
|
|
|
|
{% block head %}
|
|
{{ super() }}
|
|
<style>
|
|
.booking-review {
|
|
max-width: 920px;
|
|
margin: 2rem auto 3rem;
|
|
padding: 0;
|
|
}
|
|
|
|
.booking-review__panel {
|
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(244, 247, 244, 0.96));
|
|
border: 1px solid rgba(28, 90, 99, 0.14);
|
|
border-radius: 26px;
|
|
padding: clamp(1.4rem, 2.6vw, 2.2rem);
|
|
box-shadow: 0 18px 44px rgba(18, 49, 54, 0.08);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.booking-review__eyebrow {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.45rem;
|
|
padding: 0.42rem 0.82rem;
|
|
border-radius: 999px;
|
|
background: #e8f4f4;
|
|
color: #166271;
|
|
font-weight: 700;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.booking-review__grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 1rem;
|
|
margin: 1.5rem 0;
|
|
}
|
|
|
|
.booking-review__card {
|
|
background: #ffffff;
|
|
border: 1px solid #dbe6e7;
|
|
border-radius: 18px;
|
|
padding: 1rem 1.1rem;
|
|
}
|
|
|
|
.booking-review__label {
|
|
display: block;
|
|
font-size: 0.88rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
color: #5e757d;
|
|
margin-bottom: 0.45rem;
|
|
}
|
|
|
|
.booking-review__value {
|
|
color: #173a42;
|
|
font-weight: 700;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.booking-review__note {
|
|
padding: 1rem 1.1rem;
|
|
border-radius: 18px;
|
|
background: #f8fbfb;
|
|
border: 1px solid #dbe6e7;
|
|
color: #49626b;
|
|
line-height: 1.65;
|
|
}
|
|
|
|
.booking-review__form-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 0.9rem;
|
|
margin-top: 1.25rem;
|
|
}
|
|
|
|
.booking-review__section-title {
|
|
grid-column: 1 / -1;
|
|
font-size: 1.15rem;
|
|
color: #166271;
|
|
margin-top: 1.5rem;
|
|
margin-bottom: 0.2rem;
|
|
border-bottom: 2px solid #e8f4f4;
|
|
padding-bottom: 0.5rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.booking-review__field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.35rem;
|
|
}
|
|
|
|
.booking-review__field--full {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.booking-review__field label {
|
|
font-weight: 700;
|
|
color: #2b4f58;
|
|
font-size: 0.93rem;
|
|
}
|
|
|
|
.booking-review__field input {
|
|
border: 1px solid #cedbdd;
|
|
border-radius: 12px;
|
|
background: #ffffff;
|
|
color: #173a42;
|
|
padding: 0.75rem 0.82rem;
|
|
}
|
|
|
|
.input-group--subdomain {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.input-group--subdomain input {
|
|
border-top-right-radius: 0;
|
|
border-bottom-right-radius: 0;
|
|
border-right: none;
|
|
flex-grow: 1;
|
|
}
|
|
|
|
.input-group--subdomain .domain-suffix {
|
|
background: #e8f4f4;
|
|
border: 1px solid #cedbdd;
|
|
padding: 0.75rem 0.82rem;
|
|
border-top-right-radius: 12px;
|
|
border-bottom-right-radius: 12px;
|
|
color: #5e757d;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.booking-review__consent {
|
|
padding: 0.95rem 1rem;
|
|
border-radius: 16px;
|
|
background: #eef6f6;
|
|
border: 1px solid #d2e2e2;
|
|
}
|
|
|
|
.booking-review__consent label {
|
|
display: flex;
|
|
gap: 0.7rem;
|
|
align-items: flex-start;
|
|
font-weight: 600;
|
|
color: #21414a;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.booking-review__consent input {
|
|
margin-top: 0.2rem;
|
|
}
|
|
|
|
.booking-review__actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.85rem;
|
|
margin-top: 1.5rem;
|
|
}
|
|
|
|
/* Loading State UI (1:30 Min Process) */
|
|
#setup-loading-state,
|
|
#setup-success-state {
|
|
display: none;
|
|
text-align: center;
|
|
padding: 3rem 1rem;
|
|
}
|
|
|
|
.progress-wrapper {
|
|
width: 100%;
|
|
max-width: 520px;
|
|
margin: 2.5rem auto 1rem;
|
|
}
|
|
|
|
.progress-bar-container {
|
|
width: 100%;
|
|
height: 18px;
|
|
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.4s ease-out;
|
|
}
|
|
|
|
.progress-percent {
|
|
font-size: 1.6rem;
|
|
font-weight: 800;
|
|
color: #173a42;
|
|
margin-top: 0.8rem;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.status-messages {
|
|
font-size: 1.05rem;
|
|
color: #166271;
|
|
font-weight: 600;
|
|
min-height: 2.5rem;
|
|
margin-top: 1.2rem;
|
|
transition: color 0.3s;
|
|
}
|
|
|
|
/* Success State UI */
|
|
.success-icon {
|
|
width: 70px;
|
|
height: 70px;
|
|
background: #1b8a3e;
|
|
color: white;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 2rem;
|
|
margin: 0 auto 1.5rem;
|
|
}
|
|
|
|
.credentials-box {
|
|
background: #173a42;
|
|
color: white;
|
|
border-radius: 12px;
|
|
padding: 1.5rem;
|
|
margin: 1.5rem auto;
|
|
max-width: 450px;
|
|
text-align: left;
|
|
}
|
|
|
|
.credentials-box code {
|
|
display: block;
|
|
background: rgba(255,255,255,0.1);
|
|
padding: 0.5rem;
|
|
border-radius: 6px;
|
|
margin-top: 0.3rem;
|
|
font-size: 1.1rem;
|
|
user-select: all;
|
|
}
|
|
|
|
.warning-text {
|
|
color: #b02a37;
|
|
font-weight: 700;
|
|
background: #f8d7da;
|
|
padding: 1rem;
|
|
border-radius: 8px;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.btn.success-btn {
|
|
background-color: #1b8a3e;
|
|
border-color: #1b8a3e;
|
|
color: #fff;
|
|
}
|
|
.btn.success-btn:hover {
|
|
background-color: #146c2e;
|
|
border-color: #146c2e;
|
|
}
|
|
|
|
@media (max-width: 720px) {
|
|
.booking-review__grid, .booking-review__form-grid { grid-template-columns: 1fr; }
|
|
.booking-review__actions { flex-direction: column; }
|
|
.booking-review__actions .btn { width: 100%; min-width: 0; }
|
|
}
|
|
|
|
@media print {
|
|
header, footer, nav, .booking-review__actions { display: none !important; }
|
|
body { background: white; }
|
|
.booking-review__panel { box-shadow: none; border: none; padding: 0; }
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<section class="booking-review">
|
|
<div class="booking-review__panel">
|
|
|
|
<!-- STATE 1: FORMULAR (Checkout) -->
|
|
<div id="checkout-form-state">
|
|
<span class="booking-review__eyebrow">Buchung prüfen & abschließen</span>
|
|
<h1>{{ title }}</h1>
|
|
|
|
<div class="booking-review__grid">
|
|
<div class="booking-review__card">
|
|
<span class="booking-review__label">Paket</span>
|
|
<div class="booking-review__value">{{ package }}</div>
|
|
</div>
|
|
<div class="booking-review__card" style="border-left: 4px solid #1b8a3e;">
|
|
<span class="booking-review__label">Zahlungsart</span>
|
|
<div class="booking-review__value" style="color: #1b8a3e;">Kauf auf Rechnung</div>
|
|
<small style="color: #5e757d; font-weight: 600;">30 Tage Zahlungsziel nach Bereitstellung</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="booking-review__note">
|
|
Die Software-Instanz wird nach dem Absenden <strong>sofort für 30 Tage freigeschaltet</strong>. Die finale Laufzeit aktiviert sich automatisch nach Zahlungseingang durch Ihren Schulträger.
|
|
</div>
|
|
|
|
<form id="checkout-form" method="POST" action="{{ url_for('booking_payment') }}">
|
|
<input type="hidden" name="package" value="{{ package_key }}">
|
|
<input type="hidden" name="flow" value="{{ flow }}">
|
|
|
|
<div class="booking-review__form-grid">
|
|
<div class="booking-review__section-title">1. Angaben zur Schule & Ansprechpartner</div>
|
|
<div class="booking-review__field">
|
|
<label for="school_name">Name der Schule *</label>
|
|
<input id="school_name" name="school_name" type="text" required value="{{ form_data.school_name }}">
|
|
</div>
|
|
<div class="booking-review__field">
|
|
<label for="school_number">Dienststellennummer *</label>
|
|
<input id="school_number" name="school_number" type="text" required placeholder="Zur Legitimation" value="{{ form_data.school_number }}">
|
|
</div>
|
|
<div class="booking-review__field">
|
|
<label for="contact_person">Ansprechpartner *</label>
|
|
<input id="contact_person" name="contact_person" type="text" required value="{{ form_data.contact_person }}">
|
|
</div>
|
|
<div class="booking-review__field">
|
|
<label for="contact_email">Dienstliche E-Mail *</label>
|
|
<input id="contact_email" name="contact_email" type="email" required value="{{ form_data.contact_email }}">
|
|
</div>
|
|
|
|
<div class="booking-review__section-title">2. Rechnungsadresse (Schulträger)</div>
|
|
<div class="booking-review__field booking-review__field--full">
|
|
<label for="billing_organization">Einrichtung / Amt *</label>
|
|
<input id="billing_organization" name="billing_organization" type="text" required placeholder="z.B. Stadt Neustadt - Schulverwaltungsamt" value="{{ form_data.billing_organization }}">
|
|
</div>
|
|
<div class="booking-review__field booking-review__field--full">
|
|
<label for="billing_street">Straße & Hausnummer *</label>
|
|
<input id="billing_street" name="billing_street" type="text" required value="{{ form_data.billing_street }}">
|
|
</div>
|
|
<div class="booking-review__field">
|
|
<label for="billing_zip">PLZ *</label>
|
|
<input id="billing_zip" name="billing_zip" type="text" required value="{{ form_data.billing_zip }}">
|
|
</div>
|
|
<div class="booking-review__field">
|
|
<label for="billing_city">Ort *</label>
|
|
<input id="billing_city" name="billing_city" type="text" required value="{{ form_data.billing_city }}">
|
|
</div>
|
|
<div class="booking-review__field booking-review__field--full">
|
|
<label for="billing_reference">Kassenzeichen / Haushaltsstelle / Aktenzeichen <span style="font-weight: normal; color: #5e757d;">(Optional)</span></label>
|
|
<input id="billing_reference" name="billing_reference" type="text" placeholder="Wichtig für die Zahlungsabwicklung der Stadt" value="{{ form_data.billing_reference }}">
|
|
</div>
|
|
|
|
<div class="booking-review__section-title">3. Technische Einrichtung</div>
|
|
<div class="booking-review__field booking-review__field--full">
|
|
<label for="tenant_slug">Gewünschte Schul-URL *</label>
|
|
<div class="input-group--subdomain">
|
|
<input id="tenant_slug" name="tenant_slug" type="text" required pattern="[a-z0-9-]+" title="Nur Kleinbuchstaben, Zahlen und Bindestriche erlaubt" placeholder="gymnasium-neustadt" value="{{ form_data.tenant_slug }}">
|
|
<span class="domain-suffix">.invario-software.de</span>
|
|
</div>
|
|
<div id="tenant_slug_feedback" style="margin-top:0.4rem; font-size:0.9rem; font-weight:600;"></div>
|
|
</div>
|
|
|
|
<div class="booking-review__field booking-review__field--full booking-review__consent">
|
|
<label for="agreements_accepted">
|
|
<input id="agreements_accepted" name="agreements_accepted" type="checkbox" required>
|
|
<span>
|
|
Ich habe die <a href="{{ url_for('nutzungsbedingungen') }}" target="_blank" rel="noopener" style="color: #1a68d0; font-weight: 700;">AGB (PDF)</a>,
|
|
die <a href="{{ url_for('vertrag') }}" target="_blank" rel="noopener" style="color: #1a68d0; font-weight: 700;">Vertragsbedingungen</a> sowie den gesetzlich erforderlichen
|
|
<a href="{{ url_for('avv') }}" target="_blank" rel="noopener" style="color: #1a68d0; font-weight: 700;">Auftragsverarbeitungsvertrag (AVV)</a> gelesen und stimme diesen Dokumenten verbindlich zu.
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="form-error-alert" style="display:none; color:#b02a37; margin-top:1rem; font-weight:bold;"></div>
|
|
|
|
<div class="booking-review__actions">
|
|
<button id="booking_submit" class="btn" type="submit">Kostenpflichtig buchen & Instanz starten</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- STATE 2: LOADING (Fixiert auf 1:30 Min. / 90s Ablauf) -->
|
|
<div id="setup-loading-state">
|
|
<h2>Instanz wird bereitgestellt</h2>
|
|
<p>Ihre isolierte Umgebung wird eingerichtet, SSL-Zertifikate ausgestellt und Datenbanken initialisiert.</p>
|
|
|
|
<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">Starte Tenant Watcher...</div>
|
|
<p style="margin-top: 1.5rem; color: #5e757d; font-size: 0.9rem;">Bitte lassen Sie dieses Fenster geöffnet. Die automatische Einrichtung dauert ca. 1:30 Minuten.</p>
|
|
</div>
|
|
|
|
<!-- STATE 3: SUCCESS (Bereit zur Nutzung) -->
|
|
<div id="setup-success-state">
|
|
<div class="success-icon">✓</div>
|
|
<h2>Ihre Schule ist online!</h2>
|
|
<p>Das Setup wurde erfolgreich abgeschlossen. Ihre Rechnung sowie Zugangsdaten wurden zusätzlich per E-Mail versandt.</p>
|
|
|
|
<div class="warning-text">
|
|
Aus Datenschutzgründen (AVV) müssen Sie das generierte Passwort nach dem ersten Login zwingend ändern!
|
|
</div>
|
|
|
|
<div class="credentials-box">
|
|
<p><strong>Login-URL:</strong> <br><a id="success-url" href="#" target="_blank" style="color: #66d9e8;">-</a></p>
|
|
<p><strong>Benutzername (Admin):</strong> <code id="success-user">admin</code></p>
|
|
<p><strong>Initial-Passwort:</strong> <code id="success-pass">admin123</code></p>
|
|
</div>
|
|
|
|
<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;">
|
|
📄 Rechnung (PDF) herunterladen
|
|
</a>
|
|
<a id="btn-go-to-tenant" href="#" class="btn" target="_blank">Zur Schulplattform wechseln</a>
|
|
<button onclick="window.print();" class="btn secondary" type="button">
|
|
🖨️ Zugangsdaten drucken
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</section>
|
|
|
|
<script>
|
|
(() => {
|
|
const formState = document.getElementById('checkout-form-state');
|
|
const loadingState = document.getElementById('setup-loading-state');
|
|
const successState = document.getElementById('setup-success-state');
|
|
|
|
const form = document.getElementById('checkout-form');
|
|
const errorAlert = document.getElementById('form-error-alert');
|
|
const statusText = document.getElementById('loading-status-text');
|
|
|
|
const progressBar = document.getElementById('provisioning-progress-bar');
|
|
const progressPercent = document.getElementById('provisioning-percent');
|
|
|
|
const slugInput = document.getElementById('tenant_slug');
|
|
const feedback = document.getElementById('tenant_slug_feedback');
|
|
const submitBtn = document.getElementById('booking_submit');
|
|
let slugTimer = null;
|
|
|
|
slugInput.addEventListener('input', (e) => {
|
|
e.target.value = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
clearTimeout(slugTimer);
|
|
slugTimer = setTimeout(() => checkSubdomain(e.target.value), 600);
|
|
});
|
|
|
|
async function checkSubdomain(name) {
|
|
if (name.length < 3) {
|
|
feedback.textContent = 'Bitte mindestens 3 Zeichen eingeben.';
|
|
feedback.style.color = '#5e757d';
|
|
submitBtn.disabled = true;
|
|
return;
|
|
}
|
|
try {
|
|
const res = await fetch(`/instance_request/check_subdomain?slug=${name}`);
|
|
const j = await res.json();
|
|
if (j.available) {
|
|
feedback.textContent = 'Adresse ist frei!';
|
|
feedback.style.color = '#1b8a3e';
|
|
submitBtn.disabled = false;
|
|
} else {
|
|
feedback.textContent = 'Adresse ist bereits vergeben.';
|
|
feedback.style.color = '#b02a37';
|
|
submitBtn.disabled = true;
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
feedback.textContent = 'Prüfung fehlgeschlagen. Bitte erneut versuchen.';
|
|
feedback.style.color = '#b02a37';
|
|
}
|
|
}
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
errorAlert.style.display = 'none';
|
|
|
|
const originalBtnText = submitBtn.textContent;
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = "Wird verarbeitet...";
|
|
|
|
const formData = new FormData(form);
|
|
const tenantSlug = formData.get('tenant_slug');
|
|
|
|
try {
|
|
const response = await fetch(form.action, {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: { 'Accept': 'application/json' }
|
|
});
|
|
|
|
const contentType = response.headers.get("content-type");
|
|
let result;
|
|
if (contentType && contentType.includes("application/json")) {
|
|
result = await response.json();
|
|
} else {
|
|
const rawText = await response.text();
|
|
console.error("Server hat kein JSON gesendet:", rawText);
|
|
throw new Error("Ein Serverfehler ist aufgetreten. Bitte prüfen Sie die Konsolenausgabe.");
|
|
}
|
|
|
|
if (!response.ok || !result.success) {
|
|
throw new Error(result.error || 'Es gab ein Problem bei der Buchung.');
|
|
}
|
|
|
|
formState.style.display = 'none';
|
|
loadingState.style.display = 'block';
|
|
|
|
// Startet den fixierten 90 Sekunden Prozess
|
|
startProvisioningProcess(tenantSlug);
|
|
|
|
} catch (error) {
|
|
errorAlert.textContent = error.message;
|
|
errorAlert.style.display = 'block';
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = originalBtnText;
|
|
}
|
|
});
|
|
|
|
function startProvisioningProcess(slug) {
|
|
const totalDurationMs = 90 * 1000; // Exakt 1:30 Minuten (90 Sekunden)
|
|
let startTime = Date.now();
|
|
let backendData = null; // Speichert das finale Backend-Resultat
|
|
let isFailed = false;
|
|
|
|
const statusSteps = [
|
|
{ atMs: 0, msg: "[INFO] Processing trigger: Starting deployment for " + slug + "..." },
|
|
{ atMs: 10000, msg: "[INFO] Checking available resources and allocating network ports..." },
|
|
{ atMs: 20000, msg: "[INFO] Requesting security certificate for " + slug + "..." },
|
|
{ atMs: 30000, msg: "[INFO] Deploying certificate to proxy layer. Restarting service..." },
|
|
{ atMs: 40000, msg: "[INFO] Service containers recreated. Waiting for backend dependencies..." },
|
|
{ atMs: 50000, msg: "[INFO] Initializing storage for " + slug + "... Default admin created." },
|
|
{ atMs: 60000, msg: "[INFO] Configurations updated successfully..." },
|
|
{ atMs: 70000, msg: "[INFO] Rebuilding and restarting application stack..." },
|
|
{ atMs: 80000, msg: "[INFO] Cleaning up temporary files and orphan processes..." }
|
|
];
|
|
|
|
// 1. Hintergrund-Polling: Fragt den echten Status ab, OHNE die UI abzubrechen
|
|
const checkBackend = async () => {
|
|
if (isFailed) return;
|
|
try {
|
|
const res = await fetch(`/instance_request/status?slug=${slug}`);
|
|
const data = await res.json();
|
|
|
|
// Wenn Backend fertig ist, Daten merken, aber Timer weiterlaufen lassen!
|
|
if (data.status === 'ready' || data.status === 'active') {
|
|
backendData = data;
|
|
} else if (data.status === 'failed' || data.status === 'error') {
|
|
isFailed = true;
|
|
handleError("Fehler bei der Bereitstellung auf dem Server gemeldet.");
|
|
} else {
|
|
// Wenn noch nicht fertig, in 4 Sekunden nochmal fragen
|
|
if (!backendData) setTimeout(checkBackend, 4000);
|
|
}
|
|
} catch (err) {
|
|
if (!backendData) setTimeout(checkBackend, 4000);
|
|
}
|
|
};
|
|
|
|
// Starte das Polling nach 5 Sekunden
|
|
setTimeout(checkBackend, 5000);
|
|
|
|
// 2. Der strikte visuelle 90-Sekunden Timer (läuft in einer Endlosschleife bis die 90s voll sind)
|
|
const uiInterval = setInterval(() => {
|
|
if (isFailed) {
|
|
clearInterval(uiInterval);
|
|
return;
|
|
}
|
|
|
|
let elapsedMs = Date.now() - startTime;
|
|
|
|
// A. Fortschrittsbalken berechnen (geht maximal bis 99% während der Wartezeit)
|
|
let currentPercent = Math.min(99, Math.floor((elapsedMs / totalDurationMs) * 100));
|
|
progressBar.style.width = currentPercent + '%';
|
|
progressPercent.textContent = currentPercent + '%';
|
|
|
|
// B. Den passenden Text basierend auf der vergangenen Zeit heraussuchen
|
|
let currentStep = statusSteps.slice().reverse().find(step => elapsedMs >= step.atMs);
|
|
if (currentStep && statusText.textContent !== currentStep.msg) {
|
|
statusText.textContent = currentStep.msg;
|
|
}
|
|
|
|
// C. Die vollen 90 Sekunden sind abgelaufen
|
|
if (elapsedMs >= totalDurationMs) {
|
|
clearInterval(uiInterval); // Timer stoppen
|
|
|
|
if (backendData) {
|
|
// Backend ist fertig -> Abschluss initiieren
|
|
finishProcess(backendData);
|
|
} else {
|
|
// Fallback: Falls das Backend nach 90s ausnahmsweise noch lädt
|
|
statusText.textContent = "Warte auf finale Bestätigung des Servers (dies kann noch einen kurzen Moment dauern)...";
|
|
const waitInterval = setInterval(() => {
|
|
if (backendData) {
|
|
clearInterval(waitInterval);
|
|
finishProcess(backendData);
|
|
} else if (isFailed) {
|
|
clearInterval(waitInterval);
|
|
}
|
|
}, 2000);
|
|
}
|
|
}
|
|
}, 100); // 100ms Interval für sehr weiche Animation des Balkens
|
|
|
|
// 3. Abschluss-Funktion (wird nur aufgerufen, wenn 90s vorbei SIND und Backend ready IST)
|
|
function finishProcess(data) {
|
|
progressBar.style.width = '100%';
|
|
progressBar.style.background = '#1b8a3e';
|
|
progressPercent.textContent = '100%';
|
|
statusText.textContent = "[SUCCESS] System erfolgreich neu gestartet für " + slug + ".";
|
|
statusText.style.color = '#1b8a3e';
|
|
|
|
// Nach 1,5 Sekunden auf den Success-Screen wechseln
|
|
setTimeout(() => {
|
|
showSuccessState(
|
|
data.url || `https://${slug}.invario-software.de`,
|
|
data.username || "admin",
|
|
data.password || "admin123",
|
|
data.invoice_url
|
|
);
|
|
}, 1500);
|
|
}
|
|
|
|
// 4. Fehler-Behandlung (bricht sofort ab)
|
|
function handleError(message) {
|
|
clearInterval(uiInterval);
|
|
statusText.textContent = message;
|
|
statusText.style.color = '#b02a37';
|
|
progressBar.style.background = '#b02a37';
|
|
|
|
if (!document.getElementById('retry-btn')) {
|
|
statusText.insertAdjacentHTML('afterend', '<br><button id="retry-btn" onclick="window.location.reload();" class="btn secondary" style="margin-top:1.5rem;">Seite neu laden</button>');
|
|
}
|
|
}
|
|
}
|
|
|
|
function showSuccessState(url, user, pass, invoiceUrl) {
|
|
loadingState.style.display = 'none';
|
|
successState.style.display = 'block';
|
|
|
|
document.getElementById('success-url').textContent = url;
|
|
document.getElementById('success-url').href = url;
|
|
document.getElementById('success-user').textContent = user;
|
|
document.getElementById('success-pass').textContent = pass;
|
|
document.getElementById('btn-go-to-tenant').href = url;
|
|
|
|
const invoiceBtn = document.getElementById('btn-download-invoice');
|
|
if (invoiceUrl) {
|
|
invoiceBtn.href = invoiceUrl;
|
|
invoiceBtn.style.display = 'inline-flex';
|
|
}
|
|
}
|
|
})();
|
|
</script>
|
|
{% endblock %} |