errserve 0.10.0

Servidor de páginas de error para Traefik que devuelve el código de error real
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{code}} - {{reason}}</title>
  <style>
    :root {
      --matrix-green: #00ff41;
      --matrix-dim: #008f11;
      --matrix-dark: #003b00;
      --bg-black: #030a03;
      --glass-bg: rgba(3, 15, 3, 0.85);
      --glass-border: rgba(0, 255, 65, 0.3);
      --font-mono: 'Courier New', Courier, monospace;
    }

    * { box-sizing: border-box; margin: 0; padding: 0; }

    body {
      background-color: var(--bg-black);
      color: var(--matrix-green);
      font-family: var(--font-mono);
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      overflow: hidden;
      position: relative;
    }

    /* Efecto CRT Scanlines */
    body::before {
      content: " ";
      display: block;
      position: absolute;
      top: 0; left: 0; bottom: 0; right: 0;
      background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%),
                  linear-gradient(90deg, rgba(255, 0, 0, 0.03), rgba(0, 255, 0, 0.01), rgba(0, 0, 255, 0.03));
      z-index: 15;
      background-size: 100% 3px, 6px 100%;
      pointer-events: none;
    }

    canvas#matrix-canvas {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      z-index: 1;
    }

    .construct-card {
      position: relative;
      z-index: 10;
      max-width: 650px;
      width: 90%;
      padding: 2.5rem;
      background: var(--glass-bg);
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
      border: 1px solid var(--glass-border);
      border-radius: 12px;
      box-shadow: 0 0 30px rgba(0, 255, 65, 0.2), inset 0 0 15px rgba(0, 255, 65, 0.1);
      text-align: center;
    }

    .error-code {
      font-size: clamp(5rem, 15vw, 9rem);
      font-weight: 900;
      line-height: 0.9;
      color: var(--matrix-green);
      text-shadow: 0 0 10px var(--matrix-green), 0 0 20px var(--matrix-green);
      margin-bottom: 1rem;
      letter-spacing: -2px;
    }

    .error-reason {
      font-size: clamp(1.2rem, 3.5vw, 1.8rem);
      font-weight: bold;
      color: #ffffff;
      text-transform: uppercase;
      letter-spacing: 2px;
      margin-bottom: 1rem;
      text-shadow: 0 0 8px rgba(255, 255, 255, 0.6);
    }

    .error-description {
      font-size: 1rem;
      color: var(--matrix-dim);
      line-height: 1.6;
      margin-bottom: 2rem;
    }

    .pill-box {
      display: flex;
      gap: 1.5rem;
      justify-content: center;
      flex-wrap: wrap;
      margin-top: 1rem;
    }

    .pill-btn {
      padding: 0.8rem 1.8rem;
      font-family: var(--font-mono);
      font-size: 0.95rem;
      font-weight: bold;
      border-radius: 50px;
      cursor: pointer;
      transition: all 0.3s ease;
      text-decoration: none;
      display: inline-flex;
      align-items: center;
      gap: 0.5rem;
    }

    .pill-red {
      background: rgba(239, 68, 68, 0.15);
      border: 1px solid #ef4444;
      color: #ef4444;
      box-shadow: 0 0 15px rgba(239, 68, 68, 0.3);
    }

    .pill-red:hover {
      background: #ef4444;
      color: #000;
      box-shadow: 0 0 25px rgba(239, 68, 68, 0.8);
      transform: translateY(-2px);
    }

    .pill-blue {
      background: rgba(59, 130, 246, 0.15);
      border: 1px solid #3b82f6;
      color: #3b82f6;
      box-shadow: 0 0 15px rgba(59, 130, 246, 0.3);
    }

    .pill-blue:hover {
      background: #3b82f6;
      color: #000;
      box-shadow: 0 0 25px rgba(59, 130, 246, 0.8);
      transform: translateY(-2px);
    }

    .controls-bar {
      position: absolute;
      bottom: 1.5rem;
      z-index: 20;
      display: flex;
      gap: 0.5rem;
      background: rgba(3, 15, 3, 0.9);
      padding: 0.4rem 0.8rem;
      border-radius: 8px;
      border: 1px solid var(--matrix-dim);
    }

    .ctrl-btn {
      background: transparent;
      border: none;
      color: var(--matrix-dim);
      font-family: var(--font-mono);
      padding: 0.3rem 0.6rem;
      cursor: pointer;
      font-size: 0.8rem;
      transition: all 0.2s ease;
    }

    .ctrl-btn:hover, .ctrl-btn.active {
      color: var(--matrix-green);
      text-shadow: 0 0 5px var(--matrix-green);
    }
  </style>
