@@ -393,47 +393,174 @@ def _check_webapp_access(
393393
394394
395395def _offline_html_response () -> Response :
396- """Return a friendly HTML page when the sandbox is not reachable."""
396+ """Return a branded Craft HTML page when the sandbox is not reachable.
397+
398+ Design mirrors the default Craft web template (outputs/web/app/page.tsx):
399+ terminal window aesthetic with Minecraft-themed typing animation.
400+ """
397401 html = """<!DOCTYPE html>
398402<html lang="en">
399403<head>
400404 <meta charset="UTF-8" />
401405 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
402406 <meta http-equiv="refresh" content="15" />
403- <title>App starting… </title>
407+ <title>Craft — Starting up </title>
404408 <style>
405- * { box-sizing: border-box; margin: 0; padding: 0; }
409+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
410+
406411 body {
407- font-family: -apple-system, BlinkMacSystemFont , "Segoe UI ", sans-serif ;
408- background: #0a0a0a ;
409- color: #e5e5e5 ;
412+ font-family: ui-monospace, SFMono-Regular , "SF Mono ", Menlo, Consolas, monospace ;
413+ background: linear-gradient(to bottom right, #030712, #111827, #030712) ;
414+ min-height: 100vh ;
410415 display: flex;
416+ flex-direction: column;
411417 align-items: center;
412418 justify-content: center;
413- min-height: 100vh ;
419+ gap: 1.5rem ;
414420 padding: 2rem;
415421 }
416- .card {
422+
423+ /* Terminal window */
424+ .terminal {
425+ width: 100%;
426+ max-width: 580px;
427+ border: 2px solid #374151;
428+ border-radius: 2px;
429+ }
430+
431+ /* Title bar */
432+ .titlebar {
433+ background: #1f2937;
434+ padding: 0.5rem 0.75rem;
435+ display: flex;
436+ align-items: center;
437+ gap: 0.5rem;
438+ border-bottom: 1px solid #374151;
439+ }
440+
441+ /* Square traffic-light buttons (not circles) */
442+ .btn { width: 12px; height: 12px; border-radius: 2px; flex-shrink: 0; }
443+ .btn-red { background: #ef4444; }
444+ .btn-yellow { background: #eab308; }
445+ .btn-green { background: #22c55e; }
446+
447+ .title-label {
448+ flex: 1;
417449 text-align: center;
418- max-width: 360px;
450+ font-size: 0.75rem;
451+ color: #6b7280;
452+ margin-right: 36px; /* offset for buttons width so label is visually centred */
453+ }
454+
455+ /* Terminal body */
456+ .body {
457+ background: #111827;
458+ padding: 1.5rem;
459+ min-height: 200px;
460+ font-size: 0.875rem;
461+ color: #d1d5db;
419462 }
420- .icon {
421- font-size: 2.5rem;
422- margin-bottom: 1rem;
423- opacity: 0.6;
463+
464+ /* History lines (completed) */
465+ .history { margin-bottom: 0.25rem; }
466+ .history .prompt { color: #10b981; }
467+ .history .text { color: #6b7280; }
468+
469+ /* Active typing line */
470+ .active {
471+ display: flex;
472+ align-items: center;
473+ gap: 0.375rem;
474+ }
475+ .prompt { color: #10b981; user-select: none; }
476+ #typed { color: #d1d5db; }
477+
478+ /* Block cursor */
479+ .cursor {
480+ display: inline-block;
481+ width: 8px;
482+ height: 1.1em;
483+ background: #10b981;
484+ vertical-align: text-bottom;
485+ animation: blink 1s step-start infinite;
486+ }
487+ @keyframes blink {
488+ 0%, 100% { opacity: 1; }
489+ 50% { opacity: 0; }
490+ }
491+
492+ /* Tagline */
493+ .tagline {
494+ font-size: 0.8125rem;
495+ color: #4b5563;
496+ text-align: center;
424497 }
425- h1 { font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; }
426- p { font-size: 0.875rem; color: #a1a1aa; line-height: 1.5; }
427- .hint { margin-top: 1rem; font-size: 0.75rem; color: #52525b; }
428498 </style>
429499</head>
430500<body>
431- <div class="card">
432- <div class="icon">⚡</div>
433- <h1>App is starting…</h1>
434- <p>The app server is warming up. This page will refresh automatically.</p>
435- <p class="hint">If it stays offline, the owner needs to have their Craft session open.</p>
501+ <div class="terminal">
502+ <div class="titlebar">
503+ <div class="btn btn-red"></div>
504+ <div class="btn btn-yellow"></div>
505+ <div class="btn btn-green"></div>
506+ <span class="title-label">crafting_table</span>
507+ </div>
508+ <div class="body">
509+ <div id="history"></div>
510+ <div class="active">
511+ <span class="prompt">/></span>
512+ <span id="typed"></span><span class="cursor"></span>
513+ </div>
514+ </div>
436515 </div>
516+ <p class="tagline">Ask the owner to open their Craft session to wake it up.</p>
517+
518+ <script>
519+ var messages = [
520+ "Sandbox is asleep...",
521+ "Waiting for owner to wake it up...",
522+ "Ask the owner to open their Craft session.",
523+ "This page will refresh automatically.",
524+ "/status: idle"
525+ ];
526+
527+ var msgIndex = 0;
528+ var charIndex = 0;
529+ var typedEl = document.getElementById("typed");
530+ var historyEl = document.getElementById("history");
531+ var HISTORY_MAX = 3;
532+ var history = [];
533+
534+ function addHistory(text) {
535+ history.push(text);
536+ if (history.length > HISTORY_MAX) history.shift();
537+ historyEl.innerHTML = history.map(function(t) {
538+ return '<div class="history"><span class="prompt">/> </span><span class="text">' +
539+ t.replace(/&/g,"&").replace(/</g,"<") + "</span></div>";
540+ }).join("");
541+ }
542+
543+ function typeChar() {
544+ var msg = messages[msgIndex];
545+ if (charIndex < msg.length) {
546+ typedEl.textContent += msg[charIndex];
547+ charIndex++;
548+ setTimeout(typeChar, 55 + Math.random() * 40);
549+ } else {
550+ setTimeout(nextMessage, 1600);
551+ }
552+ }
553+
554+ function nextMessage() {
555+ addHistory(messages[msgIndex]);
556+ msgIndex = (msgIndex + 1) % messages.length;
557+ charIndex = 0;
558+ typedEl.textContent = "";
559+ setTimeout(typeChar, 300);
560+ }
561+
562+ typeChar();
563+ </script>
437564</body>
438565</html>"""
439566 return Response (content = html , status_code = 503 , media_type = "text/html" )
0 commit comments