<?php
/**
 * @package     com_htprotect
 * @copyright   (C) 2026 Pascal Lohmann / website-bereinigung.de
 * @license     GNU General Public License version 2 or later
 *
 * Darstellung des Auto-Update-Bereichs (Schalter, Liste verfuegbarer Updates mit
 * Status, Ein-Klick-Rollback je kuerzlich aktualisierter Erweiterung, Log,
 * ehrliche Grenzen-Hinweise). KEINE Logik - reine Darstellung; alle Daten kommen
 * vorbereitet aus HtpAutoUpdate::viewData(). Wird vom Bereich "Backups & Updates"
 * (HtpUi::secBackups) per einem Aufruf eingebunden, gegated wo Auto-Update greift
 * (5.4+ oder Joomla 3, vgl. HtpUi::backupsUpdates / HtpAutoUpdate::execEligible).
 *
 * Nutzt dieselben htpx-CSS-Klassen wie der Hauptrenderer, bleibt aber bewusst
 * eine eigenstaendige Klasse (kein Zugriff auf private HtpUi-Methoden).
 */

defined('_JEXEC') or die;

class HtpAutoUpdateUi
{
    /**
     * Auto-Update-Sektion ausgeben.
     *
     * @param   array  $cfg   Konfiguration
     * @param   array  $data  Anzeigedaten (erwartet $data['autoupdate'] aus viewData)
     *
     * @return void
     */
    public static function render($cfg, $data)
    {
        $au = isset($data['autoupdate']) && is_array($data['autoupdate']) ? $data['autoupdate'] : array();

        $notify    = !empty($au['notify']);
        $enabled   = !empty($au['enabled']);
        $all       = !empty($au['all']);
        $self      = !empty($au['self']); // HTProtect selbst sofort aktuell halten
        $manual    = !empty($au['manual']);
        $mailOk    = !empty($au['mail_success']);
        $malscan   = !isset($au['malscan']) || !empty($au['malscan']); // Default AN
        $keepSites = !isset($au['keep_sites']) || !empty($au['keep_sites']); // Update-Quellen aktiv halten, Default AN
        $keepList  = isset($au['keep_list']) && is_array($au['keep_list']) ? $au['keep_list'] : array(); // Quellen fuer den Ausnahmen-Dialog
        $keep      = isset($au['keep']) ? (int) $au['keep'] : 2;
        $schedule  = isset($au['schedule']) ? (string) $au['schedule'] : 'daily';
        $hour      = isset($au['hour']) ? (int) $au['hour'] : 21;
        $weekday   = isset($au['weekday']) ? (int) $au['weekday'] : 5;
        $grace     = isset($au['grace']) ? (int) $au['grace'] : 3;
        $skipMajor = !empty($au['skip_major']); // grosse Versionsspruenge (3.x->4.x) nicht auto-installieren
        $updates   = isset($au['updates']) && is_array($au['updates']) ? $au['updates'] : array();
        $vulnNames = isset($au['vuln_names']) && is_array($au['vuln_names']) ? $au['vuln_names'] : array();
        $fpm       = !isset($au['fpm']) || !empty($au['fpm']);
        $nextRun   = isset($au['next_run']) ? (int) $au['next_run'] : 0;
        $schedNext = isset($au['sched_next']) ? (int) $au['sched_next'] : 0;
        $rollbacks = isset($au['rollbacks']) && is_array($au['rollbacks']) ? $au['rollbacks'] : array();
        $history   = isset($au['history']) && is_array($au['history']) ? $au['history'] : array();
        $pending   = isset($au['pending_count']) ? (int) $au['pending_count'] : 0;
        $job       = (isset($au['job']) && is_array($au['job'])) ? $au['job'] : null;
        $lastRun   = isset($au['last_run']) ? (int) $au['last_run'] : 0;

        // Scoped CSS (einmalig, nutzt die Theme-Variablen): kompaktere Rollback-Tabelle
        // (weniger Hoehe) + schicker, runder Ausklapp-Button fuers Aktivitaetsprotokoll.
        echo '<style>'
            . '@media(min-width:681px){.htpau-rb td,.htpau-list td{padding:4px 10px}}'
            . '.htpau-list td{vertical-align:middle}.htpau-list .htpx-tg{padding:0}'
            . '.htpau-rbform{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0}'
            . '.htpau-rbform .htpx-au-dblabel{margin:0;font-size:.78rem;color:var(--dim);display:inline-flex;align-items:center;gap:5px}'
            . '.htpau-logmore{margin-top:10px}'
            . '.htpau-logmore>summary{list-style:none;display:flex;align-items:center;justify-content:center;gap:8px;'
            . 'padding:5px 16px;border:1px solid var(--line);border-radius:99px;background:var(--panel2);color:var(--dim);'
            . 'font-size:.82rem;font-weight:600;cursor:pointer;transition:all .15s;user-select:none}'
            . '.htpau-logmore>summary:hover{color:var(--acc);border-color:rgba(52,210,123,.5);background:rgba(52,210,123,.07)}'
            . '.htpau-logmore>summary::-webkit-details-marker{display:none}.htpau-logmore>summary::marker{content:""}'
            . '.htpau-chev{transition:transform .2s;font-size:.7em;opacity:.85}'
            . '.htpau-logmore[open]>summary .htpau-chev{transform:rotate(180deg)}'
            . '.htpau-logmore .htpau-less{display:none}.htpau-logmore[open] .htpau-more{display:none}.htpau-logmore[open] .htpau-less{display:inline}'
            . '.htpau-logmore>ul{margin-top:6px}.htpau-logmore>table{margin-top:6px}'
            // "Auswahl"-Unterblock optisch als zusammengehoerige Gruppe abheben.
            . '.htpau-auswahl{margin:2px 0 12px 30px;padding:12px 16px 4px;border:1px solid var(--line);'
            . 'border-left:3px solid var(--acc);border-radius:0 10px 10px 0;background:rgba(52,210,123,.035)}'
            . '.htpau-critnote{margin:0 0 14px 30px;padding:9px 14px;border:1px solid #c47f0a;'
            . 'border-left:3px solid #c47f0a;border-radius:0 8px 8px 0;background:rgba(214,140,16,.09);'
            . 'color:var(--text);font-size:.9rem}'
            // Mobil: 30px-Einrueckung der Auswahl-/Hinweis-Bloecke aufheben -> volle
            // Breite, besserer Platz am Handy (Desktop behaelt die Hierarchie-Einrueckung).
            . '@media(max-width:680px){.htpau-auswahl,.htpau-critnote{margin-left:0}}'
            // Ausnahmen-Dialog (Modal) - theme-treu ueber die HtpUi-Variablen.
            . '.htpx-kx-open{font-size:.85em;font-weight:700;text-decoration:none;white-space:nowrap;cursor:pointer}'
            . '.htpx-modal{position:fixed;inset:0;z-index:10000;display:flex;align-items:center;justify-content:center;padding:18px}'
            . '.htpx-modal[hidden]{display:none}'
            . '.htpx-modal-back{position:absolute;inset:0;background:rgba(6,9,15,.6);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}'
            . '.htpx-modal-box{position:relative;z-index:1;display:flex;flex-direction:column;width:100%;max-width:540px;max-height:84vh;background:var(--panel);color:var(--text);border:1px solid var(--line);border-radius:16px;box-shadow:0 24px 64px rgba(0,0,0,.45);overflow:hidden;animation:htpKxIn .18s cubic-bezier(.2,.8,.2,1)}'
            . '@keyframes htpKxIn{from{opacity:0;transform:translateY(14px) scale(.985)}to{opacity:1;transform:none}}'
            . '.htpx-modal-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:15px 20px;border-bottom:1px solid var(--line)}'
            . '.htpx-modal-head h3{margin:0;font-size:1.08rem;font-weight:800;color:var(--text)}'
            . '.htpx-modal-x{border:0;background:transparent;color:var(--dim);font-size:1.7rem;line-height:1;cursor:pointer;padding:0 4px;border-radius:8px}'
            . '.htpx-modal-x:hover{color:var(--text)}'
            . '.htpx-modal-scroll{overflow-y:auto;padding:14px 20px}'
            . '.htpx-kx-intro{margin:0 0 10px;font-size:.9rem;line-height:1.5}'
            . '.htpx-kx-list{list-style:none;margin:0;padding:0}'
            . '.htpx-kx-item{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:11px 2px;border-top:1px solid var(--line)}'
            . '.htpx-kx-item:first-child{border-top:0}'
            . '.htpx-kx-name{font-weight:600;min-width:0;overflow-wrap:anywhere}'
            . '.htpx-kx-tglbl{font-size:.82rem;color:var(--dim);white-space:nowrap}'
            . '.htpx-kx-lock{white-space:nowrap}'
            . '.htpx-modal-foot{display:flex;gap:10px;padding:14px 20px;border-top:1px solid var(--line);background:var(--panel2)}'
            . '@media(max-width:520px){.htpx-kx-item{flex-wrap:wrap;gap:6px}}'
            // 3-Stufen-Schalter je Erweiterung (Nie | Nur Sicherheitsupdates | Immer). Schlank: inaktive
            // Segmente nur als Symbol, das AKTIVE Segment zeigt zusaetzlich sein Label ("Label beim
            // Verstellen") - reines CSS, kein JS. Voll tastatur-/touchbar (Radiogruppe).
            . '.htpau-seg{display:inline-flex;align-items:stretch;border:1px solid var(--line);border-radius:8px;'
            . 'overflow:hidden;background:var(--panel2);vertical-align:middle;max-width:100%}'
            . '.htpau-seg input{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;margin:0}'
            . '.htpau-seg label{display:inline-flex;align-items:center;gap:5px;margin:0;padding:5px 9px;min-height:30px;'
            . 'font-size:.8rem;font-weight:600;color:var(--dim);cursor:pointer;line-height:1;white-space:nowrap;'
            . 'border-left:1px solid var(--line);transition:background .12s,color .12s;-webkit-tap-highlight-color:transparent}'
            . '.htpau-seg label:first-of-type{border-left:0}'
            . '.htpau-seg .htpau-seg-i{font-size:.98rem;line-height:1}'
            . '.htpau-seg .htpau-seg-t{display:none}'
            . '.htpau-seg label:hover{color:var(--text);background:rgba(127,127,127,.08)}'
            . '.htpau-seg input:checked+label .htpau-seg-t{display:inline}'
            . '.htpau-seg input.m-sec:checked+label,.htpau-seg input.m-all:checked+label{background:var(--acc);color:#04120b}'
            . '.htpau-seg input.m-never:checked+label{background:#8a94a6;color:#0b0f16}'
            // Light-Mode: aktives Segment (Text UND Symbol - das Icon erbt die color) HELL, genau wie die
            // uebrigen gruenen Buttons (.htpx.htpx-light .htpx-btn -> #fff). Im Dark-Mode bleibt der dunkle
            // Ink-Text (guter Kontrast auf dem helleren Akzentgruen).
            . '.htpx.htpx-light .htpau-seg input.m-sec:checked+label,'
            . '.htpx.htpx-light .htpau-seg input.m-all:checked+label,'
            . '.htpx.htpx-light .htpau-seg input.m-never:checked+label{color:#fff}'
            . '.htpau-seg input:focus-visible+label{outline:2px solid var(--acc);outline-offset:-2px}'
            . '.htpau-segwrap{display:inline-flex;flex-direction:column;gap:4px;align-items:flex-start}'
            . '.htpau-segwarn{font-size:.74rem;font-weight:600;color:#c47f0a;display:flex;align-items:center;gap:4px;max-width:220px;line-height:1.25}'
            . '</style>';

        // ── Schalter-Karte (Anker fuer Sprungmarken bleibt erhalten; eine eigene
        //    Sektions-Ueberschrift entfaellt - der Seitentitel kommt vom Hauptbereich,
        //    sonst Doppelueberschrift). ──
        // EIN Formular umschliesst Schalter-Card UND Auswahl-Liste, damit JEDER gruene
        // Speichern-Button die KOMPLETTE Seite speichert (Schalter + Auswahl) und nicht
        // der eine den anderen ueberschreibt. Der Task sitzt am jeweiligen Button
        // (autoupdate_save / autoupdate_check) - kein verstecktes htp_task-Feld.
        echo '<form method="post" action="' . self::url() . '" class="htpx-form">'
            . self::tokenField();

        echo '<div class="htpx-card htpx-card-wide" id="htp-autoupdate">'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_AU_INTRO') . '</p>';

        // Transparenz statt stiller Unsicherheit: wenn der interne HTTPS-Self-Trigger das
        // eigene Zertifikat nicht verifizieren konnte, klar im Backend warnen.
        if (!empty($au['tls_warn'])) {
            echo '<p class="htpau-critnote">&#9888; ' . htp_t('COM_HTPROTECT_AU_TLSWARN') . '</p>';
        }

        if (!empty($au['reach_warn'])) {
            echo '<p class="htpau-critnote">&#9888; ' . htp_t('COM_HTPROTECT_AU_REACHWARN') . '</p>';
        }

        // ERGAENZUNGS-Schalter (unabhaengig, kein Modus-Umschalter): mailt ueber
        // ausstehende Updates, die Auto-Update NICHT automatisch installiert ("der Rest").
        // Laeuft NEBEN dem Auto-Update; ist dieses ganz aus, ist "der Rest" = alle
        // Ausstehenden (= das fruehere "nur benachrichtigen" als Sonderfall).
        echo self::toggle('htp_autoupdate_notify', $notify ? 1 : 0,
            htp_t('COM_HTPROTECT_AU_NOTIFY'), htp_t('COM_HTPROTECT_AU_NOTIFY_HINT'), 'htpau-notify', '');

        // ── Auto-Install-Steuerungen (laufen NEBEN dem Notify - werden NICHT mehr
        //    ausgeblendet; der Notify meldet nur den nicht automatisch installierten Rest) ──
        echo '<div id="htpau-install">';

        // Schalter 0 (zuoberst): HTProtect SELBST immer sofort aktuell halten - der
        // grundlegendste Schutz-Schalter. Eigener Sofort-Pfad (alle 15 Min, ohne Karenz);
        // per 1-Klick auf der Uebersicht ebenfalls setzbar (spiegelt denselben Zustand).
        // Immer sichtbar (nicht an den Master gekoppelt).
        echo self::toggle('htp_autoupdate_self', $self ? 1 : 0,
            htp_t('COM_HTPROTECT_AU_SELF'), htp_t('COM_HTPROTECT_AU_SELF_HINT'), 'htpau-self', '');

        // Schalter 1: SICHERHEITSUPDATES automatisch installieren (Master).
        echo self::toggle('htp_autoupdate_ext', $enabled ? 1 : 0,
            htp_t('COM_HTPROTECT_AU_MASTER'), htp_t('COM_HTPROTECT_AU_MASTER_HINT'), 'htpau-ext', 'htpauSync()');

        // Transparenz: welche Erweiterungen werden dadurch (immer, sofort) aktualisiert?
        // Sichtbar sobald Schalter 1 an ist (JS), unabhaengig von Schalter 2.
        $affected = empty($vulnNames)
            ? htp_t('COM_HTPROTECT_AU_AFFECTED_NONE')
            : '<strong>' . htp_t('COM_HTPROTECT_AU_AFFECTED') . '</strong> ' . htp_esc(implode(', ', $vulnNames));
        echo '<div id="htpau-affected" class="htpx-dim" style="margin:-4px 0 10px 52px;font-size:.88rem'
            . ($enabled ? '' : ';display:none') . '">' . $affected . '</div>';

        // Schalter 2: WEITERE (ausgewaehlte) Erweiterungen - erscheint on-the-fly,
        // sobald Schalter 1 an ist (server-seitig vorgegeben, JS haelt es live aktuell).
        // Schalter 2 ist absichtlich gegated: zum AKTIVIEREN 7x hintereinander klicken
        // (Insider-Hebel; bis dahin "(coming soon)" dahinter). Deaktivieren = 1 Klick.
        // Darum KEIN onchange hier - die 7-Klick-Logik haengt im JS am Klick.
        echo '<div id="htpau-allrow" class="htpau-auswahl"' . ($enabled ? '' : ' style="display:none"') . '>'
            . self::toggle('htp_autoupdate_all', $all ? 1 : 0,
                htp_t('COM_HTPROTECT_AU_ALL')
                    // Gate bleibt (7-Klick), aber KEIN "(coming soon)" mehr - das Feature ist real.
                    // Der Span bleibt leer; die 7-Klick-Zaehlung (JS) fuellt ihn beim Klicken.
                    . '<span id="htpau-all-soon" data-left="' . htp_esc(htp_t_attr('COM_HTPROTECT_AU_ALL_GATE_LEFT'))
                    . '" style="color:var(--dim);font-style:italic;font-size:.85em'
                    . ($all ? ';display:none' : '') . '"></span>',
                htp_t('COM_HTPROTECT_AU_ALL_HINT'), 'htpau-all', '')
            . self::scheduleBlock($schedule, $hour, $weekday, $all, $grace)
            // Inhalts-Pruefung (Malware-Scan) als zweite Ebene zur Auswahl - dort lauert
            // das hoechste Risiko (Dritt-Updates); greift aber bei ALLEN Auto-Updates.
            . self::toggle('htp_autoupdate_malscan', $malscan ? 1 : 0,
                htp_t('COM_HTPROTECT_AU_MALSCAN'), htp_t('COM_HTPROTECT_AU_MALSCAN_HINT'))
            . '</div>';

        // Optionaler Schalter: auch bei JEDEM erfolgreichen Auto-Update eine Mail
        // (Default AUS; Fehlschlag/Rollback/Host-Wechsel melden ohnehin).
        echo self::toggle('htp_autoupdate_mail_success', $mailOk ? 1 : 0,
            htp_t('COM_HTPROTECT_AU_SUCCESS'), htp_t('COM_HTPROTECT_AU_SUCCESS_HINT'));

        // §7b: eigener Schalter - Backup vor JEDEM Update (auch manuell), getrennt
        // vom Auto-Update-Master, Default AUS.
        echo self::toggle('htp_autoupdate_manual', $manual ? 1 : 0,
            htp_t('COM_HTPROTECT_AU_MANUAL'), htp_t('COM_HTPROTECT_AU_MANUAL_HINT'));

        // Unabhaengiger Schalter: von Joomlas Kern bei temporaeren Haengern selbst-
        // deaktivierte Update-Quellen taeglich wieder aktivieren (EIGENE + Auto-Update-
        // Quellen immer; fremde per Schalter + 3-Runden-Opt-out). Der "(Ausnahmen)"-Link
        // dahinter oeffnet den Dialog und erscheint nur bei AKTIVEM Schalter.
        $keepLabel = htp_t('COM_HTPROTECT_AU_KEEPSITES');

        if (!empty($keepList)) {
            $keepLabel .= ' <a href="#" id="htp-kx-link" class="htpx-kx-open"'
                . ' onclick="htpKxOpen();if(window.event){window.event.stopPropagation();}return false;"'
                . ($keepSites ? '' : ' style="display:none"') . '>(' . htp_t('COM_HTPROTECT_AU_KEEPSITES_EXC') . ')</a>';
        }

        echo self::toggle('htp_keep_update_sites', $keepSites ? 1 : 0,
            $keepLabel, htp_t('COM_HTPROTECT_AU_KEEPSITES_HINT'), 'htpau-keep', 'htpKxSync()');

        // Ausnahmen-Dialog (Modal). Liegt IM aeusseren Formular (Checkboxen + Speichern
        // gehen mit), position:fixed macht es zum Overlay. Nur bei vorhandenen Quellen.
        if (!empty($keepList)) {
            echo '<div class="htpx-modal" id="htp-kx-modal" hidden>'
                . '<div class="htpx-modal-back" onclick="htpKxClose()"></div>'
                . '<div class="htpx-modal-box" role="dialog" aria-modal="true" aria-labelledby="htp-kx-title">'
                . '<div class="htpx-modal-head">'
                . '<h3 id="htp-kx-title">' . htp_t('COM_HTPROTECT_AU_KX_TITLE') . '</h3>'
                . '<button type="button" class="htpx-modal-x" onclick="htpKxClose()" aria-label="' . htp_t_attr('COM_HTPROTECT_AU_KX_CLOSE') . '">&#215;</button>'
                . '</div>'
                . '<div class="htpx-modal-scroll">'
                . '<p class="htpx-dim htpx-kx-intro">' . htp_t('COM_HTPROTECT_AU_KX_INTRO') . '</p>'
                . '<ul class="htpx-kx-list">';

            foreach ($keepList as $it) {
                $ek   = htp_esc((string) $it['extkey']);
                $nm   = htp_esc((string) $it['name']);
                $stat = !empty($it['enabled'])
                    ? self::badge('ok', htp_t('COM_HTPROTECT_AU_KX_ON'))
                    : self::badge('info', htp_t('COM_HTPROTECT_AU_KX_OFF'));

                echo '<li class="htpx-kx-item"><span class="htpx-kx-name">' . $nm . ' ' . $stat . '</span>';

                if (!empty($it['locked'])) {
                    echo '<span class="htpx-kx-lock">' . self::badge('ok', htp_t('COM_HTPROTECT_AU_KX_ALWAYS')) . '</span>';
                } else {
                    echo '<input type="hidden" name="htp_keepexcept_rendered[]" value="' . $ek . '">'
                        . '<label class="htpx-tg htpx-tg-s htpx-kx-tg"><input type="checkbox" name="htp_keepexcept[' . $ek . ']" value="1"'
                        . (!empty($it['except']) ? ' checked' : '') . '><span class="htpx-tgsl"></span>'
                        . '<span class="htpx-kx-tglbl">' . htp_t('COM_HTPROTECT_AU_KX_IGNORE') . '</span></label>';
                }

                echo '</li>';
            }

            echo '</ul></div>'
                . '<div class="htpx-modal-foot">'
                . '<button type="submit" name="htp_task" value="autoupdate_save" class="htpx-btn">' . htp_t('COM_HTPROTECT_AU_KX_SAVE') . '</button>'
                . '<button type="button" class="htpx-btn2" onclick="htpKxClose()">' . htp_t('COM_HTPROTECT_AU_KX_CLOSE') . '</button>'
                . '</div></div></div>';
        }

        // Aufbewahrung (wie viele Vorversionen behalten) - schmale Select-Box,
        // mit Abstand zum Speichern-Button (eigener Block).
        echo '<div style="margin:6px 0 18px">'
            . '<label class="htpx-label" for="htp_autoupdate_keep">' . htp_t('COM_HTPROTECT_AU_KEEP') . '</label>'
            . '<select class="htpx-input htpx-select" id="htp_autoupdate_keep" name="htp_autoupdate_keep"'
            . ' style="width:auto;min-width:72px;display:inline-block">';

        foreach (array(1, 2, 3) as $n) {
            echo '<option value="' . $n . '"' . ($keep === $n ? ' selected' : '') . '>' . $n . '</option>';
        }

        echo '</select></div>';

        echo '</div>'; // #htpau-install Ende (Auto-Install-Steuerungen)

        echo '<p style="margin-top:6px"><button type="submit" name="htp_task" value="autoupdate_save" class="htpx-btn">' . htp_t('COM_HTPROTECT_AU_SAVE_BTN') . '</button></p>';

        // Lieferketten-/Server-Hinweis (ehrliche Grenze, plan Abschnitt 8/9.10).
        echo '<p class="htpx-dim htpx-au-note">' . htp_t('COM_HTPROTECT_AU_SUPPLYCHAIN') . '</p>';

        echo '</div>';

        // ── Verwaltungs-Card "Automatische Updates" - sichtbar, sobald der Sicherheitsupdate-Master
        //    an ist (server-seitig + JS-live). Der 3-Stufen-Schalter je Erweiterung (Nie | Nur
        //    Sicherheitsupdates | Immer) ist auch OHNE "auf alle ausweiten" bedienbar: "Immer" oeffnet
        //    das Gate beim Speichern selbst (Gate-Kopplung in taskSave). Inhalt immer im DOM (versteckt),
        //    damit der Wechsel ohne Speichern/Reload sichtbar wird.
        echo '<div id="htpau-listwrap"' . ($enabled ? '' : ' style="display:none"') . '>';
        self::renderUpdateList($updates, $enabled, $skipMajor);
        echo '</div>';

        // Schliesst das die Schalter-Card + Auswahl-Liste umschliessende Formular.
        echo '</form>';

        // ── Ein-Klick-Rollback ──
        self::renderRollbacks($rollbacks);

        // ── Log ──
        // "Naechster geplanter Lauf" nur zeigen, wenn ausgewaehlte Updates aktiv sind
        // (Schalter 2) - Sicherheitsupdates laufen ohnehin sofort, nicht nach Plan.
        self::renderHistory($history, $lastRun, $all ? $nextRun : 0, $all ? $schedule : '', $schedNext, $pending, $job);

        // On-the-fly-Sichtbarkeit (ohne Speichern/Reload): Schalter 2 + Haeufigkeit
        // erscheinen sobald Schalter 1 an ist; die Verwaltungs-Card + der Zeitplan nur
        // wenn BEIDE an sind; Wochentag/Uhrzeit je nach gewaehlter Haeufigkeit.
        echo '<script>(function(){function byId(x){return document.getElementById(x);}'
            . 'function sched(){var sel=byId("htp_autoupdate_schedule");if(!sel)return;var v=sel.value,'
            . 'wd=byId("htpau-wdwrap"),hr=byId("htpau-hrwrap");if(wd)wd.style.display=(v==="weekly")?"":"none";'
            . 'if(hr)hr.style.display=(v==="daily"||v==="weekly")?"":"none";}'
            . 'function s(){var e=byId("htpau-ext"),a=byId("htpau-all"),ar=byId("htpau-allrow"),'
            . 'af=byId("htpau-affected"),sr=byId("htpau-schedrow"),lw=byId("htpau-listwrap");'
            . 'if(!e)return;var eo=e.checked;'
            . 'if(af)af.style.display=eo?"":"none";if(ar)ar.style.display=eo?"":"none";var ao=eo&&a&&a.checked;'
            . 'if(sr)sr.style.display=ao?"":"none";if(lw)lw.style.display=eo?"":"none";sched();}'
            . 'window.htpauSync=s;window.htpauSched=sched;'
            . 'var _g=byId("htpau-all");if(_g){var _gs=byId("htpau-all-soon"),_gc=0;'
            . 'var _g0=_gs?_gs.textContent:"";var _gt=_gs?(_gs.getAttribute("data-left")||""):"";'
            . '_g.addEventListener("click",function(){if(_g.checked){_gc++;'
            . 'if(_gc<7){_g.checked=false;if(_gs)_gs.textContent=" ("+_gt.replace("%d",7-_gc)+")";}'
            . 'else{_gc=0;if(_gs)_gs.style.display="none";}}'
            . 'else{_gc=0;if(_gs){_gs.style.display="";_gs.textContent=_g0;}}s();});}'
            // Ausnahmen-Dialog: oeffnen/schliessen (ESC + Backdrop) + "(Ausnahmen)"-Link
            // nur bei aktivem "Update-Quellen aktiv halten"-Schalter zeigen.
            . 'function kxEsc(e){if(e.key==="Escape"||e.keyCode===27)kxClose();}'
            . 'function kxOpen(){var m=byId("htp-kx-modal");if(m){m.hidden=false;document.addEventListener("keydown",kxEsc);}}'
            . 'function kxClose(){var m=byId("htp-kx-modal");if(m){m.hidden=true;document.removeEventListener("keydown",kxEsc);}}'
            . 'function kxSync(){var c=byId("htpau-keep"),l=byId("htp-kx-link");if(c&&l)l.style.display=c.checked?"":"none";}'
            . 'window.htpKxOpen=kxOpen;window.htpKxClose=kxClose;window.htpKxSync=kxSync;kxSync();'
            . 's();})();</script>';
    }

