'PHP Error Log', 'path' => $php_log, ); } // WordPress debug.log, prefer relocated path set by quick-fix mu-plugin $wp_debug_log = csdt_devtools_wp_debug_log_path(); $sources['wp_debug'] = array( 'label' => 'WordPress Debug Log', 'path' => $wp_debug_log, ); // Web server error log, check common paths that may be readable by the web user $web_error_candidates = array( '/var/log/apache2/error.log', '/var/log/httpd/error_log', '/var/log/nginx/error.log', '/var/log/apache2/error_log', ); foreach ( $web_error_candidates as $path ) { if ( is_readable( $path ) ) { $sources['web_error'] = array( 'label' => 'Web Server Error Log', 'path' => $path, ); break; } } // Web server access log $web_access_candidates = array( '/var/log/apache2/access.log', '/var/log/httpd/access_log', '/var/log/nginx/access.log', ); foreach ( $web_access_candidates as $path ) { if ( is_readable( $path ) ) { $sources['web_access'] = array( 'label' => 'Web Server Access Log', 'path' => $path, ); break; } } // SSH auth log, readable if www-data is in the adm group $auth_log_candidates = array( '/var/log/auth.log', // Debian/Ubuntu '/var/log/secure', // RHEL/CentOS/Fedora '/var/log/messages', // some RHEL variants ); foreach ( $auth_log_candidates as $path ) { if ( is_readable( $path ) ) { $sources['auth_ssh'] = array( 'label' => 'SSH Auth Log', 'path' => $path, ); break; } } // WP Cron log, plugin-owned location under wp-content/uploads (via // get_log_dir()), not a hardcoded wp-content root path. Sites that write a // cron log elsewhere can add it through the custom log paths below. $cron_log = self::get_log_dir() . '/cron.log'; if ( file_exists( $cron_log ) ) { $sources['wp_cron'] = array( 'label' => 'WP Cron Log', 'path' => $cron_log, ); } // Admin-configured custom paths $custom = get_option( 'csdt_custom_log_paths', array() ); if ( is_array( $custom ) ) { foreach ( $custom as $i => $cp ) { // Re-validate at read time so a path saved by an older build or a // poisoned option can't be replayed to read wp-config.php / PHP source. if ( ! empty( $cp['label'] ) && ! empty( $cp['path'] ) && self::is_safe_log_path( (string) $cp['path'] ) ) { $sources[ 'custom_' . $i ] = array( 'label' => sanitize_text_field( $cp['label'] ), 'path' => $cp['path'], 'custom' => true, ); } } } return $sources; } public static function ajax_logs_setup_php(): void { check_ajax_referer( self::LOGS_NONCE, 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Unauthorized', 403 ); } try { $log_path = self::get_log_dir() . '/php-error.log'; // Store the path; CSDT_DevTools::init() relocates the PHP error log to it // at runtime via ini_set(). No must-use plugin is written (a WordPress.org // plugin may not deploy executable code into wp-content/mu-plugins). update_option( 'csdt_php_error_log_path', $log_path, false ); wp_send_json_success( array( 'path' => $log_path, 'sources' => self::get_log_sources(), ) ); } catch ( \Throwable $e ) { CSDT_Ajax::fail( $e, __CLASS__ . '::' . __FUNCTION__ ); } } public static function ajax_logs_custom_save(): void { // Choosing which server files the log viewer may read is a real administrator's // decision. A test session holds manage_options without a password or second factor. if ( self::viewer_is_test_session() ) { wp_send_json_error( 'This tool is not available to a test session.', 403 ); } check_ajax_referer( self::LOGS_NONCE, 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Unauthorized', 403 ); } try { $raw = isset( $_POST['paths'] ) ? sanitize_text_field( wp_unslash( $_POST['paths'] ) ) : '[]'; // A JSON list of file paths: no markup to keep. $paths = json_decode( $raw, true ); if ( ! is_array( $paths ) ) { $paths = array(); } $clean = array(); $rejected = array(); foreach ( $paths as $p ) { $label = sanitize_text_field( $p['label'] ?? '' ); $path = sanitize_text_field( $p['path'] ?? '' ); if ( $label === '' || $path === '' ) { continue; } // Reject paths that could expose PHP source / wp-config / secrets; // the viewer is for log files only. if ( ! self::is_safe_log_path( $path ) ) { $rejected[] = $path; continue; } $clean[] = array( 'label' => $label, 'path' => $path, ); } update_option( 'csdt_custom_log_paths', $clean ); wp_send_json_success( array( 'sources' => self::get_log_sources(), 'rejected' => $rejected, ) ); } catch ( \Throwable $e ) { CSDT_Ajax::fail( $e, __CLASS__ . '::' . __FUNCTION__ ); } } private static function render_debug_panel(): void { $has_key = CSDT_AI_Dispatcher::has_key(); $key_url = admin_url( 'admin.php?page=' . self::TOOLS_SLUG . '&tab=security' ); $perf_on = CSDT_Options::get( 'csdt_devtools_perf_monitor_enabled' ) !== '0'; ?>
⚡ 'What is CS Monitor?', 'rec' => 'Info', 'html' => 'CS Monitor is a floating overlay panel visible only to admins. It appears on every page of your site and tracks HTTP requests, PHP errors, and hook counts in real time, without affecting other users. Its database query list needs define( 'SAVEQUERIES', true ); in wp-config.php, which a plugin may not switch on for you; the DB tab shows the line with a copy button. Remove it when you have finished, because it records every query on every request.', ), array( 'name' => 'N+1 DB query detection', 'rec' => 'Info', 'html' => 'CS Monitor groups SQL queries by normalised pattern and flags any pattern that fires 3 or more times on a single page load as a potential N+1 issue. WP core patterns (options, post counts, site icon) are downgraded to info, they are eliminated by installing the persistent object cache below.', ), array( 'name' => 'When to disable', 'rec' => 'Recommended', 'html' => 'Leave CS Monitor on during development and debugging. Turn it off on production pages that are publicly visited or performance-benchmarked, as the overlay JS adds a small overhead and its query tracking touches the same request lifecycle you are measuring.', ), ) ); ?>
✓
'#d97706', 'redis', 'memcached', 'other' => '#7c3aed', default => '#64748b', }; // Collapsed only when a maintained persistent cache is recognised. A third-party drop-in // ('other') deliberately does NOT collapse: this plugin cannot vouch for it, and the panel // is where you would go to find out what is installed. $csdt_oc_ok = in_array( $oc_type, array( 'redis', 'memcached' ), true ); ?>
🗄️ 'What this panel shows', 'rec' => 'Info', 'html' => 'Whether wp-content/object-cache.php exists, and whose it is: Redis, Memcached, a CloudScale drop-in from an older version of this plugin, or another drop-in. WordPress loads that file on every request and uses it to keep database results (options, users, post counts) in memory between requests.', ), array( 'name' => 'Why this plugin does not install one', 'rec' => 'Info', 'html' => 'An object cache has to replace WordPress core functions by their exact names, which a plugin in the WordPress.org directory may not ship. Install a dedicated object-cache plugin instead: Find an object-cache plugin opens the plugin directory search inside your own admin. Redis Object Cache is a common choice when your host provides Redis.', ), array( 'name' => 'WP core N+1 patterns', 'rec' => 'Info', 'html' => 'WordPress core makes repeated DB calls for options, user data, post counts, and the site icon on every page load. These cannot be fixed in plugin code, they are inside WP core itself. A persistent object cache is the correct solution: WordPress checks the cache before hitting the DB, so the same record is only fetched once per cache lifetime.', ), array( 'name' => 'Removing an older CloudScale drop-in', 'rec' => 'Recommended', 'html' => 'Older versions of this plugin offered an APCu drop-in. It is no longer maintained. If one is installed, Remove deletes wp-content/object-cache.php, and only when it is that CloudScale drop-in. Third-party drop-ins (Redis, Memcached) must be removed through their own plugin to prevent accidental data loss.', ), ) ); ?>

