Add custom 404 error page and enhance server status checks
This commit is contained in:
parent
25abf317cb
commit
33b1efb903
164
503.php
Normal file
164
503.php
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
<?php
|
||||||
|
http_response_code(404);
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>404 – Seite nicht gefunden · Fabian Schieder</title>
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
max-width: 480px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code {
|
||||||
|
font-size: clamp(6rem, 20vw, 10rem);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
background: linear-gradient(135deg, #6366f1 0%, #0ea5e9 60%, #a855f7 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
letter-spacing: -4px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
animation: float 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-10px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text);
|
||||||
|
-webkit-text-fill-color: var(--text);
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-desc {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-divider {
|
||||||
|
width: 48px;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 99px;
|
||||||
|
background: linear-gradient(90deg, #6366f1, #0ea5e9);
|
||||||
|
border: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.65rem 1.4rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #6366f1, #0ea5e9);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
border-color: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glow hinter der 404 */
|
||||||
|
.glow {
|
||||||
|
position: fixed;
|
||||||
|
width: 500px;
|
||||||
|
height: 500px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, rgba(99,102,241,0.12) 0%, transparent 70%);
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -60%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="background-blur"></div>
|
||||||
|
<div class="glow"></div>
|
||||||
|
|
||||||
|
<div class="error-wrap">
|
||||||
|
<div class="error-icon">🔍</div>
|
||||||
|
|
||||||
|
<div class="error-code">404</div>
|
||||||
|
|
||||||
|
<hr class="error-divider">
|
||||||
|
|
||||||
|
<h1 class="error-title">Seite nicht gefunden</h1>
|
||||||
|
|
||||||
|
<p class="error-desc">
|
||||||
|
Die Seite, die du suchst, existiert nicht (mehr)<br>
|
||||||
|
oder wurde vielleicht verschoben.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="error-actions">
|
||||||
|
<a href="/" class="btn btn-primary">
|
||||||
|
← Zurück zur Startseite
|
||||||
|
</a>
|
||||||
|
<a href="javascript:history.back()" class="btn btn-secondary">
|
||||||
|
Zurück
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@ -99,13 +99,26 @@ function extract_http_status_code(array $headers): ?int
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Decide if a HTTP status code means the service is reachable.
|
* Decide if a HTTP status code means the service is reachable.
|
||||||
* We treat 2xx/3xx as up and also 401/403 as up (service is there, but needs auth).
|
* Note: Redirects (3xx) are treated as not OK here because we intentionally don't follow redirects.
|
||||||
|
* Otherwise endpoints like /nextcloud (301 -> /nextcloud/ -> 503) would look "online".
|
||||||
*/
|
*/
|
||||||
function is_reachable_http_code(?int $code): bool
|
function is_reachable_http_code(?int $code): bool
|
||||||
{
|
{
|
||||||
if ($code === null) return false;
|
if ($code === null) return false;
|
||||||
if ($code >= 200 && $code < 400) return true;
|
|
||||||
|
// Explicitly DOWN: server-side failures like 503 Service Unavailable.
|
||||||
|
if ($code >= 500 && $code < 600) return false;
|
||||||
|
|
||||||
|
// UP: normal success
|
||||||
|
if ($code >= 200 && $code < 300) return true;
|
||||||
|
|
||||||
|
// UP: service is there but requires auth
|
||||||
if ($code === 401 || $code === 403) return true;
|
if ($code === 401 || $code === 403) return true;
|
||||||
|
|
||||||
|
// UP: rate limited, but reachable
|
||||||
|
if ($code === 429) return true;
|
||||||
|
|
||||||
|
// Redirects are handled separately (we do not follow them)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,12 +194,56 @@ function check_http_request(string $url, string $method, float $timeoutSeconds =
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP check (no redirects). Try HEAD first, then a minimal GET fallback (some servers lie on HEAD).
|
* HTTP check (no redirects).
|
||||||
|
* - Tries HEAD first.
|
||||||
|
* - If result is a redirect, probes the Location once (still without following further redirects).
|
||||||
|
* - If HEAD fails, tries a minimal GET with Range.
|
||||||
* @return array{ok:bool, code:int|null, ms:int|null, error:string|null}
|
* @return array{ok:bool, code:int|null, ms:int|null, error:string|null}
|
||||||
*/
|
*/
|
||||||
function check_http_head(string $url, float $timeoutSeconds = 1.6): array
|
function check_http_head(string $url, float $timeoutSeconds = 1.6): array
|
||||||
{
|
{
|
||||||
$head = check_http_request($url, 'HEAD', $timeoutSeconds);
|
$head = check_http_request($url, 'HEAD', $timeoutSeconds);
|
||||||
|
|
||||||
|
// If we got a redirect, probe the Location target once, because we don't follow redirects automatically.
|
||||||
|
if ($head['code'] !== null && $head['code'] >= 300 && $head['code'] < 400) {
|
||||||
|
$loc = null;
|
||||||
|
if (isset($http_response_header) && is_array($http_response_header)) {
|
||||||
|
foreach ($http_response_header as $h) {
|
||||||
|
if (stripos((string)$h, 'Location:') === 0) {
|
||||||
|
$loc = trim(substr((string)$h, strlen('Location:')));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($loc) && $loc !== '') {
|
||||||
|
// Support relative redirects
|
||||||
|
if (str_starts_with($loc, '/')) {
|
||||||
|
$p = parse_url($url);
|
||||||
|
if (is_array($p) && !empty($p['scheme']) && !empty($p['host'])) {
|
||||||
|
$port = !empty($p['port']) ? ':' . (string)$p['port'] : '';
|
||||||
|
$loc = strtolower((string)$p['scheme']) . '://' . (string)$p['host'] . $port . $loc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Probe once; no further redirects.
|
||||||
|
$probe = check_http_request($loc, 'HEAD', $timeoutSeconds);
|
||||||
|
if ($probe['code'] === null) {
|
||||||
|
$probe = check_http_request($loc, 'GET', $timeoutSeconds, "Range: bytes=0-0\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the target is clearly reachable/unreachable, use that.
|
||||||
|
if ($probe['code'] !== null && ($probe['code'] < 300 || $probe['code'] >= 400)) {
|
||||||
|
return $probe;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise keep the redirect info, but don't mark it UP.
|
||||||
|
return ['ok' => false, 'code' => $head['code'], 'ms' => $head['ms'], 'error' => 'Redirect'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['ok' => false, 'code' => $head['code'], 'ms' => $head['ms'], 'error' => 'Redirect'];
|
||||||
|
}
|
||||||
|
|
||||||
if ($head['ok']) {
|
if ($head['ok']) {
|
||||||
return $head;
|
return $head;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ require_once __DIR__ . '/includes/lib/view_helpers.php';
|
|||||||
|
|
||||||
// ── Serverstatus (mit Cache) ──────────────────────────────────────────────
|
// ── Serverstatus (mit Cache) ──────────────────────────────────────────────
|
||||||
$cacheTtlSeconds = 30;
|
$cacheTtlSeconds = 30;
|
||||||
$cacheKey = 'server_status_v2';
|
$cacheKey = 'server_status_v3';
|
||||||
$cacheFile = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $cacheKey . '.json';
|
$cacheFile = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $cacheKey . '.json';
|
||||||
|
|
||||||
$serverStatus = null;
|
$serverStatus = null;
|
||||||
|
|||||||
44
scripts/dump_headers.php
Normal file
44
scripts/dump_headers.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$url = $argv[1] ?? null;
|
||||||
|
if (!$url) {
|
||||||
|
fwrite(STDERR, "Usage: php scripts/dump_headers.php <url>\n");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ctx = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'follow_location' => 0,
|
||||||
|
'max_redirects' => 0,
|
||||||
|
'timeout' => 3.0,
|
||||||
|
'ignore_errors' => true,
|
||||||
|
'method' => 'GET',
|
||||||
|
'header' => "User-Agent: HeaderDump/1.0\r\nRange: bytes=0-0\r\n",
|
||||||
|
],
|
||||||
|
'ssl' => [
|
||||||
|
'verify_peer' => true,
|
||||||
|
'verify_peer_name' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$http_response_header = null;
|
||||||
|
set_error_handler(static function (int $severity, string $message): bool {
|
||||||
|
fwrite(STDERR, "PHP warning captured: $message\n");
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
$fp = @fopen($url, 'rb', false, $ctx);
|
||||||
|
restore_error_handler();
|
||||||
|
if ($fp !== false) {
|
||||||
|
fclose($fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($http_response_header) || !is_array($http_response_header)) {
|
||||||
|
echo "(no headers)\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($http_response_header as $h) {
|
||||||
|
echo $h, "\n";
|
||||||
|
}
|
||||||
|
|
||||||
@ -6,6 +6,7 @@ require __DIR__ . '/../includes/lib/server_status.php';
|
|||||||
$targets = [
|
$targets = [
|
||||||
['url' => 'https://example.com'],
|
['url' => 'https://example.com'],
|
||||||
['url' => 'https://example.com/does-not-exist-hopefully'],
|
['url' => 'https://example.com/does-not-exist-hopefully'],
|
||||||
|
['url' => 'https://fabianschieder.com/nextcloud'],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($targets as $t) {
|
foreach ($targets as $t) {
|
||||||
@ -15,5 +16,12 @@ foreach ($targets as $t) {
|
|||||||
echo " error=", $r['error'];
|
echo " error=", $r['error'];
|
||||||
}
|
}
|
||||||
echo PHP_EOL;
|
echo PHP_EOL;
|
||||||
}
|
|
||||||
|
|
||||||
|
// Zusatz: expliziter GET-Check (ohne Redirects), um 301/302 vs. 5xx besser zu sehen.
|
||||||
|
$g = check_http_request($t['url'], 'GET', 2.5, "Range: bytes=0-0\r\n");
|
||||||
|
echo " GET => ", ($g['ok'] ? 'UP' : 'DOWN'), " code=", ($g['code'] ?? 'null'), " ms=", ($g['ms'] ?? 'null');
|
||||||
|
if (!empty($g['error'])) {
|
||||||
|
echo " error=", $g['error'];
|
||||||
|
}
|
||||||
|
echo PHP_EOL;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user