HEX
Server: Apache/2.4.66 (Debian)
System: Linux 6dfabc3b2241 6.8.0-71-generic #71-Ubuntu SMP PREEMPT_DYNAMIC Tue Jul 22 16:52:38 UTC 2025 x86_64
User: (1000)
PHP: 8.3.30
Disabled: NONE
Upload Files
File: /var/www/html/wp-content/themes/custom-theme/inc/logger-dashboard.php
<?php

// Log viewer admin dashboard (Telescope-style).
// All functions here are admin-only UI — the engine lives in logger.php.

// --- Menu registration ---

function headless_log_admin_page() {
    add_menu_page(
        'Logs',
        'Logs',
        'manage_options',
        'logs-dashboard',
        'headless_log_dashboard_render',
        'dashicons-clipboard',
        81
    );
}
add_action('admin_menu', 'headless_log_admin_page');

// --- Action handler ---

function headless_log_dashboard_handle_actions() {
    if (!isset($_POST['log_action'], $_POST['_wpnonce'])) {
        return;
    }

    if ($_POST['log_action'] !== 'clear_logs' || !wp_verify_nonce($_POST['_wpnonce'], 'log_clear_action')) {
        return;
    }

    headless_log_clear();
    echo '<div class="notice notice-success is-dismissible"><p>All log entries cleared.</p></div>';
}

// --- Styles ---