🔎
🔔 'PHP Error Alerting', 'rec' => 'Info', 'html' => 'Monitors the PHP error log inside the Docker container once at least the configured number of new errors appears in a single check. When a new matching error appears it sends an alert via email and Telegram (whichever channels are configured). The log is polled on a WP-Cron schedule, no persistent daemon required.', ), array( 'name' => 'PHP-FPM Saturation Monitor', 'rec' => 'Info', 'html' => 'A host-side cron script probes the site over HTTP every minute. After the configured number of consecutive failed or timed-out probes it declares saturation and alerts, optionally restarting the container. (The worker counts shown in the panel come from php-fpm status and are display-only.) High saturation causes slow page loads even when DB and server CPU are healthy.', ), array( 'name' => 'Alert threshold', 'rec' => 'Recommended', 'html' => 'Leave the count at 1 while you are stabilising a site so every new error is reported, and raise it to 5-10 on a noisy production site to suppress bursts. Fatal and critical entries always alert regardless of the count, so raising it never hides an outage.', ), ) ); ?>
🔔 'How it works', 'rec' => 'Info', 'html' => 'A WP-Cron job runs every 5 minutes. It tracks the last byte-offset read for each log file and only reads new lines since the previous check, you are only alerted about errors that just appeared, not the full log history.', ), array( 'name' => 'Alert channels', 'rec' => 'Recommended', 'html' => 'Email, sent to the WordPress admin email address. Telegram, instant push notification to your phone. Configure your bot token and chat ID under AI Security Scan → Notifications & Scheduled Scan. Both channels fire independently.', ), array( 'name' => 'Threshold', 'rec' => 'Info', 'html' => 'Minimum new errors per 5-minute check before an alert fires. Set to 1 to be notified about every error. PHP Fatal and critical log entries always trigger an alert regardless of threshold.', ), array( 'name' => 'Log paths', 'rec' => 'Recommended', 'html' => 'Watches the same log sources configured in the Server Logs panel. If PHP is logging to /dev/stderr (default in many Docker setups), errors are not readable here. Enable the mu-plugin under Server Logs → PHP Error Log to redirect errors to wp-content/php-error.log.', ), ) ); ?>
set up in Security Scan → Scheduled Scans', 'cloudscale-cyber-devtools' ), array( 'a' => array( 'href' => array(), 'style' => array(), ), ) ), esc_url( admin_url( 'admin.php?page=' . self::TOOLS_SLUG . '&tab=security' ) ) ); ?>

