<?php
###############################################################################
# diagnostics.php
#
# Watch My Domains Server Edition - Diagnostic Tool
#
# Copyright (c) Softnik Technologies
# https://softnik.com
#
# Developed by akumar@codepunch.com
#
# Acknowledgements:
#   Assistance from nikita@softnik.com
#   Assistance from AI tools
#   Supervision from Dax 🐈 - sits on keyboard, denies everything
#
# ---------------------------------------------------------------------------
# ARCHITECTURE NOTES (read before reviewing or modifying)
# ---------------------------------------------------------------------------
#
# 1. DEPLOYMENT LOCATION
#    This file is always deployed by the installer into a fixed, known
#    location within the Watch My Domains SED installation tree.
#    It is never placed at an arbitrary path by the user.
#    Concerns about unauthenticated access via partial mode do not apply —
#    partial mode (app root not found) cannot occur in normal deployment.
#
# 2. DOCTRINE DBAL
#    DBAL is bundled with the application at a fixed, tested version.
#    It is NOT managed by Composer and is NOT updated independently.
#    Concerns about DBAL 3/4 API compatibility (getSchemaManager,
#    getName(), etc.) do not apply — the bundled version is known
#    and the calls used here are valid for it.
#
# 3. DATABASE DRIVERS
#    Driver extension checks are intentional and exhaustive for the
#    set of databases supported by this product.
#
# ---------------------------------------------------------------------------
# USAGE INSTRUCTIONS
# ---------------------------------------------------------------------------
#
# 1. This file is included with the Watch My Domains SED installation.
#    It is located in the tools/ folder of the installation tree.
#
# 2. Run diagnostics.php using one of the following methods:
#
#    CLI:
#       php diagnostics.php
#
#    Web:
#       https://your-server/tools/diagnostics.php
#       (Modern UI is used automatically in web mode)
#
# 3. Web access is restricted:
#       - Administrator login required
#       - Session must be valid
#       - Access allowed only within 5 minutes of login
#         (timeout is checked at the start of each run)
#
# 4. This file is protected by administrator authentication and a
#    5-minute session timeout. It is safe to leave in place, but
#    should only be used when actively diagnosing an issue.
#
###############################################################################

declare(strict_types=1);

final class DiagnosticSkipException extends \Exception {}

$IS_CLI = (php_sapi_name() === 'cli');

// UI mode is always active in web context
$UI_MODE = !$IS_CLI;

/*
|--------------------------------------------------------------------------
| Auto-detect application root
|--------------------------------------------------------------------------
| Walk up directories until we find lib/php/loader.php
|--------------------------------------------------------------------------
*/

$searchPath = __DIR__;
$BASE_PATH  = null;

while ($searchPath !== dirname($searchPath)) {
    if (file_exists($searchPath . '/lib/php/loader.php')) {
        $BASE_PATH = $searchPath;
        break;
    }
    $searchPath = dirname($searchPath);
}

if ($BASE_PATH === null) {
    $APP_LOADED = false;
} else {
    require_once $BASE_PATH . '/lib/php/loader.php';
    $APP_LOADED = true;
}

/*
|--------------------------------------------------------------------------
| UI Mode — collection structures
|
| All data is gathered into $UI_SECTIONS during the normal run, then
| rendered as a full HTML page at the very end.
|
| Section shape:
|   [ 'title' => string, 'level' => int, 'entries' => [ entry, ... ] ]
|
| Entry shapes:
|   Plain line  : [ 'kind' => 'line', 'msg' => string, 'type' => 'info'|'ok'|'fail' ]
|   Test result : [ 'kind' => 'test', 'name' => string, 'status' => 'ok'|'skip'|'fail',
|                   'time' => float,  'errmsg' => string, 'sub' => [ line, ... ] ]
|--------------------------------------------------------------------------
*/

$UI_SECTIONS    = [];
$UI_CUR_TITLE   = '';
$UI_CUR_LEVEL   = 2;
$UI_CUR_ENTRIES = [];

// Non-null while runTest() is active; out() routes here instead of $UI_CUR_ENTRIES
$UI_CAPTURE = null;

function _ui_commit_section(): void
{
    global $UI_SECTIONS, $UI_CUR_TITLE, $UI_CUR_LEVEL, $UI_CUR_ENTRIES;
    if ($UI_CUR_TITLE !== '' || !empty($UI_CUR_ENTRIES)) {
        $UI_SECTIONS[] = [
            'title'   => $UI_CUR_TITLE,
            'level'   => $UI_CUR_LEVEL,
            'entries' => $UI_CUR_ENTRIES,
        ];
    }
    $UI_CUR_TITLE   = '';
    $UI_CUR_LEVEL   = 2;
    $UI_CUR_ENTRIES = [];
}

/*
|--------------------------------------------------------------------------
| Output helpers
|--------------------------------------------------------------------------
*/

function out(string $msg, string $type = 'info'): void
{
    global $IS_CLI, $UI_MODE, $UI_CUR_ENTRIES, $UI_CAPTURE;

    if ($IS_CLI) {
        echo $msg . PHP_EOL;
        return;
    }

    if ($UI_MODE) {
        $entry = ['kind' => 'line', 'msg' => $msg, 'type' => $type];
        if ($UI_CAPTURE !== null) {
            $UI_CAPTURE[] = $entry;   // captured inside a runTest()
        } else {
            $UI_CUR_ENTRIES[] = $entry;
        }
        return;
    }
}

function heading(string $text, int $level = 2): void
{
    global $IS_CLI, $UI_MODE, $UI_CUR_LEVEL;

    if ($IS_CLI) {
        echo PHP_EOL . "=== {$text} ===" . PHP_EOL . PHP_EOL;
        return;
    }

    if ($UI_MODE) {
        _ui_commit_section();
        global $UI_CUR_TITLE;
        $UI_CUR_TITLE = $text;
        $UI_CUR_LEVEL = $level;
        return;
    }
}

function runTest(string $name, callable $fn): void
{
    global $IS_CLI, $UI_MODE, $UI_CUR_ENTRIES, $UI_CAPTURE;

    if ($IS_CLI) {
        echo "Running: {$name} ..." . PHP_EOL;
    }

    $start = microtime(true);

    if ($UI_MODE) {
        $UI_CAPTURE = [];   // open capture buffer

        try {
            $fn();
            $elapsed = round(microtime(true) - $start, 4);
            $status  = 'ok';
            $errmsg  = '';
        } catch (DiagnosticSkipException $e) {
            $elapsed = round(microtime(true) - $start, 4);
            $status  = 'skip';
            $errmsg  = $e->getMessage();
        } catch (\Throwable $e) {
            $elapsed = round(microtime(true) - $start, 4);
            $status  = 'fail';
            $errmsg  = $e->getMessage();
        }

        $sub        = $UI_CAPTURE;
        $UI_CAPTURE = null;   // close capture buffer

        $UI_CUR_ENTRIES[] = [
            'kind'   => 'test',
            'name'   => $name,
            'status' => $status,
            'time'   => $elapsed,
            'errmsg' => $errmsg,
            'sub'    => $sub,
        ];

        return;
    }

    // CLI / plain web
    try {
        $fn();
        $elapsed = round(microtime(true) - $start, 4);
        out("[OK]   {$name} ({$elapsed}s)", 'ok');
    } catch (DiagnosticSkipException $e) {
        $elapsed = round(microtime(true) - $start, 4);
        out("[SKIP] {$name} ({$elapsed}s): " . $e->getMessage(), 'info');
    } catch (\Throwable $e) {
        $elapsed = round(microtime(true) - $start, 4);
        out("[FAIL] {$name} ({$elapsed}s): " . $e->getMessage(), 'fail');
    }
}

/*
|--------------------------------------------------------------------------
| Web Access Protection
|--------------------------------------------------------------------------
| When the app loader is found, full admin auth is enforced.
| When it is not found (partial mode), the tool runs without auth —
| only generic PHP/system info is shown, nothing app-specific.
|--------------------------------------------------------------------------
*/