    /**
     * Schlanker 3-Stufen-Schalter je Erweiterung: Nie | Nur Sicherheitsupdates (Standard) | Immer.
     * Reine CSS-Radiogruppe (kein JS): inaktive Segmente nur als Symbol, das AKTIVE Segment zeigt sein
     * Label ("Label beim Verstellen" -> bleibt schmal). Sendet htp_au_mode[key]=never|security|all +
     * htp_au_rendered[]=key (Diff-Marker). Bei angreifbarer Erweiterung auf "Nie": deutliche Warnung.
     *
     * @param   string   $key    stabiler Ext-Schluessel (type|element|folder)
     * @param   string   $stage  aktuelle Stufe: 'never'|'security'|'all'
     * @param   boolean  $vuln   Erweiterung ist angreifbar (fuer die "Nie"-Warnung)
     * @param   int      $idx    laufender Index (eindeutige input-ids)
     *
     * @return string
     */
    private static function modeSwitch($key, $stage, $vuln, $idx)
    {
        $kesc = htp_esc((string) $key);
        $base = 'aum' . (int) $idx;

        $seg = function ($cls, $val, $icon, $text, $title) use ($kesc, $base, $stage) {
            $id = $base . '-' . $val;
            $ck = ($stage === $val) ? ' checked' : '';

            return '<input type="radio" class="' . $cls . '" id="' . $id . '" name="htp_au_mode[' . $kesc . ']" value="' . $val . '"' . $ck . '>'
                . '<label for="' . $id . '" title="' . htp_esc($title) . '">'
                . '<span class="htpau-seg-i" aria-hidden="true">' . $icon . '</span>'
                . '<span class="htpau-seg-t">' . htp_esc($text) . '</span></label>';
        };

        $pill = '<input type="hidden" name="htp_au_rendered[]" value="' . $kesc . '">'
            . '<span class="htpau-seg" role="radiogroup" aria-label="' . htp_esc(htp_t('COM_HTPROTECT_AU_MODE_ARIA')) . '">'
            . $seg('m-never', 'never', '&#8709;', htp_t('COM_HTPROTECT_AU_MODE_NEVER'), htp_t('COM_HTPROTECT_AU_MODE_NEVER_TT'))
            . $seg('m-sec', 'security', '&#9960;', htp_t('COM_HTPROTECT_AU_MODE_SEC'), htp_t('COM_HTPROTECT_AU_MODE_SEC_TT'))
            . $seg('m-all', 'all', '&#8635;', htp_t('COM_HTPROTECT_AU_MODE_ALL'), htp_t('COM_HTPROTECT_AU_MODE_ALL_TT'))
            . '</span>';

        if ($vuln && $stage === 'never') {
            return '<span class="htpau-segwrap">' . $pill
                . '<span class="htpau-segwarn"><span aria-hidden="true">&#9888;</span> '
                . htp_t('COM_HTPROTECT_AU_MODE_VULNWARN') . '</span></span>';
        }

        return $pill;
    }

