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 = '
';
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
Toggle ';
$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 .= 'Desktop ';
$html .= 'Terminal ';
$html .= 'Files ';
$html .= 'Diagnose ';
}
$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 = 'Status Device Name Group IP Address Actions ';
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 .= ' '.($isOnline ? 'Online' : 'Offline').' ';
$html .= '' . esc_html($n['name']) . ' ';
$html .= '' . esc_html($groupName) . ' ';
$html .= '' . esc_html($ip) . ' ';
$html .= '';
if($node_id && $public) {
$safe_node = esc_attr($node_id);
$html .= 'Desktop ';
$html .= 'Terminal ';
$html .= 'Files ';
$html .= 'Diagnose ';
}
$html .= ' ';
}
return $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'] : [];
?>
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.
Run Health Check
Status Not checked
Last OmniLog Event
Raw health output
Run a health check to collect diagnostics.
Managed Devices
Sync API Data
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.
Read MeshCentral Status
Enable Bridge Maintenance
Disable Bridge Maintenance
Enable MeshCentral Maintenance
Disable MeshCentral Maintenance
Trigger MeshCentral Update
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.
Create Live Full Backup
Create Offline Full Backup
Run Config Audit
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.
Device
Choose a cached MeshCentral device
Request Agent Uninstall
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.
Check Bridge Version
Update Bridge Now
Bridge Probes
Run safe checks against the local bridge before wiring actions into Service Desk.
Health
Version
Status
Self-Test
Routes
Groups
Devices
Users
Events
Server Info
Push Fleet
Clear Cache
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.
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 Group
Choose a cached MeshCentral group
Generate Agent Links
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.
Device
Choose a cached MeshCentral device
Generate Session Links
Open Terminal
Device Info
Diagnose Session
Uninstall Agent
Rename
Move to group...
Move
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.
Target Device Group
Choose a cached MeshCentral group
1
Linux install flow Generate links, copy linux_x64_install_command, paste it into Victor, and then sync the fleet.
Generate Windows / Mac / Linux Links
Check For New Agent
Refresh Fleet After Install
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();
}