if (!$IS_CLI) {

    if ($APP_LOADED) {

        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }

        $timedout             = false;
        $auth_timeout_minutes = 5;

        if (!\CodePunch\Config\Auth::isAdmin()) {
            $timedout = true;
        }

        if (!$timedout && defined('\CodePunch\Config\Auth::TIMEOUT')) {
            if (isset($_SESSION[\CodePunch\Config\Auth::TIMEOUT])) {
                $tseconds = $auth_timeout_minutes * 60;
                if (($_SESSION[\CodePunch\Config\Auth::TIMEOUT] + $tseconds) < time()) {
                    $timedout = true;
                }
            } else {
                $timedout = true;
            }
        }

        if ($timedout) {
            $reason     = \CodePunch\Config\Auth::isAdmin() ? 'Diagnostic+access+timed+out' : 'Session+expired';
            $returnPath = (basename(__DIR__) === 'tools') ? 'tools/diagnostics' : 'diagnostics';
            header("Location: /?logout&return={$returnPath}&r={$reason}");
            exit;
        }

        if (defined('\CodePunch\Config\Auth::TIMEOUT')) {
            $_SESSION[\CodePunch\Config\Auth::TIMEOUT] = time();
        }

    }
    // else: partial mode — no auth available, access is open
}

/*
|--------------------------------------------------------------------------
| Load Config (app only)
|--------------------------------------------------------------------------
*/

$params = null;

if ($APP_LOADED) {

    $configFile = $BASE_PATH . '/config.php';

    if (!file_exists($configFile)) {
        out("config.php not found", 'fail');
        exit;
    }

    require $configFile;

/*
|--------------------------------------------------------------------------
| Resolve DB Config (supports both formats)
|--------------------------------------------------------------------------
*/

function resolveDbConfig(): array
{
    global $db_config_database,
           $db_config_username,
           $db_config_password,
           $db_config_host,
           $db_config_driver,
           $db_extended_config_name,
           $db_extended_config;

    if (isset($db_extended_config_name) && isset($db_extended_config[$db_extended_config_name])) {
        $c = $db_extended_config[$db_extended_config_name];
        return [
            'dbname'   => $c['dbname']   ?? '',
            'user'     => $c['dbuser']   ?? '',
            'password' => $c['dbpass']   ?? '',
            'host'     => $c['dbhost']   ?? '',
            'driver'   => $c['dbdriver'] ?? '',
            'port'     => $c['port']     ?? null,
        ];
    }

    return [
        'dbname'   => $db_config_database ?? '',
        'user'     => $db_config_username ?? '',
        'password' => $db_config_password ?? '',
        'host'     => $db_config_host     ?? '',
        'driver'   => $db_config_driver   ?? '',
        'port'     => null,
    ];
}

$params = resolveDbConfig();

} // end if ($APP_LOADED) — config loading

/*
|=============================================================================
| DIAGNOSTICS BEGIN
|=============================================================================
*/

heading("Diagnostic Tool", 1);

if (!$APP_LOADED) {
    out("Application root (lib/php/loader.php) not found — running in partial mode.", 'fail');
    out("Only PHP and system information is available.", 'info');
}

heading("Application Version", 2);

if ($APP_LOADED) {

runTest("Defaults Version Info", function () {

    $version    = \CodePunch\Config\Defaults::version();
    $buildIndex = \CodePunch\Config\Defaults::BUILD_INDEX;
    $buildDate  = \CodePunch\Config\Defaults::builddate();
    $full       = \CodePunch\Config\Defaults::version_and_build();

    out("Product        : " . \CodePunch\Config\Defaults::product());
    out("Company        : " . \CodePunch\Config\Defaults::companyname());
    out("Version        : " . $version);
    out("Build Index    : " . $buildIndex);
    out("Build Date     : " . $buildDate);
    out("Version+Build  : " . $full);
});

} else {
    out("Not available — application not loaded.", 'info');
}

/*
|--------------------------------------------------------------------------
| SYSTEM INFORMATION
|--------------------------------------------------------------------------
*/

heading("SYSTEM INFORMATION", 2);

out("PHP Version: " . PHP_VERSION);
out("PHP SAPI: " . php_sapi_name());
out("Operating System: " . PHP_OS);

out("Loaded Extensions:");
$extensions = get_loaded_extensions();
sort($extensions);
out(" - " . implode(", ", $extensions));

if (extension_loaded('pdo')) {
    out("PDO Drivers: " . implode(", ", PDO::getAvailableDrivers()));
}

heading("PHP Time Settings", 3);

runTest("PHP Timezone", function () {
    $tz = date_default_timezone_get();
    out("PHP Timezone     : " . $tz);
    out("PHP Current Time : " . date("Y-m-d H:i:s"));
});

heading("Execution Environment", 3);

runTest("Runtime Context", function () {

    out("Loaded php.ini    : " . php_ini_loaded_file());
    out("Current Directory : " . getcwd());
    out("Script File       : " . __FILE__);

    if (function_exists('posix_getpwuid')) {
        $user = posix_getpwuid(posix_geteuid());
        out("OS User           : " . $user['name']);
    } else {
        out("OS User           : (posix extension not available)");
    }

    out("UID / GID         : " . getmyuid() . " / " . getmygid());
});

heading("PHP Configuration Snapshot", 3);

runTest("Important PHP ini Values", function () {

    $keys = [
        'memory_limit',
        'max_execution_time',
        'default_socket_timeout',
        'openssl.cafile',
        'openssl.capath',
        'curl.cainfo',
        'allow_url_fopen',
        'display_errors',
        'error_reporting',
    ];

    foreach ($keys as $k) {
        out(str_pad($k, 25) . " : " . ini_get($k));
    }
});

heading("php.ini Location", 3);

runTest("php.ini Path Check", function () {

    out("Loaded php.ini : " . php_ini_loaded_file());

    $scanned = php_ini_scanned_files();
    if ($scanned) {
        out("Scanned INIs  : " . $scanned);
    }
});

heading("Web Server Info", 3);

runTest("Web Server Detection", function () {

    if (isset($_SERVER['SERVER_SOFTWARE'])) {
        out("Server Software : " . $_SERVER['SERVER_SOFTWARE']);
    } else {
        out("Server Software : Not in web mode");
    }

    if (isset($_SERVER['SERVER_NAME']))   out("Server Name     : " . $_SERVER['SERVER_NAME']);
    if (isset($_SERVER['DOCUMENT_ROOT'])) out("Document Root   : " . $_SERVER['DOCUMENT_ROOT']);
});

heading("Installation Paths", 3);

if ($APP_LOADED) {
runTest("Install Folder Detection", function () {

    $installPath = \CodePunch\Base\Util::get_install_folder_path();
    $logPath     = \CodePunch\Base\Util::get_log_folder_path();
    $rootUrl     = \CodePunch\Base\Util::get_root_url();

    out("Install Folder : " . $installPath);
    out("Log Folder     : " . $logPath);
    out("Root URL       : " . $rootUrl);
});
} else {
    out("Not available — application not loaded.", 'info');
}

/*
|--------------------------------------------------------------------------
| Database Configuration Summary
|--------------------------------------------------------------------------
*/

if ($APP_LOADED) {

heading("DATABASE CONFIGURATION", 2);
out("Driver: "   . ($params['driver'] ?? ''));
out("Host: "     . ($params['host']   ?? ''));
out("Database: " . ($params['dbname'] ?? ''));
out("Port: "     . ($params['port']   ?? 'default'));

} // end APP_LOADED — DB config summary

/*
|--------------------------------------------------------------------------
| Environment Tests
|--------------------------------------------------------------------------
*/

heading("ENVIRONMENT", 2);

runTest("PHP Version >= 8.1", function () {
    if (version_compare(PHP_VERSION, '8.1.0', '<')) {
        throw new Exception("PHP 8.1+ required");
    }
});

runTest("PDO extension loaded", function () {
    if (!extension_loaded('pdo')) {
        throw new Exception("PDO not loaded");
    }
});

