refactor(divi): DiviTab geht im Reiter „Compatibility“ auf

Der eigene Divi-Reiter entfällt. DiviAdminExtras behält Admin-Hinweis, Orphan-Bereinigung vor der Ausgabe (lit_mcp_compat_profile_load), Hinweis zum nächsten Schritt und zur privaten Testseite (lit_mcp_compat_profile_intro), Zustandstexte mit Quelle (lit_mcp_compat_explanation), Automatik-Notiz und Visual-Builder-Hinweis (lit_mcp_compat_profile_extra) sowie die Spalten Quelle, Probleme und Betroffene Tools über die neue, gegen fehlerhafte Callbacks abgesicherte Erweiterung lit_mcp_compat_capability_columns. Texte, Doku, CSS (lit-mcp-compat-*) und Smoke auf den neuen Reiter umgestellt, tote divi_*-Hinweise entfernt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-11-06 13:32:29 +01:00
co-authored by Claude Opus 5.5
parent 85f4138b36
commit b40b81f585
23 changed files with 1020 additions and 1284 deletions
+100 -10
View File
@@ -6,6 +6,7 @@ namespace LIT\MCP\Admin;
use Closure;
use LIT\MCP\ChangeLog\ChangeLog;
use LIT\MCP\Compatibility\Capability;
use LIT\MCP\Compatibility\CheckReport;
use LIT\MCP\Compatibility\CheckReportStore;
use LIT\MCP\Compatibility\Compatibility;
@@ -14,7 +15,9 @@ use LIT\MCP\Compatibility\Profile;
use LIT\MCP\Compatibility\ReleaseState;
use LIT\MCP\Compatibility\ReleaseStore;
use LIT\MCP\Compatibility\Tier;
use LIT\MCP\Log;
use LIT\MCP\Plugin;
use Throwable;
defined('ABSPATH') || exit;
@@ -23,14 +26,27 @@ defined('ABSPATH') || exit;
* (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. The action lit_mcp_compat_profile_extra($id) closes a section: profiles add their
* own notes there; the action lit_mcp_compat_profile_load($id) (see loadProfiles()) runs before any output.
* 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.
*/
final class CompatibilityTab implements AdminTab
{
public const SLUG = 'compatibility';
public const EXTRA_ACTION = 'lit_mcp_compat_profile_extra';
public const LOAD_ACTION = 'lit_mcp_compat_profile_load';
public const INTRO_ACTION = 'lit_mcp_compat_profile_intro';
public const EXPLANATION_FILTER = 'lit_mcp_compat_explanation';
/**
* Filter lit_mcp_compat_capability_columns(list, $profileId): a profile adds columns to its capability table.
* Each column is array{heading: string, cell: Closure(Capability, CompatibilityMatrix): list<string>,
* code?: bool}; the tab escapes the cell items (code: shown as <code>) and prints a dash for none. Columns
* without a string heading or a Closure cell are dropped; a cell that throws (logged) or does not return a
* list<string> shows a dash. The columns apply to exactly one table: a profile may cache per call (the cell
* Closures see the matrix of that table only).
*/
public const COLUMNS_FILTER = 'lit_mcp_compat_capability_columns';
/** admin-post actions; the nonce action of a request is the action plus "_<profile ID>". */
public const RELEASE_ACTION = 'lit_mcp_compat_release';
public const WITHDRAW_ACTION = 'lit_mcp_compat_withdraw';
@@ -225,7 +241,7 @@ final class CompatibilityTab implements AdminTab
esc_html__('Gesamtzustand', 'wp-lit-mcp'),
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in stateBadge().
self::stateBadge($state),
esc_html(self::explanation($state))
esc_html($this->explanation($profile, $state))
);
if ($problems !== []) {
echo '<tr><th scope="row">' . esc_html__('Probleme', 'wp-lit-mcp') . '</th><td>';
@@ -249,6 +265,7 @@ final class CompatibilityTab implements AdminTab
);
echo '</tbody></table>';
do_action(self::INTRO_ACTION, $profile->id);
$this->renderCapabilities($profile, $matrix);
$report = ($this->reports)($profile)->latest();
$this->renderForms($profile, $report);
@@ -261,8 +278,19 @@ final class CompatibilityTab implements AdminTab
private function renderCapabilities(Profile $profile, CompatibilityMatrix $matrix): void
{
$extra = $this->extraColumns($profile);
$headings = [__('Fähigkeit', 'wp-lit-mcp'), __('Zustand', 'wp-lit-mcp')];
echo '<table class="widefat striped lit-mcp-divi-features"><caption class="screen-reader-text">'
foreach ($extra as $column) {
$headings[] = $column['heading'];
}
// Open when something needs attention; a released profile keeps the long table folded.
printf(
'<details class="lit-mcp-compat-details"%s><summary>%s</summary>',
$matrix->state() === ReleaseState::Released ? '' : ' open',
esc_html__('Fähigkeiten im Detail', 'wp-lit-mcp')
);
echo '<table class="widefat striped lit-mcp-compat-features"><caption class="screen-reader-text">'
. esc_html(sprintf(
/* translators: %s: plugin name */
__('Fähigkeiten von %s', 'wp-lit-mcp'),
@@ -276,15 +304,63 @@ 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(
'<tr><td data-label="%s">%s</td><td data-label="%s">%s</td></tr>',
'<tr><td data-label="%s">%s</td><td data-label="%s">%s</td>',
esc_attr($headings[0]),
esc_html($capability->label()),
esc_attr($headings[1]),
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in stateBadge().
self::stateBadge($matrix->of($capability)->state)
);
foreach ($extra as $index => $column) {
printf('<td data-label="%s">', esc_attr($headings[$index + 2]));
self::renderList(
self::cellItems($profile, $column, $capability, $matrix),
($column['code'] ?? false) === true
);
echo '</td>';
}
echo '</tr>';
}
echo '</tbody></table>';
echo '</tbody></table></details>';
}
/** @return list<array{heading: string, cell: Closure, code?: bool}> */
private function extraColumns(Profile $profile): array
{
$columns = apply_filters(self::COLUMNS_FILTER, [], $profile->id);
return is_array($columns)
? array_values(array_filter(
$columns,
static fn (mixed $column): bool => is_array($column)
&& is_string($column['heading'] ?? null)
&& ($column['cell'] ?? null) instanceof Closure
))
: [];
}
/**
* What one cell shows: its list, or nothing (a dash) when the profile's callback throws or returns anything but a
* list of strings – a third party's column must not break the table.
*
* @param array{heading: string, cell: Closure} $column
* @return list<string>
*/
private static function cellItems(
Profile $profile,
array $column,
Capability $capability,
CompatibilityMatrix $matrix
): array {
try {
$items = ($column['cell'])($capability, $matrix);
} catch (Throwable $e) {
Log::failure(sprintf('%s capability column "%s"', $profile->id, $column['heading']), $e);
return [];
}
return is_array($items) && array_is_list($items) && array_filter($items, 'is_string') === $items ? $items : [];
}
private function renderForms(Profile $profile, ?CheckReport $report): void
@@ -371,7 +447,7 @@ final class CompatibilityTab implements AdminTab
private function renderReport(Profile $profile, CheckReport $report): void
{
printf(
'<h3>%s</h3><p>%s</p><ul class="lit-mcp-divi-report">',
'<h3>%s</h3><p>%s</p><ul class="lit-mcp-compat-report">',
esc_html__('Letzte Prüfung', 'wp-lit-mcp'),
esc_html(sprintf(
/* translators: 1: plugin version, 2: LIT MCP version, 3: date */
@@ -406,11 +482,17 @@ final class CompatibilityTab implements AdminTab
}
/** @param list<string> $items */
private static function renderList(array $items): void
private static function renderList(array $items, bool $code = false): void
{
if ($items === []) {
echo '–';
return;
}
echo '<ul>';
foreach ($items as $item) {
echo '<li>' . esc_html($item) . '</li>';
echo $code ? '<li><code>' . esc_html($item) . '</code></li>' : '<li>' . esc_html($item) . '</li>';
}
echo '</ul>';
}
@@ -445,7 +527,15 @@ final class CompatibilityTab implements AdminTab
);
}
private static function explanation(ReleaseState $state): string
private function explanation(Profile $profile, ReleaseState $state): string
{
$generic = self::genericExplanation($state);
$text = apply_filters(self::EXPLANATION_FILTER, $generic, $profile->id, $state);
return is_string($text) ? $text : $generic;
}
private static function genericExplanation(ReleaseState $state): string
{
return match ($state) {
ReleaseState::Released => __('Alle Fähigkeiten sind nutzbar.', 'wp-lit-mcp'),
-17
View File
@@ -266,23 +266,6 @@ final class SettingsPage
'revoked' => ['success', __('Verbindung widerrufen.', 'wp-lit-mcp')],
'revoke_failed' => ['error', __('Die Verbindung konnte nicht widerrufen werden.', 'wp-lit-mcp')],
'cleared' => ['success', __('Protokoll geleert.', 'wp-lit-mcp')],
'divi_released' => ['success', __('Die Divi-Version ist für LIT MCP freigegeben.', 'wp-lit-mcp')],
'divi_release_refused' => [
'error',
__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Freigabe nicht möglich: Der letzte Prüfbericht ist nicht grün oder passt nicht mehr zur installierten Divi- oder Plugin-Version. Bitte die Prüfung erneut ausführen.',
'wp-lit-mcp'
),
],
'divi_withdrawn' => [
'success',
__('Freigabe zurückgezogen; schreibende Divi-Tools sind wieder gesperrt.', 'wp-lit-mcp'),
],
'divi_withdraw_failed' => [
'error',
__('Für die installierte Divi-Version gibt es auf dieser Website keine Freigabe.', 'wp-lit-mcp'),
],
'compat_checked' => ['success', __('Prüfung abgeschlossen; das Ergebnis steht unten.', 'wp-lit-mcp')],
'compat_check_running' => [
'warning',
@@ -0,0 +1,321 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Admin;
use Closure;
use LIT\MCP\Admin\CompatibilityTab;
use LIT\MCP\Admin\SettingsPage;
use LIT\MCP\Compatibility\Capability;
use LIT\MCP\Compatibility\CheckReportStore;
use LIT\MCP\Compatibility\CompatibilityMatrix;
use LIT\MCP\Compatibility\FeatureStatus;
use LIT\MCP\Compatibility\ReleaseState;
use LIT\MCP\Integrations\Divi\Release\DiviEnvironment;
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
use LIT\MCP\Integrations\Divi\Release\WriteProbe;
defined('ABSPATH') || exit;
/**
* What Divi adds to the tab "Compatibility" (Admin\CompatibilityTab, which owns check, release and withdrawal):
* the admin notice on every admin page while a feature is unusable, the removal of check pages a dead check left
* behind (in the tab's load action, not while rendering), the columns source / problems / affected tools of the
* capability table (E3), the source-specific explanation of the overall state, above the capability table (intro)
* the orphan report, the next step and what the check does and, closing the section, the automation note and – with a
* report – the Visual Builder hint.
*/
final class DiviAdminExtras
{
/** @var list<int> Check pages removeOrphans() deleted in this request; renderIntro() reports them. */
private array $removed = [];
/**
* @param FeatureOverview|null $overview null = a table without the affected tools.
* @param (Closure(): string)|null $version Installed Divi version ('' = none); null = DiviEnvironment.
* @param CheckReportStore|null $reports null = the Divi store.
*/
public function __construct(
private ?FeatureOverview $overview = null,
private ?Closure $version = null,
private ?CheckReportStore $reports = null
) {
}
public function register(): void
{
add_action('admin_notices', [$this, 'renderNotice']);
add_action(CompatibilityTab::LOAD_ACTION, [$this, 'removeOrphans']);
add_action(CompatibilityTab::INTRO_ACTION, [$this, 'renderIntro']);
add_action(CompatibilityTab::EXTRA_ACTION, [$this, 'renderExtras']);
add_filter(CompatibilityTab::EXPLANATION_FILTER, [$this, 'explanation'], 10, 3);
add_filter(CompatibilityTab::COLUMNS_FILTER, [$this, 'columns'], 10, 2);
}
/**
* Only for administrators, only with Divi active, only while a feature is not usable – and not on the tab
* "Compatibility", which shows the same table. Reads the cached matrix (D7), so no check runs on an admin page.
*/
public function renderNotice(): void
{
if (! current_user_can('manage_options') || $this->version() === '' || $this->onCompatibilityTab()) {
return;
}
$unusable = ReleaseGate::status()->matrix->unusable();
if ($unusable === []) {
return;
}
printf(
'<div class="notice notice-warning"><p>%s <a href="%s">%s</a></p></div>',
esc_html(sprintf(
/* 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 Tools lehnen ab, alle anderen laufen weiter.',
'wp-lit-mcp'
),
$this->version(),
implode(', ', array_map(
static fn (FeatureStatus $status): string => $status->feature->label(),
$unusable
))
)),
esc_url(SettingsPage::url(CompatibilityTab::SLUG)),
esc_html__('Details im Reiter „Compatibility“', 'wp-lit-mcp')
);
}
private function onCompatibilityTab(): bool
{
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab switch.
$tab = is_string($_GET['tab'] ?? null) ? sanitize_key(wp_unslash($_GET['tab'])) : '';
return is_object($screen) && $screen->id === SettingsPage::SCREEN_HOOK && $tab === CompatibilityTab::SLUG;
}
/**
* lit_mcp_compat_profile_load('divi'), before any output (the tab fires it for administrators on its own tab):
* deletes the check pages a dead check left behind (WriteProbe::removeOrphans(): marked drafts with the check
* title, older than ten minutes – no running check uses them). On a GET without nonce on purpose: it removes
* only the plugin's own leftovers and is idempotent. renderExtras() only reports what was deleted (renderIntro()).
*/
public function removeOrphans(string $profileId): void
{
if ($profileId !== DiviProfile::ID || ! current_user_can('manage_options') || $this->version() === '') {
return;
}
$this->removed = WriteProbe::removeOrphans();
}
/**
* Columns of Divi's capability table: where the release comes from, the problems of the capability and the
* tools it affects. Reads the overview once per table (the matrix is the same for every row).
*
* @param list<array<string, mixed>> $columns
* @return list<array<string, mixed>>
*/
public function columns(array $columns, string $profileId): array
{
if ($profileId !== DiviProfile::ID) {
return $columns;
}
$rows = null;
$row = function (Capability $capability, CompatibilityMatrix $matrix) use (&$rows): array {
$rows ??= array_column(($this->overview ?? FeatureOverview::withoutTools())->rows($matrix), null, 'id');
return $rows[$capability->id()] ?? [];
};
return [
...$columns,
[
'heading' => __('Quelle', 'wp-lit-mcp'),
'cell' => fn (Capability $c, CompatibilityMatrix $m): array => array_values(array_filter(
[$this->sourceLabel($row($c, $m)['source'] ?? null)]
)),
],
[
'heading' => __('Probleme', 'wp-lit-mcp'),
'cell' => static fn (Capability $c, CompatibilityMatrix $m): array => $row($c, $m)['problems'] ?? [],
],
[
'heading' => __('Betroffene Tools', 'wp-lit-mcp'),
'cell' => static fn (Capability $c, CompatibilityMatrix $m): array => $row($c, $m)['tools'] ?? [],
'code' => true,
],
];
}
private function sourceLabel(?string $source): string
{
return match ($source) {
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 => '',
};
}
/**
* lit_mcp_compat_profile_intro('divi'), above the capability table: the deleted check pages, the next step and
* what the check does (it writes to the site, so it says so).
*/
public function renderIntro(string $profileId): void
{
if ($profileId !== DiviProfile::ID) {
return;
}
if ($this->removed !== []) {
printf(
'<div class="notice notice-warning inline"><p>%s</p></div>',
esc_html(sprintf(
/* translators: %s: comma separated page IDs */
__('Liegengebliebene Prüfseiten gelöscht: %s', 'wp-lit-mcp'),
implode(', ', $this->removed)
))
);
}
$this->renderNextStep(ReleaseGate::status());
echo '<p class="description">' . esc_html__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Die Prüfung legt eine private Testseite an, prüft Lesen und Schreiben und löscht sie wieder.',
'wp-lit-mcp'
) . '</p>';
}
/**
* Filter lit_mcp_compat_explanation: Divi says where its release comes from (plugin, automatic, this website)
* instead of the generic sentence, so the section does not explain the state twice.
*/
public function explanation(string $text, string $profileId, ReleaseState $state): string
{
if ($profileId !== DiviProfile::ID) {
return $text;
}
$status = ReleaseGate::status();
return match (true) {
$status->state === ReleaseState::Incompatible => __(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Divi hat Stellen geändert, die LIT MCP nutzt. Die betroffenen Fähigkeiten (Tabelle unten) bleiben gesperrt, bis ein Plugin-Update von LIT MCP eingespielt ist; alle anderen laufen weiter.',
'wp-lit-mcp'
),
$status->state === ReleaseState::Untested => __(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'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'
),
$status->source === ReleaseStatus::SOURCE_CODE => __(
'Diese Version ist im Plugin als getestet hinterlegt.',
'wp-lit-mcp'
),
$status->source === ReleaseStatus::SOURCE_AUTO => __(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Automatisch freigegeben: Alle Divi-Stellen, die LIT MCP nutzt, stimmen mit der geprüften Referenz überein.',
'wp-lit-mcp'
),
default => $this->releasedOnThisSite($status),
};
}
private function releasedOnThisSite(ReleaseStatus $status): string
{
$user = get_userdata((int) ($status->release['user_id'] ?? 0));
$releasedAt = (int) strtotime((string) ($status->release['released_at'] ?? ''));
return sprintf(
/* translators: 1: date, 2: user name */
__('Auf dieser Website freigegeben am %1$s von %2$s.', 'wp-lit-mcp'),
date_i18n((string) get_option('date_format'), $releasedAt),
is_object($user) && isset($user->display_name) ? (string) $user->display_name : '?'
);
}
/** lit_mcp_compat_profile_extra('divi'): the end of the Divi section (shown only while Divi is active). */
public function renderExtras(string $profileId): void
{
if ($profileId !== DiviProfile::ID) {
return;
}
$this->renderAutomationNote();
$reports = $this->reports ?? new CheckReportStore(DiviProfile::ID, DiviProfile::REPORT_VERSION_KEY);
if ($reports->latest() !== null) {
echo '<p class="description">' . esc_html__(
'Optional: eine Divi-Seite im Visual Builder öffnen und speichern.',
'wp-lit-mcp'
) . '</p>';
}
}
/** One sentence on what to do now; a released version needs none. */
private function renderNextStep(ReleaseStatus $status): void
{
$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 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)
);
}
/** E1: what the automatic release does and how to switch it off. */
private function renderAutomationNote(): void
{
echo '<p class="description">' . 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.',
'wp-lit-mcp'
) . '</p>';
echo '<p class="description">' . sprintf(
/* translators: %s: filter name in a code element */
esc_html__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Die Automatik abschalten: Filter %s auf false setzen (z. B. in einem mu-Plugin). Dann gelten nicht hinterlegte Divi-Versionen bis zur Freigabe als ungetestet.',
'wp-lit-mcp'
),
'<code>lit_mcp_divi_auto_release</code>'
) . '</p>';
}
private function version(): string
{
return ($this->version ?? static fn (): string => DiviEnvironment::version() ?? '')();
}
}
-583
View File
@@ -1,583 +0,0 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Admin;
use Closure;
use LIT\MCP\Admin\AdminPostAction;
use LIT\MCP\Admin\AdminTab;
use LIT\MCP\Admin\CheckRoute;
use LIT\MCP\Admin\SettingsPage;
use LIT\MCP\ChangeLog\ChangeLog;
use LIT\MCP\Compatibility\ReleaseState;
use LIT\MCP\Compatibility\CheckReport;
use LIT\MCP\Compatibility\CheckReportStore;
use LIT\MCP\Integrations\Divi\Release\DiviCheckSteps;
use LIT\MCP\Integrations\Divi\Release\DiviProfile;
use LIT\MCP\Integrations\Divi\Release\DiviEnvironment;
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
use LIT\MCP\Compatibility\FeatureStatus;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
use LIT\MCP\Compatibility\ReleaseStore;
use LIT\MCP\Integrations\Divi\Release\WriteProbe;
use LIT\MCP\Plugin;
defined('ABSPATH') || exit;
/**
* Tab "Divi" (spec §6): release status of the installed Divi, the table of its features (state, source, problems,
* affected tools – E3), the latest check report, "Prüfung starten" (REST, Admin\CheckRoute), "Divi <Version>
* freigeben" (only after a fresh green report) and "Freigabe zurückziehen" (whenever a database release applies,
* whatever the overall state). Loading the tab removes check pages a dead check left behind – in the page's load
* hook (removeOrphans()), not while rendering. renderNotice() is the admin notice on every admin page while a
* feature is unusable.
*/
final class DiviTab implements AdminTab
{
public const SLUG = 'divi';
public const RELEASE_ACTION = 'lit_mcp_divi_release';
public const WITHDRAW_ACTION = 'lit_mcp_divi_withdraw';
/** @var list<int> Check pages removeOrphans() deleted in this request; render() reports them. */
private array $removed = [];
/**
* @param (Closure(): string)|null $version Installed Divi version ('' = none); null = DiviEnvironment.
* @param (Closure(string): string)|null $stateKey null = ReleaseResolver::stateKey().
* @param FeatureOverview|null $overview null = a table without the affected tools.
*/
public function __construct(
private ?CheckReportStore $reports = null,
private ?ReleaseStore $releases = null,
private ?ChangeLog $log = null,
private ?AdminPostAction $action = null,
private ?Closure $version = null,
private ?Closure $stateKey = null,
private ?FeatureOverview $overview = null
) {
}
public function slug(): string
{
return self::SLUG;
}
public function label(): string
{
return __('Divi', 'wp-lit-mcp');
}
public function registerSettings(): void
{
}
public function registerActions(): void
{
add_action('admin_post_' . self::RELEASE_ACTION, [$this, 'release']);
add_action('admin_post_' . self::WITHDRAW_ACTION, [$this, 'withdraw']);
add_action('load-' . SettingsPage::SCREEN_HOOK, [$this, 'removeOrphans']);
$this->registerNotice();
}
/** The admin notice for administrators while a Divi feature is unusable (registerActions() runs in admin only). */
public function registerNotice(): void
{
add_action('admin_notices', [$this, 'renderNotice']);
}
/**
* Only for administrators, only with Divi active, only while a feature is not usable – and not on the Divi tab,
* which shows the same table. Reads the cached matrix (D7), so no check runs on an ordinary admin page.
*/
public function renderNotice(): void
{
if (! current_user_can('manage_options') || $this->version() === '' || $this->onDiviTab()) {
return;
}
$unusable = ReleaseGate::status()->matrix->unusable();
if ($unusable === []) {
return;
}
printf(
'<div class="notice notice-warning"><p>%s <a href="%s">%s</a></p></div>',
esc_html(sprintf(
/* 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 Tools lehnen ab, alle anderen laufen weiter.',
'wp-lit-mcp'
),
$this->version(),
implode(', ', array_map(
static fn (FeatureStatus $status): string => $status->feature->label(),
$unusable
))
)),
esc_url(SettingsPage::url(self::SLUG)),
esc_html__('Details im Reiter „Divi“', 'wp-lit-mcp')
);
}
private function onDiviTab(): bool
{
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
return is_object($screen) && $screen->id === SettingsPage::SCREEN_HOOK && $this->requested();
}
/**
* load-<page> of the settings page, before any output: on this tab, for manage_options and with Divi active,
* deletes the check pages a dead check left behind (WriteProbe::removeOrphans(): marked drafts with the check
* title, older than ten minutes – no running check uses them). On a GET without nonce on purpose: it removes
* only the plugin's own leftovers and is idempotent. render() only reports what was deleted.
*/
public function removeOrphans(): void
{
if (! $this->requested() || ! current_user_can('manage_options') || $this->version() === '') {
return;
}
$this->removed = WriteProbe::removeOrphans();
}
public function release(): void
{
$action = $this->action ?? new AdminPostAction();
$action->guard();
check_admin_referer(self::RELEASE_ACTION);
$version = $this->version();
$report = $this->reports()->latest();
$fresh = $report !== null
&& $report->freshFor($version, Plugin::VERSION, $this->stateKey($version));
if ($version === '' || ! $fresh) {
$action->redirect(self::SLUG, 'divi_release_refused');
return;
}
$this->releases()->add($version, Plugin::VERSION, get_current_user_id(), $report->id);
ReleaseGate::invalidate();
$this->log?->record(
'lit/divi-release',
'divi',
$version,
sprintf('Divi %s für LIT MCP %s freigegeben (Prüfbericht %s).', $version, Plugin::VERSION, $report->id)
);
$action->redirect(self::SLUG, 'divi_released');
}
public function withdraw(): void
{
$action = $this->action ?? new AdminPostAction();
$action->guard();
check_admin_referer(self::WITHDRAW_ACTION);
$version = $this->version();
if (! $this->releases()->withdraw($version, Plugin::VERSION)) {
$action->redirect(self::SLUG, 'divi_withdraw_failed');
return;
}
ReleaseGate::invalidate();
$this->log?->record(
'lit/divi-release',
'divi',
$version,
sprintf('Freigabe von Divi %s zurückgezogen.', $version)
);
$action->redirect(self::SLUG, 'divi_withdrawn');
}
public function render(): void
{
if ($this->version() === '') {
echo '<p>' . esc_html__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text.
'Divi ist auf dieser Website nicht aktiv; die Divi-Freigabe ist ohne Bedeutung.',
'wp-lit-mcp'
) . '</p>';
return;
}
$status = ReleaseGate::status();
$report = $this->reports()->latest();
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 Tools ab, die sie brauchen; alle anderen laufen weiter.',
'wp-lit-mcp'
) . '</p>';
if ($this->removed !== []) {
printf(
'<div class="notice notice-warning inline"><p>%s</p></div>',
esc_html(sprintf(
/* translators: %s: comma separated page IDs */
__('Liegengebliebene Prüfseiten gelöscht: %s', 'wp-lit-mcp'),
implode(', ', $this->removed)
))
);
}
$this->renderNextStep($status);
$this->renderStatus($status);
$this->renderFeatures($status);
$this->renderForms($status, $report);
if ($report !== null) {
$this->renderReport($report);
}
}
/** 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>';
printf(
'<tr><th scope="row">%s</th><td>%s</td></tr>',
esc_html__('Divi-Version', 'wp-lit-mcp'),
esc_html($status->version === '' ? __('nicht erkannt', 'wp-lit-mcp') : $status->version)
);
printf(
'<tr><th scope="row">%s</th><td>%s – %s</td></tr>',
esc_html__('Gesamtzustand', 'wp-lit-mcp'),
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in stateBadge().
$this->stateBadge($status->state),
esc_html($this->explanation($status))
);
if ($status->problems !== []) {
echo '<tr><th scope="row">' . esc_html__('Geändert in Divi', 'wp-lit-mcp') . '</th><td><ul>';
foreach ($status->problems as $problem) {
echo '<li>' . esc_html($problem) . '</li>';
}
echo '</ul></td></tr>';
}
echo '</tbody></table>';
}
private function renderFeatures(ReleaseStatus $status): void
{
// 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 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) {
// 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);
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 */
private function renderList(array $items, bool $code): void
{
if ($items === []) {
echo '–';
return;
}
echo '<ul>';
foreach ($items as $item) {
echo $code ? '<li><code>' . esc_html($item) . '</code></li>' : '<li>' . esc_html($item) . '</li>';
}
echo '</ul>';
}
private function sourceLabel(?string $source): string
{
return match ($source) {
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 => '–',
};
}
/** E1: what the automatic release does and how to switch it off. */
private function renderAutomationNote(): void
{
echo '<p class="description">' . 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.',
'wp-lit-mcp'
) . '</p>';
echo '<p class="description">' . sprintf(
/* translators: %s: filter name in a code element */
esc_html__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Die Automatik abschalten: Filter %s auf false setzen (z. B. in einem mu-Plugin). Dann gelten nicht hinterlegte Divi-Versionen bis zur Freigabe unten als ungetestet.',
'wp-lit-mcp'
),
'<code>lit_mcp_divi_auto_release</code>'
) . '</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) {
ReleaseState::Released => __('freigegeben', 'wp-lit-mcp'),
ReleaseState::Untested => __('ungetestet', 'wp-lit-mcp'),
ReleaseState::Incompatible => __('inkompatibel', 'wp-lit-mcp'),
};
}
private function explanation(ReleaseStatus $status): string
{
if ($status->state === ReleaseState::Incompatible) {
return __(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Divi hat Stellen geändert, die LIT MCP nutzt. Die betroffenen Fähigkeiten (Tabelle unten) bleiben gesperrt, bis ein Plugin-Update von LIT MCP eingespielt ist; alle anderen laufen weiter.',
'wp-lit-mcp'
);
}
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 Tools der ungetesteten Fähigkeiten (Tabelle unten) lehnen ab, bis die Version freigegeben ist; alle anderen laufen weiter.',
'wp-lit-mcp'
);
}
if ($status->source === ReleaseStatus::SOURCE_CODE) {
return __('Diese Version ist im Plugin als getestet hinterlegt.', 'wp-lit-mcp');
}
if ($status->source === ReleaseStatus::SOURCE_AUTO) {
return __(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Automatisch freigegeben: Alle Divi-Stellen, die LIT MCP nutzt, stimmen mit der geprüften Referenz überein.',
'wp-lit-mcp'
);
}
$user = get_userdata((int) ($status->release['user_id'] ?? 0));
$releasedAt = (int) strtotime((string) ($status->release['released_at'] ?? ''));
return sprintf(
/* translators: 1: date, 2: user name */
__('Auf dieser Website freigegeben am %1$s von %2$s.', 'wp-lit-mcp'),
date_i18n((string) get_option('date_format'), $releasedAt),
is_object($user) && isset($user->display_name) ? (string) $user->display_name : '?'
);
}
private function renderForms(ReleaseStatus $status, ?CheckReport $report): void
{
$fresh = $report !== null
&& $report->freshFor($status->version, Plugin::VERSION, $this->stateKey($status->version));
$untested = $status->state === ReleaseState::Untested && $status->version !== '';
printf('<form method="post" action="%s">', esc_url(CheckRoute::url(DiviProfile::ID)));
printf('<input type="hidden" name="_wpnonce" value="%s">', esc_attr(wp_create_nonce('wp_rest')));
// The check is the one thing to do while untested without a usable report; otherwise the release leads.
submit_button(
__('Prüfung starten', 'wp-lit-mcp'),
$untested && ! $fresh ? 'primary' : 'secondary',
'lit-mcp-divi-check',
false
);
echo '<p class="description">' . esc_html__(
// phpcs:ignore Generic.Files.LineLength.TooLong -- WordPress.WP.I18n forbids splitting the text argument.
'Die Prüfung legt eine private Testseite an, prüft Lesen und Schreiben und löscht sie wieder. Sie dauert einige Sekunden.',
'wp-lit-mcp'
) . '</p>';
echo '</form>';
if ($untested) {
if ($fresh) {
$this->actionForm(
self::RELEASE_ACTION,
/* translators: %s: Divi version */
sprintf(__('Divi %s freigeben', 'wp-lit-mcp'), $status->version),
'primary'
);
} else {
echo '<p>' . 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 Divi-Version – Prüfung erneut ausführen.',
'wp-lit-mcp'
) . '</p>';
}
}
// A database release applies whatever the overall state: one incompatible feature must not hide the way back.
if ($status->source === ReleaseStatus::SOURCE_DATABASE || $status->release !== null) {
$this->actionForm(self::WITHDRAW_ACTION, __('Freigabe zurückziehen', 'wp-lit-mcp'), 'secondary');
}
}
private function actionForm(string $action, string $label, string $type): void
{
printf('<form method="post" action="%s">', esc_url(admin_url('admin-post.php')));
printf('<input type="hidden" name="action" value="%s">', esc_attr($action));
wp_nonce_field($action);
submit_button($label, $type, 'submit', false);
echo '</form>';
}
private function renderReport(CheckReport $report): void
{
printf(
'<h3>%s</h3><p>%s</p><ul class="lit-mcp-divi-report">',
esc_html__('Letzte Prüfung', 'wp-lit-mcp'),
esc_html(sprintf(
/* translators: 1: Divi version, 2: plugin version, 3: date */
__('Divi %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))
))
);
foreach ($report->steps as $step) {
[$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(DiviCheckSteps::label($step))
);
if ($step->skipped) {
echo ' (' . esc_html__('nicht ausgeführt', 'wp-lit-mcp') . ')';
}
if ($step->problems !== []) {
echo '<ul>';
foreach ($step->problems as $problem) {
echo '<li>' . esc_html($problem) . '</li>';
}
echo '</ul>';
}
echo '</li>';
}
echo '</ul><p class="description">' . esc_html__(
'Optional: eine Divi-Seite im Visual Builder öffnen und speichern.',
'wp-lit-mcp'
) . '</p>';
}
/** Whether this request opens this tab of the settings page (the load hook fires for every tab). */
private function requested(): bool
{
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab switch.
$tab = isset($_GET['tab']) ? sanitize_key(wp_unslash((string) $_GET['tab'])) : '';
return $tab === self::SLUG;
}
private function version(): string
{
return ($this->version ?? static fn (): string => DiviEnvironment::version() ?? '')();
}
private function stateKey(string $version): string
{
return ($this->stateKey ?? static fn (string $v): string => ReleaseResolver::stateKey($v))($version);
}
private function releases(): ReleaseStore
{
return $this->releases ?? new ReleaseStore(DiviProfile::ID, DiviProfile::REPORT_VERSION_KEY);
}
private function reports(): CheckReportStore
{
return $this->reports ?? new CheckReportStore(DiviProfile::ID, DiviProfile::REPORT_VERSION_KEY);
}
}
@@ -72,8 +72,8 @@ final class DiviCheckCommand
}
/**
* Prüft die installierte Divi-Version wie der Reiter „Divi“: Manifeste, Fingerabdrücke, Modulkatalog, Format
* und Schreibtest auf einer Wegwerfseite. Gibt danach den Zustand je Fähigkeit aus und gibt nichts frei.
* Prüft die installierte Divi-Version wie der Reiter „Compatibility“: 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).
*
+6 -9
View File
@@ -8,13 +8,12 @@ use Closure;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\ToolAvailability;
use LIT\MCP\Admin\CheckRoute;
use LIT\MCP\Admin\SettingsPage;
use LIT\MCP\ChangeLog\ChangeLog;
use LIT\MCP\Compatibility\CompatibilityCheck;
use LIT\MCP\Compatibility\Profile;
use LIT\MCP\Compatibility\ProvidesCompatibility;
use LIT\MCP\Integrations\Bootable;
use LIT\MCP\Integrations\Divi\Admin\DiviTab;
use LIT\MCP\Integrations\Divi\Admin\DiviAdminExtras;
use LIT\MCP\Integrations\Divi\Cli\DiviCheckCommand;
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
use LIT\MCP\Integrations\Divi\Design\ColorRestorePlanner;
@@ -227,7 +226,7 @@ final class DiviIntegration implements Integration, Bootable, ProvidesCompatibil
return [new DiviLogoUsage()];
}
/** Which tools each Divi feature affects – for lit-site-status and the Divi tab (E3). */
/** Which tools each Divi feature affects – for lit-site-status and the tab "Compatibility" (E3). */
public function featureOverview(): FeatureOverview
{
return new FeatureOverview($this->allAbilities ?? static fn (): array => []);
@@ -250,8 +249,8 @@ final class DiviIntegration implements Integration, Bootable, ProvidesCompatibil
/**
* Divi's own boot steps, registered whether or not Divi is loaded yet (Bootable): a theme update checks the
* fingerprints again at once (spec §5.4), the admin check as REST route and WP-CLI command (spec §6, §7) and
* the tab "Divi" after the plugin's own tabs.
* fingerprints again at once (spec §5.4), the WP-CLI command (spec §7) and the Divi additions to the admin tab
* "Compatibility" (spec §6; the check route itself is generic, Admin\CheckRoute).
*/
public function boot(): void
{
@@ -273,10 +272,8 @@ final class DiviIntegration implements Integration, Bootable, ProvidesCompatibil
$profile = $this->profile();
DiviCheckCommand::register(static fn (): CompatibilityCheck => CompatibilityCheck::forProfile($profile));
add_filter(
SettingsPage::TABS_FILTER,
fn (array $tabs): array => [...$tabs, new DiviTab(log: $this->log, overview: $this->featureOverview())]
);
// No tab of its own: notice, orphan cleanup, columns and notes join the tab "Compatibility".
(new DiviAdminExtras($this->featureOverview()))->register();
}
public function abilities(): array
@@ -8,7 +8,8 @@ defined('ABSPATH') || exit;
/**
* Divi versions checked with this plugin version (spec §5.3, source "code"): add a version after
* wp lit-mcp divi-check ran green against it. Any other version needs a release in the Divi tab of each site.
* wp lit-mcp divi-check ran green against it. Any other version needs a release in the tab "Compatibility" of each
* site.
*/
final class DiviReleases
{
@@ -22,7 +22,7 @@ final class FeatureOverview
{
}
/** An overview that knows no tools (tests, and a Divi tab built without a registry). */
/** An overview that knows no tools (tests, and an admin tab built without a registry). */
public static function withoutTools(): self
{
return new self(static fn (): array => []);
@@ -24,8 +24,8 @@ 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 → Divi die Prüfung '
. 'ausführt und die Version freigibt. Es wurde nichts geändert.';
. 'freigegeben. Das ist gesperrt, bis ein Administrator unter Einstellungen → LIT MCP → Compatibility 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.';
private const FEATURE_INCOMPATIBLE = '%1$s hat Stellen geändert, die LIT MCP für %2$s nutzt. Das ist gesperrt, '
+2 -2
View File
@@ -176,8 +176,8 @@ final class Plugin
Limits $limits,
ToolAvailability $tools
): IntegrationRegistry {
// E3: lit-site-status and the Divi tab name the tools each feature affects; the registry exists only after
// the Divi integration, so it is asked at call time.
// E3: lit-site-status and the tab "Compatibility" name the tools each feature affects; the registry exists
// only after the Divi integration, so it is asked at call time.
$registry = null;
$allAbilities = static function () use (&$registry): array {
return $registry instanceof IntegrationRegistry ? $registry->abilities() : [];