#!/usr/bin/env php * * This file is part of a Moko Consulting project. * * SPDX-License-Identifier: GPL-3.0-or-later * * FILE INFORMATION * DEFGROUP: moko-platform.CLI * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/client_dashboard.php * VERSION: 01.00.00 * BRIEF: Generate unified client dashboard HTML */ declare(strict_types=1); final class ClientDashboard { private string $giteaUrl = 'https://git.mokoconsulting.tech'; private string $token = ''; private string $org = 'MokoConsulting'; private string $outputFile = ''; private bool $checkSsl = true; private bool $checkUptime = true; private int $sslWarnDays = 30; private int $httpTimeout = 10; public function run(): int { $this->parseArgs(); if ($this->token === '') { $this->token = getenv('GA_TOKEN') ?: ''; } if ($this->token === '') { $this->log('ERROR: --token or GA_TOKEN required.'); $this->printUsage(); return 1; } $this->log('Gathering client data...'); $clients = $this->discoverClients(); if ($clients === null) { $this->log('ERROR: Could not fetch client repos.'); return 1; } $this->log('Found ' . count($clients) . ' client(s).'); foreach ($clients as &$client) { $this->enrichClient($client); } unset($client); $html = $this->renderDashboard($clients); if ($this->outputFile !== '') { file_put_contents($this->outputFile, $html); $this->log("Dashboard: {$this->outputFile}"); } else { fwrite(STDOUT, $html); } return 0; } /** @return array>|null */ private function discoverClients(): ?array { $clients = []; $orgs = $this->fetchAllOrgs(); if (!in_array($this->org, $orgs, true)) { array_unshift($orgs, $this->org); } foreach ($orgs as $orgName) { $page = 1; while (true) { $resp = $this->api( 'GET', "/api/v1/orgs/{$orgName}/repos" . "?limit=50&page={$page}" ); if ($resp['code'] !== 200) { break; } $repos = json_decode($resp['body'], true); if (!is_array($repos) || empty($repos)) { break; } foreach ($repos as $repo) { $name = $repo['name'] ?? ''; if ( !str_starts_with($name, 'client-waas-') || !empty($repo['archived']) ) { continue; } $clients[] = [ 'repo' => $repo['full_name'] ?? '', 'name' => str_replace('client-waas-', '', $name), 'description' => $repo['description'] ?? '', 'updated' => $repo['updated_at'] ?? '', 'url' => $repo['html_url'] ?? '', ]; } $page++; } } usort($clients, fn($a, $b) => strcasecmp($a['name'], $b['name'])); return $clients; } /** @return string[] */ private function fetchAllOrgs(): array { $resp = $this->api('GET', '/api/v1/user/orgs?limit=50'); if ($resp['code'] !== 200) { return [$this->org]; } $orgs = json_decode($resp['body'], true); if (!is_array($orgs)) { return [$this->org]; } return array_map(fn($o) => $o['username'] ?? '', $orgs); } /** @param array $client */ private function enrichClient(array &$client): void { $repo = $client['repo']; $this->log(" Checking {$client['name']}..."); // Fetch variables $resp = $this->api('GET', "/api/v1/repos/{$repo}/actions/variables"); $vars = []; if ($resp['code'] === 200) { $varList = json_decode($resp['body'], true); if (is_array($varList)) { foreach ($varList as $v) { $vars[$v['name'] ?? ''] = $v['data'] ?? ''; } } } $client['vars'] = $vars; $client['dev_url'] = $vars['DEV_SITE_URL'] ?? ''; $client['live_url'] = $vars['LIVE_SITE_URL'] ?? ''; $client['has_dev'] = isset($vars['DEV_SYNC_HOST']); $client['has_live'] = isset($vars['LIVE_SSH_HOST']); $client['dev_status'] = 'unknown'; $client['live_status'] = 'unknown'; if ($this->checkUptime) { if ($client['dev_url'] !== '') { $client['dev_status'] = $this->checkHttp($client['dev_url']); } if ($client['live_url'] !== '') { $client['live_status'] = $this->checkHttp($client['live_url']); } } // SSL $client['ssl_expiry'] = null; $client['ssl_days'] = null; $client['ssl_status'] = 'unknown'; $domain = $vars['MONITORED_DOMAINS'] ?? ''; if ($domain === '' && $client['live_url'] !== '') { $parsed = parse_url($client['live_url']); $domain = $parsed['host'] ?? ''; } if ($this->checkSsl && $domain !== '') { $domain = trim(explode("\n", $domain)[0]); $ssl = $this->checkSslCert($domain); $client['ssl_domain'] = $domain; $client['ssl_expiry'] = $ssl['expiry']; $client['ssl_days'] = $ssl['days']; if ($ssl['days'] === null) { $client['ssl_status'] = 'error'; } elseif ($ssl['days'] < $this->sslWarnDays) { $client['ssl_status'] = 'warning'; } else { $client['ssl_status'] = 'ok'; } } // Last release $client['last_release'] = ''; $client['last_release_date'] = ''; $relResp = $this->api('GET', "/api/v1/repos/{$repo}/releases?limit=1"); if ($relResp['code'] === 200) { $rels = json_decode($relResp['body'], true); if (is_array($rels) && !empty($rels)) { $client['last_release'] = $rels[0]['name'] ?? ''; $client['last_release_date'] = substr($rels[0]['created_at'] ?? '', 0, 10); } } } private function checkHttp(string $url): string { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_NOBODY, true); curl_setopt($ch, CURLOPT_TIMEOUT, $this->httpTimeout); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($code === 0) { return 'down'; } return ($code >= 200 && $code < 400) ? 'up' : "http-{$code}"; } /** @return array{expiry: ?string, days: ?int} */ private function checkSslCert(string $domain): array { $ctx = stream_context_create([ 'ssl' => [ 'capture_peer_cert' => true, 'verify_peer' => false, 'verify_peer_name' => false, ], ]); $client = @stream_socket_client( "ssl://{$domain}:443", $errno, $errstr, $this->httpTimeout, STREAM_CLIENT_CONNECT, $ctx ); if (!$client) { return ['expiry' => null, 'days' => null]; } $params = stream_context_get_params($client); fclose($client); $cert = $params['options']['ssl']['peer_certificate'] ?? null; if ($cert === null) { return ['expiry' => null, 'days' => null]; } $info = openssl_x509_parse($cert); $validTo = $info['validTo_time_t'] ?? 0; if ($validTo === 0) { return ['expiry' => null, 'days' => null]; } $expiry = date('Y-m-d', $validTo); $days = (int) round(($validTo - time()) / 86400); return ['expiry' => $expiry, 'days' => $days]; } /** @param array> $clients */ private function renderDashboard(array $clients): string { $generated = date('Y-m-d H:i:s T'); $total = count($clients); $up = 0; $sslWarn = 0; foreach ($clients as $c) { if ($c['live_status'] === 'up' || $c['dev_status'] === 'up') { $up++; } if ($c['ssl_status'] === 'warning') { $sslWarn++; } } $cards = ''; foreach ($clients as $c) { $cards .= $this->renderCard($c); } $warnCls = $sslWarn > 0 ? 'stat-warn' : 'stat-ok'; return << Moko Client Dashboard