    /**
     * Liste der verfuegbaren Updates mit Status-Badge und Per-Erweiterung-Schalter.
     *
     * @param   array    $updates  Info-Datensaetze aus viewData
     * @param   boolean  $enabled  Master-Schalter an?
     *
     * @return void
     */
    private static function renderUpdateList($updates, $enabled, $skipMajor = false)
    {
        // ALLE verfuegbaren Updates installierter Erweiterungen anzeigen (collectInfos
        // hat das Rauschen mit extension_id 0 - z. B. Joomla-Sprachpakete - bereits
        // entfernt). Auch nicht-auto-updatebare Typen (Pakete) werden gezeigt, damit
        // HTProtect dieselben Updates sieht wie Joomla - nur ohne Auto-Schalter.
        $rows = is_array($updates) ? $updates : array();

        // "Auf Updates pruefen" ist ein Submit-Button im selben (aeusseren) Formular -
        // sein eigener Task autoupdate_check (kein verschachteltes <form>).
        echo '<div class="htpx-card htpx-card-wide" id="htp-au-list">'
            . '<h4 class="htpx-sub">' . htp_t('COM_HTPROTECT_AU_LIST_TITLE') . ' '
            . '<button type="submit" name="htp_task" value="autoupdate_check" formaction="index.php?option=com_htprotect&amp;sec=backups#htp-au-list" class="htpx-btn2 htpx-btn-s" style="margin-left:8px">'
            . htp_t('COM_HTPROTECT_AU_CHECK_BTN') . '</button>'
            . '</h4>';

        if (empty($rows)) {
            echo '<p>' . self::badge('info', htp_t('COM_HTPROTECT_AU_LIST_NONE')) . '</p></div>';

            return;
        }

        // Die Per-Erweiterung-Auswahl liegt im selben (aeusseren) Formular wie die
        // Schalter-Card - kein eigenes <form> hier, damit der Speichern-Button alles
        // gemeinsam speichert (mergeIncludes wertet rendered vs. on aus).
        $tE = htp_t('COM_HTPROTECT_AU_TH_EXT');
        $tV = htp_t('COM_HTPROTECT_AU_TH_VERSION');
        $tS = htp_t('COM_HTPROTECT_AU_TH_STATUS');
        $tA = htp_t('COM_HTPROTECT_AU_TH_AUTO');

        echo '<table class="htpx-table htpx-stack htpau-list"><thead><tr>'
            . '<th>' . $tE . '</th><th>' . $tV . '</th><th>' . $tS . '</th><th>' . $tA . '</th>'
            . '</tr></thead><tbody>';

        $i = 0;

        foreach ($rows as $u) {
            $i++;
            $key    = HtpAutoUpdate::extKey($u['type'], $u['element'], isset($u['folder']) ? $u['folder'] : '');
            $status = isset($u['status']) ? $u['status'] : '';
            $name   = isset($u['name']) ? (string) $u['name'] : (string) $u['element'];
            $vuln   = !empty($u['vulnerable']);

            $verCell = htp_esc((string) $u['installed']);

            if (!empty($u['has_update'])) {
                $verCell .= ' &rarr; <strong>' . htp_esc((string) $u['available']) . '</strong>';
            }

            // Auto-Schalter (3 Stufen: Nie | Nur Sicherheitsupdates | Immer). HTProtect selbst hat einen
            // eigenen Selbst-Update-Schalter (feste Zeile, kein Schalter hier). Pakete u. ae.
            // ('unsupported') sind nicht auto-updatebar -> nur informativ. Alle anderen - AUCH angreifbare -
            // bekommen den Schalter: "Nie" ist ein bewusster Opt-out MIT Warnung (die Luecke wird weiter
            // gemeldet + vom WAF geschuetzt), genau der jDownloads-Fall aus dem Forum.
            if (!empty($u['self'])) {
                // HTProtect selbst wird ueber den eigenen "Selbst-Update"-Schalter (Schalter 0 oben)
                // gesteuert, NICHT ueber diese Auswahlliste. Darum feste Zeile OHNE Schalter - und
                // bewusst OHNE htp_au_rendered, sonst sickerte com_htprotect beim Speichern in die
                // Auswahlliste (autoupdate_include). Das Badge zeigt den Zustand jenes Schalters.
                $autoCell = !empty($u['self_on'])
                    ? self::badge('ok', htp_t('COM_HTPROTECT_AU_AUTO_SELF'))
                    : self::badge('info', htp_t('COM_HTPROTECT_AU_AUTO_SELF_OFF'));
            } elseif ($status === 'unsupported') {
                $autoCell = '<span class="htpx-dim">&mdash;</span>';
            } else {
                // Effektive Stufe direkt aus dem Info-Datensatz: 'never' (autoupdate_never/Template-Default)
                // -> Nie; 'included' -> Immer; sonst -> Nur Sicherheitsupdates (Boden).
                $stage    = !empty($u['never']) ? 'never' : (!empty($u['included']) ? 'all' : 'security');
                $autoCell = self::modeSwitch($key, $stage, $vuln, $i);
            }

            // Bereits INSTALLIERTE Version zentral zurueckgerufen? -> Hinweis-Badge
            // (nur Information; HTProtect rollt nichts automatisch zurueck).
            $recallBadge = !empty($u['recall_inst']) ? ' ' . self::badge('err', htp_t('COM_HTPROTECT_AU_RECALL_BADGE')) : '';

            echo '<tr><td data-label="' . $tE . '"><strong>' . htp_esc($name) . '</strong>'
                . ($vuln ? ' ' . self::badge('err', htp_t('COM_HTPROTECT_AU_VULN_BADGE')) : '')
                . $recallBadge
                . '<br><span class="htpx-mono htpx-dim">' . htp_esc((string) $u['element'])
                . (isset($u['folder']) && $u['folder'] !== '' ? ' (' . htp_esc((string) $u['folder']) . ')' : '') . '</span></td>'
                . '<td data-label="' . $tV . '">' . $verCell . '</td>'
                . '<td data-label="' . $tS . '">' . self::statusBadge($status, $u) . '</td>'
                . '<td data-label="' . $tA . '">' . $autoCell . '</td></tr>';
        }

        echo '</tbody></table>'
            . '<div style="margin-top:14px;display:flex;align-items:flex-start;gap:18px;flex-wrap:wrap">'
            . '<button type="submit" name="htp_task" value="autoupdate_save" formaction="index.php?option=com_htprotect&amp;sec=backups#htp-au-list" class="htpx-btn" style="flex:0 0 auto">' . htp_t('COM_HTPROTECT_AU_SAVE_EXC_BTN') . '</button>'
            // Checkbox rechts daneben: grosse Versionsspruenge (SemVer-Major, z. B. 3.x -> 4.x)
            // von der Auto-Installation ausnehmen. Liegt im selben Formular -> wird mit
            // "Auswahl speichern" gespeichert. Default nicht angehakt (Bestandsverhalten).
            // Marker: signalisiert dem Save, dass die Checkbox gerendert war (sonst wuerde
            // ein Speichern bei leerer Liste den Wert still zuruecksetzen).
            . '<input type="hidden" name="htp_au_skipmajor_shown" value="1">'
            . '<label class="htpx-au-skipmajor" style="flex:1 1 300px;min-width:0;display:flex;align-items:flex-start;gap:9px;cursor:pointer;font-size:.9rem;line-height:1.4">'
            . '<input type="checkbox" name="htp_autoupdate_skip_major" value="1" style="margin-top:6px;flex:0 0 auto"' . ($skipMajor ? ' checked' : '') . '>'
            . '<span><strong>' . htp_t('COM_HTPROTECT_AU_SKIPMAJOR_LABEL') . '</strong>'
            . '<br><span class="htpx-dim" style="font-weight:400">' . htp_t('COM_HTPROTECT_AU_SKIPMAJOR_HINT') . '</span></span>'
            . '</label>'
            . '</div>'
            . '</div>';
    }

