diff --git a/assets/css/admin.css b/assets/css/admin.css
index 1fe2cdd..b5c7ced 100644
--- a/assets/css/admin.css
+++ b/assets/css/admin.css
@@ -132,7 +132,43 @@
vertical-align: text-bottom;
}
-/* Tab "Compatibility": the report is a plain list, the icon replaces the bullet. */
+/* Tab "Kompatibilität": one white section per plugin under the overview table. */
+.lit-mcp-compat {
+ /* Jump links from the overview: keep the heading clear of the fixed admin bar. */
+ scroll-margin-top: 56px;
+ margin: 24px 0;
+ padding: 4px 20px 16px;
+ background: #fff;
+ border: 1px solid #c3c4c7;
+ overflow-wrap: break-word;
+}
+
+.lit-mcp-compat h3 {
+ margin: 1.2em 0 0.5em;
+ font-size: 14px;
+}
+
+.lit-mcp-compat-overview {
+ margin-bottom: 8px;
+}
+
+.lit-mcp-compat .notice.inline ul {
+ margin: 0 0 0.5em 2em;
+ list-style: disc;
+}
+
+.lit-mcp-compat-actions {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px 12px;
+}
+
+.lit-mcp-compat-actions form {
+ margin: 0;
+}
+
+/* The report is a plain list, the icon replaces the bullet. */
.lit-mcp-compat-report {
list-style: none;
margin-left: 0;
@@ -144,7 +180,7 @@
}
.lit-mcp-status-yes-alt .dashicons {
- color: #008a20;
+ color: #007017;
}
.lit-mcp-status-warning .dashicons {
@@ -155,6 +191,10 @@
color: #d63638;
}
+.lit-mcp-status-minus .dashicons {
+ color: #50575e;
+}
+
/* Folded help ("Wo finde ich das?") and the Divi detail table. */
.lit-mcp-hint,
.lit-mcp-compat-details {
@@ -176,6 +216,12 @@
margin: 1em 0 0.5em;
}
+.lit-mcp-compat-details summary:focus-visible,
+.lit-mcp-hint summary:focus-visible {
+ outline: 2px solid #2271b1;
+ outline-offset: 2px;
+}
+
/* Only on phones: text that replaces a hidden column. */
.lit-mcp-phone-only {
display: none;
@@ -435,9 +481,14 @@
}
}
-/* Tab "Compatibility": on phones each row of the feature table becomes a card named by data-label. */
+/* Tab "Kompatibilität": on phones each row of the overview and feature tables becomes a card named by data-label. */
@media screen and (max-width: 782px) {
- .lit-mcp-compat-features thead {
+ .lit-mcp-compat {
+ padding: 4px 12px 12px;
+ }
+
+ .lit-mcp-compat-features thead,
+ .lit-mcp-compat-overview thead {
position: absolute;
width: 1px;
height: 1px;
@@ -446,17 +497,23 @@
}
.lit-mcp-compat-features tr,
- .lit-mcp-compat-features td {
+ .lit-mcp-compat-features th,
+ .lit-mcp-compat-features td,
+ .lit-mcp-compat-overview tr,
+ .lit-mcp-compat-overview th,
+ .lit-mcp-compat-overview td {
display: block;
}
- /* One card per row: a rule and room between them, the feature name first and bold. */
- .lit-mcp-compat-features tr {
+ /* One card per row: a rule and room between them, the name first and bold. */
+ .lit-mcp-compat-features tr,
+ .lit-mcp-compat-overview tr {
padding: 8px 0;
border-bottom: 1px solid #c3c4c7;
}
- .lit-mcp-compat-features td:first-child {
+ .lit-mcp-compat-features th:first-child,
+ .lit-mcp-compat-overview th:first-child {
font-weight: 600;
}
@@ -464,7 +521,10 @@
overflow-wrap: anywhere;
}
- .lit-mcp-compat-features td::before {
+ .lit-mcp-compat-features th::before,
+ .lit-mcp-compat-features td::before,
+ .lit-mcp-compat-overview th::before,
+ .lit-mcp-compat-overview td::before {
content: attr(data-label) ": ";
font-weight: 600;
}
@@ -472,6 +532,16 @@
.lit-mcp-compat-features ul {
margin: 0;
}
+
+ /* Touch targets: the link in the overview and the check/release buttons. */
+ .lit-mcp-compat-overview a {
+ display: inline-block;
+ padding: 4px 0;
+ }
+
+ .lit-mcp-compat-actions .button {
+ min-height: 40px;
+ }
}
/* Tab "Protokoll": filters in their own block above the table (core hides ".tablenav .actions"
@@ -567,3 +637,7 @@
.lit-mcp-unit {
margin-left: 4px;
}
+
+.lit-mcp-compat-versions th {
+ width: 30%;
+}
diff --git a/docs/guides/overview.md b/docs/guides/overview.md
index fab4d09..b4c1b88 100644
--- a/docs/guides/overview.md
+++ b/docs/guides/overview.md
@@ -93,16 +93,17 @@ nichts geändert. Vorgehen:
Werkzeuge (`tools`).
3. Ein anderes Werkzeug nutzen, das die betroffene Fähigkeit nicht braucht – ist z. B. nur „Seiten schreiben“
betroffen, funktioniert `lit-divi-outline` weiter, und der Nutzer kann die Änderung selbst im Divi-Builder machen.
-4. Den Nutzer informieren: bei `divi_untested` soll ein Administrator unter Einstellungen → LIT MCP → Compatibility die
+4. Den Nutzer informieren: bei `divi_untested` soll ein Administrator unter Einstellungen → LIT MCP → Kompatibilität die
Prüfung ausführen und die Version freigeben; bei `divi_incompatible` muss LIT MCP aktualisiert werden. Die Sperre
nicht über andere Werkzeuge umgehen.
5. Dasselbe gilt für Pods: Ist eine Pods-Fähigkeit (`pods-…`) nicht nutzbar, antwortet das betroffene Werkzeug mit
- `compat_incompatible`; der Stand steht im selben Reiter „Compatibility“.
+ `compat_incompatible`; der Stand steht im selben Reiter „Kompatibilität“.
Seitenweite Änderungen (Optionen, Menüs und Menü-Positionen, Medien, Divi-Design, Theme Builder, globale
Bibliotheksmodule) leeren den Cache von WP Super Cache, Änderungen an einem Beitrag nur dessen Seite. Dabei leert der
Purge eines einzelnen Beitrags die Cache-Dateien unter dem Host, über den der MCP-Aufruf eingeht; bei verschiedenen
Hosts (z. B. mit und ohne `www`) bleiben die Dateien des anderen Hosts womöglich ungeleert (der Komplett-Purge ist davon
-nicht betroffen). Ist der Cache dafür gesperrt, bleibt er unverändert; der Zustand steht im Reiter „Compatibility“
-und in `lit-site-status` (`wpsc`). Der Nachlauf-Purge, der auf ein gedrosseltes Leeren des ganzen Caches folgt, braucht WP-Cron (oder einen
-System-Cron, der `wp-cron.php` aufruft).
+nicht betroffen). Ist der Cache dafür gesperrt, bleibt er unverändert; der Zustand steht im Reiter „Kompatibilität“
+und in `lit-site-status` (`wpsc`). Der Nachlauf-Purge, der auf ein gedrosseltes Leeren des ganzen Caches folgt, braucht
+WP-Cron (oder einen System-Cron, der `wp-cron.php` aufruft). Auf Divi-Seiten leert jede Prüfung im Reiter den ganzen
+Cache einmal: Divi selbst leert ihn beim Anlegen und Löschen der Testseite.
diff --git a/docs/guides/workflows.md b/docs/guides/workflows.md
index b5ec522..e86eb11 100644
--- a/docs/guides/workflows.md
+++ b/docs/guides/workflows.md
@@ -34,7 +34,7 @@ insgesamt:
2. Mit Werkzeugen weiterarbeiten, deren Fähigkeit `released` ist (z. B. Seiten lesen mit `lit-divi-outline`), und den
gesperrten Teil auslassen.
3. Dem Nutzer sagen, welche Fähigkeit gesperrt ist: bei `untested` muss ein Administrator unter Einstellungen → LIT
- MCP → Compatibility die Prüfung ausführen und freigeben, bei `incompatible` muss LIT MCP aktualisiert werden.
+ MCP → Kompatibilität die Prüfung ausführen und freigeben, bei `incompatible` muss LIT MCP aktualisiert werden.
## Bibliothek und globale Module
diff --git a/readme.txt b/readme.txt
index ab6488a..d42f3a2 100644
--- a/readme.txt
+++ b/readme.txt
@@ -205,7 +205,7 @@ Weg nicht: der Wechsel auf 1.0.2 ist einmalig von Hand (ZIP hochladen), danach l
== Divi-Freigabe ==
-LIT MCP schreibt nur mit Divi-Versionen, die für diese Plugin-Version geprüft sind. Nach einem Divi-Update lesen die Divi-Werkzeuge weiter; schreibende Divi-Werkzeuge antworten mit „divi_untested“, bis ein Administrator unter Einstellungen → LIT MCP → Compatibility die Prüfung startet und die Version freigibt. Die Prüfung legt eine Entwurfsseite an, schreibt und rendert sie und löscht sie danach endgültig. Hat Divi Stellen geändert, die LIT MCP nachbildet, ist die Version „inkompatibel“ (divi_incompatible) – dann hilft nur ein Update von LIT MCP. Für Entwickler: `wp --user= lit-mcp divi-check [--diff] [--update-reference]`.
+LIT MCP schreibt nur mit Divi-Versionen, die für diese Plugin-Version geprüft sind. Nach einem Divi-Update lesen die Divi-Werkzeuge weiter; schreibende Divi-Werkzeuge antworten mit „divi_untested“, bis ein Administrator unter Einstellungen → LIT MCP → Kompatibilität die Prüfung startet und die Version freigibt. Die Prüfung legt eine Entwurfsseite an, schreibt und rendert sie und löscht sie danach endgültig. Hat Divi Stellen geändert, die LIT MCP nachbildet, ist die Version „inkompatibel“ (divi_incompatible) – dann hilft nur ein Update von LIT MCP. Für Entwickler: `wp --user= lit-mcp divi-check [--diff] [--update-reference]`.
== Changelog ==
diff --git a/src/Admin/CompatibilityTab.php b/src/Admin/CompatibilityTab.php
index f7cb2f7..344c3f3 100644
--- a/src/Admin/CompatibilityTab.php
+++ b/src/Admin/CompatibilityTab.php
@@ -22,14 +22,16 @@ use Throwable;
defined('ABSPATH') || exit;
/**
- * Tab "Compatibility": one section per third-party plugin of Compatibility::profiles() – its version, overall state
- * (word and icon, never colour alone), what the tier means for a release, the capabilities, tested and released
- * versions, the latest check report and "Prüfung starten" (Admin\CheckRoute). A profile that is not active is one
- * line and never asks for its matrix. Everything that comes from a third-party plugin (version, problem texts, step
- * labels) is escaped on output. Profiles hook in: lit_mcp_compat_profile_load($id) runs before any output (see
- * loadProfiles()), lit_mcp_compat_profile_intro($id) opens the section above the capability table (next step,
- * notices), lit_mcp_compat_profile_extra($id) closes it, and the filter lit_mcp_compat_explanation($text, $id,
- * ReleaseState) replaces the generic sentence next to the overall state.
+ * Tab "Kompatibilität" (slug "compatibility"): an overview table of all third-party plugins of
+ * Compatibility::profiles() (version, state as word and icon – never colour alone –, kind of release, link to the
+ * section), then one section per active plugin: state and next step first, then the actions ("Prüfung starten",
+ * Admin\CheckRoute, release, withdrawal) and, folded, the details (capabilities, tested and released versions, the
+ * latest check report). A profile that is not active is one overview row and never asks for its matrix. Everything
+ * that comes from a third-party plugin (version, problem texts, step labels) is escaped on output. Profiles hook in:
+ * lit_mcp_compat_profile_load($id) runs before any output (see loadProfiles()),
+ * lit_mcp_compat_profile_intro($id) opens the section below the state notice (next step, notices),
+ * lit_mcp_compat_profile_extra($id) closes it, and the filter lit_mcp_compat_explanation($text, $id, ReleaseState)
+ * replaces the generic sentence next to the overall state.
*/
final class CompatibilityTab implements AdminTab
{
@@ -94,7 +96,7 @@ final class CompatibilityTab implements AdminTab
public function label(): string
{
- return __('Compatibility', 'wp-lit-mcp');
+ return __('Kompatibilität', 'wp-lit-mcp');
}
public function registerSettings(): void
@@ -227,71 +229,213 @@ final class CompatibilityTab implements AdminTab
public function render(): void
{
- foreach ($this->compatibility->profiles() as $profile) {
- $this->renderProfile($profile);
+ $profiles = $this->compatibility->profiles();
+ $this->renderOverview($profiles);
+ foreach ($profiles as $profile) {
+ if ($this->compatibility->active($profile)) {
+ $this->renderProfile($profile);
+ }
}
}
- private function renderProfile(Profile $profile): void
+ /**
+ * The whole page at a glance: one sentence on whether anything needs attention, then one row per plugin. Rows of
+ * active plugins link to their section; an inactive plugin is one short row.
+ *
+ * @param list $profiles
+ */
+ private function renderOverview(array $profiles): void
{
- printf('', esc_attr($profile->id));
- printf('%s
', esc_html($profile->label));
+ $active = array_filter($profiles, fn (Profile $profile): bool => $this->compatibility->active($profile));
+ $attention = count(array_filter(
+ $active,
+ fn (Profile $profile): bool => $this->compatibility->matrix($profile)->state() !== ReleaseState::Released
+ ));
+
+ echo '' . esc_html__(
+ // phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
+ 'Hier siehst du, ob LIT MCP zu deinen Plugins passt. Was nicht passt, sperrt LIT MCP; alles andere läuft weiter.',
+ 'wp-lit-mcp'
+ ) . '
';
+ echo '' . esc_html__('Übersicht', 'wp-lit-mcp') . '
';
+ [$class, $text] = match (true) {
+ $active === [] => ['info', __('Kein unterstütztes Plugin ist aktiv. Es gibt nichts zu tun.', 'wp-lit-mcp')],
+ $attention === 0 => [
+ 'success',
+ __('Alles in Ordnung: Alle aktiven Plugins sind freigegeben.', 'wp-lit-mcp'),
+ ],
+ default => ['warning', sprintf(
+ /* translators: %d: number of plugins */
+ _n(
+ '%d aktives Plugin braucht deine Aufmerksamkeit.',
+ '%d aktive Plugins brauchen deine Aufmerksamkeit.',
+ $attention,
+ 'wp-lit-mcp'
+ ),
+ $attention
+ )],
+ };
+ printf('', esc_attr($class), esc_html($text));
+
+ $headings = [
+ __('Plugin', 'wp-lit-mcp'),
+ __('Version', 'wp-lit-mcp'),
+ __('Zustand', 'wp-lit-mcp'),
+ __('Freigabe', 'wp-lit-mcp'),
+ __('Details', 'wp-lit-mcp'),
+ ];
+ echo ''
+ . esc_html__('Alle unterstützten Plugins und ihr Zustand', 'wp-lit-mcp') . '';
+ foreach ($headings as $heading) {
+ echo '| ' . esc_html($heading) . ' | ';
+ }
+ echo '
';
+ foreach ($profiles as $profile) {
+ $this->renderOverviewRow($profile, $headings);
+ }
+ echo '
';
+ }
+
+ /** @param list $headings */
+ private function renderOverviewRow(Profile $profile, array $headings): void
+ {
+ printf(
+ '| %s | ',
+ esc_attr($headings[0]),
+ esc_html($profile->label)
+ );
if (! $this->compatibility->active($profile)) {
- echo '' . esc_html__('nicht installiert oder nicht aktiv', 'wp-lit-mcp') . '
';
+ $none = '–'
+ . esc_html__('keine Angabe', 'wp-lit-mcp') . '';
+ printf(
+ '%5$s | %6$s | %5$s | '
+ . '%5$s |
',
+ esc_attr($headings[1]),
+ esc_attr($headings[2]),
+ esc_attr($headings[3]),
+ esc_attr($headings[4]),
+ $none, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above.
+ ' ' . esc_html__('nicht aktiv', 'wp-lit-mcp')
+ . ''
+ );
return;
}
+ printf(
+ '%5$s | %6$s | %7$s | '
+ . '%9$s'
+ . ' %10$s | ',
+ esc_attr($headings[1]),
+ esc_attr($headings[2]),
+ esc_attr($headings[3]),
+ esc_attr($headings[4]),
+ esc_html($this->compatibility->version($profile) ?? __('nicht erkannt', 'wp-lit-mcp')),
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in stateBadge().
+ self::stateBadge($this->compatibility->matrix($profile)->state()),
+ esc_html(self::tierHint($profile->tier)),
+ esc_attr($profile->id),
+ esc_html__('Details', 'wp-lit-mcp'),
+ esc_html(sprintf(
+ /* translators: %s: plugin name */
+ __('zu %s', 'wp-lit-mcp'),
+ $profile->label
+ ))
+ );
+ }
+
+ private function renderProfile(Profile $profile): void
+ {
$matrix = $this->compatibility->matrix($profile);
$problems = $this->compatibility->problems($profile);
$state = $matrix->state();
- echo '';
-
do_action(self::INTRO_ACTION, $profile->id);
- $this->renderCapabilities($profile, $matrix);
+
+ // 2. Actions.
$report = ($this->reports)($profile)->latest();
$this->renderForms($profile, $report);
+
+ // 3. Details, folded while everything is fine.
+ printf(
+ '%s'
+ . ' %s
',
+ $state === ReleaseState::Released ? '' : ' open',
+ esc_html__('Details', 'wp-lit-mcp'),
+ esc_html(sprintf(
+ /* translators: %s: plugin name */
+ __('zu %s', 'wp-lit-mcp'),
+ $profile->label
+ ))
+ );
+ $this->renderCapabilities($profile, $matrix);
+ $this->renderVersions($profile);
if ($report !== null) {
$this->renderReport($profile, $report);
}
+ echo ' ';
do_action(self::EXTRA_ACTION, $profile->id);
echo '';
}
+ private static function noticeClass(ReleaseState $state): string
+ {
+ return match ($state) {
+ ReleaseState::Released => 'success',
+ ReleaseState::Untested => 'warning',
+ ReleaseState::Incompatible => 'error',
+ };
+ }
+
+ private function renderVersions(Profile $profile): void
+ {
+ $rows = [
+ __('Im Plugin getestet', 'wp-lit-mcp') => $profile->testedVersions === []
+ ? '–' : esc_html(implode(', ', $profile->testedVersions)),
+ __('Von dir freigegeben', 'wp-lit-mcp') => esc_html($this->releasedVersions($profile)) ?: '–',
+ ];
+ echo '' . esc_html__('Versionen', 'wp-lit-mcp') . '
';
+ echo ''
+ . esc_html(sprintf(
+ /* translators: %s: plugin name */
+ __('Getestete und freigegebene Versionen von %s', 'wp-lit-mcp'),
+ $profile->label
+ )) . '';
+ foreach ($rows as $heading => $value) {
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $value is escaped above.
+ printf('| %s | %s |
', esc_html($heading), $value);
+ }
+ echo '
';
+ }
+
private function renderCapabilities(Profile $profile, CompatibilityMatrix $matrix): void
{
$extra = $this->extraColumns($profile);
@@ -300,12 +444,7 @@ final class CompatibilityTab implements AdminTab
$headings[] = $column['heading'];
}
- // Open when something needs attention; a released profile keeps the long table folded.
- printf(
- '%s
',
- $matrix->state() === ReleaseState::Released ? '' : ' open',
- esc_html__('Fähigkeiten im Detail', 'wp-lit-mcp')
- );
+ echo '' . esc_html__('Fähigkeiten', 'wp-lit-mcp') . '
';
echo ''
. esc_html(sprintf(
/* translators: %s: plugin name */
@@ -320,7 +459,7 @@ final class CompatibilityTab implements AdminTab
foreach ($profile->capabilities as $capability) {
// data-label: on phones each row becomes a card and the cell names itself.
printf(
- '| %s | %s | ',
+ '
| %s | %s | ',
esc_attr($headings[0]),
esc_html($capability->label()),
esc_attr($headings[1]),
@@ -337,7 +476,7 @@ final class CompatibilityTab implements AdminTab
}
echo '
';
}
- echo '
';
+ echo '';
}
/** @return list */
@@ -385,6 +524,7 @@ final class CompatibilityTab implements AdminTab
$fresh = $report !== null && $report->freshFor($version, Plugin::VERSION, $this->stateKey($profile, $version));
$releasable = $this->releasable($profile, $version);
+ echo '' . esc_html__('Aktionen', 'wp-lit-mcp') . '
';
printf('
';
if ($releasable && $fresh) {
@@ -407,11 +546,6 @@ final class CompatibilityTab implements AdminTab
sprintf(__('%1$s %2$s freigeben', 'wp-lit-mcp'), $profile->label, $version),
'primary'
);
- } elseif ($releasable) {
- echo '
' . esc_html__(
- 'Freigeben geht erst nach einer vollständig grünen Prüfung dieser Version – Prüfung erneut ausführen.',
- 'wp-lit-mcp'
- ) . '
';
}
// A release applies whatever the overall state: one incompatible capability must not hide the way back.
@@ -425,6 +559,16 @@ final class CompatibilityTab implements AdminTab
true
);
}
+ echo '
';
+
+ echo '' . esc_html__('Die Prüfung dauert einige Sekunden.', 'wp-lit-mcp') . '
';
+ if ($releasable && ! $fresh) {
+ echo '' . esc_html__(
+ // phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
+ 'Freigeben geht erst nach einer vollständig grünen Prüfung dieser Version. Führe die Prüfung erneut aus.',
+ 'wp-lit-mcp'
+ ) . '
';
+ }
}
/** ": " – the accessible name starts with the visible text (WCAG 2.5.3). */
@@ -467,7 +611,7 @@ final class CompatibilityTab implements AdminTab
esc_html__('Letzte Prüfung', 'wp-lit-mcp'),
esc_html(sprintf(
/* translators: 1: plugin version, 2: LIT MCP version, 3: date */
- __('Version %1$s mit LIT MCP %2$s, %3$s', 'wp-lit-mcp'),
+ __('Geprüft: Version %1$s mit LIT MCP %2$s, %3$s', 'wp-lit-mcp'),
$report->version,
$report->pluginVersion,
date_i18n((string) get_option('date_format'), (int) strtotime($report->createdAt))
@@ -556,7 +700,7 @@ final class CompatibilityTab implements AdminTab
return match ($state) {
ReleaseState::Released => __('Alle Fähigkeiten sind nutzbar.', 'wp-lit-mcp'),
ReleaseState::Untested => __(
- 'Diese Version ist nicht geprüft. Die ungetesteten Fähigkeiten sind gesperrt, bis sie freigegeben ist.',
+ 'Diese Version ist nicht geprüft. Die ungetesteten Fähigkeiten sind gesperrt, bis du sie freigibst.',
'wp-lit-mcp'
),
ReleaseState::Incompatible => __(
@@ -566,15 +710,29 @@ final class CompatibilityTab implements AdminTab
};
}
+ /** Short and in plain words: the tier's name never reaches the screen. */
private static function tierHint(Tier $tier): string
{
return match ($tier) {
Tier::ReleasePerVersion => __('Freigabe pro Version nötig', 'wp-lit-mcp'),
- Tier::LockOnFailure => __(
- 'Freigabe wird dokumentiert, Nutzung automatisch, solange die Prüfung grün ist',
+ Tier::LockOnFailure => __('Sperrt automatisch bei Problemen', 'wp-lit-mcp'),
+ Tier::ReportOnly => __('Nur Anzeige', 'wp-lit-mcp'),
+ };
+ }
+
+ private static function tierExplanation(Tier $tier): string
+ {
+ return match ($tier) {
+ Tier::ReleasePerVersion => __(
+ 'Jede neue Version musst du einmal freigeben, bevor LIT MCP damit schreibt.',
'wp-lit-mcp'
),
- Tier::ReportOnly => __('nur Anzeige', 'wp-lit-mcp'),
+ Tier::LockOnFailure => __(
+ // phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
+ 'LIT MCP sperrt betroffene Fähigkeiten selbst, sobald die Prüfung Probleme findet. Eine Freigabe hält nur fest, dass du diese Version geprüft hast.',
+ 'wp-lit-mcp'
+ ),
+ Tier::ReportOnly => __('Probleme werden nur angezeigt; nichts wird gesperrt.', 'wp-lit-mcp'),
};
}
}
diff --git a/src/Admin/SettingsPage.php b/src/Admin/SettingsPage.php
index cc3a697..e1a4def 100644
--- a/src/Admin/SettingsPage.php
+++ b/src/Admin/SettingsPage.php
@@ -114,7 +114,7 @@ final class SettingsPage
'wp-lit-mcp'
),
__('Passt es zu Divi, Pods & Co.?', 'wp-lit-mcp') => __(
- 'Tab „Compatibility“: Fremd-Plugins prüfen, Versionen freigeben und sehen, was gesperrt ist.',
+ 'Tab „Kompatibilität“: Fremd-Plugins prüfen, Versionen freigeben und sehen, was gesperrt ist.',
'wp-lit-mcp'
),
];
diff --git a/src/Integrations/Divi/Admin/DiviAdminExtras.php b/src/Integrations/Divi/Admin/DiviAdminExtras.php
index 00860e2..9da1c27 100644
--- a/src/Integrations/Divi/Admin/DiviAdminExtras.php
+++ b/src/Integrations/Divi/Admin/DiviAdminExtras.php
@@ -87,7 +87,7 @@ final class DiviAdminExtras
))
)),
esc_url(SettingsPage::url(CompatibilityTab::SLUG)),
- esc_html__('Details im Reiter „Compatibility“', 'wp-lit-mcp')
+ esc_html__('Details im Reiter „Kompatibilität“', 'wp-lit-mcp')
);
}
@@ -298,6 +298,8 @@ final class DiviAdminExtras
/** E1: what the automatic release does and how to switch it off. */
private function renderAutomationNote(): void
{
+ echo ''
+ . esc_html__('So funktioniert die automatische Freigabe', 'wp-lit-mcp') . '
';
echo '' . esc_html__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Automatik: Ist die installierte Divi-Version im Plugin nicht als getestet hinterlegt, gibt LIT MCP jede Fähigkeit einzeln frei, wenn alle benutzten Divi-Funktionen vorhanden sind, ihre Aufrufe (Signaturen) und die Fingerabdrücke nachgebauter Divi-Abläufe unverändert sind und alle Konstanten definiert sind. Das prüft Aufrufformen, nicht das Verhalten von Divi. Fehlt für eine Stelle die Referenz, ist die Fähigkeit ungetestet; weicht etwas ab, ist sie inkompatibel.',
@@ -311,7 +313,7 @@ final class DiviAdminExtras
'wp-lit-mcp'
),
'lit_mcp_divi_auto_release'
- ) . '
';
+ ) . '
';
}
private function version(): string
diff --git a/src/Integrations/Divi/Cli/DiviCheckCommand.php b/src/Integrations/Divi/Cli/DiviCheckCommand.php
index 7797e5d..2de8ce0 100644
--- a/src/Integrations/Divi/Cli/DiviCheckCommand.php
+++ b/src/Integrations/Divi/Cli/DiviCheckCommand.php
@@ -72,7 +72,7 @@ final class DiviCheckCommand
}
/**
- * Prüft die installierte Divi-Version wie der Reiter „Compatibility“: Manifeste, Fingerabdrücke, Modulkatalog,
+ * Prüft die installierte Divi-Version wie der Reiter „Kompatibilität“: Manifeste, Fingerabdrücke, Modulkatalog,
* Format und Schreibtest auf einer Wegwerfseite. Gibt danach den Zustand je Fähigkeit aus und gibt nichts frei.
* Exit-Code 0: Prüfung grün und alle Fähigkeiten nutzbar; 1: Prüfung rot; 2: Prüfung grün, aber mindestens
* eine Fähigkeit nicht nutzbar (z. B. Version noch nicht in DiviReleases::TESTED).
diff --git a/src/Integrations/Divi/Release/ReleaseStatus.php b/src/Integrations/Divi/Release/ReleaseStatus.php
index 28bb45d..9cabd72 100644
--- a/src/Integrations/Divi/Release/ReleaseStatus.php
+++ b/src/Integrations/Divi/Release/ReleaseStatus.php
@@ -24,7 +24,7 @@ final class ReleaseStatus
public const SOURCE_AUTO = CompatibilityMatrix::SOURCE_AUTO;
private const FEATURE_UNTESTED = 'Achtung: %1$s ist auf dieser Website für %2$s noch nicht für LIT MCP '
- . 'freigegeben. Das ist gesperrt, bis ein Administrator unter Einstellungen → LIT MCP → Compatibility die '
+ . 'freigegeben. Das ist gesperrt, bis ein Administrator unter Einstellungen → LIT MCP → Kompatibilität die '
. 'Prüfung ausführt und die Version freigibt. Es wurde nichts geändert.';
private const FEATURE_UNTESTED_HINT = 'Gesperrt: %s. Andere Divi-Werkzeuge funktionieren weiter, sofern ihre '
. 'Fähigkeiten freigegeben sind. Den Nutzer bitten, die Divi-Prüfung auszuführen und die Version freizugeben.';
diff --git a/tests/Integration/pods-compat-smoke.sh b/tests/Integration/pods-compat-smoke.sh
index 9f198e1..c4888b1 100644
--- a/tests/Integration/pods-compat-smoke.sh
+++ b/tests/Integration/pods-compat-smoke.sh
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# End-to-end checks of the Pods profile of the compatibility layer (plan 2026-11-06-kompatibilitaetsschicht, 1.25):
-# status, check route /pods-check, tab "Compatibility", release (refused for a tested version, simulated for an
+# status, check route /pods-check, tab "Kompatibilität", release (refused for a tested version, simulated for an
# untested one) and the lock by a red stored report. Runs INSIDE the DDEV web container:
# cd /home/janniclas/Projekte/WordPress && timeout 600 ddev exec bash wp-content/plugins/WP-LIT-MCP/tests/Integration/pods-compat-smoke.sh
# Temporarily activates the installed Pods plugin (restored afterwards); removes the lit_mcp_pods_* options, the
diff --git a/tests/Unit/Admin/CompatibilityTabTest.php b/tests/Unit/Admin/CompatibilityTabTest.php
index ab25d06..23f7945 100644
--- a/tests/Unit/Admin/CompatibilityTabTest.php
+++ b/tests/Unit/Admin/CompatibilityTabTest.php
@@ -157,6 +157,14 @@ final class CompatibilityTabTest extends TestCase
);
}
+ /** The part of $html from the first $from to the next $to (the markers included). */
+ private static function between(string $html, string $from, string $to): string
+ {
+ $start = (int) strpos($html, $from);
+
+ return substr($html, $start, (int) strpos($html, $to, $start) - $start + strlen($to));
+ }
+
private static function html(CompatibilityTab $tab): string
{
ob_start();
@@ -181,7 +189,166 @@ final class CompatibilityTabTest extends TestCase
self::assertInstanceOf(AdminTab::class, $tab);
self::assertSame('compatibility', $tab->slug());
- self::assertSame('Compatibility', $tab->label());
+ self::assertSame('Kompatibilität', $tab->label());
+ }
+
+ /** @param list $profiles */
+ private function tabOver(array $profiles): CompatibilityTab
+ {
+ $reports = Mockery::mock(CheckReportStore::class);
+ $reports->allows('latest')->andReturn(null);
+ $store = Mockery::mock(ReleaseStore::class);
+ $store->allows('entries')->andReturn([]);
+ $store->allows('find')->andReturn(null);
+
+ return new CompatibilityTab(
+ new Compatibility($profiles),
+ FakeChangeLog::forbidden(),
+ new AdminPostAction(static function (): void {
+ }),
+ static fn (): CheckReportStore => $reports,
+ static fn (): ReleaseStore => $store
+ );
+ }
+
+ /** A second profile; $capability keeps the capability IDs unique across profiles. */
+ private static function other(
+ string $id,
+ bool $active,
+ Tier $tier = Tier::LockOnFailure,
+ FakeCapability $capability = FakeCapability::Write
+ ): Profile {
+ return new Profile(
+ id: $id,
+ label: ucfirst($id),
+ tier: $tier,
+ capabilities: [$capability],
+ active: static fn (): bool => $active,
+ version: static fn (): ?string => '2.0.0',
+ matrix: $tier === Tier::ReleasePerVersion
+ ? static fn (): CompatibilityMatrix => CompatibilityMatrix::uniform([$capability], ReleaseState::Released, [], null)
+ : null
+ );
+ }
+
+ public function testTheOverviewListsEveryProfileAndLinksToTheSectionsOfTheActiveOnes(): void
+ {
+ $html = self::html($this->tabOver([
+ self::other('alpha', true),
+ self::other('beta', false, capability: FakeCapability::Other),
+ self::other('gamma', true, capability: FakeCapability::Read),
+ ]));
+
+ self::assertStringContainsString('Übersicht
', $html);
+ foreach (['Alpha', 'Beta', 'Gamma'] as $label) {
+ self::assertStringContainsString('data-label="Plugin">' . $label . '', $html);
+ }
+ self::assertStringContainsString('href="#lit-mcp-compat-alpha"', $html);
+ self::assertStringContainsString('href="#lit-mcp-compat-gamma"', $html);
+ self::assertStringNotContainsString('#lit-mcp-compat-beta', $html);
+ self::assertStringContainsString('id="lit-mcp-compat-alpha"', $html);
+ self::assertStringNotContainsString('id="lit-mcp-compat-beta"', $html);
+ self::assertSame(1, substr_count($html, 'nicht aktiv'));
+ // A link target in the page for every link, and the link names its plugin after the visible word.
+ self::assertStringContainsString('>Details zu Alpha', $html);
+ }
+
+ public function testTheOverviewSaysAtOnceWhetherEverythingIsFine(): void
+ {
+ $fine = self::html($this->tabOver([self::other('alpha', true)]));
+ $attention = self::html($this->tabOver([
+ self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Untested)),
+ ]));
+
+ self::assertStringContainsString('notice-success inline', $fine);
+ self::assertStringContainsString('Alles in Ordnung', $fine);
+ self::assertStringContainsString('notice-warning inline', $attention);
+ self::assertStringContainsString('1 aktives Plugin braucht deine Aufmerksamkeit.', $attention);
+ }
+
+ public function testTheOverviewTableIsAccessibleAndResponsive(): void
+ {
+ $html = self::html($this->tabOver([self::other('alpha', true)]));
+ $table = self::between($html, 'lit-mcp-compat-overview', '');
+
+ self::assertStringContainsString('', $table);
+ self::assertSame(5, substr_count($table, ''));
+ self::assertStringContainsString(' | Alpha | ', $table);
+ foreach (['Version', 'Zustand', 'Freigabe', 'Details'] as $label) {
+ self::assertStringContainsString('', $table);
+ }
+ self::assertStringContainsString('Sperrt automatisch bei Problemen', $table);
+ }
+
+ public function testTierNamesNeverReachTheScreen(): void
+ {
+ foreach ([Tier::ReleasePerVersion, Tier::LockOnFailure, Tier::ReportOnly] as $tier) {
+ $html = self::html($this->tabOver([self::other('alpha', true, $tier)]));
+
+ foreach (['LockOnFailure', 'ReleasePerVersion', 'ReportOnly'] as $raw) {
+ self::assertStringNotContainsString($raw, $html);
+ }
+ }
+ }
+
+ public function testASectionShowsStateThenActionsThenDetails(): void
+ {
+ $html = self::html($this->tab(
+ self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Untested)),
+ self::report()
+ ));
+ $section = substr($html, (int) strpos($html, 'Fake <Plugin>', $section);
+ $order = array_map(
+ static fn (string $needle): int => (int) strpos($section, $needle),
+ ['notice notice-warning inline', 'Aktionen', 'Fähigkeiten', 'Versionen', 'Letzte Prüfung']
+ );
+ $sorted = $order;
+ sort($sorted);
+ self::assertSame($sorted, $order);
+ self::assertNotContains(0, $order);
+ }
+
+ public function testStateAndNextStepAreWordsAndNeverOnlyColour(): void
+ {
+ foreach (self::states() as [$state, $word]) {
+ $html = self::html($this->tab(self::profile(Tier::ReleasePerVersion, self::matrix($state))));
+
+ // overview row, section notice and one row per capability: the word beside an icon hidden from AT
+ $expected = 2 + count(self::CAPS);
+ self::assertSame($expected, substr_count($html, '' . $word . ''));
+ self::assertSame($expected, substr_count($html, 'aria-hidden="true"> ' . $word . ''));
+ }
+ }
+
+ public function testTheFeatureTableNamesEachRowByItsCapability(): void
+ {
+ $html = self::html($this->tab(self::profile()));
+
+ self::assertStringContainsString('Fake fake-read | ', $html);
+ self::assertStringContainsString('', $html);
+ }
+
+ public function testTheDetailsSummaryNamesThePlugin(): void
+ {
+ $html = self::html($this->tab(self::profile()));
+
+ self::assertStringContainsString(
+ 'Details zu Fake <Plugin>',
+ $html
+ );
+ }
+
+ public function testProblemsStandInTheStateNoticeEscaped(): void
+ {
+ $html = self::html($this->tab(
+ self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Incompatible, ['Problem ']))
+ ));
+
+ self::assertStringContainsString('notice notice-error inline', $html);
+ self::assertStringContainsString('Problem <A>', $html);
}
public function testAnInactiveProfileIsOneLineAndNeverAsksForTheMatrix(): void
@@ -190,10 +357,13 @@ final class CompatibilityTabTest extends TestCase
// ReleasePerVersion with a matrix closure that fails the test when called.
$html = self::html($this->tab(self::profile(Tier::ReleasePerVersion, null, active: false)));
- self::assertStringContainsString('nicht installiert oder nicht aktiv', $html);
- self::assertStringContainsString('Fake <Plugin>', $html);
+ self::assertStringContainsString('nicht aktiv', $html);
+ self::assertStringContainsString('data-label="Plugin">Fake <Plugin>', $html);
+ self::assertStringContainsString('Kein unterstütztes Plugin ist aktiv', $html);
+ self::assertStringNotContainsString(' [Tier::ReleasePerVersion, 'Freigabe pro Version nötig'],
- 'lock' => [
- Tier::LockOnFailure,
- 'Freigabe wird dokumentiert, Nutzung automatisch, solange die Prüfung grün ist',
- ],
- 'report' => [Tier::ReportOnly, 'nur Anzeige'],
+ 'lock' => [Tier::LockOnFailure, 'Sperrt automatisch bei Problemen'],
+ 'report' => [Tier::ReportOnly, 'Nur Anzeige'],
];
}
@@ -289,8 +456,8 @@ final class CompatibilityTabTest extends TestCase
{
$html = self::html($this->tab(self::profile()));
- self::assertStringContainsString('Getestete Versionen', $html);
- self::assertStringContainsString('Freigegebene Versionen', $html);
+ self::assertStringContainsString('Im Plugin getestet | ', $html);
+ self::assertStringContainsString('Von dir freigegeben | ', $html);
self::assertSame(2, substr_count($html, '– | '));
}
@@ -492,7 +659,7 @@ final class CompatibilityTabTest extends TestCase
$html = self::html($this->tab(self::profile(), self::report()));
self::assertLessThan(strpos($html, 'lit-mcp-compat-details'), strpos($html, '[INTRO]'));
- self::assertGreaterThan(strpos($html, ''), strpos($html, '[EXTRA]'));
+ self::assertGreaterThan(strpos($html, ''), strpos($html, '[EXTRA]'));
}
public function testAnInactiveProfileFiresNeitherIntroNorExtra(): void
@@ -542,7 +709,7 @@ final class CompatibilityTabTest extends TestCase
$html = self::html($this->tab(self::profile()));
self::assertStringContainsString('Gut | ', $html);
- self::assertSame(3, substr_count($html, ''));
+ self::assertSame(3, substr_count(self::between($html, 'lit-mcp-compat-features', ''), ' | '));
self::assertStringContainsString(' | | ', $html);
}
@@ -567,7 +734,7 @@ final class CompatibilityTabTest extends TestCase
self::assertStringContainsString('– | ', $html);
self::assertStringContainsString(' | ', $html);
- self::assertStringContainsString('', $html);
+ self::assertStringContainsString('Versionen', $html);
self::assertStringContainsString('fake capability column "Kaputt" failed', $logged);
self::assertStringContainsString('Zelle ', $logged);
}
@@ -591,7 +758,10 @@ final class CompatibilityTabTest extends TestCase
{
Filters\expectApplied(CompatibilityTab::COLUMNS_FILTER)->once()->andReturn('kaputt');
- self::assertSame(2, substr_count(self::html($this->tab(self::profile())), ''));
+ self::assertSame(
+ 2,
+ substr_count(self::between(self::html($this->tab(self::profile())), 'lit-mcp-compat-features', ''), ' | ')
+ );
}
public function testEverythingFromThirdPartiesIsEscaped(): void
diff --git a/tests/Unit/Admin/SettingsPageTest.php b/tests/Unit/Admin/SettingsPageTest.php
index 567fdbb..0c72fa8 100644
--- a/tests/Unit/Admin/SettingsPageTest.php
+++ b/tests/Unit/Admin/SettingsPageTest.php
@@ -278,7 +278,7 @@ final class SettingsPageTest extends TestCase
static fn (array $tab): bool => $tab['id'] === 'lit-mcp-overview'
&& str_contains($tab['content'], 'Tab „Verbindung“')
&& str_contains($tab['content'], 'Tab „Tools“')
- && str_contains($tab['content'], 'Tab „Compatibility“')
+ && str_contains($tab['content'], 'Tab „Kompatibilität“')
));
(new SettingsPage([$this->tab('verbindung', 'Verbindung')]))->helpTabFor($screen);
diff --git a/tests/Unit/Integrations/Divi/Admin/DiviAdminExtrasTest.php b/tests/Unit/Integrations/Divi/Admin/DiviAdminExtrasTest.php
index 02b04d8..813711e 100644
--- a/tests/Unit/Integrations/Divi/Admin/DiviAdminExtrasTest.php
+++ b/tests/Unit/Integrations/Divi/Admin/DiviAdminExtrasTest.php
@@ -132,7 +132,7 @@ final class DiviAdminExtrasTest extends TestCase
self::assertStringContainsString('Seiten schreiben, Modulkatalog', $html);
self::assertStringContainsString('alle anderen laufen weiter', $html);
self::assertStringContainsString('page=lit-mcp&tab=compatibility', $html);
- self::assertStringContainsString('Reiter „Compatibility“', $html);
+ self::assertStringContainsString('Reiter „Kompatibilität“', $html);
}
public function testNoNoticeWhileEveryFeatureIsUsable(): void
diff --git a/tests/Unit/Integrations/Divi/Release/ReleaseStatusTest.php b/tests/Unit/Integrations/Divi/Release/ReleaseStatusTest.php
index 212b580..534e333 100644
--- a/tests/Unit/Integrations/Divi/Release/ReleaseStatusTest.php
+++ b/tests/Unit/Integrations/Divi/Release/ReleaseStatusTest.php
@@ -77,7 +77,7 @@ final class ReleaseStatusTest extends TestCase
self::assertSame(
'Achtung: Divi 5.3.0 ist auf dieser Website für „Seiten schreiben“ noch nicht für LIT MCP freigegeben. '
- . 'Das ist gesperrt, bis ein Administrator unter Einstellungen → LIT MCP → Compatibility die Prüfung ausführt '
+ . 'Das ist gesperrt, bis ein Administrator unter Einstellungen → LIT MCP → Kompatibilität die Prüfung ausführt '
. 'und die Version freigibt. Es wurde nichts geändert.',
$payload['message']
);
diff --git a/tests/Unit/PluginTest.php b/tests/Unit/PluginTest.php
index 7385510..9b2313c 100644
--- a/tests/Unit/PluginTest.php
+++ b/tests/Unit/PluginTest.php
@@ -172,8 +172,10 @@ final class PluginTest extends TestCase
self::assertInstanceOf(CompatibilityTab::class, $tab);
self::assertSame('compatibility', $tab->slug());
- self::assertStringContainsString('id="lit-mcp-compat-divi"', $html);
- self::assertStringContainsString('id="lit-mcp-compat-pods"', $html);
+ // Both plugins are inactive in this registry: one overview row each, no section.
+ self::assertStringContainsString('data-label="Plugin">Divi | ', $html);
+ self::assertStringContainsString('data-label="Plugin">Pods', $html);
+ self::assertStringNotContainsString(' | |