🖥️ HOST CRON 'What is PHP-FPM saturation?', 'rec' => 'Info', 'html' => 'PHP-FPM (FastCGI Process Manager) maintains a pool of worker processes that handle requests. When all workers are busy (e.g. a traffic spike, a slow DB query holding workers open, or a runaway loop), new requests queue up and the site appears frozen or times out. This is called saturation.', ), array( 'name' => 'Why a host cron, not WP-Cron?', 'rec' => 'Critical', 'html' => 'WP-Cron runs inside PHP-FPM. If PHP-FPM is fully saturated, WP-Cron can\'t execute, so a WordPress-based monitor would be silenced exactly when you need it most. This monitor runs as a shell script on the host OS (outside Docker), so it fires even when every PHP worker is consumed.', ), array( 'name' => 'How the detection works', 'rec' => 'Info', 'html' => 'Every minute the script probes the HTTP URL. If the probe times out or fails N consecutive times (the threshold), saturation is declared. It then sends a Telegram notification and email alert, optionally restarts the WordPress container, and POSTs an event to this panel via the callback URL.', ), array( 'name' => 'Current Workers display', 'rec' => 'Info', 'html' => 'Shows live active / idle / total worker counts from the PHP-FPM status page (pm.status_path). Requires pm.status_path = /fpm-status in your www.conf and a matching nginx location block. Click Refresh at any time to re-poll.', ), array( 'name' => 'Auto-restart', 'rec' => 'Optional', 'html' => 'When enabled, the script issues a docker restart {container} command after declaring saturation. A restart cooldown prevents thrashing. Use with care on production, a restart drops all in-flight requests.', ), array( 'name' => 'Setup', 'rec' => 'Info', 'html' => 'Copy the settings from the Host-side monitor section below into whatever monitors PHP-FPM on your host. The callback URL and token wire it back to this panel so saturation events appear in the audit trail automatically.', ), ), 'Monitors PHP-FPM worker exhaustion from the host OS. Alerts via Telegram + email, can auto-restart the container, and logs events back to this panel.' ); ?>
· · · ·
📋
* * * * * /path/to/your-fpm-monitor.sh 2>/dev/null
$ev ) : $ev_color = match ( $ev['type'] ?? '' ) { 'recovered' => '#16a34a', 'restarted' => '#d97706', default => '#dc2626', }; $ev_icon = match ( $ev['type'] ?? '' ) { 'recovered' => '✓', 'restarted' => '🔄', default => '🚨', }; $ev_label = match ( $ev['type'] ?? '' ) { 'recovered' => __( 'Recovered', 'cloudscale-cyber-devtools' ), 'restarted' => __( 'Auto-restarted', 'cloudscale-cyber-devtools' ), default => __( 'Saturated', 'cloudscale-cyber-devtools' ), }; $row_bg = $i % 2 === 0 ? '#ffffff' : '#f8fafc'; ?>
⚡ 'What is OPcache?', 'rec' => 'Info', 'html' => 'PHP OPcache compiles PHP scripts to bytecode and caches them in shared memory, so they don\'t need to be parsed and compiled on every request. This typically reduces response time by 30-50% and is enabled by default in PHP 5.5+.', ), array( 'name' => 'When to flush', 'rec' => 'Info', 'html' => 'OPcache must be flushed after PHP files change on disk, otherwise running workers may serve stale bytecode. This can cause SIGSEGV crashes if a worker has an old cached version that conflicts with the new file. Flush it from this panel after any deployment that changes PHP files.', ), array( 'name' => 'Flushing from your own deployment tooling', 'rec' => 'Optional', 'html' => 'Flushing OPcache needs an administrator session and a nonce, so it cannot be triggered from a deployment script or CI job; use the button above. A token endpoint that allowed this was removed: a stored token standing in for a nonce is exactly the check WordPress.org asks plugins not to ship, and nothing in the plugin used it.', ), array( 'name' => 'Hit rate', 'rec' => 'Info', 'html' => 'A healthy OPcache hit rate is above 95%. Low hit rates (below 70%) indicate the cache is too small for your codebase, or that scripts are being invalidated too frequently. Increase opcache.memory_consumption in php.ini if the cache is frequently full.', ), ) ); ?>

