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">—</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); ?>–<?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'))); ?>">
← 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 →
</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
}