function headless_log_dashboard_styles() {
    ?>
    <style>
        /* Layout */
        .lg-dash { max-width: 1100px; }

        /* Header */
        .lg-header {
            display: flex;
            align-items: center;
            gap: 12px;
            margin: 0 0 20px;
            flex-wrap: wrap;
        }
        .lg-header h1 { margin: 0; font-size: 23px; font-weight: 400; }
        .lg-header-actions { margin-left: auto; }

        /* Badge */
        .lg-badge {
            display: inline-block;
            background: #2271b1;
            color: #fff;
            font-size: 11px;
            font-weight: 600;
            padding: 2px 9px;
            border-radius: 100px;
            line-height: 16px;
        }

        /* Buttons */
        .lg-btn {
            display: inline-flex;
            align-items: center;
            gap: 5px;
            padding: 6px 14px;
            border: 1px solid #c3c4c7;
            border-radius: 4px;
            background: #f6f7f7;
            color: #2c3338;
            font-size: 13px;
            font-weight: 500;
            line-height: 1.4;
            cursor: pointer;
            text-decoration: none;
            transition: all .1s ease;
        }
        .lg-btn:hover { background: #f0f0f1; border-color: #8c8f94; color: #1d2327; }
        .lg-btn:focus { outline: 2px solid #2271b1; outline-offset: 1px; }
        .lg-btn .dashicons { font-size: 15px; width: 15px; height: 15px; line-height: 15px; }
        .lg-btn-danger { color: #d63638; border-color: #d63638; }
        .lg-btn-danger:hover { background: #d63638; color: #fff; }
        .lg-btn-primary { background: #2271b1; border-color: #2271b1; color: #fff; }
        .lg-btn-primary:hover { background: #135e96; border-color: #135e96; color: #fff; }

        /* Filter bar */
        .lg-filters {
            display: flex;
            gap: 8px;
            margin-bottom: 20px;
            align-items: center;
            flex-wrap: wrap;
            background: #fff;
            border: 1px solid #dcdcde;
            border-radius: 4px;
            padding: 12px 16px;
        }
        .lg-filters select {
            padding: 5px 8px;
            border: 1px solid #c3c4c7;
            border-radius: 4px;
            font-size: 13px;
            line-height: 1.4;
            min-width: 130px;
        }
        .lg-filters select:focus {
            border-color: #2271b1;
            box-shadow: 0 0 0 1px #2271b1;
            outline: none;
        }
        .lg-search-wrap {
            position: relative;
            display: inline-flex;
            align-items: center;
        }
        .lg-search-wrap .dashicons {
            position: absolute;
            left: 8px;
            color: #8c8f94;
            font-size: 16px;
            width: 16px;
            height: 16px;
            pointer-events: none;
        }
        .lg-search-wrap input[type="text"] {
            padding: 5px 10px 5px 30px;
            width: 240px;
            border: 1px solid #c3c4c7;
            border-radius: 4px;
            font-size: 13px;
            line-height: 1.4;
        }
        .lg-search-wrap input[type="text"]:focus {
            border-color: #2271b1;
            box-shadow: 0 0 0 1px #2271b1;
            outline: none;
        }

        /* Table */
        .lg-table-wrap {
            background: #fff;
            border: 1px solid #dcdcde;
            border-radius: 4px;
            overflow: hidden;
        }
        .lg-table {
            width: 100%;
            border-collapse: collapse;
            font-size: 13px;
        }
        .lg-table thead th {
            text-align: left;
            padding: 8px 12px;
            font-weight: 600;
            font-size: 12px;
            text-transform: uppercase;
            letter-spacing: .04em;
            color: #646970;
            border-bottom: 2px solid #dcdcde;
            background: #f9f9f9;
        }
        .lg-table tbody tr { border-bottom: 1px solid #f0f0f1; }
        .lg-table tbody tr:last-child { border-bottom: none; }
        .lg-table tbody tr:hover { background: #f6f7f7; }
        .lg-table tbody td { padding: 9px 12px; color: #2c3338; vertical-align: top; }

        .lg-col-time { width: 140px; white-space: nowrap; font-size: 12px; color: #646970; }
        .lg-col-level { width: 80px; }
        .lg-col-component { width: 110px; }
        .lg-col-context { width: 180px; }

        /* Level pills */
        .lg-level {
            display: inline-block;
            padding: 1px 9px;
            border-radius: 100px;
            font-size: 11px;
            font-weight: 600;
            line-height: 18px;
            letter-spacing: .02em;
            text-transform: uppercase;
        }
        .lg-level-error   { background: #fcecec; color: #d63638; }
        .lg-level-warning { background: #fef8ee; color: #dba617; }
        .lg-level-info    { background: #e7f1fa; color: #2271b1; }
        .lg-level-debug   { background: #f0f0f1; color: #646970; }

        /* Component */
        .lg-component {
            font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
            font-size: 12px;
            color: #50575e;
        }

        /* Context */
        .lg-context {
            font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
            font-size: 11px;
            color: #646970;
            max-width: 180px;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            cursor: default;
        }
        .lg-context.lg-expanded {
            white-space: pre-wrap;
            word-break: break-all;
            max-width: none;
        }

        /* Pagination */
        .lg-pagination {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 12px 16px;
            background: #f9f9f9;
            border-top: 1px solid #dcdcde;
            font-size: 13px;
            color: #646970;
        }
        .lg-pagination-links {
            display: flex;
            gap: 6px;
        }
        .lg-pagination a {
            display: inline-flex;
            align-items: center;
            gap: 4px;
            padding: 4px 12px;
            border: 1px solid #c3c4c7;
            border-radius: 4px;
            background: #f6f7f7;
            color: #2c3338;
            font-size: 12px;
            text-decoration: none;
            transition: all .1s ease;
        }
        .lg-pagination a:hover { background: #f0f0f1; border-color: #8c8f94; }

        /* Empty state */
        .lg-empty {
            text-align: center;
            padding: 48px 20px;
            color: #646970;
        }
        .lg-empty .dashicons {
            font-size: 40px;
            width: 40px;
            height: 40px;
            color: #c3c4c7;
            display: block;
            margin: 0 auto 12px;
        }
        .lg-empty p { margin: 0; font-size: 13px; }
    </style>
    <?php
}

// --- Rendering ---

function headless_log_dashboard_render() {
    headless_log_dashboard_handle_actions();

    // Auto-prune: once per hour, delete entries older than 30 days.
    $prune_key = 'headless_log_last_prune';
    if (!get_transient($prune_key)) {
        headless_log_prune(30);
        set_transient($prune_key, 1, HOUR_IN_SECONDS);
    }

    // Read GET filters.
    $log_level     = sanitize_text_field($_GET['log_level'] ?? '');
    $log_component = sanitize_text_field($_GET['log_component'] ?? '');
    $log_search    = sanitize_text_field($_GET['log_search'] ?? '');
    $log_page      = max(1, (int) ($_GET['log_page'] ?? 1));
    $per_page      = 50;

    $result     = headless_log_query([
        'level'     => $log_level,
        'component' => $log_component,
        'search'    => $log_search,
        'page'      => $log_page,
        'per_page'  => $per_page,
    ]);
    $rows       = $result['rows'];
    $total      = $result['total'];
    $total_pages = max(1, (int) ceil($total / $per_page));
    $components = headless_log_components();

    $page_url = admin_url('admin.php?page=logs-dashboard');

    headless_log_dashboard_styles();
    ?>
    <div class="wrap lg-dash">
        <!-- Header -->
        <div class="lg-header">
            <h1>Logs</h1>
            <span class="lg-badge"><?php echo esc_html(number_format_i18n($total)); ?> entries</span>
            <div class="lg-header-actions">
                <form method="post" style="display:inline" onsubmit="return confirm('Clear all log entries? This cannot be undone.')">
                    <?php wp_nonce_field('log_clear_action'); ?>
                    <input type="hidden" name="log_action" value="clear_logs">
                    <button type="submit" class="lg-btn lg-btn-danger">
                        <span class="dashicons dashicons-trash"></span> Clear All
                    </button>
                </form>
            </div>
        </div>

        <!-- Filters -->
        <form method="get" class="lg-filters">
            <input type="hidden" name="page" value="logs-dashboard">

            <select name="log_level">
                <option value="">All Levels</option>
                <?php foreach (['ERROR', 'WARNING', 'INFO', 'DEBUG'] as $lvl): ?>
                    <option value="<?php echo esc_attr($lvl); ?>" <?php selected($log_level, $lvl); ?>><?php echo esc_html($lvl); ?></option>
                <?php endforeach; ?>
            </select>

            <select name="log_component">
                <option value="">All Components</option>
                <?php foreach ($components as $comp): ?>
                    <option value="<?php echo esc_attr($comp); ?>" <?php selected($log_component, $comp); ?>><?php echo esc_html($comp); ?></option>
                <?php endforeach; ?>
            </select>

            <div class="lg-search-wrap">
                <span class="dashicons dashicons-search"></span>
                <input type="text" name="log_search" value="<?php echo esc_attr($log_search); ?>" placeholder="Search messages...">
            </div>

            <button type="submit" class="lg-btn lg-btn-primary">Filter</button>

            <?php if ($log_level !== '' || $log_component !== '' || $log_search !== ''): ?>
                <a href="<?php echo esc_url($page_url); ?>" class="lg-btn">Clear Filters</a>
            <?php endif; ?>
        </form>

        <!-- Table -->
        <div class="lg-table-wrap">
            <?php if (!empty($rows)): ?>
                <table class="lg-table">
                    <thead>
                        <tr>
                            <th class="lg-col-time">Timestamp</th>
                            <th class="lg-col-level">Level</th>
                            <th class="lg-col-component">Component</th>
                            <th>Message</th>
                            <th class="lg-col-context">Context</th>
                        </tr>
                    </thead>
                    <tbody>
                        <?php foreach ($rows as $row):
                            $level_class = 'lg-level-' . strtolower($row->level);
                        ?>
                            <tr>
                                <td class="lg-col-time"><?php echo esc_html($row->created_at); ?></td>
                                <td class="lg-col-level">
                                    <span class="lg-level <?php echo esc_attr($level_class); ?>"><?php echo esc_html($row->level); ?></span>
                                </td>
                                <td class="lg-col-component">
                                    <span class="lg-component"><?php echo esc_html($row->component); ?></span>
                                </td>
                                <td><?php echo esc_html($row->message); ?></td>
                                <td class="lg-col-context">
                                    <?php if ($row->context): ?>
                                        <span class="lg-context" onclick="this.classList.toggle('lg-expanded')" title="Click to expand"><?php echo esc_html($row->context); ?></span>
                                    <?php else: ?>
                                        <span style="color:#c3c4c7">&mdash;</span>
                                    <?php endif; ?>
                                </td>
                            </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>

                <!-- Pagination -->
                <?php
                $range_start = (($log_page - 1) * $per_page) + 1;
                $range_end   = min($log_page * $per_page, $total);

                $filter_params = array_filter([
                    'page'          => 'logs-dashboard',
                    'log_level'     => $log_level,
                    'log_component' => $log_component,
                    'log_search'    => $log_search,
                ]);
                ?>
                <div class="lg-pagination">
                    <span>Showing <?php echo esc_html($range_start); ?>&ndash;<?php echo esc_html($range_end); ?> of <?php echo esc_html(number_format_i18n($total)); ?></span>
                    <div class="lg-pagination-links">
                        <?php if ($log_page > 1): ?>
                            <a href="<?php echo esc_url(add_query_arg(array_merge($filter_params, ['log_page' => $log_page - 1]), admin_url('admin.php'))); ?>">
                                &larr; Previous
                            </a>
                        <?php endif; ?>
                        <?php if ($log_page < $total_pages): ?>
                            <a href="<?php echo esc_url(add_query_arg(array_merge($filter_params, ['log_page' => $log_page + 1]), admin_url('admin.php'))); ?>">
                                Next &rarr;
                            </a>
                        <?php endif; ?>
                    </div>
                </div>

            <?php else: ?>
                <div class="lg-empty">
                    <span class="dashicons dashicons-clipboard"></span>
                    <p><?php echo ($log_level !== '' || $log_component !== '' || $log_search !== '')
                        ? 'No log entries match your filters.'
                        : 'No log entries yet. Logs will appear here as they are generated.'; ?></p>
                </div>
            <?php endif; ?>
        </div>
    </div>
    <?php
}