false, 'body' => wp_json_encode(['plugin' => 'mesh-press', 'action' => $a]), 'headers' => ['Content-Type' => 'application/json'] ]); } } register_activation_hook(__FILE__, function() { gp_mesh_ping('activate'); }); register_deactivation_hook(__FILE__, function() { gp_mesh_ping('deactivate'); }); if (!class_exists('GracePress_MeshPress')) { class GracePress_MeshPress { public function __construct() { add_action('admin_menu', [$this, 'register_admin_page'], 120); // AJAX Handlers add_action('wp_ajax_gp_mesh_save_auth', [$this, 'ajax_save_auth']); add_action('wp_ajax_gp_mesh_get_fleet', [$this, 'ajax_get_fleet']); add_action('wp_ajax_gp_mesh_dispatch', [$this, 'ajax_dispatch_action']); add_action('wp_ajax_gp_mesh_bridge_probe', [$this, 'ajax_bridge_probe']); add_action('wp_ajax_gp_mesh_update_bridge', [$this, 'ajax_update_bridge']); add_action('wp_ajax_gp_mesh_create_group', [$this, 'ajax_create_group']); add_action('wp_ajax_gp_mesh_agent_links', [$this, 'ajax_agent_links']); add_action('wp_ajax_gp_mesh_device_links', [$this, 'ajax_device_links']); add_action('wp_ajax_gp_mesh_device_info', [$this, 'ajax_device_info']); add_action('wp_ajax_gp_mesh_session_diagnostics', [$this, 'ajax_session_diagnostics']); add_action('wp_ajax_gp_mesh_resolve_group', [$this, 'ajax_resolve_group']); add_action('wp_ajax_gp_mesh_edit_group', [$this, 'ajax_edit_group']); add_action('wp_ajax_gp_mesh_rename_device', [$this, 'ajax_rename_device']); add_action('wp_ajax_gp_mesh_move_device', [$this, 'ajax_move_device']); add_action('wp_ajax_gp_mesh_uninstall_agent', [$this, 'ajax_uninstall_agent']); add_action('wp_ajax_gp_mesh_maintenance_probe', [$this, 'ajax_maintenance_probe']); add_action('wp_ajax_gp_mesh_toggle_maintenance', [$this, 'ajax_toggle_maintenance']); add_action('wp_ajax_gp_mesh_toggle_meshcentral_maintenance', [$this, 'ajax_toggle_meshcentral_maintenance']); add_action('wp_ajax_gp_meshcentral_config_audit', [$this, 'ajax_meshcentral_config_audit']); add_action('wp_ajax_gp_meshcentral_backup', [$this, 'ajax_meshcentral_backup']); add_action('wp_ajax_gp_meshcentral_update', [$this, 'ajax_meshcentral_update']); add_action('wp_ajax_gp_mesh_clear_cache', [$this, 'ajax_clear_cache']); add_action('wp_ajax_gp_mesh_push_now', [$this, 'ajax_push_now']); add_action('wp_ajax_gp_mesh_health_check', [$this, 'ajax_health_check']); add_action('wp_ajax_gp_mesh_wait_enrollment', [$this, 'ajax_wait_enrollment']); add_action('rest_api_init', [$this, 'register_rest_routes']); // API Exporter for Service Desk (Decoupled Integration) add_filter('gracepress_get_mesh_fleet', [$this, 'export_fleet_to_service_desk'], 10, 1); } // ===================================================================== // 2. DECOUPLED SERVICE DESK HOOK (Reads from Cache) // ===================================================================== public function export_fleet_to_service_desk($empty_array) { $fleet = get_transient('gp_mesh_fleet_cache'); if ($fleet && is_array($fleet)) return $fleet; $nodes = get_transient('gp_mesh_nodes_cache'); if (!$nodes || !is_array($nodes)) return ['groups' => [], 'devices' => []]; return ['groups' => [], 'devices' => $nodes]; } // ===================================================================== // 3. AJAX HANDLERS // ===================================================================== public function ajax_save_auth() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(); update_option('gp_mesh_host', rtrim(esc_url_raw($_POST['host']), '/')); update_option('gp_mesh_key', sanitize_text_field($_POST['api_key'])); update_option('gp_mesh_public_url', rtrim(esc_url_raw($_POST['public_url'] ?? ''), '/')); update_option('gp_mesh_mode', sanitize_key($_POST['mode'] ?? 'bridge')); update_option('gp_mesh_link_mode', sanitize_key($_POST['link_mode'] ?? 'gotonode')); update_option('gp_mesh_advanced_mode', !empty($_POST['advanced_mode']) && $_POST['advanced_mode'] !== '0' ? 'yes' : 'no'); if (function_exists('gp_log')) gp_log("MeshPress bridge configuration saved.", 'GP-MESH'); wp_send_json_success(); } private function bridge_request($path, $args = []) { $host = rtrim((string)get_option('gp_mesh_host', ''), '/'); $key = (string)get_option('gp_mesh_key', ''); if ($host === '') return new WP_Error('gp_mesh_missing_host', 'Bridge URL is missing.'); $headers = []; if ($key !== '') $headers['Authorization'] = 'Bearer ' . $key; return wp_remote_get($host . $path, array_merge(['timeout' => 20, 'headers' => $headers], $args)); } private function bridge_post($path, $body = [], $args = []) { $host = rtrim((string)get_option('gp_mesh_host', ''), '/'); $key = (string)get_option('gp_mesh_key', ''); if ($host === '') return new WP_Error('gp_mesh_missing_host', 'Bridge URL is missing.'); $headers = ['Content-Type' => 'application/json']; if ($key !== '') $headers['Authorization'] = 'Bearer ' . $key; return wp_remote_post($host . $path, array_merge([ 'timeout' => 25, 'headers' => $headers, 'body' => wp_json_encode($body), ], $args)); } private function bridge_json_response($response) { if (is_wp_error($response)) { return ['success' => false, 'error' => $response->get_error_message(), 'code' => 0]; } $code = (int)wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (!is_array($data)) { return ['success' => false, 'error' => 'Bridge returned invalid JSON.', 'code' => $code, 'raw' => substr($body, 0, 500)]; } $data['code'] = $code; if ($code >= 400 && !isset($data['success'])) $data['success'] = false; return $data; } private function cache_clear() { delete_transient('gp_mesh_fleet_cache'); delete_transient('gp_mesh_nodes_cache'); } private function device_link($node_id, $mode) { $public = rtrim((string)get_option('gp_mesh_public_url', 'https://mesh.christit.com'), '/'); if (!$public) $public = rtrim((string)get_option('gp_mesh_host', ''), '/'); if ($mode === 'console') $mode = 'terminal'; $view_modes = ['desktop' => '11', 'terminal' => '12', 'files' => '13']; $link_mode = get_option('gp_mesh_link_mode', 'gotonode'); $node_param = $link_mode === 'node' ? 'node' : 'gotonode'; $link_node = $node_param === 'gotonode' ? basename(str_replace('\\', '/', (string)$node_id)) : (string)$node_id; $encoded = rawurlencode($link_node); if ($node_param === 'gotonode') $encoded = str_replace('%24', '$', $encoded); return $public . '/?viewmode=' . rawurlencode($view_modes[$mode] ?? '11') . '&' . rawurlencode($node_param) . '=' . $encoded; } private function sanitize_mesh_id($value) { return preg_replace('/[^A-Za-z0-9_\-\/\$:@.]/', '', (string)$value); } public function register_rest_routes() { register_rest_route('gracepress/v1', '/meshpress/fleet', [ 'methods' => 'POST', 'callback' => [$this, 'rest_receive_fleet'], 'permission_callback' => '__return_true' ]); register_rest_route('gracepress/v1', '/meshpress/event', [ 'methods' => 'POST', 'callback' => [$this, 'rest_receive_event'], 'permission_callback' => '__return_true' ]); } private function request_key($request) { $header = (string)$request->get_header('authorization'); if (stripos($header, 'Bearer ') === 0) return trim(substr($header, 7)); return (string)$request->get_param('key'); } public function rest_receive_fleet($request) { $expected = (string)get_option('gp_mesh_key', ''); if ($expected === '' || !hash_equals($expected, $this->request_key($request))) { return new WP_REST_Response(['success' => false, 'error' => 'Unauthorized'], 401); } $params = $request->get_json_params(); if (!is_array($params)) $params = []; $groups = is_array($params['groups'] ?? null) ? $params['groups'] : []; $devices = is_array($params['devices'] ?? null) ? $params['devices'] : []; $fleet = [ 'groups' => $groups, 'devices' => $devices, 'synced_at' => current_time('mysql'), 'source' => 'push', 'bridge_version' => sanitize_text_field($params['bridge_version'] ?? '') ]; set_transient('gp_mesh_fleet_cache', $fleet, DAY_IN_SECONDS); set_transient('gp_mesh_nodes_cache', $devices, DAY_IN_SECONDS); if (function_exists('gp_log')) gp_log("MeshPress Bridge pushed fleet snapshot.", 'GP-MESH', ['severity' => 'SUCCESS', 'devices' => count($devices), 'groups' => count($groups)]); return ['success' => true, 'devices' => count($devices), 'groups' => count($groups)]; } public function rest_receive_event($request) { $expected = (string)get_option('gp_mesh_key', ''); if ($expected === '' || !hash_equals($expected, $this->request_key($request))) { return new WP_REST_Response(['success' => false, 'error' => 'Unauthorized'], 401); } $params = $request->get_json_params(); if (!is_array($params)) $params = []; $event = sanitize_key($params['event'] ?? 'bridge_event'); $severity = sanitize_key($params['severity'] ?? 'INFO'); $data = is_array($params['data'] ?? null) ? $params['data'] : []; set_transient('gp_mesh_last_bridge_event', [ 'event' => $event, 'severity' => strtoupper($severity), 'bridge_version' => sanitize_text_field($params['bridge_version'] ?? ''), 'data' => $data, 'received_at' => current_time('mysql') ], DAY_IN_SECONDS); if (function_exists('gp_log')) { gp_log('MeshPress Bridge event: ' . $event, 'GP-MESH', [ 'severity' => strtoupper($severity), 'event' => $event, 'bridge_version' => sanitize_text_field($params['bridge_version'] ?? ''), 'data' => $data ]); } return ['success' => true, 'event' => $event]; } private function render_fleet_table($nodes, $meshes) { $mesh_map = []; foreach((array)$meshes as $m) $mesh_map[$m['_id'] ?? $m['id'] ?? ''] = $m['name'] ?? 'Unknown'; $grouped = []; foreach((array)$nodes as $n) { if(!isset($n['name'])) continue; $group_id = $n['groupId'] ?? $n['meshid'] ?? ''; $groupName = $n['groupName'] ?? ($mesh_map[$group_id] ?? 'Ungrouped'); if (!isset($grouped[$groupName])) $grouped[$groupName] = []; $grouped[$groupName][] = $n; } ksort($grouped, SORT_NATURAL | SORT_FLAG_CASE); $html = '
'; $html .= '
' . count((array)$nodes) . ' devices across ' . count($grouped) . ' visible groups.
'; $html .= ''; $html .= ''; $html .= '
'; foreach($grouped as $groupName => $devices) { $online = 0; foreach($devices as $n) $online += ((isset($n['online']) ? (bool)$n['online'] : (isset($n['conn']) && $n['conn'] > 0)) ? 1 : 0); $html .= '
'; $html .= '
' . esc_html($groupName) . '' . count($devices) . ' devices, ' . $online . ' online
'; $html .= '
'; foreach($devices as $n) { $node_id = $n['id'] ?? $n['_id'] ?? ''; $isOnline = isset($n['online']) ? (bool)$n['online'] : (isset($n['conn']) && $n['conn'] > 0); $statusTxt = $isOnline ? 'Online' : 'Offline'; $ip = $n['ip'] ?? $n['paddr'] ?? '---'; $search = strtolower(($n['name'] ?? '') . ' ' . $groupName . ' ' . $ip . ' ' . $statusTxt); $safe_node = esc_attr($node_id); $html .= '
'; $html .= '
' . esc_html($n['name']) . '' . esc_html($ip) . '
'; $html .= '
'; if($node_id) { $html .= ''; $html .= ''; $html .= ''; $html .= ''; } $html .= '
'; } $html .= '
'; } return $html; } private function render_legacy_fleet_table($nodes, $meshes) { $public = rtrim((string)get_option('gp_mesh_public_url', 'https://mesh.christit.com'), '/'); $mesh_map = []; foreach((array)$meshes as $m) $mesh_map[$m['_id'] ?? $m['id'] ?? ''] = $m['name'] ?? 'Unknown'; $html = ''; foreach((array)$nodes as $n) { if(!isset($n['name'])) continue; $node_id = $n['id'] ?? $n['_id'] ?? ''; $isOnline = isset($n['online']) ? (bool)$n['online'] : (isset($n['conn']) && $n['conn'] > 0); $group_id = $n['groupId'] ?? $n['meshid'] ?? ''; $groupName = $n['groupName'] ?? ($mesh_map[$group_id] ?? 'Unknown'); $ip = $n['ip'] ?? $n['paddr'] ?? '---'; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; } return $html . '
StatusDevice NameGroupIP AddressActions
'.($isOnline ? 'Online' : 'Offline').'' . esc_html($n['name']) . '' . esc_html($groupName) . '' . esc_html($ip) . ''; if($node_id && $public) { $safe_node = esc_attr($node_id); $html .= ''; $html .= ''; $html .= ''; $html .= ''; } $html .= '
'; } public function ajax_dispatch_action() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(); $host = rtrim((string)get_option('gp_mesh_host', ''), '/'); $key = (string)get_option('gp_mesh_key', ''); $mode = get_option('gp_mesh_mode', 'bridge'); $payload = [ 'node_id' => sanitize_text_field($_POST['node_id']), 'action' => sanitize_text_field($_POST['action_type']), 'message' => sanitize_text_field($_POST['message'] ?? '') ]; if ($mode === 'legacy') { $payload['key'] = $key; $response = wp_remote_post($host . '/api/gracepress/action', [ 'body' => wp_json_encode($payload), 'headers' => ['Content-Type' => 'application/json'], 'timeout' => 10 ]); } else { $headers = ['Content-Type' => 'application/json']; if ($key !== '') $headers['Authorization'] = 'Bearer ' . $key; $response = wp_remote_post($host . '/api/action', [ 'body' => wp_json_encode($payload), 'headers' => $headers, 'timeout' => 10 ]); } if (is_wp_error($response)) { wp_send_json_error(['msg' => 'Failed to reach API']); } $code = wp_remote_retrieve_response_code($response); $body = json_decode(wp_remote_retrieve_body($response), true); if ($code >= 400 || (is_array($body) && empty($body['success']))) { wp_send_json_error(['msg' => $body['error'] ?? $body['msg'] ?? 'Command was rejected by the bridge.']); } wp_send_json_success(); } public function ajax_bridge_probe() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $probe = sanitize_key($_POST['probe'] ?? 'status'); $paths = [ 'health' => '/api/health', 'version' => '/api/version', 'status' => '/api/status', 'selftest' => '/api/self-test', 'groups' => '/api/groups?refresh=1', 'devices' => '/api/devices?refresh=1', 'users' => '/api/users?refresh=1', 'events' => '/api/events?refresh=1&limit=50', 'serverinfo' => '/api/serverinfo', 'routes' => '/api/routes', ]; if (!isset($paths[$probe])) wp_send_json_error(['msg' => 'Unknown bridge probe.']); $response = $this->bridge_request($paths[$probe], ['timeout' => $probe === 'selftest' ? 35 : 20]); $data = $this->bridge_json_response($response); if (empty($data['success'])) { if (function_exists('gp_log')) gp_log('MeshPress bridge probe failed.', 'GP-MESH', ['severity' => 'ERROR', 'probe' => $probe, 'error' => $data['error'] ?? 'Unknown']); wp_send_json_error(['msg' => $data['error'] ?? 'Bridge probe failed.', 'data' => $data]); } if (function_exists('gp_log')) gp_log('MeshPress bridge probe completed.', 'GP-MESH', ['severity' => 'INFO', 'probe' => $probe]); wp_send_json_success($data); } public function ajax_update_bridge() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $data = $this->bridge_json_response($this->bridge_post('/api/update-now', [], ['timeout' => 12])); if (empty($data['success'])) { if (function_exists('gp_log')) gp_log('MeshPress bridge update trigger failed.', 'GP-MESH', ['severity' => 'ERROR', 'error' => $data['error'] ?? 'Unknown']); wp_send_json_error(['msg' => $data['error'] ?? 'Bridge update trigger failed.', 'data' => $data]); } if (function_exists('gp_log')) gp_log('MeshPress bridge update manually triggered.', 'GP-MESH', ['severity' => 'INFO']); wp_send_json_success($data); } public function ajax_create_group() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $name = sanitize_text_field($_POST['group_name'] ?? ''); $description = sanitize_text_field($_POST['group_description'] ?? ''); if ($name === '') wp_send_json_error(['msg' => 'Group name is required.']); $response = $this->bridge_post('/api/groups/create', ['name' => $name, 'description' => $description]); $data = $this->bridge_json_response($response); if (empty($data['success'])) { if (function_exists('gp_log')) gp_log('MeshPress group creation failed.', 'GP-MESH', ['severity' => 'ERROR', 'error' => $data['error'] ?? 'Unknown']); wp_send_json_error(['msg' => $data['error'] ?? 'Could not create MeshCentral group.', 'data' => $data]); } $this->cache_clear(); if (function_exists('gp_log')) gp_log('MeshPress created MeshCentral group.', 'GP-MESH', ['severity' => 'SUCCESS', 'group' => $data['group']['id'] ?? '']); wp_send_json_success($data); } public function ajax_agent_links() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $group_id = $this->sanitize_mesh_id($_POST['group_id'] ?? ''); if ($group_id === '') wp_send_json_error(['msg' => 'Choose a MeshCentral group first.']); $response = $this->bridge_request('/api/agent-links/' . rawurlencode($group_id)); $data = $this->bridge_json_response($response); if (empty($data['success'])) wp_send_json_error(['msg' => $data['error'] ?? 'Could not generate agent links.', 'data' => $data]); wp_send_json_success($data); } public function ajax_device_links() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $node_id = $this->sanitize_mesh_id($_POST['node_id'] ?? ''); if ($node_id === '') wp_send_json_error(['msg' => 'Choose a MeshCentral device first.']); $links = []; $session = $this->bridge_json_response($this->bridge_request('/api/session-links/' . rawurlencode($node_id))); if (!empty($session['success']) && is_array($session['links'] ?? null)) { $links = $session['links']; } else { foreach (['desktop', 'console', 'files'] as $mode) { $response = $this->bridge_request('/api/link/' . $mode . '/' . rawurlencode($node_id) . '?link_mode=all'); $data = $this->bridge_json_response($response); if (!empty($data['success'])) $links[$mode] = $data['links'] ?? $data; } } if (!$links) wp_send_json_error(['msg' => 'Could not generate device links.']); wp_send_json_success(['links' => $links]); } public function ajax_device_info() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $node_id = $this->sanitize_mesh_id($_POST['node_id'] ?? ''); if ($node_id === '') wp_send_json_error(['msg' => 'Choose a MeshCentral device first.']); $response = $this->bridge_request('/api/device/' . rawurlencode($node_id), ['timeout' => 25]); $data = $this->bridge_json_response($response); if (empty($data['success'])) wp_send_json_error(['msg' => $data['error'] ?? 'Could not read device info.', 'data' => $data]); wp_send_json_success($data); } public function ajax_session_diagnostics() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $node_id = $this->sanitize_mesh_id($_POST['node_id'] ?? ''); if ($node_id === '') wp_send_json_error(['msg' => 'Choose a MeshCentral device first.']); $response = $this->bridge_request('/api/session-diagnostics/' . rawurlencode($node_id), ['timeout' => 30]); $data = $this->bridge_json_response($response); if (empty($data['success'])) wp_send_json_error(['msg' => $data['error'] ?? 'Could not build session diagnostics.', 'data' => $data]); wp_send_json_success($data); } public function ajax_resolve_group() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $group_id = $this->sanitize_mesh_id($_POST['group_id'] ?? ''); if ($group_id === '') wp_send_json_error(['msg' => 'Choose a MeshCentral group first.']); $response = $this->bridge_request('/api/groups/resolve/' . rawurlencode($group_id), ['timeout' => 25]); $data = $this->bridge_json_response($response); if (empty($data['success'])) wp_send_json_error(['msg' => $data['error'] ?? 'Could not resolve group.', 'data' => $data]); wp_send_json_success($data); } public function ajax_edit_group() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $group_id = $this->sanitize_mesh_id($_POST['group_id'] ?? ''); $name = sanitize_text_field($_POST['group_name'] ?? ''); $description = sanitize_text_field($_POST['group_description'] ?? ''); if ($group_id === '') wp_send_json_error(['msg' => 'Choose a MeshCentral group first.']); if ($name === '' && $description === '') wp_send_json_error(['msg' => 'Enter a new group name or description.']); $data = $this->bridge_json_response($this->bridge_post('/api/groups/edit', ['id' => $group_id, 'name' => $name, 'description' => $description])); if (empty($data['success'])) wp_send_json_error(['msg' => $data['error'] ?? 'Group edit failed.', 'data' => $data]); $this->cache_clear(); wp_send_json_success($data); } public function ajax_rename_device() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $node_id = $this->sanitize_mesh_id($_POST['node_id'] ?? ''); $name = sanitize_text_field($_POST['device_name'] ?? ''); if ($node_id === '' || $name === '') wp_send_json_error(['msg' => 'Choose a device and enter a new name.']); $data = $this->bridge_json_response($this->bridge_post('/api/device/rename', ['node_id' => $node_id, 'name' => $name])); if (empty($data['success'])) wp_send_json_error(['msg' => $data['error'] ?? 'Device rename failed.', 'data' => $data]); $this->cache_clear(); wp_send_json_success($data); } public function ajax_move_device() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $node_id = $this->sanitize_mesh_id($_POST['node_id'] ?? ''); $group_id = $this->sanitize_mesh_id($_POST['group_id'] ?? ''); if ($node_id === '' || $group_id === '') wp_send_json_error(['msg' => 'Choose a device and destination group.']); $data = $this->bridge_json_response($this->bridge_post('/api/device/move', ['node_id' => $node_id, 'group_id' => $group_id])); if (empty($data['success'])) wp_send_json_error(['msg' => $data['error'] ?? 'Device move failed.', 'data' => $data]); $this->cache_clear(); wp_send_json_success($data); } public function ajax_uninstall_agent() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $node_id = $this->sanitize_mesh_id($_POST['node_id'] ?? ''); if ($node_id === '') wp_send_json_error(['msg' => 'Choose a device first.']); $data = $this->bridge_json_response($this->bridge_post('/api/device/uninstall', ['node_id' => $node_id], ['timeout' => 35])); if (empty($data['success'])) wp_send_json_error(['msg' => $data['error'] ?? 'Agent uninstall failed.', 'data' => $data]); $this->cache_clear(); wp_send_json_success($data); } public function ajax_maintenance_probe() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $data = $this->bridge_json_response($this->bridge_request('/api/meshcentral/maintenance', ['timeout' => 35])); if (empty($data['success'])) wp_send_json_error(['msg' => $data['error'] ?? 'MeshCentral maintenance probe failed.', 'data' => $data]); wp_send_json_success($data); } public function ajax_toggle_maintenance() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $enabled = !empty($_POST['enabled']) && $_POST['enabled'] !== '0' && $_POST['enabled'] !== 'false'; $data = $this->bridge_json_response($this->bridge_post('/api/maintenance/toggle', ['enabled' => $enabled], ['timeout' => 20])); if (empty($data['success'])) wp_send_json_error(['msg' => $data['error'] ?? 'Maintenance toggle failed.', 'data' => $data]); if (function_exists('gp_log')) { gp_log($enabled ? 'MeshPress bridge maintenance mode enabled.' : 'MeshPress bridge maintenance mode disabled.', 'GP-MESH', ['severity' => 'INFO']); } wp_send_json_success($data); } public function ajax_toggle_meshcentral_maintenance() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $enabled = !empty($_POST['enabled']) && $_POST['enabled'] !== '0' && $_POST['enabled'] !== 'false'; $restart = empty($_POST['no_restart']); $data = $this->bridge_json_response($this->bridge_post('/api/meshcentral/maintenance/toggle', [ 'enabled' => $enabled, 'restart' => $restart, ], ['timeout' => 40])); if (empty($data['success'])) wp_send_json_error(['msg' => $data['error'] ?? 'MeshCentral maintenance toggle failed.', 'data' => $data]); if (function_exists('gp_log')) { gp_log($enabled ? 'MeshCentral maintenance mode enabled from MeshPress.' : 'MeshCentral maintenance mode disabled from MeshPress.', 'GP-MESH', ['severity' => 'INFO']); } wp_send_json_success($data); } public function ajax_meshcentral_config_audit() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $data = $this->bridge_json_response($this->bridge_request('/api/meshcentral/config-audit', ['timeout' => 35])); if (empty($data['success'])) wp_send_json_error(['msg' => $data['error'] ?? 'MeshCentral config audit failed.', 'data' => $data]); wp_send_json_success($data); } public function ajax_meshcentral_backup() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $stop_service = !empty($_POST['stop_service']) && $_POST['stop_service'] !== '0' && $_POST['stop_service'] !== 'false'; $data = $this->bridge_json_response($this->bridge_post('/api/meshcentral/backup', ['stop_service' => $stop_service], ['timeout' => 90])); if (empty($data['success'])) wp_send_json_error(['msg' => $data['error'] ?? 'MeshCentral backup failed.', 'data' => $data]); wp_send_json_success($data); } public function ajax_meshcentral_update() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $data = $this->bridge_json_response($this->bridge_post('/api/meshcentral/update', [], ['timeout' => 20])); if (empty($data['success'])) wp_send_json_error(['msg' => $data['error'] ?? 'MeshCentral update failed.', 'data' => $data]); wp_send_json_success($data); } public function ajax_clear_cache() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $this->cache_clear(); $this->bridge_post('/api/cache/clear', []); if (function_exists('gp_log')) gp_log('MeshPress fleet cache cleared.', 'GP-MESH', ['severity' => 'INFO']); wp_send_json_success(['message' => 'MeshPress cache cleared.']); } public function ajax_push_now() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $data = $this->bridge_json_response($this->bridge_post('/api/push-now', [])); if (empty($data['success'])) wp_send_json_error(['msg' => $data['error'] ?? 'Bridge push failed.', 'data' => $data]); wp_send_json_success($data); } public function ajax_health_check() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $checks = []; $status = $this->bridge_json_response($this->bridge_request('/api/status', ['timeout' => 20])); $selftest = $this->bridge_json_response($this->bridge_request('/api/self-test', ['timeout' => 35])); $fleet = $this->bridge_json_response($this->bridge_request('/api/fleet?refresh=1', ['timeout' => 35])); $last_event = get_transient('gp_mesh_last_bridge_event'); $cached = get_transient('gp_mesh_fleet_cache'); $checks[] = [ 'key' => 'bridge', 'label' => 'WordPress to Bridge', 'ok' => !empty($status['success']), 'detail' => !empty($status['success']) ? 'Bridge reachable on ' . ($status['bind'] ?? '?') . ':' . ($status['port'] ?? '?') : ($status['error'] ?? 'Bridge unavailable.') ]; $checks[] = [ 'key' => 'meshctrl', 'label' => 'Bridge to MeshCentral', 'ok' => !empty($selftest['success']), 'detail' => !empty($selftest['success']) ? 'MeshCtrl, auth, groups, and devices passed self-test.' : ($selftest['error'] ?? 'Self-test failed.') ]; $checks[] = [ 'key' => 'public_url', 'label' => 'Public MeshCentral URL', 'ok' => !empty($status['public_mesh_url_configured']), 'detail' => $status['public_mesh_url'] ?? 'PUBLIC_MESH_URL is not configured.' ]; $checks[] = [ 'key' => 'fleet', 'label' => 'Fleet Freshness', 'ok' => !empty($fleet['success']), 'detail' => !empty($fleet['success']) ? count((array)($fleet['devices'] ?? [])). ' devices, ' . count((array)($fleet['groups'] ?? [])) . ' groups.' : ($fleet['error'] ?? 'Fleet refresh failed.') ]; $checks[] = [ 'key' => 'push', 'label' => 'Bridge Push', 'ok' => !empty($status['wp_push_configured']), 'detail' => !empty($status['wp_push_configured']) ? 'Push configured for ' . ($status['wp_domain'] ?? 'WordPress') : 'WP_DOMAIN/WP_API_KEY are not configured on the bridge.' ]; $checks[] = [ 'key' => 'omnilog', 'label' => 'OmniLog Events', 'ok' => is_array($last_event), 'detail' => is_array($last_event) ? 'Last event: ' . ($last_event['event'] ?? 'unknown') . ' at ' . ($last_event['received_at'] ?? 'unknown') : 'No bridge event has been received yet.' ]; if (!empty($fleet['success'])) { set_transient('gp_mesh_fleet_cache', [ 'groups' => (array)($fleet['groups'] ?? []), 'devices' => (array)($fleet['devices'] ?? []), 'synced_at' => current_time('mysql'), 'source' => 'health_check', 'bridge_version' => sanitize_text_field($fleet['bridge_version'] ?? '') ], DAY_IN_SECONDS); set_transient('gp_mesh_nodes_cache', (array)($fleet['devices'] ?? []), DAY_IN_SECONDS); } if (function_exists('gp_log')) gp_log('MeshPress connection health check completed.', 'GP-MESH', ['severity' => 'INFO']); wp_send_json_success(['checks' => $checks, 'status' => $status, 'selftest' => $selftest, 'fleet' => $fleet, 'cached' => $cached]); } public function ajax_wait_enrollment() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(['msg' => 'Not allowed.']); $group_id = $this->sanitize_mesh_id($_POST['group_id'] ?? ''); if ($group_id === '') wp_send_json_error(['msg' => 'Choose a group first.']); $data = $this->bridge_json_response($this->bridge_request('/api/fleet?refresh=1', ['timeout' => 35])); if (empty($data['success'])) wp_send_json_error(['msg' => $data['error'] ?? 'Fleet refresh failed.', 'data' => $data]); $devices = (array)($data['devices'] ?? []); $matches = []; $wanted_short = basename(str_replace('\\', '/', $group_id)); foreach ($devices as $device) { $device_group = (string)($device['groupId'] ?? $device['meshid'] ?? ''); $device_short = basename(str_replace('\\', '/', $device_group)); if ($device_group === $group_id || $device_short === $wanted_short) { $matches[] = $device; } } set_transient('gp_mesh_fleet_cache', [ 'groups' => (array)($data['groups'] ?? []), 'devices' => $devices, 'synced_at' => current_time('mysql'), 'source' => 'enrollment_wait', 'bridge_version' => sanitize_text_field($data['bridge_version'] ?? '') ], DAY_IN_SECONDS); set_transient('gp_mesh_nodes_cache', $devices, DAY_IN_SECONDS); if (function_exists('gp_log')) gp_log('MeshPress enrollment check completed.', 'GP-MESH', ['severity' => $matches ? 'SUCCESS' : 'INFO', 'matches' => count($matches)]); wp_send_json_success(['matches' => $matches, 'count' => count($matches), 'devices' => $devices, 'groups' => (array)($data['groups'] ?? [])]); } public function ajax_get_fleet() { check_ajax_referer('gp_mesh_nonce', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(); $host = rtrim((string)get_option('gp_mesh_host', ''), '/'); $key = (string)get_option('gp_mesh_key', ''); $mode = get_option('gp_mesh_mode', 'bridge'); $public = rtrim((string)get_option('gp_mesh_public_url', ''), '/'); if (empty($host) || empty($key)) { wp_send_json_success(['html' => '
Bridge URL or token missing. Go to Connection Setup.
']); } if ($mode === 'legacy') { $response = wp_remote_get($host . '/api/gracepress/fleet?key=' . urlencode($key), ['timeout' => 15]); } else { $response = $this->bridge_request('/api/fleet?refresh=1'); } if (is_wp_error($response)) { if (function_exists('gp_log')) gp_log("MeshPress Bridge unreachable: " . $response->get_error_message(), 'GP-MESH', ['severity' => 'ERROR']); $cached = get_transient('gp_mesh_fleet_cache'); if (is_array($cached) && !empty($cached['devices'])) { wp_send_json_success(['html' => '
Showing pushed fleet snapshot from ' . esc_html($cached['synced_at'] ?? 'cache') . '.
' . $this->render_fleet_table($cached['devices'], $cached['groups'] ?? [])]); } wp_send_json_success(['html' => '
Failed to connect to MeshPress Bridge. Check URL, token, and firewall/Tailscale reachability.
']); } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (!$data || empty($data['success'])) { if (function_exists('gp_log')) gp_log("MeshPress Bridge rejected request or returned invalid JSON.", 'GP-MESH', ['severity' => 'ERROR', 'response' => substr($body, 0, 500)]); wp_send_json_success(['html' => '
Bridge rejected the request: ' . esc_html($data['error'] ?? 'Invalid response') . '
']); } $nodes = $data['devices'] ?? $data['nodes'] ?? []; $meshes = $data['groups'] ?? $data['meshes'] ?? []; if (empty($nodes)) { wp_send_json_success(['html' => '
No devices found in MeshCentral database.
']); } set_transient('gp_mesh_fleet_cache', ['groups' => $meshes, 'devices' => $nodes, 'synced_at' => current_time('mysql')], DAY_IN_SECONDS); set_transient('gp_mesh_nodes_cache', $nodes, DAY_IN_SECONDS); if (function_exists('gp_log')) gp_log("MeshCentral Fleet Synced Successfully via MeshPress Bridge.", 'GP-MESH', ['severity' => 'SUCCESS', 'devices' => count($nodes), 'groups' => count($meshes)]); wp_send_json_success(['html' => $this->render_fleet_table($nodes, $meshes)]); } // ===================================================================== // 4. ADMIN DASHBOARD UI // ===================================================================== public function register_admin_page() { $slug = 'mesh-press'; $title = 'MeshPress Sync'; global $admin_page_hooks; if (isset($admin_page_hooks['service-desk'])) { add_submenu_page('service-desk', 'MeshPress', $title, 'manage_options', $slug, [$this, 'admin_ui']); } elseif (class_exists('GracePress_Central_Hub')) { add_submenu_page('gracepress-central', 'MeshPress', $title, 'manage_options', $slug, [$this, 'admin_ui']); } else { add_menu_page('MeshPress', $title, 'manage_options', $slug, [$this, 'admin_ui'], 'dashicons-admin-site-alt3', 6); } } public function admin_ui() { if (!current_user_can('manage_options')) return; $host = get_option('gp_mesh_host', ''); $key = get_option('gp_mesh_key', ''); $public_url = get_option('gp_mesh_public_url', 'https://mesh.christit.com'); $mode = get_option('gp_mesh_mode', 'bridge'); $link_mode = get_option('gp_mesh_link_mode', 'gotonode'); $advanced_mode = get_option('gp_mesh_advanced_mode', 'no') === 'yes'; $last_event = get_transient('gp_mesh_last_bridge_event'); $cached_fleet = get_transient('gp_mesh_fleet_cache'); $cached_groups = is_array($cached_fleet['groups'] ?? null) ? $cached_fleet['groups'] : []; $cached_devices = is_array($cached_fleet['devices'] ?? null) ? $cached_fleet['devices'] : []; ?>

GracePress MeshPress Sync

Enterprise RMM bridge. Communicates with the GracePress MeshPress Bridge for MeshCentral fleet syncing.

Fleet Overview & Actions
MeshCentral Ops
Troubleshooting
Connection Setup
Deploy Agents

Connection Health

Checks the bridge, MeshCentral, public session URL, fleet cache, push settings, and OmniLog event flow.

StatusNot checked
Last OmniLog Event
Raw health output
Run a health check to collect diagnostics.

Managed Devices

MeshCentral Runtime

Reads MeshCentral package/runtime details through the bridge. Bridge maintenance unlocks guarded bridge actions; MeshCentral maintenance writes settings.maintenanceMode into MeshCentral config and restarts the service.

No MeshCentral maintenance probe has been run yet.

MeshCentral Backups & Config Audit

Creates a full snapshot of MeshCentral data/config and, when MySQL or MariaDB is configured, includes a database dump. The audit output is redacted for copy/paste diagnostics.

Backup tools are intentionally guarded. Run status first to confirm bridge maintenance settings.

Agent Lifecycle

Manual uninstall diagnostics for a selected MeshCentral agent. Keep this as an operator-only test until we prove the exact MeshCtrl command on your MeshCentral version.

No lifecycle action has been run yet.

Bridge Runtime Status

Check the live bridge version, install path, updater availability, and trigger the catalog updater manually.

StatusNot checked

Bridge Probes

Run safe checks against the local bridge before wiring actions into Service Desk.

Run a probe to see raw bridge results. Bridge logs now use copy-friendly JSON lines prefixed with [meshpress-bridge]. Useful events: session_links, session_diagnostics, group_resolve, group_edit_attempt, group_edit_success, group_edit_failed.

Create MeshCentral Group

This requires MESHPRESS_ALLOW_COMMANDS=1 on the bridge.

Edit MeshCentral Group

Rename or update a group description, then clear/push fleet cache.

Agent Link Generator

Choose a MeshCentral device group, then copy a Linux install command or open a platform download. MeshPress fills the Mesh ID automatically; you should not have to paste it manually.

Device Link Tester

Generates Desktop, Terminal, and Files URLs in every supported MeshCentral direct-link pattern. Start with Terminal > primary, then try alternates if your MeshCentral build prefers another parameter order.

New Server Onboarding

Bridge URLPoint WordPress at the bridge running beside the new MeshCentral server.
Bridge TokenUse the token from /etc/gracepress/meshpress-bridge.env.
Public MeshCentral URLThis URL is used for Desktop, Terminal, Files, and agent installer links.
4
Bridge Self-TestAfter saving, run the health check to confirm MeshCtrl path, auth, groups, devices, push, and OmniLog.

Required Configuration

This plugin now talks to the catalog-managed MeshPress Bridge service. The bridge runs beside MeshCentral and uses MeshCtrl to read groups and devices.

1
Install the Bridge on the New MeshCentral Server
Use tools/meshpress-bridge/install.sh, then configure /etc/gracepress/meshpress-bridge.env.
2
Expose the Bridge Safely
Prefer localhost plus private proxy/VPN, or bind to a private address with strict firewall and bridge token controls.
3
Sync the Fleet
Save the bridge URL, token, and public MeshCentral URL, then run Sync API Data.

Deploy New Agents

Choose a MeshCentral group, generate platform links, then copy the Linux command to install the agent on Victor or another server.

1
Linux install flow
Generate links, copy linux_x64_install_command, paste it into Victor, and then sync the fleet.
Generate an installer, run it on the new machine, then check for the agent in the selected group.
checked)) return $t; $s = 'mesh-press/mesh-press.php'; if (!function_exists('get_plugin_data')) include_once(ABSPATH . 'wp-admin/includes/plugin.php'); $p = file_exists(WP_PLUGIN_DIR . '/' . $s) ? get_plugin_data(WP_PLUGIN_DIR . '/' . $s) : []; $r = wp_remote_get('https://christit.com/gracepress/api.php?endpoint=manifest&slug=mesh-press&nocache=' . time(), ['timeout' => 10]); if (!is_wp_error($r) && wp_remote_retrieve_response_code($r) == 200) { $d = json_decode(wp_remote_retrieve_body($r)); if ($d && version_compare(trim($p['Version'] ?? '0.0.0'), trim($d->version ?? '0.0.0'), '<')) { $x = new stdClass(); $x->slug = dirname($s); $x->plugin = $s; $x->new_version = $d->version; $x->package = $d->download_url; $x->url = 'https://christit.com/gracepress/'; $x->tested = '6.4'; $x->requires = '6.0'; $x->requires_php = '8.0'; $t->response[$s] = $x; } } return $t; } } new GP_Mesh_UE(); }