mirror of
https://github.com/avecms/AVE.cms.git
synced 2026-08-01 08:45:44 +00:00
1336 lines
78 KiB
PHP
1336 lines
78 KiB
PHP
<?php
|
||
|
||
/*
|
||
|--------------------------------------------------------------------------------------
|
||
| AVE.cms
|
||
|--------------------------------------------------------------------------------------
|
||
| @package AVE.cms
|
||
| @file system/App/Frontend/Debug/PublicDebugToolbar.php
|
||
| @author AVE.cms <support@ave-cms.ru>
|
||
| @copyright 2007-2026 (c) AVE.cms
|
||
| @link https://ave-cms.ru
|
||
| @version 3.3
|
||
*/
|
||
|
||
namespace App\Frontend\Debug;
|
||
|
||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||
|
||
use App\Common\Auth;
|
||
use App\Common\DebugChannel;
|
||
use App\Common\HookCatalog;
|
||
use App\Common\PublicConfiguration;
|
||
use App\Common\PublicModuleRuntime;
|
||
use App\Common\Registry;
|
||
use App\Frontend\PublicPageContext;
|
||
use App\Frontend\PublicProfiler;
|
||
use App\Helpers\Hooks;
|
||
use DB;
|
||
|
||
/** Protected, dependency-free profiler injected into public HTML responses. */
|
||
class PublicDebugToolbar
|
||
{
|
||
public static function boot()
|
||
{
|
||
if (!self::allowed()) {
|
||
return;
|
||
}
|
||
|
||
PublicLayoutInspector::boot();
|
||
Hooks::enableTrace(true);
|
||
if (class_exists('App\\Common\\Db\\Core\\QueryDebug')) {
|
||
\App\Common\Db\Core\QueryLogger::enableCallerTrace(true);
|
||
\App\Common\Db\Core\QueryDebug::init(true);
|
||
PublicDebugCollector::boot();
|
||
}
|
||
}
|
||
|
||
public static function inject($html, $core = null)
|
||
{
|
||
$html = (string) $html;
|
||
$allowed = self::allowed();
|
||
$decorate = $allowed && !defined('ONLYCONTENT') && !self::isAjax() && stripos($html, '</body>') !== false;
|
||
$html = PublicLayoutInspector::finalize($html, $decorate);
|
||
if (!$decorate) {
|
||
return $html;
|
||
}
|
||
|
||
$panel = self::render($core);
|
||
return preg_replace('/<\/body>/i', $panel . '</body>', $html, 1);
|
||
}
|
||
|
||
protected static function allowed()
|
||
{
|
||
$config = PublicConfiguration::all();
|
||
$debug = isset($config['debug']) && is_array($config['debug']) ? $config['debug'] : array();
|
||
if (empty($debug['enabled'])) { return false; }
|
||
$groups = isset($debug['groups']) && is_array($debug['groups']) ? array_map('intval', $debug['groups']) : array(1);
|
||
$user = Auth::publicUser();
|
||
if (is_array($user) && in_array((int) $user['group'], $groups, true)) {
|
||
return true;
|
||
}
|
||
|
||
return Auth::systemUserCan('view_public_debug');
|
||
}
|
||
|
||
protected static function render($core)
|
||
{
|
||
$queries = self::queries();
|
||
$timeline = PublicProfiler::timeline();
|
||
$document = is_object($core) && isset($core->curentdoc) ? $core->curentdoc : null;
|
||
$elapsed = defined('START_MICROTIME') ? microtime(true) - self::timestamp(START_MICROTIME) : 0.0;
|
||
$status = (int) (http_response_code() ?: 200);
|
||
$method = isset($_SERVER['REQUEST_METHOD']) ? (string) $_SERVER['REQUEST_METHOD'] : 'GET';
|
||
$uri = self::safeUri(isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/');
|
||
$systemUser = Auth::systemUser();
|
||
$publicUser = Auth::publicUser();
|
||
$queryTime = 0.0;
|
||
foreach ($queries as $query) { $queryTime += (float) $query['time']; }
|
||
|
||
$overview = self::cards(array(
|
||
array('Время ответа', number_format($elapsed * 1000, 1, ',', ' ') . ' ms', self::metricTone($elapsed * 1000, 300, 800)),
|
||
array('Пиковая память', self::bytes(memory_get_peak_usage(true)), 'blue'),
|
||
array('SQL-запросы', count($queries) . ' · ' . number_format($queryTime * 1000, 1, ',', ' ') . ' ms', self::metricTone($queryTime * 1000, 80, 250)),
|
||
array('HTTP-статус', (string) $status, $status >= 500 ? 'red' : ($status >= 400 ? 'amber' : 'green')),
|
||
));
|
||
$overview .= self::definitionList(array(
|
||
'Метод' => $method,
|
||
'Адрес' => $uri,
|
||
'PHP runtime' => PHP_VERSION,
|
||
'Системный пользователь' => self::systemUserLabel($systemUser),
|
||
'Публичный пользователь' => self::publicUserLabel($publicUser),
|
||
'Public-модули' => implode(', ', array_keys(PublicModuleRuntime::loaded())) ?: '—',
|
||
'Данные документа' => Registry::get('document_snapshot_source', 'не запрашивались') === 'snapshot' ? 'JSON-снимок' : (Registry::get('document_snapshot_source', '') === 'database' ? 'БД + пересборка' : 'не запрашивались'),
|
||
'Отложенная страница' => PublicPageContext::hasDeferredPage() ? 'Да' : 'Нет',
|
||
));
|
||
$overview = self::sectionHead('Сводка запроса', $method . ' · ' . $uri, self::statusBadge($status)) . $overview;
|
||
|
||
$documentHtml = self::documentPanel($document);
|
||
$debugEntries = DebugChannel::all();
|
||
$debugHtml = self::sectionHead('Данные приложения', 'Структурированные значения из Debug::panel()', self::countBadge(count($debugEntries), $debugEntries ? 'violet' : 'slate'))
|
||
. self::debugEntries($debugEntries);
|
||
|
||
$sqlHtml = self::sectionHead('SQL-запросы', number_format($queryTime * 1000, 2, ',', ' ') . ' ms суммарно', self::countBadge(count($queries)))
|
||
. self::queryTable($queries);
|
||
$timelineHtml = self::sectionHead('Timeline', 'Этапы формирования текущего ответа', self::countBadge(self::leafCount($timeline)))
|
||
. self::timeline($timeline);
|
||
$cacheItems = method_exists('DB', 'getDebugInfo') ? DB::getDebugInfo() : array();
|
||
$cacheItems = is_array($cacheItems) ? $cacheItems : array();
|
||
$cacheHtml = self::sectionHead('Кеш', 'События кеширования текущего запроса', self::countBadge(self::leafCount($cacheItems)))
|
||
. self::dataView($cacheItems);
|
||
|
||
$httpData = self::masked(array(
|
||
'GET' => $_GET,
|
||
'POST' => $_POST,
|
||
'COOKIE' => $_COOKIE,
|
||
'HEADERS' => self::headers(),
|
||
));
|
||
$httpHtml = self::sectionHead('HTTP', $method . ' · ' . $uri, self::countBadge(self::leafCount($httpData)))
|
||
. self::httpGroups($httpData);
|
||
$sessionData = self::masked(isset($_SESSION) && is_array($_SESSION) ? $_SESSION : array());
|
||
$sessionHtml = self::sectionHead('Контексты авторизации', 'Публичный профиль и роль панели управления связаны в единую учётную запись', self::countBadge(($systemUser ? 1 : 0) + ($publicUser ? 1 : 0), $systemUser ? 'green' : 'slate'))
|
||
. self::identityPanel($systemUser, $publicUser)
|
||
. self::sectionHead('PHP Session', session_id() !== '' ? 'ID ' . self::shortId(session_id()) : 'Сессия не запущена', self::countBadge(self::leafCount($sessionData)))
|
||
. self::dataView($sessionData);
|
||
$errors = PublicDebugCollector::errors();
|
||
$errorsHtml = self::sectionHead('Ошибки PHP', $errors ? 'Зафиксированы в текущем запросе' : 'Текущий запрос выполнен без предупреждений', self::countBadge(count($errors), $errors ? 'red' : 'green'))
|
||
. self::errorList($errors);
|
||
$hookTrace = Hooks::trace();
|
||
$hookEntries = isset($hookTrace['entries']) && is_array($hookTrace['entries']) ? $hookTrace['entries'] : array();
|
||
$hooksHtml = self::sectionHead('Хуки', 'События и обработчики текущего публичного запроса', self::countBadge(count($hookEntries), $hookEntries ? 'blue' : 'slate'))
|
||
. self::hookList($hookEntries, isset($hookTrace['dropped']) ? (int) $hookTrace['dropped'] : 0);
|
||
$files = array_map(function ($file) { return str_replace(BASEPATH . '/', '', $file); }, get_included_files());
|
||
sort($files, SORT_NATURAL | SORT_FLAG_CASE);
|
||
$filesHtml = self::sectionHead('Подключённые файлы', 'PHP-файлы текущего запроса', self::countBadge(count($files)))
|
||
. self::fileList($files);
|
||
$inspectorEntries = PublicLayoutInspector::entries();
|
||
$inspectorHtml = self::sectionHead(
|
||
'Состав страницы',
|
||
'Управляемые элементы в порядке фактического вывода',
|
||
self::countBadge(count($inspectorEntries), $inspectorEntries ? 'blue' : 'slate')
|
||
) . self::inspectorPanel($inspectorEntries);
|
||
|
||
$tabs = array(
|
||
'overview' => array('Обзор', null),
|
||
'document' => array('Документ', $document && isset($document->Id) ? (int) $document->Id : 0),
|
||
'debug' => array('Данные', count($debugEntries)),
|
||
'timeline' => array('Timeline', self::leafCount($timeline)),
|
||
'sql' => array('SQL', count($queries)),
|
||
'cache' => array('Кеш', self::leafCount($cacheItems)),
|
||
'http' => array('HTTP', self::leafCount($httpData)),
|
||
'session' => array('Session', self::leafCount($sessionData)),
|
||
'errors' => array('Ошибки', count($errors)),
|
||
'hooks' => array('Хуки', count($hookEntries)),
|
||
'elements' => array('Элементы', count($inspectorEntries)),
|
||
'files' => array('Файлы', count($files)),
|
||
);
|
||
$contents = array('overview' => $overview, 'document' => $documentHtml, 'debug' => $debugHtml, 'timeline' => $timelineHtml, 'sql' => $sqlHtml, 'cache' => $cacheHtml, 'http' => $httpHtml, 'session' => $sessionHtml, 'errors' => $errorsHtml, 'hooks' => $hooksHtml, 'elements' => $inspectorHtml, 'files' => $filesHtml);
|
||
$nav = ''; $sections = '';
|
||
foreach ($tabs as $id => $tab) {
|
||
$active = $id === 'overview';
|
||
$nav .= '<button type="button" role="tab" aria-selected="' . ($active ? 'true' : 'false') . '" aria-controls="pdt-tab-' . $id . '" data-pdt-tab="' . $id . '"' . ($active ? ' class="is-active"' : '') . '><span>' . self::e($tab[0]) . '</span>' . ($tab[1] !== null ? '<em' . ($id === 'errors' && $tab[1] > 0 ? ' class="is-alert"' : '') . '>' . (int) $tab[1] . '</em>' : '') . '</button>';
|
||
$sections .= '<section id="pdt-tab-' . $id . '" role="tabpanel" data-pdt-panel="' . $id . '"' . (!$active ? ' hidden' : '') . '>' . $contents[$id] . '</section>';
|
||
}
|
||
|
||
$inspectorJson = json_encode($inspectorEntries, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
|
||
if ($inspectorJson === false) { $inspectorJson = '[]'; }
|
||
|
||
return '<div id="pdt-root"><button id="pdt-toggle" type="button" aria-label="Открыть публичную отладку"><b>AVE</b><span>' . number_format($elapsed * 1000, 0, ',', ' ') . ' ms</span><span>' . count($queries) . ' SQL</span><span>' . self::bytes(memory_get_peak_usage(true)) . '</span>' . ($errors ? '<i>' . count($errors) . '</i>' : '') . '</button><div id="pdt-panel" hidden><header data-pdt-drag-handle><div class="pdt-brand"><strong>AVE Public Debug</strong><span class="pdt-method">' . self::e($method) . '</span>' . self::statusBadge($status) . '</div><code>' . self::e($uri) . '</code><div class="pdt-head-actions"><button id="pdt-reset-position" type="button" aria-label="Сбросить положение" title="Сбросить положение">↺</button><button id="pdt-close" type="button" aria-label="Закрыть" title="Закрыть">×</button></div></header><nav role="tablist" aria-label="Разделы отладки">' . $nav . '</nav><main>' . $sections . '</main></div></div><script type="application/json" id="pdt-inspector-data">' . $inspectorJson . '</script>' . self::assets();
|
||
}
|
||
|
||
protected static function queries()
|
||
{
|
||
$result = array();
|
||
if (method_exists('DB', 'getQueries')) {
|
||
foreach ((array) DB::getQueries() as $item) {
|
||
$item = (array) $item;
|
||
$result[] = array('source' => 'Native', 'query' => isset($item['query']) ? $item['query'] : '', 'time' => self::number(isset($item['time']) ? $item['time'] : 0), 'caller' => self::caller(isset($item['caller']) ? $item['caller'] : array()));
|
||
}
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
protected static function queryTable(array $queries)
|
||
{
|
||
if (!$queries) { return self::emptyState('SQL-запросы не собраны', 'В текущем запросе обращения к базе данных не зафиксированы.'); }
|
||
$html = self::filterBar('sql', 'Найти SQL или файл вызова', count($queries));
|
||
$html .= '<div class="pdt-query-list" data-pdt-filter-list="sql">';
|
||
foreach ($queries as $index => $query) {
|
||
$milliseconds = (float) $query['time'] * 1000;
|
||
$tone = self::metricTone($milliseconds, 15, 50);
|
||
$filter = self::lower($query['query'] . ' ' . $query['caller'] . ' ' . $query['source']);
|
||
$html .= '<details class="pdt-query" data-pdt-filter-item data-pdt-filter-text="' . self::e($filter) . '"><summary>'
|
||
. '<span class="pdt-query-index">#' . ($index + 1) . '</span>'
|
||
. '<span class="pdt-badge pdt-badge-blue">' . self::e($query['source']) . '</span>'
|
||
. '<code>' . self::e($query['caller'] ?: 'Источник не определён') . '</code>'
|
||
. '<strong class="pdt-value-' . $tone . '">' . number_format($milliseconds, 2, ',', ' ') . ' ms</strong>'
|
||
. '</summary><pre><code>' . self::e(self::mask($query['query'])) . '</code></pre></details>';
|
||
}
|
||
|
||
return $html . '<div class="pdt-filter-empty" data-pdt-filter-empty hidden>Совпадений не найдено.</div></div>';
|
||
}
|
||
|
||
protected static function hookList(array $entries, $dropped)
|
||
{
|
||
if (!$entries) {
|
||
return self::emptyState('Хуки не вызывались', 'Текущий маршрут не прошел через зарегистрированные lifecycle-точки.');
|
||
}
|
||
|
||
$html = self::filterBar('hooks', 'Найти событие или обработчик', count($entries));
|
||
if ((int) $dropped > 0) {
|
||
$html .= '<div class="pdt-notice">Не показано записей: ' . (int) $dropped . '. Ограничение защищает от переполнения отладчика.</div>';
|
||
}
|
||
|
||
$html .= '<div class="pdt-query-list" data-pdt-filter-list="hooks">';
|
||
foreach ($entries as $index => $entry) {
|
||
$name = isset($entry['name']) ? (string) $entry['name'] : '';
|
||
$handler = isset($entry['handler']) && $entry['handler'] !== '' ? (string) $entry['handler'] : 'Нет подписчиков';
|
||
$status = isset($entry['status']) ? (string) $entry['status'] : 'ok';
|
||
$duration = isset($entry['duration']) ? (float) $entry['duration'] * 1000 : 0.0;
|
||
$definition = HookCatalog::get($name);
|
||
$description = is_array($definition) && isset($definition['description']) ? (string) $definition['description'] : '';
|
||
$tone = $status === 'error' ? 'red' : ($status === 'unhandled' ? 'slate' : self::metricTone($duration, 5, 20));
|
||
$filter = self::lower($name . ' ' . $handler . ' ' . $description . ' ' . $status);
|
||
$priority = isset($entry['priority']) && $entry['priority'] !== null ? 'P' . (int) $entry['priority'] : '—';
|
||
$details = array(
|
||
'Описание' => $description !== '' ? $description : 'Событие не описано в каталоге',
|
||
'Тип' => isset($entry['kind']) ? (string) $entry['kind'] : 'action',
|
||
'Приоритет' => $priority,
|
||
'Глубина' => isset($entry['depth']) ? (int) $entry['depth'] : 0,
|
||
'Состояние' => $status,
|
||
);
|
||
if (!empty($entry['exception'])) { $details['Ошибка'] = (string) $entry['exception']; }
|
||
$html .= '<details class="pdt-query" data-pdt-filter-item data-pdt-filter-text="' . self::e($filter) . '"><summary>'
|
||
. '<span class="pdt-query-index">#' . ($index + 1) . '</span>'
|
||
. '<span class="pdt-badge pdt-badge-' . $tone . '">' . self::e($priority) . '</span>'
|
||
. '<code>' . self::e($name) . '</code>'
|
||
. '<strong class="pdt-value-' . $tone . '">' . number_format($duration, 2, ',', ' ') . ' ms</strong>'
|
||
. '</summary>' . self::definitionList(array_merge(array('Обработчик' => $handler), $details)) . '</details>';
|
||
}
|
||
|
||
return $html . '<div class="pdt-filter-empty" data-pdt-filter-empty hidden>Совпадений не найдено.</div></div>';
|
||
}
|
||
|
||
protected static function cards(array $cards)
|
||
{
|
||
$html = '<div class="pdt-cards">';
|
||
foreach ($cards as $card) {
|
||
$tone = isset($card[2]) ? preg_replace('/[^a-z]/', '', (string) $card[2]) : 'blue';
|
||
$html .= '<div class="pdt-metric pdt-tone-' . self::e($tone) . '"><small>' . self::e($card[0]) . '</small><strong>' . self::e($card[1]) . '</strong></div>';
|
||
}
|
||
|
||
return $html . '</div>';
|
||
}
|
||
|
||
protected static function definitionList(array $items)
|
||
{
|
||
$html = '<dl class="pdt-dl">';
|
||
foreach ($items as $key => $value) {
|
||
$html .= '<div><dt>' . self::e($key) . '</dt><dd>' . self::e($value) . '</dd></div>';
|
||
}
|
||
|
||
return $html . '</dl>';
|
||
}
|
||
|
||
protected static function tree($value)
|
||
{
|
||
return self::dataView($value);
|
||
}
|
||
|
||
protected static function errorList(array $errors)
|
||
{
|
||
if (!$errors) { return self::emptyState('Ошибок нет', 'PHP-предупреждения и ошибки в текущем запросе не зафиксированы.', 'success'); }
|
||
$html = '<div class="pdt-errors">';
|
||
foreach ($errors as $error) {
|
||
$type = isset($error['type']) ? (string) $error['type'] : 'PHP';
|
||
$tone = stripos($type, 'notice') !== false || stripos($type, 'deprecated') !== false ? 'amber' : 'red';
|
||
$html .= '<article><div><span class="pdt-badge pdt-badge-' . $tone . '">' . self::e($type) . '</span>'
|
||
. (isset($error['code']) ? '<small>Код ' . (int) $error['code'] . '</small>' : '') . '</div>'
|
||
. '<p>' . self::e(isset($error['message']) ? $error['message'] : '') . '</p>'
|
||
. '<code>' . self::e((isset($error['file']) ? $error['file'] : '') . ':' . (isset($error['line']) ? $error['line'] : 0)) . '</code></article>';
|
||
}
|
||
|
||
return $html . '</div>';
|
||
}
|
||
|
||
protected static function debugEntries(array $entries)
|
||
{
|
||
if (!$entries) {
|
||
return self::emptyState('Записей нет', 'В текущем запросе Debug::panel() не вызывался.');
|
||
}
|
||
|
||
$html = '<div class="pdt-debug-entries">';
|
||
foreach ($entries as $index => $entry) {
|
||
$source = (isset($entry['file']) ? $entry['file'] : '') . (isset($entry['line']) ? ':' . (int) $entry['line'] : '');
|
||
$html .= '<article><header><div><span>' . ($index + 1) . '</span><div><strong>' . self::e(isset($entry['label']) ? $entry['label'] : 'Данные') . '</strong>'
|
||
. '<code>' . self::e($source) . '</code></div></div><span class="pdt-badge pdt-badge-violet">' . self::e(isset($entry['type']) ? $entry['type'] : 'value') . '</span></header>'
|
||
. self::dataView(self::masked(isset($entry['value']) ? $entry['value'] : null)) . '</article>';
|
||
}
|
||
|
||
return $html . '</div>';
|
||
}
|
||
|
||
protected static function documentPanel($document)
|
||
{
|
||
if (!is_object($document)) {
|
||
return self::sectionHead('Документ', 'Маршрут не связан с документом', self::countBadge(0))
|
||
. self::emptyState('Документ не определён', 'Ответ сформирован системным маршрутом, модулем или страницей ошибки.');
|
||
}
|
||
|
||
$id = self::objectValue($document, array('Id', 'id'), 0);
|
||
$rubricId = self::objectValue($document, array('rubric_id', 'rubricId'), 0);
|
||
$templateId = self::objectValue($document, array('document_template_id', 'template_id', 'rubric_template_id'), 0);
|
||
$status = (int) self::objectValue($document, array('document_status', 'status'), 0);
|
||
$title = self::objectValue($document, array('document_title', 'title'), 'Без названия');
|
||
|
||
$html = self::sectionHead('Документ', (string) $title, self::countBadge((int) $id, 'blue'));
|
||
$html .= self::cards(array(
|
||
array('ID документа', $id ?: '—', 'blue'),
|
||
array('Рубрика', $rubricId ? '#' . $rubricId : '—', 'violet'),
|
||
array('Шаблон', $templateId ? '#' . $templateId : '—', 'amber'),
|
||
array('Состояние', $status === 1 ? 'Опубликован' : 'Не опубликован', $status === 1 ? 'green' : 'red'),
|
||
));
|
||
$html .= self::definitionList(array(
|
||
'Заголовок' => $title,
|
||
'Алиас' => self::objectValue($document, array('document_alias', 'alias'), '—'),
|
||
'Рубрика' => self::objectValue($document, array('rubric_title'), $rubricId ? '#' . $rubricId : '—'),
|
||
'Опубликован' => self::objectValue($document, array('document_published'), '—'),
|
||
'Изменён' => self::objectValue($document, array('document_changed'), '—'),
|
||
'Кеш документа' => self::objectValue($document, array('document_cache'), '—'),
|
||
));
|
||
return $html;
|
||
}
|
||
|
||
protected static function objectValue($object, array $keys, $fallback = null)
|
||
{
|
||
foreach ($keys as $key) {
|
||
if (is_object($object) && isset($object->{$key})) { return $object->{$key}; }
|
||
}
|
||
|
||
return $fallback;
|
||
}
|
||
|
||
protected static function sectionHead($title, $meta = '', $badge = '')
|
||
{
|
||
return '<div class="pdt-section-head"><div><h2>' . self::e($title) . '</h2>'
|
||
. ($meta !== '' ? '<p>' . self::e($meta) . '</p>' : '') . '</div>' . $badge . '</div>';
|
||
}
|
||
|
||
protected static function countBadge($count, $tone = 'slate')
|
||
{
|
||
$tone = preg_replace('/[^a-z]/', '', (string) $tone);
|
||
return '<span class="pdt-count pdt-count-' . self::e($tone) . '">' . (int) $count . '</span>';
|
||
}
|
||
|
||
protected static function statusBadge($status)
|
||
{
|
||
$status = (int) $status;
|
||
$tone = $status >= 500 ? 'red' : ($status >= 400 ? 'amber' : ($status >= 300 ? 'violet' : 'green'));
|
||
return '<span class="pdt-badge pdt-badge-' . $tone . '">HTTP ' . $status . '</span>';
|
||
}
|
||
|
||
protected static function metricTone($value, $warning, $danger)
|
||
{
|
||
if ((float) $value >= (float) $danger) { return 'red'; }
|
||
if ((float) $value >= (float) $warning) { return 'amber'; }
|
||
return 'green';
|
||
}
|
||
|
||
protected static function timeline(array $timeline)
|
||
{
|
||
$rows = array();
|
||
self::flattenTimeline($timeline, array(), $rows);
|
||
if (!$rows) { return self::emptyState('Timeline пуст', 'Этапы выполнения текущего запроса не зафиксированы.'); }
|
||
|
||
$max = 0.0;
|
||
foreach ($rows as $row) {
|
||
if ($row['seconds'] !== null) { $max = max($max, $row['seconds']); }
|
||
}
|
||
|
||
$html = '<div class="pdt-timeline">';
|
||
foreach ($rows as $row) {
|
||
$width = $row['seconds'] !== null && $max > 0 ? max(2, min(100, ($row['seconds'] / $max) * 100)) : 0;
|
||
$tone = $row['seconds'] !== null ? self::metricTone($row['seconds'] * 1000, 15, 50) : 'blue';
|
||
$html .= '<div class="pdt-timeline-row"><code>' . self::e(implode(' › ', $row['path'])) . '</code>'
|
||
. '<div class="pdt-timeline-track">' . ($width > 0 ? '<i class="pdt-bg-' . $tone . '" style="width:' . number_format($width, 2, '.', '') . '%"></i>' : '') . '</div>'
|
||
. '<strong class="pdt-value-' . $tone . '">' . self::e($row['label']) . '</strong></div>';
|
||
}
|
||
|
||
return $html . '</div>';
|
||
}
|
||
|
||
protected static function flattenTimeline($value, array $path, array &$rows)
|
||
{
|
||
if (is_array($value)) {
|
||
foreach ($value as $key => $child) {
|
||
$childPath = $path;
|
||
$childPath[] = (string) $key;
|
||
self::flattenTimeline($child, $childPath, $rows);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
$seconds = null;
|
||
$label = self::scalarLabel($value);
|
||
if (is_numeric($value)) {
|
||
$seconds = (float) $value;
|
||
$label = number_format($seconds * 1000, 2, ',', ' ') . ' ms';
|
||
} elseif (is_string($value) && preg_match('/^([0-9\s,.]+)\s*sec$/i', trim($value), $match)) {
|
||
$seconds = self::number(str_replace(' ', '', $match[1]));
|
||
$label = number_format($seconds * 1000, 2, ',', ' ') . ' ms';
|
||
}
|
||
|
||
$rows[] = array('path' => $path, 'seconds' => $seconds, 'label' => $label);
|
||
}
|
||
|
||
protected static function leafCount($value)
|
||
{
|
||
if (!is_array($value)) { return $value === null ? 0 : 1; }
|
||
$count = 0;
|
||
foreach ($value as $child) { $count += self::leafCount($child); }
|
||
return $count;
|
||
}
|
||
|
||
protected static function dataView($value, $depth = 0)
|
||
{
|
||
if ($depth === 0 && is_array($value) && !$value) {
|
||
return self::emptyState('Данные не собраны', 'Для текущего запроса значения отсутствуют.');
|
||
}
|
||
|
||
if ($depth >= 7) { return '<div class="pdt-depth-limit">Вложенность ограничена</div>'; }
|
||
if (!is_array($value)) { return '<div class="pdt-data-scalar">' . self::scalarValue($value) . '</div>'; }
|
||
|
||
$html = '<div class="pdt-data' . ($depth > 0 ? ' is-nested' : '') . '">';
|
||
foreach ($value as $key => $child) {
|
||
$label = is_int($key) ? '#' . $key : (string) $key;
|
||
if (is_array($child)) {
|
||
$html .= '<details class="pdt-data-node"' . ($depth < 1 ? ' open' : '') . '><summary><code>' . self::e($label) . '</code>'
|
||
. '<span>' . count($child) . ' ' . self::plural(count($child), 'элемент', 'элемента', 'элементов') . '</span></summary>'
|
||
. self::dataView($child, $depth + 1) . '</details>';
|
||
} else {
|
||
$html .= '<div class="pdt-data-row"><code>' . self::e($label) . '</code><div>' . self::scalarValue($child) . '</div></div>';
|
||
}
|
||
}
|
||
|
||
return $html . '</div>';
|
||
}
|
||
|
||
protected static function scalarValue($value)
|
||
{
|
||
if ($value === null) { return '<span class="pdt-scalar pdt-scalar-null">null</span>'; }
|
||
if (is_bool($value)) { return '<span class="pdt-scalar pdt-scalar-bool">' . ($value ? 'true' : 'false') . '</span>'; }
|
||
if (is_int($value) || is_float($value)) { return '<span class="pdt-scalar pdt-scalar-number">' . self::e($value) . '</span>'; }
|
||
if ($value === '') { return '<span class="pdt-scalar pdt-scalar-empty">пустая строка</span>'; }
|
||
return '<span class="pdt-scalar pdt-scalar-string">' . self::e($value) . '</span>';
|
||
}
|
||
|
||
protected static function identityPanel($systemUser, $publicUser)
|
||
{
|
||
$system = is_array($systemUser) ? array(
|
||
'ID' => isset($systemUser['id']) ? (int) $systemUser['id'] : 0,
|
||
'Имя' => isset($systemUser['name']) ? (string) $systemUser['name'] : '',
|
||
'Email' => isset($systemUser['email']) ? (string) $systemUser['email'] : '',
|
||
'Роль' => isset($systemUser['role']) ? (string) $systemUser['role'] : '',
|
||
) : array();
|
||
$public = is_array($publicUser) ? array(
|
||
'ID' => isset($publicUser['Id']) ? (int) $publicUser['Id'] : (isset($publicUser['id']) ? (int) $publicUser['id'] : 0),
|
||
'Имя' => self::publicUserLabel($publicUser),
|
||
'Email' => isset($publicUser['email']) ? (string) $publicUser['email'] : '',
|
||
'Группа' => isset($publicUser['group']) ? (int) $publicUser['group'] : 0,
|
||
) : array();
|
||
|
||
return '<div class="pdt-identities">'
|
||
. '<article><header><span class="pdt-badge ' . ($system ? 'pdt-badge-green' : 'pdt-count-slate') . '">Система</span><strong>' . ($system ? 'Авторизован' : 'Не авторизован') . '</strong></header>'
|
||
. ($system ? self::dataView($system) : self::emptyState('Системного входа нет', 'Общий auth_token не найден.')) . '</article>'
|
||
. '<article><header><span class="pdt-badge ' . ($public ? 'pdt-badge-blue' : 'pdt-count-slate') . '">Публичный сайт</span><strong>' . ($public ? 'Авторизован' : 'Гостевая сессия') . '</strong></header>'
|
||
. ($public ? self::dataView($public) : self::emptyState('Публичный пользователь не вошел', 'Legacy-поля Anonymous относятся только к гостевой сессии сайта.')) . '</article>'
|
||
. '</div>';
|
||
}
|
||
|
||
protected static function systemUserLabel($user)
|
||
{
|
||
if (!is_array($user)) { return 'Не авторизован'; }
|
||
$name = trim((string) (isset($user['name']) ? $user['name'] : ''));
|
||
if ($name === '') { $name = isset($user['email']) ? (string) $user['email'] : 'Системный пользователь'; }
|
||
return $name . ' (#' . (isset($user['id']) ? (int) $user['id'] : 0) . ')';
|
||
}
|
||
|
||
protected static function publicUserLabel($user)
|
||
{
|
||
if (!is_array($user)) { return 'Гость'; }
|
||
$parts = array();
|
||
foreach (array('firstname', 'lastname', 'name') as $key) {
|
||
if (!empty($user[$key])) { $parts[] = trim((string) $user[$key]); }
|
||
}
|
||
|
||
$name = trim(implode(' ', array_unique($parts)));
|
||
if ($name === '') { $name = isset($user['email']) ? (string) $user['email'] : 'Публичный пользователь'; }
|
||
$id = isset($user['Id']) ? (int) $user['Id'] : (isset($user['id']) ? (int) $user['id'] : 0);
|
||
return $name . ' (#' . $id . ')';
|
||
}
|
||
|
||
protected static function scalarLabel($value)
|
||
{
|
||
if (is_bool($value)) { return $value ? 'Да' : 'Нет'; }
|
||
if ($value === null) { return 'null'; }
|
||
if (is_array($value)) { return count($value) . ' элементов'; }
|
||
return (string) $value;
|
||
}
|
||
|
||
protected static function httpGroups(array $groups)
|
||
{
|
||
$html = '<div class="pdt-http-groups">';
|
||
foreach ($groups as $name => $values) {
|
||
$count = self::leafCount($values);
|
||
$tone = $name === 'POST' ? 'violet' : ($name === 'COOKIE' ? 'amber' : ($name === 'HEADERS' ? 'green' : 'blue'));
|
||
$html .= '<section><header><span class="pdt-badge pdt-badge-' . $tone . '">' . self::e($name) . '</span>' . self::countBadge($count, $tone) . '</header>'
|
||
. self::dataView(is_array($values) ? $values : array($values)) . '</section>';
|
||
}
|
||
|
||
return $html . '</div>';
|
||
}
|
||
|
||
protected static function fileList(array $files)
|
||
{
|
||
if (!$files) { return self::emptyState('Файлы не собраны', 'Список подключённых PHP-файлов пуст.'); }
|
||
$html = self::filterBar('files', 'Найти подключённый файл', count($files));
|
||
$html .= '<div class="pdt-file-list" data-pdt-filter-list="files">';
|
||
foreach ($files as $index => $file) {
|
||
$directory = dirname($file);
|
||
$name = basename($file);
|
||
$html .= '<div data-pdt-filter-item data-pdt-filter-text="' . self::e(self::lower($file)) . '"><span>' . ($index + 1) . '</span><div><strong>' . self::e($name) . '</strong>'
|
||
. '<code>' . self::e($directory === '.' ? '/' : $directory . '/') . '</code></div></div>';
|
||
}
|
||
|
||
return $html . '<div class="pdt-filter-empty" data-pdt-filter-empty hidden>Совпадений не найдено.</div></div>';
|
||
}
|
||
|
||
protected static function inspectorPanel(array $entries)
|
||
{
|
||
if (!$entries) {
|
||
return self::emptyState('Элементы не найдены', 'Страница не содержит поддерживаемых границ вывода.');
|
||
}
|
||
|
||
$available = count(array_filter($entries, function ($entry) { return !empty($entry['has_boundary']); }));
|
||
$html = '<div class="pdt-inspector-actions"><button type="button" class="pdt-command" data-pdt-inspector-toggle>Показать на странице</button><span>Доступно областей: <b>' . (int) $available . '</b></span></div>';
|
||
$html .= self::filterBar('elements', 'Тип, название, ID, alias или тег', count($entries));
|
||
$html .= '<div class="pdt-inspector-list" data-pdt-filter-list="elements">';
|
||
foreach ($entries as $entry) {
|
||
$type = isset($entry['type']) ? (string) $entry['type'] : 'component';
|
||
$token = isset($entry['token']) ? (int) $entry['token'] : 0;
|
||
$title = isset($entry['title']) && $entry['title'] !== '' ? (string) $entry['title'] : self::inspectorLabel($type);
|
||
$identity = isset($entry['entity_id']) && $entry['entity_id'] !== '' ? '#' . $entry['entity_id'] : '';
|
||
$alias = isset($entry['alias']) && $entry['alias'] !== '' ? (string) $entry['alias'] : '';
|
||
$tag = isset($entry['tag']) ? (string) $entry['tag'] : '';
|
||
$search = self::lower(self::inspectorLabel($type) . ' ' . $title . ' ' . $identity . ' ' . $alias . ' ' . $tag);
|
||
$depth = isset($entry['depth']) ? max(0, min(8, (int) $entry['depth'])) : 0;
|
||
$html .= '<article class="pdt-inspector-row pdt-inspector-' . self::e($type) . (!empty($entry['has_boundary']) ? '' : ' is-unmapped') . '" data-pdt-filter-item data-pdt-filter-text="' . self::e($search) . '" data-pdt-inspector-row="' . $token . '" style="--pdt-depth:' . $depth . '">';
|
||
$html .= '<span class="pdt-inspector-dot" aria-hidden="true"></span><div class="pdt-inspector-info"><small>' . self::e(self::inspectorLabel($type)) . ($identity !== '' ? ' · ' . self::e($identity) : '') . '</small><strong>' . self::e($title) . '</strong>';
|
||
if ($alias !== '' || $tag !== '') { $html .= '<code>' . self::e($tag !== '' ? $tag : $alias) . '</code>'; }
|
||
$html .= '</div><div class="pdt-inspector-row-actions">';
|
||
if (!empty($entry['has_boundary'])) { $html .= '<button type="button" data-pdt-inspector-find="' . $token . '">Найти</button>'; }
|
||
if ($tag !== '') { $html .= '<button type="button" data-pdt-inspector-copy="' . $token . '">Копировать тег</button>'; }
|
||
if (!empty($entry['edit_url'])) { $html .= '<a href="' . self::e($entry['edit_url']) . '" target="_blank" rel="noopener">Редактировать</a>'; }
|
||
$html .= '</div></article>';
|
||
}
|
||
|
||
return $html . '<div class="pdt-filter-empty" data-pdt-filter-empty hidden>Совпадений не найдено.</div></div>';
|
||
}
|
||
|
||
protected static function inspectorLabel($type)
|
||
{
|
||
$labels = array(
|
||
'document' => 'Документ',
|
||
'block' => 'Блок',
|
||
'request' => 'Запрос',
|
||
'navigation' => 'Навигация',
|
||
'module' => 'Модуль',
|
||
);
|
||
|
||
return isset($labels[$type]) ? $labels[$type] : 'Компонент';
|
||
}
|
||
|
||
protected static function filterBar($id, $placeholder, $count)
|
||
{
|
||
return '<div class="pdt-filter"><span aria-hidden="true">⌕</span><input type="search" data-pdt-filter="' . self::e($id) . '" placeholder="' . self::e($placeholder) . '" autocomplete="off"><small>' . (int) $count . '</small></div>';
|
||
}
|
||
|
||
protected static function emptyState($title, $text, $tone = 'neutral')
|
||
{
|
||
return '<div class="pdt-empty pdt-empty-' . self::e($tone) . '"><strong>' . self::e($title) . '</strong><span>' . self::e($text) . '</span></div>';
|
||
}
|
||
|
||
protected static function shortId($value)
|
||
{
|
||
$value = (string) $value;
|
||
if (strlen($value) <= 16) { return $value; }
|
||
return substr($value, 0, 8) . '…' . substr($value, -6);
|
||
}
|
||
|
||
protected static function lower($value)
|
||
{
|
||
return function_exists('mb_strtolower') ? mb_strtolower((string) $value, 'UTF-8') : strtolower((string) $value);
|
||
}
|
||
|
||
protected static function plural($count, $one, $few, $many)
|
||
{
|
||
$count = abs((int) $count) % 100;
|
||
$last = $count % 10;
|
||
if ($count > 10 && $count < 20) { return $many; }
|
||
if ($last > 1 && $last < 5) { return $few; }
|
||
if ($last === 1) { return $one; }
|
||
return $many;
|
||
}
|
||
|
||
protected static function headers()
|
||
{
|
||
$headers = array();
|
||
foreach ($_SERVER as $key => $value) {
|
||
if (strpos($key, 'HTTP_') !== 0 && !in_array($key, array('CONTENT_TYPE', 'CONTENT_LENGTH'), true)) { continue; }
|
||
$name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', preg_replace('/^HTTP_/', '', $key)))));
|
||
$headers[$name] = $value;
|
||
}
|
||
|
||
return $headers;
|
||
}
|
||
|
||
protected static function masked($value, $key = '')
|
||
{
|
||
if (is_array($value)) {
|
||
$result = array();
|
||
foreach ($value as $itemKey => $itemValue) { $result[$itemKey] = self::masked($itemValue, (string) $itemKey); }
|
||
return $result;
|
||
}
|
||
|
||
if (preg_match('/pass|password|passwd|secret|token|csrf|auth|cookie|api.?key|session|sessid|phpsessid/i', $key)) { return '***'; }
|
||
return is_scalar($value) || $value === null ? $value : gettype($value);
|
||
}
|
||
|
||
protected static function safeUri($uri)
|
||
{
|
||
$uri = (string) $uri;
|
||
$parts = parse_url($uri);
|
||
if (!is_array($parts) || empty($parts['query'])) { return $uri; }
|
||
parse_str($parts['query'], $query);
|
||
$query = self::masked($query);
|
||
$path = isset($parts['path']) ? $parts['path'] : '/';
|
||
return $path . ($query ? '?' . http_build_query($query) : '');
|
||
}
|
||
|
||
protected static function caller($caller)
|
||
{
|
||
if (!is_array($caller) || !$caller) { return ''; }
|
||
for ($index = count($caller) - 1; $index >= 0; $index--) {
|
||
$frame = $caller[$index];
|
||
if (!is_array($frame) || empty($frame['call_file'])) { continue; }
|
||
$file = str_replace('\\', '/', (string) $frame['call_file']);
|
||
if (strpos($file, '/system/App/Common/Db/') !== false) { continue; }
|
||
$file = str_replace(BASEPATH . '/', '', $file);
|
||
return $file . (isset($frame['call_line']) ? ':' . $frame['call_line'] : '');
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
protected static function mask($value)
|
||
{
|
||
return preg_replace('/((?:password|passwd|secret|token|api[_-]?key)\s*=\s*)([\'\"][^\'\"]*[\'\"]|[^\s,;]+)/i', '$1***', (string) $value);
|
||
}
|
||
|
||
protected static function number($value)
|
||
{
|
||
return (float) str_replace(',', '.', (string) $value);
|
||
}
|
||
|
||
protected static function timestamp($value)
|
||
{
|
||
$parts = explode(' ', trim((string) $value), 2);
|
||
return count($parts) === 2 ? (float) $parts[1] + (float) $parts[0] : (float) $value;
|
||
}
|
||
|
||
protected static function bytes($bytes)
|
||
{
|
||
return number_format((float) $bytes / 1048576, 1, ',', ' ') . ' MB';
|
||
}
|
||
|
||
protected static function isAjax()
|
||
{
|
||
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower((string) $_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
|
||
}
|
||
|
||
protected static function e($value)
|
||
{
|
||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||
}
|
||
|
||
protected static function assets()
|
||
{
|
||
return <<<'HTML'
|
||
<style>
|
||
#pdt-root,#pdt-root *,#pdt-panel,#pdt-panel *,#pdi-layer,#pdi-layer *{box-sizing:border-box;letter-spacing:0}
|
||
#pdt-root,#pdt-panel,#pdi-layer{font:13px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#182033;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
|
||
#pdt-root{position:fixed;z-index:2147483646;right:12px;top:12px}
|
||
#pdt-root.is-header-mounted{position:relative;right:auto;top:auto;flex:0 0 118px;width:118px;margin-right:10px}
|
||
#pdt-root.is-header-mounted #pdt-toggle{width:100%;justify-content:center}
|
||
#pdt-root.is-header-mounted #pdt-toggle span:nth-of-type(n+2){display:none}
|
||
#pdt-root button,#pdt-root input,#pdt-panel button,#pdt-panel input{font:inherit}
|
||
#pdt-root button:focus-visible,#pdt-root input:focus-visible,#pdt-root summary:focus-visible,#pdt-panel button:focus-visible,#pdt-panel input:focus-visible,#pdt-panel a:focus-visible,#pdt-panel summary:focus-visible,#pdi-layer button:focus-visible{outline:2px solid #2563eb;outline-offset:2px}
|
||
#pdt-root [hidden],#pdt-panel [hidden],#pdi-layer [hidden]{display:none!important}
|
||
#pdt-toggle{display:flex;align-items:center;gap:10px;min-height:40px;padding:0 13px;border:1px solid #334155;border-radius:6px;background:#111827;color:#e5e7eb;box-shadow:0 8px 24px rgba(15,23,42,.28);cursor:pointer;transition:transform .15s ease,background-color .15s ease,box-shadow .15s ease}
|
||
#pdt-toggle:hover{background:#1e293b;box-shadow:0 10px 28px rgba(15,23,42,.34)}
|
||
#pdt-toggle:active{transform:scale(.96)}
|
||
#pdt-toggle b{color:#60a5fa;font-size:12px}
|
||
#pdt-toggle span,#pdt-toggle i{}
|
||
#pdt-toggle i{display:grid;place-items:center;min-width:20px;height:20px;padding:0 5px;border-radius:10px;background:#dc2626;color:#fff;font-size:11px;font-style:normal;font-weight:700}
|
||
#pdt-panel{position:fixed;z-index:2147483647;right:12px;bottom:12px;width:min(1240px,calc(100vw - 24px));height:min(78vh,820px);border:1px solid #334155;border-radius:8px;background:#f5f7fa;box-shadow:0 24px 70px rgba(15,23,42,.4);overflow:hidden}
|
||
#pdt-panel>header{height:56px;display:flex;align-items:center;gap:12px;padding:0 8px 0 16px;background:#111827;color:#fff;cursor:grab;touch-action:none;user-select:none}
|
||
#pdt-panel.is-dragging>header{cursor:grabbing}
|
||
.pdt-brand{display:flex;align-items:center;gap:8px;white-space:nowrap}
|
||
.pdt-brand strong{font-size:14px}
|
||
.pdt-method{display:inline-flex;align-items:center;height:24px;padding:0 7px;border:1px solid #475569;border-radius:4px;color:#cbd5e1;font:700 11px/1 ui-monospace,SFMono-Regular,Consolas,monospace}
|
||
#pdt-panel>header>code{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#94a3b8;font:12px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;user-select:text}
|
||
.pdt-head-actions{display:flex;align-items:center;flex:0 0 auto;gap:2px;margin-left:auto}
|
||
#pdt-reset-position,#pdt-close{display:grid;place-items:center;flex:0 0 40px;width:40px;height:40px;border:0;border-radius:5px;background:transparent;color:#cbd5e1;line-height:1;cursor:pointer;transition:transform .15s ease,background-color .15s ease,color .15s ease}
|
||
#pdt-reset-position{font-size:20px}#pdt-close{font-size:25px}
|
||
#pdt-reset-position:hover,#pdt-close:hover{background:#334155;color:#fff}
|
||
#pdt-reset-position:active,#pdt-close:active{transform:scale(.96)}
|
||
#pdt-panel>nav{height:52px;display:flex;align-items:center;gap:3px;padding:5px 10px;border-bottom:1px solid #dce3eb;background:#fff;overflow-x:auto;overflow-y:hidden;scrollbar-width:thin}
|
||
#pdt-panel>nav button{display:flex;align-items:center;gap:7px;min-height:40px;padding:0 10px;border:0;border-radius:5px;background:transparent;color:#526074;font-weight:650;cursor:pointer;white-space:nowrap;transition:transform .15s ease,background-color .15s ease,color .15s ease}
|
||
#pdt-panel>nav button:hover{background:#f1f5f9;color:#1e293b}
|
||
#pdt-panel>nav button:active{transform:scale(.96)}
|
||
#pdt-panel>nav button.is-active{background:#dbeafe;color:#1d4ed8}
|
||
#pdt-panel>nav button em{display:grid;place-items:center;min-width:20px;height:20px;padding:0 5px;border-radius:10px;background:#e8edf3;color:#64748b;font-size:10px;font-style:normal;font-weight:750;}
|
||
#pdt-panel>nav button.is-active em{background:#fff;color:#1d4ed8}
|
||
#pdt-panel>nav button em.is-alert{background:#fee2e2;color:#b91c1c}
|
||
#pdt-panel>main{height:calc(100% - 108px);overflow:auto;padding:16px;overscroll-behavior:contain}
|
||
#pdt-panel section[role=tabpanel]{max-width:100%}
|
||
.pdt-section-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin:0 0 12px;padding:0 2px}
|
||
.pdt-section-head h2{margin:0;color:#172033;font-size:17px;line-height:1.3;font-weight:720;text-wrap:balance}
|
||
.pdt-section-head p{margin:3px 0 0;color:#6b778a;font-size:12px;overflow-wrap:anywhere;text-wrap:pretty}
|
||
.pdt-badge,.pdt-count{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;border-radius:4px;font-weight:700;}
|
||
.pdt-badge{min-height:24px;padding:0 7px;font-size:10px;text-transform:uppercase}
|
||
.pdt-count{min-width:26px;height:26px;padding:0 7px;font-size:11px}
|
||
.pdt-badge-blue,.pdt-count-blue{background:#dbeafe;color:#1d4ed8}.pdt-badge-green,.pdt-count-green{background:#dcfce7;color:#15803d}.pdt-badge-amber,.pdt-count-amber{background:#fef3c7;color:#a16207}.pdt-badge-red,.pdt-count-red{background:#fee2e2;color:#b91c1c}.pdt-badge-violet,.pdt-count-violet{background:#ede9fe;color:#6d28d9}.pdt-badge-slate,.pdt-count-slate{background:#e8edf3;color:#526074}
|
||
.pdt-cards{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:12px}
|
||
.pdt-metric{position:relative;min-width:0;padding:13px 14px 14px;border:0;border-radius:7px;background:#2563eb;box-shadow:0 1px 2px rgba(15,23,42,.08);overflow:hidden}
|
||
.pdt-metric.pdt-tone-green{background:#15803d}.pdt-metric.pdt-tone-amber{background:#b45309}.pdt-metric.pdt-tone-red{background:#dc2626}.pdt-metric.pdt-tone-violet{background:#7c3aed}.pdt-metric.pdt-tone-blue{background:#2563eb}
|
||
.pdt-metric small,.pdt-metric strong{display:block;overflow-wrap:anywhere}
|
||
.pdt-metric small{color:rgba(255,255,255,.82);font-size:11px}
|
||
.pdt-metric strong{margin-top:5px;color:#fff;font-size:18px;line-height:1.25;}
|
||
.pdt-dl{margin:0;border:1px solid #dce3eb;border-radius:7px;background:#fff;overflow:hidden}
|
||
.pdt-dl>div{display:grid;grid-template-columns:180px minmax(0,1fr);border-bottom:1px solid #edf1f5}
|
||
.pdt-dl>div:last-child{border-bottom:0}
|
||
.pdt-dl dt,.pdt-dl dd{margin:0;padding:10px 12px}
|
||
.pdt-dl dt{background:#fafbfc;color:#526074;font-weight:650}
|
||
.pdt-dl dd{min-width:0;color:#273449;overflow-wrap:anywhere}
|
||
.pdt-identities{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-bottom:18px}
|
||
.pdt-identities>article{min-width:0;border:1px solid #dce3eb;border-radius:7px;background:#fff;overflow:hidden}
|
||
.pdt-identities>article>header{display:flex;align-items:center;gap:9px;min-height:46px;padding:8px 11px;border-bottom:1px solid #edf1f5;background:#fafbfc}
|
||
.pdt-identities>article>header strong{color:#273449;font-size:12px}
|
||
.pdt-identities .pdt-data{border:0;border-radius:0}
|
||
.pdt-identities .pdt-empty{min-height:132px;border:0;border-radius:0}
|
||
.pdt-filter{display:grid;grid-template-columns:24px minmax(0,1fr) auto;align-items:center;gap:4px;min-height:44px;margin-bottom:10px;padding:0 10px;border:1px solid #ccd6e2;border-radius:6px;background:#fff}
|
||
.pdt-filter:focus-within{border-color:#60a5fa;box-shadow:0 0 0 3px rgba(59,130,246,.12)}
|
||
.pdt-filter>span{color:#8491a3;font-size:20px;line-height:1}
|
||
.pdt-filter input{min-width:0;height:40px;border:0;outline:0;background:transparent;color:#172033}
|
||
.pdt-filter input::placeholder{color:#8a97a8}
|
||
.pdt-filter small{padding:2px 6px;border-radius:4px;background:#edf1f5;color:#64748b;font-weight:700;}
|
||
.pdt-query-list{display:grid;gap:8px}
|
||
.pdt-query{border:1px solid #dce3eb;border-radius:6px;background:#fff;overflow:hidden}
|
||
.pdt-query summary{display:grid;grid-template-columns:42px 62px minmax(0,1fr) 90px;align-items:center;gap:9px;min-height:44px;padding:7px 11px;cursor:pointer;list-style:none}
|
||
.pdt-query summary::-webkit-details-marker{display:none}
|
||
.pdt-query[open]{border-color:#b8c5d4}
|
||
.pdt-query-index{color:#8491a3;font-size:11px;}
|
||
.pdt-query summary code{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#6b778a;font:11px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace}
|
||
.pdt-query summary strong{justify-self:end;font-size:11px;}
|
||
.pdt-query pre{max-height:330px;margin:0;padding:13px 14px;border-top:1px solid #26344a;background:#101827;color:#dbeafe;white-space:pre-wrap;overflow:auto;overflow-wrap:anywhere;font:12px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace}
|
||
.pdt-value-green{color:#15803d}.pdt-value-amber{color:#a16207}.pdt-value-red{color:#b91c1c}.pdt-value-blue{color:#1d4ed8}.pdt-value-slate{color:#64748b}.pdt-notice{margin:0 0 10px;padding:9px 11px;border:1px solid #f5d38a;border-radius:6px;background:#fffbeb;color:#8a5a00;font-size:11px}
|
||
.pdt-debug-entries{display:grid;gap:10px}
|
||
.pdt-debug-entries>article{min-width:0;border:1px solid #dce3eb;border-radius:7px;background:#fff;overflow:hidden}
|
||
.pdt-debug-entries>article>header{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:52px;padding:7px 11px;border-bottom:1px solid #edf1f5;background:#fafbfc}
|
||
.pdt-debug-entries>article>header>div{display:flex;align-items:center;min-width:0;gap:10px}
|
||
.pdt-debug-entries>article>header>div>span{display:grid;place-items:center;flex:0 0 25px;width:25px;height:25px;border-radius:5px;background:#ede9fe;color:#6d28d9;font-size:10px;font-weight:750;}
|
||
.pdt-debug-entries>article>header>div>div{min-width:0}
|
||
.pdt-debug-entries>article>header strong,.pdt-debug-entries>article>header code{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.pdt-debug-entries>article>header strong{color:#273449;font-size:12px}.pdt-debug-entries>article>header code{margin-top:2px;color:#8491a3;font:10px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace}
|
||
.pdt-debug-entries>article>.pdt-data{border:0;border-radius:0}
|
||
.pdt-timeline{border:1px solid #dce3eb;border-radius:7px;background:#fff;overflow:hidden}
|
||
.pdt-timeline-row{display:grid;grid-template-columns:minmax(190px,1.3fr) minmax(120px,2fr) 88px;align-items:center;gap:12px;min-height:43px;padding:7px 12px;border-bottom:1px solid #edf1f5}
|
||
.pdt-timeline-row:last-child{border-bottom:0}
|
||
.pdt-timeline-row>code{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#435269;font:11px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace}
|
||
.pdt-timeline-track{height:6px;border-radius:3px;background:#edf1f5;overflow:hidden}
|
||
.pdt-timeline-track i{display:block;height:100%;border-radius:3px}.pdt-bg-green{background:#22c55e}.pdt-bg-amber{background:#f59e0b}.pdt-bg-red{background:#ef4444}.pdt-bg-blue{background:#3b82f6}
|
||
.pdt-timeline-row>strong{text-align:right;font-size:11px;}
|
||
.pdt-data{border:1px solid #dce3eb;border-radius:7px;background:#fff;overflow:hidden}
|
||
.pdt-data.is-nested{margin:0;border:0;border-top:1px solid #edf1f5;border-radius:0;background:#fafbfc}
|
||
.pdt-data-row{display:grid;grid-template-columns:minmax(150px,32%) minmax(0,1fr);border-bottom:1px solid #edf1f5}
|
||
.pdt-data-row:last-child{border-bottom:0}
|
||
.pdt-data-row>code,.pdt-data-row>div{min-width:0;padding:9px 11px}
|
||
.pdt-data-row>code{color:#475569;font:600 11px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace}
|
||
.pdt-data-row>div{overflow-wrap:anywhere}
|
||
.pdt-data-node{border-bottom:1px solid #edf1f5}.pdt-data-node:last-child{border-bottom:0}
|
||
.pdt-data-node>summary{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:40px;padding:6px 11px;cursor:pointer;list-style:none}
|
||
.pdt-data-node>summary::-webkit-details-marker{display:none}
|
||
.pdt-data-node>summary:before{content:"›";flex:0 0 auto;color:#8491a3;font-size:17px;transition:transform .15s ease}
|
||
.pdt-data-node[open]>summary:before{transform:rotate(90deg)}
|
||
.pdt-data-node>summary code{min-width:0;margin-right:auto;overflow:hidden;text-overflow:ellipsis;color:#475569;font:600 11px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace}
|
||
.pdt-data-node>summary span{color:#8491a3;font-size:10px;white-space:nowrap}
|
||
.pdt-data-node>.pdt-data{padding-left:18px}
|
||
.pdt-scalar{font:12px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace}.pdt-scalar-number{color:#1d4ed8}.pdt-scalar-bool{color:#6d28d9;font-weight:700}.pdt-scalar-null,.pdt-scalar-empty{color:#8491a3;font-style:italic}.pdt-scalar-string{color:#273449}
|
||
.pdt-depth-limit,.pdt-data-scalar{padding:10px 12px;color:#8491a3}
|
||
.pdt-http-groups{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;align-items:start}
|
||
.pdt-http-groups>section{min-width:0}
|
||
.pdt-http-groups>section>header{display:flex;align-items:center;justify-content:space-between;min-height:36px;padding:0 2px 6px}
|
||
.pdt-errors{display:grid;gap:8px}
|
||
.pdt-errors article{display:grid;grid-template-columns:130px minmax(0,1fr);gap:6px 12px;padding:12px;border:1px solid #fecaca;border-left:3px solid #ef4444;border-radius:6px;background:#fff}
|
||
.pdt-errors article>div{display:flex;align-items:flex-start;gap:7px}.pdt-errors article small{color:#8491a3;font-size:10px;white-space:nowrap}
|
||
.pdt-errors p{margin:1px 0;color:#273449;overflow-wrap:anywhere;text-wrap:pretty}
|
||
.pdt-errors code{grid-column:2;color:#6b778a;overflow-wrap:anywhere;font:11px/1.45 ui-monospace,SFMono-Regular,Consolas,monospace}
|
||
.pdt-file-list{border:1px solid #dce3eb;border-radius:7px;background:#fff;overflow:hidden}
|
||
.pdt-file-list>[data-pdt-filter-item]{display:grid;grid-template-columns:44px minmax(0,1fr);align-items:center;min-height:46px;border-bottom:1px solid #edf1f5}
|
||
.pdt-file-list>[data-pdt-filter-item]:last-of-type{border-bottom:0}
|
||
.pdt-file-list>[data-pdt-filter-item]>span{padding:0 10px;color:#94a3b8;text-align:right;font-size:10px;}
|
||
.pdt-file-list>[data-pdt-filter-item]>div{min-width:0;padding:6px 11px;border-left:1px solid #edf1f5}
|
||
.pdt-file-list strong,.pdt-file-list code{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.pdt-file-list strong{color:#273449;font-size:12px}.pdt-file-list code{margin-top:2px;color:#8491a3;font:10px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace}
|
||
.pdt-empty{display:grid;place-items:center;min-height:150px;padding:24px;border:1px dashed #cbd5e1;border-radius:7px;background:#fff;color:#6b778a;text-align:center}
|
||
.pdt-empty strong{color:#435269;font-size:14px}.pdt-empty span{max-width:520px;margin-top:4px;text-wrap:pretty}.pdt-empty-success{border-color:#bbf7d0;background:#f7fef9}.pdt-empty-success strong{color:#15803d}
|
||
.pdt-filter-empty{padding:22px;color:#6b778a;text-align:center}
|
||
.pdt-inspector-actions{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px;padding:10px 12px;border-radius:7px;background:#eaf2ff;color:#334155;box-shadow:inset 0 0 0 1px rgba(37,99,235,.1)}
|
||
.pdt-inspector-actions span{font-size:11px}.pdt-inspector-actions b{}
|
||
.pdt-command{min-height:40px;padding:0 14px;border:0;border-radius:5px;background:#2563eb;color:#fff;font-weight:700;cursor:pointer;transition:transform .15s ease,background-color .15s ease}
|
||
.pdt-command:hover{background:#1d4ed8}.pdt-command:active{transform:scale(.96)}.pdt-command.is-active{background:#0f766e}
|
||
.pdt-inspector-list{display:grid;gap:7px}
|
||
.pdt-inspector-row{--pdt-tone:#64748b;display:grid;grid-template-columns:12px minmax(0,1fr) auto;align-items:center;gap:11px;min-height:64px;margin-left:calc(var(--pdt-depth) * 14px);padding:9px 10px;border-radius:7px;background:#fff;box-shadow:0 1px 2px rgba(15,23,42,.08),inset 0 0 0 1px rgba(15,23,42,.07)}
|
||
.pdt-inspector-row.is-unmapped{opacity:.58}.pdt-inspector-document{--pdt-tone:#16a34a}.pdt-inspector-block{--pdt-tone:#2563eb}.pdt-inspector-request{--pdt-tone:#7c3aed}.pdt-inspector-navigation{--pdt-tone:#d97706}.pdt-inspector-module{--pdt-tone:#0f766e}
|
||
.pdt-inspector-dot{width:10px;height:10px;border-radius:50%;background:var(--pdt-tone);box-shadow:0 0 0 3px rgba(100,116,139,.14)}
|
||
.pdt-inspector-info{min-width:0}.pdt-inspector-info small,.pdt-inspector-info strong,.pdt-inspector-info code{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.pdt-inspector-info small{color:var(--pdt-tone);font-size:10px;font-weight:750;text-transform:uppercase}.pdt-inspector-info strong{margin-top:2px;color:#273449;font-size:12px}.pdt-inspector-info code{margin-top:2px;color:#7a8798;font:10px/1.35 ui-monospace,SFMono-Regular,Consolas,monospace}
|
||
.pdt-inspector-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:4px}
|
||
.pdt-inspector-row-actions button,.pdt-inspector-row-actions a{display:inline-flex;align-items:center;justify-content:center;min-height:36px;padding:0 10px;border:0;border-radius:5px;background:#eef2f7;color:#435269;font-size:11px;font-weight:700;text-decoration:none;cursor:pointer;transition:transform .15s ease,background-color .15s ease,color .15s ease}
|
||
.pdt-inspector-row-actions button:hover,.pdt-inspector-row-actions a:hover{background:#dbeafe;color:#1d4ed8}.pdt-inspector-row-actions button:active,.pdt-inspector-row-actions a:active{transform:scale(.96)}
|
||
#pdi-layer{position:fixed;z-index:2147483644;inset:0;pointer-events:none}
|
||
.pdi-box{--pdi-tone:#64748b;position:fixed;border:2px solid var(--pdi-tone);border-radius:4px;background:rgba(100,116,139,.07);box-shadow:0 0 0 1px rgba(255,255,255,.72),0 4px 18px rgba(15,23,42,.12);pointer-events:none}
|
||
.pdi-document{--pdi-tone:#16a34a;background:rgba(22,163,74,.07)}.pdi-block{--pdi-tone:#2563eb;background:rgba(37,99,235,.07)}.pdi-request{--pdi-tone:#7c3aed;background:rgba(124,58,237,.07)}.pdi-navigation{--pdi-tone:#d97706;background:rgba(217,119,6,.07)}.pdi-module{--pdi-tone:#0f766e;background:rgba(15,118,110,.07)}
|
||
.pdi-box.is-selected{box-shadow:0 0 0 3px rgba(37,99,235,.2),0 6px 24px rgba(15,23,42,.18)}
|
||
.pdi-label{position:absolute;left:-2px;top:-28px;display:flex;align-items:center;max-width:min(360px,80vw);height:26px;padding:0 9px;border:0;border-radius:4px;background:var(--pdi-tone);color:#fff;box-shadow:0 4px 14px rgba(15,23,42,.24);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10px;font-weight:750;cursor:pointer;pointer-events:auto}
|
||
.pdi-box.is-label-inside .pdi-label{top:2px;left:2px}
|
||
#pdi-control{position:fixed;z-index:2147483645;left:12px;top:12px;display:flex;align-items:center;gap:9px;min-height:42px;padding:4px 5px 4px 12px;border-radius:6px;background:#111827;color:#e5e7eb;box-shadow:0 10px 30px rgba(15,23,42,.34);pointer-events:auto}
|
||
#pdi-control strong{font-size:11px}#pdi-control span{color:#93c5fd;font-size:10px;}
|
||
#pdi-control button{min-width:34px;height:34px;border:0;border-radius:4px;background:#334155;color:#fff;font-size:20px;line-height:1;cursor:pointer;transition:transform .15s ease,background-color .15s ease}
|
||
#pdi-control button:hover{background:#475569}#pdi-control button:active{transform:scale(.96)}
|
||
@media(max-width:760px){
|
||
#pdt-root{right:8px;top:8px}
|
||
#pdt-root.is-header-mounted{flex-basis:64px;width:64px;margin-right:6px}
|
||
#pdt-root.is-header-mounted #pdt-toggle span{display:none}
|
||
#pdt-toggle span:nth-of-type(n+2){display:none}
|
||
#pdt-panel{right:6px;bottom:6px;left:auto!important;top:auto!important;width:calc(100vw - 12px);height:calc(100dvh - 12px);max-height:none}
|
||
#pdt-panel>header{height:52px;padding-left:11px}.pdt-brand strong{font-size:12px}#pdt-panel>header>code{display:none}
|
||
#pdt-reset-position{display:none}
|
||
#pdt-panel>nav{height:50px;padding:4px 7px}
|
||
#pdt-panel>main{height:calc(100% - 102px);padding:11px}
|
||
.pdt-cards{grid-template-columns:repeat(2,minmax(0,1fr))}.pdt-metric{padding:11px}.pdt-metric strong{font-size:15px}
|
||
.pdt-dl>div{grid-template-columns:112px minmax(0,1fr)}.pdt-dl dt,.pdt-dl dd{padding:9px}
|
||
.pdt-query summary{grid-template-columns:36px minmax(0,1fr) 76px}.pdt-query summary .pdt-badge{display:none}.pdt-query summary code{grid-column:2}
|
||
.pdt-timeline-row{grid-template-columns:minmax(130px,1.4fr) 74px}.pdt-timeline-track{display:none}
|
||
.pdt-http-groups{grid-template-columns:1fr}.pdt-data-row{grid-template-columns:minmax(100px,38%) minmax(0,1fr)}
|
||
.pdt-identities{grid-template-columns:1fr}
|
||
.pdt-errors article{grid-template-columns:1fr}.pdt-errors code{grid-column:1}
|
||
.pdt-inspector-row{grid-template-columns:12px minmax(0,1fr);margin-left:0}.pdt-inspector-row-actions{grid-column:2;justify-content:flex-start;flex-wrap:wrap}.pdt-inspector-actions{align-items:flex-start;flex-direction:column}.pdt-inspector-actions .pdt-command{width:100%}
|
||
#pdi-control{left:6px;top:6px}
|
||
}
|
||
@media(prefers-reduced-motion:reduce){#pdt-root *,#pdt-panel *,#pdi-layer *{transition-duration:0s!important;scroll-behavior:auto!important}}
|
||
</style>
|
||
<script>
|
||
(function(window,document){
|
||
'use strict';
|
||
var AvePublicDebug={
|
||
initialized:false,
|
||
storageKey:'ave-public-debug-tab',
|
||
positionKey:'ave-public-debug-position',
|
||
previousOverflow:'',
|
||
inspectorActive:false,
|
||
inspectorFrame:0,
|
||
drag:null,
|
||
init:function(){
|
||
if(this.initialized){return;}
|
||
this.root=document.getElementById('pdt-root');
|
||
this.panel=document.getElementById('pdt-panel');
|
||
this.toggleButton=document.getElementById('pdt-toggle');
|
||
this.closeButton=document.getElementById('pdt-close');
|
||
this.resetButton=document.getElementById('pdt-reset-position');
|
||
this.dragHandle=this.panel?this.panel.querySelector('[data-pdt-drag-handle]'):null;
|
||
if(!this.root||!this.panel||!this.toggleButton||!this.closeButton){return;}
|
||
this.inspectorEntries=this.readInspectorData();
|
||
this.inspectorByToken={};
|
||
for(var index=0;index<this.inspectorEntries.length;index++){
|
||
this.inspectorByToken[String(this.inspectorEntries[index].token)]=this.inspectorEntries[index];
|
||
}
|
||
this.captureInspectorAnchors();
|
||
this.initialized=true;
|
||
this.build();
|
||
this.events();
|
||
this.restoreState();
|
||
},
|
||
build:function(){
|
||
document.body.appendChild(this.panel);
|
||
var headerUser=document.querySelector('.pre_header_user');
|
||
if(headerUser){
|
||
headerUser.insertBefore(this.root,headerUser.firstChild);
|
||
this.root.classList.add('is-header-mounted');
|
||
}
|
||
this.applyStoredPosition();
|
||
},
|
||
events:function(){
|
||
var self=this;
|
||
this.toggleButton.addEventListener('click',function(){self.setOpen(true);});
|
||
this.closeButton.addEventListener('click',function(){self.setOpen(false);});
|
||
if(this.resetButton){this.resetButton.addEventListener('click',function(){self.resetPosition();});}
|
||
this.panel.addEventListener('click',function(event){self.onPanelClick(event);});
|
||
this.panel.addEventListener('keydown',function(event){self.onTabKeydown(event);});
|
||
if(this.dragHandle){
|
||
this.dragHandle.addEventListener('pointerdown',function(event){self.startDrag(event);});
|
||
this.dragHandle.addEventListener('pointermove',function(event){self.moveDrag(event);});
|
||
this.dragHandle.addEventListener('pointerup',function(event){self.endDrag(event);});
|
||
this.dragHandle.addEventListener('pointercancel',function(event){self.endDrag(event);});
|
||
}
|
||
document.addEventListener('keydown',function(event){
|
||
if(event.key!=='Escape'){return;}
|
||
if(self.inspectorActive){self.disableInspector();return;}
|
||
if(!self.panel.hidden){self.setOpen(false);}
|
||
});
|
||
window.addEventListener('resize',function(){self.clampCurrentPosition();self.scheduleInspectorLayout();});
|
||
window.addEventListener('scroll',function(){self.scheduleInspectorLayout();},{passive:true,capture:true});
|
||
this.each(this.panel.querySelectorAll('[data-pdt-filter]'),function(input){
|
||
input.addEventListener('input',function(){self.filter(input);});
|
||
});
|
||
},
|
||
onPanelClick:function(event){
|
||
var button=this.closest(event.target,'[data-pdt-tab]');
|
||
if(button){this.activateTab(button,false);}
|
||
var toggle=this.closest(event.target,'[data-pdt-inspector-toggle]');
|
||
if(toggle){event.preventDefault();this.toggleInspector();return;}
|
||
var find=this.closest(event.target,'[data-pdt-inspector-find]');
|
||
if(find){event.preventDefault();this.focusInspector(find.getAttribute('data-pdt-inspector-find'));return;}
|
||
var copy=this.closest(event.target,'[data-pdt-inspector-copy]');
|
||
if(copy){event.preventDefault();this.copyInspectorTag(copy,copy.getAttribute('data-pdt-inspector-copy'));}
|
||
},
|
||
onTabKeydown:function(event){
|
||
var button=this.closest(event.target,'[data-pdt-tab]');
|
||
var keys=['ArrowLeft','ArrowRight','Home','End'];
|
||
if(!button||keys.indexOf(event.key)===-1){return;}
|
||
event.preventDefault();
|
||
var tabs=Array.prototype.slice.call(this.panel.querySelectorAll('[data-pdt-tab]'));
|
||
var index=tabs.indexOf(button);
|
||
if(event.key==='Home'){index=0;}
|
||
else if(event.key==='End'){index=tabs.length-1;}
|
||
else{index=(index+(event.key==='ArrowRight'?1:-1)+tabs.length)%tabs.length;}
|
||
this.activateTab(tabs[index],true);
|
||
},
|
||
setOpen:function(open){
|
||
this.panel.hidden=!open;
|
||
this.toggleButton.hidden=open;
|
||
if(open){
|
||
this.previousOverflow=document.documentElement.style.overflow;
|
||
document.documentElement.style.overflow='hidden';
|
||
this.clampCurrentPosition();
|
||
this.closeButton.focus();
|
||
return;
|
||
}
|
||
document.documentElement.style.overflow=this.previousOverflow;
|
||
this.toggleButton.focus();
|
||
},
|
||
activateTab:function(button,focus){
|
||
if(!button){return;}
|
||
var self=this;
|
||
var id=button.getAttribute('data-pdt-tab');
|
||
this.each(this.panel.querySelectorAll('[data-pdt-tab]'),function(item){
|
||
var active=item===button;
|
||
item.classList.toggle('is-active',active);
|
||
item.setAttribute('aria-selected',active?'true':'false');
|
||
item.tabIndex=active?0:-1;
|
||
});
|
||
this.each(this.panel.querySelectorAll('[data-pdt-panel]'),function(item){
|
||
item.hidden=item.getAttribute('data-pdt-panel')!==id;
|
||
});
|
||
self.store(id);
|
||
if(focus){button.focus();}
|
||
},
|
||
filter:function(input){
|
||
var id=input.getAttribute('data-pdt-filter');
|
||
var list=this.panel.querySelector('[data-pdt-filter-list="'+id+'"]');
|
||
if(!list){return;}
|
||
var query=input.value.toLocaleLowerCase();
|
||
var visible=0;
|
||
this.each(list.querySelectorAll('[data-pdt-filter-item]'),function(item){
|
||
var show=(item.getAttribute('data-pdt-filter-text')||'').indexOf(query)!==-1;
|
||
item.hidden=!show;
|
||
if(show){visible++;}
|
||
});
|
||
var empty=list.querySelector('[data-pdt-filter-empty]');
|
||
if(empty){empty.hidden=visible!==0;}
|
||
},
|
||
restoreState:function(){
|
||
var saved=this.restore()||'overview';
|
||
var button=this.panel.querySelector('[data-pdt-tab="'+saved+'"]')||this.panel.querySelector('[data-pdt-tab]');
|
||
this.activateTab(button,false);
|
||
},
|
||
startDrag:function(event){
|
||
if(!this.canDrag(event)){return;}
|
||
var rect=this.panel.getBoundingClientRect();
|
||
this.drag={pointer:event.pointerId,startX:event.clientX,startY:event.clientY,left:rect.left,top:rect.top};
|
||
this.panel.style.left=rect.left+'px';
|
||
this.panel.style.top=rect.top+'px';
|
||
this.panel.style.right='auto';
|
||
this.panel.style.bottom='auto';
|
||
this.panel.classList.add('is-dragging');
|
||
this.dragHandle.setPointerCapture(event.pointerId);
|
||
event.preventDefault();
|
||
},
|
||
moveDrag:function(event){
|
||
if(!this.drag||this.drag.pointer!==event.pointerId){return;}
|
||
var position=this.clampPosition(this.drag.left+event.clientX-this.drag.startX,this.drag.top+event.clientY-this.drag.startY);
|
||
this.panel.style.left=position.x+'px';
|
||
this.panel.style.top=position.y+'px';
|
||
event.preventDefault();
|
||
},
|
||
endDrag:function(event){
|
||
if(!this.drag||this.drag.pointer!==event.pointerId){return;}
|
||
this.panel.classList.remove('is-dragging');
|
||
if(this.dragHandle.hasPointerCapture&&this.dragHandle.hasPointerCapture(event.pointerId)){this.dragHandle.releasePointerCapture(event.pointerId);}
|
||
this.drag=null;
|
||
this.savePosition();
|
||
},
|
||
canDrag:function(event){
|
||
if(event.button!==undefined&&event.button!==0){return false;}
|
||
if(window.matchMedia&&window.matchMedia('(max-width:760px)').matches){return false;}
|
||
return !this.closest(event.target,'button,a,input,select,textarea,summary,code,[contenteditable="true"]');
|
||
},
|
||
clampPosition:function(x,y){
|
||
var margin=6;
|
||
var width=this.panel.offsetWidth||this.panel.getBoundingClientRect().width;
|
||
var height=this.panel.offsetHeight||this.panel.getBoundingClientRect().height;
|
||
return {
|
||
x:Math.max(margin,Math.min(Number(x)||margin,Math.max(margin,window.innerWidth-width-margin))),
|
||
y:Math.max(margin,Math.min(Number(y)||margin,Math.max(margin,window.innerHeight-height-margin)))
|
||
};
|
||
},
|
||
clampCurrentPosition:function(){
|
||
if(window.matchMedia&&window.matchMedia('(max-width:760px)').matches){return;}
|
||
if(!this.panel.style.left||!this.panel.style.top){return;}
|
||
var position=this.clampPosition(parseFloat(this.panel.style.left),parseFloat(this.panel.style.top));
|
||
this.panel.style.left=position.x+'px';
|
||
this.panel.style.top=position.y+'px';
|
||
this.storePosition(position);
|
||
},
|
||
savePosition:function(){
|
||
if(!this.panel.style.left||!this.panel.style.top){return;}
|
||
this.storePosition({x:parseFloat(this.panel.style.left),y:parseFloat(this.panel.style.top)});
|
||
},
|
||
storePosition:function(position){
|
||
try{window.sessionStorage.setItem(this.positionKey,JSON.stringify(position));}catch(error){}
|
||
},
|
||
applyStoredPosition:function(){
|
||
if(window.matchMedia&&window.matchMedia('(max-width:760px)').matches){return;}
|
||
var position=null;
|
||
try{position=JSON.parse(window.sessionStorage.getItem(this.positionKey)||'null');}catch(error){}
|
||
if(!position||!isFinite(position.x)||!isFinite(position.y)){return;}
|
||
position=this.clampPosition(position.x,position.y);
|
||
this.panel.style.left=position.x+'px';
|
||
this.panel.style.top=position.y+'px';
|
||
this.panel.style.right='auto';
|
||
this.panel.style.bottom='auto';
|
||
},
|
||
resetPosition:function(){
|
||
try{window.sessionStorage.removeItem(this.positionKey);}catch(error){}
|
||
this.panel.style.removeProperty('left');
|
||
this.panel.style.removeProperty('top');
|
||
this.panel.style.removeProperty('right');
|
||
this.panel.style.removeProperty('bottom');
|
||
},
|
||
readInspectorData:function(){
|
||
var node=document.getElementById('pdt-inspector-data');
|
||
if(!node){return [];}
|
||
try{var value=JSON.parse(node.textContent||'[]');return Array.isArray(value)?value:[];}catch(error){return [];}
|
||
},
|
||
toggleInspector:function(){
|
||
if(this.inspectorActive){this.disableInspector();return;}
|
||
this.enableInspector();
|
||
},
|
||
enableInspector:function(){
|
||
if(this.inspectorActive){return;}
|
||
this.inspectorMarkers=this.findInspectorMarkers();
|
||
this.inspectorLayer=document.createElement('div');
|
||
this.inspectorLayer.id='pdi-layer';
|
||
this.inspectorControl=document.createElement('div');
|
||
this.inspectorControl.id='pdi-control';
|
||
this.inspectorControl.innerHTML='<strong>Инспектор страницы</strong><span></span><button type="button" aria-label="Выключить инспектор" title="Выключить">×</button>';
|
||
var self=this;
|
||
var areaCount=Object.keys(this.inspectorMarkers).reduce(function(total,token){return total+self.inspectorMarkers[token].length;},0);
|
||
this.inspectorControl.querySelector('span').textContent=areaCount+' областей';
|
||
this.inspectorControl.querySelector('button').addEventListener('click',function(){self.disableInspector();});
|
||
this.inspectorBoxes={};
|
||
this.each(this.inspectorEntries,function(entry){
|
||
var token=String(entry.token);
|
||
if(!self.inspectorMarkers[token]){return;}
|
||
var type=/^(document|block|request|navigation|module)$/.test(entry.type)?entry.type:'component';
|
||
self.inspectorBoxes[token]=[];
|
||
self.inspectorMarkers[token].forEach(function(pair,pairIndex){
|
||
var box=document.createElement('div');
|
||
box.className='pdi-box pdi-'+type;
|
||
box.setAttribute('data-pdi-token',token);
|
||
box.setAttribute('data-pdi-pair',String(pairIndex));
|
||
var label=document.createElement('button');
|
||
label.type='button';
|
||
label.className='pdi-label';
|
||
label.textContent=self.inspectorTitle(entry)+(pairIndex>0?' · '+(pairIndex+1):'');
|
||
label.addEventListener('click',function(){self.showInspectorRow(token);});
|
||
box.appendChild(label);
|
||
self.inspectorLayer.appendChild(box);
|
||
self.inspectorBoxes[token].push(box);
|
||
});
|
||
});
|
||
document.body.appendChild(this.inspectorLayer);
|
||
document.body.appendChild(this.inspectorControl);
|
||
this.inspectorActive=true;
|
||
this.syncInspectorButtons();
|
||
this.setOpen(false);
|
||
this.scheduleInspectorLayout();
|
||
},
|
||
disableInspector:function(){
|
||
if(this.inspectorLayer&&this.inspectorLayer.parentNode){this.inspectorLayer.parentNode.removeChild(this.inspectorLayer);}
|
||
if(this.inspectorControl&&this.inspectorControl.parentNode){this.inspectorControl.parentNode.removeChild(this.inspectorControl);}
|
||
this.inspectorLayer=null;
|
||
this.inspectorControl=null;
|
||
this.inspectorBoxes={};
|
||
this.inspectorActive=false;
|
||
this.syncInspectorButtons();
|
||
},
|
||
captureInspectorAnchors:function(){
|
||
var markers=this.findInspectorCommentMarkers();
|
||
Object.keys(markers).forEach(function(token){
|
||
markers[token].forEach(function(pair,pairIndex){
|
||
if(!pair.begin||!pair.end||pair.begin.parentNode!==pair.end.parentNode){return;}
|
||
var anchor=token+'-'+pairIndex;
|
||
var node=pair.begin.nextSibling;
|
||
while(node&&node!==pair.end){
|
||
if(node.nodeType===1){
|
||
var values=(node.getAttribute('data-ave-inspect-anchor')||'').split(/\s+/).filter(Boolean);
|
||
if(values.indexOf(anchor)===-1){values.push(anchor);node.setAttribute('data-ave-inspect-anchor',values.join(' '));}
|
||
}
|
||
node=node.nextSibling;
|
||
}
|
||
});
|
||
});
|
||
},
|
||
findInspectorMarkers:function(){
|
||
var markers=this.findInspectorCommentMarkers();
|
||
var anchorGroups={};
|
||
this.each(document.querySelectorAll('[data-ave-inspect-anchor]'),function(element){
|
||
(element.getAttribute('data-ave-inspect-anchor')||'').split(/\s+/).forEach(function(anchor){
|
||
if(!/^\d+-\d+$/.test(anchor)){return;}
|
||
anchorGroups[anchor]=anchorGroups[anchor]||[];
|
||
anchorGroups[anchor].push(element);
|
||
});
|
||
});
|
||
var anchored={};
|
||
Object.keys(anchorGroups).forEach(function(anchor){
|
||
var token=anchor.split('-')[0];
|
||
anchored[token]=anchored[token]||[];
|
||
anchored[token].push({elements:anchorGroups[anchor]});
|
||
});
|
||
Object.keys(anchored).forEach(function(token){if(anchored[token].length){markers[token]=anchored[token];}});
|
||
return markers;
|
||
},
|
||
findInspectorCommentMarkers:function(){
|
||
var walker=document.createTreeWalker(document.body,NodeFilter.SHOW_COMMENT,null,false);
|
||
var markers={},stacks={};
|
||
while(walker.nextNode()){
|
||
var match=/^ave-inspect:(begin|end):(\d+)$/.exec((walker.currentNode.nodeValue||'').trim());
|
||
if(!match){continue;}
|
||
var token=match[2];
|
||
markers[token]=markers[token]||[];
|
||
stacks[token]=stacks[token]||[];
|
||
if(match[1]==='begin'){stacks[token].push(walker.currentNode);continue;}
|
||
var begin=stacks[token].length?stacks[token].pop():null;
|
||
if(begin){markers[token].push({begin:begin,end:walker.currentNode});}
|
||
}
|
||
Object.keys(markers).forEach(function(token){if(!markers[token].length){delete markers[token];}});
|
||
return markers;
|
||
},
|
||
inspectorPairRect:function(markers){
|
||
if(markers&&markers.elements){
|
||
var elementRects=[];
|
||
markers.elements.forEach(function(element){
|
||
if(!element||!element.isConnected){return;}
|
||
Array.prototype.push.apply(elementRects,Array.prototype.slice.call(element.getClientRects()));
|
||
});
|
||
var visible=elementRects.filter(function(rect){return rect.width>.5&&rect.height>.5;});
|
||
if(!visible.length){return null;}
|
||
var elementLeft=visible[0].left,elementTop=visible[0].top,elementRight=visible[0].right,elementBottom=visible[0].bottom;
|
||
for(var elementIndex=1;elementIndex<visible.length;elementIndex++){
|
||
elementLeft=Math.min(elementLeft,visible[elementIndex].left);elementTop=Math.min(elementTop,visible[elementIndex].top);
|
||
elementRight=Math.max(elementRight,visible[elementIndex].right);elementBottom=Math.max(elementBottom,visible[elementIndex].bottom);
|
||
}
|
||
return {left:elementLeft,top:elementTop,right:elementRight,bottom:elementBottom,width:elementRight-elementLeft,height:elementBottom-elementTop};
|
||
}
|
||
if(!markers||!markers.begin||!markers.end){return null;}
|
||
var range=document.createRange();
|
||
try{range.setStartAfter(markers.begin);range.setEndBefore(markers.end);}catch(error){return null;}
|
||
var rects=Array.prototype.slice.call(range.getClientRects()).filter(function(rect){return rect.width>.5&&rect.height>.5;});
|
||
if(!rects.length){return null;}
|
||
var left=rects[0].left,top=rects[0].top,right=rects[0].right,bottom=rects[0].bottom;
|
||
for(var index=1;index<rects.length;index++){
|
||
left=Math.min(left,rects[index].left);top=Math.min(top,rects[index].top);
|
||
right=Math.max(right,rects[index].right);bottom=Math.max(bottom,rects[index].bottom);
|
||
}
|
||
return {left:left,top:top,right:right,bottom:bottom,width:right-left,height:bottom-top};
|
||
},
|
||
inspectorRect:function(token){
|
||
var pairs=this.inspectorMarkers&&this.inspectorMarkers[String(token)];
|
||
if(!pairs){return null;}
|
||
for(var index=0;index<pairs.length;index++){
|
||
var rect=this.inspectorPairRect(pairs[index]);
|
||
if(rect){return rect;}
|
||
}
|
||
return null;
|
||
},
|
||
scheduleInspectorLayout:function(){
|
||
if(!this.inspectorActive||this.inspectorFrame){return;}
|
||
var self=this;
|
||
this.inspectorFrame=window.requestAnimationFrame(function(){self.inspectorFrame=0;self.layoutInspector();});
|
||
},
|
||
layoutInspector:function(){
|
||
if(!this.inspectorActive){return;}
|
||
var self=this;
|
||
Object.keys(this.inspectorBoxes||{}).forEach(function(token){
|
||
self.inspectorBoxes[token].forEach(function(box,pairIndex){
|
||
var rect=self.inspectorPairRect(self.inspectorMarkers[token][pairIndex]);
|
||
if(!rect){box.hidden=true;return;}
|
||
box.hidden=false;
|
||
box.style.left=Math.max(0,rect.left-2)+'px';
|
||
box.style.top=Math.max(0,rect.top-2)+'px';
|
||
box.style.width=Math.max(4,Math.min(window.innerWidth-Math.max(0,rect.left-2),rect.width+4))+'px';
|
||
box.style.height=Math.max(4,rect.height+4)+'px';
|
||
box.classList.toggle('is-label-inside',rect.top<32);
|
||
});
|
||
});
|
||
},
|
||
focusInspector:function(token){
|
||
if(!this.inspectorActive){this.enableInspector();}
|
||
this.setOpen(false);
|
||
var rect=this.inspectorRect(token);
|
||
if(rect){window.scrollBy({top:rect.top-110,left:0,behavior:'smooth'});}
|
||
var self=this;
|
||
window.setTimeout(function(){
|
||
self.each(document.querySelectorAll('.pdi-box.is-selected'),function(box){box.classList.remove('is-selected');});
|
||
var boxes=self.inspectorBoxes&&self.inspectorBoxes[String(token)];
|
||
if(boxes){self.each(boxes,function(box){box.classList.add('is-selected');});}
|
||
self.scheduleInspectorLayout();
|
||
},250);
|
||
},
|
||
showInspectorRow:function(token){
|
||
this.setOpen(true);
|
||
var tab=this.panel.querySelector('[data-pdt-tab="elements"]');
|
||
this.activateTab(tab,false);
|
||
var row=this.panel.querySelector('[data-pdt-inspector-row="'+String(token)+'"]');
|
||
if(row){row.scrollIntoView({block:'center'});}
|
||
},
|
||
inspectorTitle:function(entry){
|
||
var labels={document:'Документ',block:'Блок',request:'Запрос',navigation:'Навигация',module:'Модуль'};
|
||
var title=labels[entry.type]||'Компонент';
|
||
if(entry.entity_id){title+=' #'+entry.entity_id;}
|
||
if(entry.title){title+=' · '+entry.title;}
|
||
return title;
|
||
},
|
||
syncInspectorButtons:function(){
|
||
var active=this.inspectorActive;
|
||
this.each(this.panel.querySelectorAll('[data-pdt-inspector-toggle]'),function(button){
|
||
button.classList.toggle('is-active',active);
|
||
button.textContent=active?'Скрыть на странице':'Показать на странице';
|
||
});
|
||
},
|
||
copyInspectorTag:function(button,token){
|
||
var entry=this.inspectorByToken[String(token)];
|
||
if(!entry||!entry.tag){return;}
|
||
var original=button.textContent;
|
||
var done=function(){button.textContent='Скопировано';window.setTimeout(function(){button.textContent=original;},1200);};
|
||
if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(entry.tag).then(done).catch(function(){});return;}
|
||
var area=document.createElement('textarea');area.value=entry.tag;area.style.position='fixed';area.style.opacity='0';document.body.appendChild(area);area.select();
|
||
try{document.execCommand('copy');done();}catch(error){}document.body.removeChild(area);
|
||
},
|
||
store:function(value){
|
||
try{window.sessionStorage.setItem(this.storageKey,value);}catch(error){}
|
||
},
|
||
restore:function(){
|
||
try{return window.sessionStorage.getItem(this.storageKey);}catch(error){return null;}
|
||
},
|
||
closest:function(element,selector){
|
||
if(!element){return null;}
|
||
if(element.closest){return element.closest(selector);}
|
||
while(element){
|
||
if(element.matches&&element.matches(selector)){return element;}
|
||
element=element.parentElement;
|
||
}
|
||
return null;
|
||
},
|
||
each:function(items,callback){
|
||
Array.prototype.forEach.call(items,callback);
|
||
}
|
||
};
|
||
window.AvePublicDebug=AvePublicDebug;
|
||
if(document.body){
|
||
AvePublicDebug.init();
|
||
}else if(document.readyState==='loading'){
|
||
document.addEventListener('DOMContentLoaded',function(){AvePublicDebug.init();});
|
||
}else{
|
||
AvePublicDebug.init();
|
||
}
|
||
})(window,document);
|
||
</script>
|
||
HTML;
|
||
}
|
||
}
|