From 5b5f37de365ff84fc231e414a719d1b2ff4ceff6 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 01 Jan 2026 22:38:58 +0000
Subject: [PATCH] =further feed block testing

---
 inc/managers/AdminPages.php |  752 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 711 insertions(+), 41 deletions(-)

diff --git a/inc/managers/AdminPages.php b/inc/managers/AdminPages.php
index d0a1f8c..a2147a0 100644
--- a/inc/managers/AdminPages.php
+++ b/inc/managers/AdminPages.php
@@ -2,6 +2,7 @@
 namespace JVBase\managers;
 
 use JVBase\utility\Features;
+use WP_REST_Response;
 
 if (!defined('ABSPATH')) {
     exit; // Exit if accessed directly
@@ -33,7 +34,7 @@
             'icon' => jvbCSSIcon('settings'),
             'position' => 0
         ];
-		$this->subpages = apply_filters('jvbAdminSubpages', []);
+		$this->subpages = get_option(BASE.'adminSubpage', []);
 //        delete_option(BASE.'admin_actions');
 //        delete_option(BASE.'admin_subpages');
 //        $this->getSubpages();
@@ -43,8 +44,167 @@
         // Hook into WordPress admin
         add_action('admin_menu', [$this, 'registerAdminPages']);
         add_action('admin_enqueue_scripts', [$this, 'enqueueAdminAssets']);
+
+		add_filter(BASE.'admin_action_filter', [$this, 'handleCacheActions'], 10, 3);
+
+		add_action('rest_api_init', [$this, 'registerRestRoutes']);
+		// Handle form submissions
+		add_action('admin_init', [$this, 'handleAdminPageSubmission']);
+		add_action('admin_notices', [$this, 'displayAdminNotices']);
     }
 
