Редактор темы

Вопрос? Оптимизация dle

  • Автор темы Автор темы art994
  • Дата начала Дата начала

art994

Посетитель
Регистрация
7 Июн 2015
Сообщения
80
Рейтинг реакций
3
Баллы
8
Возраст
31
1/3
Автор темы
Всем привет. Как можно оптимизировать движок DLE если на сайте 40 000 новостей с постерами?
 
art994 непонятно что вы хотите оптимизировать, чтобы что-то оптимизировать нужно начать с того в чем у вас проблема? Купите помощнее сервер, если что-то не тянет, а лучше даже возьмите выделенный сервер, у которого будет достаточно ресурсов, чтобы тянуть такие объёмы данных и толпу ваших посетителей. Сервера под кино, смотрите у меня в подписи
 
1) Redis Cache or Memcache + период кэширования ставь больше (Смотри по расходу RAM на сервере, если забьется сервер ляжет если нет Swap - поставь Swap 1-2gb хотя бы)
2) Кеш статики на стороне браузера пользователей настраивается в Nginx заголовками (это уже если прям много пользователей, а так не особо поможет)
3) В базе сделай внешние ключи между таблицами связанными + uniq ключи поставь по уникальным полям это ускоряет запросы у DLE стокового треш там
4) Если Nginx то выстави php-fpm оптимальные настройки
5) Настрой буфера в mysql сервере и конфиг под ресурсы сервера поковыряй, выдели больше озу под данные чтобы через диск не гоняло
6) Отключи ненужные модули в DLE там потыкать в настройках можно, они запросы делают дополнительные

PS: Все это не поможет если сайт работает на калькуляторе-сервере, все равно сервер нужен адекватной производительности под DLE. Те кто говорят бери выделенный, не слушай дорого и нет ризона если трафика на сайт идет мало. Для 40к постов при среднем трафике хватит core4 ram8 сервера, но тут еще от процессора и типа озу зависит, не бери старье на DDR3 памяти. Желательно на Ryzen или топовом проце с хорошей частотой, Mysql любит такие процессоры и шустрее Xeon. Можно и Xeon но там по частоте похуже, но и Xeon потянет. Есть еще узкие моменты на уровне кода php и настроек php, но тут долго расписывать. Итак написал достаточно...
 
Автор темы
scr1pt0 Спасибо за очень подробный ответ. Сейчас стоит fastpanel php-fm. Вроде с кешем работает не совсем тормознуто, но форум например работает быстрее dle.
 
art994 Ну Fastpanel тут обновили, чет с сохранением конфигов nginx беда приходится на самом сервере файлики править, а так панель норм переезды делать удобно между серверами например 1 командой все переносишь и остается только dns у доменов заменить на новые
 
Автор темы
scr1pt0 а nginx вы где что правите? будет очень полезно. и еще бы команду волшебную для переноса. Спасибо
 
3) В базе сделай внешние ключи между таблицами связанными + uniq ключи поставь по уникальным полям это ускоряет запросы у DLE стокового треш там

Скажите измеряли скорость до и после по 3 пункту ? И как проделать такие правки, может у вас есть готовый плагин для внесение правок?
 
sneiks Ну на холостых не заметишь разницы, тут надо нагрузочные тесты делать, но ускорять будет в любом случае базу наличие грамотно расставленных ключей. Но там и сама база не нормализована у DLE структура базы на ужасном уровне
 
art994 Я вообще рекомендую отказаться от nginx + apache2 связки (CGI or FastCGI) и перейти на чистый nginx, он гораздо шустрее работает. Apache2 очень медленный по сравнению с Nginx. Там просто надо .htaccess файл перевести в формат конфига nginx.
 
sneiks Ну на холостых не заметишь разницы, тут надо нагрузочные тесты делать, но ускорять будет в любом случае базу наличие грамотно расставленных ключей. Но там и сама база не нормализована у DLE структура базы на ужасном уровне
Так есть универсальное решение правки базы? Думаю многим было бы полезно. Готов поддержать на чай с печенками
 
sneiks Можно руками проставить ключи есть клиент на винду для работы с базой HeidiSQL называется, плагинов нет на такое насколько знаю но думаю кто нибудь напишет, но многие разрабы модулей и плагинов сами не проставляют некоторые ключи в базе замечал, как будто не знают что это такое)
 