</head>
<body>

  <canvas id="matrix-canvas"></canvas>

  <main class="construct-card">
    <div class="error-code" id="display-code">{{code}}</div>
    <h1 class="error-reason" id="display-reason">{{reason}}</h1>
    <p class="error-description" id="display-description">{{description}}</p>

    <div class="pill-box">
      <a href="/" class="pill-btn pill-blue" onclick="playAudio('blue')">Píldora Azul (Inicio)</a>
      <button onclick="triggerRedPill()" class="pill-btn pill-red">Píldora Roja (Hackear)</button>
    </div>
  </main>

  <div class="controls-bar">
    <button class="ctrl-btn active" onclick="setTheme('green')">Verde Matrix</button>
    <button class="ctrl-btn" onclick="setTheme('red')">Rojo Agente</button>
    <button class="ctrl-btn" onclick="setTheme('cyan')">Cian Nabucodonosor</button>
  </div>

  <script>
    // Fallback si se abre sin procesar con Jinja
    document.addEventListener("DOMContentLoaded", () => {
      const codeEl = document.getElementById("display-code");
      const reasonEl = document.getElementById("display-reason");
      const descEl = document.getElementById("display-description");
      const params = new URLSearchParams(window.location.search);

      if (codeEl.innerText.includes("{{") || codeEl.innerText.trim() === "") {
        codeEl.innerText = params.get("code") || "404";
      }
      if (reasonEl.innerText.includes("{{") || reasonEl.innerText.trim() === "") {
        reasonEl.innerText = params.get("reason") || "ANOMALÍA EN EL CONSTRUCTO";
      }
      if (descEl.innerText.includes("{{") || descEl.innerText.trim() === "") {
        descEl.innerText = params.get("description") || "La ubicación de memoria solicitada no existe o ha sido desasignada por los Agentes.";
      }
    });

    // Sintetizador Web Audio API
    let audioCtx = null;
    function initAudio() {
      if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    }

    function playAudio(type) {
      initAudio();
      if (!audioCtx) return;

      const osc = audioCtx.createOscillator();
      const gain = audioCtx.createGain();
      osc.connect(gain);
      gain.connect(audioCtx.destination);

      const now = audioCtx.currentTime;
      if (type === 'red') {
        osc.type = 'sawtooth';
        osc.frequency.setValueAtTime(150, now);
        osc.frequency.exponentialRampToValueAtTime(40, now + 0.4);
        gain.gain.setValueAtTime(0.2, now);
        gain.gain.linearRampToValueAtTime(0, now + 0.4);
        osc.start(now);
        osc.stop(now + 0.4);
      } else {
        osc.type = 'sine';
        osc.frequency.setValueAtTime(800, now);
        osc.frequency.exponentialRampToValueAtTime(400, now + 0.2);
        gain.gain.setValueAtTime(0.1, now);
        gain.gain.linearRampToValueAtTime(0, now + 0.2);
        osc.start(now);
        osc.stop(now + 0.2);
      }
    }

    // Lluvia Digital Matrix Canvas
    const canvas = document.getElementById('matrix-canvas');
    const ctx = canvas.getContext('2d');

    let width, height;
    let columns = [];
    const katakana = 'アァカサタナハマヤャラワガザダバパイィキシチニヒミリヰギジヂビピウゥクスツヌフムユュルグズブヅプエェケセテネヘメレヱゲゼデベペオォコソトノホモヨョロヲゴゾドボポヴッン0123456789ABCDEF';

    let mainColor = '#00ff41';
    let dimColor = '#008f11';

    function resize() {
      width = canvas.width = window.innerWidth;
      height = canvas.height = window.innerHeight;
      const colWidth = 18;
      const colCount = Math.floor(width / colWidth);
      columns = Array(colCount).fill(0).map(() => ({
        y: Math.random() * -100,
        speed: Math.random() * 2 + 1
      }));
    }
    window.addEventListener('resize', resize);
    resize();

    function setTheme(color) {
      playAudio('blue');
      const root = document.documentElement;
      if (color === 'red') {
        mainColor = '#ef4444';
        dimColor = '#991b1b';
        root.style.setProperty('--matrix-green', '#ef4444');
        root.style.setProperty('--matrix-dim', '#991b1b');
        root.style.setProperty('--glass-border', 'rgba(239, 68, 68, 0.4)');
      } else if (color === 'cyan') {
        mainColor = '#06b6d4';
        dimColor = '#0e7490';
        root.style.setProperty('--matrix-green', '#06b6d4');
        root.style.setProperty('--matrix-dim', '#0e7490');
        root.style.setProperty('--glass-border', 'rgba(6, 182, 212, 0.4)');
      } else {
        mainColor = '#00ff41';
        dimColor = '#008f11';
        root.style.setProperty('--matrix-green', '#00ff41');
        root.style.setProperty('--matrix-dim', '#008f11');
        root.style.setProperty('--glass-border', 'rgba(0, 255, 65, 0.3)');
      }
      document.querySelectorAll('.ctrl-btn').forEach(b => b.classList.remove('active'));
      event.target.classList.add('active');
    }

    function triggerRedPill() {
      playAudio('red');
      columns.forEach(c => c.speed *= 5);
      setTimeout(() => {
        columns.forEach(c => c.speed /= 5);
      }, 1500);
    }

    function animate() {
      ctx.fillStyle = 'rgba(3, 10, 3, 0.1)';
      ctx.fillRect(0, 0, width, height);

      ctx.fontSize = '16px';
      ctx.font = '16px monospace';

      columns.forEach((col, i) => {
        const char = katakana[Math.floor(Math.random() * katakana.length)];
        const x = i * 18;
        const y = col.y * 18;

        // Primer carácter brillante
        ctx.fillStyle = '#ffffff';
        ctx.fillText(char, x, y);

        // Carácter anterior opaco
        ctx.fillStyle = mainColor;
        ctx.fillText(char, x, y - 18);

        if (y > height && Math.random() > 0.975) {
          col.y = 0;
        }
        col.y += col.speed * 0.5;
      });

      requestAnimationFrame(animate);
    }

    animate();
  </script>
</body>
</html>