    /**
     * Ein-Klick-Rollback je kuerzlich aktualisierter Erweiterung.
     *
     * @param   array  $rollbacks  Rollback-Ziele aus viewData
     *
     * @return void
     */
    private static function renderRollbacks($rollbacks)
    {
        if (empty($rollbacks)) {
            return;
        }

        echo '<div class="htpx-card htpx-card-wide" id="htp-au-rollbacks">'
            . '<h4 class="htpx-sub">' . htp_t('COM_HTPROTECT_AU_RB_TITLE') . '</h4>'
            . '<p class="htpx-dim">' . htp_t('COM_HTPROTECT_AU_RB_INTRO') . '</p>';

        $tE = htp_t('COM_HTPROTECT_AU_TH_EXT');
        $tV = htp_t('COM_HTPROTECT_AU_RB_TH_VERSIONS');
        $tD = htp_t('COM_HTPROTECT_AU_RB_TH_DATE');
        $tA = htp_t('COM_HTPROTECT_BACKUPS_TH_ACTIONS');

        $open = '<table class="htpx-table htpx-stack htpau-rb"><thead><tr>'
            . '<th>' . $tE . '</th><th>' . $tV . '</th><th>' . $tD . '</th><th>' . $tA . '</th>'
            . '</tr></thead><tbody>';

        // Neueste zuerst (viewData sortiert nach Datum absteigend). Erste 8 sichtbar,
        // den Rest (bis 50) in ein Ausklapp-Element - damit die Liste nicht ellenlang
        // wird (wie das Aktivitaetsprotokoll).
        $total = count($rollbacks);
        $cap   = min($total, 50);
        $split = 8;

        echo $open;

        $shown  = 0;
        $opened = false;

        foreach ($rollbacks as $r) {
            if ($shown >= $cap) {
                break;
            }

            if ($shown === $split && !$opened) {
                echo '</tbody></table>'
                    . '<details class="htpau-logmore"><summary>'
                    . '<span class="htpau-chev">&#9662;</span>'
                    . '<span class="htpau-more">' . htp_t('COM_HTPROTECT_AU_LOG_MORE', $cap - $split) . '</span>'
                    . '<span class="htpau-less">' . htp_t('COM_HTPROTECT_AU_LOG_LESS') . '</span>'
                    . '</summary>' . $open;
                $opened = true;
            }

            echo self::rollbackRow($r, $tE, $tV, $tD, $tA);
            $shown++;
        }

        echo '</tbody></table>';

        if ($opened) {
            echo '</details>';
        }

        echo '</div>';
    }

