feat: admin settings page UI/UX pass with WordPress-native patterns

- Header: h1 + logo in a wrapping flex row, hr.wp-header-end before the tabs,
  aria-current on the active tab; own CSS/JS enqueued only on this screen.
- Verbindung: ordered "So verbindest du Claude" steps, copy button (core
  clipboard.js + wp.a11y.speak), Site-Health-style status list, German details.
- Zugriff: roles as form-table fieldset, connections with site date format,
  empty-state row, confirm on revoke; ConnectionUsage records last use via
  WP_Application_Passwords::record_application_password_usage (once a day).
- Tools: plain-language legend, counter as inline notice, groups as core
  postboxes (postbox.js toggles, per-user closed state, summary when closed),
  fixed column widths with new "Art" column, submit above and below.
- Protokoll: site-timezone times with UTC tooltip, result icons, filters with
  labels in the list table nav, retention link to Limits.
- Limits: short labels with units and descriptions; snapshot count hidden but
  preserved.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 16:50:58 +02:00
co-authored by Claude Opus 5.5
parent 5965e32acb
commit 843f45aeec
22 changed files with 1535 additions and 242 deletions
+75 -5
View File
@@ -18,13 +18,18 @@ final class LogListTable extends \WP_List_Table
{
private const PER_PAGE = 50;
/** @var (callable(): void)|null */
private $renderFilters;
/**
* @param array{
* ability: string, user_id: int, result?: string, date_from?: string, date_to?: string
* } $filters
* @param (callable(): void)|null $renderFilters prints the filter controls into the top table nav
*/
public function __construct(private ChangeLog $log, private array $filters)
public function __construct(private ChangeLog $log, private array $filters, ?callable $renderFilters = null)
{
$this->renderFilters = $renderFilters;
parent::__construct(['singular' => 'lit_mcp_log_entry', 'plural' => 'lit_mcp_log_entries', 'ajax' => false]);
}
@@ -32,7 +37,7 @@ final class LogListTable extends \WP_List_Table
public function get_columns(): array
{
return [
'created_at' => __('Zeit (UTC)', 'wp-lit-mcp'),
'created_at' => __('Zeit', 'wp-lit-mcp'),
'user' => __('Benutzer', 'wp-lit-mcp'),
'client' => __('Client', 'wp-lit-mcp'),
'ability' => __('Tool', 'wp-lit-mcp'),
@@ -57,6 +62,22 @@ final class LogListTable extends \WP_List_Table
esc_html_e('Keine Einträge.', 'wp-lit-mcp');
}
/**
* Filters left of the pagination in the top table nav, as on WordPress' own list screens.
*
* @param string $which
*/
protected function extra_tablenav($which): void
{
if ($which !== 'top' || $this->renderFilters === null) {
return;
}
echo '<div class="alignleft actions">';
($this->renderFilters)();
echo '</div>';
}
/**
* Highlights rows with result "partial" (a write that failed after an irreversible
* step, spec §2 principle 7/8.2) so admins notice them without reading every row.
@@ -65,12 +86,61 @@ final class LogListTable extends \WP_List_Table
*/
public function single_row($item): void
{
$style = ($item['result'] ?? '') === 'partial' ? ' style="background:#fcf3e3;"' : '';
printf('<tr%s>', $style); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fixed string.
$class = ($item['result'] ?? '') === 'partial' ? ' class="lit-mcp-partial"' : '';
printf('<tr%s>', $class); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fixed string.
$this->single_row_columns($item);
echo '</tr>';
}
/**
* Stored in UTC (MySQL format); shown in the site's timezone and date/time format,
* with the UTC value as tooltip.
*
* @param array<string, mixed> $item
*/
protected function column_created_at($item): string
{
$utc = (string) ($item['created_at'] ?? '');
$timestamp = preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $utc) === 1
? strtotime($utc . ' UTC')
: false;
if ($timestamp === false) {
return '–';
}
$format = get_option('date_format') . ' ' . get_option('time_format');
return sprintf(
'<span title="%s">%s</span>',
esc_attr('UTC: ' . $utc),
esc_html((string) wp_date($format, $timestamp))
);
}
/** @param array<string, mixed> $item */
protected function column_ability($item): string
{
return '<span class="lit-mcp-tool-name">' . esc_html((string) ($item['ability'] ?? '')) . '</span>';
}
/** @param array<string, mixed> $item */
protected function column_result($item): string
{
$result = (string) ($item['result'] ?? '');
[$icon, $word] = match ($result) {
'ok' => ['yes-alt', __('erfolgreich', 'wp-lit-mcp')],
'partial' => ['warning', __('teilweise', 'wp-lit-mcp')],
default => ['dismiss', $result],
};
return sprintf(
'<span class="lit-mcp-result lit-mcp-result-%1$s">'
. '<span class="dashicons dashicons-%1$s" aria-hidden="true"></span> %2$s</span>',
esc_attr($icon),
esc_html($word)
);
}
/**
* @param array<string, mixed> $item
* @param string $column_name
@@ -144,7 +214,7 @@ final class LogListTable extends \WP_List_Table
return sprintf(
'<a href="%s">%s</a>',
esc_url(admin_url('revision.php?revision=' . $revisionId)),
esc_html__('ansehen / wiederherstellen', 'wp-lit-mcp')
esc_html__('Revision ansehen', 'wp-lit-mcp')
);
}
}