Fix: Installer + Wizard Step 5 robuster gegen IPv6/SSL-Fehler

- installer.sh: mailwolt-apply-domains mit 3-Phasen certbot (HTTP → LE → SSL),
  IPv6-Check vor certbot, Zertifikat-Ablauf-Check (10 Tage), Version-Datei schreiben
- WizardDomains: noipv6-Status aus Helper-Output erkennen
- Wizard: retryDomains()-Methode für Wiederholung ohne neuen Wizard-Durchlauf
- Step 5 Blade: Hints pro Fehlerstatus, Retry-Button, "Trotzdem zum Login"
- UpdatePage: Version aus Datei, Fallback auf git describe (kein "dev" mehr)
- UpdatePage: refreshLowLevelState behandelt fehlende State-Datei als idle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
boban 2026-04-24 13:16:42 +02:00
parent 3c8eaa16df
commit 01e7db589a
5 changed files with 260 additions and 72 deletions

View File

@ -67,12 +67,18 @@ class WizardDomains extends Command
$ssl ? 1 : 0, $ssl ? 1 : 0,
)); ));
$helperOk = $out !== null && !str_contains((string) $out, '[x]'); $helperOk = $out !== null && !str_contains((string) $out, '[x]');
$outStr = (string) $out;
foreach (['ui', 'mail', 'webmail'] as $key) { foreach (['ui', 'mail', 'webmail'] as $key) {
$status = file_get_contents(self::STATE_DIR . "/{$key}"); $status = file_get_contents(self::STATE_DIR . "/{$key}");
if ($status === 'running' || $status === 'pending') { if ($status === 'running' || $status === 'pending') {
file_put_contents(self::STATE_DIR . "/{$key}", $helperOk ? 'done' : 'error'); $domain = $domains[$key] ?? '';
if ($domain && str_contains($outStr, "[!] {$domain}:")) {
file_put_contents(self::STATE_DIR . "/{$key}", 'noipv6');
} else {
file_put_contents(self::STATE_DIR . "/{$key}", $helperOk ? 'done' : 'error');
}
} }
} }

View File