if ($APP_LOADED) {

runTest("Driver extension loaded", function () use ($params) {

    $driver = $params['driver'];

    if ($driver === 'pdo_sqlsrv' && !extension_loaded('pdo_sqlsrv')) throw new Exception("pdo_sqlsrv not loaded");
    if ($driver === 'pdo_mysql'  && !extension_loaded('pdo_mysql'))  throw new Exception("pdo_mysql not loaded");
    if ($driver === 'pdo_pgsql'  && !extension_loaded('pdo_pgsql'))  throw new Exception("pdo_pgsql not loaded");
    if ($driver === 'pdo_sqlite' && !extension_loaded('pdo_sqlite')) throw new Exception("pdo_sqlite not loaded");
    if ($driver === 'oci8'       && !extension_loaded('oci8'))       throw new Exception("oci8 not loaded");
});

/*
|--------------------------------------------------------------------------
| Direct PDO Test
|--------------------------------------------------------------------------
*/

heading("DIRECT PDO TEST", 2);

runTest("Direct PDO Connect", function () use ($params) {

    switch ($params['driver']) {

        case 'sqlsrv':
        case 'pdo_sqlsrv':
            $server = $params['host'];
            if (!empty($params['port'])) $server .= "," . $params['port'];
            $dsn = "sqlsrv:Server={$server};Database={$params['dbname']};Encrypt=1;TrustServerCertificate=1";
            break;

        case 'pdo_mysql':
            $dsn = "mysql:host={$params['host']};dbname={$params['dbname']}";
            if (!empty($params['port'])) $dsn .= ";port={$params['port']}";
            break;

        case 'pdo_pgsql':
            $dsn = "pgsql:host={$params['host']};dbname={$params['dbname']}";
            if (!empty($params['port'])) $dsn .= ";port={$params['port']}";
            break;

        case 'pdo_sqlite':
            $dsn = "sqlite:{$params['dbname']}";
            break;

        case 'oci8':
            $port = !empty($params['port']) ? ':' . $params['port'] : '';
            $dsn  = "oci:dbname=//{$params['host']}{$port}/{$params['dbname']}";
            break;

        default:
            throw new DiagnosticSkipException(
                "Direct PDO test not applicable for configured driver: "
                . ($params['driver'] ?? '(unknown)')
                . ". This diagnostic currently implements this check only for PDO-based drivers."
            );
    }

    $pdo       = new PDO($dsn, $params['user'], $params['password']);
    $pdoDriver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);

    if ($pdoDriver === 'oci') {
        $pdo->query("SELECT 1 FROM dual");
    } else {
        $pdo->query("SELECT 1");
    }
});

/*
|--------------------------------------------------------------------------
| Doctrine Direct Test
|--------------------------------------------------------------------------
*/

heading("DOCTRINE TEST", 2);

runTest("Doctrine DBAL Generation", function () {

    if (method_exists(\Doctrine\DBAL\Connection::class, 'createSchemaManager')) {
        out("DBAL 3+ detected (createSchemaManager exists)");
    }

    if (class_exists('\Doctrine\DBAL\Version')) {
        out("DBAL 2 or 3 (Version class exists)");
    }

    if (!class_exists('\Doctrine\DBAL\Version')) {
        out("Likely DBAL 4+ (Version class removed)");
    }
});

runTest("Doctrine Connect", function () use ($params) {

    $connParams = [
        'dbname'   => $params['dbname'],
        'user'     => $params['user'],
        'password' => $params['password'],
        'host'     => $params['host'],
        'driver'   => $params['driver'],
    ];

    if (!empty($params['port'])) $connParams['port'] = $params['port'];

    if ($connParams['driver'] === 'pdo_sqlsrv' || $connParams['driver'] === 'sqlsrv') {
        $connParams['driverOptions'] = ['TrustServerCertificate' => true];
    }

    $conn = \Doctrine\DBAL\DriverManager::getConnection($connParams);
    $conn->connect();

    if (!$conn->isConnected()) throw new Exception("Doctrine not connected");

    if ($connParams['driver'] === 'oci8') {
        $conn->executeQuery("SELECT 1 FROM dual");
    } else {
        $conn->executeQuery("SELECT 1");
    }
});

/*
|--------------------------------------------------------------------------
| Application Database Wrapper Test
|--------------------------------------------------------------------------
*/

heading("APPLICATION DB CLASS TEST", 2);

runTest("Database Wrapper Connect", function () use ($params) {

    $db = new \CodePunch\DB\Database();

    $connParams = [
        'dbname'   => $params['dbname'],
        'user'     => $params['user'],
        'password' => $params['password'],
        'host'     => $params['host'],
        'driver'   => $params['driver'],
    ];

    if (!empty($params['port'])) $connParams['port'] = $params['port'];

    if ($connParams['driver'] === 'pdo_sqlsrv' || $connParams['driver'] === 'sqlsrv') {
        $connParams['driverOptions'] = ['TrustServerCertificate' => true];
    }

    $db->setConnectionParams($connParams);
    $conn = $db->getConnection();

    if (!$conn || !$conn->isConnected()) throw new Exception("Wrapper not connected");

    if ($connParams['driver'] === 'oci8') {
        $conn->executeQuery("SELECT 1 FROM dual");
    } else {
        $conn->executeQuery("SELECT 1");
    }
});

runTest("Fetch Database Server Version", function () use ($params) {

    $connParams = [
        'dbname'   => $params['dbname'],
        'user'     => $params['user'],
        'password' => $params['password'],
        'host'     => $params['host'],
        'driver'   => $params['driver'],
    ];

    if (!empty($params['port'])) $connParams['port'] = $params['port'];

    if ($connParams['driver'] === 'pdo_sqlsrv' || $connParams['driver'] === 'sqlsrv') {
        $connParams['driverOptions'] = ['TrustServerCertificate' => true];
    }

    $conn     = \Doctrine\DBAL\DriverManager::getConnection($connParams);
    $conn->connect();
    $platform = strtolower($conn->getDatabasePlatform()->getName());

    out("Doctrine Platform: " . $platform);

    switch ($platform) {
        case 'mssql':
            out("SQL Server Version: " . $conn->executeQuery("SELECT @@VERSION")->fetchOne());
            out("Current Database: "   . $conn->executeQuery("SELECT DB_NAME()")->fetchOne());
            break;
        case 'mysql':
            out("MySQL Version: "    . $conn->executeQuery("SELECT VERSION()")->fetchOne());
            out("Current Database: " . $conn->executeQuery("SELECT DATABASE()")->fetchOne());
            break;
        case 'postgresql':
            out("PostgreSQL Version: " . $conn->executeQuery("SELECT version()")->fetchOne());
            out("Current Database: "   . $conn->executeQuery("SELECT current_database()")->fetchOne());
            break;
        case 'sqlite':
            out("SQLite Version: " . $conn->executeQuery("SELECT sqlite_version()")->fetchOne());
            out("SQLite File: "    . $params['dbname']);
            break;
        case 'oracle':
        case 'oci8':
            out("Oracle Version: " . $conn->executeQuery("SELECT version FROM product_component_version WHERE rownum = 1")->fetchOne());
            break;
        default:
            out("Platform detected but not specifically handled: " . $platform);
    }
});

runTest("Fetch Database Current Time", function () use ($params) {

    $connParams = [
        'dbname'   => $params['dbname'],
        'user'     => $params['user'],
        'password' => $params['password'],
        'host'     => $params['host'],
        'driver'   => $params['driver'],
    ];

    if (!empty($params['port'])) $connParams['port'] = $params['port'];

    if ($connParams['driver'] === 'pdo_sqlsrv' || $connParams['driver'] === 'sqlsrv') {
        $connParams['driverOptions'] = ['TrustServerCertificate' => true];
    }

    $conn     = \Doctrine\DBAL\DriverManager::getConnection($connParams);
    $conn->connect();
    $platform = strtolower($conn->getDatabasePlatform()->getName());

    if ($platform === 'oracle') {
        $time = $conn->executeQuery("SELECT CURRENT_TIMESTAMP FROM dual")->fetchOne();
    } else {
        $time = $conn->executeQuery("SELECT CURRENT_TIMESTAMP")->fetchOne();
    }

    out("Database Current Time: " . $time);
});

heading("Database Time Settings", 2);