    /**
     * Eine Rollback-Tabellenzeile rendern (fuer sichtbare + ausgeklappte Tabelle).
     *
     * @param   array   $r   Rollback-Ziel
     * @param   string  $tE  Spaltenlabel Erweiterung
     * @param   string  $tV  Spaltenlabel Versionen
     * @param   string  $tD  Spaltenlabel Datum
     * @param   string  $tA  Spaltenlabel Aktionen
     *
     * @return string
     */
    private static function rollbackRow($r, $tE, $tV, $tD, $tA)
    {
        $cur  = (string) $r['current'];
        $prev = (string) $r['previous'];
        $vers = ($cur !== '' ? htp_esc($cur) : '?') . ' &rarr; <strong>' . ($prev !== '' ? htp_esc($prev) : '?') . '</strong>';

        $html = '<tr><td data-label="' . $tE . '"><span class="htpx-mono">' . htp_esc((string) $r['element']) . '</span></td>'
            . '<td data-label="' . $tV . '">' . $vers . '</td>'
            . '<td data-label="' . $tD . '">' . ($r['ts'] > 0 ? htp_esc(htp_local_time((int) $r['ts'], 'd.m.Y H:i')) : '') . '</td>'
            . '<td data-label="' . $tA . '">'
            . '<form method="post" class="htpau-rbform" action="index.php?option=com_htprotect&amp;sec=backups#htp-au-rollbacks" onsubmit="return confirm(\'' . htp_t('COM_HTPROTECT_AU_RB_CONFIRM') . '\');">'
            . self::tokenField()
            . '<input type="hidden" name="htp_task" value="autoupdate_rollback">'
            . '<input type="hidden" name="htp_eid" value="' . (int) $r['eid'] . '">'
            . '<input type="hidden" name="htp_snap" value="' . htp_esc((string) $r['snap']) . '">';

        // DB-Restore nur anbieten, wenn ein DB-Backup vorliegt - mit Warnung
        // (seit dem Update entstandene Daten gehen verloren), default AUS.
        if (!empty($r['has_db'])) {
            $html .= '<label class="htpx-au-dblabel"><input type="checkbox" name="htp_restore_db" value="1"> '
                . htp_t('COM_HTPROTECT_AU_RB_DB') . '</label>';
        }

        $html .= '<button type="submit" class="htpx-btn2 htpx-btn-s">'
            . htp_t('COM_HTPROTECT_AU_RB_BTN', $prev !== '' ? htp_esc($prev) : '?') . '</button>'
            . '</form></td></tr>';

        return $html;
    }

