Jake Vanderwerf
2026-01-28 8c6502de2f8ec2bd8382cd6945c327d7be400e14
inc/managers/AdminPages.php
@@ -34,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();
@@ -48,6 +48,9 @@
      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']);
    }
   /**
@@ -95,10 +98,10 @@
      switch ($action) {
         case 'flush-all':
            wp_cache_flush();
            $total = Cache::flushAll();
            return new \WP_REST_Response([
               'success' => true,
               'message' => 'All caches flushed successfully'
               'message' => $total.' caches flushed successfully'
            ]);
         case 'flush-cache':
@@ -110,7 +113,7 @@
               ], 400);
            }
            \JVBase\managers\CacheManager::invalidateAll($group);
            Cache::for($group)?->flush();
            return new \WP_REST_Response([
               'success' => true,
@@ -131,14 +134,30 @@
   public function handleIconAction(\WP_REST_Request $request): \WP_REST_Response
   {
      $action = sanitize_text_field($request->get_param('action'));
      $icons = \JVBase\managers\IconsManager::getInstance();
      $source = sanitize_text_field($request->get_param('source') ?? 'icons');
      $icons = IconsManager::for($source);
      switch ($action) {
         case 'refresh-icons':
            // Force regenerate CSS immediately
            $icons->forceRefresh();
            IconsManager::regenerateAllCSS([$source => true]);
            return new \WP_REST_Response([
               'success' => true,
               'message' => 'Icon CSS regenerated successfully'
               'message' => "Icon CSS regenerated successfully for '{$source}'"
            ]);
         case 'refresh-all-icons':
            // Regenerate all icon sources
            foreach (['icons', 'forms', 'dash'] as $src) {
               IconsManager::for($src)->forceRefresh();
            }
            IconsManager::regenerateAllCSS();
            return new \WP_REST_Response([
               'success' => true,
               'message' => 'All icon CSS files regenerated successfully'
            ]);
         case 'restore-icon-version':
@@ -182,6 +201,9 @@
            }
            if ($icons->mergeVersions($timestamps)) {
               // Regenerate CSS after merge
               IconsManager::regenerateAllCSS([$source => true]);
               return new \WP_REST_Response([
                  'success' => true,
                  'message' => 'Icon versions merged successfully'
@@ -434,7 +456,7 @@
    protected function renderStatusItems():void
    {
        // Get queue stats
        $queue_status = JVB()->queue()->getQueueStatus();
        $queue_status = JVB()->queue()->getStatus();
      error_log('Queue Status: '.print_r($queue_status, true));
        // Other system checks
@@ -579,9 +601,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
            }
@@ -636,7 +658,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);
@@ -655,20 +677,22 @@
   public function renderCachePage():void
   {
      $connections = CacheManager::getAllConnections();
      $groups = Cache::getAllGroups();
      // 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);
      // Separate by type
      $generic = [];
      $specific = [];
         if ($is_generic) {
            $generic_groups[$group] = $configs;
      foreach ($groups as $group => $data) {
         if ($this->isBoundToContentOrTaxonomy($group)) {
            $specific[$group] = $data;
         } else {
            $content_specific[$group] = $configs;
            $generic[$group] = $data;
         }
      }
@@ -678,7 +702,7 @@
         <div class="jvb-cache-actions">
            <button type="button" class="button button-primary" data-action="flush-all">
               <?= jvbIcon('arrows-clockwise'); ?>
               <?= jvbDashIcon('arrows-clockwise'); ?>
               Flush All Caches
            </button>
         </div>
@@ -703,7 +727,7 @@
                        <td><?= $this->formatConnections($configs); ?></td>
                        <td>
                           <button type="button" class="button" data-action="flush-cache" data-group="<?= esc_attr($group); ?>">
                              <?= jvbIcon('trash'); ?> Flush
                              <?= jvbDashIcon('trash'); ?> Flush
                           </button>
                        </td>
                     </tr>
@@ -724,13 +748,26 @@
               </tr>
               </thead>
               <tbody>
               <?php foreach ($content_specific as $group => $configs): ?>
               <?php foreach ($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); ?>">
                           <?= jvbIcon('trash'); ?> Flush
                           <?= jvbDashIcon('trash'); ?> Flush
                        </button>
                     </td>
                  </tr>
               <?php endforeach; ?>
               <?php foreach ($generic as $group => $data): ?>
                  <tr>
                     <td><strong><?= esc_html($group); ?></strong></td>
                     <td><?= $this->formatConnections($data); ?></td>
                     <td>
                        <button type="button" class="button"
                              data-action="flush-cache"
                              data-group="<?= esc_attr($group); ?>">
                           <?= jvbDashIcon('trash'); ?> Flush
                        </button>
                     </td>
                  </tr>
@@ -816,15 +853,24 @@
      return false;
   }
   protected function formatConnections(array $configs): string
   protected function formatConnections(array $data): string
   {
      $connections = [];
      foreach ($configs as $config) {
         $parent = $config['parent'] ?? 'unknown';
         $scope = $config['scope'] ?? 'id';
         $connections[] = "{$parent} ({$scope})";
      $parts = [];
      if (!empty($data['connects_to'])) {
         $targets = array_map(function($conn) {
            $flush_text = $conn['flush'] ? ' (flush all)' : '';
            return $conn['group'] . $flush_text;
         }, $data['connects_to']);
         $parts[] = '<strong>Invalidates:</strong> ' . implode(', ', $targets);
      }
      return esc_html(implode(', ', $connections));
      if (!empty($data['connected_from'])) {
         $sources = array_map(fn($conn) => $conn['group'], $data['connected_from']);
         $parts[] = '<strong>Invalidated by:</strong> ' . implode(', ', $sources);
      }
      return $parts ? implode('<br>', $parts) : 'No connections';
   }
   public function handleCacheActions($response, $request, $action):WP_REST_Response
@@ -850,7 +896,8 @@
            ], 400);
         }
         \JVBase\managers\CacheManager::invalidateAll($group);
         $group = sanitize_text_field($request->get_param('group'));
         Cache::invalidateGroup($group);
         return new WP_REST_Response([
            'success' => true,
@@ -858,74 +905,19 @@
         ]);
      }
      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
   {
      $icons = \JVBase\managers\IconsManager::getInstance();
      // 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 = IconsManager::for($current_source);
      $versions = $icons->getVersionHistory();
      $nonce = wp_create_nonce('wp_rest');
@@ -933,18 +925,30 @@
      <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">
               <?= jvbIcon('arrows-clockwise'); ?>
            <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" id="merge-versions-btn" disabled>
               <?= jvbIcon('git-merge'); ?>
            <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</h2>
         <h2>Version History for <?= esc_html(ucfirst($current_source)); ?></h2>
         <table class="wp-list-table widefat fixed striped">
            <thead>
            <tr>
@@ -974,18 +978,18 @@
                     <td>
                        <?= esc_html($version['icon_count']); ?> icons
                        <button type="button"
                              class="button-link"
                              data-action="view-icon-list"
                              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"
                        <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']); ?>">
                           <?= jvbIcon('arrow-counter-clockwise'); ?> Restore
                           <?= jvbDashIcon('arrow-counter-clockwise'); ?> Restore
                        </button>
                     </td>
                  </tr>
@@ -1009,10 +1013,11 @@
         (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, ...data };
               const body = { action, source: currentSource, ...data };
               return fetch(apiUrl, {
                  method: 'POST',
@@ -1069,34 +1074,28 @@
            });
            // Force refresh button
            const refreshBtn = document.getElementById('refresh-icons-btn');
            if (refreshBtn) {
               refreshBtn.addEventListener('click', function() {
                  if (confirm('Force regenerate icon CSS? This will reload the page.')) {
                     this.disabled = true;
                     callIconAction('refresh-icons');
                  }
               });
            }
            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
            const mergeBtn = document.getElementById('merge-versions-btn');
            if (mergeBtn) {
               mergeBtn.addEventListener('click', function() {
                  const checkboxes = document.querySelectorAll('.version-checkbox:checked');
                  const timestamps = Array.from(checkboxes).map(cb => parseInt(cb.value));
            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 (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 });
                  }
               });
            }
               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 => {
@@ -1113,4 +1112,70 @@
      </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>';
      }
   }
}