runTest("Database Timezone Check", function () use ($params) {

    switch ($params['driver']) {
        case 'sqlsrv':
        case 'pdo_sqlsrv':
            $server = $params['host'];
            if (!empty($params['port'])) $server .= "," . $params['port'];
            $dsn = "sqlsrv:Server={$server};Database={$params['dbname']};Encrypt=1;TrustServerCertificate=1;LoginTimeout=5";
            break;
        case 'pdo_mysql':
            $dsn = "mysql:host={$params['host']};dbname={$params['dbname']}";
            if (!empty($params['port'])) $dsn .= ";port={$params['port']}";
            break;
        case 'pdo_pgsql':
            $dsn = "pgsql:host={$params['host']};dbname={$params['dbname']}";
            if (!empty($params['port'])) $dsn .= ";port={$params['port']}";
            break;
        case 'pdo_sqlite':
            $dsn = "sqlite:{$params['dbname']}";
            break;
        case 'oci8':
            $port = !empty($params['port']) ? ':' . $params['port'] : '';
            $dsn  = "oci:dbname=//{$params['host']}{$port}/{$params['dbname']}";
            break;
        default:
            throw new DiagnosticSkipException(
                "Database timezone check not implemented for configured driver: "
                . ($params['driver'] ?? '(unknown)')
                . ". The database connection may still be working normally."
            );
    }

    $pdo    = new PDO($dsn, $params['user'], $params['password']);
    $driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);

    if ($driver === 'mysql') {

        $tz = $pdo->query("
            SELECT @@global.time_zone AS global_tz, @@session.time_zone AS session_tz, NOW() AS now_time
        ")->fetch(PDO::FETCH_ASSOC);

        out("MySQL Global TZ  : " . $tz['global_tz']);
        out("MySQL Session TZ : " . $tz['session_tz']);
        out("MySQL Time       : " . $tz['now_time']);

    } elseif ($driver === 'sqlsrv') {

        $time = $pdo->query("SELECT SYSDATETIMEOFFSET() AS now")->fetch(PDO::FETCH_ASSOC);
        out("SQL Server Time        : " . $time['now']);

        $offset = $pdo->query("SELECT DATENAME(TZOFFSET, SYSDATETIMEOFFSET()) AS offset_val")->fetch(PDO::FETCH_ASSOC);
        if (!empty($offset['offset_val'])) out("SQL Server UTC Offset : " . $offset['offset_val']);

    } elseif ($driver === 'pgsql') {

        $row = $pdo->query("
            SELECT current_setting('TimeZone') AS tz, NOW() AS current_time
        ")->fetch(PDO::FETCH_ASSOC);

        out("PostgreSQL Timezone : " . $row['tz']);
        out("PostgreSQL Time     : " . $row['current_time']);

    } elseif ($driver === 'sqlite') {

        $row = $pdo->query("
            SELECT datetime('now') AS utc_time, datetime('now','localtime') AS local_time
        ")->fetch(PDO::FETCH_ASSOC);

        out("SQLite UTC Time    : " . $row['utc_time']);
        out("SQLite Local Time  : " . $row['local_time']);
        out("Note: SQLite has no server timezone setting.");

    } elseif ($driver === 'oci') {

        $row = $pdo->query("SELECT DBTIMEZONE AS db_tz, SESSIONTIMEZONE AS session_tz FROM dual")->fetch(PDO::FETCH_ASSOC);
        $row = array_change_key_case($row, CASE_UPPER);
        out("Oracle DB Timezone      : " . $row['DB_TZ']);
        out("Oracle Session Timezone : " . $row['SESSION_TZ']);

        $time = $pdo->query("SELECT CURRENT_TIMESTAMP FROM dual")->fetch(PDO::FETCH_NUM);
        out("Oracle Current Time     : " . $time[0]);

    } else {
        out("Timezone check not implemented for driver: {$driver}");
    }
});

/*
|--------------------------------------------------------------------------
| Schema Metadata Test
|--------------------------------------------------------------------------
*/

heading("SCHEMA TEST", 2);

runTest("List Tables", function () use ($params) {

    $connParams = [
        'dbname'   => $params['dbname'],
        'user'     => $params['user'],
        'password' => $params['password'],
        'host'     => $params['host'],
        'driver'   => $params['driver'],
    ];

    if (!empty($params['port'])) $connParams['port'] = $params['port'];

    if ($connParams['driver'] === 'pdo_sqlsrv' || $connParams['driver'] === 'sqlsrv') {
        $connParams['driverOptions'] = ['TrustServerCertificate' => true];
    }

    $conn = \Doctrine\DBAL\DriverManager::getConnection($connParams);
    $conn->connect();

    $sm     = $conn->getSchemaManager();
    $tables = $sm->listTableNames();

    if (!$tables) throw new Exception("No tables found.");

    sort($tables);
    out("Tables:");
    out(implode(", ", $tables));
    out("Total tables: " . count($tables));
});

heading("DNS TESTS", 2);

runTest("Detect DNS Library", function () {

    $hasNamespaced = class_exists('\NetDNS2\Resolver', false);
    $hasPear       = class_exists('Net_DNS2_Resolver', false);

    if (!$hasPear) {
        $ipath = stream_resolve_include_path('Net/DNS2.php');
        if ($ipath !== false && is_file($ipath)) {
            require_once 'Net/DNS2.php';
            $hasPear = class_exists('Net_DNS2_Resolver', false);
            if ($hasPear) out("PEAR Net_DNS2 loaded from: " . $ipath);
        }
    }

    if ($hasNamespaced) out("Namespaced NetDNS2 detected (bundled).");
    if ($hasPear)       out("PEAR Net_DNS2 detected (system).");

    if (!$hasNamespaced && !$hasPear) {
        throw new Exception(
            "No supported NetDNS library detected. "
            . "Bundled NetDNS2 was not found, and PEAR Net_DNS2 is not installed or not available in include_path."
        );
    }
});

runTest("NetDNS2 Query Test", function () {

    if (!class_exists('\NetDNS2\Resolver')) throw new Exception("Namespaced NetDNS2 not active.");

    $resolver = new \NetDNS2\Resolver(['nameservers' => ['8.8.8.8'], 'timeout' => 3]);
    $response = $resolver->query('google.com', 'A');

    if (!$response || empty($response->answer)) throw new Exception("No response returned.");

    $ips = [];
    foreach ($response->answer as $rr) {
        if (isset($rr->address)) $ips[] = $rr->address;
    }

    out("Resolved: " . implode(", ", $ips));
});

runTest("PEAR Net_DNS2 Query Test", function () {

    if (!class_exists('Net_DNS2_Resolver')) {
        throw new DiagnosticSkipException(
            "PEAR Net_DNS2 not active. This is an optional legacy/system PEAR check. "
            . "If bundled NetDNS2 is working, this message can usually be ignored."
        );
    }

    $resolver = new \Net_DNS2_Resolver(['nameservers' => ['8.8.8.8'], 'timeout' => 3]);
    $response = $resolver->query('google.com', 'A');

    if (!$response || empty($response->answer)) throw new Exception("No response returned.");

    $ips = [];
    foreach ($response->answer as $rr) {
        if (isset($rr->address)) $ips[] = $rr->address;
    }

    out("Resolved: " . implode(", ", $ips));
});

heading("WHOIS / RDAP TESTS", 2);

runTest("WHOIS Lookup (port 43)", function () {

    $domain      = "example.com";
    $whoisServer = "whois.verisign-grs.com";

    $fp = @fsockopen($whoisServer, 43, $errno, $errstr, 5);
    if (!$fp) throw new Exception("Connection failed: {$errstr} ({$errno})");

    fwrite($fp, $domain . "\r\n");
    stream_set_timeout($fp, 5);
    $response = fread($fp, 2048);
    fclose($fp);

    if (!$response) throw new Exception("No WHOIS response received.");

    out("WHOIS response received (" . strlen($response) . " bytes)");
});

runTest("RDAP Lookup (HTTPS)", function () {

    $url     = "https://rdap.verisign.com/com/v1/domain/example.com";
    $context = stream_context_create(['http' => ['timeout' => 5]]);

    $response = @file_get_contents($url, false, $context);
    if ($response === false) throw new Exception("RDAP HTTPS request failed.");

    $json = json_decode($response, true);
    if (!isset($json['ldhName'])) throw new Exception("Invalid RDAP JSON response.");

    out("RDAP domain: " . $json['ldhName']);
});

runTest("IANA RDAP Bootstrap List (dns.json)", function () {

    $url     = "https://data.iana.org/rdap/dns.json";
    $context = stream_context_create(['http' => ['timeout' => 5]]);

    $response = @file_get_contents($url, false, $context);
    if ($response === false) throw new Exception("Unable to fetch IANA RDAP bootstrap list.");

    $json = json_decode($response, true);
    if (!isset($json['services']) || !is_array($json['services'])) {
        throw new Exception("Invalid RDAP bootstrap JSON.");
    }

    $services = $json['services'];
    out("Bootstrap services count: " . count($services));
    if (!empty($services[0][1][0])) out("Example RDAP server: " . $services[0][1][0]);
});

heading("Network Resolution", 2);

runTest("DNS Resolution", function () {

    $records = dns_get_record("data.iana.org", DNS_A + DNS_AAAA);

    foreach ($records as $r) {
        if (isset($r['ip']))   out("IPv4: " . $r['ip']);
        if (isset($r['ipv6'])) out("IPv6: " . $r['ipv6']);
    }
});

heading("IPv4 Connectivity Tests", 2);

runTest("IPv4 forced cURL to IANA", function () {

    if (!function_exists('curl_version')) throw new Exception("cURL not installed");

    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL            => "https://data.iana.org/rdap/dns.json",
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 5,
        CURLOPT_IPRESOLVE      => CURL_IPRESOLVE_V4,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
    ]);

    $result = curl_exec($ch);
    if ($result === false) throw new Exception(curl_error($ch));

    $ip = curl_getinfo($ch, CURLINFO_PRIMARY_IP);
    curl_close($ch);

    out("Connected via IPv4 IP: {$ip}");
});