ok_daa потестите
Код:
<?xml version="1.0" encoding="utf-8"?>
<dleplugin>
  <name>DBOptimizerPro</name>
  <description><![CDATA[
Оптимизация БД DLE: индексы, UNIQUE, FOREIGN KEY, обслуживание таблиц (ANALYZE/CHECK/OPTIMIZE/REPAIR).

Как открыть:
- Админка → Сторонние модули → DBOptimizerPro
- или /admin.php?mod=db_optimizer

Важно:
- ALTER TABLE/OPTIMIZE может быть долгим и блокировать таблицы.
- Перед применением сделайте бэкап.
- Лог: /engine/data/dboptimizerpro.log
]]></description>
  <icon></icon>
  <version>1.1.0</version>
  <dleversion>18.0</dleversion>
  <versioncompare>greater</versioncompare>
  <upgradeurl></upgradeurl>
  <filedelete>0</filedelete>
  <needplugin></needplugin>
  <mnotice>0</mnotice>

  <mysqlinstall><![CDATA[
INSERT IGNORE INTO `{prefix}_admin_sections` (`name`, `title`, `descr`, `icon`, `allow_groups`)
VALUES ('db_optimizer', 'DBOptimizerPro', 'Оптимизация БД (индексы, UNIQUE, FOREIGN KEY)', '', '1');
]]></mysqlinstall>

  <mysqlupgrade><![CDATA[]]></mysqlupgrade>
  <mysqlenable><![CDATA[]]></mysqlenable>
  <mysqldisable><![CDATA[]]></mysqldisable>

  <mysqldelete><![CDATA[
DELETE FROM `{prefix}_admin_sections` WHERE `name`='db_optimizer';
]]></mysqldelete>

  <phpinstall><![CDATA[
@unlink(ENGINE_DIR . '/cache/system/adminmenu.php');
]]></phpinstall>

  <phpupgrade><![CDATA[]]></phpupgrade>
  <phpenable><![CDATA[]]></phpenable>
  <phpdisable><![CDATA[]]></phpdisable>

  <phpdelete><![CDATA[
@unlink(ENGINE_DIR . '/cache/system/adminmenu.php');
]]></phpdelete>

  <notice><![CDATA[]]></notice>

  <file name="engine/inc/db_optimizer.php">
    <operation action="create">
      <replacecode><![CDATA[
<?php
/**
 * DBOptimizerPro - модуль админки DLE
 * Версия: 1.1.0
 *
 * Реализовано:
 * - Обслуживание таблиц: ANALYZE / CHECK / OPTIMIZE / REPAIR (REPAIR только MyISAM)
 * - Индексы (обычные)
 * - UNIQUE (с проверкой дублей)
 * - FOREIGN KEY (с проверками: InnoDB, типы, индекс, сироты)
 *
 * Важно:
 * - FOREIGN KEY сам по себе не "ускоритель". Чаще ускоряют индексы.
 * - Любой ALTER TABLE/OPTIMIZE может быть долгим и блокировать таблицы.
 * - Сделайте бэкап перед применением.
 */

if (!defined('DATALIFEENGINE') || !defined('LOGGED_IN')) {
    die('Hacking attempt!');
}

if (!isset($member_id) || !is_array($member_id) || (int)$member_id['user_group'] !== 1) {
    if (function_exists('msg')) {
        msg('error', 'Доступ запрещён', 'Модуль доступен только администраторам (группа 1).');
    }
    die('Access denied');
}

define('DBOP_VERSION', '1.1.0');

// ---------------- helpers ----------------

function dbop_h($v) { return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); }

function dbop_bytes($bytes) {
    $bytes = (float)$bytes;
    if ($bytes < 1024) return sprintf('%.0f B', $bytes);
    $u = array('KB','MB','GB','TB','PB');
    $i = 0;
    $bytes /= 1024;
    while ($bytes >= 1024 && $i < count($u)-1) { $bytes /= 1024; $i++; }
    return sprintf('%.2f %s', $bytes, $u[$i]);
}

function dbop_escape_like($s) {
    return str_replace(array('\\', '%', '_'), array('\\\\', '\\%', '\\_'), (string)$s);
}

function dbop_require_hash() {
    global $dle_login_hash;
    if (!isset($_REQUEST['user_hash']) || $_REQUEST['user_hash'] !== $dle_login_hash) {
        msg('error', 'Ошибка безопасности', 'Неверный ключ безопасности (user_hash). Обновите страницу и повторите действие.');
    }
}

function dbop_log($line) {
    $file = ENGINE_DIR . '/data/dboptimizerpro.log';
    $ts = date('Y-m-d H:i:s');
    @file_put_contents($file, '['.$ts.'] '.$line."\n", FILE_APPEND);
}

function dbop_is_safe_identifier($s) {
    return (bool)preg_match('/^[A-Za-z0-9_]+$/', (string)$s);
}

// --------- DB introspection ---------

function dbop_get_tables($prefix, $only_prefix=true) {
    global $db;

    $out = array();

    $where = "TABLE_SCHEMA = DATABASE()";
    if ($only_prefix && $prefix !== '') {
        // хотим только таблицы DLE: PREFIX_*
        $like = dbop_escape_like($prefix . '_') . '%';
        $where .= " AND TABLE_NAME LIKE '{$like}' ESCAPE '\\\\'";
    }

    $sql = "SELECT TABLE_NAME, ENGINE, TABLE_ROWS, DATA_LENGTH, INDEX_LENGTH, DATA_FREE
            FROM information_schema.TABLES
            WHERE {$where}
            ORDER BY TABLE_NAME";
    $db->query($sql);
    while ($r = $db->get_row()) {
        $out[$r['TABLE_NAME']] = array(
            'table'  => $r['TABLE_NAME'],
            'engine' => $r['ENGINE'],
            'rows'   => (int)$r['TABLE_ROWS'],
            'data'   => (int)$r['DATA_LENGTH'],
            'index'  => (int)$r['INDEX_LENGTH'],
            'free'   => (int)$r['DATA_FREE'],
        );
    }
    $db->free();
    return $out;
}

function dbop_get_columns($table) {
    global $db;
    $cols = array();
    $db->query("SHOW COLUMNS FROM `{$table}`");
    while ($r = $db->get_row()) $cols[] = $r['Field'];
    $db->free();
    return $cols;
}

function dbop_get_column_types($table) {
    global $db;
    $types = array();
    $db->query("SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT
                FROM information_schema.COLUMNS
                WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '".$db->safesql($table)."'");
    while ($r = $db->get_row()) {
        $types[$r['COLUMN_NAME']] = array(
            'type' => $r['COLUMN_TYPE'],
            'null' => $r['IS_NULLABLE'],
            'def'  => $r['COLUMN_DEFAULT'],
        );
    }
    $db->free();
    return $types;
}

function dbop_get_indexes($table) {
    global $db;
    $idx = array();
    $db->query("SHOW INDEX FROM `{$table}`");
    while ($r = $db->get_row()) {
        $name = $r['Key_name'];
        if (!isset($idx[$name])) {
            $idx[$name] = array(
                'unique' => ((int)$r['Non_unique'] === 0),
                'cols'   => array(),
            );
        }
        $seq = (int)$r['Seq_in_index'];
        $idx[$name]['cols'][$seq] = $r['Column_name'];
    }
    $db->free();
    foreach ($idx as $k => $v) {
        ksort($idx[$k]['cols']);
        $idx[$k]['cols'] = array_values($idx[$k]['cols']);
    }
    return $idx;
}

function dbop_has_index_prefix($indexes, $cols, $need_unique=false) {
    $n = count($cols);
    foreach ($indexes as $idx) {
        if ($need_unique && empty($idx['unique'])) continue;
        if (count($idx['cols']) < $n) continue;
        $ok = true;
        for ($i=0; $i<$n; $i++) {
            if ((string)$idx['cols'][$i] !== (string)$cols[$i]) { $ok = false; break; }
        }
        if ($ok) return true;
    }
    return false;
}

function dbop_fk_exists($table, $col, $ref_table, $ref_col) {
    global $db;
    $sql = "SELECT CONSTRAINT_NAME
            FROM information_schema.KEY_COLUMN_USAGE
            WHERE TABLE_SCHEMA = DATABASE()
              AND TABLE_NAME = '".$db->safesql($table)."'
              AND COLUMN_NAME = '".$db->safesql($col)."'
              AND REFERENCED_TABLE_NAME = '".$db->safesql($ref_table)."'
              AND REFERENCED_COLUMN_NAME = '".$db->safesql($ref_col)."'
            LIMIT 1";
    $row = $db->super_query($sql);
    return !empty($row['CONSTRAINT_NAME']);
}

function dbop_get_table_engine($tables, $table) {
    return isset($tables[$table]['engine']) ? strtoupper((string)$tables[$table]['engine']) : '';
}

// --------- Definitions (indexes, unique, foreign keys) ---------

function dbop_defs($prefix, $tables) {

    $p = ($prefix !== '') ? ($prefix . '_') : '';

    $t_post   = $p . 'post';
    $t_comm   = $p . 'comments';
    $t_users  = $p . 'users';
    $t_extra  = $p . 'post_extras';
    $t_tags   = $p . 'tags';

    $has = function($t) use ($tables) { return isset($tables[$t]); };

    $defs = array(
        'indexes' => array(),
        'unique'  => array(),
        'fks'     => array(),
    );

    // ---- обычные индексы ----
    if ($has($t_post)) {
        $defs['indexes'][] = array(
            'id'    => 'idx_post_approve_date',
            'table' => $t_post,
            'name'  => 'idx_approve_date',
            'cols'  => array('approve','date'),
            'why'   => 'Частая выборка: approve=1 + сортировка по date (главная/категории).',
        );
        $defs['indexes'][] = array(
            'id'    => 'idx_post_autor',
            'table' => $t_post,
            'name'  => 'idx_autor',
            'cols'  => array('autor'),
            'why'   => 'Выборки по автору.',
        );
    }

    if ($has($t_comm)) {
        $defs['indexes'][] = array(
            'id'    => 'idx_comments_post_approve_date',
            'table' => $t_comm,
            'name'  => 'idx_post_approve_date',
            'cols'  => array('post_id','approve','date'),
            'why'   => 'Вывод комментариев: post_id + approve + сортировка по date.',
        );
    }

    if ($has($t_tags)) {
        $defs['indexes'][] = array(
            'id'    => 'idx_tags_tag',
            'table' => $t_tags,
            'name'  => 'idx_tag',
            'cols'  => array('tag'),
            'why'   => 'Поиск по тегу.',
        );
        $defs['indexes'][] = array(
            'id'    => 'idx_tags_news_id',
            'table' => $t_tags,
            'name'  => 'idx_news_id',
            'cols'  => array('news_id'),
            'why'   => 'Связь тегов с новостями.',
        );
    }

    // ---- UNIQUE ----
    if ($has($t_users)) {
        $defs['unique'][] = array(
            'id'    => 'uq_users_name',
            'table' => $t_users,
            'name'  => 'uq_name',
            'cols'  => array('name'),
            'why'   => 'Имя пользователя обычно должно быть уникальным.',
            'dup_sql' => "SELECT `name` AS v, COUNT(*) AS c FROM `{$t_users}` GROUP BY `name` HAVING c>1 LIMIT 50",
        );
        $defs['unique'][] = array(
            'id'    => 'uq_users_email',
            'table' => $t_users,
            'name'  => 'uq_email',
            'cols'  => array('email'),
            'why'   => 'Email часто уникален. Если у вас разрешены одинаковые email - не ставьте.',
            'dup_sql' => "SELECT `email` AS v, COUNT(*) AS c FROM `{$t_users}` WHERE `email`<>'' GROUP BY `email` HAVING c>1 LIMIT 50",
        );
    }

    if ($has($t_extra)) {
        $defs['unique'][] = array(
            'id'    => 'uq_post_extras_news_id',
            'table' => $t_extra,
            'name'  => 'uq_news_id',
            'cols'  => array('news_id'),
            'why'   => 'Обычно 1 запись post_extras на 1 новость. Ускоряет JOIN по news_id и защищает от дублей.',
            'dup_sql' => "SELECT `news_id` AS v, COUNT(*) AS c FROM `{$t_extra}` GROUP BY `news_id` HAVING c>1 LIMIT 50",
        );
    }

    if ($has($t_tags)) {
        $defs['unique'][] = array(
            'id'    => 'uq_tags_news_tag',
            'table' => $t_tags,
            'name'  => 'uq_news_tag',
            'cols'  => array('news_id','tag'),
            'why'   => 'Убирает дубли связей (news_id + tag).',
            'dup_sql' => "SELECT CONCAT(`news_id`,':',`tag`) AS v, COUNT(*) AS c FROM `{$t_tags}` GROUP BY `news_id`,`tag` HAVING c>1 LIMIT 50",
        );
    }

    // ---- FOREIGN KEYS ----
    if ($has($t_comm) && $has($t_post)) {
        $defs['fks'][] = array(
            'id'        => 'fk_comments_post',
            'table'     => $t_comm,
            'col'       => 'post_id',
            'ref_table' => $t_post,
            'ref_col'   => 'id',
            'name'      => 'fk_comments_post',
            'on_delete' => 'CASCADE',
            'on_update' => 'CASCADE',
            'why'       => 'Комментарии должны ссылаться на существующую новость. При удалении новости логично удалять комментарии.',
            'orphan_sql'=> "SELECT COUNT(*) AS c FROM `{$t_comm}` c LEFT JOIN `{$t_post}` p ON p.`id`=c.`post_id` WHERE c.`post_id`<>0 AND p.`id` IS NULL",
        );
    }

    if ($has($t_extra) && $has($t_post)) {
        $defs['fks'][] = array(
            'id'        => 'fk_post_extras_post',
            'table'     => $t_extra,
            'col'       => 'news_id',
            'ref_table' => $t_post,
            'ref_col'   => 'id',
            'name'      => 'fk_post_extras_post',
            'on_delete' => 'CASCADE',
            'on_update' => 'CASCADE',
            'why'       => 'post_extras должен ссылаться на новость. При удалении новости extras должны удаляться.',
            'orphan_sql'=> "SELECT COUNT(*) AS c FROM `{$t_extra}` e LEFT JOIN `{$t_post}` p ON p.`id`=e.`news_id` WHERE e.`news_id`<>0 AND p.`id` IS NULL",
        );
    }

    if ($has($t_tags) && $has($t_post)) {
        $defs['fks'][] = array(
            'id'        => 'fk_tags_post',
            'table'     => $t_tags,
            'col'       => 'news_id',
            'ref_table' => $t_post,
            'ref_col'   => 'id',
            'name'      => 'fk_tags_post',
            'on_delete' => 'CASCADE',
            'on_update' => 'CASCADE',
            'why'       => 'Связи тегов должны ссылаться на существующую новость.',
            'orphan_sql'=> "SELECT COUNT(*) AS c FROM `{$t_tags}` t LEFT JOIN `{$t_post}` p ON p.`id`=t.`news_id` WHERE t.`news_id`<>0 AND p.`id` IS NULL",
        );
    }

    return $defs;
}

// ------------- Validation for defs -------------

function dbop_validate_index_def($def, $tables) {
    $table = $def['table'];
    if (!isset($tables[$table])) return array(false, 'Таблица не найдена');
    $cols = dbop_get_columns($table);
    $set = array_flip($cols);
    foreach ($def['cols'] as $c) {
        if (!isset($set[$c])) return array(false, 'Нет колонки: '.$c);
    }
    $idxs = dbop_get_indexes($table);
    if (dbop_has_index_prefix($idxs, $def['cols'], false)) return array(false, 'Индекс уже есть');
    return array(true, '');
}

function dbop_validate_unique_def($def, $tables) {
    $table = $def['table'];
    if (!isset($tables[$table])) return array(false, 'Таблица не найдена');
    $cols = dbop_get_columns($table);
    $set = array_flip($cols);
    foreach ($def['cols'] as $c) {
        if (!isset($set[$c])) return array(false, 'Нет колонки: '.$c);
    }
    $idxs = dbop_get_indexes($table);
    if (dbop_has_index_prefix($idxs, $def['cols'], true)) return array(false, 'UNIQUE уже есть');
    global $db;
    if (!empty($def['dup_sql'])) {
        $db->query($def['dup_sql']);
        $row = $db->get_row();
        $hasDup = !empty($row);
        $db->free();
        if ($hasDup) return array(false, 'Есть дубли (сначала исправьте)');
    }
    return array(true, '');
}

function dbop_validate_fk_def($def, $tables) {
    global $db;

    $t  = $def['table'];
    $rt = $def['ref_table'];
    $c  = $def['col'];
    $rc = $def['ref_col'];

    if (!isset($tables[$t]) || !isset($tables[$rt])) return array(false, 'Таблица/ссылка не найдена');

    $eng1 = dbop_get_table_engine($tables, $t);
    $eng2 = dbop_get_table_engine($tables, $rt);
    if ($eng1 !== 'INNODB' || $eng2 !== 'INNODB') return array(false, 'Требуется InnoDB (обе таблицы)');

    $cols1 = array_flip(dbop_get_columns($t));
    $cols2 = array_flip(dbop_get_columns($rt));
    if (!isset($cols1[$c]))  return array(false, 'Нет колонки '.$c.' в '.$t);
    if (!isset($cols2[$rc])) return array(false, 'Нет колонки '.$rc.' в '.$rt);

    $types1 = dbop_get_column_types($t);
    $types2 = dbop_get_column_types($rt);
    $ct1 = isset($types1[$c]['type']) ? strtolower($types1[$c]['type']) : '';
    $ct2 = isset($types2[$rc]['type']) ? strtolower($types2[$rc]['type']) : '';
    if ($ct1 && $ct2 && $ct1 !== $ct2) {
        return array(false, 'Типы колонок не совпадают: '.$ct1.' vs '.$ct2);
    }

    $idx1 = dbop_get_indexes($t);
    $hasChildIndex = dbop_has_index_prefix($idx1, array($c), false) || dbop_has_index_prefix($idx1, array($c), true);
    if (!$hasChildIndex) return array(false, 'Нужен индекс на '.$t.'.'.$c);

    if (!empty($def['orphan_sql'])) {
        $row = $db->super_query($def['orphan_sql']);
        $cnt = isset($row['c']) ? (int)$row['c'] : 0;
        if ($cnt > 0) return array(false, 'Есть сироты: '.$cnt);
    }

    if (dbop_fk_exists($t, $c, $rt, $rc)) return array(false, 'FK уже существует');

    return array(true, '');
}

// --------- Apply operations ---------

function dbop_apply_index($def) {
    global $db;
    $table = $def['table'];
    $name  = $def['name'];
    $cols  = $def['cols'];

    if (!dbop_is_safe_identifier($name)) return array(false, 'Плохое имя индекса');
    foreach ($cols as $c) if (!dbop_is_safe_identifier($c)) return array(false, 'Плохая колонка');

    $cols_sql = '`' . implode('`,`', $cols) . '`';
    $sql = "ALTER TABLE `{$table}` ADD INDEX `{$name}` ({$cols_sql})";
    dbop_log('Alter: '.$sql);
    $db->query($sql);
    return array(true, $sql);
}

function dbop_apply_unique($def) {
    global $db;
    $table = $def['table'];
    $name  = $def['name'];
    $cols  = $def['cols'];

    if (!dbop_is_safe_identifier($name)) return array(false, 'Плохое имя индекса');
    foreach ($cols as $c) if (!dbop_is_safe_identifier($c)) return array(false, 'Плохая колонка');

    $cols_sql = '`' . implode('`,`', $cols) . '`';
    $sql = "ALTER TABLE `{$table}` ADD UNIQUE INDEX `{$name}` ({$cols_sql})";
    dbop_log('Alter: '.$sql);
    $db->query($sql);
    return array(true, $sql);
}

function dbop_apply_fk($def) {
    global $db;
    $t  = $def['table'];
    $c  = $def['col'];
    $rt = $def['ref_table'];
    $rc = $def['ref_col'];
    $name = $def['name'];

    foreach (array($c,$rc,$name) as $x) {
        if (!dbop_is_safe_identifier($x)) return array(false, 'Плохой идентификатор');
    }

    $onDel = !empty($def['on_delete']) ? strtoupper($def['on_delete']) : 'RESTRICT';
    $onUpd = !empty($def['on_update']) ? strtoupper($def['on_update']) : 'RESTRICT';
    $allowed = array('RESTRICT','CASCADE','SET NULL','NO ACTION');
    if (!in_array($onDel, $allowed, true)) $onDel = 'RESTRICT';
    if (!in_array($onUpd, $allowed, true)) $onUpd = 'RESTRICT';

    $sql = "ALTER TABLE `{$t}` ADD CONSTRAINT `{$name}` FOREIGN KEY (`{$c}`) REFERENCES `{$rt}`(`{$rc}`) ON DELETE {$onDel} ON UPDATE {$onUpd}";
    dbop_log('Alter: '.$sql);
    $db->query($sql);
    return array(true, $sql);
}

function dbop_convert_engine($table, $engine) {
    global $db;
    $engine = strtoupper($engine);
    if (!in_array($engine, array('INNODB','MYISAM'), true)) return array(false, 'Недопустимый engine');
    $sql = "ALTER TABLE `{$table}` ENGINE={$engine}";
    dbop_log('Alter: '.$sql);
    $db->query($sql);
    return array(true, $sql);
}

// ---------------- Controller ----------------

$prefix = defined('PREFIX') ? PREFIX : ((isset($config['dbprefix']) && $config['dbprefix']) ? $config['dbprefix'] : '');
$only_prefix = true;
if (isset($_GET['show_all']) && (int)$_GET['show_all'] === 1) $only_prefix = false;

$tables = dbop_get_tables($prefix, $only_prefix);
$defs   = dbop_defs($prefix, $tables);

$tab = isset($_REQUEST['tab']) ? (string)$_REQUEST['tab'] : 'home';
$action = isset($_REQUEST['action']) ? (string)$_REQUEST['action'] : '';

function dbop_json_exit($arr) {
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($arr, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
    exit;
}

// AJAX actions
if ($action) {
    dbop_require_hash();

    if ($action === 'sections') {
        // добавить/убрать из "Сторонние модули" (на случай, если запись не создалась при установке)
        global $db;
        $row = $db->super_query("SELECT id FROM " . PREFIX . "_admin_sections WHERE name='db_optimizer'");
        if ($row) {
            $db->query("DELETE FROM " . PREFIX . "_admin_sections WHERE name='db_optimizer'");
            @unlink(ENGINE_DIR . '/cache/system/adminmenu.php');
            dbop_json_exit(array('success'=>true,'message'=>'Ссылка убрана из меню «Сторонние модули»'));
        } else {
            $db->query("INSERT IGNORE INTO " . PREFIX . "_admin_sections (name, title, descr, icon, allow_groups)
                        VALUES ('db_optimizer', 'DBOptimizerPro', 'Оптимизация БД (индексы, UNIQUE, FOREIGN KEY)', '', '1')");
            @unlink(ENGINE_DIR . '/cache/system/adminmenu.php');
            dbop_json_exit(array('success'=>true,'message'=>'Ссылка добавлена в меню «Сторонние модули»'));
        }
    }

    if ($action === 'maintenance') {
        $cmd = strtolower((string)($_POST['cmd'] ?? 'analyze'));
        $allowed_cmds = array('analyze'=>'ANALYZE','check'=>'CHECK','optimize'=>'OPTIMIZE','repair'=>'REPAIR');
        if (!isset($allowed_cmds[$cmd])) dbop_json_exit(array('success'=>false,'message'=>'Неизвестная команда'));

        $sel = isset($_POST['tables']) && is_array($_POST['tables']) ? $_POST['tables'] : array();
        $sel = array_values(array_unique(array_map('strval',$sel)));
        $safe = array();
        foreach ($sel as $t) if (isset($tables[$t])) $safe[] = $t;
        if (!$safe) dbop_json_exit(array('success'=>false,'message'=>'Не выбраны таблицы'));

        $results = array();
        foreach ($safe as $tname) {
            $sqlCmd = $allowed_cmds[$cmd];

            if ($sqlCmd === 'REPAIR') {
                $eng = strtoupper((string)$tables[$tname]['engine']);
                if ($eng !== 'MYISAM') {
                    $results[] = array('table'=>$tname,'status'=>'SKIP','msg'=>'REPAIR только для MyISAM');
                    continue;
                }
            }

            $q = "{$sqlCmd} TABLE `{$tname}`";
            dbop_log('Run: '.$q);
            $db->query($q);
            while ($r = $db->get_row()) {
                $results[] = array('table'=>$r['Table'],'type'=>$r['Msg_type'],'msg'=>$r['Msg_text']);
            }
            $db->free();
        }

        dbop_json_exit(array('success'=>true,'message'=>'Готово','results'=>$results));
    }

    if ($action === 'apply_indexes' || $action === 'apply_unique' || $action === 'apply_fks') {
        $items = isset($_POST['items']) && is_array($_POST['items']) ? $_POST['items'] : array();
        $items = array_values(array_unique(array_map('strval',$items)));
        if (!$items) dbop_json_exit(array('success'=>false,'message'=>'Ничего не выбрано'));

        $kind = ($action === 'apply_indexes') ? 'indexes' : (($action === 'apply_unique') ? 'unique' : 'fks');
        $defsList = $defs[$kind];

        $map = array();
        foreach ($defsList as $d) $map[$d['id']] = $d;

        $applied = array();
        $skipped = array();

        foreach ($items as $id) {
            if (!isset($map[$id])) { $skipped[] = array('id'=>$id,'reason'=>'unknown'); continue; }
            $d = $map[$id];

            if ($kind === 'indexes') {
                list($ok,$why) = dbop_validate_index_def($d, $tables);
                if (!$ok) { $skipped[] = array('id'=>$id,'reason'=>$why); continue; }
                list($ok2,$sql) = dbop_apply_index($d);
                if (!$ok2) { $skipped[] = array('id'=>$id,'reason'=>$sql); continue; }
                $applied[] = $sql;
            }
            if ($kind === 'unique') {
                list($ok,$why) = dbop_validate_unique_def($d, $tables);
                if (!$ok) { $skipped[] = array('id'=>$id,'reason'=>$why); continue; }
                list($ok2,$sql) = dbop_apply_unique($d);
                if (!$ok2) { $skipped[] = array('id'=>$id,'reason'=>$sql); continue; }
                $applied[] = $sql;
            }
            if ($kind === 'fks') {
                list($ok,$why) = dbop_validate_fk_def($d, $tables);
                if (!$ok) { $skipped[] = array('id'=>$id,'reason'=>$why); continue; }
                list($ok2,$sql) = dbop_apply_fk($d);
                if (!$ok2) { $skipped[] = array('id'=>$id,'reason'=>$sql); continue; }
                $applied[] = $sql;
            }
        }

        dbop_json_exit(array(
            'success'=>true,
            'message'=>'Операция завершена',
            'applied'=>$applied,
            'skipped'=>$skipped,
        ));
    }

    if ($action === 'convert_engine') {
        $items = isset($_POST['tables']) && is_array($_POST['tables']) ? $_POST['tables'] : array();
        $items = array_values(array_unique(array_map('strval',$items)));
        $target = strtoupper((string)($_POST['target'] ?? 'INNODB'));
        if (!in_array($target, array('INNODB','MYISAM'), true)) $target = 'INNODB';
        if (!$items) dbop_json_exit(array('success'=>false,'message'=>'Не выбраны таблицы'));

        $applied = array();
        $skipped = array();

        foreach ($items as $t) {
            if (!isset($tables[$t])) { $skipped[] = array('table'=>$t,'reason'=>'unknown'); continue; }
            $cur = strtoupper((string)$tables[$t]['engine']);
            if ($cur === $target) { $skipped[] = array('table'=>$t,'reason'=>'уже '.$target); continue; }
            list($ok,$sql) = dbop_convert_engine($t, $target);
            if (!$ok) { $skipped[] = array('table'=>$t,'reason'=>$sql); continue; }
            $applied[] = $sql;
        }

        dbop_json_exit(array('success'=>true,'message'=>'Готово','applied'=>$applied,'skipped'=>$skipped));
    }

    dbop_json_exit(array('success'=>false,'message'=>'Unknown action'));
}

// ---------------- UI (Admin) ----------------

$__name  = 'DBOptimizerPro';
$__descr = 'Оптимизация БД (индексы, UNIQUE, FOREIGN KEY)';

echoheader($__name, $__descr);

$adminUrl = $config['admin_path'] ?? 'admin.php';
$userHash = $dle_login_hash;

$box = ($config['version_id'] >= 12) ? 'panel' : 'box';
$boxHeader = ($config['version_id'] >= 12) ? 'panel-heading' : 'box-header';
$boxContent = 'box-content';
$boxFooter = ($config['version_id'] >= 12) ? 'panel-footer' : 'box-footer';
$tabsClass = ($config['version_id'] >= 12) ? 'nav-tabs-solid' : 'nav-tabs-left';

function dbop_tab_link($tab, $label, $icon='') {
    $u = 'admin.php?mod=db_optimizer&tab='.$tab.(isset($_GET['show_all']) && (int)$_GET['show_all']===1 ? '&show_all=1' : '');
    $i = $icon ? '<i class="'.$icon.'"></i> ' : '';
    $active = ((isset($_REQUEST['tab']) && $_REQUEST['tab']===$tab) || (!isset($_REQUEST['tab']) && $tab==='home')) ? ' class="active"' : '';
    return '<li'.$active.'><a href="'.$u.'">'.$i.dbop_h($label).'</a></li>';
}

echo '<style>
/* читабельные табы (у вас были "белое на белом") */
.dbop-wrap .nav-tabs>li>a{color:#333 !important; background:#f6f7f9; border:1px solid #e2e5ea; margin-right:6px; border-bottom:none;}
.dbop-wrap .nav-tabs>li.active>a{background:#ffffff !important; color:#000 !important; font-weight:600;}
.dbop-wrap .nav-tabs{border-bottom:1px solid #e2e5ea;}
.dbop-steps .step{border:1px solid #e5e5e5;border-radius:10px;padding:12px;margin:10px 0;background:#fff;}
.dbop-steps .step h4{margin:0 0 6px 0;}
.dbop-small{color:#666;font-size:12px;}
.dbop-badge{display:inline-block;padding:3px 8px;border-radius:999px;font-size:12px;border:1px solid #ddd;background:#fafafa;}
.dbop-badge.ok{background:#e8fff0;border-color:#bfe8cd;}
.dbop-badge.bad{background:#fff0f0;border-color:#f1c1c1;}
.dbop-badge.warn{background:#fff8e6;border-color:#f2d49a;}
.dbop-table code{font-size:12px;}
</style>';

echo '<div class="dbop-wrap '.$box.'">';
echo '  <div class="'.$boxHeader.'">';
echo '    <ul class="nav nav-tabs '.$tabsClass.'">';
echo          dbop_tab_link('home', 'Быстрый старт', 'icon-home');
echo          dbop_tab_link('tables', 'Таблицы', 'icon-database');
echo          dbop_tab_link('indexes', 'Индексы', 'icon-list');
echo          dbop_tab_link('unique', 'UNIQUE', 'icon-check');
echo          dbop_tab_link('fks', 'FOREIGN KEY', 'icon-link');
echo          dbop_tab_link('docs', 'Инструкция', 'icon-book');
echo '    </ul>';
echo '  </div>';

echo '  <div class="'.$boxContent.'">';

echo '<div class="alert alert-warning" style="margin-bottom:10px;">
<b>DBOptimizerPro v'.dbop_h(DBOP_VERSION).'</b><br>
Изменения структуры (ALTER TABLE, OPTIMIZE) могут быть долгими и блокировать таблицы.
Перед применением сделайте бэкап. Лог: <code>/engine/data/dboptimizerpro.log</code>
</div>';

if ($prefix === '') {
    echo '<div class="alert alert-danger"><b>Проблема:</b> не удалось определить PREFIX. Модуль не знает, какие таблицы считать DLE.</div>';
} else {
    echo '<div class="alert alert-info" style="margin-bottom:10px;">
<b>Фильтр:</b> сейчас показаны '.($only_prefix ? 'только таблицы DLE (<code>'.dbop_h($prefix).'_*</code>)' : 'все таблицы текущей БД').'.
<a href="admin.php?mod=db_optimizer&tab='.dbop_h($tab).($only_prefix ? '&show_all=1' : '').'">'.($only_prefix ? 'Показать все' : 'Показать только DLE').'</a>
</div>';
}

if ($tab === 'home') {

    $totalData = 0; $totalIdx = 0;
    foreach ($tables as $t) { $totalData += $t['data']; $totalIdx += $t['index']; }

    echo '<div class="dbop-steps">';

    echo '<div class="step">
        <h4>Сценарий 1. Безопасно и быстро (рекомендуется)</h4>
        <div class="dbop-small">Обновить статистику БД и добавить обычные индексы (без UNIQUE и FOREIGN KEY).</div>
        <ol style="margin:8px 0 0 18px;">
          <li>Вкладка <b>Таблицы</b> → выделите таблицы → <b>ANALYZE</b> → "Выполнить".</li>
          <li>Вкладка <b>Индексы</b> → отметьте "можно добавить" → "Добавить выбранные индексы".</li>
        </ol>
    </div>';

    echo '<div class="step">
        <h4>Сценарий 2. Уникальность данных (UNIQUE)</h4>
        <div class="dbop-small">UNIQUE не применится, если в таблице есть дубли - модуль покажет примеры.</div>
        <ol style="margin:8px 0 0 18px;">
          <li>Вкладка <b>UNIQUE</b> → проверьте статус.</li>
          <li>Если есть дубли - сначала исправьте, потом применяйте.</li>
        </ol>
    </div>';

    echo '<div class="step">
        <h4>Сценарий 3. FOREIGN KEY (осторожно)</h4>
        <div class="dbop-small">FOREIGN KEY повышает целостность. Требует InnoDB и отсутствие "сирот".</div>
        <ol style="margin:8px 0 0 18px;">
          <li>Вкладка <b>FOREIGN KEY</b> → убедитесь, что всё "можно добавить".</li>
          <li>Если таблицы не InnoDB - вкладка <b>Таблицы</b> → конвертация в InnoDB.</li>
        </ol>
    </div>';

    echo '</div>';

    echo '<hr>';
    echo '<div class="row"><div class="col-md-6">
        <div class="alert alert-info">
        <b>Сводка</b><br>
        Таблиц: <b>'.count($tables).'</b><br>
        Data: <b>'.dbop_h(dbop_bytes($totalData)).'</b><br>
        Index: <b>'.dbop_h(dbop_bytes($totalIdx)).'</b><br>
        Total: <b>'.dbop_h(dbop_bytes($totalData+$totalIdx)).'</b>
        </div>
    </div><div class="col-md-6">
        <div class="alert alert-default">
        <b>Что реально ускоряет чаще всего</b><br>
        1) индексы под реальные WHERE/ORDER BY<br>
        2) кэш (Redis/Memcached), PHP-FPM, настройки MySQL<br>
        3) анализ slow query log и правка запросов
        </div>
    </div></div>';
}

if ($tab === 'tables') {

    echo '<h3 style="margin-top:0;">Таблицы и обслуживание</h3>';
    echo '<div class="dbop-small">Команды: <b>ANALYZE</b> (рекомендуется), <b>CHECK</b>, <b>OPTIMIZE</b> (осторожно), <b>REPAIR</b> (только MyISAM).</div>';

    echo '<div class="alert alert-warning" style="margin-top:10px;">
<b>OPTIMIZE</b> на InnoDB часто равен перестройке таблицы и может быть очень долгим.
Используйте после массовых удалений или если видите реальную фрагментацию/раздувание.
</div>';

    echo '<form id="dbop-maint-form" onsubmit="return false;">';
    echo '<input type="hidden" name="user_hash" value="'.dbop_h($userHash).'">';

    echo '<div style="margin:8px 0;">';
    echo '<select name="cmd" class="uniform">
            <option value="analyze">ANALYZE (рекомендуется)</option>
            <option value="check">CHECK</option>
            <option value="optimize">OPTIMIZE (осторожно)</option>
            <option value="repair">REPAIR (только MyISAM)</option>
          </select> ';
    echo '<button class="btn btn-primary" onclick="dbop_runMaintenance();">Выполнить команду на выбранных таблицах</button>';
    echo '</div>';

    echo '<table class="table table-normal table-hover dbop-table" style="width:100%;">';
    echo '<thead><tr>
        <th style="width:30px;"><input type="checkbox" onclick="dbop_toggleAll(this, \'tables[]\');"></th>
        <th>Таблица</th>
        <th>Engine</th>
        <th style="text-align:right;">Rows*</th>
        <th style="text-align:right;">Data</th>
        <th style="text-align:right;">Index</th>
        <th style="text-align:right;">Total</th>
        <th style="text-align:right;">Free</th>
    </tr></thead><tbody>';

    foreach ($tables as $t) {
        $total = $t['data'] + $t['index'];
        $eng = strtoupper((string)$t['engine']);
        $badge = ($eng==='INNODB') ? 'ok' : (($eng==='MYISAM') ? 'warn' : 'warn');
        echo '<tr>
            <td><input type="checkbox" name="tables[]" value="'.dbop_h($t['table']).'"></td>
            <td><code>'.dbop_h($t['table']).'</code></td>
            <td><span class="dbop-badge '.$badge.'">'.dbop_h($eng).'</span></td>
            <td style="text-align:right;">'.dbop_h($t['rows']).'</td>
            <td style="text-align:right;">'.dbop_h(dbop_bytes($t['data'])).'</td>
            <td style="text-align:right;">'.dbop_h(dbop_bytes($t['index'])).'</td>
            <td style="text-align:right;"><b>'.dbop_h(dbop_bytes($total)).'</b></td>
            <td style="text-align:right;">'.dbop_h(dbop_bytes($t['free'])).'</td>
        </tr>';
    }

    echo '</tbody></table>';
    echo '<div class="dbop-small">* TABLE_ROWS в InnoDB - оценка, не точное значение.</div>';
    echo '</form>';

    echo '<hr><h4>Переключить движок таблиц (для FOREIGN KEY нужен InnoDB)</h4>';
    echo '<div class="dbop-small">Конвертация - это ALTER TABLE и может быть долгой. Делайте в непиковое время.</div>';
    echo '<div style="margin:8px 0;">';
    echo '<select id="dbop-engine-target" class="uniform"><option value="INNODB">INNODB</option><option value="MYISAM">MYISAM</option></select> ';
    echo '<button class="btn btn-warning" onclick="dbop_convertEngine();">Конвертировать выбранные таблицы</button>';
    echo '</div>';

    echo '<div id="dbop-maint-result"></div>';
}

if ($tab === 'indexes') {

    echo '<h3 style="margin-top:0;">Индексы (обычные)</h3>';
    echo '<div class="dbop-small">Индексы обычно безопаснее, чем UNIQUE/FK. Они ускоряют SELECT, но увеличивают размер БД и могут немного замедлить INSERT/UPDATE.</div>';

    if (!$defs['indexes']) {
        echo '<div class="alert alert-warning">Список рекомендуемых индексов пуст. Обычно это значит, что не найдены таблицы <code>'.dbop_h($prefix).'_post</code>/<code>'.dbop_h($prefix).'_comments</code> и т.д.</div>';
    }

    $rows = array();
    foreach ($defs['indexes'] as $d) {
        $table = $d['table'];
        $idxs = isset($tables[$table]) ? dbop_get_indexes($table) : array();
        list($ok,$reason) = dbop_validate_index_def($d, $tables);
        $exists = isset($tables[$table]) ? dbop_has_index_prefix($idxs, $d['cols'], false) : false;
        $status = $exists ? array('ok','уже есть') : ($ok ? array('warn','можно добавить') : array('bad',$reason));
        $rows[] = array($d, $status);
    }

    echo '<form id="dbop-index-form" onsubmit="return false;">';
    echo '<input type="hidden" name="user_hash" value="'.dbop_h($userHash).'">';

    echo '<table class="table table-normal table-hover dbop-table" style="width:100%;">';
    echo '<thead><tr>
        <th style="width:30px;"></th>
        <th>Статус</th>
        <th>Таблица</th>
        <th>Индекс</th>
        <th>Колонки</th>
        <th>Зачем</th>
    </tr></thead><tbody>';

    foreach ($rows as $r) {
        $d = $r[0];
        $st = $r[1];
        $can = ($st[0] === 'warn');
        echo '<tr>
            <td>'.($can ? '<input type="checkbox" name="items[]" value="'.dbop_h($d['id']).'">' : '<input type="checkbox" disabled>') .'</td>
            <td><span class="dbop-badge '.$st[0].'">'.dbop_h($st[1]).'</span></td>
            <td><code>'.dbop_h($d['table']).'</code></td>
            <td><code>'.dbop_h($d['name']).'</code></td>
            <td><code>'.dbop_h(implode(', ', $d['cols'])).'</code></td>
            <td>'.dbop_h($d['why']).'</td>
        </tr>';
    }

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

    echo '<button class="btn btn-success" onclick="dbop_applySelected(\'apply_indexes\', \'#dbop-index-form\');">Добавить выбранные индексы</button>';
    echo '<div id="dbop-index-result" style="margin-top:10px;"></div>';
    echo '</form>';
}

if ($tab === 'unique') {

    echo '<h3 style="margin-top:0;">UNIQUE ключи (с проверкой дублей)</h3>';
    echo '<div class="dbop-small">UNIQUE не применится, если уже есть дубликаты. Плагин покажет примеры дублей.</div>';

    if (!$defs['unique']) {
        echo '<div class="alert alert-warning">Список UNIQUE пуст (скорее всего, таблицы DLE не найдены по префиксу).</div>';
    }

    $rows = array();
    foreach ($defs['unique'] as $d) {
        $table = $d['table'];
        $idxs = isset($tables[$table]) ? dbop_get_indexes($table) : array();

        $exists = isset($tables[$table]) ? dbop_has_index_prefix($idxs, $d['cols'], true) : false;

        list($ok,$reason) = dbop_validate_unique_def($d, $tables);
        $status = $exists ? array('ok','уже есть') : ($ok ? array('warn','можно добавить') : array('bad',$reason));
        $rows[] = array($d,$status);
    }

    echo '<form id="dbop-unique-form" onsubmit="return false;">';
    echo '<input type="hidden" name="user_hash" value="'.dbop_h($userHash).'">';

    echo '<table class="table table-normal table-hover dbop-table" style="width:100%;">';
    echo '<thead><tr>
        <th style="width:30px;"></th>
        <th>Статус</th>
        <th>Таблица</th>
        <th>UNIQUE</th>
        <th>Колонки</th>
        <th>Комментарий</th>
        <th>Дубли (если есть)</th>
    </tr></thead><tbody>';

    foreach ($rows as $r) {
        $d = $r[0]; $st = $r[1];
        $can = ($st[0] === 'warn');
        $dupPreview = '';
        if ($st[0] === 'bad' && strpos($st[1], 'дубли') !== false && !empty($d['dup_sql'])) {
            $db->query($d['dup_sql']);
            $i = 0;
            $list = array();
            while (($row = $db->get_row()) && $i < 5) {
                $list[] = $row['v'].' (x'.$row['c'].')';
                $i++;
            }
            $db->free();
            if ($list) $dupPreview = implode('<br>', array_map('dbop_h', $list));
        }

        echo '<tr>
            <td>'.($can ? '<input type="checkbox" name="items[]" value="'.dbop_h($d['id']).'">' : '<input type="checkbox" disabled>') .'</td>
            <td><span class="dbop-badge '.$st[0].'">'.dbop_h($st[1]).'</span></td>
            <td><code>'.dbop_h($d['table']).'</code></td>
            <td><code>'.dbop_h($d['name']).'</code></td>
            <td><code>'.dbop_h(implode(', ', $d['cols'])).'</code></td>
            <td>'.dbop_h($d['why']).'</td>
            <td>'.($dupPreview ?: '<span class="dbop-small">-</span>').'</td>
        </tr>';
    }

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

    echo '<button class="btn btn-success" onclick="dbop_applySelected(\'apply_unique\', \'#dbop-unique-form\');">Добавить выбранные UNIQUE</button>';
    echo '<div id="dbop-unique-result" style="margin-top:10px;"></div>';
    echo '</form>';
}

if ($tab === 'fks') {

    echo '<h3 style="margin-top:0;">FOREIGN KEY (внешние ключи)</h3>';
    echo '<div class="alert alert-info">
<b>Что будет сделано:</b> добавятся связи между связанными таблицами DLE (например comments.post_id → post.id).<br>
<b>Почему это может не примениться:</b> не InnoDB, разные типы колонок, нет индекса на дочерней колонке, есть "сироты".
</div>';

    if (!$defs['fks']) {
        echo '<div class="alert alert-warning">Список FOREIGN KEY пуст (скорее всего, таблицы DLE не найдены по префиксу).</div>';
    }

    $needInno = array();
    foreach ($defs['fks'] as $d) {
        $needInno[$d['table']] = true;
        $needInno[$d['ref_table']] = true;
    }

    $notInno = array();
    foreach (array_keys($needInno) as $t) {
        if (isset($tables[$t])) {
            $eng = strtoupper((string)$tables[$t]['engine']);
            if ($eng !== 'INNODB') $notInno[] = $t.' ('.$eng.')';
        }
    }

    if ($notInno) {
        echo '<div class="alert alert-warning"><b>Сейчас не InnoDB:</b><br>'.implode('<br>', array_map('dbop_h', $notInno)).'<br>
        Сначала переведите эти таблицы в InnoDB (вкладка "Таблицы").</div>';
    } elseif ($defs['fks']) {
        echo '<div class="alert alert-success">Нужные таблицы уже InnoDB.</div>';
    }

    $rows = array();
    foreach ($defs['fks'] as $d) {
        list($ok,$reason) = dbop_validate_fk_def($d, $tables);
        $exists = dbop_fk_exists($d['table'], $d['col'], $d['ref_table'], $d['ref_col']);
        $status = $exists ? array('ok','уже есть') : ($ok ? array('warn','можно добавить') : array('bad',$reason));

        $orphanInfo = '';
        if (!empty($d['orphan_sql'])) {
            $row = $db->super_query($d['orphan_sql']);
            $cnt = isset($row['c']) ? (int)$row['c'] : 0;
            $orphanInfo = ($cnt>0) ? '<span class="dbop-badge bad">сироты: '.dbop_h($cnt).'</span>' : '<span class="dbop-badge ok">сирот нет</span>';
        }

        $rows[] = array($d,$status,$orphanInfo);
    }

    echo '<form id="dbop-fk-form" onsubmit="return false;">';
    echo '<input type="hidden" name="user_hash" value="'.dbop_h($userHash).'">';

    echo '<table class="table table-normal table-hover dbop-table" style="width:100%;">';
    echo '<thead><tr>
        <th style="width:30px;"></th>
        <th>Статус</th>
        <th>Связь</th>
        <th>Действия</th>
        <th>Зачем</th>
    </tr></thead><tbody>';

    foreach ($rows as $r) {
        $d = $r[0]; $st = $r[1]; $or = $r[2];
        $can = ($st[0] === 'warn');
        $rel = '<code>'.dbop_h($d['table']).'.'.$d['col'].'</code> → <code>'.dbop_h($d['ref_table']).'.'.$d['ref_col'].'</code>';
        $act = 'ON DELETE '.dbop_h($d['on_delete']).', ON UPDATE '.dbop_h($d['on_update']).' &nbsp; '.$or;
        echo '<tr>
            <td>'.($can ? '<input type="checkbox" name="items[]" value="'.dbop_h($d['id']).'">' : '<input type="checkbox" disabled>') .'</td>
            <td><span class="dbop-badge '.$st[0].'">'.dbop_h($st[1]).'</span></td>
            <td>'.$rel.'<br><span class="dbop-small">constraint: <code>'.dbop_h($d['name']).'</code></span></td>
            <td>'.$act.'</td>
            <td>'.dbop_h($d['why']).'</td>
        </tr>';
    }

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

    echo '<div class="alert alert-warning"><b>Перед применением FK:</b> убедитесь, что у вас есть бэкап. Если таблицы большие, ALTER может идти долго.</div>';
    echo '<button class="btn btn-success" onclick="dbop_applySelected(\'apply_fks\', \'#dbop-fk-form\');">Добавить выбранные FOREIGN KEY</button>';
    echo '<div id="dbop-fk-result" style="margin-top:10px;"></div>';
    echo '</form>';
}

if ($tab === 'docs') {

    echo '<h3 style="margin-top:0;">Инструкция</h3>';

    echo '<div class="panel panel-default"><div class="panel-heading"><b>1) Что это делает</b></div><div class="panel-body">
    <ul>
      <li><b>ANALYZE / CHECK / OPTIMIZE / REPAIR</b> - обслуживание таблиц.</li>
      <li><b>Индексы</b> - предлагает и добавляет индексы (если их нет).</li>
      <li><b>UNIQUE</b> - добавляет уникальные ключи только если нет дублей.</li>
      <li><b>FOREIGN KEY</b> - добавляет внешние ключи только если таблицы InnoDB, типы совпадают, нет сирот и есть индекс на дочерней колонке.</li>
    </ul>
    </div></div>';

    echo '<div class="panel panel-default"><div class="panel-heading"><b>2) Как работать - по шагам</b></div><div class="panel-body">
      <div class="dbop-steps">
        <div class="step">
          <h4>Шаг A: Обновить статистику</h4>
          <div>Вкладка <b>Таблицы</b> → выделить таблицы → <b>ANALYZE</b> → "Выполнить".</div>
        </div>
        <div class="step">
          <h4>Шаг B: Добавить индексы</h4>
          <div>Вкладка <b>Индексы</b> → отметить "можно добавить" → "Добавить выбранные индексы".</div>
        </div>
        <div class="step">
          <h4>Шаг C: UNIQUE (по желанию)</h4>
          <div>Вкладка <b>UNIQUE</b> → если есть дубли - сначала исправить дубли, потом добавлять.</div>
        </div>
        <div class="step">
          <h4>Шаг D: FOREIGN KEY (по желанию)</h4>
          <div>Вкладка <b>FOREIGN KEY</b> → проверить InnoDB + сироты. Если нужно - вкладка <b>Таблицы</b> → конвертация в InnoDB. После этого применить.</div>
        </div>
      </div>
    </div></div>';

    echo '<div class="panel panel-default"><div class="panel-heading"><b>3) Если пункт меню не появился</b></div><div class="panel-body">
      <div class="dbop-small">Иногда запись в <code>'.dbop_h(PREFIX).'_admin_sections</code> не добавляется (например, из-за ошибки установки). Нажмите кнопку ниже.</div>
      <button class="btn btn-primary" onclick="dbop_toggleSections(); return false;">Ссылка в меню «Сторонние модули» (вкл/выкл)</button>
      <div id="dbop-sections-result" style="margin-top:10px;"></div>
    </div></div>';

    echo '<div class="panel panel-default"><div class="panel-heading"><b>4) Логи</b></div><div class="panel-body">
      <div>Лог всех ALTER/команд: <code>/engine/data/dboptimizerpro.log</code></div>
    </div></div>';
}

echo '  </div>'; // content

echo '  <div class="'.$boxFooter.'" style="display:flex;justify-content:space-between;align-items:center;">';
echo '    <div class="dbop-small">DBOptimizerPro v'.dbop_h(DBOP_VERSION).'</div>';
echo '    <div class="dbop-small">prefix: <code>'.dbop_h($prefix).'</code></div>';
echo '  </div>';

echo '</div>'; // box

?>
<script>
function dbop_toggleAll(cb, name){
  var c = cb.checked;
  document.querySelectorAll('input[name="'+name+'"]').forEach(function(x){ x.checked = c; });
}
function dbop_applySelected(action, formSelector){
  var form = document.querySelector(formSelector);
  var items = [];
  form.querySelectorAll('input[name="items[]"]:checked').forEach(function(x){ items.push(x.value); });
  if (!items.length) { DLEalert('Ничего не выбрано', 'DBOptimizerPro'); return; }
  if (!confirm('Применить выбранные изменения? На больших таблицах это может занять время.')) return;

  var fd = new FormData();
  fd.append('user_hash', '<?=dbop_h($userHash)?>');
  items.forEach(function(v){ fd.append('items[]', v); });

  fetch('admin.php?mod=db_optimizer&action='+encodeURIComponent(action)+'&tab=<?=dbop_h($tab)?><?=isset($_GET['show_all']) && (int)$_GET['show_all']===1 ? '&show_all=1' : ''?>', {
    method: 'POST',
    body: fd,
    credentials: 'same-origin'
  }).then(r=>r.json()).then(function(data){
    var boxId = (action==='apply_indexes') ? 'dbop-index-result' : (action==='apply_unique' ? 'dbop-unique-result' : 'dbop-fk-result');
    var el = document.getElementById(boxId);
    if (!el) return;

    var html = '';
    if (data.success) {
      html += '<div class="alert alert-success"><b>'+escapeHtml(data.message)+'</b></div>';
      if (data.applied && data.applied.length) {
        html += '<div class="alert alert-info"><b>Применено:</b><pre style="white-space:pre-wrap;">'+escapeHtml(data.applied.join("\\n"))+'</pre></div>';
      }
      if (data.skipped && data.skipped.length) {
        html += '<div class="alert alert-warning"><b>Пропущено:</b><pre style="white-space:pre-wrap;">'+escapeHtml(JSON.stringify(data.skipped, null, 2))+'</pre></div>';
      }
    } else {
      html += '<div class="alert alert-danger">'+escapeHtml(data.message||'Ошибка')+'</div>';
    }
    el.innerHTML = html;
  }).catch(function(e){
    DLEalert('Ошибка: '+e, 'DBOptimizerPro');
  });
}
function dbop_runMaintenance(){
  var form = document.getElementById('dbop-maint-form');
  if (!form) return;
  var checked = form.querySelectorAll('input[name="tables[]"]:checked').length;
  if (!checked) { DLEalert('Выберите таблицы', 'DBOptimizerPro'); return; }
  if (!confirm('Выполнить команду обслуживания на выбранных таблицах?')) return;

  var fd = new FormData(form);
  fetch('admin.php?mod=db_optimizer&action=maintenance&tab=tables<?=isset($_GET['show_all']) && (int)$_GET['show_all']===1 ? '&show_all=1' : ''?>', {
    method: 'POST',
    body: fd,
    credentials: 'same-origin'
  }).then(r=>r.json()).then(function(data){
    var el = document.getElementById('dbop-maint-result');
    if (!el) return;
    if (!data.success) {
      el.innerHTML = '<div class="alert alert-danger">'+escapeHtml(data.message||'Ошибка')+'</div>';
      return;
    }
    var rows = data.results || [];
    var html = '<div class="alert alert-success"><b>'+escapeHtml(data.message)+'</b></div>';
    html += '<div class="alert alert-info"><pre style="white-space:pre-wrap;">'+escapeHtml(JSON.stringify(rows, null, 2))+'</pre></div>';
    el.innerHTML = html;
  }).catch(function(e){
    DLEalert('Ошибка: '+e, 'DBOptimizerPro');
  });
}
function dbop_convertEngine(){
  var form = document.getElementById('dbop-maint-form');
  if (!form) return;
  var checked = form.querySelectorAll('input[name="tables[]"]:checked').length;
  if (!checked) { DLEalert('Выберите таблицы', 'DBOptimizerPro'); return; }
  if (!confirm('Конвертировать выбранные таблицы? Это ALTER TABLE и может быть долгим.')) return;

  var fd = new FormData();
  fd.append('user_hash', '<?=dbop_h($userHash)?>');
  var target = document.getElementById('dbop-engine-target') ? document.getElementById('dbop-engine-target').value : 'INNODB';
  fd.append('target', target);
  form.querySelectorAll('input[name="tables[]"]:checked').forEach(function(x){ fd.append('tables[]', x.value); });

  fetch('admin.php?mod=db_optimizer&action=convert_engine&tab=tables<?=isset($_GET['show_all']) && (int)$_GET['show_all']===1 ? '&show_all=1' : ''?>', {
    method: 'POST',
    body: fd,
    credentials: 'same-origin'
  }).then(r=>r.json()).then(function(data){
    var el = document.getElementById('dbop-maint-result');
    if (!el) return;
    if (!data.success) {
      el.innerHTML = '<div class="alert alert-danger">'+escapeHtml(data.message||'Ошибка')+'</div>';
      return;
    }
    var html = '<div class="alert alert-success"><b>'+escapeHtml(data.message)+'</b></div>';
    if (data.applied && data.applied.length) html += '<div class="alert alert-info"><b>Применено:</b><pre style="white-space:pre-wrap;">'+escapeHtml(data.applied.join("\\n"))+'</pre></div>';
    if (data.skipped && data.skipped.length) html += '<div class="alert alert-warning"><b>Пропущено:</b><pre style="white-space:pre-wrap;">'+escapeHtml(JSON.stringify(data.skipped, null, 2))+'</pre></div>';
    el.innerHTML = html;
  }).catch(function(e){
    DLEalert('Ошибка: '+e, 'DBOptimizerPro');
  });
}
function dbop_toggleSections(){
  fetch('admin.php?mod=db_optimizer&action=sections&tab=docs&user_hash=<?=dbop_h($userHash)?><?=isset($_GET['show_all']) && (int)$_GET['show_all']===1 ? '&show_all=1' : ''?>', {
    method: 'GET',
    credentials: 'same-origin'
  }).then(r=>r.json()).then(function(data){
    var el = document.getElementById('dbop-sections-result');
    if (!el) { DLEalert(data.message||'Готово', 'DBOptimizerPro'); return; }
    el.innerHTML = data.success ? '<div class="alert alert-success">'+escapeHtml(data.message)+'</div>' : '<div class="alert alert-danger">'+escapeHtml(data.message||'Ошибка')+'</div>';
  }).catch(function(e){
    DLEalert('Ошибка: '+e, 'DBOptimizerPro');
  });
}
function escapeHtml(s){
  return String(s).replace(/[&<>"']/g, function(m){
    return ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[m]);
  });
}
</script>
<?php echofooter(); ?>

]]></replacecode>
      <enabled>1</enabled>
    </operation>
  </file>

</dleplugin>
 
Всем привет. Как можно оптимизировать движок DLE если на сайте 40 000 новостей с постерами?
1. Взять сервер шустрее, более производительный с хорошим процом и большим объемом памяти;
2. Установить и настроить Redis на сервере
3. После того, как возьмешь другой сервер, настрой и оптимизируй MySQL для быстрой работы;
4. Найти плагины, которые ускоряют и оптимизируют базу данных, желательно платные, чтобы они не сделали вредоносные инъекции.
 
Всем привет. Как можно оптимизировать движок DLE если на сайте 40 000 новостей с постерами?
Вот так примерно можно оптимизировать сервер с сайтом на dle и с более большей базой. Подогнать под своё железо Настройка-оптимизация VPS сервера (Стр. 1) - Visavi.net
 
Последнее редактирование:
ok_daa потестите
Код:
<?xml version="1.0" encoding="utf-8"?>
<dleplugin>
  <name>DBOptimizerPro</name>
  <description><![CDATA[
Оптимизация БД DLE: индексы, UNIQUE, FOREIGN KEY, обслуживание таблиц (ANALYZE/CHECK/OPTIMIZE/REPAIR).

Как открыть:
- Админка → Сторонние модули → DBOptimizerPro
- или /admin.php?mod=db_optimizer

Важно:
- ALTER TABLE/OPTIMIZE может быть долгим и блокировать таблицы.
- Перед применением сделайте бэкап.
- Лог: /engine/data/dboptimizerpro.log
]]></description>
  <icon></icon>
  <version>1.1.0</version>
  <dleversion>18.0</dleversion>
  <versioncompare>greater</versioncompare>
  <upgradeurl></upgradeurl>
  <filedelete>0</filedelete>
  <needplugin></needplugin>
  <mnotice>0</mnotice>

  <mysqlinstall><![CDATA[
INSERT IGNORE INTO `{prefix}_admin_sections` (`name`, `title`, `descr`, `icon`, `allow_groups`)
VALUES ('db_optimizer', 'DBOptimizerPro', 'Оптимизация БД (индексы, UNIQUE, FOREIGN KEY)', '', '1');
]]></mysqlinstall>

  <mysqlupgrade><![CDATA[]]></mysqlupgrade>
  <mysqlenable><![CDATA[]]></mysqlenable>
  <mysqldisable><![CDATA[]]></mysqldisable>

  <mysqldelete><![CDATA[
DELETE FROM `{prefix}_admin_sections` WHERE `name`='db_optimizer';
]]></mysqldelete>

  <phpinstall><![CDATA[
@unlink(ENGINE_DIR . '/cache/system/adminmenu.php');
]]></phpinstall>

  <phpupgrade><![CDATA[]]></phpupgrade>
  <phpenable><![CDATA[]]></phpenable>
  <phpdisable><![CDATA[]]></phpdisable>

  <phpdelete><![CDATA[
@unlink(ENGINE_DIR . '/cache/system/adminmenu.php');
]]></phpdelete>

  <notice><![CDATA[]]></notice>

  <file name="engine/inc/db_optimizer.php">
    <operation action="create">
      <replacecode><![CDATA[
<?php
/**
 * DBOptimizerPro - модуль админки DLE
 * Версия: 1.1.0
 *
 * Реализовано:
 * - Обслуживание таблиц: ANALYZE / CHECK / OPTIMIZE / REPAIR (REPAIR только MyISAM)
 * - Индексы (обычные)
 * - UNIQUE (с проверкой дублей)
 * - FOREIGN KEY (с проверками: InnoDB, типы, индекс, сироты)
 *
 * Важно:
 * - FOREIGN KEY сам по себе не "ускоритель". Чаще ускоряют индексы.
 * - Любой ALTER TABLE/OPTIMIZE может быть долгим и блокировать таблицы.
 * - Сделайте бэкап перед применением.
 */

if (!defined('DATALIFEENGINE') || !defined('LOGGED_IN')) {
    die('Hacking attempt!');
}

if (!isset($member_id) || !is_array($member_id) || (int)$member_id['user_group'] !== 1) {
    if (function_exists('msg')) {
        msg('error', 'Доступ запрещён', 'Модуль доступен только администраторам (группа 1).');
    }
    die('Access denied');
}

define('DBOP_VERSION', '1.1.0');

// ---------------- helpers ----------------

function dbop_h($v) { return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); }

function dbop_bytes($bytes) {
    $bytes = (float)$bytes;
    if ($bytes < 1024) return sprintf('%.0f B', $bytes);
    $u = array('KB','MB','GB','TB','PB');
    $i = 0;
    $bytes /= 1024;
    while ($bytes >= 1024 && $i < count($u)-1) { $bytes /= 1024; $i++; }
    return sprintf('%.2f %s', $bytes, $u[$i]);
}

function dbop_escape_like($s) {
    return str_replace(array('\\', '%', '_'), array('\\\\', '\\%', '\\_'), (string)$s);
}

function dbop_require_hash() {
    global $dle_login_hash;
    if (!isset($_REQUEST['user_hash']) || $_REQUEST['user_hash'] !== $dle_login_hash) {
        msg('error', 'Ошибка безопасности', 'Неверный ключ безопасности (user_hash). Обновите страницу и повторите действие.');
    }
}

function dbop_log($line) {
    $file = ENGINE_DIR . '/data/dboptimizerpro.log';
    $ts = date('Y-m-d H:i:s');
    @file_put_contents($file, '['.$ts.'] '.$line."\n", FILE_APPEND);
}

function dbop_is_safe_identifier($s) {
    return (bool)preg_match('/^[A-Za-z0-9_]+$/', (string)$s);
}

// --------- DB introspection ---------

function dbop_get_tables($prefix, $only_prefix=true) {
    global $db;

    $out = array();

    $where = "TABLE_SCHEMA = DATABASE()";
    if ($only_prefix && $prefix !== '') {
        // хотим только таблицы DLE: PREFIX_*
        $like = dbop_escape_like($prefix . '_') . '%';
        $where .= " AND TABLE_NAME LIKE '{$like}' ESCAPE '\\\\'";
    }

    $sql = "SELECT TABLE_NAME, ENGINE, TABLE_ROWS, DATA_LENGTH, INDEX_LENGTH, DATA_FREE
            FROM information_schema.TABLES
            WHERE {$where}
            ORDER BY TABLE_NAME";
    $db->query($sql);
    while ($r = $db->get_row()) {
        $out[$r['TABLE_NAME']] = array(
            'table'  => $r['TABLE_NAME'],
            'engine' => $r['ENGINE'],
            'rows'   => (int)$r['TABLE_ROWS'],
            'data'   => (int)$r['DATA_LENGTH'],
            'index'  => (int)$r['INDEX_LENGTH'],
            'free'   => (int)$r['DATA_FREE'],
        );
    }
    $db->free();
    return $out;
}

function dbop_get_columns($table) {
    global $db;
    $cols = array();
    $db->query("SHOW COLUMNS FROM `{$table}`");
    while ($r = $db->get_row()) $cols[] = $r['Field'];
    $db->free();
    return $cols;
}

function dbop_get_column_types($table) {
    global $db;
    $types = array();
    $db->query("SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT
                FROM information_schema.COLUMNS
                WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '".$db->safesql($table)."'");
    while ($r = $db->get_row()) {
        $types[$r['COLUMN_NAME']] = array(
            'type' => $r['COLUMN_TYPE'],
            'null' => $r['IS_NULLABLE'],
            'def'  => $r['COLUMN_DEFAULT'],
        );
    }
    $db->free();
    return $types;
}

function dbop_get_indexes($table) {
    global $db;
    $idx = array();
    $db->query("SHOW INDEX FROM `{$table}`");
    while ($r = $db->get_row()) {
        $name = $r['Key_name'];
        if (!isset($idx[$name])) {
            $idx[$name] = array(
                'unique' => ((int)$r['Non_unique'] === 0),
                'cols'   => array(),
            );
        }
        $seq = (int)$r['Seq_in_index'];
        $idx[$name]['cols'][$seq] = $r['Column_name'];
    }
    $db->free();
    foreach ($idx as $k => $v) {
        ksort($idx[$k]['cols']);
        $idx[$k]['cols'] = array_values($idx[$k]['cols']);
    }
    return $idx;
}

function dbop_has_index_prefix($indexes, $cols, $need_unique=false) {
    $n = count($cols);
    foreach ($indexes as $idx) {
        if ($need_unique && empty($idx['unique'])) continue;
        if (count($idx['cols']) < $n) continue;
        $ok = true;
        for ($i=0; $i<$n; $i++) {
            if ((string)$idx['cols'][$i] !== (string)$cols[$i]) { $ok = false; break; }
        }
        if ($ok) return true;
    }
    return false;
}

function dbop_fk_exists($table, $col, $ref_table, $ref_col) {
    global $db;
    $sql = "SELECT CONSTRAINT_NAME
            FROM information_schema.KEY_COLUMN_USAGE
            WHERE TABLE_SCHEMA = DATABASE()
              AND TABLE_NAME = '".$db->safesql($table)."'
              AND COLUMN_NAME = '".$db->safesql($col)."'
              AND REFERENCED_TABLE_NAME = '".$db->safesql($ref_table)."'
              AND REFERENCED_COLUMN_NAME = '".$db->safesql($ref_col)."'
            LIMIT 1";
    $row = $db->super_query($sql);
    return !empty($row['CONSTRAINT_NAME']);
}

function dbop_get_table_engine($tables, $table) {
    return isset($tables[$table]['engine']) ? strtoupper((string)$tables[$table]['engine']) : '';
}

// --------- Definitions (indexes, unique, foreign keys) ---------

function dbop_defs($prefix, $tables) {

    $p = ($prefix !== '') ? ($prefix . '_') : '';

    $t_post   = $p . 'post';
    $t_comm   = $p . 'comments';
    $t_users  = $p . 'users';
    $t_extra  = $p . 'post_extras';
    $t_tags   = $p . 'tags';

    $has = function($t) use ($tables) { return isset($tables[$t]); };

    $defs = array(
        'indexes' => array(),
        'unique'  => array(),
        'fks'     => array(),
    );

    // ---- обычные индексы ----
    if ($has($t_post)) {
        $defs['indexes'][] = array(
            'id'    => 'idx_post_approve_date',
            'table' => $t_post,
            'name'  => 'idx_approve_date',
            'cols'  => array('approve','date'),
            'why'   => 'Частая выборка: approve=1 + сортировка по date (главная/категории).',
        );
        $defs['indexes'][] = array(
            'id'    => 'idx_post_autor',
            'table' => $t_post,
            'name'  => 'idx_autor',
            'cols'  => array('autor'),
            'why'   => 'Выборки по автору.',
        );
    }

    if ($has($t_comm)) {
        $defs['indexes'][] = array(
            'id'    => 'idx_comments_post_approve_date',
            'table' => $t_comm,
            'name'  => 'idx_post_approve_date',
            'cols'  => array('post_id','approve','date'),
            'why'   => 'Вывод комментариев: post_id + approve + сортировка по date.',
        );
    }

    if ($has($t_tags)) {
        $defs['indexes'][] = array(
            'id'    => 'idx_tags_tag',
            'table' => $t_tags,
            'name'  => 'idx_tag',
            'cols'  => array('tag'),
            'why'   => 'Поиск по тегу.',
        );
        $defs['indexes'][] = array(
            'id'    => 'idx_tags_news_id',
            'table' => $t_tags,
            'name'  => 'idx_news_id',
            'cols'  => array('news_id'),
            'why'   => 'Связь тегов с новостями.',
        );
    }

    // ---- UNIQUE ----
    if ($has($t_users)) {
        $defs['unique'][] = array(
            'id'    => 'uq_users_name',
            'table' => $t_users,
            'name'  => 'uq_name',
            'cols'  => array('name'),
            'why'   => 'Имя пользователя обычно должно быть уникальным.',
            'dup_sql' => "SELECT `name` AS v, COUNT(*) AS c FROM `{$t_users}` GROUP BY `name` HAVING c>1 LIMIT 50",
        );
        $defs['unique'][] = array(
            'id'    => 'uq_users_email',
            'table' => $t_users,
            'name'  => 'uq_email',
            'cols'  => array('email'),
            'why'   => 'Email часто уникален. Если у вас разрешены одинаковые email - не ставьте.',
            'dup_sql' => "SELECT `email` AS v, COUNT(*) AS c FROM `{$t_users}` WHERE `email`<>'' GROUP BY `email` HAVING c>1 LIMIT 50",
        );
    }

    if ($has($t_extra)) {
        $defs['unique'][] = array(
            'id'    => 'uq_post_extras_news_id',
            'table' => $t_extra,
            'name'  => 'uq_news_id',
            'cols'  => array('news_id'),
            'why'   => 'Обычно 1 запись post_extras на 1 новость. Ускоряет JOIN по news_id и защищает от дублей.',
            'dup_sql' => "SELECT `news_id` AS v, COUNT(*) AS c FROM `{$t_extra}` GROUP BY `news_id` HAVING c>1 LIMIT 50",
        );
    }

    if ($has($t_tags)) {
        $defs['unique'][] = array(
            'id'    => 'uq_tags_news_tag',
            'table' => $t_tags,
            'name'  => 'uq_news_tag',
            'cols'  => array('news_id','tag'),
            'why'   => 'Убирает дубли связей (news_id + tag).',
            'dup_sql' => "SELECT CONCAT(`news_id`,':',`tag`) AS v, COUNT(*) AS c FROM `{$t_tags}` GROUP BY `news_id`,`tag` HAVING c>1 LIMIT 50",
        );
    }

    // ---- FOREIGN KEYS ----
    if ($has($t_comm) && $has($t_post)) {
        $defs['fks'][] = array(
            'id'        => 'fk_comments_post',
            'table'     => $t_comm,
            'col'       => 'post_id',
            'ref_table' => $t_post,
            'ref_col'   => 'id',
            'name'      => 'fk_comments_post',
            'on_delete' => 'CASCADE',
            'on_update' => 'CASCADE',
            'why'       => 'Комментарии должны ссылаться на существующую новость. При удалении новости логично удалять комментарии.',
            'orphan_sql'=> "SELECT COUNT(*) AS c FROM `{$t_comm}` c LEFT JOIN `{$t_post}` p ON p.`id`=c.`post_id` WHERE c.`post_id`<>0 AND p.`id` IS NULL",
        );
    }

    if ($has($t_extra) && $has($t_post)) {
        $defs['fks'][] = array(
            'id'        => 'fk_post_extras_post',
            'table'     => $t_extra,
            'col'       => 'news_id',
            'ref_table' => $t_post,
            'ref_col'   => 'id',
            'name'      => 'fk_post_extras_post',
            'on_delete' => 'CASCADE',
            'on_update' => 'CASCADE',
            'why'       => 'post_extras должен ссылаться на новость. При удалении новости extras должны удаляться.',
            'orphan_sql'=> "SELECT COUNT(*) AS c FROM `{$t_extra}` e LEFT JOIN `{$t_post}` p ON p.`id`=e.`news_id` WHERE e.`news_id`<>0 AND p.`id` IS NULL",
        );
    }

    if ($has($t_tags) && $has($t_post)) {
        $defs['fks'][] = array(
            'id'        => 'fk_tags_post',
            'table'     => $t_tags,
            'col'       => 'news_id',
            'ref_table' => $t_post,
            'ref_col'   => 'id',
            'name'      => 'fk_tags_post',
            'on_delete' => 'CASCADE',
            'on_update' => 'CASCADE',
            'why'       => 'Связи тегов должны ссылаться на существующую новость.',
            'orphan_sql'=> "SELECT COUNT(*) AS c FROM `{$t_tags}` t LEFT JOIN `{$t_post}` p ON p.`id`=t.`news_id` WHERE t.`news_id`<>0 AND p.`id` IS NULL",
        );
    }

    return $defs;
}

// ------------- Validation for defs -------------

function dbop_validate_index_def($def, $tables) {
    $table = $def['table'];
    if (!isset($tables[$table])) return array(false, 'Таблица не найдена');
    $cols = dbop_get_columns($table);
    $set = array_flip($cols);
    foreach ($def['cols'] as $c) {
        if (!isset($set[$c])) return array(false, 'Нет колонки: '.$c);
    }
    $idxs = dbop_get_indexes($table);
    if (dbop_has_index_prefix($idxs, $def['cols'], false)) return array(false, 'Индекс уже есть');
    return array(true, '');
}

function dbop_validate_unique_def($def, $tables) {
    $table = $def['table'];
    if (!isset($tables[$table])) return array(false, 'Таблица не найдена');
    $cols = dbop_get_columns($table);
    $set = array_flip($cols);
    foreach ($def['cols'] as $c) {
        if (!isset($set[$c])) return array(false, 'Нет колонки: '.$c);
    }
    $idxs = dbop_get_indexes($table);
    if (dbop_has_index_prefix($idxs, $def['cols'], true)) return array(false, 'UNIQUE уже есть');
    global $db;
    if (!empty($def['dup_sql'])) {
        $db->query($def['dup_sql']);
        $row = $db->get_row();
        $hasDup = !empty($row);
        $db->free();
        if ($hasDup) return array(false, 'Есть дубли (сначала исправьте)');
    }
    return array(true, '');
}

function dbop_validate_fk_def($def, $tables) {
    global $db;

    $t  = $def['table'];
    $rt = $def['ref_table'];
    $c  = $def['col'];
    $rc = $def['ref_col'];

    if (!isset($tables[$t]) || !isset($tables[$rt])) return array(false, 'Таблица/ссылка не найдена');

    $eng1 = dbop_get_table_engine($tables, $t);
    $eng2 = dbop_get_table_engine($tables, $rt);
    if ($eng1 !== 'INNODB' || $eng2 !== 'INNODB') return array(false, 'Требуется InnoDB (обе таблицы)');

    $cols1 = array_flip(dbop_get_columns($t));
    $cols2 = array_flip(dbop_get_columns($rt));
    if (!isset($cols1[$c]))  return array(false, 'Нет колонки '.$c.' в '.$t);
    if (!isset($cols2[$rc])) return array(false, 'Нет колонки '.$rc.' в '.$rt);

    $types1 = dbop_get_column_types($t);
    $types2 = dbop_get_column_types($rt);
    $ct1 = isset($types1[$c]['type']) ? strtolower($types1[$c]['type']) : '';
    $ct2 = isset($types2[$rc]['type']) ? strtolower($types2[$rc]['type']) : '';
    if ($ct1 && $ct2 && $ct1 !== $ct2) {
        return array(false, 'Типы колонок не совпадают: '.$ct1.' vs '.$ct2);
    }

    $idx1 = dbop_get_indexes($t);
    $hasChildIndex = dbop_has_index_prefix($idx1, array($c), false) || dbop_has_index_prefix($idx1, array($c), true);
    if (!$hasChildIndex) return array(false, 'Нужен индекс на '.$t.'.'.$c);

    if (!empty($def['orphan_sql'])) {
        $row = $db->super_query($def['orphan_sql']);
        $cnt = isset($row['c']) ? (int)$row['c'] : 0;
        if ($cnt > 0) return array(false, 'Есть сироты: '.$cnt);
    }

    if (dbop_fk_exists($t, $c, $rt, $rc)) return array(false, 'FK уже существует');

    return array(true, '');
}

// --------- Apply operations ---------

function dbop_apply_index($def) {
    global $db;
    $table = $def['table'];
    $name  = $def['name'];
    $cols  = $def['cols'];

    if (!dbop_is_safe_identifier($name)) return array(false, 'Плохое имя индекса');
    foreach ($cols as $c) if (!dbop_is_safe_identifier($c)) return array(false, 'Плохая колонка');

    $cols_sql = '`' . implode('`,`', $cols) . '`';
    $sql = "ALTER TABLE `{$table}` ADD INDEX `{$name}` ({$cols_sql})";
    dbop_log('Alter: '.$sql);
    $db->query($sql);
    return array(true, $sql);
}

function dbop_apply_unique($def) {
    global $db;
    $table = $def['table'];
    $name  = $def['name'];
    $cols  = $def['cols'];

    if (!dbop_is_safe_identifier($name)) return array(false, 'Плохое имя индекса');
    foreach ($cols as $c) if (!dbop_is_safe_identifier($c)) return array(false, 'Плохая колонка');

    $cols_sql = '`' . implode('`,`', $cols) . '`';
    $sql = "ALTER TABLE `{$table}` ADD UNIQUE INDEX `{$name}` ({$cols_sql})";
    dbop_log('Alter: '.$sql);
    $db->query($sql);
    return array(true, $sql);
}

function dbop_apply_fk($def) {
    global $db;
    $t  = $def['table'];
    $c  = $def['col'];
    $rt = $def['ref_table'];
    $rc = $def['ref_col'];
    $name = $def['name'];

    foreach (array($c,$rc,$name) as $x) {
        if (!dbop_is_safe_identifier($x)) return array(false, 'Плохой идентификатор');
    }

    $onDel = !empty($def['on_delete']) ? strtoupper($def['on_delete']) : 'RESTRICT';
    $onUpd = !empty($def['on_update']) ? strtoupper($def['on_update']) : 'RESTRICT';
    $allowed = array('RESTRICT','CASCADE','SET NULL','NO ACTION');
    if (!in_array($onDel, $allowed, true)) $onDel = 'RESTRICT';
    if (!in_array($onUpd, $allowed, true)) $onUpd = 'RESTRICT';

    $sql = "ALTER TABLE `{$t}` ADD CONSTRAINT `{$name}` FOREIGN KEY (`{$c}`) REFERENCES `{$rt}`(`{$rc}`) ON DELETE {$onDel} ON UPDATE {$onUpd}";
    dbop_log('Alter: '.$sql);
    $db->query($sql);
    return array(true, $sql);
}

function dbop_convert_engine($table, $engine) {
    global $db;
    $engine = strtoupper($engine);
    if (!in_array($engine, array('INNODB','MYISAM'), true)) return array(false, 'Недопустимый engine');
    $sql = "ALTER TABLE `{$table}` ENGINE={$engine}";
    dbop_log('Alter: '.$sql);
    $db->query($sql);
    return array(true, $sql);
}

// ---------------- Controller ----------------

$prefix = defined('PREFIX') ? PREFIX : ((isset($config['dbprefix']) && $config['dbprefix']) ? $config['dbprefix'] : '');
$only_prefix = true;
if (isset($_GET['show_all']) && (int)$_GET['show_all'] === 1) $only_prefix = false;

$tables = dbop_get_tables($prefix, $only_prefix);
$defs   = dbop_defs($prefix, $tables);

$tab = isset($_REQUEST['tab']) ? (string)$_REQUEST['tab'] : 'home';
$action = isset($_REQUEST['action']) ? (string)$_REQUEST['action'] : '';

function dbop_json_exit($arr) {
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($arr, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
    exit;
}

// AJAX actions
if ($action) {
    dbop_require_hash();

    if ($action === 'sections') {
        // добавить/убрать из "Сторонние модули" (на случай, если запись не создалась при установке)
        global $db;
        $row = $db->super_query("SELECT id FROM " . PREFIX . "_admin_sections WHERE name='db_optimizer'");
        if ($row) {
            $db->query("DELETE FROM " . PREFIX . "_admin_sections WHERE name='db_optimizer'");
            @unlink(ENGINE_DIR . '/cache/system/adminmenu.php');
            dbop_json_exit(array('success'=>true,'message'=>'Ссылка убрана из меню «Сторонние модули»'));
        } else {
            $db->query("INSERT IGNORE INTO " . PREFIX . "_admin_sections (name, title, descr, icon, allow_groups)
                        VALUES ('db_optimizer', 'DBOptimizerPro', 'Оптимизация БД (индексы, UNIQUE, FOREIGN KEY)', '', '1')");
            @unlink(ENGINE_DIR . '/cache/system/adminmenu.php');
            dbop_json_exit(array('success'=>true,'message'=>'Ссылка добавлена в меню «Сторонние модули»'));
        }
    }

    if ($action === 'maintenance') {
        $cmd = strtolower((string)($_POST['cmd'] ?? 'analyze'));
        $allowed_cmds = array('analyze'=>'ANALYZE','check'=>'CHECK','optimize'=>'OPTIMIZE','repair'=>'REPAIR');
        if (!isset($allowed_cmds[$cmd])) dbop_json_exit(array('success'=>false,'message'=>'Неизвестная команда'));

        $sel = isset($_POST['tables']) && is_array($_POST['tables']) ? $_POST['tables'] : array();
        $sel = array_values(array_unique(array_map('strval',$sel)));
        $safe = array();
        foreach ($sel as $t) if (isset($tables[$t])) $safe[] = $t;
        if (!$safe) dbop_json_exit(array('success'=>false,'message'=>'Не выбраны таблицы'));

        $results = array();
        foreach ($safe as $tname) {
            $sqlCmd = $allowed_cmds[$cmd];

            if ($sqlCmd === 'REPAIR') {
                $eng = strtoupper((string)$tables[$tname]['engine']);
                if ($eng !== 'MYISAM') {
                    $results[] = array('table'=>$tname,'status'=>'SKIP','msg'=>'REPAIR только для MyISAM');
                    continue;
                }
            }

            $q = "{$sqlCmd} TABLE `{$tname}`";
            dbop_log('Run: '.$q);
            $db->query($q);
            while ($r = $db->get_row()) {
                $results[] = array('table'=>$r['Table'],'type'=>$r['Msg_type'],'msg'=>$r['Msg_text']);
            }
            $db->free();
        }

        dbop_json_exit(array('success'=>true,'message'=>'Готово','results'=>$results));
    }

    if ($action === 'apply_indexes' || $action === 'apply_unique' || $action === 'apply_fks') {
        $items = isset($_POST['items']) && is_array($_POST['items']) ? $_POST['items'] : array();
        $items = array_values(array_unique(array_map('strval',$items)));
        if (!$items) dbop_json_exit(array('success'=>false,'message'=>'Ничего не выбрано'));

        $kind = ($action === 'apply_indexes') ? 'indexes' : (($action === 'apply_unique') ? 'unique' : 'fks');
        $defsList = $defs[$kind];

        $map = array();
        foreach ($defsList as $d) $map[$d['id']] = $d;

        $applied = array();
        $skipped = array();

        foreach ($items as $id) {
            if (!isset($map[$id])) { $skipped[] = array('id'=>$id,'reason'=>'unknown'); continue; }
            $d = $map[$id];

            if ($kind === 'indexes') {
                list($ok,$why) = dbop_validate_index_def($d, $tables);
                if (!$ok) { $skipped[] = array('id'=>$id,'reason'=>$why); continue; }
                list($ok2,$sql) = dbop_apply_index($d);
                if (!$ok2) { $skipped[] = array('id'=>$id,'reason'=>$sql); continue; }
                $applied[] = $sql;
            }
            if ($kind === 'unique') {
                list($ok,$why) = dbop_validate_unique_def($d, $tables);
                if (!$ok) { $skipped[] = array('id'=>$id,'reason'=>$why); continue; }
                list($ok2,$sql) = dbop_apply_unique($d);
                if (!$ok2) { $skipped[] = array('id'=>$id,'reason'=>$sql); continue; }
                $applied[] = $sql;
            }
            if ($kind === 'fks') {
                list($ok,$why) = dbop_validate_fk_def($d, $tables);
                if (!$ok) { $skipped[] = array('id'=>$id,'reason'=>$why); continue; }
                list($ok2,$sql) = dbop_apply_fk($d);
                if (!$ok2) { $skipped[] = array('id'=>$id,'reason'=>$sql); continue; }
                $applied[] = $sql;
            }
        }

        dbop_json_exit(array(
            'success'=>true,
            'message'=>'Операция завершена',
            'applied'=>$applied,
            'skipped'=>$skipped,
        ));
    }

    if ($action === 'convert_engine') {
        $items = isset($_POST['tables']) && is_array($_POST['tables']) ? $_POST['tables'] : array();
        $items = array_values(array_unique(array_map('strval',$items)));
        $target = strtoupper((string)($_POST['target'] ?? 'INNODB'));
        if (!in_array($target, array('INNODB','MYISAM'), true)) $target = 'INNODB';
        if (!$items) dbop_json_exit(array('success'=>false,'message'=>'Не выбраны таблицы'));

        $applied = array();
        $skipped = array();

        foreach ($items as $t) {
            if (!isset($tables[$t])) { $skipped[] = array('table'=>$t,'reason'=>'unknown'); continue; }
            $cur = strtoupper((string)$tables[$t]['engine']);
            if ($cur === $target) { $skipped[] = array('table'=>$t,'reason'=>'уже '.$target); continue; }
            list($ok,$sql) = dbop_convert_engine($t, $target);
            if (!$ok) { $skipped[] = array('table'=>$t,'reason'=>$sql); continue; }
            $applied[] = $sql;
        }

        dbop_json_exit(array('success'=>true,'message'=>'Готово','applied'=>$applied,'skipped'=>$skipped));
    }

    dbop_json_exit(array('success'=>false,'message'=>'Unknown action'));
}

// ---------------- UI (Admin) ----------------

$__name  = 'DBOptimizerPro';
$__descr = 'Оптимизация БД (индексы, UNIQUE, FOREIGN KEY)';

echoheader($__name, $__descr);

$adminUrl = $config['admin_path'] ?? 'admin.php';
$userHash = $dle_login_hash;

$box = ($config['version_id'] >= 12) ? 'panel' : 'box';
$boxHeader = ($config['version_id'] >= 12) ? 'panel-heading' : 'box-header';
$boxContent = 'box-content';
$boxFooter = ($config['version_id'] >= 12) ? 'panel-footer' : 'box-footer';
$tabsClass = ($config['version_id'] >= 12) ? 'nav-tabs-solid' : 'nav-tabs-left';

function dbop_tab_link($tab, $label, $icon='') {
    $u = 'admin.php?mod=db_optimizer&tab='.$tab.(isset($_GET['show_all']) && (int)$_GET['show_all']===1 ? '&show_all=1' : '');
    $i = $icon ? '<i class="'.$icon.'"></i> ' : '';
    $active = ((isset($_REQUEST['tab']) && $_REQUEST['tab']===$tab) || (!isset($_REQUEST['tab']) && $tab==='home')) ? ' class="active"' : '';
    return '<li'.$active.'><a href="'.$u.'">'.$i.dbop_h($label).'</a></li>';
}

echo '<style>
/* читабельные табы (у вас были "белое на белом") */
.dbop-wrap .nav-tabs>li>a{color:#333 !important; background:#f6f7f9; border:1px solid #e2e5ea; margin-right:6px; border-bottom:none;}
.dbop-wrap .nav-tabs>li.active>a{background:#ffffff !important; color:#000 !important; font-weight:600;}
.dbop-wrap .nav-tabs{border-bottom:1px solid #e2e5ea;}
.dbop-steps .step{border:1px solid #e5e5e5;border-radius:10px;padding:12px;margin:10px 0;background:#fff;}
.dbop-steps .step h4{margin:0 0 6px 0;}
.dbop-small{color:#666;font-size:12px;}
.dbop-badge{display:inline-block;padding:3px 8px;border-radius:999px;font-size:12px;border:1px solid #ddd;background:#fafafa;}
.dbop-badge.ok{background:#e8fff0;border-color:#bfe8cd;}
.dbop-badge.bad{background:#fff0f0;border-color:#f1c1c1;}
.dbop-badge.warn{background:#fff8e6;border-color:#f2d49a;}
.dbop-table code{font-size:12px;}
</style>';

echo '<div class="dbop-wrap '.$box.'">';
echo '  <div class="'.$boxHeader.'">';
echo '    <ul class="nav nav-tabs '.$tabsClass.'">';
echo          dbop_tab_link('home', 'Быстрый старт', 'icon-home');
echo          dbop_tab_link('tables', 'Таблицы', 'icon-database');
echo          dbop_tab_link('indexes', 'Индексы', 'icon-list');
echo          dbop_tab_link('unique', 'UNIQUE', 'icon-check');
echo          dbop_tab_link('fks', 'FOREIGN KEY', 'icon-link');
echo          dbop_tab_link('docs', 'Инструкция', 'icon-book');
echo '    </ul>';
echo '  </div>';

echo '  <div class="'.$boxContent.'">';

echo '<div class="alert alert-warning" style="margin-bottom:10px;">
<b>DBOptimizerPro v'.dbop_h(DBOP_VERSION).'</b><br>
Изменения структуры (ALTER TABLE, OPTIMIZE) могут быть долгими и блокировать таблицы.
Перед применением сделайте бэкап. Лог: <code>/engine/data/dboptimizerpro.log</code>
</div>';

if ($prefix === '') {
    echo '<div class="alert alert-danger"><b>Проблема:</b> не удалось определить PREFIX. Модуль не знает, какие таблицы считать DLE.</div>';
} else {
    echo '<div class="alert alert-info" style="margin-bottom:10px;">
<b>Фильтр:</b> сейчас показаны '.($only_prefix ? 'только таблицы DLE (<code>'.dbop_h($prefix).'_*</code>)' : 'все таблицы текущей БД').'.
<a href="admin.php?mod=db_optimizer&tab='.dbop_h($tab).($only_prefix ? '&show_all=1' : '').'">'.($only_prefix ? 'Показать все' : 'Показать только DLE').'</a>
</div>';
}

if ($tab === 'home') {

    $totalData = 0; $totalIdx = 0;
    foreach ($tables as $t) { $totalData += $t['data']; $totalIdx += $t['index']; }

    echo '<div class="dbop-steps">';

    echo '<div class="step">
        <h4>Сценарий 1. Безопасно и быстро (рекомендуется)</h4>
        <div class="dbop-small">Обновить статистику БД и добавить обычные индексы (без UNIQUE и FOREIGN KEY).</div>
        <ol style="margin:8px 0 0 18px;">
          <li>Вкладка <b>Таблицы</b> → выделите таблицы → <b>ANALYZE</b> → "Выполнить".</li>
          <li>Вкладка <b>Индексы</b> → отметьте "можно добавить" → "Добавить выбранные индексы".</li>
        </ol>
    </div>';

    echo '<div class="step">
        <h4>Сценарий 2. Уникальность данных (UNIQUE)</h4>
        <div class="dbop-small">UNIQUE не применится, если в таблице есть дубли - модуль покажет примеры.</div>
        <ol style="margin:8px 0 0 18px;">
          <li>Вкладка <b>UNIQUE</b> → проверьте статус.</li>
          <li>Если есть дубли - сначала исправьте, потом применяйте.</li>
        </ol>
    </div>';

    echo '<div class="step">
        <h4>Сценарий 3. FOREIGN KEY (осторожно)</h4>
        <div class="dbop-small">FOREIGN KEY повышает целостность. Требует InnoDB и отсутствие "сирот".</div>
        <ol style="margin:8px 0 0 18px;">
          <li>Вкладка <b>FOREIGN KEY</b> → убедитесь, что всё "можно добавить".</li>
          <li>Если таблицы не InnoDB - вкладка <b>Таблицы</b> → конвертация в InnoDB.</li>
        </ol>
    </div>';

    echo '</div>';

    echo '<hr>';
    echo '<div class="row"><div class="col-md-6">
        <div class="alert alert-info">
        <b>Сводка</b><br>
        Таблиц: <b>'.count($tables).'</b><br>
        Data: <b>'.dbop_h(dbop_bytes($totalData)).'</b><br>
        Index: <b>'.dbop_h(dbop_bytes($totalIdx)).'</b><br>
        Total: <b>'.dbop_h(dbop_bytes($totalData+$totalIdx)).'</b>
        </div>
    </div><div class="col-md-6">
        <div class="alert alert-default">
        <b>Что реально ускоряет чаще всего</b><br>
        1) индексы под реальные WHERE/ORDER BY<br>
        2) кэш (Redis/Memcached), PHP-FPM, настройки MySQL<br>
        3) анализ slow query log и правка запросов
        </div>
    </div></div>';
}

if ($tab === 'tables') {

    echo '<h3 style="margin-top:0;">Таблицы и обслуживание</h3>';
    echo '<div class="dbop-small">Команды: <b>ANALYZE</b> (рекомендуется), <b>CHECK</b>, <b>OPTIMIZE</b> (осторожно), <b>REPAIR</b> (только MyISAM).</div>';

    echo '<div class="alert alert-warning" style="margin-top:10px;">
<b>OPTIMIZE</b> на InnoDB часто равен перестройке таблицы и может быть очень долгим.
Используйте после массовых удалений или если видите реальную фрагментацию/раздувание.
</div>';

    echo '<form id="dbop-maint-form" onsubmit="return false;">';
    echo '<input type="hidden" name="user_hash" value="'.dbop_h($userHash).'">';

    echo '<div style="margin:8px 0;">';
    echo '<select name="cmd" class="uniform">
            <option value="analyze">ANALYZE (рекомендуется)</option>
            <option value="check">CHECK</option>
            <option value="optimize">OPTIMIZE (осторожно)</option>
            <option value="repair">REPAIR (только MyISAM)</option>
          </select> ';
    echo '<button class="btn btn-primary" onclick="dbop_runMaintenance();">Выполнить команду на выбранных таблицах</button>';
    echo '</div>';

    echo '<table class="table table-normal table-hover dbop-table" style="width:100%;">';
    echo '<thead><tr>
        <th style="width:30px;"><input type="checkbox" onclick="dbop_toggleAll(this, \'tables[]\');"></th>
        <th>Таблица</th>
        <th>Engine</th>
        <th style="text-align:right;">Rows*</th>
        <th style="text-align:right;">Data</th>
        <th style="text-align:right;">Index</th>
        <th style="text-align:right;">Total</th>
        <th style="text-align:right;">Free</th>
    </tr></thead><tbody>';

    foreach ($tables as $t) {
        $total = $t['data'] + $t['index'];
        $eng = strtoupper((string)$t['engine']);
        $badge = ($eng==='INNODB') ? 'ok' : (($eng==='MYISAM') ? 'warn' : 'warn');
        echo '<tr>
            <td><input type="checkbox" name="tables[]" value="'.dbop_h($t['table']).'"></td>
            <td><code>'.dbop_h($t['table']).'</code></td>
            <td><span class="dbop-badge '.$badge.'">'.dbop_h($eng).'</span></td>
            <td style="text-align:right;">'.dbop_h($t['rows']).'</td>
            <td style="text-align:right;">'.dbop_h(dbop_bytes($t['data'])).'</td>
            <td style="text-align:right;">'.dbop_h(dbop_bytes($t['index'])).'</td>
            <td style="text-align:right;"><b>'.dbop_h(dbop_bytes($total)).'</b></td>
            <td style="text-align:right;">'.dbop_h(dbop_bytes($t['free'])).'</td>
        </tr>';
    }

    echo '</tbody></table>';
    echo '<div class="dbop-small">* TABLE_ROWS в InnoDB - оценка, не точное значение.</div>';
    echo '</form>';

    echo '<hr><h4>Переключить движок таблиц (для FOREIGN KEY нужен InnoDB)</h4>';
    echo '<div class="dbop-small">Конвертация - это ALTER TABLE и может быть долгой. Делайте в непиковое время.</div>';
    echo '<div style="margin:8px 0;">';
    echo '<select id="dbop-engine-target" class="uniform"><option value="INNODB">INNODB</option><option value="MYISAM">MYISAM</option></select> ';
    echo '<button class="btn btn-warning" onclick="dbop_convertEngine();">Конвертировать выбранные таблицы</button>';
    echo '</div>';

    echo '<div id="dbop-maint-result"></div>';
}

if ($tab === 'indexes') {

    echo '<h3 style="margin-top:0;">Индексы (обычные)</h3>';
    echo '<div class="dbop-small">Индексы обычно безопаснее, чем UNIQUE/FK. Они ускоряют SELECT, но увеличивают размер БД и могут немного замедлить INSERT/UPDATE.</div>';

    if (!$defs['indexes']) {
        echo '<div class="alert alert-warning">Список рекомендуемых индексов пуст. Обычно это значит, что не найдены таблицы <code>'.dbop_h($prefix).'_post</code>/<code>'.dbop_h($prefix).'_comments</code> и т.д.</div>';
    }

    $rows = array();
    foreach ($defs['indexes'] as $d) {
        $table = $d['table'];
        $idxs = isset($tables[$table]) ? dbop_get_indexes($table) : array();
        list($ok,$reason) = dbop_validate_index_def($d, $tables);
        $exists = isset($tables[$table]) ? dbop_has_index_prefix($idxs, $d['cols'], false) : false;
        $status = $exists ? array('ok','уже есть') : ($ok ? array('warn','можно добавить') : array('bad',$reason));
        $rows[] = array($d, $status);
    }

    echo '<form id="dbop-index-form" onsubmit="return false;">';
    echo '<input type="hidden" name="user_hash" value="'.dbop_h($userHash).'">';

    echo '<table class="table table-normal table-hover dbop-table" style="width:100%;">';
    echo '<thead><tr>
        <th style="width:30px;"></th>
        <th>Статус</th>
        <th>Таблица</th>
        <th>Индекс</th>
        <th>Колонки</th>
        <th>Зачем</th>
    </tr></thead><tbody>';

    foreach ($rows as $r) {
        $d = $r[0];
        $st = $r[1];
        $can = ($st[0] === 'warn');
        echo '<tr>
            <td>'.($can ? '<input type="checkbox" name="items[]" value="'.dbop_h($d['id']).'">' : '<input type="checkbox" disabled>') .'</td>
            <td><span class="dbop-badge '.$st[0].'">'.dbop_h($st[1]).'</span></td>
            <td><code>'.dbop_h($d['table']).'</code></td>
            <td><code>'.dbop_h($d['name']).'</code></td>
            <td><code>'.dbop_h(implode(', ', $d['cols'])).'</code></td>
            <td>'.dbop_h($d['why']).'</td>
        </tr>';
    }

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

    echo '<button class="btn btn-success" onclick="dbop_applySelected(\'apply_indexes\', \'#dbop-index-form\');">Добавить выбранные индексы</button>';
    echo '<div id="dbop-index-result" style="margin-top:10px;"></div>';
    echo '</form>';
}

if ($tab === 'unique') {

    echo '<h3 style="margin-top:0;">UNIQUE ключи (с проверкой дублей)</h3>';
    echo '<div class="dbop-small">UNIQUE не применится, если уже есть дубликаты. Плагин покажет примеры дублей.</div>';

    if (!$defs['unique']) {
        echo '<div class="alert alert-warning">Список UNIQUE пуст (скорее всего, таблицы DLE не найдены по префиксу).</div>';
    }

    $rows = array();
    foreach ($defs['unique'] as $d) {
        $table = $d['table'];
        $idxs = isset($tables[$table]) ? dbop_get_indexes($table) : array();

        $exists = isset($tables[$table]) ? dbop_has_index_prefix($idxs, $d['cols'], true) : false;

        list($ok,$reason) = dbop_validate_unique_def($d, $tables);
        $status = $exists ? array('ok','уже есть') : ($ok ? array('warn','можно добавить') : array('bad',$reason));
        $rows[] = array($d,$status);
    }

    echo '<form id="dbop-unique-form" onsubmit="return false;">';
    echo '<input type="hidden" name="user_hash" value="'.dbop_h($userHash).'">';

    echo '<table class="table table-normal table-hover dbop-table" style="width:100%;">';
    echo '<thead><tr>
        <th style="width:30px;"></th>
        <th>Статус</th>
        <th>Таблица</th>
        <th>UNIQUE</th>
        <th>Колонки</th>
        <th>Комментарий</th>
        <th>Дубли (если есть)</th>
    </tr></thead><tbody>';

    foreach ($rows as $r) {
        $d = $r[0]; $st = $r[1];
        $can = ($st[0] === 'warn');
        $dupPreview = '';
        if ($st[0] === 'bad' && strpos($st[1], 'дубли') !== false && !empty($d['dup_sql'])) {
            $db->query($d['dup_sql']);
            $i = 0;
            $list = array();
            while (($row = $db->get_row()) && $i < 5) {
                $list[] = $row['v'].' (x'.$row['c'].')';
                $i++;
            }
            $db->free();
            if ($list) $dupPreview = implode('<br>', array_map('dbop_h', $list));
        }

        echo '<tr>
            <td>'.($can ? '<input type="checkbox" name="items[]" value="'.dbop_h($d['id']).'">' : '<input type="checkbox" disabled>') .'</td>
            <td><span class="dbop-badge '.$st[0].'">'.dbop_h($st[1]).'</span></td>
            <td><code>'.dbop_h($d['table']).'</code></td>
            <td><code>'.dbop_h($d['name']).'</code></td>
            <td><code>'.dbop_h(implode(', ', $d['cols'])).'</code></td>
            <td>'.dbop_h($d['why']).'</td>
            <td>'.($dupPreview ?: '<span class="dbop-small">-</span>').'</td>
        </tr>';
    }

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

    echo '<button class="btn btn-success" onclick="dbop_applySelected(\'apply_unique\', \'#dbop-unique-form\');">Добавить выбранные UNIQUE</button>';
    echo '<div id="dbop-unique-result" style="margin-top:10px;"></div>';
    echo '</form>';
}

if ($tab === 'fks') {

    echo '<h3 style="margin-top:0;">FOREIGN KEY (внешние ключи)</h3>';
    echo '<div class="alert alert-info">
<b>Что будет сделано:</b> добавятся связи между связанными таблицами DLE (например comments.post_id → post.id).<br>
<b>Почему это может не примениться:</b> не InnoDB, разные типы колонок, нет индекса на дочерней колонке, есть "сироты".
</div>';

    if (!$defs['fks']) {
        echo '<div class="alert alert-warning">Список FOREIGN KEY пуст (скорее всего, таблицы DLE не найдены по префиксу).</div>';
    }

    $needInno = array();
    foreach ($defs['fks'] as $d) {
        $needInno[$d['table']] = true;
        $needInno[$d['ref_table']] = true;
    }

    $notInno = array();
    foreach (array_keys($needInno) as $t) {
        if (isset($tables[$t])) {
            $eng = strtoupper((string)$tables[$t]['engine']);
            if ($eng !== 'INNODB') $notInno[] = $t.' ('.$eng.')';
        }
    }

    if ($notInno) {
        echo '<div class="alert alert-warning"><b>Сейчас не InnoDB:</b><br>'.implode('<br>', array_map('dbop_h', $notInno)).'<br>
        Сначала переведите эти таблицы в InnoDB (вкладка "Таблицы").</div>';
    } elseif ($defs['fks']) {
        echo '<div class="alert alert-success">Нужные таблицы уже InnoDB.</div>';
    }

    $rows = array();
    foreach ($defs['fks'] as $d) {
        list($ok,$reason) = dbop_validate_fk_def($d, $tables);
        $exists = dbop_fk_exists($d['table'], $d['col'], $d['ref_table'], $d['ref_col']);
        $status = $exists ? array('ok','уже есть') : ($ok ? array('warn','можно добавить') : array('bad',$reason));

        $orphanInfo = '';
        if (!empty($d['orphan_sql'])) {
            $row = $db->super_query($d['orphan_sql']);
            $cnt = isset($row['c']) ? (int)$row['c'] : 0;
            $orphanInfo = ($cnt>0) ? '<span class="dbop-badge bad">сироты: '.dbop_h($cnt).'</span>' : '<span class="dbop-badge ok">сирот нет</span>';
        }

        $rows[] = array($d,$status,$orphanInfo);
    }

    echo '<form id="dbop-fk-form" onsubmit="return false;">';
    echo '<input type="hidden" name="user_hash" value="'.dbop_h($userHash).'">';

    echo '<table class="table table-normal table-hover dbop-table" style="width:100%;">';
    echo '<thead><tr>
        <th style="width:30px;"></th>
        <th>Статус</th>
        <th>Связь</th>
        <th>Действия</th>
        <th>Зачем</th>
    </tr></thead><tbody>';

    foreach ($rows as $r) {
        $d = $r[0]; $st = $r[1]; $or = $r[2];
        $can = ($st[0] === 'warn');
        $rel = '<code>'.dbop_h($d['table']).'.'.$d['col'].'</code> → <code>'.dbop_h($d['ref_table']).'.'.$d['ref_col'].'</code>';
        $act = 'ON DELETE '.dbop_h($d['on_delete']).', ON UPDATE '.dbop_h($d['on_update']).' &nbsp; '.$or;
        echo '<tr>
            <td>'.($can ? '<input type="checkbox" name="items[]" value="'.dbop_h($d['id']).'">' : '<input type="checkbox" disabled>') .'</td>
            <td><span class="dbop-badge '.$st[0].'">'.dbop_h($st[1]).'</span></td>
            <td>'.$rel.'<br><span class="dbop-small">constraint: <code>'.dbop_h($d['name']).'</code></span></td>
            <td>'.$act.'</td>
            <td>'.dbop_h($d['why']).'</td>
        </tr>';
    }

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

    echo '<div class="alert alert-warning"><b>Перед применением FK:</b> убедитесь, что у вас есть бэкап. Если таблицы большие, ALTER может идти долго.</div>';
    echo '<button class="btn btn-success" onclick="dbop_applySelected(\'apply_fks\', \'#dbop-fk-form\');">Добавить выбранные FOREIGN KEY</button>';
    echo '<div id="dbop-fk-result" style="margin-top:10px;"></div>';
    echo '</form>';
}

if ($tab === 'docs') {

    echo '<h3 style="margin-top:0;">Инструкция</h3>';

    echo '<div class="panel panel-default"><div class="panel-heading"><b>1) Что это делает</b></div><div class="panel-body">
    <ul>
      <li><b>ANALYZE / CHECK / OPTIMIZE / REPAIR</b> - обслуживание таблиц.</li>
      <li><b>Индексы</b> - предлагает и добавляет индексы (если их нет).</li>
      <li><b>UNIQUE</b> - добавляет уникальные ключи только если нет дублей.</li>
      <li><b>FOREIGN KEY</b> - добавляет внешние ключи только если таблицы InnoDB, типы совпадают, нет сирот и есть индекс на дочерней колонке.</li>
    </ul>
    </div></div>';

    echo '<div class="panel panel-default"><div class="panel-heading"><b>2) Как работать - по шагам</b></div><div class="panel-body">
      <div class="dbop-steps">
        <div class="step">
          <h4>Шаг A: Обновить статистику</h4>
          <div>Вкладка <b>Таблицы</b> → выделить таблицы → <b>ANALYZE</b> → "Выполнить".</div>
        </div>
        <div class="step">
          <h4>Шаг B: Добавить индексы</h4>
          <div>Вкладка <b>Индексы</b> → отметить "можно добавить" → "Добавить выбранные индексы".</div>
        </div>
        <div class="step">
          <h4>Шаг C: UNIQUE (по желанию)</h4>
          <div>Вкладка <b>UNIQUE</b> → если есть дубли - сначала исправить дубли, потом добавлять.</div>
        </div>
        <div class="step">
          <h4>Шаг D: FOREIGN KEY (по желанию)</h4>
          <div>Вкладка <b>FOREIGN KEY</b> → проверить InnoDB + сироты. Если нужно - вкладка <b>Таблицы</b> → конвертация в InnoDB. После этого применить.</div>
        </div>
      </div>
    </div></div>';

    echo '<div class="panel panel-default"><div class="panel-heading"><b>3) Если пункт меню не появился</b></div><div class="panel-body">
      <div class="dbop-small">Иногда запись в <code>'.dbop_h(PREFIX).'_admin_sections</code> не добавляется (например, из-за ошибки установки). Нажмите кнопку ниже.</div>
      <button class="btn btn-primary" onclick="dbop_toggleSections(); return false;">Ссылка в меню «Сторонние модули» (вкл/выкл)</button>
      <div id="dbop-sections-result" style="margin-top:10px;"></div>
    </div></div>';

    echo '<div class="panel panel-default"><div class="panel-heading"><b>4) Логи</b></div><div class="panel-body">
      <div>Лог всех ALTER/команд: <code>/engine/data/dboptimizerpro.log</code></div>
    </div></div>';
}

echo '  </div>'; // content

echo '  <div class="'.$boxFooter.'" style="display:flex;justify-content:space-between;align-items:center;">';
echo '    <div class="dbop-small">DBOptimizerPro v'.dbop_h(DBOP_VERSION).'</div>';
echo '    <div class="dbop-small">prefix: <code>'.dbop_h($prefix).'</code></div>';
echo '  </div>';

echo '</div>'; // box

?>
<script>
function dbop_toggleAll(cb, name){
  var c = cb.checked;
  document.querySelectorAll('input[name="'+name+'"]').forEach(function(x){ x.checked = c; });
}
function dbop_applySelected(action, formSelector){
  var form = document.querySelector(formSelector);
  var items = [];
  form.querySelectorAll('input[name="items[]"]:checked').forEach(function(x){ items.push(x.value); });
  if (!items.length) { DLEalert('Ничего не выбрано', 'DBOptimizerPro'); return; }
  if (!confirm('Применить выбранные изменения? На больших таблицах это может занять время.')) return;

  var fd = new FormData();
  fd.append('user_hash', '<?=dbop_h($userHash)?>');
  items.forEach(function(v){ fd.append('items[]', v); });

  fetch('admin.php?mod=db_optimizer&action='+encodeURIComponent(action)+'&tab=<?=dbop_h($tab)?><?=isset($_GET['show_all']) && (int)$_GET['show_all']===1 ? '&show_all=1' : ''?>', {
    method: 'POST',
    body: fd,
    credentials: 'same-origin'
  }).then(r=>r.json()).then(function(data){
    var boxId = (action==='apply_indexes') ? 'dbop-index-result' : (action==='apply_unique' ? 'dbop-unique-result' : 'dbop-fk-result');
    var el = document.getElementById(boxId);
    if (!el) return;

    var html = '';
    if (data.success) {
      html += '<div class="alert alert-success"><b>'+escapeHtml(data.message)+'</b></div>';
      if (data.applied && data.applied.length) {
        html += '<div class="alert alert-info"><b>Применено:</b><pre style="white-space:pre-wrap;">'+escapeHtml(data.applied.join("\\n"))+'</pre></div>';
      }
      if (data.skipped && data.skipped.length) {
        html += '<div class="alert alert-warning"><b>Пропущено:</b><pre style="white-space:pre-wrap;">'+escapeHtml(JSON.stringify(data.skipped, null, 2))+'</pre></div>';
      }
    } else {
      html += '<div class="alert alert-danger">'+escapeHtml(data.message||'Ошибка')+'</div>';
    }
    el.innerHTML = html;
  }).catch(function(e){
    DLEalert('Ошибка: '+e, 'DBOptimizerPro');
  });
}
function dbop_runMaintenance(){
  var form = document.getElementById('dbop-maint-form');
  if (!form) return;
  var checked = form.querySelectorAll('input[name="tables[]"]:checked').length;
  if (!checked) { DLEalert('Выберите таблицы', 'DBOptimizerPro'); return; }
  if (!confirm('Выполнить команду обслуживания на выбранных таблицах?')) return;

  var fd = new FormData(form);
  fetch('admin.php?mod=db_optimizer&action=maintenance&tab=tables<?=isset($_GET['show_all']) && (int)$_GET['show_all']===1 ? '&show_all=1' : ''?>', {
    method: 'POST',
    body: fd,
    credentials: 'same-origin'
  }).then(r=>r.json()).then(function(data){
    var el = document.getElementById('dbop-maint-result');
    if (!el) return;
    if (!data.success) {
      el.innerHTML = '<div class="alert alert-danger">'+escapeHtml(data.message||'Ошибка')+'</div>';
      return;
    }
    var rows = data.results || [];
    var html = '<div class="alert alert-success"><b>'+escapeHtml(data.message)+'</b></div>';
    html += '<div class="alert alert-info"><pre style="white-space:pre-wrap;">'+escapeHtml(JSON.stringify(rows, null, 2))+'</pre></div>';
    el.innerHTML = html;
  }).catch(function(e){
    DLEalert('Ошибка: '+e, 'DBOptimizerPro');
  });
}
function dbop_convertEngine(){
  var form = document.getElementById('dbop-maint-form');
  if (!form) return;
  var checked = form.querySelectorAll('input[name="tables[]"]:checked').length;
  if (!checked) { DLEalert('Выберите таблицы', 'DBOptimizerPro'); return; }
  if (!confirm('Конвертировать выбранные таблицы? Это ALTER TABLE и может быть долгим.')) return;

  var fd = new FormData();
  fd.append('user_hash', '<?=dbop_h($userHash)?>');
  var target = document.getElementById('dbop-engine-target') ? document.getElementById('dbop-engine-target').value : 'INNODB';
  fd.append('target', target);
  form.querySelectorAll('input[name="tables[]"]:checked').forEach(function(x){ fd.append('tables[]', x.value); });

  fetch('admin.php?mod=db_optimizer&action=convert_engine&tab=tables<?=isset($_GET['show_all']) && (int)$_GET['show_all']===1 ? '&show_all=1' : ''?>', {
    method: 'POST',
    body: fd,
    credentials: 'same-origin'
  }).then(r=>r.json()).then(function(data){
    var el = document.getElementById('dbop-maint-result');
    if (!el) return;
    if (!data.success) {
      el.innerHTML = '<div class="alert alert-danger">'+escapeHtml(data.message||'Ошибка')+'</div>';
      return;
    }
    var html = '<div class="alert alert-success"><b>'+escapeHtml(data.message)+'</b></div>';
    if (data.applied && data.applied.length) html += '<div class="alert alert-info"><b>Применено:</b><pre style="white-space:pre-wrap;">'+escapeHtml(data.applied.join("\\n"))+'</pre></div>';
    if (data.skipped && data.skipped.length) html += '<div class="alert alert-warning"><b>Пропущено:</b><pre style="white-space:pre-wrap;">'+escapeHtml(JSON.stringify(data.skipped, null, 2))+'</pre></div>';
    el.innerHTML = html;
  }).catch(function(e){
    DLEalert('Ошибка: '+e, 'DBOptimizerPro');
  });
}
function dbop_toggleSections(){
  fetch('admin.php?mod=db_optimizer&action=sections&tab=docs&user_hash=<?=dbop_h($userHash)?><?=isset($_GET['show_all']) && (int)$_GET['show_all']===1 ? '&show_all=1' : ''?>', {
    method: 'GET',
    credentials: 'same-origin'
  }).then(r=>r.json()).then(function(data){
    var el = document.getElementById('dbop-sections-result');
    if (!el) { DLEalert(data.message||'Готово', 'DBOptimizerPro'); return; }
    el.innerHTML = data.success ? '<div class="alert alert-success">'+escapeHtml(data.message)+'</div>' : '<div class="alert alert-danger">'+escapeHtml(data.message||'Ошибка')+'</div>';
  }).catch(function(e){
    DLEalert('Ошибка: '+e, 'DBOptimizerPro');
  });
}
function escapeHtml(s){
  return String(s).replace(/[&<>"']/g, function(m){
    return ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[m]);
  });
}
</script>
<?php echofooter(); ?>

]]></replacecode>
      <enabled>1</enabled>
    </operation>
  </file>

</dleplugin>
Вы автор плагина?
 
Последнее редактирование:
art994 сообщи пожалуйста о результатах, может что то доработать нужно (кроме CSS плагина)
 

Похожие темы Последние темы Популярные темы

Назад
Сверху