[ 'right' => 'Bottom right', 'left' => 'Bottom left', ], 'launcher_style' => [ 'icon' => 'Round button with an icon', 'label' => 'Button with text', ], 'assistant_apply' => [ 'everyone' => 'Anyone who can use the assistant applies their own changes', 'admins' => 'Administrators apply; everyone else sends a request', ], 'widget_mode' => [ 'light' => 'Light', 'dark' => 'Dark, for dark sites', ], 'server_scope' => [ 'all' => 'Everywhere: the REST API, admin-ajax and page loads', 'rest' => 'The REST API only', 'off' => 'Off: page fixes only', ], 'widget_audience' => [ 'capability' => 'Only people with the capability below', 'logged_in' => 'Anyone who is signed in', 'everyone' => 'Every visitor', ], ]; private const PREVIEW_SCRIPT = <<<'JS' (function () { var preview = document.querySelector('[data-sarcio-preview]'); if (!preview) return; var button = preview.querySelector('.sarcio-preview-button'); var path = button.querySelector('path'); var label = button.querySelector('.sarcio-preview-label'); var field = function (key) { return document.querySelector('[name="sarcio_settings[' + key + ']"]'); }; var update = function () { var style = field('launcher_style'); var text = field('launcher_label'); var color = field('launcher_color'); var position = field('launcher_position'); var mode = field('widget_mode'); var icon = document.querySelector('[name="sarcio_settings[launcher_icon]"]:checked'); var isLabel = style && style.value === 'label'; button.classList.toggle('is-label', isLabel); button.classList.toggle('is-left', !!position && position.value === 'left'); preview.classList.toggle('is-dark', !!mode && mode.value === 'dark'); label.hidden = !isLabel; label.textContent = text && text.value.trim() !== '' ? text.value.trim().slice(0, 40) : 'Report a bug'; if (color) button.style.background = color.value; if (icon && window.sarcioIcons[icon.value]) path.setAttribute('d', window.sarcioIcons[icon.value]); }; document.addEventListener('input', update); document.addEventListener('change', update); })(); JS; public function __construct(private readonly Plugin $plugin) { } public function register(): void { add_action('admin_init', [$this, 'registerSettings']); add_action('admin_menu', [$this, 'registerMenu']); add_action('admin_post_' . self::CHECK_ACTION, [$this, 'checkNow']); add_action('admin_enqueue_scripts', [$this, 'enqueueStyles']); add_filter('plugin_action_links_' . plugin_basename($this->plugin->pluginFile()), [$this, 'actionLinks']); } /** @param array $links */ public function actionLinks(array $links): array { array_unshift($links, sprintf('%s', esc_url(self::url()), esc_html__('Settings', 'sarcio'))); return $links; } public function registerMenu(): void { add_options_page( __('Sarcio', 'sarcio'), __('Sarcio', 'sarcio'), 'manage_options', self::SLUG, [$this, 'render'], ); } public function enqueueStyles(string $hook): void { if ($hook !== 'settings_page_' . self::SLUG) { return; } $icons = []; foreach (Launcher::ICONS as $key => $path) { $icons[$key] = $path !== '' ? $path : self::BUBBLE_PATH; } wp_register_script('sarcio-settings', false, [], SARCIO_WP_VERSION, true); wp_enqueue_script('sarcio-settings'); wp_add_inline_script('sarcio-settings', 'window.sarcioIcons=' . InlineJson::encode($icons) . ';' . self::PREVIEW_SCRIPT); wp_add_inline_style('common', implode('', [ '.sarcio-connect-form{display:inline-block;margin:0}', '.sarcio-status{max-width:48rem;margin:1rem 0 1.5rem;padding:1rem 1.25rem}', '.sarcio-status h2{margin:0 0 .5rem;font-size:1.2em}', '.sarcio-status .sarcio-dot{display:inline-block;width:.6em;height:.6em;border-radius:50%;margin-right:.45em;vertical-align:.08em;background:#8c8f94}', '.sarcio-status.is-ok .sarcio-dot{background:#00a32a}', '.sarcio-status.is-warning .sarcio-dot{background:#dba617}', '.sarcio-status.is-error .sarcio-dot{background:#d63638}', '.sarcio-status ul{margin:.25rem 0 0}', '.sarcio-status form{margin-top:.75rem}', '.sarcio-connect{margin-top:1rem}', '.sarcio-connect-new{margin:1em 0}', '.sarcio-connect .button-link{vertical-align:baseline}', '.sarcio-usage{margin-top:1rem;padding-top:.75rem;border-top:1px solid #f0f0f1}', '.sarcio-usage h3{margin:0 0 .5rem;font-size:1em}', '.sarcio-usage p{margin:.25rem 0}', '.sarcio-usage progress{width:12rem;vertical-align:middle;margin-left:.5rem}', '.sarcio-usage.is-near h3,.sarcio-usage.is-near strong{color:#996800}', '.sarcio-usage.is-reached h3,.sarcio-usage.is-reached strong{color:#b32d2e}', '.sarcio-look{display:flex;flex-wrap:wrap;gap:2rem;align-items:flex-start}', '.sarcio-look .form-table{max-width:42rem;margin-top:0}', '.sarcio-look select{min-width:17rem}', '.sarcio-icons{display:flex;flex-wrap:wrap;gap:.5rem}', '.sarcio-icons label{cursor:pointer}', '.sarcio-icons input{position:absolute;opacity:0}', '.sarcio-icon-swatch{display:flex;align-items:center;justify-content:center;width:40px;height:40px;border:2px solid #dcdcde;border-radius:8px;background:#fff}', '.sarcio-icon-swatch svg{width:22px;height:22px;fill:#1d2327}', '.sarcio-icons input:checked+.sarcio-icon-swatch{border-color:#2271b1;box-shadow:0 0 0 1px #2271b1}', '.sarcio-icons input:focus-visible+.sarcio-icon-swatch{outline:2px solid #2271b1;outline-offset:2px}', '.sarcio-preview{position:relative;width:22rem;height:13rem;border:1px solid #dcdcde;border-radius:8px;background:linear-gradient(#f6f7f7,#fff);overflow:hidden}', '.sarcio-preview.is-dark{background:linear-gradient(#1d2327,#2c3338)}', '.sarcio-preview-caption{position:absolute;top:.75rem;left:1rem;margin:0;color:#646970}', '.sarcio-preview-button{position:absolute;bottom:14px;right:14px;display:flex;align-items:center;justify-content:center;gap:8px;width:56px;height:56px;border-radius:50%;color:#fff;font:600 14px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;box-shadow:0 4px 8px rgba(27,36,48,.1),0 8px 24px rgba(27,36,48,.16)}', '.sarcio-preview-button svg{width:26px;height:26px;fill:#fff}', '.sarcio-preview-button.is-label{width:auto;height:48px;padding:0 20px 0 16px;border-radius:999px}', '.sarcio-preview-button.is-label svg{width:22px;height:22px}', '.sarcio-preview-button.is-left{right:auto;left:14px}', '.sarcio-advanced{max-width:60rem;margin-top:1.5rem;border-top:1px solid #c3c4c7;padding-top:1rem}', '.sarcio-advanced>summary{cursor:pointer;font-size:1.1em;font-weight:600}', '.sarcio-advanced h3{margin-top:1.75rem}', ])); } public function registerSettings(): void { register_setting(self::GROUP, Options::OPTION, [ 'sanitize_callback' => [Options::class, 'sanitize'], 'type' => 'array', ]); $sections = [ 'sarcio_connect' => __('Connect this site', 'sarcio'), 'sarcio_look' => __('How the reporter looks', 'sarcio'), 'sarcio_reporter' => __('Bug reporter', 'sarcio'), 'sarcio_assistant' => __('Assistant', 'sarcio'), 'sarcio_server' => __('Server-side fixes', 'sarcio'), 'sarcio_sidecar' => __('Sarcio sidecar (for developers)', 'sarcio'), ]; foreach ($sections as $id => $title) { add_settings_section($id, $title, '__return_null', self::SLUG); } $fields = [ ['site_key', __('Site key', 'sarcio'), 'sarcio_connect', 'text', __('Starts with pk_. In your Sarcio dashboard, open Sites and copy the site key.', 'sarcio'), 'sites'], ['sidecar_key', __('Secret key', 'sarcio'), 'sarcio_connect', 'password', __('Starts with sk_. In your Sarcio dashboard, open Sites → Manage → Sidecar keys. It lets this site fetch its server-side fixes, and it never leaves your server. Leave blank to keep the saved key; type clear to remove it.', 'sarcio'), 'keys'], ['launcher_style', __('Button', 'sarcio'), 'sarcio_look', 'select', ''], ['launcher_label', __('Button text', 'sarcio'), 'sarcio_look', 'text', __('Shown on the button with text. Up to 40 characters.', 'sarcio')], ['launcher_icon', __('Icon', 'sarcio'), 'sarcio_look', 'icons', ''], ['launcher_color', __('Color', 'sarcio'), 'sarcio_look', 'color', __('Match your brand. The default is Sarcio green.', 'sarcio')], ['launcher_position', __('Position', 'sarcio'), 'sarcio_look', 'select', ''], ['widget_mode', __('Report panel', 'sarcio'), 'sarcio_look', 'select', __('Pick dark if your site is dark.', 'sarcio')], ['widget_enabled', __('Show the bug reporter', 'sarcio'), 'sarcio_reporter', 'checkbox', __('Adds the “Report a bug” button, and applies approved page fixes for every visitor.', 'sarcio')], ['widget_audience', __('Who can report bugs', 'sarcio'), 'sarcio_reporter', 'select', __('Page fixes apply to every visitor whatever you choose here.', 'sarcio')], ['widget_capability', __('Required capability', 'sarcio'), 'sarcio_reporter', 'text', __('Used by the first choice above. The default, edit_posts, means editors, authors and admins.', 'sarcio')], ['review_in_admin', __('Sarcio Inbox in wp-admin', 'sarcio'), 'sarcio_reporter', 'checkbox', __('Review, preview and approve fixes from the Sarcio menu here. Turn off to review in Sarcio only.', 'sarcio')], ['widget_in_admin', __('Also show in wp-admin', 'sarcio'), 'sarcio_reporter', 'checkbox', __('Off by default: people report from the site itself.', 'sarcio')], ['report_email', __('Email the monthly report to', 'sarcio'), 'sarcio_assistant', 'text', __('One or more addresses, separated by commas. On the first of each month they receive what changed on the site the month before: the assistant’s changes and the fixes kept. The same report is under Sarcio → Report at any time.', 'sarcio')], ['assistant_apply', __('Who applies changes', 'sarcio'), 'sarcio_assistant', 'select', __('With requests on, an editor previews a change as usual and sends it; an administrator applies or declines it under Sarcio → Requests, and the workspace Inbox lists it for the agency.', 'sarcio')], ['assistant_disabled', __('What the assistant may do', 'sarcio'), 'sarcio_assistant', 'abilities', __('Untick anything the assistant should not be able to do on this site, for anyone. Each person is still limited to what their WordPress role allows.', 'sarcio')], ['bulk_enabled', __('Receive changes your agency writes for several sites', 'sarcio'), 'sarcio_assistant', 'checkbox', __('Off by default. When on, this site asks Sarcio every five minutes whether your agency wrote a change for it on Sarcio’s Across sites screen. If so, the assistant prepares it here as a preview and leaves it under Sarcio → Requests. Nothing on the site changes until an administrator looks at it and presses Apply, which makes it under their own name. These changes can only touch content, menus, media, products, comments, categories, SEO and the site’s look: never plugins, themes, users or site settings. Each check sends the site key and secret key, nothing about your visitors.', 'sarcio')], ['server_scope', __('Apply server-side fixes to', 'sarcio'), 'sarcio_server', 'select', ''], ['pull_refresh_sec', __('Check for new fixes every (seconds)', 'sarcio'), 'sarcio_server', 'number', __('A fix approved in Sarcio is live on this site within this time.', 'sarcio')], ['sidecar_dsn', __('Sidecar address', 'sarcio'), 'sarcio_sidecar', 'text', __('Only for a server that runs the Sarcio sidecar service, which applies new fixes within seconds; most sites leave this empty. For example unix:///run/sarcio/sarcio.sock or tcp://127.0.0.1:7071.', 'sarcio')], ['shim_token', __('Sidecar token', 'sarcio'), 'sarcio_sidecar', 'password', __('Only if your sidecar asks for one. Leave blank to keep the saved token.', 'sarcio')], ['evaluate_timeout_ms', __('Sidecar time limit (ms)', 'sarcio'), 'sarcio_sidecar', 'number', __('If the sidecar takes longer, the request runs without fixes.', 'sarcio')], ['routeset_ttl_sec', __('Patch list cache (seconds)', 'sarcio'), 'sarcio_sidecar', 'number', __('How long the site reuses the list of fixed routes before asking again.', 'sarcio')], ]; foreach ($fields as $field) { [$key, $label, $section, $type, $help] = $field; add_settings_field( 'sarcio_field_' . $key, $label, [$this, 'renderField'], self::SLUG, $section, ['help' => $help, 'key' => $key, 'label_for' => 'sarcio_field_' . $key, 'link' => $field[5] ?? '', 'type' => $type], ); } } public function render(): void { if (!current_user_can('manage_options')) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only: WordPress's own redirect after a verified settings save. if (isset($_GET['settings-updated']) && Options::serverTransport() === 'pull') { (new Pull($this->plugin))->run(); } global $wp_settings_sections; $titles = $wp_settings_sections[self::SLUG] ?? []; echo '

' . esc_html__('Sarcio', 'sarcio') . '

'; $this->renderCheckNotice(); $this->renderStatus(); echo '
'; settings_fields(self::GROUP); $connected = (string) Options::get('site_key') !== '' && (string) Options::get('sidecar_key') !== ''; echo '

' . esc_html($connected ? __('Keys', 'sarcio') : __('Or enter the keys yourself', 'sarcio')) . '

'; echo '

' . esc_html($connected ? __('Connect to Sarcio filled these in. You can also paste keys from your Sarcio dashboard.', 'sarcio') : __('If you would rather copy them from your Sarcio dashboard, paste both keys and save.', 'sarcio')) . '

'; echo ''; do_settings_fields(self::SLUG, 'sarcio_connect'); echo ''; echo '

' . esc_html($titles['sarcio_look']['title'] ?? '') . '

'; echo '
'; echo ''; do_settings_fields(self::SLUG, 'sarcio_look'); echo ''; echo '

' . esc_html($titles['sarcio_assistant']['title'] ?? '') . '

'; echo '
'; echo ''; do_settings_fields(self::SLUG, 'sarcio_assistant'); echo ''; $this->renderLauncherPreview(); echo '
'; echo '
' . esc_html__('Advanced settings', 'sarcio') . ''; foreach (self::ADVANCED_SECTIONS as $section) { echo '

' . esc_html($titles[$section]['title'] ?? '') . '

'; echo ''; do_settings_fields(self::SLUG, $section); echo ''; } echo '
'; submit_button(__('Save settings', 'sarcio')); echo '
'; } public function checkNow(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('You are not allowed to do that.', 'sarcio'), 403); } check_admin_referer(self::CHECK_ACTION); $ok = Options::serverTransport() === 'pull' && (new Pull($this->plugin))->run(); wp_safe_redirect(add_query_arg('sarcio-checked', $ok ? '1' : '0', self::url())); exit; } /** @param array{help:string,key:string,link:string,type:string} $args */ public function renderField(array $args): void { $key = $args['key']; $id = 'sarcio_field_' . $key; $name = Options::OPTION . '[' . $key . ']'; $locked = Options::isLocked($key); $value = Options::get($key); $disabled = $locked ? ' disabled' : ''; switch ($args['type']) { case 'checkbox': printf( '', esc_attr($id), esc_attr($name), checked((bool) $value, true, false), esc_attr($disabled), ); break; case 'password': printf( '', esc_attr($id), esc_attr($name), esc_attr((string) $value !== '' ? __('Saved (leave blank to keep it)', 'sarcio') : ''), esc_attr($disabled), ); break; case 'color': printf( '', esc_attr($id), esc_attr($name), esc_attr((string) $value !== '' ? (string) $value : '#10b981'), esc_attr($disabled), ); break; case 'abilities': $disabled = \Sarcio\WordPress\Assistant\Catalog::disabled(); echo '
' . esc_html__('What the assistant may do', 'sarcio') . ''; printf('', esc_attr(Options::OPTION . '[assistant_switches]')); foreach (\Sarcio\WordPress\Assistant\Catalog::all() as $ability) { printf( '', esc_attr(Options::OPTION . '[assistant_enabled][]'), esc_attr($ability->name), checked(!in_array($ability->name, $disabled, true), true, false), esc_attr($disabled !== [] && $locked ? ' disabled' : ($locked ? ' disabled' : '')), esc_html($ability->label), esc_html($ability->kind === \Sarcio\WordPress\Assistant\Ability::WRITE ? __('changes something', 'sarcio') : __('only looks', 'sarcio')), ); } echo '
'; $bridged = \Sarcio\WordPress\Recognised::bridged(); $refused = \Sarcio\WordPress\Assistant\AbilitiesBridge::refused(); if ($bridged !== [] || $refused !== []) { echo '

'; if ($bridged !== []) { echo esc_html(sprintf( /* translators: %s: plugin namespaces with counts. */ __('Other plugins offering their own abilities here: %s.', 'sarcio'), implode(', ', array_map(static fn (string $namespace, int $count): string => $namespace . ' (' . $count . ')', array_keys($bridged), $bridged)), )); } if ($refused !== []) { echo ' ' . esc_html(sprintf( /* translators: %s: ability names. */ __('Never offered, because they run code or read files on the server: %s.', 'sarcio'), implode(', ', $refused), )); } echo '

'; } break; case 'icons': echo '
' . esc_html__('Icon', 'sarcio') . ''; foreach (Launcher::ICON_LABELS as $icon => $label) { $path = Launcher::ICONS[$icon] !== '' ? Launcher::ICONS[$icon] : self::BUBBLE_PATH; printf( '', esc_attr($label), esc_attr($name), esc_attr($icon), checked((string) $value, $icon, false), esc_attr($disabled), esc_attr($path), ); } echo '
'; break; case 'select': $labels = self::CHOICE_LABELS[$key] ?? []; printf(''; break; default: printf( '', esc_attr($args['type']), esc_attr($id), esc_attr($name), esc_attr((string) $value), esc_attr($disabled), ); } if ($locked) { echo '

' . esc_html(sprintf( /* translators: %s: PHP constant name. */ __('Set in wp-config.php (%s).', 'sarcio'), Options::CONSTANTS[$key] ?? '', )) . '

'; } if ($args['help'] !== '') { echo '

' . wp_kses($args['help'], ['code' => [], 'strong' => []]); if ($args['link'] !== '') { printf( ' %s', esc_url(self::dashboardUrl($args['link'] === 'keys' ? self::sitePath() : '/sites')), esc_html__('Open your Sarcio dashboard', 'sarcio'), ); } echo '

'; } } private function renderLauncherPreview(): void { $icon = (string) Options::get('launcher_icon'); $path = (Launcher::ICONS[$icon] ?? '') !== '' ? Launcher::ICONS[$icon] : self::BUBBLE_PATH; $color = (string) Options::get('launcher_color') !== '' ? (string) Options::get('launcher_color') : '#10b981'; $classes = trim( (Options::get('launcher_style') === 'label' ? 'is-label ' : '') . (Options::get('launcher_position') === 'left' ? 'is-left' : ''), ); printf( '', Options::get('widget_mode') === 'dark' ? ' is-dark' : '', esc_attr(self::BUBBLE_PATH), esc_html__('Preview', 'sarcio'), esc_attr($classes), esc_attr($color), esc_attr($path), Options::get('launcher_style') === 'label' ? '' : ' hidden', esc_html((string) Options::get('launcher_label')), ); } private static function url(): string { return admin_url('options-general.php?page=' . self::SLUG); } private static function dashboardUrl(string $path): string { $workspace = Embed::workspaceUrl(); return $workspace !== null ? $workspace . $path : Connect::homeUrl() . '/'; } private static function sitePath(): string { $siteKey = (string) Options::get('site_key'); return $siteKey === '' ? '/sites' : '/sites/' . rawurlencode($siteKey); } private function renderConnectButtons(): void { if (!current_user_can('manage_options')) { return; } echo '
'; echo wp_kses(Connect::button('connect', __('Connect to Sarcio', 'sarcio'), 'button button-primary button-hero', 'plugin-settings'), Connect::ALLOWED_HTML); echo '
' . esc_html__('New to Sarcio?', 'sarcio') . ' ' . wp_kses(Connect::button('signup', __('Start free', 'sarcio'), 'button-link', 'plugin-settings'), Connect::ALLOWED_HTML) . ' ' . esc_html__('— free for 14 days, no card needed, and this site connects itself.', 'sarcio') . '
'; echo '
'; } private function renderUsage(): void { $usage = Usage::get(); if ($usage === null) { return; } echo '
'; echo '

' . esc_html(sprintf( /* translators: %s: Sarcio plan name. */ __('AI this month (%s plan)', 'sarcio'), $usage['planName'], )) . '

'; printf( '

%s

', esc_html(sprintf( /* translators: 1: drafts used, 2: drafts included, 3: percent. */ __('Drafts: %1$s of %2$s (%3$d%%)', 'sarcio'), number_format_i18n($usage['draftsUsed']), number_format_i18n($usage['draftsIncluded']), $usage['draftsPercent'], )), (int) $usage['draftsPercent'], ); printf( '

%s

', esc_html(sprintf( /* translators: 1: credits used, 2: credits available, 3: percent. */ __('Credits: %1$s of %2$s (%3$d%%)', 'sarcio'), number_format_i18n($usage['creditsUsed'], 1), number_format_i18n($usage['creditsAvailable']), $usage['creditsPercent'], )), (int) $usage['creditsPercent'], ); if ($usage['state'] === 'reached') { echo '

' . esc_html__('This month\'s AI budget is used up. New reports are still captured and wait for you to triage them.', 'sarcio') . '