heading("IDNA Conversion Tests", 2);

runTest("IDNA Convert + Reconvert", function () {

    $domain = "müller.de";

    $ascii = \CodePunch\Base\Util::idn_convert($domain);
    $utf8  = \CodePunch\Base\Util::idn_reconvert($ascii);

    if (!$ascii || !$utf8) throw new Exception("IDN conversion failed");

    out("Original : {$domain}");
    out("ASCII    : {$ascii}");
    out("Reconv   : {$utf8}");

    if (mb_strtolower($utf8) !== mb_strtolower($domain)) throw new Exception("Round-trip mismatch");
});

heading("SED Util cURL Tests", 2);

runTest("Util::curl_get_url", function () {

    $r = \CodePunch\Base\Util::curl_get_url(
        "https://data.iana.org/rdap/dns.json",
        10, false, false, "-"
    );

    if ($r['status'] != 200) throw new Exception("HTTP status " . $r['status']);

    out("Status: " . $r['status']);
});

runTest("Util::curl_get_url_data", function () {

    $r = \CodePunch\Base\Util::curl_get_url_data(
        "https://data.iana.org/rdap/dns.json",
        10
    );

    if ($r['status'] != 200) throw new Exception("HTTP status " . $r['status']);

    out("Final URL : " . $r['furl']);
    out("Remote IP : " . $r['ip']);
});

} // end if ($APP_LOADED) — app-dependent tests

heading("DIAGNOSTICS COMPLETE", 2);

/*
|=============================================================================
| UI Mode — render full HTML page from collected data
|=============================================================================
*/