Moko Client Dashboard

Generated {$generated}

{$total}
Clients
{$up}
Sites Up
{$sslWarn}
SSL Warnings
{$cards}
Moko Consulting — client_dashboard.php
HTML; } /** @param array $c */ private function renderCard(array $c): string { $name = htmlspecialchars($c['name']); $repoUrl = htmlspecialchars($c['url']); $ls = $c['live_status']; if ($ls === 'up') { $badge = 'UP'; } elseif ($ls === 'down') { $badge = 'DOWN'; } else { $badge = '' . htmlspecialchars($ls) . ''; } $rows = ''; if ($c['live_url'] !== '') { $u = htmlspecialchars($c['live_url']); $rows .= "
Live" . "{$u}
"; } if ($c['dev_url'] !== '') { $u = htmlspecialchars($c['dev_url']); $ds = $c['dev_status'] === 'up' ? ' (up)' : ''; $rows .= "
Dev" . "{$u}{$ds}
"; } if ($c['ssl_days'] !== null) { $cls = match ($c['ssl_status']) { 'ok' => 'ok', 'warning' => 'wn', default => 'er' }; $stxt = htmlspecialchars("{$c['ssl_expiry']} ({$c['ssl_days']}d)"); $rows .= "
SSL" . "{$stxt}
"; } if ($c['last_release'] !== '') { $rel = htmlspecialchars($c['last_release']); $rd = htmlspecialchars($c['last_release_date']); $rows .= "
Release" . "{$rel} ({$rd})
"; } $dc = $c['has_dev'] ? 'configured' : 'missing'; $lc = $c['has_live'] ? 'configured' : 'missing'; $upd = substr($c['updated'], 0, 10); return <<
{$name}{$badge}
{$rows}
Infrastructure
Dev Server{$dc}
Live Server{$lc}
Last Push{$upd}
CARD; } /** @return array{code: int, body: string} */ private function api(string $method, string $endpoint): array { $url = $this->giteaUrl . $endpoint; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Accept: application/json', "Authorization: token {$this->token}", ]); $body = curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { curl_close($ch); return ['code' => 0, 'body' => '']; } curl_close($ch); return ['code' => $code, 'body' => $body]; } private function parseArgs(): void { $args = $_SERVER['argv'] ?? []; $count = count($args); for ($i = 1; $i < $count; $i++) { switch ($args[$i]) { case '--token': $this->token = $args[++$i] ?? ''; break; case '--gitea-url': $this->giteaUrl = rtrim($args[++$i] ?? '', '/'); break; case '--org': $this->org = $args[++$i] ?? ''; break; case '--output': case '-o': $this->outputFile = $args[++$i] ?? ''; break; case '--no-ssl': $this->checkSsl = false; break; case '--no-uptime': $this->checkUptime = false; break; case '--ssl-warn-days': $this->sslWarnDays = (int) ($args[++$i] ?? 30); break; case '--help': case '-h': $this->printUsage(); exit(0); default: $this->log("WARNING: Unknown arg: {$args[$i]}"); break; } } } private function printUsage(): void { $this->log('Usage: client_dashboard.php --token TOKEN [options]'); $this->log(''); $this->log('Generate unified client status dashboard (HTML).'); $this->log(''); $this->log('Options:'); $this->log(' --token Gitea token (or GA_TOKEN)'); $this->log(' --gitea-url Gitea URL'); $this->log(' --org Primary org (default: MokoConsulting)'); $this->log(' -o, --output Output HTML file (default: stdout)'); $this->log(' --no-ssl Skip SSL checks'); $this->log(' --no-uptime Skip HTTP uptime checks'); $this->log(' --ssl-warn-days SSL warning days (default: 30)'); $this->log(' --help, -h Show this help'); } private function log(string $message): void { fwrite(STDERR, $message . PHP_EOL); } } $app = new ClientDashboard(); exit($app->run());