@ -182,6 +182,29 @@ class Wizard extends Component
} }
} }
public function retryDomains(): void
{
@unlink(self::STATE_DIR . '/done');
foreach (['ui', 'mail', 'webmail'] as $k) {
file_put_contents(self::STATE_DIR . "/{$k}", 'pending');
}
$this->domainStatus = ['ui' => 'pending', 'mail' => 'pending', 'webmail' => 'pending'];
$this->setupDone = false;
$ssl = (!$this->skipSsl && app()->isProduction()) ? 1 : 0;
$artisan = base_path('artisan');
$cmd = sprintf(
'nohup php %s mailwolt:wizard-domains --ui=%s --mail=%s --webmail=%s --ssl=%d > /dev/null 2>&1 &',
escapeshellarg($artisan),
escapeshellarg($this->ui_domain),
escapeshellarg($this->mail_domain),
escapeshellarg($this->webmail_domain),
$ssl,
);
@shell_exec($cmd);
}
public function goToLogin(): mixed public function goToLogin(): mixed
{ {
return redirect()->route('login')->with('setup_done', true); return redirect()->route('login')->with('setup_done', true);

View File

@ -205,7 +205,7 @@ class UpdatePage extends Component
$rcRaw = @trim(@file_get_contents(self::STATE_DIR . '/rc') ?: ''); $rcRaw = @trim(@file_get_contents(self::STATE_DIR . '/rc') ?: '');
$this->lowState = $state !== '' ? $state : null; $this->lowState = $state !== '' ? $state : null;
$this->running = ($this->lowState !== 'done'); $this->running = ($this->lowState === 'running');
$this->rc = ($this->lowState === 'done' && is_numeric($rcRaw)) ? (int) $rcRaw : null; $this->rc = ($this->lowState === 'done' && is_numeric($rcRaw)) ? (int) $rcRaw : null;
} }
@ -255,16 +255,14 @@ class UpdatePage extends Component
protected function readCurrentVersion(): ?string protected function readCurrentVersion(): ?string
{ {
// Lokal: direkt aus git describe lesen damit Entwicklungsumgebung immer aktuell ist
if (app()->isLocal()) {
$tag = @trim((string) shell_exec('git -C ' . escapeshellarg(base_path()) . ' describe --tags --abbrev=0 2>/dev/null'));
$v = $this->normalizeVersion($tag);
if ($v) return $v;
}
$v = @trim(@file_get_contents(self::VERSION_FILE) ?: ''); $v = @trim(@file_get_contents(self::VERSION_FILE) ?: '');
if ($v !== '') return $v; if ($v !== '') return $v;
// Fallback: git tag (lokal immer, production wenn Datei fehlt)
$tag = @trim((string) shell_exec('git -C ' . escapeshellarg(base_path()) . ' describe --tags --abbrev=0 2>/dev/null'));
$v = $this->normalizeVersion($tag);
if ($v) return $v;
$raw = @trim(@file_get_contents(self::VERSION_FILE_RAW) ?: ''); $raw = @trim(@file_get_contents(self::VERSION_FILE_RAW) ?: '');
if ($raw !== '') return $this->normalizeVersion($raw); if ($raw !== '') return $this->normalizeVersion($raw);

View File

@ -583,20 +583,11 @@ PHP_FPM_SOCK="/run/php/php${PHPV}-fpm.sock"
APP_DIR="/var/www/mailwolt" APP_DIR="/var/www/mailwolt"
NGINX_SITE="/etc/nginx/sites-available/mailwolt.conf" NGINX_SITE="/etc/nginx/sites-available/mailwolt.conf"
ACME_ROOT="/var/www/letsencrypt"
mkdir -p "${ACME_ROOT}/.well-known/acme-challenge"
# Alle Server-Namen sammeln # --- Phase 1: HTTP-only Vhosts mit ACME-Challenge ---
ALL_NAMES="${UI_HOST} ${WEBMAIL_HOST}" cat > "${NGINX_SITE}" <<CONF
# Zertifikat-Pfade ermitteln (certbot oder self-signed)
if [ "$SSL_AUTO" = "1" ] && [ -f "/etc/letsencrypt/live/${UI_HOST}/fullchain.pem" ]; then
CERT="/etc/letsencrypt/live/${UI_HOST}/fullchain.pem"
KEY="/etc/letsencrypt/live/${UI_HOST}/privkey.pem"
else
CERT="/etc/mailwolt/ssl/cert.pem"
KEY="/etc/mailwolt/ssl/key.pem"
fi
cat > "$NGINX_SITE" <<CONF
server { server {
listen 80; listen 80;
listen [::]:80; listen [::]:80;
@ -605,8 +596,10 @@ server {
root ${APP_DIR}/public; root ${APP_DIR}/public;
index index.php index.html; index index.php index.html;
location /.well-known/acme-challenge/ { root /var/www/html; } location /.well-known/acme-challenge/ {
root ${ACME_ROOT};
try_files \$uri =404;
}
location / { location / {
try_files \$uri \$uri/ /index.php?\$query_string; try_files \$uri \$uri/ /index.php?\$query_string;
} }
@ -617,52 +610,158 @@ server {
location ^~ /livewire/ { location ^~ /livewire/ {
try_files \$uri /index.php?\$query_string; try_files \$uri /index.php?\$query_string;
} }
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)$ {
expires 30d; access_log off;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name ${UI_HOST} ${WEBMAIL_HOST};
ssl_certificate ${CERT};
ssl_certificate_key ${KEY};
ssl_protocols TLSv1.2 TLSv1.3;
root ${APP_DIR}/public;
index index.php index.html;
location / {
try_files \$uri \$uri/ /index.php?\$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:${PHP_FPM_SOCK};
}
location ^~ /livewire/ {
try_files \$uri /index.php?\$query_string;
}
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)$ {
expires 30d; access_log off;
}
} }
CONF CONF
nginx -t && systemctl reload nginx
# --- Phase 2: Let's Encrypt Zertifikate holen ---
# Prüfen ob Server globales IPv6 hat (nötig wenn AAAA-Records existieren)
has_global_ipv6() {
ip -6 addr show scope global 2>/dev/null | grep -q 'inet6'
}
cert_needs_action() {
local domain="$1"
local cert="/etc/letsencrypt/live/${domain}/fullchain.pem"
[ ! -f "${cert}" ] && return 0
# 0 = gültig für >10 Tage → überspringen; 1 = läuft ab → erneuern
openssl x509 -checkend 864000 -noout -in "${cert}" 2>/dev/null && return 1
return 0
}
certbot_safe() {
local domain="$1"
local has_aaaa
has_aaaa=$(dig +short AAAA "${domain}" 2>/dev/null | head -1)
if [ -n "${has_aaaa}" ] && ! has_global_ipv6; then
echo "[!] ${domain}: AAAA-Record vorhanden aber kein IPv6 auf diesem Server — Let's Encrypt würde fehlschlagen. Self-signed wird verwendet." >&2
return 1
fi
certbot certonly --webroot \
-w "${ACME_ROOT}" \
-d "${domain}" \
--non-interactive --agree-tos \
--email "webmaster@${domain}" \
--no-eff-email
}
if [ "${SSL_AUTO}" = "1" ]; then
for DOMAIN in "${UI_HOST}" "${WEBMAIL_HOST}"; do
[ -z "${DOMAIN}" ] && continue
if cert_needs_action "${DOMAIN}"; then
certbot_safe "${DOMAIN}" || true
fi
done
fi
# --- Phase 3: Finale Vhosts (LE-Cert oder self-signed Fallback) ---
(
cat <<CONF
server {
listen 80;
listen [::]:80;
server_name ${UI_HOST} ${WEBMAIL_HOST};
location /.well-known/acme-challenge/ {
root ${ACME_ROOT};
try_files \$uri =404;
}
location / { return 301 https://\$host\$request_uri; }
}
CONF
if [ -n "${UI_HOST}" ]; then
if [ "${SSL_AUTO}" = "1" ] && [ -f "/etc/letsencrypt/live/${UI_HOST}/fullchain.pem" ]; then
CERT_UI="/etc/letsencrypt/live/${UI_HOST}/fullchain.pem"
KEY_UI="/etc/letsencrypt/live/${UI_HOST}/privkey.pem"
else
CERT_UI="/etc/mailwolt/ssl/cert.pem"
KEY_UI="/etc/mailwolt/ssl/key.pem"
fi
cat <<CONF
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name ${UI_HOST};
ssl_certificate ${CERT_UI};
ssl_certificate_key ${KEY_UI};
ssl_protocols TLSv1.2 TLSv1.3;
root ${APP_DIR}/public;
index index.php index.html;
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:${PHP_FPM_SOCK};
}
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)$ { expires 30d; access_log off; }
}
CONF
fi
if [ -n "${WEBMAIL_HOST}" ]; then
if [ "${SSL_AUTO}" = "1" ] && [ -f "/etc/letsencrypt/live/${WEBMAIL_HOST}/fullchain.pem" ]; then
CERT_WM="/etc/letsencrypt/live/${WEBMAIL_HOST}/fullchain.pem"
KEY_WM="/etc/letsencrypt/live/${WEBMAIL_HOST}/privkey.pem"
else
CERT_WM="/etc/mailwolt/ssl/cert.pem"
KEY_WM="/etc/mailwolt/ssl/key.pem"
fi
cat <<CONF
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name ${WEBMAIL_HOST};
ssl_certificate ${CERT_WM};
ssl_certificate_key ${KEY_WM};
ssl_protocols TLSv1.2 TLSv1.3;
root ${APP_DIR}/public;
index index.php index.html;
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:${PHP_FPM_SOCK};
}
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)$ { expires 30d; access_log off; }
}
CONF
fi
) > "${NGINX_SITE}"
nginx -t && systemctl reload nginx nginx -t && systemctl reload nginx
HELPER HELPER
chmod 755 /usr/local/sbin/mailwolt-apply-domains chmod 755 /usr/local/sbin/mailwolt-apply-domains
# ===== Sudoers für www-data (certbot + helper) ===== # ===== Sudoers für www-data (helper) =====
cat > /etc/sudoers.d/mailwolt-certbot <<'SUDOERS' cat > /etc/sudoers.d/mailwolt-certbot <<'SUDOERS'
www-data ALL=(root) NOPASSWD: /usr/bin/certbot www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-apply-domains
SUDOERS SUDOERS
chmod 440 /etc/sudoers.d/mailwolt-certbot chmod 440 /etc/sudoers.d/mailwolt-certbot
# git safe.directory damit spätere pulls als root möglich sind # git safe.directory damit spätere pulls als root möglich sind
git config --global --add safe.directory "${APP_DIR}" || true git config --global --add safe.directory "${APP_DIR}" || true
# ===== Version-Datei schreiben =====
mkdir -p /var/lib/mailwolt
GIT_TAG="$(sudo -u "$APP_USER" -H bash -lc "git -C ${APP_DIR} describe --tags --abbrev=0 2>/dev/null || echo ''")"
if [ -n "$GIT_TAG" ]; then
echo "${GIT_TAG#v}" > /var/lib/mailwolt/version
echo "$GIT_TAG" > /var/lib/mailwolt/version_raw
log "Version: ${GIT_TAG}"
else
warn "Kein Git-Tag gefunden — Version-Datei wird nicht geschrieben"
fi
# ===== App-User/Gruppen & Rechte (am ENDE ausführen) ===== # ===== App-User/Gruppen & Rechte (am ENDE ausführen) =====
# User anlegen (nur falls noch nicht vorhanden) + Passwort setzen + Gruppe # User anlegen (nur falls noch nicht vorhanden) + Passwort setzen + Gruppe
if ! id -u "$APP_USER" >/dev/null 2>&1; then if ! id -u "$APP_USER" >/dev/null 2>&1; then

