feat(admin): Einstellungsseite überarbeitet, auch für das Handy

Verbindung: Probleme vor der Anleitung. Tools: Suche, Filter, Alle ein-/ausklappen, Zurücksetzen je Gruppe.
Divi-Reiter in Klartext mit Statussymbolen. Rollenhinweise, Hilfe-Tab, Reiter als Raster und Karten auf dem Handy,
Kontrast, Tippflächen, einheitliche Du-Anrede. Bestätigungen über data-lit-mcp-confirm statt Inline-onclick.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-11-05 16:36:16 +01:00
co-authored by Claude Sonnet 5.5
parent 165e61d7fe
commit 96fa58188b
16 changed files with 865 additions and 84 deletions
+51 -16
View File
@@ -84,7 +84,7 @@ final class AccessTab implements AdminTab
private static function ownRoleWarning(): string
{
return __(
'Keine Ihrer Rollen ist mehr freigegeben – Sie selbst können LIT MCP nicht mehr nutzen.',
'Keine deiner Rollen ist mehr freigegeben. Du kannst LIT MCP dann selbst nicht mehr nutzen.',
'wp-lit-mcp'
);
}
@@ -105,11 +105,14 @@ final class AccessTab implements AdminTab
);
foreach (wp_roles()->get_names() as $role => $name) {
printf(
'<label><input type="checkbox" name="%s[]" value="%s"%s> %s</label><br>',
'<label><input type="checkbox" name="%1$s[]" value="%2$s"%3$s'
. ' aria-describedby="lit-mcp-role-%2$s"> %4$s</label>'
. ' <span class="description" id="lit-mcp-role-%2$s">(%5$s)</span><br>',
esc_attr(Access::OPTION),
esc_attr((string) $role),
checked(in_array($role, $allowed, true), true, false),
esc_html(translate_user_role((string) $name))
esc_html(translate_user_role((string) $name)),
esc_html(self::roleHint((string) $role))
);
}
echo '</fieldset><p class="description">'
@@ -122,6 +125,19 @@ final class AccessTab implements AdminTab
$this->renderConnections();
}
/** What a role may do in WordPress, in one phrase, so the admin sees whom they are letting in. */
private static function roleHint(string $role): string
{
$caps = (array) (((array) wp_roles()->roles)[$role]['capabilities'] ?? []);
return match (true) {
! empty($caps['manage_options']) => __('verwaltet die Website', 'wp-lit-mcp'),
! empty($caps['edit_others_posts']) => __('bearbeitet auch fremde Inhalte', 'wp-lit-mcp'),
! empty($caps['edit_posts']) || ! empty($caps['edit_pages']) => __('bearbeitet Inhalte', 'wp-lit-mcp'),
default => __('keine Bearbeitungsrechte erkannt', 'wp-lit-mcp'),
};
}
public function handleRevoke(): void
{
$this->action->guard();
@@ -240,17 +256,22 @@ final class AccessTab implements AdminTab
echo '<tr>';
echo '<td class="column-user">' . esc_html($connection['user_login']) . '</td>';
$created = (string) wp_date($dateFormat . ' ' . $timeFormat, $connection['created']);
// The "Verbunden am" column is hidden on phones; the date then shows under the client instead.
printf(
'<td class="column-client"><strong>%s</strong><br><span class="lit-mcp-muted">%s</span></td>',
'<td class="column-client"><strong>%s</strong><br><span class="lit-mcp-muted">%s</span>'
. '<br class="lit-mcp-phone-only"><span class="lit-mcp-muted lit-mcp-phone-only">%s</span></td>',
esc_html($connection['name']),
esc_html($connection['app_id'])
);
printf(
'<td class="column-created">%s</td>',
esc_html((string) wp_date($dateFormat . ' ' . $timeFormat, $connection['created']))
esc_html($connection['app_id']),
esc_html(sprintf(
/* translators: %s: date and time */
__('Verbunden am %s', 'wp-lit-mcp'),
$created
))
);
printf('<td class="column-created">%s</td>', esc_html($created));
$lastUsed = $connection['last_used'] === null
? '–'
? __('Noch nie', 'wp-lit-mcp')
: (string) wp_date($dateFormat, $connection['last_used']);
echo '<td class="column-last-used">' . esc_html($lastUsed) . '</td>';
echo '<td class="column-actions">';
@@ -259,17 +280,31 @@ final class AccessTab implements AdminTab
printf('<input type="hidden" name="user_id" value="%s">', esc_attr((string) $connection['user_id']));
printf('<input type="hidden" name="uuid" value="%s">', esc_attr($connection['uuid']));
wp_nonce_field(self::REVOKE_ACTION . '_' . $connection['uuid']);
$confirm = esc_js(sprintf(
/* translators: %s: client name */
__('Verbindung „%s“ widerrufen? Der Assistent muss sich danach neu anmelden.', 'wp-lit-mcp'),
$connection['name']
));
submit_button(
__('Widerrufen', 'wp-lit-mcp'),
'delete small',
'submit',
false,
['onclick' => "return window.confirm('" . $confirm . "');"]
[
// One id per row: submit_button() would give every button the id "submit".
'id' => 'lit-mcp-revoke-' . $connection['uuid'],
// Starts with the visible word (WCAG 2.5.3) and says whose connection it is.
'aria-label' => sprintf(
/* translators: 1: client name, 2: user login */
__('Widerrufen: %1$s, %2$s', 'wp-lit-mcp'),
$connection['name'],
$connection['user_login']
),
'data-lit-mcp-confirm' => sprintf(
/* translators: 1: client name, 2: user login */
__(
'Verbindung „%1$s“ von %2$s widerrufen? Der Assistent muss sich danach neu anmelden.',
'wp-lit-mcp'
),
$connection['name'],
$connection['user_login']
),
]
);
echo '</form></td></tr>';
}
+40 -6
View File
@@ -34,15 +34,19 @@ final class ConnectionTab implements AdminTab
public function render(): void
{
$checks = $this->status->checks(true);
$this->renderProblems($checks);
echo '<h2>' . esc_html__('So verbindest du einen KI-Assistenten', 'wp-lit-mcp') . '</h2>';
echo '<ol class="lit-mcp-steps"><li>';
printf(
'<label for="lit-mcp-url">%s</label>'
'<span class="lit-mcp-url-line"><label for="lit-mcp-url">%s</label>'
. '<span class="lit-mcp-copy-row copy-to-clipboard-container">'
. '<input id="lit-mcp-url" type="text" class="large-text code" readonly value="%s">'
. '<button type="button" class="button lit-mcp-copy" data-clipboard-target="#lit-mcp-url">%s</button>'
. '<span class="success hidden" aria-hidden="true">%s</span></span>',
esc_html__('Server-URL kopieren:', 'wp-lit-mcp'),
. '<button type="button" class="button lit-mcp-copy hide-if-no-js"'
. ' data-clipboard-target="#lit-mcp-url">%s</button>'
. '<span class="success hidden" aria-hidden="true">%s</span></span></span>',
esc_html__('Server-URL', 'wp-lit-mcp'),
esc_attr(ConnectionStatus::serverUrl()),
esc_html__('URL kopieren', 'wp-lit-mcp'),
esc_html__('Kopiert!', 'wp-lit-mcp')
@@ -50,7 +54,17 @@ final class ConnectionTab implements AdminTab
echo '</li><li>' . esc_html__(
'Im KI-Assistenten einen eigenen MCP-Server (Connector) hinzufügen und die URL einfügen.',
'wp-lit-mcp'
) . '</li><li>';
);
printf(
'<details class="lit-mcp-hint"><summary>%s</summary><p>%s</p></details>',
esc_html__('Wo finde ich das?', 'wp-lit-mcp'),
esc_html__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Je nach Assistent heißt der Punkt „Konnektoren“, „Connectors“ oder „MCP-Server“ und steht meist in den Einstellungen. Als Name passt der Name dieser Website; ein Passwort oder einen Schlüssel brauchst du nicht, die Anmeldung läuft über WordPress.',
'wp-lit-mcp'
)
);
echo '</li><li>';
printf(
/* translators: %s: link to the "Zugriff" tab */
esc_html__('Anmelden und den Zugriff bestätigen. Danach erscheint die Verbindung unter %s.', 'wp-lit-mcp'),
@@ -59,7 +73,7 @@ final class ConnectionTab implements AdminTab
echo '</li></ol>';
echo '<h2>' . esc_html__('Status', 'wp-lit-mcp') . '</h2><ul class="lit-mcp-status">';
foreach ($this->status->checks(true) as $check) {
foreach ($checks as $check) {
[$icon, $state] = match ($check['ok']) {
true => ['yes-alt', __('In Ordnung:', 'wp-lit-mcp')],
false => ['dismiss', __('Problem:', 'wp-lit-mcp')],
@@ -83,4 +97,24 @@ final class ConnectionTab implements AdminTab
esc_html__('Werkzeuge → Website-Zustand', 'wp-lit-mcp')
);
}
/**
* A failed check makes the steps above useless, so it shows before them, not only in the list below.
*
* @param list<array{label: string, ok: ?bool, message: string}> $checks
*/
private function renderProblems(array $checks): void
{
$failed = array_filter($checks, static fn (array $check): bool => $check['ok'] === false);
if ($failed === []) {
return;
}
echo '<div class="notice notice-error inline"><p><strong>'
. esc_html__('Ein Assistent kann sich noch nicht verbinden.', 'wp-lit-mcp') . '</strong></p><ul>';
foreach ($failed as $check) {
printf('<li>%s: %s</li>', esc_html($check['label']), esc_html($check['message']));
}
echo '</ul></div>';
}
}
+10 -1
View File
@@ -58,7 +58,16 @@ final class LimitsTab implements AdminTab
'log_retention_days' => [
__('Protokoll aufbewahren', 'wp-lit-mcp'),
__('Tage', 'wp-lit-mcp'),
__('Ältere Einträge im Protokoll werden automatisch gelöscht.', 'wp-lit-mcp'),
sprintf(
/* translators: 1: minimum days, 2: maximum days */
__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text.
'Zwischen %1$d und %2$d Tagen. Ältere Einträge im Protokoll werden automatisch gelöscht.',
'wp-lit-mcp'
),
$bounds['log_retention_days'][0],
$bounds['log_retention_days'][1]
),
],
];
+3 -5
View File
@@ -256,6 +256,7 @@ final class LogTab implements AdminTab
}
echo '</select>';
printf('<span class="lit-mcp-dates" role="group" aria-label="%s">', esc_attr__('Zeitraum', 'wp-lit-mcp'));
foreach (['date_from' => __('von', 'wp-lit-mcp'), 'date_to' => __('bis', 'wp-lit-mcp')] as $name => $label) {
$id = 'lit-mcp-filter-' . str_replace('_', '-', $name);
printf(
@@ -267,6 +268,7 @@ final class LogTab implements AdminTab
esc_attr($extra[$name])
);
}
echo '</span>';
submit_button(__('Filtern', 'wp-lit-mcp'), '', 'filter_action', false, ['id' => 'lit-mcp-filter-submit']);
@@ -319,10 +321,6 @@ final class LogTab implements AdminTab
printf('<div class="lit-mcp-clear-log"><form method="post" action="%s">', esc_url(admin_url('admin-post.php')));
printf('<input type="hidden" name="action" value="%s">', esc_attr(self::CLEAR_ACTION));
wp_nonce_field(self::CLEAR_ACTION);
$confirmMessage = esc_js(__(
'Das gesamte Protokoll unwiderruflich löschen?',
'wp-lit-mcp'
));
submit_button(
sprintf(
/* translators: %d: number of log entries */
@@ -332,7 +330,7 @@ final class LogTab implements AdminTab
'delete',
'submit',
false,
['onclick' => "return window.confirm('" . $confirmMessage . "');"]
['data-lit-mcp-confirm' => __('Das gesamte Protokoll unwiderruflich löschen?', 'wp-lit-mcp')]
);
echo '</form></div>';
}
+66 -2
View File
@@ -44,6 +44,7 @@ final class SettingsPage
add_action('admin_menu', [$this, 'addMenu']);
add_action('admin_init', [$this, 'registerSettings']);
add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);
add_filter('removable_query_args', [$this, 'removableQueryArgs']);
foreach ($this->tabs() as $tab) {
$tab->registerActions();
}
@@ -51,13 +52,72 @@ final class SettingsPage
public function addMenu(): void
{
add_options_page(
$hook = add_options_page(
__('LIT MCP', 'wp-lit-mcp'),
__('LIT MCP', 'wp-lit-mcp'),
'manage_options',
self::SLUG,
[$this, 'render']
);
if (is_string($hook)) {
add_action('load-' . $hook, [$this, 'addHelpTab']);
}
}
/**
* WordPress removes these from the address bar after the page loaded, so a reload or a copied link
* does not show a finished action's notice again.
*
* @param array<int, string> $args
* @return array<int, string>
*/
public function removableQueryArgs(array $args): array
{
$args[] = 'lit_mcp_notice';
return $args;
}
/** Short "Hilfe" panel at the top right; the tabs explain themselves, this only says where to start. */
public function addHelpTab(): void
{
$screen = get_current_screen();
if ($screen !== null) {
$this->helpTabFor($screen);
}
}
/** @param object $screen A WP_Screen. */
public function helpTabFor(object $screen): void
{
$items = [
__('Verbinden', 'wp-lit-mcp') => __(
'Tab „Verbindung“: Server-URL kopieren und im KI-Assistenten als MCP-Server hinzufügen.',
'wp-lit-mcp'
),
__('Wer darf?', 'wp-lit-mcp') => __(
'Tab „Zugriff“: Rollen festlegen und bestehende Verbindungen widerrufen.',
'wp-lit-mcp'
),
__('Was darf der Assistent?', 'wp-lit-mcp') => __(
'Tab „Tools“: jedes Tool direkt, indirekt oder aus; ganze Gruppen auf „nur lesen“ stellen.',
'wp-lit-mcp'
),
__('Was ist passiert?', 'wp-lit-mcp') => __(
'Tab „Protokoll“: jede Änderung des Assistenten mit Benutzer, Ergebnis und Zeitpunkt.',
'wp-lit-mcp'
),
];
$content = '';
foreach ($items as $title => $text) {
$content .= sprintf('<p><strong>%s</strong> %s</p>', esc_html($title), esc_html($text));
}
$screen->add_help_tab([
'id' => 'lit-mcp-overview',
'title' => __('Überblick', 'wp-lit-mcp'),
'content' => $content,
]);
}
public function registerSettings(): void
@@ -87,6 +147,10 @@ final class SettingsPage
wp_enqueue_script('lit-mcp-admin', $base . '/js/admin.js', $deps, $this->version, true);
wp_localize_script('lit-mcp-admin', 'litMcpAdmin', [
'copied' => __('Die URL wurde in die Zwischenablage kopiert.', 'wp-lit-mcp'),
/* translators: %d: number of tools matching the filter */
'toolsShown' => __('%d Tools angezeigt', 'wp-lit-mcp'),
'toolShown' => __('1 Tool angezeigt', 'wp-lit-mcp'),
'groupReset' => __('Gruppe auf Standard zurückgesetzt. Noch nicht gespeichert.', 'wp-lit-mcp'),
]);
}
@@ -200,7 +264,7 @@ final class SettingsPage
],
'divi_withdrawn' => [
'success',
__('Freigabe zurückgezogen; schreibende Divi-Werkzeuge sind wieder gesperrt.', 'wp-lit-mcp'),
__('Freigabe zurückgezogen; schreibende Divi-Tools sind wieder gesperrt.', 'wp-lit-mcp'),
],
'divi_withdraw_failed' => [
'error',
+72 -12
View File
@@ -87,6 +87,7 @@ final class ToolsTab implements AdminTab
echo '<form method="post" action="options.php">';
settings_fields(self::GROUP);
submit_button(null, 'primary', 'submit-top');
$this->renderToolbar();
// The admin script mirrors a group's mode onto its rows before saving; it needs the label pattern.
printf(
'<div class="lit-mcp-groups" data-like-group="%s">',
@@ -105,10 +106,11 @@ final class ToolsTab implements AdminTab
private function renderIntro(): void
{
echo '<p class="description">'
. esc_html__('Lege fest, welche Tools der KI-Assistent nutzen kann.', 'wp-lit-mcp') . ' '
. esc_html__('Änderungen gelten ab der nächsten Verbindung.', 'wp-lit-mcp') . ' '
. esc_html__('Im KI-Assistenten die Verbindung dafür gegebenenfalls neu herstellen.', 'wp-lit-mcp')
. '</p>';
. esc_html__('Lege fest, welche Tools der KI-Assistent nutzen kann.', 'wp-lit-mcp') . '</p>';
echo '<div class="notice notice-info inline"><p><strong>'
. esc_html__('Änderungen gelten ab der nächsten Verbindung.', 'wp-lit-mcp') . '</strong> '
. esc_html__('Stelle die Verbindung im KI-Assistenten danach neu her.', 'wp-lit-mcp')
. '</p></div>';
echo '<dl class="lit-mcp-legend">';
$legend = [
@@ -123,6 +125,26 @@ final class ToolsTab implements AdminTab
echo '</dl>';
}
/** Search, "only changed" and open/close for the many tools; needs the admin script, so it hides without JS. */
private function renderToolbar(): void
{
printf(
'<div class="lit-mcp-toolbar hide-if-no-js">'
. '<label class="screen-reader-text" for="lit-mcp-tool-search">%1$s</label>'
. '<input type="search" id="lit-mcp-tool-search" class="lit-mcp-tool-search" placeholder="%1$s">'
. '<label><input type="checkbox" class="lit-mcp-only-changed"> %2$s</label>'
. '<button type="button" class="button lit-mcp-collapse-all">%3$s</button>'
. '<button type="button" class="button lit-mcp-expand-all">%4$s</button>'
. '<span class="lit-mcp-filter-status" role="status"></span></div>',
esc_attr__('Tool suchen', 'wp-lit-mcp'),
esc_html__('Nur Tools mit eigener Einstellung', 'wp-lit-mcp'),
esc_html__('Alle einklappen', 'wp-lit-mcp'),
esc_html__('Alle aufklappen', 'wp-lit-mcp')
);
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in kindLegend().
echo '<p class="description lit-mcp-kind-legend">' . $this->kindLegend() . '</p>';
}
/** Only when the direct tools get many: then the count is worth a warning, otherwise it is noise. */
private function renderCounter(int $direct): void
{
@@ -196,7 +218,7 @@ final class ToolsTab implements AdminTab
)),
esc_html($this->summary($modes, $policy->isReadonly($group)))
);
$this->renderGroupControls($policy, $group, $mixed);
// Title, toggle, controls: the same order for the eye and for Tab, so the stylesheet needs no "order".
printf(
'<div class="handle-actions hide-if-no-js"><button type="button" class="handlediv" aria-expanded="%s">'
. '<span class="screen-reader-text">%s</span>'
@@ -208,6 +230,7 @@ final class ToolsTab implements AdminTab
$label
))
);
$this->renderGroupControls($policy, $group, $label, $mixed);
echo '</div><div class="inside">';
printf(
@@ -239,7 +262,7 @@ final class ToolsTab implements AdminTab
return $readonly ? __('nur lesen', 'wp-lit-mcp') . ' · ' . $summary : $summary;
}
private function renderGroupControls(ExposurePolicy $policy, string $group, bool $mixed): void
private function renderGroupControls(ExposurePolicy $policy, string $group, string $label, bool $mixed): void
{
$selectId = 'lit-mcp-group-mode-' . self::idPart($group);
$groupMode = $policy->groupMode($group);
@@ -275,6 +298,15 @@ final class ToolsTab implements AdminTab
checked($policy->isReadonly($group), true, false),
esc_html__('Nur lesen', 'wp-lit-mcp')
);
printf(
'<button type="button" class="button-link lit-mcp-group-reset hide-if-no-js" aria-label="%s">%s</button>',
esc_attr(sprintf(
/* translators: %s: group label */
__('Auf Standard zurücksetzen: %s', 'wp-lit-mcp'),
$label
)),
esc_html__('Auf Standard zurücksetzen', 'wp-lit-mcp')
);
if ($mixed) {
echo '<span class="description">'
. esc_html__('gemischt – einzelne Tools haben eigene Einstellungen', 'wp-lit-mcp')
@@ -389,13 +421,12 @@ final class ToolsTab implements AdminTab
private function kind(Ability $ability): string
{
$annotations = $ability->annotations();
$styles = self::kindStyles();
[$icon, $text] = match (true) {
isset(self::DELETING[$ability->name()]) => ['trash', self::DELETING[$ability->name()] === 'trash'
? __('Papierkorb', 'wp-lit-mcp')
: __('löscht', 'wp-lit-mcp')],
($annotations['destructive'] ?? false) === true => ['update', __('überschreibt', 'wp-lit-mcp')],
($annotations['readonly'] ?? false) === true => ['visibility', __('liest', 'wp-lit-mcp')],
default => ['edit', __('schreibt', 'wp-lit-mcp')],
isset(self::DELETING[$ability->name()]) => $styles[self::DELETING[$ability->name()]],
($annotations['destructive'] ?? false) === true => $styles['overwrite'],
($annotations['readonly'] ?? false) === true => $styles['read'],
default => $styles['write'],
};
return sprintf(
@@ -406,6 +437,35 @@ final class ToolsTab implements AdminTab
);
}
/** @return array<string, array{string, string}> Kind → [dashicon, word]. */
private static function kindStyles(): array
{
return [
'read' => ['visibility', __('liest', 'wp-lit-mcp')],
'write' => ['edit', __('schreibt', 'wp-lit-mcp')],
'overwrite' => ['update', __('überschreibt', 'wp-lit-mcp')],
'delete' => ['trash', __('löscht', 'wp-lit-mcp')],
'trash' => ['trash', __('Papierkorb', 'wp-lit-mcp')],
];
}
/** On phones the "Art" column shows icons only; this line says what they mean (touch has no tooltips). */
private function kindLegend(): string
{
$styles = self::kindStyles();
$parts = [];
foreach (['read', 'write', 'overwrite', 'delete'] as $key) {
[$icon, $text] = $styles[$key];
$parts[] = sprintf(
'<span class="dashicons dashicons-%s" aria-hidden="true"></span> %s',
esc_attr($icon),
esc_html($key === 'delete' ? $text . ' / ' . $styles['trash'][1] : $text)
);
}
return implode(' · ', $parts);
}
/** Safe fragment for an HTML id from a group id or tool name ("lit/guide" → "lit-guide"). */
private static function idPart(string $value): string
{
+101 -17
View File
@@ -107,7 +107,7 @@ final class DiviTab implements AdminTab
/* translators: 1: Divi version, 2: comma separated feature names */
__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'LIT MCP: Mit Divi %1$s sind diese Fähigkeiten nicht nutzbar: %2$s. Die betroffenen Werkzeuge lehnen ab, alle anderen laufen weiter.',
'LIT MCP: Mit Divi %1$s sind diese Fähigkeiten nicht nutzbar: %2$s. Die betroffenen Tools lehnen ab, alle anderen laufen weiter.',
'wp-lit-mcp'
),
$this->version(),
@@ -211,7 +211,7 @@ final class DiviTab implements AdminTab
echo '<h2>' . esc_html__('Divi-Freigabe', 'wp-lit-mcp') . '</h2>';
echo '<p>' . esc_html__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'LIT MCP prüft nach einem Divi-Update jede Fähigkeit einzeln (z. B. „Seiten schreiben“). Ist sie nicht nutzbar, lehnen nur die Werkzeuge ab, die sie brauchen; alle anderen laufen weiter.',
'LIT MCP prüft nach einem Divi-Update jede Fähigkeit einzeln (z. B. „Seiten schreiben“). Ist sie nicht nutzbar, lehnen nur die Tools ab, die sie brauchen; alle anderen laufen weiter.',
'wp-lit-mcp'
) . '</p>';
if ($this->removed !== []) {
@@ -225,6 +225,7 @@ final class DiviTab implements AdminTab
);
}
$this->renderNextStep($status);
$this->renderStatus($status);
$this->renderFeatures($status);
$this->renderForms($status, $report);
@@ -233,6 +234,45 @@ final class DiviTab implements AdminTab
}
}
/** One sentence on what to do now, above the details; a released version needs none. */
private function renderNextStep(ReleaseStatus $status): void
{
if ($status->version === '') {
return;
}
$text = match ($status->state) {
ReleaseState::Untested => sprintf(
/* translators: %s: Divi version */
__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text.
'Divi %s ist noch nicht geprüft. Starte unten die Prüfung; danach kannst du die Version freigeben.',
'wp-lit-mcp'
),
$status->version
),
ReleaseState::Incompatible => sprintf(
/* translators: %s: Divi version */
__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text.
'Divi %s hat Stellen geändert, die LIT MCP nutzt. Die betroffenen Tools bleiben gesperrt, bis ein Plugin-Update erscheint.',
'wp-lit-mcp'
),
$status->version
),
default => null,
};
if ($text === null) {
return;
}
printf(
'<div class="notice notice-%s inline"><p><strong>%s</strong></p></div>',
$status->state === ReleaseState::Incompatible ? 'error' : 'warning',
esc_html($text)
);
}
private function renderStatus(ReleaseStatus $status): void
{
echo '<table class="form-table" role="presentation"><tbody>';
@@ -242,9 +282,10 @@ final class DiviTab implements AdminTab
esc_html($status->version === '' ? __('nicht erkannt', 'wp-lit-mcp') : $status->version)
);
printf(
'<tr><th scope="row">%s</th><td><strong>%s</strong> – %s</td></tr>',
'<tr><th scope="row">%s</th><td>%s – %s</td></tr>',
esc_html__('Gesamtzustand', 'wp-lit-mcp'),
esc_html($this->stateLabel($status->state)),
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in stateBadge().
$this->stateBadge($status->state),
esc_html($this->explanation($status))
);
if ($status->problems !== []) {
@@ -259,31 +300,47 @@ final class DiviTab implements AdminTab
private function renderFeatures(ReleaseStatus $status): void
{
echo '<h3>' . esc_html__('Fähigkeiten', 'wp-lit-mcp') . '</h3>';
// Open when something needs attention; a released version keeps the long table folded.
printf(
'<details class="lit-mcp-divi-details"%s><summary>%s</summary>',
$status->state === ReleaseState::Released ? '' : ' open',
esc_html__('Fähigkeiten im Detail', 'wp-lit-mcp')
);
echo '<table class="widefat striped lit-mcp-divi-features"><thead><tr>';
$headings = [
__('Fähigkeit', 'wp-lit-mcp'),
__('Zustand', 'wp-lit-mcp'),
__('Quelle', 'wp-lit-mcp'),
__('Probleme', 'wp-lit-mcp'),
__('Betroffene Werkzeuge', 'wp-lit-mcp'),
__('Betroffene Tools', 'wp-lit-mcp'),
];
foreach ($headings as $heading) {
echo '<th scope="col">' . esc_html($heading) . '</th>';
}
echo '</tr></thead><tbody>';
foreach (($this->overview ?? FeatureOverview::withoutTools())->rows($status->matrix) as $row) {
echo '<tr><td>' . esc_html($row['label']) . '</td>';
echo '<td><strong>' . esc_html($this->stateLabel(ReleaseState::from($row['state']))) . '</strong></td>';
echo '<td>' . esc_html($this->sourceLabel($row['source'])) . '</td>';
echo '<td>';
// data-label: on phones each row becomes a card and the cell names itself.
printf('<tr><td data-label="%s">%s</td>', esc_attr($headings[0]), esc_html($row['label']));
printf(
'<td data-label="%s">%s</td>',
esc_attr($headings[1]),
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in stateBadge().
$this->stateBadge(ReleaseState::from($row['state']))
);
printf(
'<td data-label="%s">%s</td>',
esc_attr($headings[2]),
esc_html($this->sourceLabel($row['source']))
);
printf('<td data-label="%s">', esc_attr($headings[3]));
$this->renderList($row['problems'], false);
echo '</td><td>';
printf('</td><td data-label="%s">', esc_attr($headings[4]));
$this->renderList($row['tools'], true);
echo '</td></tr>';
}
echo '</tbody></table>';
$this->renderAutomationNote();
echo '</details>';
}
/** @param list<string> $items */
@@ -305,9 +362,9 @@ final class DiviTab implements AdminTab
private function sourceLabel(?string $source): string
{
return match ($source) {
ReleaseStatus::SOURCE_CODE => __('Plugin (code)', 'wp-lit-mcp'),
ReleaseStatus::SOURCE_DATABASE => __('Freigabe auf dieser Website (database)', 'wp-lit-mcp'),
ReleaseStatus::SOURCE_AUTO => __('Automatik (auto)', 'wp-lit-mcp'),
ReleaseStatus::SOURCE_CODE => __('Im Plugin als getestet hinterlegt', 'wp-lit-mcp'),
ReleaseStatus::SOURCE_DATABASE => __('Von dir auf dieser Website freigegeben', 'wp-lit-mcp'),
ReleaseStatus::SOURCE_AUTO => __('Automatisch geprüft', 'wp-lit-mcp'),
default => '–',
};
}
@@ -331,6 +388,23 @@ final class DiviTab implements AdminTab
) . '</p>';
}
/** Same coloured icons as the status list on the tab "Verbindung"; the word stays, colour is never alone. */
private function stateBadge(ReleaseState $state): string
{
$icon = match ($state) {
ReleaseState::Released => 'yes-alt',
ReleaseState::Untested => 'warning',
ReleaseState::Incompatible => 'dismiss',
};
return sprintf(
'<span class="lit-mcp-state lit-mcp-status-%1$s"><span class="dashicons dashicons-%1$s"'
. ' aria-hidden="true"></span> <strong>%2$s</strong></span>',
$icon,
esc_html($this->stateLabel($state))
);
}
private function stateLabel(ReleaseState $state): string
{
return match ($state) {
@@ -352,7 +426,7 @@ final class DiviTab implements AdminTab
if ($status->state === ReleaseState::Untested) {
return __(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Diese Version ist nicht geprüft. Die Werkzeuge der ungetesteten Fähigkeiten (Tabelle unten) lehnen ab, bis die Version freigegeben ist; alle anderen laufen weiter.',
'Diese Version ist nicht geprüft. Die Tools der ungetesteten Fähigkeiten (Tabelle unten) lehnen ab, bis die Version freigegeben ist; alle anderen laufen weiter.',
'wp-lit-mcp'
);
}
@@ -446,8 +520,18 @@ final class DiviTab implements AdminTab
))
);
foreach ($report->steps as $step) {
$mark = $step->skipped ? '–' : ($step->green() ? '✓' : '✗');
printf('<li>%s %s', esc_html($mark), esc_html($step->label()));
[$icon, $word] = match (true) {
$step->skipped => ['minus', __('übersprungen', 'wp-lit-mcp')],
$step->green() => ['yes-alt', __('bestanden', 'wp-lit-mcp')],
default => ['dismiss', __('fehlgeschlagen', 'wp-lit-mcp')],
};
printf(
'<li class="lit-mcp-state lit-mcp-status-%1$s"><span class="dashicons dashicons-%1$s"'
. ' aria-hidden="true"></span><span class="screen-reader-text">%2$s:</span> %3$s',
esc_attr($icon),
esc_html($word),
esc_html($step->label())
);
if ($step->skipped) {
echo ' (' . esc_html__('nicht ausgeführt', 'wp-lit-mcp') . ')';
}