if ($UI_MODE) {

    // Commit the last open section
    _ui_commit_section();

    // -----------------------------------------------------------------------
    // Tally totals
    // -----------------------------------------------------------------------
    $total_tests = 0;
    $total_pass  = 0;
    $total_fail  = 0;
    $total_skip  = 0;

    foreach ($UI_SECTIONS as $sec) {
        foreach ($sec['entries'] as $e) {
            if (($e['kind'] ?? '') === 'test') {
                $total_tests++;
                if ($e['status'] === 'ok')   $total_pass++;
                if ($e['status'] === 'fail') $total_fail++;
                if ($e['status'] === 'skip') $total_skip++;
            }
        }
    }

    $countedTests   = $total_pass + $total_fail;
    $passRate       = $countedTests > 0 ? round(($total_pass / $countedTests) * 100) : 0;
    $overallStatus  = $total_fail === 0 ? 'ALL SYSTEMS GO' : "{$total_fail} CHECK(S) FAILED";
    $overallCls     = $total_fail === 0 ? 'hero-ok' : 'hero-fail';
    $runTime       = date('Y-m-d H:i:s');
	$phpVersion = PHP_VERSION;

    // -----------------------------------------------------------------------
    // Logo paths — relative URL derived from document root
    // -----------------------------------------------------------------------
    $logoBase   = '/lib/layouts/images/logos/';
    $logoLight  = $logoBase . 'logo.png';
    $logoDark   = $logoBase . 'logo-dbg.png';
    $subFolder  = '';

    // Prefix install sub-folder if app is not at document root
    $docRoot = rtrim($_SERVER['DOCUMENT_ROOT'] ?? '', '/');
    if ($APP_LOADED && $BASE_PATH !== null && $docRoot !== '' && strpos($BASE_PATH, $docRoot) === 0) {
        $subFolder = rtrim(substr($BASE_PATH, strlen($docRoot)), '/');
        if ($subFolder !== '') {
            $logoLight = $subFolder . $logoLight;
            $logoDark  = $subFolder . $logoDark;
        }
    }

    $logoLightSafe = htmlspecialchars($logoLight, ENT_QUOTES, 'UTF-8');
    $logoDarkSafe  = htmlspecialchars($logoDark,  ENT_QUOTES, 'UTF-8');

    // Base URL — the sub-folder prefix, or '/' if installed at document root
    $baseUrl     = $subFolder !== '' ? $subFolder : '';
    $baseUrlSafe = htmlspecialchars($baseUrl . '/', ENT_QUOTES, 'UTF-8');

    // -----------------------------------------------------------------------
    // Logout URL — only meaningful when app is loaded and auth is active
    // -----------------------------------------------------------------------
    $returnPath = (basename(__DIR__) === 'tools') ? 'tools/diagnostics' : 'diagnostics';
    $logoutUrl  = $APP_LOADED
        ? htmlspecialchars('/?logout&return=' . $returnPath, ENT_QUOTES, 'UTF-8')
        : null;

    // -----------------------------------------------------------------------
    // Render helpers
    // -----------------------------------------------------------------------

    function render_line(array $entry): string
    {
        $cls  = match($entry['type'] ?? 'info') {
            'ok'   => 'line-ok',
            'fail' => 'line-fail',
            default => 'line-info',
        };
        $safe = htmlspecialchars($entry['msg'] ?? '', ENT_QUOTES, 'UTF-8');
        return "<div class='line {$cls}'>{$safe}</div>";
    }

    function render_section_entries(array $entries): string
    {
        $html = '';
        foreach ($entries as $entry) {

            if (($entry['kind'] ?? 'line') === 'test') {

                $status  = $entry['status'] ?? 'fail';
                $rowCls  = $status === 'ok' ? 'test-ok' : ($status === 'skip' ? 'test-skip' : 'test-fail');
                $icon    = $status === 'ok' ? '&#10003;' : ($status === 'skip' ? '&#8212;' : '&#10007;');
                $iconCls = $status === 'ok' ? 'ti-ok' : ($status === 'skip' ? 'ti-skip' : 'ti-fail');
                $name    = htmlspecialchars($entry['name'], ENT_QUOTES, 'UTF-8');
                $ms      = number_format(($entry['time'] ?? 0) * 1000, 1) . 'ms';

                $errHtml = '';
                if (!empty($entry['errmsg'])) {
                    $errClass = $status === 'skip' ? 'test-msg test-msg-skip' : 'test-err';
                    $errHtml = '<div class="' . $errClass . '">'
                             . htmlspecialchars($entry['errmsg'], ENT_QUOTES, 'UTF-8')
                             . '</div>';
                }

                $subHtml = '';
                if (!empty($entry['sub'])) {
                    $subHtml = '<div class="test-sub">';
                    foreach ($entry['sub'] as $sub) {
                        $subHtml .= render_line($sub);
                    }
                    $subHtml .= '</div>';
                }

                $html .= "
                <div class='test-row {$rowCls}'>
                  <div class='test-hd'>
                    <span class='ti {$iconCls}'>{$icon}</span>
                    <span class='tn'>{$name}</span>
                    <span class='tt'>{$ms}</span>
                  </div>
                  {$errHtml}
                  {$subHtml}
                </div>";

            } else {
                $html .= render_line($entry);
            }
        }
        return $html;
    }

    // -----------------------------------------------------------------------
    // Build sections HTML
    // -----------------------------------------------------------------------
    $sections_html = '';

    foreach ($UI_SECTIONS as $sec) {

        $title   = $sec['title']   ?? '';
        $level   = $sec['level']   ?? 2;
        $entries = $sec['entries'] ?? [];

        // Level-1 is the page title — shown in the hero
        if ($level === 1) continue;

        $s_pass = $s_fail = $s_skip = 0;
        foreach ($entries as $e) {
            if (($e['kind'] ?? '') === 'test') {
                if ($e['status'] === 'ok')   $s_pass++;
                if ($e['status'] === 'fail') $s_fail++;
                if ($e['status'] === 'skip') $s_skip++;
            }
        }

        $hasTests = ($s_pass + $s_fail + $s_skip) > 0;
        $cardCls  = 'card' . ($level === 2 ? ' card-l2' : '');
        if ($s_fail > 0)     $cardCls .= ' card-fail';
        elseif ($s_pass > 0) $cardCls .= ' card-ok';

        $titleSafe = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');

        $badgeHtml = '';
        if ($hasTests) {
            if ($s_fail > 0) {
                $badgeHtml = "<span class='badge bdg-fail'>{$s_fail} failed</span>";
            } elseif ($s_pass > 0 && $s_skip > 0) {
                $badgeHtml = "<span class='badge bdg-mixed'>{$s_pass} passed, {$s_skip} skipped</span>";
            } elseif ($s_pass > 0) {
                $badgeHtml = "<span class='badge bdg-ok'>{$s_pass} passed</span>";
            } elseif ($s_skip > 0) {
                $badgeHtml = "<span class='badge bdg-skip'>{$s_skip} skipped</span>";
            }
        }

        $body = render_section_entries($entries);

        $sections_html .= "
        <div class='{$cardCls}'>
          <div class='card-hd'>
            <span class='card-title'>{$titleSafe}</span>
            {$badgeHtml}
          </div>
          <div class='card-body'>{$body}</div>
        </div>";
    }

    // -----------------------------------------------------------------------
    // Emit page
    // -----------------------------------------------------------------------

    // Logout button HTML — omitted in partial mode (no auth to log out of)
    $logoutBtnHtml = $logoutUrl !== null
        ? "<a href=\"{$logoutUrl}\" class=\"logout-btn\" title=\"Log out\">
          <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">
            <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\"/>
            <polyline points=\"16 17 21 12 16 7\"/>
            <line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\"/>
          </svg>
          <span>Logout</span>
        </a>"
        : '';

    // Partial mode notice — shown in hero when app is not loaded
    $partialNoticeHtml = !$APP_LOADED
        ? "<div class=\"partial-notice\">
            <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"width:14px;height:14px;flex-shrink:0\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\"/><line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\"/></svg>
            Partial mode &mdash; application root not found. Showing PHP &amp; system information only.
          </div>"
        : '';

    // Countdown only makes sense when auth is active
    $countdownHtml = $APP_LOADED
        ? "<span class=\"countdown\" id=\"countdown\"><span class=\"countdown-dot\"></span> <span id=\"countdown-label\">clearing in 5:00</span></span>"
        : '';

    header('Content-Type: text/html; charset=utf-8');

    echo <<<HTML
<!DOCTYPE html>
<html lang="en" data-theme="system">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Diagnostics — Watch My Domains</title>
<link rel="shortcut icon" type="image/x-icon" href="../lib/layouts/images/favicon.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

  /* ── DARK palette ── */
  :root,
  [data-theme="dark"] {
    --bg:       #101216;
    --surf:     #181b23;
    --surf3:    #1e2130;
    --hd-bg:    rgba(255,255,255,0.025);

    --bd:       rgba(255,255,255,0.10);
    --bd-ok:    rgba(52,211,153,0.32);
    --bd-fail:  rgba(251,113,133,0.38);

    --tx:       #eef0f8;
    --tx2:      #c8cde0;
    --tx3:      #818799;

    --ok:       #34d399;
    --ok-dim:   rgba(52,211,153,0.13);
    --fail:     #fb7185;
    --fail-dim: rgba(251,113,133,0.13);
    --fail-txt: #fca5a5;
    --fail-bar: rgba(251,113,133,0.50);
    --fail-bg:  rgba(251,113,133,0.09);
	
	--skip:      #fbbf24;
    --skip-dim:  rgba(251,191,36,0.12);
    --skip-txt:  #fcd34d;
    --skip-bar:  rgba(251,191,36,0.45);

    --ac:       #818cf8;
    --ac2:      #a78bfa;

    --dot-color: rgba(129,140,248,0.06);
    --hero-glow: rgba(129,140,248,0.15);

    --tok-bg:   rgba(129,140,248,0.10);
    --tok-bd:   rgba(129,140,248,0.22);
  }

  /* ── LIGHT palette ── */
  [data-theme="light"] {
    --bg:       #f4f5f9;
    --surf:     #ffffff;
    --surf3:    #f0f1f6;
    --hd-bg:    rgba(0,0,0,0.025);

    --bd:       rgba(0,0,0,0.09);
    --bd-ok:    rgba(5,150,105,0.25);
    --bd-fail:  rgba(220,38,38,0.25);

    --tx:       #111318;
    --tx2:      #374151;
    --tx3:      #6b7280;

    --ok:       #059669;
    --ok-dim:   rgba(5,150,105,0.10);
    --fail:     #dc2626;
    --fail-dim: rgba(220,38,38,0.08);
    --fail-txt: #b91c1c;
    --fail-bar: rgba(220,38,38,0.45);
    --fail-bg:  rgba(220,38,38,0.06);
	
    --skip:      #d97706;
    --skip-dim:  rgba(217,119,6,0.10);
    --skip-txt:  #b45309;
    --skip-bar:  rgba(217,119,6,0.35);

    --ac:       #4f46e5;
    --ac2:      #7c3aed;

    --dot-color: rgba(79,70,229,0.05);
    --hero-glow: rgba(79,70,229,0.10);

    --tok-bg:   rgba(79,70,229,0.07);
    --tok-bd:   rgba(79,70,229,0.18);
  }

  /* ── SYSTEM: follow OS preference ── */
  @media (prefers-color-scheme: light) {
    [data-theme="system"] {
      --bg:       #f4f5f9;
      --surf:     #ffffff;
      --surf3:    #f0f1f6;
      --hd-bg:    rgba(0,0,0,0.025);
      --bd:       rgba(0,0,0,0.09);
      --bd-ok:    rgba(5,150,105,0.25);
      --bd-fail:  rgba(220,38,38,0.25);
      --tx:       #111318;
      --tx2:      #374151;
      --tx3:      #6b7280;
      --ok:       #059669;
      --ok-dim:   rgba(5,150,105,0.10);
      --fail:     #dc2626;
      --fail-dim: rgba(220,38,38,0.08);
      --fail-txt: #b91c1c;
      --fail-bar: rgba(220,38,38,0.45);
      --fail-bg:  rgba(220,38,38,0.06);
      --skip:      #d97706;
      --skip-dim:  rgba(217,119,6,0.10);
      --skip-txt:  #b45309;
      --skip-bar:  rgba(217,119,6,0.35);
      --ac:       #4f46e5;
      --ac2:      #7c3aed;
      --dot-color: rgba(79,70,229,0.05);
      --hero-glow: rgba(79,70,229,0.10);
      --tok-bg:   rgba(79,70,229,0.07);
      --tok-bd:   rgba(79,70,229,0.18);
    }
  }

  :root {
    --mono: 'IBM Plex Mono', monospace;
    --sans: 'DM Sans', sans-serif;
    --r:    10px;
    --rsm:  7px;
  }

  html { scroll-behavior: smooth; }

  body {
    background: var(--bg);
    color: var(--tx);
    font-family: var(--sans);
    font-size: 14px;
    line-height: 1.6;
    min-height: 100vh;
    transition: background 0.2s, color 0.2s;
  }

  body::before {
    content: '';
    position: fixed; inset: 0;
    background-image: radial-gradient(var(--dot-color) 1px, transparent 1px);
    background-size: 28px 28px;
    pointer-events: none; z-index: 0;
  }

  .wrap {
    position: relative; z-index: 1;
    max-width: 980px; margin: 0 auto;
    padding: 44px 24px 88px;
  }

  /* ── HERO ── */
  .hero {
    padding: 36px 40px;
    background: var(--surf);
    border: 1px solid var(--bd);
    border-radius: 16px;
    margin-bottom: 32px;
    position: relative; overflow: hidden;
  }
  .hero::before {
    content: '';
    position: absolute; top: -80px; right: -80px;
    width: 320px; height: 320px;
    background: radial-gradient(circle, var(--hero-glow) 0%, transparent 65%);
    pointer-events: none;
  }

  /* top row inside hero: eyebrow left, theme toggle right */
  .hero-top {
    display: flex; align-items: center;
    justify-content: space-between;
    margin-bottom: 14px;
  }
  .hero-eye {
    font-family: var(--mono);
    font-size: 10.5px; letter-spacing: 0.16em;
    text-transform: uppercase; color: var(--ac);
    display: flex; align-items: center; gap: 8px;
  }
  .hero-eye::before {
    content: '';
    display: inline-block;
    width: 20px; height: 2px;
    background: var(--ac); border-radius: 2px;
  }
  .hero-title {
    font-size: 30px; font-weight: 700;
    letter-spacing: -0.025em; color: var(--tx);
    margin-bottom: 6px;
  }
  .hero-title em {
    font-style: normal;
    background: linear-gradient(120deg, var(--ac), var(--ac2));
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
  }
  .hero-meta {
    font-family: var(--mono);
    font-size: 12px; color: var(--tx3);
    margin-bottom: 18px;
  }
  .hero-badge {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 7px 16px; border-radius: 100px;
    font-family: var(--mono);
    font-size: 12px; font-weight: 600; letter-spacing: 0.06em;
  }
  .hero-ok   { background: var(--ok-dim);   color: var(--ok);   border: 1px solid var(--bd-ok); }
  .hero-fail { background: var(--fail-dim);  color: var(--fail); border: 1px solid var(--bd-fail); }
  .hero-badge::before {
    content: '';
    width: 7px; height: 7px; border-radius: 50%;
    background: currentColor; box-shadow: 0 0 7px currentColor;
    animation: blink 2.2s ease-in-out infinite;
  }
  @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.3} }

  /* ── THEME TOGGLE ── */
  .theme-toggle {
    display: flex; align-items: center;
    background: var(--tok-bg);
    border: 1px solid var(--tok-bd);
    border-radius: 100px;
    padding: 3px;
    gap: 2px;
    position: relative; z-index: 1;
    flex-shrink: 0;
  }
  .tt-btn {
    display: flex; align-items: center; justify-content: center;
    gap: 5px;
    padding: 5px 11px;
    border-radius: 100px;
    border: none; cursor: pointer;
    font-family: var(--mono); font-size: 11px;
    font-weight: 500; letter-spacing: 0.04em;
    background: transparent;
    color: var(--tx3);
    transition: background 0.15s, color 0.15s;
    white-space: nowrap;
  }
  .tt-btn svg { width: 13px; height: 13px; flex-shrink: 0; }
  .tt-btn:hover { color: var(--tx); }
  .tt-btn.active {
    background: var(--surf);
    color: var(--ac);
    box-shadow: 0 1px 4px rgba(0,0,0,0.15);
  }
  [data-theme="light"] .tt-btn.active {
    box-shadow: 0 1px 4px rgba(0,0,0,0.12);
  }

  /* ── LOGOUT BUTTON ── */
  .logout-btn {
    display: flex; align-items: center; gap: 6px;
    padding: 6px 13px;
    border-radius: 100px;
    border: 1px solid var(--bd);
    cursor: pointer;
    font-family: var(--mono); font-size: 11px;
    font-weight: 500; letter-spacing: 0.04em;
    background: transparent;
    color: var(--tx3);
    text-decoration: none;
    transition: border-color 0.15s, color 0.15s, background 0.15s;
    flex-shrink: 0;
    white-space: nowrap;
  }
  .logout-btn svg { width: 13px; height: 13px; flex-shrink: 0; }
  .logout-btn:hover {
    color: var(--fail);
    border-color: var(--bd-fail);
    background: var(--fail-dim);
  }

  /* wrapper so theme toggle and logout sit side by side */
  .hero-controls {
    display: flex; align-items: center; gap: 10px; flex-shrink: 0;
  }

  /* ── SUMMARY BAR ── */
  .sum-bar {
    display: flex; gap: 10px; flex-wrap: wrap;
    margin-bottom: 30px;
  }
  .sum-pill {
    display: flex; align-items: center; gap: 8px;
    padding: 9px 18px;
    background: var(--surf); border: 1px solid var(--bd);
    border-radius: 100px;
    font-family: var(--mono); font-size: 12.5px;
    color: var(--tx2);
  }
  .sum-pill b { font-size: 15px; font-weight: 600; }
  .sp-ok   b { color: var(--ok); }
  .sp-fail b { color: var(--fail); }
  .sp-skip b { color: var(--skip); }
  .sp-all  b { color: var(--ac); }

  /* ── CARDS ── */
  .card {
    background: var(--surf);
    border: 1px solid var(--bd);
    border-radius: var(--r);
    margin-bottom: 10px; overflow: hidden;
  }
  .card-l2  { margin-bottom: 20px; }
  .card-ok  { border-color: var(--bd-ok); }
  .card-fail{ border-color: var(--bd-fail); }

  .card-hd {
    display: flex; align-items: center;
    justify-content: space-between; gap: 10px;
    padding: 13px 20px;
    background: var(--hd-bg);
    border-bottom: 1px solid var(--bd);
  }
  .card-l2 .card-hd { padding: 15px 24px; }

  .card-title {
    font-weight: 600; font-size: 12px;
    letter-spacing: 0.07em; text-transform: uppercase;
    color: var(--tx2);
  }
  .card-l2 .card-title { font-size: 13px; color: var(--tx); }

  .badge {
    font-family: var(--mono); font-size: 11px;
    padding: 3px 10px; border-radius: 100px; flex-shrink: 0;
  }
  .bdg-ok    { background: var(--ok-dim);   color: var(--ok);   border: 1px solid var(--bd-ok); }
  .bdg-fail  { background: var(--fail-dim); color: var(--fail); border: 1px solid var(--bd-fail); }
  .bdg-skip  { background: var(--skip-dim); color: var(--skip); border: 1px solid var(--skip-bar); }
  .bdg-mixed { background: var(--tok-bg);   color: var(--tx2);  border: 1px solid var(--tok-bd); }

  .card-body {
    padding: 14px 20px;
    display: flex; flex-direction: column; gap: 5px;
  }
  .card-l2 .card-body { padding: 18px 24px; }

  /* ── PLAIN INFO LINES ── */
  .line {
    font-family: var(--mono);
    font-size: 12.5px; padding: 2px 0;
    word-break: break-all;
    color: var(--tx2);
  }
  .line-ok   { color: var(--ok); }
  .line-fail { color: var(--fail); }

  /* ── TEST ROWS ── */
  .test-row {
    background: var(--surf3);
    border: 1px solid var(--bd);
    border-radius: var(--rsm);
    padding: 11px 14px;
    display: flex; flex-direction: column; gap: 6px;
  }
  .test-ok   { border-color: var(--bd-ok); }
  .test-fail { border-color: var(--bd-fail); }
  .test-skip { border-color: var(--skip-bar); }

  .test-hd { display: flex; align-items: center; gap: 10px; }

  .ti {
    font-size: 14px; font-weight: 700;
    flex-shrink: 0; width: 20px; text-align: center;
  }
  .ti-ok   { color: var(--ok); }
  .ti-fail { color: var(--fail); }
  .ti-skip { color: var(--skip); }

  .tn {
    font-family: var(--sans);
    font-size: 13.5px; font-weight: 500;
    color: var(--tx); flex: 1;
  }
  .tt {
    font-family: var(--mono);
    font-size: 11px; color: var(--tx3); flex-shrink: 0;
  }

  .test-err {
    font-family: var(--mono);
    font-size: 12px; color: var(--fail-txt);
    margin-left: 30px; padding: 6px 10px;
    background: var(--fail-bg);
    border-radius: 5px;
    border-left: 2px solid var(--fail-bar);
    line-height: 1.5;
  }

  .test-msg {
    font-family: var(--mono);
    font-size: 12px;
    margin-left: 30px; padding: 6px 10px;
    border-radius: 5px;
    line-height: 1.5;
  }

  .test-msg-skip {
    color: var(--skip-txt);
    background: var(--skip-dim);
    border-left: 2px solid var(--skip-bar);
  }

  .test-sub {
    margin-left: 30px;
    display: flex; flex-direction: column; gap: 1px;
    padding-top: 7px; border-top: 1px solid var(--bd);
  }
  .test-sub .line {
    font-size: 12px; color: var(--tx2); padding: 1px 0;
  }

  /* ── FOOTER ── */
  .foot {
    margin-top: 52px; padding-top: 22px;
    border-top: 1px solid var(--bd);
    text-align: center;
    font-family: var(--mono); font-size: 11px; color: var(--tx3);
    display: flex; align-items: center; justify-content: center; gap: 16px;
    flex-wrap: wrap;
  }

  /* ── AUTO-CLEAR COUNTDOWN ── */
  .countdown {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 3px 10px;
    border-radius: 100px;
    border: 1px solid var(--bd);
    font-family: var(--mono); font-size: 11px;
    color: var(--tx3);
    transition: color 0.3s, border-color 0.3s, background 0.3s;
  }
  .countdown.warn {
    color: var(--fail);
    border-color: var(--bd-fail);
    background: var(--fail-dim);
  }
  .countdown-dot {
    width: 5px; height: 5px; border-radius: 50%;
    background: currentColor; flex-shrink: 0;
  }

  @media (max-width: 600px) {
    .wrap { padding: 20px 14px 60px; }
    .hero { padding: 24px 20px; }
    .hero-title { font-size: 22px; }
    .tt-btn span { display: none; }
    .tt-btn { padding: 5px 8px; }
  }

  /* ── LOGO ── */
  .site-logo {
    display: block;
    margin-bottom: 28px;
    line-height: 0;
  }
  .site-logo img {
    height: 52px;
    width: auto;
  }
  /* swap src via content-visibility trick — use data attributes + CSS display */
  .logo-light { display: block; }
  .logo-dark  { display: none;  }

  [data-theme="dark"] .logo-light  { display: none;  }
  [data-theme="dark"] .logo-dark   { display: block; }

  /* system theme: respect OS preference */
  @media (prefers-color-scheme: dark) {
    [data-theme="system"] .logo-light { display: none;  }
    [data-theme="system"] .logo-dark  { display: block; }
  }

  /* ── PARTIAL MODE NOTICE ── */
  .partial-notice {
    display: inline-flex; align-items: center; gap: 7px;
    margin-bottom: 14px;
    padding: 7px 14px;
    border-radius: 100px;
    background: rgba(251,191,36,0.10);
    border: 1px solid rgba(251,191,36,0.30);
    font-family: var(--mono); font-size: 11.5px;
    color: #fbbf24;
    line-height: 1.4;
  }
  [data-theme="light"] .partial-notice {
    background: rgba(217,119,6,0.08);
    border-color: rgba(217,119,6,0.25);
    color: #b45309;
  }