📋 'Log Sources', 'rec' => 'Info', 'html' => 'The panel automatically detects common log file locations for your server stack, Apache, Nginx, PHP-FPM, WordPress debug log, and any custom paths you add. Each source button shows a colour-coded status: green (readable), amber (empty), red (not found or permission denied).', ), array( 'name' => 'PHP Error Log', 'rec' => 'Recommended', 'html' => 'If PHP is logging to /dev/stderr (the default in many Docker/container setups), errors cannot be read here. Click Enable to install a mu-plugin that redirects PHP errors to wp-content/php-error.log. The mu-plugin runs on every request before other plugins load.', ), array( 'name' => 'Filters', 'rec' => 'Info', 'html' => 'Colour coding: red = error/critical, amber = warning, blue = notice/info, grey = debug.', ), array( 'name' => 'Auto-refresh', 'rec' => 'Optional', 'html' => 'Enable Tail mode to poll the selected log every 30 seconds automatically. Useful when watching a running process or debugging a live issue without leaving the page.', ), array( 'name' => 'Custom Log Paths', 'rec' => 'Optional', 'html' => 'Add any absolute file path your web server user can read. Common extras: application logs (Laravel storage/logs/laravel.log), cron output files, or a custom PHP-FPM pool log. Labels are free-text, choose something descriptive. Custom paths are saved as a WordPress option and survive plugin updates.', ), array( 'name' => 'Permissions', 'rec' => 'Info', 'html' => 'System logs (e.g. /var/log/syslog, /var/log/auth.log) are typically owned by root and not readable by www-data. This is intentional OS hardening, the plugin shows a clear "permission denied" message rather than an error. To expose a system log, add your web server user to the adm group or use a log-shipping tool.', ), ) ); ?>

⚠️ ' . esc_html( CSDT_Paths::relative_to_root( $mu_dir ) ) . '' ); ?>
$src ) : ?>

