Public Access
b491241a58
Universal: Sync Feature Branch Versions / Sync feature branches with dev (push) Has been skipped
# Conflicts: # .mokogitea/CLAUDE.md # .mokogitea/ISSUE_TEMPLATE/config.yml # .mokogitea/ISSUE_TEMPLATE/documentation.md # .mokogitea/ISSUE_TEMPLATE/feature_request.md # .mokogitea/ISSUE_TEMPLATE/security.md # .mokogitea/branch-protection.yml # .mokogitea/bulk-repo-sync.yml # .mokogitea/pr-branch-check.yml # .mokogitea/renovate.yml # .mokogitea/sync-wikis.yml # .mokogitea/workflows/auto-bump.yml # .mokogitea/workflows/auto-release.yml # .mokogitea/workflows/ci-platform.yml # .mokogitea/workflows/cleanup.yml # .mokogitea/workflows/gitleaks.yml # .mokogitea/workflows/issue-branch.yml # .mokogitea/workflows/notify.yml # .mokogitea/workflows/pre-release.yml # .mokogitea/workflows/repo-health.yml # .mokogitea/workflows/security-audit.yml # .script-registry.json # CHANGELOG.md # PLUGIN_SCRIPTS.md # README.md # analysis/index.md # automation/bulk_joomla_template.php # automation/bulk_sync.php # automation/enrich_manifest_xml.php # automation/enrich_mokostandards_xml.php # automation/index.md # automation/migrate_to_gitea.php # automation/push_files.php # automation/push_manifest_xml.php # automation/push_mokostandards_xml.php # automation/repo_cleanup.php # bin/moko # cli/archive_repo.php # cli/audit_query.php # cli/badge_update.php # cli/branch_rename.php # cli/bulk_workflow_push.php # cli/bulk_workflow_trigger.php # cli/changelog_promote.php # cli/changelog_prune.php # cli/client_dashboard.php # cli/client_health_check.php # cli/client_inventory.php # cli/client_provision.php # cli/completion.php # cli/create_project.php # cli/create_repo.php # cli/deploy_joomla.php # cli/dev_branch_reset.php # cli/grafana_dashboard.php # cli/joomla_build.php # cli/joomla_compat_check.php # cli/joomla_metadata_validate.php # cli/joomla_release.php # cli/license_manage.php # cli/manifest_element.php # cli/manifest_licensing.php # cli/manifest_read.php # cli/package_build.php # cli/platform_detect.php # cli/release.php # cli/release_body_update.php # cli/release_cascade.php # cli/release_create.php # cli/release_manage.php # cli/release_mirror.php # cli/release_notes.php # cli/release_package.php # cli/release_promote.php # cli/release_publish.php # cli/release_validate.php # cli/release_verify.php # cli/scaffold_client.php # cli/sync_rulesets.php # cli/theme_lint.php # cli/updates_xml_build.php # cli/updates_xml_sync.php # cli/version_auto_bump.php # cli/version_bump.php # cli/version_bump_remote.php # cli/version_check.php # cli/version_read.php # cli/version_reset_dev.php # cli/version_set_platform.php # cli/wiki_sync.php # cli/workflow_sync.php # composer.json # deploy/backup-before-deploy.php # deploy/deploy-dolibarr.php # deploy/deploy-joomla.php # deploy/deploy-sftp.php # deploy/health-check.php # deploy/rollback-joomla.php # deploy/sync-joomla.php # fix/fix_line_endings.php # fix/fix_permissions.php # fix/fix_tabs.php # fix/fix_trailing_spaces.php # fix/index.md # index.md # lib/CliBase.php # lib/Common.php # lib/Enterprise/AbstractProjectPlugin.php # lib/Enterprise/ApiClient.php # lib/Enterprise/AuditLogger.php # lib/Enterprise/CheckpointManager.php # lib/Enterprise/CliFramework.php # lib/Enterprise/Config.php # lib/Enterprise/ConfigValidator.php # lib/Enterprise/EnterpriseReadinessValidator.php # lib/Enterprise/ErrorRecovery.php # lib/Enterprise/FileFixUtility.php # lib/Enterprise/GitHubAdapter.php # lib/Enterprise/GitPlatformAdapter.php # lib/Enterprise/InputValidator.php # lib/Enterprise/ManifestParser.php # lib/Enterprise/ManifestReader.php # lib/Enterprise/MetricsCollector.php # lib/Enterprise/MokoGiteaAdapter.php # lib/Enterprise/PackageBuilder.php # lib/Enterprise/PlatformAdapterFactory.php # lib/Enterprise/PluginFactory.php # lib/Enterprise/PluginRegistry.php # lib/Enterprise/Plugins/ApiPlugin.php # lib/Enterprise/Plugins/DocumentationPlugin.php # lib/Enterprise/Plugins/DolibarrPlugin.php # lib/Enterprise/Plugins/GenericPlugin.php # lib/Enterprise/Plugins/JoomlaPlugin.php # lib/Enterprise/Plugins/McpServerPlugin.php # lib/Enterprise/Plugins/MobilePlugin.php # lib/Enterprise/Plugins/NodeJsPlugin.php # lib/Enterprise/Plugins/PythonPlugin.php # lib/Enterprise/Plugins/TerraformPlugin.php # lib/Enterprise/Plugins/WordPressPlugin.php # lib/Enterprise/ProjectConfigValidator.php # lib/Enterprise/ProjectMetricsCollector.php # lib/Enterprise/ProjectPluginInterface.php # lib/Enterprise/ProjectTypeDetector.php # lib/Enterprise/RecoveryError.php # lib/Enterprise/RecoveryManager.php # lib/Enterprise/RepositoryHealthChecker.php # lib/Enterprise/RepositorySynchronizer.php # lib/Enterprise/RetryHelper.php # lib/Enterprise/SecurityValidator.php # lib/Enterprise/SourceResolver.php # lib/Enterprise/SynchronizationException.php # lib/Enterprise/TransactionManager.php # lib/Enterprise/UnifiedValidation.php # lib/index.md # lib/plugins/Joomla/UpdateXmlGenerator.php # maintenance/index.md # maintenance/pin_action_shas.php # maintenance/repo_inventory.php # maintenance/rotate_secrets.php # maintenance/setup_labels.php # maintenance/sync_dolibarr_readmes.php # maintenance/update_repo_inventory.php # maintenance/update_sha_hashes.php # maintenance/update_version_from_readme.php # mcp/config.example.json # mcp/package.json # mcp/src/config.ts # mcp/src/index.ts # mcp/src/runner.ts # mcp/src/types.ts # phpcs.xml # plugin_health_check.php # plugin_list.php # plugin_metrics.php # plugin_readiness.php # plugin_validate.php # release/generate_dolibarr_version_txt.php # release/generate_joomla_update_xml.php # src/functions.php # templates/configs/README.md # templates/configs/index.md # templates/configs/manifest.xml.template # templates/configs/manifest.yml.template # templates/configs/mokostandards.xml.template # templates/configs/mokostandards.yml.template # templates/configs/phpcs.xml # templates/docs/README.md # templates/docs/extra/README.md # templates/docs/extra/index.md # templates/docs/index.md # templates/docs/required/GOVERNANCE.md # templates/docs/required/README.md # templates/docs/required/index.md # templates/docs/required/template-CONTRIBUTING.md # templates/docs/required/template-README.md # templates/docs/required/template-SECURITY.md # templates/index.md # templates/licenses/README.md # templates/licenses/index.md # templates/makefiles/README.md # templates/mokogitea/CLAUDE.dolibarr.md.template # templates/mokogitea/CLAUDE.joomla.md.template # templates/mokogitea/CLAUDE.md.template # templates/mokogitea/ISSUE_TEMPLATE/config.yml # templates/mokogitea/ISSUE_TEMPLATE/documentation.md # templates/mokogitea/ISSUE_TEMPLATE/dolibarr_module_id_request.md # templates/mokogitea/ISSUE_TEMPLATE/feature_request.md # templates/mokogitea/ISSUE_TEMPLATE/security.md # templates/mokogitea/README.md # templates/mokogitea/copilot-instructions.dolibarr.md.template # templates/mokogitea/copilot-instructions.joomla.md.template # templates/mokogitea/copilot-instructions.md.template # templates/mokogitea/dependabot.yml.template # templates/mokogitea/override.tf.template # templates/required/README.md # templates/schemas/README.md # templates/schemas/manifest-schema.xsd # templates/schemas/moko-platform-schema.xsd # templates/schemas/mokostandards-schema.xsd # templates/schemas/schemas/README.md # templates/schemas/template-repository-structure.xml # templates/scripts/README.md # templates/scripts/common/CliBase.template.php # templates/scripts/fix/index.md # templates/scripts/index.md # templates/scripts/release/index.md # templates/scripts/release/package_dolibarr.php # templates/scripts/release/package_joomla.php # templates/scripts/sftp-config/README.md # templates/scripts/validate/dolibarr_module.php # templates/scripts/validate/index.md # templates/scripts/validate/validate_manifest.php # templates/scripts/validate/validate_structure.php # templates/security/README.md # templates/security/index.php # templates/stubs/dolibarr.php # templates/stubs/joomla.php # templates/web/index.php # tests/Enterprise/GitPlatformAdapterTest.php # tests/Unit/VersionBumpTest.php # tests/Unit/VersionReadTest.php # tests/index.md # tests/test_circuit_breaker_handling.php # tests/test_enterprise_libraries.php # validate/SECURITY_SCANNING.md # validate/auto_detect_platform.php # validate/check_changelog.php # validate/check_client_theme.php # validate/check_composer_deps.php # validate/check_dolibarr_module.php # validate/check_enterprise_readiness.php # validate/check_file_integrity.php # validate/check_joomla_manifest.php # validate/check_language_structure.php # validate/check_license_headers.php # validate/check_no_secrets.php # validate/check_paths.php # validate/check_php_syntax.php # validate/check_repo_health.php # validate/check_structure.php # validate/check_tabs.php # validate/check_version_consistency.php # validate/check_wiki_health.php # validate/check_xml_wellformed.php # validate/index.md # validate/scan_drift.php # wrappers/auto_detect_platform.php # wrappers/bulk_sync.php # wrappers/check_changelog.php # wrappers/check_dolibarr_module.php # wrappers/check_enterprise_readiness.php # wrappers/check_joomla_manifest.php # wrappers/check_language_structure.php # wrappers/check_license_headers.php # wrappers/check_no_secrets.php # wrappers/check_paths.php # wrappers/check_php_syntax.php # wrappers/check_repo_health.php # wrappers/check_structure.php # wrappers/check_tabs.php # wrappers/check_version_consistency.php # wrappers/check_xml_wellformed.php # wrappers/deploy_sftp.php # wrappers/fix_line_endings.php # wrappers/fix_permissions.php # wrappers/fix_tabs.php # wrappers/fix_trailing_spaces.php # wrappers/gen_wrappers.php # wrappers/index.md # wrappers/pin_action_shas.php # wrappers/plugin_health_check.php # wrappers/plugin_list.php # wrappers/plugin_metrics.php # wrappers/plugin_readiness.php # wrappers/plugin_validate.php # wrappers/scan_drift.php # wrappers/setup_labels.php # wrappers/sync_dolibarr_readmes.php # wrappers/update_sha_hashes.php # wrappers/update_version_from_readme.php
495 lines
16 KiB
PHP
495 lines
16 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
|
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* This file is part of a Moko Consulting project.
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* FILE INFORMATION
|
|
<<<<<<< HEAD
|
|
* DEFGROUP: MokoCLI.CLI
|
|
* INGROUP: MokoCLI
|
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
|
* PATH: /cli/client_dashboard.php
|
|
* VERSION: 09.25.05
|
|
=======
|
|
* DEFGROUP: mokoplatform.CLI
|
|
* INGROUP: mokoplatform
|
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
|
* PATH: /cli/client_dashboard.php
|
|
* VERSION: 09.32.00
|
|
>>>>>>> main
|
|
* BRIEF: Generate unified client dashboard HTML
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
|
|
|
use MokoCli\CliFramework;
|
|
|
|
class ClientDashboardCli extends CliFramework
|
|
{
|
|
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;
|
|
|
|
protected function configure(): void
|
|
{
|
|
$this->setDescription('Generate unified client dashboard HTML');
|
|
$this->addArgument('--token', 'Gitea token (or MOKOGITEA_TOKEN)', '');
|
|
$this->addArgument('--gitea-url', 'Gitea URL', 'https://git.mokoconsulting.tech');
|
|
$this->addArgument('--org', 'Primary org (default: MokoConsulting)', 'MokoConsulting');
|
|
$this->addArgument('--output', 'Output HTML file (default: stdout)', '');
|
|
$this->addArgument('-o', 'Output HTML file (alias)', '');
|
|
$this->addArgument('--no-ssl', 'Skip SSL checks', false);
|
|
$this->addArgument('--no-uptime', 'Skip HTTP uptime checks', false);
|
|
$this->addArgument('--ssl-warn-days', 'SSL warning days (default: 30)', '30');
|
|
}
|
|
|
|
protected function run(): int
|
|
{
|
|
$this->giteaUrl = rtrim($this->getArgument('--gitea-url'), '/');
|
|
$this->token = $this->getArgument('--token');
|
|
$this->org = $this->getArgument('--org');
|
|
$this->outputFile = $this->getArgument('--output') ?: $this->getArgument('-o');
|
|
$this->checkSsl = !$this->getArgument('--no-ssl');
|
|
$this->checkUptime = !$this->getArgument('--no-uptime');
|
|
$this->sslWarnDays = (int) $this->getArgument('--ssl-warn-days');
|
|
|
|
if ($this->token === '') {
|
|
$this->token = getenv('MOKOGITEA_TOKEN') ?: '';
|
|
}
|
|
|
|
if ($this->token === '') {
|
|
$this->log('ERROR', '--token or MOKOGITEA_TOKEN required.');
|
|
return 1;
|
|
}
|
|
|
|
$this->log('INFO', 'Gathering client data...');
|
|
$clients = $this->discoverClients();
|
|
|
|
if ($clients === null) {
|
|
$this->log('ERROR', 'Could not fetch client repos.');
|
|
return 1;
|
|
}
|
|
|
|
$this->log('INFO', '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('INFO', "Dashboard: {$this->outputFile}");
|
|
} else {
|
|
fwrite(STDOUT, $html);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/** @return array<int, array<string, mixed>>|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<string, mixed> $client */
|
|
private function enrichClient(array &$client): void
|
|
{
|
|
$repo = $client['repo'];
|
|
$this->log('INFO', " Checking {$client['name']}...");
|
|
|
|
$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']);
|
|
}
|
|
}
|
|
|
|
$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';
|
|
}
|
|
}
|
|
|
|
$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<int, array<string, mixed>> $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 <<<HTML
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Moko Client Dashboard</title>
|
|
<style>
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0f172a;color:#e2e8f0;padding:24px}
|
|
h1{font-size:1.5rem;font-weight:600;margin-bottom:4px}
|
|
.sub{color:#94a3b8;font-size:.875rem;margin-bottom:24px}
|
|
.stats{display:flex;gap:16px;margin-bottom:24px;flex-wrap:wrap}
|
|
.st{background:#1e293b;border-radius:8px;padding:16px 20px;min-width:140px}
|
|
.sv{font-size:1.5rem;font-weight:700}
|
|
.sl{color:#94a3b8;font-size:.75rem;text-transform:uppercase;letter-spacing:.05em}
|
|
.stat-ok .sv{color:#4ade80}
|
|
.stat-warn .sv{color:#fbbf24}
|
|
.g{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:16px}
|
|
.c{background:#1e293b;border-radius:8px;padding:20px;border:1px solid #334155;transition:border-color .2s}
|
|
.c:hover{border-color:#475569}
|
|
.ch{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
|
|
.cn{font-size:1.1rem;font-weight:600;text-transform:capitalize}
|
|
.cn a{color:#e2e8f0;text-decoration:none}
|
|
.cn a:hover{color:#60a5fa}
|
|
.b{font-size:.7rem;padding:2px 8px;border-radius:999px;font-weight:600;text-transform:uppercase}
|
|
.b-up{background:#064e3b;color:#4ade80}
|
|
.b-dn{background:#7f1d1d;color:#fca5a5}
|
|
.b-un{background:#374151;color:#9ca3af}
|
|
.rs{display:flex;flex-direction:column;gap:8px}
|
|
.r{display:flex;justify-content:space-between;font-size:.85rem}
|
|
.rl{color:#94a3b8}
|
|
.rv{color:#e2e8f0;text-align:right;max-width:60%}
|
|
.rv a{color:#60a5fa;text-decoration:none}
|
|
.rv a:hover{text-decoration:underline}
|
|
.ok{color:#4ade80}.wn{color:#fbbf24}.er{color:#f87171}
|
|
.st2{font-size:.7rem;text-transform:uppercase;letter-spacing:.08em;color:#64748b;
|
|
margin-top:8px;margin-bottom:4px;padding-top:8px;border-top:1px solid #334155}
|
|
footer{margin-top:32px;text-align:center;color:#64748b;font-size:.75rem}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Moko Client Dashboard</h1>
|
|
<p class="sub">Generated {$generated}</p>
|
|
<div class="stats">
|
|
<div class="st"><div class="sv">{$total}</div><div class="sl">Clients</div></div>
|
|
<div class="st stat-ok"><div class="sv">{$up}</div><div class="sl">Sites Up</div></div>
|
|
<div class="st {$warnCls}"><div class="sv">{$sslWarn}</div><div class="sl">SSL Warnings</div></div>
|
|
</div>
|
|
<div class="g">{$cards}</div>
|
|
<footer>Moko Consulting — client_dashboard.php</footer>
|
|
</body>
|
|
</html>
|
|
HTML;
|
|
}
|
|
|
|
/** @param array<string, mixed> $c */
|
|
private function renderCard(array $c): string
|
|
{
|
|
$name = htmlspecialchars($c['name']);
|
|
$repoUrl = htmlspecialchars($c['url']);
|
|
|
|
$ls = $c['live_status'];
|
|
|
|
if ($ls === 'up') {
|
|
$badge = '<span class="b b-up">UP</span>';
|
|
} elseif ($ls === 'down') {
|
|
$badge = '<span class="b b-dn">DOWN</span>';
|
|
} else {
|
|
$badge = '<span class="b b-un">' . htmlspecialchars($ls) . '</span>';
|
|
}
|
|
|
|
$rows = '';
|
|
|
|
if ($c['live_url'] !== '') {
|
|
$u = htmlspecialchars($c['live_url']);
|
|
$rows .= "<div class=\"r\"><span class=\"rl\">Live</span>"
|
|
. "<span class=\"rv\"><a href=\"{$u}\" target=\"_blank\">{$u}</a></span></div>";
|
|
}
|
|
|
|
if ($c['dev_url'] !== '') {
|
|
$u = htmlspecialchars($c['dev_url']);
|
|
$ds = $c['dev_status'] === 'up' ? ' (up)' : '';
|
|
$rows .= "<div class=\"r\"><span class=\"rl\">Dev</span>"
|
|
. "<span class=\"rv\"><a href=\"{$u}\" target=\"_blank\">{$u}</a>{$ds}</span></div>";
|
|
}
|
|
|
|
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 .= "<div class=\"r\"><span class=\"rl\">SSL</span>"
|
|
. "<span class=\"rv {$cls}\">{$stxt}</span></div>";
|
|
}
|
|
|
|
if ($c['last_release'] !== '') {
|
|
$rel = htmlspecialchars($c['last_release']);
|
|
$rd = htmlspecialchars($c['last_release_date']);
|
|
$rows .= "<div class=\"r\"><span class=\"rl\">Release</span>"
|
|
. "<span class=\"rv\">{$rel} ({$rd})</span></div>";
|
|
}
|
|
|
|
$dc = $c['has_dev'] ? '<span class="ok">configured</span>' : '<span class="er">missing</span>';
|
|
$lc = $c['has_live'] ? '<span class="ok">configured</span>' : '<span class="er">missing</span>';
|
|
$upd = substr($c['updated'], 0, 10);
|
|
|
|
return <<<CARD
|
|
<div class="c">
|
|
<div class="ch"><span class="cn"><a href="{$repoUrl}" target="_blank">{$name}</a></span>{$badge}</div>
|
|
<div class="rs">{$rows}
|
|
<div class="st2">Infrastructure</div>
|
|
<div class="r"><span class="rl">Dev Server</span><span class="rv">{$dc}</span></div>
|
|
<div class="r"><span class="rl">Live Server</span><span class="rv">{$lc}</span></div>
|
|
<div class="r"><span class="rl">Last Push</span><span class="rv">{$upd}</span></div>
|
|
</div></div>
|
|
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];
|
|
}
|
|
}
|
|
|
|
$app = new ClientDashboardCli();
|
|
exit($app->execute());
|