</style>
</head>
<body>
<div class="wrap">

  <div class="site-logo">
    <a href="{$baseUrlSafe}tools/">
      <img class="logo-light" src="{$logoLightSafe}" alt="Watch My Domains">
      <img class="logo-dark"  src="{$logoDarkSafe}"  alt="Watch My Domains">
    </a>
  </div>

  <div class="hero">
    <div class="hero-top">
      <div class="hero-eye">Watch My Domains — Server Edition</div>
      <div class="hero-controls">
        <div class="theme-toggle" id="themeToggle">
          <button class="tt-btn" data-t="light" title="Light mode">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <circle cx="12" cy="12" r="4"/><line x1="12" y1="2" x2="12" y2="4"/><line x1="12" y1="20" x2="12" y2="22"/>
              <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
              <line x1="2" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="22" y2="12"/>
              <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
            </svg>
            <span>Light</span>
          </button>
          <button class="tt-btn" data-t="system" title="System default">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>
            </svg>
            <span>System</span>
          </button>
          <button class="tt-btn" data-t="dark" title="Dark mode">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
            </svg>
            <span>Dark</span>
          </button>
        </div>
        {$logoutBtnHtml}
      </div>
    </div>
    <div class="hero-title"><em>Diagnostic</em> Report</div>
    <div class="hero-meta">Run at {$runTime} &nbsp;&middot;&nbsp; PHP {$phpVersion} &nbsp;&middot;&nbsp; {$total_tests} checks</div>
    {$partialNoticeHtml}
    <div class="hero-badge {$overallCls}">{$overallStatus}</div>
  </div>

  <div class="sum-bar">
    <div class="sum-pill sp-all"><b>{$total_tests}</b>&nbsp;total</div>
    <div class="sum-pill sp-ok" ><b>{$total_pass}</b>&nbsp;passed</div>
    <div class="sum-pill sp-fail"><b>{$total_fail}</b>&nbsp;failed</div>
    <div class="sum-pill sp-skip"><b>{$total_skip}</b>&nbsp;skipped</div>
    <div class="sum-pill sp-all"><b>{$passRate}%</b>&nbsp;pass rate</div>
  </div>

  {$sections_html}

  <div class="foot">
    <span>Softnik Technologies &nbsp;&middot;&nbsp; diagnostics.php &nbsp;&middot;&nbsp; Remove from production when done</span>
    {$countdownHtml}
  </div>