$cp ) : ?>
✓
$src ) { $path = $src['path']; if ( ! file_exists( $path ) ) { $statuses[ $key ] = array( 'status' => 'not_found' ); } elseif ( ! is_readable( $path ) ) { $statuses[ $key ] = array( 'status' => 'permission_denied' ); } elseif ( filesize( $path ) === 0 ) { $statuses[ $key ] = array( 'status' => 'empty' ); } else { $statuses[ $key ] = array( 'status' => 'ok' ); } } wp_send_json_success( $statuses ); } catch ( \Throwable $e ) { CSDT_Ajax::fail( $e, __CLASS__ . '::' . __FUNCTION__ ); } } public static function ajax_server_logs_fetch(): void { check_ajax_referer( self::LOGS_NONCE, 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Unauthorized', 403 ); } try { $source_key = isset( $_POST['source'] ) ? sanitize_key( $_POST['source'] ) : ''; $lines_req = min( 1000, max( 50, absint( wp_unslash( $_POST['lines'] ?? 300 ) ) ) ); $sources = self::get_log_sources(); if ( ! isset( $sources[ $source_key ] ) ) { wp_send_json_error( array( 'message' => 'Unknown source.' ) ); return; } $path = $sources[ $source_key ]['path']; if ( ! file_exists( $path ) ) { wp_send_json_success( array( 'status' => 'not_found', 'lines' => array(), 'count' => 0, 'path' => $path, ) ); return; } if ( ! is_readable( $path ) ) { wp_send_json_success( array( 'status' => 'permission_denied', 'lines' => array(), 'count' => 0, 'path' => $path, ) ); return; } // Read last N lines efficiently without loading the entire file // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- streaming local log file; WP Filesystem API lacks partial-read/seek support $handle = is_readable( $path ) ? fopen( $path, 'rb' ) : false; if ( ! $handle ) { wp_send_json_success( array( 'status' => 'error', 'lines' => array(), 'count' => 0, 'path' => $path, ) ); return; } fseek( $handle, 0, SEEK_END ); $file_size = ftell( $handle ); $chunk_size = 65536; // 64 KB chunks, read from end $buffer = ''; $pos = $file_size; $line_count = 0; while ( $pos > 0 && $line_count < $lines_req + 1 ) { $read = min( $chunk_size, $pos ); $pos -= $read; fseek( $handle, $pos ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- partial read of local log file $buffer = fread( $handle, $read ) . $buffer; $line_count = substr_count( $buffer, "\n" ); } fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- paired with fopen above $all_lines = explode( "\n", $buffer ); // Remove trailing empty line if ( end( $all_lines ) === '' ) { array_pop( $all_lines ); } $lines = array_slice( $all_lines, -$lines_req ); wp_send_json_success( array( 'status' => count( $lines ) > 0 ? 'ok' : 'empty', 'lines' => $lines, 'count' => count( $lines ), 'path' => $path, ) ); } catch ( \Throwable $e ) { CSDT_Ajax::fail( $e, __CLASS__ . '::' . __FUNCTION__ ); } } /** * Renders the Code Block Migrator panel. * * @since 1.5.0 * @return void */ /** * The Mermaid diagrams card, at the top of the Code Block tab. * * WHY IT EXISTS. Mermaid shipped in 1.10.947 and is the readme's headline feature, but no admin * screen mentioned it: this tab only offered a migrator for old blocks, and the Code Block * Settings panel on Diagnostics covers colours. Reported 2026-09-30: "this doesnt really sell * the mermaid feature". So it shows a diagram actually drawn by the bundled library, the text * that produced it, and the three steps to get one into a post. * * The example is drawn by assets/cs-mermaid-card.js with the same settings the front end uses * (securityLevel 'strict', the same font), so what it shows is what a reader gets. * * @return void */ private static function render_mermaid_card(): void { $example = implode( "\n", array( 'flowchart LR', ' V([Visitor]) --> F{Firewall}', ' F -->|allowed| W[WordPress]', ' F -->|blocked| X[403 Forbidden]', ' W --> C[(Database)]', ' W --> P[Your post]', ) ); ?>
📊 'What it does', 'rec' => 'Overview', 'html' => 'A CloudScale Code block whose language is Mermaid diagram is drawn on the page as a diagram instead of shown as code. Flowcharts, sequence diagrams, class and state diagrams, Gantt charts, pie charts, timelines and mind maps all work, written as plain text you can edit like any other code.', ), array( 'name' => 'Charts', 'rec' => 'Info', 'html' => 'For numbers rather than boxes and arrows, set the block language to Chart. Write one line per setting or series: type: bar, title: Revenue by quarter, x: Q1, Q2, Q3, then 2025: 12, 15, 17 and 2026: 19, 23, 26. Types are line, area, bar, hbar, stacked, pie and donut. Readers get a legend that hides or shows each series, tooltips, and the same Source and theme buttons. The chart library is bundled and loads only on pages with a chart.', ), array( 'name' => 'Chart.js, Vega and Vega-Lite', 'rec' => 'Info', 'html' => 'A chart you already have as JSON can be pasted as it is. With the language set to Chart or Chart.js configuration, a block that starts with { is read as a Chart.js configuration (type, data, options), which is what AI assistants usually write. With the language set to Vega or Vega-Lite, the block is drawn from its specification. Colours you set are kept; anything left unset follows the light or dark theme. It must be plain JSON: no functions, comments or trailing commas. A Vega chart may load data files from this site only, so put the data in the specification or upload the file to the media library.', ), array( 'name' => 'What readers get', 'rec' => 'Info', 'html' => 'The drawn diagram, with a Source button that shows the text, a Copy image button, and the light/dark toggle every code block has, which redraws the diagram to match. Copy image puts a picture of the diagram or chart on the clipboard, with its title and legend, ready to paste into a message or a slide. In the Source view the same button copies the text. A browser that cannot copy a picture copies the text and says so. A diagram with a mistake in it shows its source and the error instead of a blank box.', ), array( 'name' => 'Performance and privacy', 'rec' => 'Info', 'html' => 'Mermaid ' . esc_html( self::MERMAID_VERSION ) . ', Chart.js ' . esc_html( self::CHARTJS_VERSION ) . ', Vega ' . esc_html( self::VEGA_VERSION ) . ' and Vega-Lite ' . esc_html( self::VEGA_LITE_VERSION ) . ' are bundled in the plugin. Each is loaded only on pages that contain a diagram or chart that needs it, so every other page pays nothing. No CDN is contacted, and Vega expressions are run by its interpreter rather than eval, so charts work under a strict Content Security Policy.', ), array( 'name' => 'Safety', 'rec' => 'Info', 'html' => 'Diagrams render in Mermaid\'s strict security mode: labels are sanitised and click handlers or scripts in the diagram text are ignored, so a diagram cannot run code on your site.', ), ) ); ?>

  1. Mermaid diagram, or paste a ```mermaid fence.', 'cloudscale-cyber-devtools' ), array( 'strong' => array(), 'code' => array(), ) ); ?>

Chart for line, bar and pie charts. Explain has the format.', 'cloudscale-cyber-devtools' ), array( 'strong' => array() ) ); ?>

→

🔄 CODE BLOCK MIGRATOR 'Scan Posts', 'rec' => 'Informational', 'html' => 'Scans all posts and pages for code blocks that can be upgraded to CloudScale Code Blocks with full syntax highlighting: WordPress wp:code and wp:preformatted blocks, and blocks made by the Code Block Pro plugin.', ), array( 'name' => 'Code Block Pro', 'rec' => 'Informational', 'html' => 'A Code Block Pro block is converted from the code its author typed, so the text is carried over exactly, indentation included. Its language is kept when this block highlights that language, and is otherwise left to be detected. A header title is kept. Its own colour theme, font, line numbers and line highlights are not carried over: the block takes the site theme set under Re-apply Default Theme. A block that holds no readable code is left untouched. Code Block Pro does not need to be installed or active for its blocks to be converted.', ), array( 'name' => 'Preview', 'rec' => 'Recommended', 'html' => 'Shows a side-by-side before/after diff for each post before committing any changes, so you can review exactly what will be converted.', ), array( 'name' => 'Migrate', 'rec' => 'Optional', 'html' => 'Converts detected legacy blocks to CloudScale format. Each post is saved with the converted markup.

Take a backup first, this cannot be undone without one.', ), ) ); ?>

array() ) ), '' . esc_html__( 'Scan Posts', 'cloudscale-cyber-devtools' ) . '' ); ?>

🎨 'Pick palette + mode', 'rec' => 'Step 1', 'html' => 'In the two dropdowns above, choose a Color Theme (Atom One, Nord, Dracula, Tokyo Night, GitHub, Monokai, etc.) and a Default Mode (Dark or Light). These become the site-wide defaults.', ), array( 'name' => 'Save the default', 'rec' => 'Step 2', 'html' => 'Click 💾 Save default theme. The values are written to the site-wide options. From now on, every new code block automatically inherits these.', ), array( 'name' => 'Apply to existing', 'rec' => 'Step 3', 'html' => 'Click 🎨 Re-apply default theme to all blocks. The plugin walks every CloudScale Code Block in every post and clears its per-block theme override so existing blocks snap to the new default too. Without this step, only new blocks pick up the change.', ), array( 'name' => 'What it leaves alone', 'rec' => 'Informational', 'html' => 'Only the per-block theme attribute is reset. language, title, GitHub source URL, prefix/suffix, and all other per-block settings are not touched.', ), array( 'name' => 'Per-block overrides', 'rec' => 'Informational', 'html' => 'After re-applying, any block can still set its own theme override from the block inspector, handy for a single demo block that should always render dark or always light regardless of the site default.', ), array( 'name' => 'Reversibility', 'rec' => 'Warning', 'html' => 'There is no undo button. If you have many blocks with intentional per-block theme overrides you want to keep, take a database backup first.', ), ) ); ?>