View File

@ -167,6 +167,11 @@
<div wire:poll.2s="pollSetup"></div> <div wire:poll.2s="pollSetup"></div>
@endif @endif
@php
$anyFailed = collect($domainStatus)->contains(fn($s) => in_array($s, ['error','nodns','noipv6']));
$allDone = $setupDone;
@endphp
<div style="margin-bottom:20px"> <div style="margin-bottom:20px">
<div style="font-size:15px;font-weight:600;color:var(--mw-t1)">Domains werden eingerichtet</div> <div style="font-size:15px;font-weight:600;color:var(--mw-t1)">Domains werden eingerichtet</div>
<div style="font-size:12px;color:var(--mw-t4);margin-top:3px">SSL-Zertifikate werden beantragt und Nginx wird konfiguriert</div> <div style="font-size:12px;color:var(--mw-t4);margin-top:3px">SSL-Zertifikate werden beantragt und Nginx wird konfiguriert</div>
@ -175,33 +180,90 @@
@php @php
$labels = ['ui' => $ui_domain, 'mail' => $mail_domain, 'webmail' => $webmail_domain]; $labels = ['ui' => $ui_domain, 'mail' => $mail_domain, 'webmail' => $webmail_domain];
$statusConfig = [ $statusConfig = [
'pending' => ['icon' => '…', 'color' => 'var(--mw-t5)', 'bg' => 'var(--mw-bg3)', 'label' => 'Warte …'], 'pending' => [
'running' => ['icon' => '↻', 'color' => '#7dd3fc', 'bg' => 'rgba(14,165,233,.08)', 'label' => 'Wird registriert …', 'spin' => true], 'icon' => '…', 'color' => 'var(--mw-t5)', 'bg' => 'var(--mw-bg3)',
'done' => ['icon' => '✓', 'color' => 'rgba(34,197,94,.9)', 'bg' => 'rgba(34,197,94,.07)', 'label' => 'Abgeschlossen'], 'label' => 'Warte …', 'hint' => null,
'nodns' => ['icon' => '!', 'color' => '#fbbf24', 'bg' => 'rgba(251,191,36,.07)', 'label' => 'Kein DNS-Eintrag gefunden'], ],
'error' => ['icon' => '✗', 'color' => '#f87171', 'bg' => 'rgba(239,68,68,.07)', 'label' => 'Fehler bei Registrierung'], 'running' => [
'skip' => ['icon' => '', 'color' => 'var(--mw-t4)', 'bg' => 'var(--mw-bg3)', 'label' => 'SSL übersprungen'], 'icon' => '↻', 'color' => '#7dd3fc', 'bg' => 'rgba(14,165,233,.08)',
'label' => 'Wird registriert …', 'spin' => true, 'hint' => null,
],
'done' => [
'icon' => '✓', 'color' => 'rgba(34,197,94,.9)', 'bg' => 'rgba(34,197,94,.07)',
'label' => 'SSL-Zertifikat ausgestellt', 'hint' => null,
],
'nodns' => [
'icon' => '!', 'color' => '#fbbf24', 'bg' => 'rgba(251,191,36,.07)',
'label' => 'Kein DNS-Eintrag gefunden',
'hint' => 'Der A-Record dieser Domain zeigt nicht auf diesen Server oder ist noch nicht propagiert. DNS-Einstellungen prüfen und danach Retry klicken.',
'hints_extra' => ['DNS A-Record auf Server-IP setzen', 'DNS-Propagierung abwarten (bis zu 24h)', 'Mit dig +short A domain.com prüfen'],
],
'noipv6' => [
'icon' => '!', 'color' => '#fbbf24', 'bg' => 'rgba(251,191,36,.07)',
'label' => 'IPv6 nicht konfiguriert',
'hint' => 'Die Domain hat einen AAAA-Record, aber dieser Server hat kein aktives IPv6. Let\'s Encrypt prüft alle DNS-Records.',
'hints_extra' => ['IPv6 am Server aktivieren', 'ODER: AAAA-Record aus dem DNS entfernen'],
],
'error' => [
'icon' => '✗', 'color' => '#f87171', 'bg' => 'rgba(239,68,68,.07)',
'label' => 'SSL-Zertifikat fehlgeschlagen',
'hint' => 'Let\'s Encrypt konnte die Domain nicht verifizieren. Self-signed Zertifikat wird verwendet.',
'hints_extra' => ['Port 80 muss von außen erreichbar sein', 'Firewall-Regeln prüfen (ufw allow 80)', 'AAAA-Record ohne IPv6 am Server entfernen', 'http://domain/.well-known/acme-challenge/ im Browser testen'],
],
'skip' => [
'icon' => '', 'color' => 'var(--mw-t4)', 'bg' => 'var(--mw-bg3)',
'label' => 'SSL übersprungen', 'hint' => null,
],
]; ];
@endphp @endphp
<div style="display:flex;flex-direction:column;gap:10px"> <div style="display:flex;flex-direction:column;gap:8px">
@foreach(['ui' => 'UI Domain', 'mail' => 'Mail Domain', 'webmail' => 'Webmail Domain'] as $key => $typeLabel) @foreach(['ui' => 'UI Domain', 'mail' => 'Mail Domain', 'webmail' => 'Webmail Domain'] as $key => $typeLabel)
@php @php
$st = $domainStatus[$key] ?? 'pending'; $st = $domainStatus[$key] ?? 'pending';
$cfg = $statusConfig[$st] ?? $statusConfig['pending']; $cfg = $statusConfig[$st] ?? $statusConfig['pending'];
@endphp @endphp
<div style="display:flex;align-items:center;gap:12px;padding:12px 14px;background:{{ $cfg['bg'] }};border-radius:8px;border:1px solid var(--mw-b2)"> <div style="border-radius:8px;border:1px solid var(--mw-b2);overflow:hidden">
<div style="width:28px;height:28px;border-radius:50%;background:{{ $cfg['bg'] }};border:1px solid {{ $cfg['color'] }};display:flex;align-items:center;justify-content:center;font-size:13px;color:{{ $cfg['color'] }};flex-shrink:0;{{ isset($cfg['spin']) ? 'animation:spin .9s linear infinite' : '' }}"> <div style="display:flex;align-items:center;gap:12px;padding:12px 14px;background:{{ $cfg['bg'] }}">
{{ $cfg['icon'] }} <div style="width:28px;height:28px;border-radius:50%;background:{{ $cfg['bg'] }};border:1px solid {{ $cfg['color'] }};display:flex;align-items:center;justify-content:center;font-size:13px;color:{{ $cfg['color'] }};flex-shrink:0;{{ isset($cfg['spin']) ? 'animation:spin .9s linear infinite' : '' }}">
{{ $cfg['icon'] }}
</div>
<div style="flex:1;min-width:0">
<div style="font-size:11px;color:var(--mw-t4)">{{ $typeLabel }}</div>
<div style="font-size:12.5px;color:var(--mw-t1);font-family:monospace;margin-top:1px">{{ $labels[$key] }}</div>
</div>
<span style="font-size:11.5px;color:{{ $cfg['color'] }};white-space:nowrap">{{ $cfg['label'] }}</span>
</div> </div>
<div style="flex:1;min-width:0"> @if(!empty($cfg['hint']))
<div style="font-size:11px;color:var(--mw-t4)">{{ $typeLabel }}</div> <div style="padding:10px 14px;background:rgba(0,0,0,.15);border-top:1px solid var(--mw-b2)">
<div style="font-size:12.5px;color:var(--mw-t1);font-family:monospace;margin-top:1px">{{ $labels[$key] }}</div> <div style="font-size:11.5px;color:var(--mw-t3);line-height:1.5;margin-bottom:{{ !empty($cfg['hints_extra']) ? '8px' : '0' }}">
{{ $cfg['hint'] }}
</div>
@if(!empty($cfg['hints_extra']))
<ul style="margin:0;padding-left:14px;display:flex;flex-direction:column;gap:3px">
@foreach($cfg['hints_extra'] as $hint)
<li style="font-size:11px;color:var(--mw-t4);line-height:1.4">{{ $hint }}</li>
@endforeach
</ul>
@endif
</div> </div>
<span style="font-size:11.5px;color:{{ $cfg['color'] }}">{{ $cfg['label'] }}</span> @endif
</div> </div>
@endforeach @endforeach
</div> </div>
@if($allDone && $anyFailed)
<div style="margin-top:16px;padding:12px 14px;background:rgba(239,68,68,.05);border:1px solid rgba(239,68,68,.2);border-radius:8px">
<div style="font-size:12px;color:var(--mw-t3);margin-bottom:10px">
Einige Domains konnten nicht vollständig eingerichtet werden. Du kannst es erneut versuchen oder mit Self-signed Zertifikat fortfahren.
</div>
<button wire:click="retryDomains" wire:loading.attr="disabled" style="display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border-radius:7px;font-size:12px;font-weight:500;cursor:pointer;border:1px solid rgba(99,102,241,.4);background:rgba(99,102,241,.12);color:#a5b4fc">
<svg wire:loading.remove wire:target="retryDomains" width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M10 6A4 4 0 1 1 8.5 2.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><path d="M8.5 1v2h2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
<svg wire:loading wire:target="retryDomains" width="12" height="12" viewBox="0 0 12 12" fill="none" style="animation:spin .7s linear infinite"><path d="M10 6A4 4 0 1 1 6 2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Erneut versuchen
</button>
</div>
@endif
@endif @endif
</div> </div>
@ -234,7 +296,7 @@
<div style="display:flex;justify-content:flex-end;margin-top:20px"> <div style="display:flex;justify-content:flex-end;margin-top:20px">
<button wire:click="goToLogin" class="mbx-btn-primary" style="font-size:12.5px;width:fit-content"> <button wire:click="goToLogin" class="mbx-btn-primary" style="font-size:12.5px;width:fit-content">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 6.5l2.5 2.5 5.5-5.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 6.5l2.5 2.5 5.5-5.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
Zum Login {{ collect($domainStatus)->contains(fn($s) => in_array($s, ['error','nodns','noipv6'])) ? 'Trotzdem zum Login' : 'Zum Login' }}
</button> </button>
</div> </div>
@endif @endif