</div>

<script>
(function () {
  var KEY = 'sed_diag_theme';
  var html = document.documentElement;
  var btns = document.querySelectorAll('.tt-btn');

  function apply(t) {
    html.setAttribute('data-theme', t);
    btns.forEach(function (b) {
      b.classList.toggle('active', b.getAttribute('data-t') === t);
    });
  }

  // Restore saved preference, default to 'system'
  var saved = localStorage.getItem(KEY) || 'system';
  apply(saved);

  btns.forEach(function (btn) {
    btn.addEventListener('click', function () {
      var t = btn.getAttribute('data-t');
      localStorage.setItem(KEY, t);
      apply(t);
    });
  });
})();

// ── Auto-clear countdown ──
(function () {
  var pill = document.getElementById('countdown');
  if (!pill) return;   // partial mode — no countdown

  var SECONDS  = 5 * 60;
  var WARN_AT  = 60;
  var logoutBtn = document.querySelector('.logout-btn');
  var logoutHref = logoutBtn ? logoutBtn.getAttribute('href') : '/';
  var label    = document.getElementById('countdown-label');
  var remaining = SECONDS;

  function fmt(s) {
    var m = Math.floor(s / 60);
    var sec = s % 60;
    return 'clearing in ' + m + ':' + (sec < 10 ? '0' : '') + sec;
  }

  function tick() {
    remaining--;
    label.textContent = fmt(remaining);
    if (remaining <= WARN_AT) {
      pill.classList.add('warn');
    }
    if (remaining <= 0) {
      window.location.href = logoutHref;
    }
  }

  label.textContent = fmt(remaining);
  setInterval(tick, 1000);
})();
</script>
</body>
</html>
HTML;

    exit;
}