'HexaHost GameCloud', 'description' => 'Global GameCloud integration — dashboard, reconciliation, and event polling.', 'version' => HEXAGAMECLOUD_ADDON_VERSION, 'author' => 'HexaHost', 'language' => 'english', 'fields' => [ 'api_url' => [ 'FriendlyName' => 'GameCloud API URL', 'Type' => 'text', 'Size' => '128', 'Default' => 'https://api.example.net', 'Description' => 'Base URL of the GameCloud API (no trailing slash)', ], 'integration_id' => [ 'FriendlyName' => 'Integration ID', 'Type' => 'text', 'Size' => '64', 'Description' => 'Matches WHMCS_INTEGRATION_ID on GameCloud', ], 'api_secret' => [ 'FriendlyName' => 'API Secret', 'Type' => 'password', 'Size' => '128', 'Description' => 'HMAC signing secret — stored encrypted by WHMCS', ], 'sso_origin' => [ 'FriendlyName' => 'SSO Panel Origin', 'Type' => 'text', 'Size' => '128', 'Default' => 'https://panel.example.net', 'Description' => 'Customer panel URL for SSO redirects', ], 'event_poll_limit' => [ 'FriendlyName' => 'Event poll batch size', 'Type' => 'text', 'Size' => '4', 'Default' => '50', ], 'auto_reconcile' => [ 'FriendlyName' => 'Auto-reconcile on daily cron', 'Type' => 'yesno', 'Description' => 'Run dry reconciliation during daily event poll', ], ], ]; } function hexagamecloud_activate(): array { try { $sql = file_get_contents(__DIR__ . '/sql/install.sql'); if ($sql === false) { throw new RuntimeException('Unable to read install.sql'); } foreach (array_filter(array_map('trim', explode(';', $sql))) as $statement) { if ($statement !== '') { Capsule::connection()->statement($statement); } } return [ 'status' => 'success', 'description' => 'HexaHost GameCloud addon activated. Copy hooks/hexagamecloud.php to includes/hooks/ for cron polling.', ]; } catch (Throwable $exception) { return [ 'status' => 'error', 'description' => 'Activation failed: ' . $exception->getMessage(), ]; } } function hexagamecloud_deactivate(): array { return [ 'status' => 'success', 'description' => 'Addon deactivated. Module tables were kept for audit history.', ]; } function hexagamecloud_output(array $vars): void { $action = $_GET['action'] ?? 'dashboard'; $modulelink = $vars['modulelink']; $storage = new HexaGameCloudModuleStorage(); try { $client = hexagamecloud_create_client($vars); } catch (Throwable $exception) { hexagamecloud_render_error($vars, $exception->getMessage()); return; } switch ($action) { case 'events': hexagamecloud_page_events($vars, $client, $storage, $modulelink); break; case 'reconciliation': hexagamecloud_page_reconciliation($vars, $client, $storage, $modulelink); break; case 'poll': hexagamecloud_action_poll($vars, $client, $storage, $modulelink); break; case 'reconcile': hexagamecloud_action_reconcile($vars, $client, $storage, $modulelink); break; case 'ack': hexagamecloud_action_ack($vars, $client, $storage, $modulelink); break; default: hexagamecloud_page_dashboard($vars, $client, $storage, $modulelink); break; } } function hexagamecloud_create_client(array $vars): HexaGameCloudAddonApiClient { $apiUrl = trim((string) ($vars['api_url'] ?? '')); $integrationId = trim((string) ($vars['integration_id'] ?? '')); $apiSecret = trim((string) ($vars['api_secret'] ?? '')); if ($apiUrl === '' || $integrationId === '' || $apiSecret === '') { throw new RuntimeException('Configure API URL, Integration ID and API Secret in addon settings.'); } return new HexaGameCloudAddonApiClient($apiUrl, $integrationId, $apiSecret); } function hexagamecloud_page_dashboard( array $vars, HexaGameCloudAddonApiClient $client, HexaGameCloudModuleStorage $storage, string $modulelink ): void { $health = $client->health(); $stats = $client->dashboardStats(); $accounts = $client->listAccounts(); $unlinked = hexagamecloud_count_unlinked_whmcs_services($accounts['accounts'] ?? []); $orphans = 0; foreach ($accounts['accounts'] ?? [] as $account) { if (($account['linkStatus'] ?? '') === 'TERMINATED' && ($account['gameCloudStatus'] ?? '') !== 'DELETED') { $orphans++; } } $storage->recordOperation('dashboard:view', 'success', [ 'integrationId' => $stats['integrationId'] ?? null, ]); echo hexagamecloud_render_template('dashboard', [ 'modulelink' => $modulelink, 'version' => HEXAGAMECLOUD_ADDON_VERSION, 'health' => $health, 'stats' => $stats, 'linkedServices' => count($accounts['accounts'] ?? []), 'unlinkedWhmcsServices' => $unlinked, 'driftCount' => $orphans, 'lastError' => $storage->getLastError(), ]); } function hexagamecloud_page_events( array $vars, HexaGameCloudAddonApiClient $client, HexaGameCloudModuleStorage $storage, string $modulelink ): void { $localEvents = $storage->listLocalEvents(100); $pendingCount = $storage->countLocalEventsByStatus('pending'); echo hexagamecloud_render_template('events', [ 'modulelink' => $modulelink, 'localEvents' => $localEvents, 'pendingCount' => $pendingCount, 'deadLetterCount' => $storage->countLocalEventsByStatus('dead_letter'), ]); } function hexagamecloud_page_reconciliation( array $vars, HexaGameCloudAddonApiClient $client, HexaGameCloudModuleStorage $storage, string $modulelink ): void { $runs = $storage->listReconciliationRuns(20); $dryRun = isset($_GET['dry']) ? true : false; $liveResult = null; if (isset($_GET['run'])) { try { $liveResult = $client->reconcileAll([ 'dryRun' => $dryRun, 'autoRepair' => !$dryRun && !empty($_GET['repair']), ]); $storage->saveReconciliationRun($liveResult, $dryRun ? 'dry_run' : 'live'); } catch (Throwable $exception) { $storage->setLastError($exception->getMessage()); $liveResult = ['error' => $exception->getMessage()]; } } echo hexagamecloud_render_template('reconciliation', [ 'modulelink' => $modulelink, 'runs' => $runs, 'liveResult' => $liveResult, 'dryRun' => $dryRun, ]); } function hexagamecloud_action_poll( array $vars, HexaGameCloudAddonApiClient $client, HexaGameCloudModuleStorage $storage, string $modulelink ): void { $limit = (int) ($vars['event_poll_limit'] ?? 50); $poller = new HexaGameCloudEventPoller($client, $storage, max(1, min($limit, 100))); try { $result = $poller->pollAndAcknowledge(); $storage->recordOperation('events:poll', 'success', $result); } catch (Throwable $exception) { $storage->setLastError($exception->getMessage()); $storage->recordOperation('events:poll', 'error', ['message' => $exception->getMessage()]); } header('Location: ' . $modulelink . '&action=events'); exit; } function hexagamecloud_action_reconcile( array $vars, HexaGameCloudAddonApiClient $client, HexaGameCloudModuleStorage $storage, string $modulelink ): void { header('Location: ' . $modulelink . '&action=reconciliation&run=1&dry=1'); exit; } function hexagamecloud_action_ack( array $vars, HexaGameCloudAddonApiClient $client, HexaGameCloudModuleStorage $storage, string $modulelink ): void { $eventId = (int) ($_POST['local_event_id'] ?? 0); $deadLetter = !empty($_POST['dead_letter']); $local = $storage->getLocalEvent($eventId); if ($local && !empty($local['remote_event_id'])) { try { $client->acknowledgeEvents([$local['remote_event_id']], $deadLetter, 'Manual ack from WHMCS addon'); $storage->updateLocalEventStatus($eventId, $deadLetter ? 'dead_letter' : 'acknowledged'); } catch (Throwable $exception) { $storage->setLastError($exception->getMessage()); } } header('Location: ' . $modulelink . '&action=events'); exit; } function hexagamecloud_count_unlinked_whmcs_services(array $linkedAccounts): int { $linkedIds = []; foreach ($linkedAccounts as $account) { if (!empty($account['externalServiceId'])) { $linkedIds[(string) $account['externalServiceId']] = true; } } $hosting = Capsule::table('tblhosting') ->join('tblproducts', 'tblproducts.id', '=', 'tblhosting.packageid') ->where('tblproducts.servertype', 'hexagamecloud') ->whereIn('tblhosting.domainstatus', ['Active', 'Suspended', 'Pending']) ->select('tblhosting.id') ->get(); $unlinked = 0; foreach ($hosting as $row) { if (!isset($linkedIds[(string) $row->id])) { $unlinked++; } } return $unlinked; } function hexagamecloud_render_template(string $name, array $data): string { $path = __DIR__ . '/templates/' . $name . '.tpl'; if (!is_readable($path)) { return '
' . htmlspecialchars((string) $event['payload'], ENT_QUOTES, 'UTF-8') . '' . htmlspecialchars(json_encode($data['liveResult'], JSON_PRETTY_PRINT), ENT_QUOTES, 'UTF-8') . '', $html ); } else { $html = str_replace('{{reconciliation.result}}', '', $html); } return $html; } function hexagamecloud_render_error(array $vars, string $message): void { echo '
Configure the addon under Setup → Addon Modules → HexaHost GameCloud.
'; }