    /**
     * Letzter Lauf + Log der letzten Aktionen.
     *
     * @param   array  $history  Log-Eintraege
     * @param   int    $lastRun  Zeitstempel letzter Hintergrund-Lauf
     *
     * @return void
     */
    private static function renderHistory($history, $lastRun, $nextRun = 0, $schedule = 'weekly', $schedNext = 0, $pending = 0, $job = null)
    {
        echo '<div class="htpx-card htpx-card-wide">'
            . '<h4 class="htpx-sub">' . htp_t('COM_HTPROTECT_AU_LOG_TITLE') . '</h4>';

        echo '<p class="htpx-dim">' . ($lastRun > 0
            ? htp_t('COM_HTPROTECT_AU_LAST_RUN', htp_esc(htp_local_time($lastRun, 'd.m.Y H:i')))
                . ' <a href="index.php?option=com_config#jform_force_ssl-lbl">(' . htp_t('COM_HTPROTECT_AU_TZ_NOTE') . ')</a>'
            : htp_t('COM_HTPROTECT_AU_LAST_RUN_NEVER')) . '</p>';

        // Fortschritt/Timing - EHRLICH: die Verarbeitung ist besucher-getrieben (der
        // Selbst-Trigger ist auf hoechstens alle 15 Min gedrosselt, Joomlas
        // Aufgabenplaner feuert ebenfalls nur auf Requests). Ohne Besucher/echten Cron
        // wartet es -> KEINE "spaetestens"-Schein-Garantie, klare Besucher-Aussage.
        if (is_array($job)) {
            $phaseLabels = array(
                'BACKUP'   => htp_t('COM_HTPROTECT_AU_PHASE_BACKUP'),
                'UPDATE'   => htp_t('COM_HTPROTECT_AU_PHASE_UPDATE'),
                'VERIFY'   => htp_t('COM_HTPROTECT_AU_PHASE_VERIFY'),
                'FINALIZE' => htp_t('COM_HTPROTECT_AU_PHASE_FINALIZE'),
            );
            $ph   = isset($job['phase']) ? (string) $job['phase'] : '';
            $tot  = isset($job['total']) ? (int) $job['total'] : 0;
            $idx  = isset($job['idx']) ? (int) $job['idx'] : 0;
            $name = isset($job['name']) ? trim((string) $job['name']) : '';

            if ($tot > 0 && isset($phaseLabels[$ph])) {
                $line = ($name !== '')
                    ? htp_t('COM_HTPROTECT_AU_JOB_RUNNING_NAMED', htp_esc($name), min($idx + 1, $tot), $tot, $phaseLabels[$ph])
                    : htp_t('COM_HTPROTECT_AU_JOB_RUNNING', min($idx + 1, $tot), $tot, $phaseLabels[$ph]);
            } else {
                $line = htp_t('COM_HTPROTECT_AU_JOB_PREP');
            }

            echo '<p style="margin-top:2px">' . $line . '</p>';
        } elseif ((int) $pending > 0) {
            echo '<p class="htpx-dim" style="margin-top:-6px">' . htp_t('COM_HTPROTECT_AU_PENDING', (int) $pending) . '</p>';
        } elseif ($schedule === 'instant') {
            echo '<p class="htpx-dim" style="margin-top:-6px">' . htp_t('COM_HTPROTECT_AU_IDLE_INSTANT') . '</p>';
        } elseif ((int) $nextRun > 0) {
            echo '<p class="htpx-dim" style="margin-top:-6px">' . htp_t('COM_HTPROTECT_AU_NEXT_RUN', htp_esc(htp_local_time((int) $nextRun, 'd.m.Y, H:i'))) . '</p>';
        }

        if (empty($history)) {
            echo '<p>' . self::badge('info', htp_t('COM_HTPROTECT_AU_LOG_NONE')) . '</p></div>';

            return;
        }

        // Erste 10 sichtbar, den Rest (bis 50) in ein schickes Ausklapp-Element.
        $total = count($history);
        $cap   = min($total, 50);
        $split = 10;

        echo '<ul class="htpx-syslist">';

        $shown  = 0;
        $opened = false;

        foreach ($history as $h) {
            if ($shown >= $cap) {
                break;
            }

            if ($shown === $split && !$opened) {
                echo '</ul><details class="htpau-logmore"><summary>'
                    . '<span class="htpau-chev">&#9662;</span>'
                    . '<span class="htpau-more">' . htp_t('COM_HTPROTECT_AU_LOG_MORE', $cap - $split) . '</span>'
                    . '<span class="htpau-less">' . htp_t('COM_HTPROTECT_AU_LOG_LESS') . '</span>'
                    . '</summary><ul class="htpx-syslist">';
                $opened = true;
            }

            $when   = isset($h['t']) ? htp_local_time((int) $h['t'], 'd.m.Y H:i') : '';
            $name   = isset($h['name']) ? (string) $h['name'] : '';
            $from   = isset($h['from']) ? (string) $h['from'] : '';
            $to     = isset($h['to']) ? (string) $h['to'] : '';
            $result = isset($h['result']) ? (string) $h['result'] : '';

            $verPart = '';

            if ($from !== '' || $to !== '') {
                // Gleiche Version auf beiden Seiten (z. B. Kern-Hotfix, der DIE laufende
                // Version patcht) -> kein irrefuehrender "X -> X"-Pfeil, nur die Version.
                $verPart = ($from !== '' && $to !== '' && $from === $to)
                    ? ' ' . htp_esc($to)
                    : ' ' . htp_esc($from) . ($to !== '' ? ' &rarr; ' . htp_esc($to) : '');
            }

            // Verdichtete Wiederholungen (mehrfach identisch uebersprungen) als "N&times;".
            $n     = isset($h['n']) ? (int) $h['n'] : 1;
            $times = $n > 1 ? ' <span class="htpx-dim">&middot; ' . $n . '&times;</span>' : '';

            echo '<li><span class="htpx-mono htpx-dim">' . htp_esc($when) . '</span> '
                . self::resultBadge($result) . ' '
                . htp_esc($name) . $verPart . $times . '</li>';

            $shown++;
        }

        echo '</ul>';

        if ($opened) {
            echo '</details>';
        }

        echo '</div>';
    }