+	/**
+	 * Register REST API routes for admin actions
+	 */
+	public function registerRestRoutes(): void
+	{
+		register_rest_route('jvb/v1', '/admin-cache', [
+			'methods' => 'POST',
+			'callback' => [$this, 'handleCacheAction'],
+			'permission_callback' => [$this, 'checkAdminPermission']
+		]);
+
+		register_rest_route('jvb/v1', '/admin-icons', [
+			'methods' => 'POST',
+			'callback' => [$this, 'handleIconAction'],
+			'permission_callback' => [$this, 'checkAdminPermission']
+		]);
+	}
+
+	/**
+	 * Check if user has admin permissions
+	 */
+	public function checkAdminPermission(\WP_REST_Request $request): bool
+	{
+		if (!current_user_can('manage_options')) {
+			return false;
+		}
+
+		// Verify nonce
+		$nonce = $request->get_header('X-WP-Nonce');
+		if (!wp_verify_nonce($nonce, 'wp_rest')) {
+			return false;
+		}
+
+		return true;
+	}
+
+	/**
+	 * Handle cache-related actions
+	 */
+	public function handleCacheAction(\WP_REST_Request $request): \WP_REST_Response
+	{
+		$action = sanitize_text_field($request->get_param('action'));
+
+		switch ($action) {
+			case 'flush-all':
+				wp_cache_flush();
+				return new \WP_REST_Response([
+					'success' => true,
+					'message' => 'All caches flushed successfully'
+				]);
+
+			case 'flush-cache':
+				$group = sanitize_text_field($request->get_param('group'));
+				if (empty($group)) {
+					return new \WP_REST_Response([
+						'success' => false,
+						'message' => 'No cache group specified'
+					], 400);
+				}
+
+				\JVBase\managers\CacheManager::invalidateAll($group);
+
+				return new \WP_REST_Response([
+					'success' => true,
+					'message' => "Cache group '{$group}' flushed successfully"
+				]);
+
+			default:
+				return new \WP_REST_Response([
+					'success' => false,
+					'message' => 'Invalid action'
+				], 400);
+		}
+	}
+
+	/**
+	 * Handle icon-related actions
+	 */
+	public function handleIconAction(\WP_REST_Request $request): \WP_REST_Response
+	{
+		$action = sanitize_text_field($request->get_param('action'));
+		$source = sanitize_text_field($request->get_param('source') ?? 'icons'); // Add source param
+		$icons = \JVBase\managers\IconsManager::for($source);
+
+		switch ($action) {
+			case 'refresh-icons':
+				$icons->forceRefresh();
+				return new \WP_REST_Response([
+					'success' => true,
+					'message' => "Icon CSS regenerated successfully for '{$source}'"
+				]);
+
+			case 'restore-icon-version':
+				$timestamp = (int)$request->get_param('timestamp');
+				if (empty($timestamp)) {
+					return new \WP_REST_Response([
+						'success' => false,
+						'message' => 'No timestamp provided'
+					], 400);
+				}
+
+				if ($icons->restoreVersion($timestamp)) {
+					return new \WP_REST_Response([
+						'success' => true,
+						'message' => 'Icon version restored successfully'
+					]);
+				}
+
+				return new \WP_REST_Response([
+					'success' => false,
+					'message' => 'Failed to restore icon version'
+				], 500);
+
+			case 'merge-icon-versions':
+				$timestamps = $request->get_param('timestamps');
+
+				if (empty($timestamps) || !is_array($timestamps)) {
+					return new \WP_REST_Response([
+						'success' => false,
+						'message' => 'No versions selected for merging'
+					], 400);
+				}
+
+				$timestamps = array_map('intval', $timestamps);
+
+				if (count($timestamps) < 2) {
+					return new \WP_REST_Response([
+						'success' => false,
+						'message' => 'Please select at least 2 versions to merge'
+					], 400);
+				}
+
+				if ($icons->mergeVersions($timestamps)) {
+					return new \WP_REST_Response([
+						'success' => true,
+						'message' => 'Icon versions merged successfully'
+					]);
+				}
+
+				return new \WP_REST_Response([
+					'success' => false,
+					'message' => 'Failed to merge icon versions'
+				], 500);
+
+			default:
+				return new \WP_REST_Response([
+					'success' => false,
+					'message' => 'Invalid action'
+				], 400);
+		}
+	}
+
     /**
      * Register a subpage to appear under the main settings page
      *
@@ -179,6 +339,14 @@
             BASE.'cache',
             [$this, 'renderCachePage']
         );
+		add_submenu_page(
+			$this->main_page['menu_slug'],
+			'Icon Management',
+			'Icons',
+			'manage_options',
+			BASE.'icons',
+			[$this, 'renderIconsPage']
+		);
 
 //        $this->getSubpages();
         // Add registered subpages
@@ -415,9 +583,9 @@
             if (current_user_can($action['capability'])) {
                 ?>
                 <a data-action="<?=$action['slug']?>" class="jvb-action">
-                    <?= jvbIcon($action['icon']); ?>
+                    <?= jvbDashIcon($action['icon']); ?>
                     <span class="jvb-link-title"><?= esc_html($action['label'])?></span>
-                    <span class="loader"><?=jvbIcon('arrows-clockwise')?><?=jvbIcon('check')?></span>
+                    <span class="loader"><?=jvbDashIcon('arrows-clockwise')?><?=jvbDashIcon('check')?></span>
                 </a>
                 <?php
             }
@@ -472,7 +640,7 @@
      */
     protected function getIcon(string $icon = 'logo', bool $css = false): string
     {
-        $svg = jvbIcon($icon, ['wrap' => false]);
+        $svg = jvbDashIcon($icon, ['wrap' => false]);
         if ($css) {
             // For CSS, replace currentColor with brand color
             $svg = str_replace('currentColor', '#FF0080', $svg);
@@ -489,42 +657,544 @@
         return 'data:image/svg+xml;base64,' . base64_encode($svg);
     }
 
-    public function renderCachePage()
-    {
-        $groups = get_option(BASE.'all_cache_groups', []);
+	public function renderCachePage():void
+	{
+		$connections = CacheManager::getAllConnections();
 
-        ?>
-        <h1>Manage Cache</h1>
-        <?php
-        foreach ($groups as $group => $caches) {
-            ?>
-            <details>
-                <summary class="row btw"><h2><?=$group?></h2></summary>
-                <table>
-                    <thead>
-                        <tr>
-                            <th scope="col"><input type="checkbox" name="select-all-<?=$group?>" id="select-all-<?=$group?>">
-                                <label for="select-all-<?=$group?>">All</label></th>
-                            <th scope="col">Cache Key</th>
-                            <th scope="col">Actions</th>
-                        </tr>
-                    </thead>
-                    <tbody>
-                    <?php
-                    foreach ($caches as $key) {
-                        ?>
-                        <tr>
-                            <td><input type="checkbox" name="select-<?=$group?>-<?=$key?>" id="select-<?=$group?>-<?=$key?>"><label for="select-<?=$group?>-<?=$key?>"></label></td>
-                            <td><?= $key ?></td>
-                            <td><button type="button" data-action="flush-<?=$group?>-<?=$key?>"><?= jvbIcon('trash')?></button></td>
-                        </tr>
-                        <?php
-                    }
-                    ?>
-                    </tbody>
-                </table>
-            </details>
-            <?php
-        }
-    }
+		// Separate generic vs. specific caches
+		$generic_groups = [];
+		$content_specific = [];
+		$nonce = wp_create_nonce('wp_rest');
+
+		foreach ($connections as $group => $configs) {
+			$is_generic = !$this->isBoundToContentOrTaxonomy($group);
+
+			if ($is_generic) {
+				$generic_groups[$group] = $configs;
+			} else {
+				$content_specific[$group] = $configs;
+			}
+		}
+
+		?>
+		<div class="wrap jvb-admin-wrap">
+			<h1>Cache Management</h1>
+
+			<div class="jvb-cache-actions">
+				<button type="button" class="button button-primary" data-action="flush-all">
+					<?= jvbDashIcon('arrows-clockwise'); ?>
+					Flush All Caches
+				</button>
+			</div>
+
+			<div class="jvb-cache-section">
+				<h2>Generic Caches &amp; Connections</h2>
+				<table class="wp-list-table widefat fixed striped">
+					<thead>
+					<tr>
+						<th class="manage-column">Cache Group</th>
+						<th class="manage-column">Connected To</th>
+						<th class="manage-column">Actions</th>
+					</tr>
+					</thead>
+					<tbody>
+					<?php if (empty($generic_groups)): ?>
+						<tr><td colspan="3">No generic caches registered</td></tr>
+					<?php else: ?>
+						<?php foreach ($generic_groups as $group => $configs): ?>
+							<tr>
+								<td><strong><?= esc_html($group); ?></strong></td>
+								<td><?= $this->formatConnections($configs); ?></td>
+								<td>
+									<button type="button" class="button" data-action="flush-cache" data-group="<?= esc_attr($group); ?>">
+										<?= jvbDashIcon('trash'); ?> Flush
+									</button>
+								</td>
+							</tr>
+						<?php endforeach; ?>
+					<?php endif; ?>
+					</tbody>
+				</table>
+			</div>
+
+			<details class="jvb-cache-section">
+				<summary><h2>Content-Specific Caches</h2></summary>
+				<table class="wp-list-table widefat fixed striped">
+					<thead>
+					<tr>
+						<th>Cache Group</th>
+						<th>Connected To</th>
+						<th>Actions</th>
+					</tr>
+					</thead>
+					<tbody>
+					<?php foreach ($content_specific as $group => $configs): ?>
+						<tr>
+							<td><strong><?= esc_html($group); ?></strong></td>
+							<td><?= $this->formatConnections($configs); ?></td>
+							<td>
+								<button type="button" class="button" data-action="flush-cache" data-group="<?= esc_attr($group); ?>">
+									<?= jvbDashIcon('trash'); ?> Flush
+								</button>
+							</td>
+						</tr>
+					<?php endforeach; ?>
+					</tbody>
+				</table>
+			</details>
+		</div>
+		<script>
+			(function() {
+				const apiUrl = '<?= esc_js(rest_url('jvb/v1/admin-cache')); ?>';
+				const nonce = '<?= esc_js($nonce); ?>';
+
+				function callCacheAction(action, data = {}) {
+					const body = { action, ...data };
+
+					return fetch(apiUrl, {
+						method: 'POST',
+						headers: {
+							'Content-Type': 'application/json',
+							'X-WP-Nonce': nonce
+						},
+						body: JSON.stringify(body)
+					})
+						.then(response => response.json())
+						.then(data => {
+							if (data.success) {
+								alert(data.message || 'Success!');
+								location.reload();
+							} else {
+								alert('Error: ' + (data.message || 'Unknown error'));
+							}
+						})
+						.catch(error => {
+							alert('Network error: ' + error.message);
+							console.error('Error:', error);
+						});
+				}
+
+				// Flush all caches
+				document.querySelector('[data-action="flush-all"]')?.addEventListener('click', function() {
+					if (confirm('Flush all caches? This may temporarily slow down your site.')) {
+						this.disabled = true;
+						callCacheAction('flush-all');
+					}
+				});
+
+				// Flush individual cache groups
+				document.querySelectorAll('[data-action="flush-cache"]').forEach(btn => {
+					btn.addEventListener('click', function() {
+						const group = this.getAttribute('data-group');
+						if (confirm(`Flush cache group "${group}"?`)) {
+							this.disabled = true;
+							callCacheAction('flush-cache', { group: group });
+						}
+					});
+				});
+			})();
+		</script>
+		<?php
+	}
+
+	protected function isBoundToContentOrTaxonomy(string $group): bool
+	{
+		$group = jvbNoBase($group);
+
+		if (defined('JVB_CONTENT')) {
+			foreach (JVB_CONTENT as $key => $config) {
+				if (jvbNoBase($key) === $group) {
+					return true;
+				}
+			}
+		}
+
+		if (defined('JVB_TAXONOMY')) {
+			foreach (JVB_TAXONOMY as $key => $config) {
+				if (jvbNoBase($key) === $group) {
+					return true;
+				}
+			}
+		}
+
+		return false;
+	}
+
+	protected function formatConnections(array $configs): string
+	{
+		$connections = [];
+		foreach ($configs as $config) {
+			$parent = $config['parent'] ?? 'unknown';
+			$scope = $config['scope'] ?? 'id';
+			$connections[] = "{$parent} ({$scope})";
+		}
+		return esc_html(implode(', ', $connections));
+	}
+
+	public function handleCacheActions($response, $request, $action):WP_REST_Response
+	{
+		if (!str_starts_with($action, 'flush-')) {
+			return $response;
+		}
+
+		if ($action === 'flush-all') {
+			wp_cache_flush();
+			return new WP_REST_Response([
+				'success' => true,
+				'message' => 'All caches flushed successfully'
+			]);
+		}
+
+		if (str_starts_with($action, 'flush-cache')) {
+			$group = $request->get_param('group');
+			if (empty($group)) {
+				return new WP_REST_Response([
+					'success' => false,
+					'message' => 'No cache group specified'
+				], 400);
+			}
+
+			\JVBase\managers\CacheManager::invalidateAll($group);
+
+			return new WP_REST_Response([
+				'success' => true,
+				'message' => "Cache group '{$group}' flushed successfully"
+			]);
+		}
+
+		if ($action === 'merge-icon-versions') {
+			$timestamps = $request->get_param('timestamps');
+
+			if (empty($timestamps) || !is_array($timestamps)) {
+				return new WP_REST_Response([
+					'success' => false,
+					'message' => 'No versions selected for merging'
+				], 400);
+			}
+
+			// Convert to integers
+			$timestamps = array_map('intval', $timestamps);
+
+			if (count($timestamps) < 2) {
+				return new WP_REST_Response([
+					'success' => false,
+					'message' => 'Please select at least 2 versions to merge'
+				], 400);
+			}
+
+			$icons = \JVBase\managers\IconsManager::getInstance();
+
+			if ($icons->mergeVersions($timestamps)) {
+				return new WP_REST_Response([
+					'success' => true,
+					'message' => 'Icon versions merged successfully'
+				]);
+			}
+
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'Failed to merge icon versions'
+			], 500);
+		}
+
+		if ($action === 'refresh-icons') {
+			$icons = \JVBase\managers\IconsManager::getInstance();
+			$icons->forceRefresh();
+
+			return new WP_REST_Response([
+				'success' => true,
+				'message' => 'Icon CSS refresh triggered'
+			]);
+		}
+
+		if ($action === 'restore-icon-version') {
+			$timestamp = (int)$request->get_param('timestamp');
+			$icons = \JVBase\managers\IconsManager::getInstance();
+
+			if ($icons->restoreVersion($timestamp)) {
+				return new WP_REST_Response([
+					'success' => true,
+					'message' => 'Icon version restored successfully'
+				]);
+			}
+
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'Failed to restore icon version'
+			], 500);
+		}
+
+		return $response;
+	}
+
+	public function renderIconsPage():void
+	{
+		// Get current source from query param or default to 'icons'
+		$current_source = $_GET['icon_source'] ?? 'icons';
+		$current_source = sanitize_text_field($current_source);
+
+		// Get all registered icon sources
+		$all_sources = ['icons', 'forms', 'dash']; // You could get this dynamically if needed
+
+		$icons = \JVBase\managers\IconsManager::for($current_source);
+		$versions = $icons->getVersionHistory();
+		$nonce = wp_create_nonce('wp_rest');
+
+		?>
+		<div class="wrap jvb-admin-wrap">
+			<h1>Icon Management</h1>
+
+			<!-- Source Selector -->
+			<div class="jvb-icon-source-selector">
+				<label for="icon-source-select">Icon Source:</label>
+				<select id="icon-source-select" onchange="window.location.href='<?= admin_url('admin.php?page=' . BASE . 'icons&icon_source='); ?>' + this.value">
+					<?php foreach ($all_sources as $source): ?>
+						<option value="<?= esc_attr($source); ?>" <?= selected($current_source, $source, false); ?>>
+							<?= esc_html(ucfirst($source)); ?>
+						</option>
+					<?php endforeach; ?>
+				</select>
+			</div>
+
+			<div class="jvb-icon-actions">
+				<button type="button" class="button button-primary" data-action="refresh-icons" data-source="<?= esc_attr($current_source); ?>">
+					<?= jvbDashIcon('arrows-clockwise'); ?>
+					Force Refresh CSS
+				</button>
+				<button type="button" class="button" data-action="merge-icon-versions" data-source="<?= esc_attr($current_source); ?>" id="merge-versions-btn" disabled>
+					<?= jvbDashIcon('git-merge'); ?>
+					Merge Selected Versions
+				</button>
+			</div>
+
+			<h2>Version History for <?= esc_html(ucfirst($current_source)); ?></h2>
+			<table class="wp-list-table widefat fixed striped">
+				<thead>
+				<tr>
+					<th class="check-column">
+						<input type="checkbox" id="select-all-versions">
+						<label for="select-all-versions" class="screen-reader-text">Select All</label>
+					</th>
+					<th>Date/Time</th>
+					<th>Icon Count</th>
+					<th>File Size</th>
+					<th>Actions</th>
+				</tr>
+				</thead>
+				<tbody>
+				<?php if (empty($versions)): ?>
+					<tr><td colspan="5">No version history available</td></tr>
+				<?php else: ?>
+					<?php foreach (array_reverse($versions) as $index => $version): ?>
+						<tr>
+							<th class="check-column">
+								<input type="checkbox"
+									   name="version-select"
+									   class="version-checkbox"
+									   value="<?= esc_attr($version['timestamp']); ?>">
+							</th>
+							<td><?= esc_html(date('Y-m-d H:i:s', $version['timestamp'])); ?></td>
+							<td>
+								<?= esc_html($version['icon_count']); ?> icons
+								<button type="button"
+										class="button-link view-icon-list-btn"
+										data-timestamp="<?= esc_attr($version['timestamp']); ?>">
+									(view)
+								</button>
+							</td>
+							<td><?= esc_html($version['size_formatted']); ?></td>
+							<td>
+								<button type="button" class="button restore-version-btn"
+										data-action="restore-icon-version"
+										data-source="<?= esc_attr($current_source); ?>"
+										data-timestamp="<?= esc_attr($version['timestamp']); ?>">
+									<?= jvbDashIcon('arrow-counter-clockwise'); ?> Restore
+								</button>
+							</td>
+						</tr>
+						<tr id="icon-list-<?= esc_attr($version['timestamp']); ?>" class="icon-list-row" style="display: none;">
+							<td colspan="5">
+								<div class="icon-list-content">
+									<?php foreach ($version['iconList'] as $style => $icons): ?>
+										<strong><?= esc_html(ucfirst($style)); ?>:</strong>
+										<?= esc_html(implode(', ', $icons)); ?><br>
+									<?php endforeach; ?>
+								</div>
+							</td>
+						</tr>
+					<?php endforeach; ?>
+				<?php endif; ?>
+				</tbody>
+			</table>
+		</div>
+
+		<script>
+			(function() {
+				const apiUrl = '<?= esc_js(rest_url('jvb/v1/admin-icons')); ?>';
+				const nonce = '<?= esc_js($nonce); ?>';
+				const currentSource = '<?= esc_js($current_source); ?>';
+
+				// Helper function for API calls
+				function callIconAction(action, data = {}) {
+					const body = { action, source: currentSource, ...data };
+
+					return fetch(apiUrl, {
+						method: 'POST',
+						headers: {
+							'Content-Type': 'application/json',
+							'X-WP-Nonce': nonce
+						},
+						body: JSON.stringify(body)
+					})
+						.then(response => response.json())
+						.then(data => {
+							if (data.success) {
+								alert(data.message || 'Success!');
+								location.reload();
+							} else {
+								alert('Error: ' + (data.message || 'Unknown error'));
+							}
+							return data;
+						})
+						.catch(error => {
+							alert('Network error: ' + error.message);
+							console.error('Error:', error);
+						});
+				}
+
+				// Enable/disable merge button based on selection
+				document.querySelectorAll('.version-checkbox').forEach(checkbox => {
+					checkbox.addEventListener('change', function() {
+						const checkedCount = document.querySelectorAll('.version-checkbox:checked').length;
+						document.getElementById('merge-versions-btn').disabled = checkedCount < 2;
+					});
+				});
+
+				// Select all functionality
+				const selectAll = document.getElementById('select-all-versions');
+				if (selectAll) {
+					selectAll.addEventListener('change', function() {
+						document.querySelectorAll('.version-checkbox').forEach(checkbox => {
+							checkbox.checked = this.checked;
+							checkbox.dispatchEvent(new Event('change'));
+						});
+					});
+				}
+
+				// Toggle icon list view
+				document.querySelectorAll('.view-icon-list-btn').forEach(btn => {
+					btn.addEventListener('click', function() {
+						const timestamp = this.getAttribute('data-timestamp');
+						const row = document.getElementById('icon-list-' + timestamp);
+						if (row) {
+							row.style.display = row.style.display === 'none' ? '' : 'none';
+						}
+					});
+				});
+
+				// Force refresh button
+				document.querySelector('[data-action="refresh-icons"]')?.addEventListener('click', function() {
+					if (confirm('Force regenerate icon CSS? This will reload the page.')) {
+						this.disabled = true;
+						callIconAction('refresh-icons');
+					}
+				});
+
+				// Merge versions button
+				document.getElementById('merge-versions-btn')?.addEventListener('click', function() {
+					const checkboxes = document.querySelectorAll('.version-checkbox:checked');
+					const timestamps = Array.from(checkboxes).map(cb => parseInt(cb.value));
+
+					if (timestamps.length < 2) {
+						alert('Please select at least 2 versions to merge');
+						return;
+					}
+
+					if (confirm(`Merge ${timestamps.length} versions? This will create a new CSS file with all unique icons.`)) {
+						this.disabled = true;
+						callIconAction('merge-icon-versions', { timestamps: timestamps });
+					}
+				});
+
+				// Restore version buttons
+				document.querySelectorAll('.restore-version-btn').forEach(btn => {
+					btn.addEventListener('click', function() {
+						const timestamp = parseInt(this.getAttribute('data-timestamp'));
+
+						if (confirm('Restore this icon version? This will reload the page.')) {
+							this.disabled = true;
+							callIconAction('restore-icon-version', { timestamp: timestamp });
+						}
+					});
+				});
+			})();
+		</script>
+		<?php
+	}
+
+	public static function addSubpage(string $key, array $value):void
+	{
+		$option = get_option(BASE.'adminSubpage', []);
+		if (empty($option) || !array_key_exists($key, $option)) {
+			$option[$key] = $value;
+			update_option(BASE.'adminSubpage', $option);
+		}
+	}
+
+	/**
+	 * Handle admin page form submissions
+	 * Fires after WordPress admin_init
+	 */
+	public function handleAdminPageSubmission(): void
+	{
+		// Only process on our admin pages
+		if (!isset($_GET['page']) || strpos($_GET['page'], BASE) !== 0) {
+			return;
+		}
+
+		// Check for form submission
+		if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['submit'])) {
+			return;
+		}
+
+		// Verify nonce
+		$nonce_field = BASE . 'admin_page_nonce';
+		if (!isset($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], $nonce_field)) {
+			add_action('admin_notices', function() {
+				echo '<div class="notice notice-error"><p>Security check failed. Please try again.</p></div>';
+			});
+			return;
+		}
+
+		$page_slug = sanitize_text_field($_GET['page']);
+
+		// Allow other classes to handle their page submissions
+		$result = apply_filters('jvb_admin_page_submission', null, $page_slug, $_POST);
+
+		// Store result in transient for after redirect
+		if (is_array($result)) {
+			set_transient(BASE . 'admin_notice_' . get_current_user_id(), $result, 30);
+		}
+
+		// Redirect to prevent form resubmission (POST-Redirect-GET pattern)
+		wp_safe_redirect(add_query_arg(['page' => $page_slug], admin_url('admin.php')));
+		exit;
+	}
+
+	/**
+	 * Display admin notices from form submissions
+	 */
+	public function displayAdminNotices(): void
+	{
+		$notice = get_transient(BASE . 'admin_notice_' . get_current_user_id());
+
+		if ($notice && is_array($notice)) {
+			delete_transient(BASE . 'admin_notice_' . get_current_user_id());
+
+			$type = $notice['success'] ? 'success' : 'error';
+			$message = $notice['message'] ?? 'Settings saved.';
+
+			echo '<div class="notice notice-' . esc_attr($type) . ' is-dismissible"><p>' . esc_html($message) . '</p></div>';
+		}
+	}
 }

--
Gitblit v1.10.0