diff options
| author | Liam Mitchell <[email protected]> | 2026-03-09 19:06:36 -0700 |
|---|---|---|
| committer | Liam Mitchell <[email protected]> | 2026-03-09 19:06:36 -0700 |
| commit | d1abc50ee9d4fb72efc646e17decafea741caa34 (patch) | |
| tree | e4288e00f2f7ca0391b83d986efcb69d3ba66a83 /src/zenserver/frontend/html/404.html | |
| parent | Allow requests with invalid content-types unless specified in command line or... (diff) | |
| parent | updated chunk–block analyser (#818) (diff) | |
| download | zen-d1abc50ee9d4fb72efc646e17decafea741caa34.tar.xz zen-d1abc50ee9d4fb72efc646e17decafea741caa34.zip | |
Merge branch 'main' into lm/restrict-content-type
Diffstat (limited to 'src/zenserver/frontend/html/404.html')
| -rw-r--r-- | src/zenserver/frontend/html/404.html | 486 |
1 files changed, 486 insertions, 0 deletions
diff --git a/src/zenserver/frontend/html/404.html b/src/zenserver/frontend/html/404.html new file mode 100644 index 000000000..829ef2097 --- /dev/null +++ b/src/zenserver/frontend/html/404.html @@ -0,0 +1,486 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<title>Ooops</title> +<style> + * { margin: 0; padding: 0; box-sizing: border-box; } + + :root { + --deep-space: #00000f; + --nebula-blue: #0a0a2e; + --star-white: #ffffff; + --star-blue: #c8d8ff; + --star-yellow: #fff3c0; + --star-red: #ffd0c0; + --nebula-glow: rgba(60, 80, 180, 0.12); + } + + body { + background: var(--deep-space); + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + font-family: 'Courier New', monospace; + overflow: hidden; + } + + starfield-bg { + display: block; + position: fixed; + inset: 0; + z-index: 0; + } + + canvas { + display: block; + width: 100%; + height: 100%; + } + + .page-content { + position: relative; + z-index: 1; + text-align: center; + color: rgba(200, 216, 255, 0.85); + letter-spacing: 0.25em; + text-transform: uppercase; + pointer-events: none; + user-select: none; + } + + .page-content h1 { + font-size: clamp(1.2rem, 4vw, 2.4rem); + font-weight: 300; + letter-spacing: 0.6em; + text-shadow: 0 0 40px rgba(120, 160, 255, 0.6), 0 0 80px rgba(80, 120, 255, 0.3); + animation: pulse 6s ease-in-out infinite; + } + + .page-content p { + margin-top: 1.2rem; + font-size: clamp(0.55rem, 1.5vw, 0.75rem); + letter-spacing: 0.4em; + opacity: 0.45; + } + + @keyframes pulse { + 0%, 100% { opacity: 0.7; } + 50% { opacity: 1; } + } + + .globe-link { + display: block; + margin: 0 auto 2rem; + width: 160px; + height: 160px; + pointer-events: auto; + cursor: pointer; + border-radius: 50%; + position: relative; + } + + .globe-link:hover .globe-glow { + opacity: 0.6; + } + + .globe-glow { + position: absolute; + inset: -18px; + border-radius: 50%; + background: radial-gradient(circle, rgba(80, 140, 255, 0.35) 0%, transparent 70%); + opacity: 0.35; + transition: opacity 0.4s; + pointer-events: none; + } + + .globe-link canvas { + display: block; + width: 160px; + height: 160px; + border-radius: 50%; + } +</style> +</head> +<body> + +<starfield-bg + star-count="380" + speed="0.6" + depth="true" + nebula="true" + shooting-stars="true" +></starfield-bg> + +<div class="page-content"> + <a class="globe-link" href="/dashboard/" title="Back to Dashboard"> + <div class="globe-glow"></div> + <canvas id="globe" width="320" height="320"></canvas> + </a> + <h1>404 NOT FOUND</h1> +</div> + +<script> +class StarfieldBg extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + } + + connectedCallback() { + this.shadowRoot.innerHTML = ` + <style> + :host { display: block; position: absolute; inset: 0; overflow: hidden; } + canvas { width: 100%; height: 100%; display: block; } + </style> + <canvas></canvas> + `; + + this.canvas = this.shadowRoot.querySelector('canvas'); + this.ctx = this.canvas.getContext('2d'); + + this.starCount = parseInt(this.getAttribute('star-count') || '350'); + this.speed = parseFloat(this.getAttribute('speed') || '0.6'); + this.useDepth = this.getAttribute('depth') !== 'false'; + this.useNebula = this.getAttribute('nebula') !== 'false'; + this.useShooting = this.getAttribute('shooting-stars') !== 'false'; + + this.stars = []; + this.shooters = []; + this.nebulaTime = 0; + this.frame = 0; + + this.resize(); + this.init(); + + this._ro = new ResizeObserver(() => { this.resize(); this.init(); }); + this._ro.observe(this); + + this.raf = requestAnimationFrame(this.tick.bind(this)); + } + + disconnectedCallback() { + cancelAnimationFrame(this.raf); + this._ro.disconnect(); + } + + resize() { + const dpr = window.devicePixelRatio || 1; + const rect = this.getBoundingClientRect(); + this.W = rect.width || window.innerWidth; + this.H = rect.height || window.innerHeight; + this.canvas.width = this.W * dpr; + this.canvas.height = this.H * dpr; + this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + } + + init() { + const COLORS = ['#ffffff', '#c8d8ff', '#d0e8ff', '#fff3c0', '#ffd0c0', '#e0f0ff']; + this.stars = Array.from({ length: this.starCount }, () => ({ + x: Math.random() * this.W, + y: Math.random() * this.H, + z: this.useDepth ? Math.random() : 1, // depth: 0=far, 1=near + r: Math.random() * 1.4 + 0.2, + color: COLORS[Math.floor(Math.random() * COLORS.length)], + twinkleOffset: Math.random() * Math.PI * 2, + twinkleSpeed: 0.008 + Math.random() * 0.012, + })); + } + + spawnShooter() { + const edge = Math.random() < 0.7 ? 'top' : 'left'; + const angle = (Math.random() * 30 + 15) * (Math.PI / 180); + this.shooters.push({ + x: edge === 'top' ? Math.random() * this.W : -10, + y: edge === 'top' ? -10 : Math.random() * this.H * 0.5, + vx: Math.cos(angle) * (6 + Math.random() * 6), + vy: Math.sin(angle) * (6 + Math.random() * 6), + len: 80 + Math.random() * 120, + life: 1, + decay: 0.012 + Math.random() * 0.018, + }); + } + + tick() { + this.raf = requestAnimationFrame(this.tick.bind(this)); + this.frame++; + const ctx = this.ctx; + const W = this.W, H = this.H; + + // Background + ctx.fillStyle = '#00000f'; + ctx.fillRect(0, 0, W, H); + + // Nebula clouds (subtle) + if (this.useNebula) { + this.nebulaTime += 0.003; + this.drawNebula(ctx, W, H); + } + + // Stars + for (const s of this.stars) { + const twinkle = 0.55 + 0.45 * Math.sin(this.frame * s.twinkleSpeed + s.twinkleOffset); + const radius = s.r * (this.useDepth ? (0.3 + s.z * 0.7) : 1); + const alpha = (this.useDepth ? (0.25 + s.z * 0.75) : 1) * twinkle; + + // Tiny drift + s.x += (s.z * this.speed * 0.08) * (this.useDepth ? 1 : 0); + s.y += (s.z * this.speed * 0.04) * (this.useDepth ? 1 : 0); + if (s.x > W + 2) s.x = -2; + if (s.y > H + 2) s.y = -2; + + // Glow for bright stars + if (radius > 1.1 && alpha > 0.6) { + const grd = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, radius * 3.5); + grd.addColorStop(0, s.color.replace(')', `, ${alpha * 0.5})`).replace('rgb', 'rgba')); + grd.addColorStop(1, 'transparent'); + ctx.beginPath(); + ctx.arc(s.x, s.y, radius * 3.5, 0, Math.PI * 2); + ctx.fillStyle = grd; + ctx.fill(); + } + + ctx.beginPath(); + ctx.arc(s.x, s.y, radius, 0, Math.PI * 2); + ctx.fillStyle = hexToRgba(s.color, alpha); + ctx.fill(); + } + + // Shooting stars + if (this.useShooting) { + if (this.frame % 140 === 0 && Math.random() < 0.65) this.spawnShooter(); + for (let i = this.shooters.length - 1; i >= 0; i--) { + const s = this.shooters[i]; + const tailX = s.x - s.vx * (s.len / Math.hypot(s.vx, s.vy)); + const tailY = s.y - s.vy * (s.len / Math.hypot(s.vx, s.vy)); + + const grd = ctx.createLinearGradient(tailX, tailY, s.x, s.y); + grd.addColorStop(0, `rgba(255,255,255,0)`); + grd.addColorStop(0.7, `rgba(200,220,255,${s.life * 0.5})`); + grd.addColorStop(1, `rgba(255,255,255,${s.life})`); + + ctx.beginPath(); + ctx.moveTo(tailX, tailY); + ctx.lineTo(s.x, s.y); + ctx.strokeStyle = grd; + ctx.lineWidth = 1.5 * s.life; + ctx.lineCap = 'round'; + ctx.stroke(); + + // Head dot + ctx.beginPath(); + ctx.arc(s.x, s.y, 1.5 * s.life, 0, Math.PI * 2); + ctx.fillStyle = `rgba(255,255,255,${s.life})`; + ctx.fill(); + + s.x += s.vx; + s.y += s.vy; + s.life -= s.decay; + + if (s.life <= 0 || s.x > W + 200 || s.y > H + 200) { + this.shooters.splice(i, 1); + } + } + } + } + + drawNebula(ctx, W, H) { + const t = this.nebulaTime; + const blobs = [ + { x: W * 0.25, y: H * 0.3, rx: W * 0.35, ry: H * 0.25, color: '40,60,180', a: 0.055 }, + { x: W * 0.75, y: H * 0.65, rx: W * 0.30, ry: H * 0.22, color: '100,40,160', a: 0.04 }, + { x: W * 0.5, y: H * 0.5, rx: W * 0.45, ry: H * 0.35, color: '20,50,120', a: 0.035 }, + ]; + ctx.save(); + for (const b of blobs) { + const ox = Math.sin(t * 0.7 + b.x) * 30; + const oy = Math.cos(t * 0.5 + b.y) * 20; + const grd = ctx.createRadialGradient(b.x + ox, b.y + oy, 0, b.x + ox, b.y + oy, Math.max(b.rx, b.ry)); + grd.addColorStop(0, `rgba(${b.color}, ${b.a})`); + grd.addColorStop(0.5, `rgba(${b.color}, ${b.a * 0.4})`); + grd.addColorStop(1, `rgba(${b.color}, 0)`); + ctx.save(); + ctx.scale(b.rx / Math.max(b.rx, b.ry), b.ry / Math.max(b.rx, b.ry)); + ctx.beginPath(); + const scale = Math.max(b.rx, b.ry); + ctx.arc((b.x + ox) / (b.rx / scale), (b.y + oy) / (b.ry / scale), scale, 0, Math.PI * 2); + ctx.fillStyle = grd; + ctx.fill(); + ctx.restore(); + } + ctx.restore(); + } +} + +function hexToRgba(hex, alpha) { + // Handle named-ish values or full hex + const c = hex.startsWith('#') ? hex : '#ffffff'; + const r = parseInt(c.slice(1,3), 16); + const g = parseInt(c.slice(3,5), 16); + const b = parseInt(c.slice(5,7), 16); + return `rgba(${r},${g},${b},${alpha.toFixed(3)})`; +} + +customElements.define('starfield-bg', StarfieldBg); +</script> + +<script> +(function() { + const canvas = document.getElementById('globe'); + const ctx = canvas.getContext('2d'); + const W = canvas.width, H = canvas.height; + const R = W * 0.44; + const cx = W / 2, cy = H / 2; + + // Simplified continent outlines as lon/lat polygon chains (degrees). + // Each continent is an array of [lon, lat] points. + const continents = [ + // North America + [[-130,50],[-125,55],[-120,60],[-115,65],[-100,68],[-85,70],[-75,65],[-60,52],[-65,45],[-70,42],[-75,35],[-80,30],[-85,28],[-90,28],[-95,25],[-100,20],[-105,20],[-110,25],[-115,30],[-120,35],[-125,42],[-130,50]], + // South America + [[-80,10],[-75,5],[-70,5],[-65,0],[-60,-5],[-55,-5],[-50,-10],[-45,-15],[-40,-20],[-40,-25],[-42,-30],[-48,-32],[-52,-34],[-55,-38],[-60,-42],[-65,-50],[-68,-55],[-70,-48],[-72,-40],[-75,-30],[-78,-15],[-80,-5],[-80,5],[-80,10]], + // Europe + [[-10,36],[-5,38],[0,40],[2,43],[5,44],[8,46],[10,48],[15,50],[18,54],[20,56],[25,58],[28,60],[30,62],[35,65],[40,68],[38,60],[35,55],[30,50],[28,48],[25,45],[22,40],[20,38],[15,36],[10,36],[5,36],[0,36],[-5,36],[-10,36]], + // Africa + [[-15,14],[-17,16],[-15,22],[-12,28],[-5,32],[0,35],[5,37],[10,35],[15,32],[20,30],[25,30],[30,28],[35,25],[38,18],[40,12],[42,5],[44,0],[42,-5],[40,-12],[38,-18],[35,-25],[32,-30],[30,-34],[25,-33],[20,-30],[15,-28],[12,-20],[10,-10],[8,-5],[5,0],[2,5],[0,5],[-5,5],[-10,6],[-15,10],[-15,14]], + // Asia (simplified) + [[30,35],[35,38],[40,40],[45,42],[50,45],[55,48],[60,50],[65,55],[70,60],[75,65],[80,68],[90,70],[100,68],[110,65],[120,60],[125,55],[130,50],[135,45],[140,40],[138,35],[130,30],[120,25],[110,20],[105,15],[100,10],[95,12],[90,20],[85,22],[80,25],[75,28],[70,30],[65,35],[55,35],[45,35],[40,35],[35,35],[30,35]], + // Australia + [[115,-12],[120,-14],[125,-15],[130,-14],[135,-13],[138,-16],[140,-18],[145,-20],[148,-22],[150,-25],[152,-28],[150,-33],[148,-35],[145,-37],[140,-38],[135,-36],[130,-33],[125,-30],[120,-25],[118,-22],[116,-20],[114,-18],[115,-15],[115,-12]], + ]; + + function project(lon, lat, rotation) { + // Convert to radians and apply rotation + var lonR = (lon + rotation) * Math.PI / 180; + var latR = lat * Math.PI / 180; + + var x3 = Math.cos(latR) * Math.sin(lonR); + var y3 = -Math.sin(latR); + var z3 = Math.cos(latR) * Math.cos(lonR); + + // Only visible if facing us + if (z3 < 0) return null; + + return { x: cx + x3 * R, y: cy + y3 * R, z: z3 }; + } + + var rotation = 0; + + function draw() { + requestAnimationFrame(draw); + rotation += 0.15; + ctx.clearRect(0, 0, W, H); + + // Atmosphere glow + var atm = ctx.createRadialGradient(cx, cy, R * 0.85, cx, cy, R * 1.15); + atm.addColorStop(0, 'rgba(60,130,255,0.12)'); + atm.addColorStop(0.5, 'rgba(60,130,255,0.06)'); + atm.addColorStop(1, 'rgba(60,130,255,0)'); + ctx.beginPath(); + ctx.arc(cx, cy, R * 1.15, 0, Math.PI * 2); + ctx.fillStyle = atm; + ctx.fill(); + + // Ocean sphere + var oceanGrad = ctx.createRadialGradient(cx - R * 0.3, cy - R * 0.3, R * 0.1, cx, cy, R); + oceanGrad.addColorStop(0, '#1a4a8a'); + oceanGrad.addColorStop(0.5, '#0e2d5e'); + oceanGrad.addColorStop(1, '#071838'); + ctx.beginPath(); + ctx.arc(cx, cy, R, 0, Math.PI * 2); + ctx.fillStyle = oceanGrad; + ctx.fill(); + + // Draw continents + for (var c = 0; c < continents.length; c++) { + var pts = continents[c]; + var projected = []; + var allVisible = true; + + for (var i = 0; i < pts.length; i++) { + var p = project(pts[i][0], pts[i][1], rotation); + if (!p) { allVisible = false; break; } + projected.push(p); + } + + if (!allVisible || projected.length < 3) continue; + + ctx.beginPath(); + ctx.moveTo(projected[0].x, projected[0].y); + for (var i = 1; i < projected.length; i++) { + ctx.lineTo(projected[i].x, projected[i].y); + } + ctx.closePath(); + + // Shade based on average depth + var avgZ = 0; + for (var i = 0; i < projected.length; i++) avgZ += projected[i].z; + avgZ /= projected.length; + var brightness = 0.3 + avgZ * 0.7; + + var r = Math.round(30 * brightness); + var g = Math.round(100 * brightness); + var b = Math.round(50 * brightness); + ctx.fillStyle = 'rgb(' + r + ',' + g + ',' + b + ')'; + ctx.fill(); + } + + // Grid lines (longitude) + ctx.strokeStyle = 'rgba(100,160,255,0.08)'; + ctx.lineWidth = 0.7; + for (var lon = -180; lon < 180; lon += 30) { + ctx.beginPath(); + var started = false; + for (var lat = -90; lat <= 90; lat += 3) { + var p = project(lon, lat, rotation); + if (p) { + if (!started) { ctx.moveTo(p.x, p.y); started = true; } + else ctx.lineTo(p.x, p.y); + } else { + started = false; + } + } + ctx.stroke(); + } + + // Grid lines (latitude) + for (var lat = -60; lat <= 60; lat += 30) { + ctx.beginPath(); + var started = false; + for (var lon = -180; lon <= 180; lon += 3) { + var p = project(lon, lat, rotation); + if (p) { + if (!started) { ctx.moveTo(p.x, p.y); started = true; } + else ctx.lineTo(p.x, p.y); + } else { + started = false; + } + } + ctx.stroke(); + } + + // Specular highlight + var spec = ctx.createRadialGradient(cx - R * 0.35, cy - R * 0.35, 0, cx - R * 0.35, cy - R * 0.35, R * 0.8); + spec.addColorStop(0, 'rgba(180,210,255,0.18)'); + spec.addColorStop(0.4, 'rgba(120,160,255,0.05)'); + spec.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.beginPath(); + ctx.arc(cx, cy, R, 0, Math.PI * 2); + ctx.fillStyle = spec; + ctx.fill(); + + // Rim light + ctx.beginPath(); + ctx.arc(cx, cy, R, 0, Math.PI * 2); + ctx.strokeStyle = 'rgba(80,140,255,0.2)'; + ctx.lineWidth = 1.5; + ctx.stroke(); + } + + draw(); +})(); +</script> +</body> +</html> |