    /**
     * Status-Badge je Eligibility-Status.
     *
     * @param   string  $status  Status-Schluessel
     * @param   array   $u       Info-Datensatz
     *
     * @return string
     */
    private static function statusBadge($status, $u)
    {
        switch ($status) {
            case 'ready':
                return !empty($u['has_update'])
                    ? self::badge('warn', htp_t('COM_HTPROTECT_AU_ST_AVAILABLE'))
                    : self::badge('ok', htp_t('COM_HTPROTECT_AU_ST_UPTODATE'));

            case 'off':
                return self::badge('info', htp_t('COM_HTPROTECT_AU_ST_OFF'));

            case 'never':
                // "Nie": bewusst kein Auto-Update (auch keine Sicherheit). Steht ein Update an, ist es
                // MANUELL relevant -> deutlicher Hinweis; sonst schlicht aktuell. Die Angreifbar-Warnung
                // sitzt am Schalter (modeSwitch), damit sie nicht doppelt erscheint.
                return !empty($u['has_update'])
                    ? self::badge('warn', htp_t('COM_HTPROTECT_AU_ST_NEVER_UPD'))
                    : self::badge('ok', htp_t('COM_HTPROTECT_AU_ST_UPTODATE'));

            case 'suspended':
                return self::badge('err', htp_t('COM_HTPROTECT_AU_ST_SUSPENDED'));

            case 'key':
                // Neutraler, unaufgeregter Hinweis: ein Download/Install-Versuch steht
                // noch aus und wird automatisch wiederholt. Bewusst 'info' (grau) statt
                // 'warn' - und der Text nennt KEINEN "Key" (unsere Quelle hat keinen).
                return self::badge('info', htp_t('COM_HTPROTECT_AU_ST_KEY'));

            case 'key_manual':
                // Nach mehreren erfolglosen automatischen Versuchen endgueltig aufgegeben
                // -> der Nutzer muss manuell aktualisieren. Neutral (warn), KEINE
                // Ursachen-Behauptung (Key/Lizenz/Quelle/Netz - nicht sicher bekannt).
                return self::badge('warn', htp_t('COM_HTPROTECT_AU_ST_MANUALREQ'));

            case 'mal_cooldown':
                return self::badge('warn', htp_t('COM_HTPROTECT_AU_ST_MALSCAN'));

            case 'mal_blocked':
                return self::badge('err', htp_t('COM_HTPROTECT_AU_ST_MALBLOCK'));

            case 'recalled':
                return self::badge('err', htp_t('COM_HTPROTECT_AU_ST_RECALLED'));

            case 'held':
                return self::badge('warn', htp_t('COM_HTPROTECT_AU_ST_HELD'));

            case 'major_hold':
                // Grosser Versionssprung, bewusst nicht auto-installiert -> sichtbar (warn),
                // bleibt manuell installierbar. Kein Fehler-, sondern ein Hinweiszustand.
                return self::badge('warn', htp_t('COM_HTPROTECT_AU_ST_MAJORHOLD'));

            case 'no_site':
                return self::badge('info', htp_t('COM_HTPROTECT_AU_ST_NOSITE'));

            case 'host_changed':
                return self::badge('err', htp_t('COM_HTPROTECT_AU_ST_HOSTCHANGED'));

            case 'unsupported':
                return self::badge('info', htp_t('COM_HTPROTECT_AU_ST_MANUAL'));

            default:
                return self::badge('info', htp_esc($status));
        }
    }