'; } elseif ($usage['state'] === 'near') { echo '

' . esc_html__('Most of this month\'s AI budget is used.', 'sarcio') . '

'; } if ($usage['billingUrl'] !== '') { printf('

%s

', esc_url($usage['billingUrl']), esc_html__('Add more credits', 'sarcio')); } echo '
'; } private function renderCheckNotice(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only: the outcome of Connect::finish(), which checked its pending marker. $connectError = isset($_GET['sarcio-connect-error']) ? sanitize_key(wp_unslash($_GET['sarcio-connect-error'])) : null; if ($connectError !== null) { $messages = [ 'expired' => __('That connection had expired. Click Connect to Sarcio again.', 'sarcio'), 'keys' => __('This site already has two active secret keys in Sarcio. Revoke one under Sites → Manage → Keys, then connect again.', 'sarcio'), ]; printf( '

%s

', esc_html($messages[$connectError] ?? __('Sarcio could not finish connecting this site. Please try again.', 'sarcio')), ); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only: the result of checkNow(), which verified its nonce. $checked = isset($_GET['sarcio-checked']) ? sanitize_key(wp_unslash($_GET['sarcio-checked'])) : null; if ($checked === null) { return; } printf( '

%s

', $checked === '1' ? 'success' : 'error', esc_html($checked === '1' ? __('Checked with Sarcio: this site has its latest fixes.', 'sarcio') : __('Could not reach Sarcio. Check the secret key; the fixes already on this site keep working.', 'sarcio')), ); } private function renderStatus(): void { $tone = 'ok'; $lines = []; $canCheck = false; $transport = Options::serverTransport(); $shim = $this->plugin->shim(); if ((string) Options::get('site_key') === '') { $tone = 'warning'; $title = __('Not connected yet', 'sarcio'); $lines[] = __('Connect this site with one click. Sarcio fills in the keys for you.', 'sarcio'); } elseif (!Autoloader::shimAvailable()) { $tone = 'error'; $title = __('Connected, but server-side fixes are off', 'sarcio'); $lines[] = __('Part of the plugin is missing. Install it again (the same way you installed it) to turn server-side fixes back on.', 'sarcio'); } elseif ($transport === 'none' || $shim === null) { $tone = 'warning'; $title = __('Connected for page fixes', 'sarcio'); $lines[] = __('The bug reporter and page fixes are on. Connect to Sarcio, or add the secret key, to receive server-side fixes as well.', 'sarcio'); } elseif ($transport === 'sidecar') { $client = Client::connect((string) Options::get('sidecar_dsn'), 1.0); $healthy = $client !== null && $client->health(1.0); $client?->close(); $tone = $healthy ? 'ok' : 'error'; $title = $healthy ? __('Connected to Sarcio through the sidecar', 'sarcio') : __('The Sarcio sidecar is not answering', 'sarcio'); $lines[] = $healthy ? __('New fixes go live within seconds.', 'sarcio') : __('Your site runs as normal, without server-side fixes, until the sidecar is back.', 'sarcio'); $lines[] = $this->activeFixesLine(count($shim->routeSet())); } else { $canCheck = true; $pull = Pull::status(); $title = __('Connected to Sarcio', 'sarcio'); if ($pull === []) { $lines[] = __('Not checked for fixes yet. It happens on its own shortly, or check now.', 'sarcio'); } elseif (!empty($pull['ok'])) { $lines[] = sprintf( /* translators: %s: human time difference, e.g. "2 mins". */ __('Last checked for fixes %s ago.', 'sarcio'), human_time_diff((int) $pull['at']), ); } else { $tone = 'error'; $lines[] = sprintf( /* translators: %s: human time difference, e.g. "2 mins". */ __('The last check, %s ago, could not reach Sarcio. Check the secret key; the fixes already on this site keep working.', 'sarcio'), human_time_diff((int) $pull['at']), ); } $lines[] = $this->activeFixesLine(count($shim->routeSet())); $lines[] = sprintf( /* translators: %d: number of seconds. */ __('New fixes go live within %d seconds of being approved.', 'sarcio'), (int) Options::get('pull_refresh_sec'), ); } if ($transport !== 'none' && Options::get('server_scope') === 'off') { $lines[] = __('Server-side fixes are switched off under Advanced settings.', 'sarcio'); } printf('

%s

    ', esc_attr($tone), esc_html($title)); foreach ($lines as $line) { echo '
  • ' . esc_html($line) . '
  • '; } echo '
'; if ($canCheck) { printf('
', esc_url(admin_url('admin-post.php'))); printf('', esc_attr(self::CHECK_ACTION)); wp_nonce_field(self::CHECK_ACTION); submit_button(__('Check for fixes now', 'sarcio'), 'secondary', 'submit', false); echo '
'; } if (!Connect::connected()) { $this->renderConnectButtons(); } else { $this->renderUsage(); printf( '

%s · %s

', esc_url(admin_url('admin.php?page=' . ReviewPage::SLUG)), esc_html__('Review reports', 'sarcio'), esc_url(self::dashboardUrl('/')), esc_html__('Open your Sarcio dashboard', 'sarcio'), ); } echo '
'; } private function activeFixesLine(int $count): string { return $count === 0 ? __('No server-side fixes active right now.', 'sarcio') : sprintf( /* translators: %d: number of active server-side fixes. */ _n('%d server-side fix active.', '%d server-side fixes active.', $count, 'sarcio'), $count, ); } }