    /**
     * Badge je Log-Ergebnis.
     *
     * @param   string  $result  Ergebnis
     *
     * @return string
     */
    private static function resultBadge($result)
    {
        switch ($result) {
            case 'ok':
                return self::badge('ok', htp_t('COM_HTPROTECT_AU_RES_OK'));

            case 'rollback':
                return self::badge('warn', htp_t('COM_HTPROTECT_AU_RES_ROLLBACK'));

            case 'crash_rollback':
                return self::badge('warn', htp_t('COM_HTPROTECT_AU_RES_CRASH'));

            case 'self_heal':
                return self::badge('warn', htp_t('COM_HTPROTECT_AU_RES_HEAL'));

            case 'suspend':
                return self::badge('err', htp_t('COM_HTPROTECT_AU_RES_SUSPEND'));

            case 'manual_rollback':
                return self::badge('info', htp_t('COM_HTPROTECT_AU_RES_MANUAL'));

            case 'manual_backup':
                return self::badge('ok', htp_t('COM_HTPROTECT_AU_RES_MBACKUP'));

            case 'manual_skip':
                return self::badge('warn', htp_t('COM_HTPROTECT_AU_RES_SKIP'));

            case 'manual':
                // Terminal: Auto-Update endgueltig aufgegeben -> manuell erforderlich.
                return self::badge('warn', htp_t('COM_HTPROTECT_AU_ST_MANUALREQ'));

            case 'manual_rollback_partial':
            case 'fail':
                return self::badge('err', htp_t('COM_HTPROTECT_AU_RES_FAIL'));

            case 'hotfix':
                return self::badge('ok', htp_t('COM_HTPROTECT_AU_RES_HOTFIX'));

            case 'skip':
            default:
                return self::badge('info', htp_t('COM_HTPROTECT_AU_RES_SKIP'));
        }
    }

    /**
     * Block "Update-Haeufigkeit" (Schalter 2): Sofort / Taeglich+Uhrzeit /
     * Woechentlich+Wochentag+Uhrzeit. Sichtbar nur wenn Schalter 2 an ($allOn);
     * Wochentag/Uhrzeit per JS je nach Auswahl. Angreifbare Updates laufen IMMER
     * sofort (Hinweis), unabhaengig hiervon.
     *
     * @param   string   $schedule  'instant'|'daily'|'weekly'
     * @param   int      $hour      0-23
     * @param   int      $weekday   1=Mo .. 7=So
     * @param   boolean  $allOn     Schalter 2 aktiv (Start-Sichtbarkeit)
     *
     * @return string
     */
    private static function scheduleBlock($schedule, $hour, $weekday, $allOn, $grace = 0)
    {
        $sel = 'class="htpx-input htpx-select" style="width:auto;display:inline-block"';

        $out = '<div id="htpau-schedrow" style="margin:10px 0 4px' . ($allOn ? '' : ';display:none') . '">'
            . '<label class="htpx-label" for="htp_autoupdate_schedule">' . htp_t('COM_HTPROTECT_AU_SCHED') . '</label>'
            . '<select ' . $sel . ' id="htp_autoupdate_schedule" name="htp_autoupdate_schedule" onchange="htpauSched()">';

        foreach (array('instant' => 'COM_HTPROTECT_AU_SCHED_INSTANT', 'daily' => 'COM_HTPROTECT_AU_SCHED_DAILY', 'weekly' => 'COM_HTPROTECT_AU_SCHED_WEEKLY') as $val => $key) {
            $out .= '<option value="' . $val . '"' . ($schedule === $val ? ' selected' : '') . '>' . htp_t($key) . '</option>';
        }

        $out .= '</select>';

        // Wochentag (nur 'weekly').
        $out .= '<span id="htpau-wdwrap"' . ($schedule === 'weekly' ? '' : ' style="display:none"') . '> '
            . '<select ' . $sel . ' id="htp_autoupdate_weekday" name="htp_autoupdate_weekday">';

        foreach (array(1 => 'MON', 2 => 'TUE', 3 => 'WED', 4 => 'THU', 5 => 'FRI', 6 => 'SAT', 7 => 'SUN') as $n => $abbr) {
            $dayKey = 'COM_HTPROTECT_AU_DAY_' . $abbr; // dynamisch -> Variable (langcheck-konform)
            $out .= '<option value="' . $n . '"' . ($weekday === $n ? ' selected' : '') . '>' . htp_t($dayKey) . '</option>';
        }

        $out .= '</select></span>';

        // Uhrzeit (volle Stunde, 'daily' + 'weekly').
        $out .= '<span id="htpau-hrwrap"' . (($schedule === 'daily' || $schedule === 'weekly') ? '' : ' style="display:none"') . '> '
            . htp_t('COM_HTPROTECT_AU_AT') . ' '
            . '<select ' . $sel . ' id="htp_autoupdate_hour" name="htp_autoupdate_hour">';

        for ($h = 0; $h < 24; $h++) {
            $out .= '<option value="' . $h . '"' . ($hour === $h ? ' selected' : '') . '>' . ($h < 10 ? '0' : '') . $h . ':00</option>';
        }

        $out .= '</select></span>';

        // Karenz-Zeit (Soak) - rechts daneben. Gilt fuer die Auswahl-Updates,
        // UNABHAENGIG von der Frequenz (auch bei 'instant'). Sicherheitsupdates
        // ignorieren sie (laufen sofort).
        $out .= '<span style="margin-left:18px;white-space:nowrap">' . htp_t('COM_HTPROTECT_AU_GRACE') . ' '
            . '<select ' . $sel . ' id="htp_autoupdate_grace" name="htp_autoupdate_grace">';

        // 14-Tage-Option entfernt (praxisfern). Ein evtl. noch gespeicherter 14-Wert wird
        // fuer die ANZEIGE auf 7 gekappt -> kein leeres Dropdown, das beim Speichern still
        // auf "keine" zurueckfiele; das naechste Speichern normalisiert ihn dann auf 7.
        $graceSel = ($grace > 7) ? 7 : $grace;

        foreach (array(0 => 'COM_HTPROTECT_AU_GRACE_NONE', 1 => 'COM_HTPROTECT_AU_GRACE_24H', 3 => 'COM_HTPROTECT_AU_GRACE_3', 7 => 'COM_HTPROTECT_AU_GRACE_7') as $val => $key) {
            $out .= '<option value="' . $val . '"' . ($graceSel === $val ? ' selected' : '') . '>' . htp_t($key) . '</option>';
        }

        $out .= '</select></span>'
            . '<p class="htpx-dim" style="margin:6px 0 0">' . htp_t('COM_HTPROTECT_AU_SCHED_HINT') . ' ' . htp_t('COM_HTPROTECT_AU_GRACE_HINT') . '</p>'
            . '</div>';

        return $out;
    }

    // ── eigenstaendige kleine Helfer (kein Zugriff auf private HtpUi-Methoden) ──

    private static function url()
    {
        return 'index.php?option=com_htprotect&sec=backups#htp-autoupdate';
    }

    private static function tokenField()
    {
        return '<input type="hidden" name="' . htp_esc(htp_form_token()) . '" value="1">';
    }

    private static function badge($type, $text)
    {
        return '<span class="htpx-badge htpx-badge-' . $type . '">' . $text . '</span>';
    }

    private static function toggle($name, $checked, $label, $hint = '', $id = '', $onchange = '')
    {
        return '<label class="htpx-tg htpx-tg-hh">'
            . '<input type="checkbox"' . ($id !== '' ? ' id="' . htp_esc($id) . '"' : '')
            . ($onchange !== '' ? ' onchange="' . htp_esc($onchange) . '"' : '')
            . ' name="' . htp_esc($name) . '" value="1"' . ($checked ? ' checked' : '') . '>'
            . '<span class="htpx-tgsl"></span>'
            . '<span class="htpx-tgtext">' . $label
            . ($hint !== '' ? '<small>' . $hint . '</small>' : '')
            . '</span></label>';
    }
}
