Skip to content

Commit ce69c91

Browse files
rohoswaggerclaude
andcommitted
feat(craft): terminal UI for offline sandbox page
Replace the generic offline page with a terminal UI matching the default Craft web template (page.tsx). Uses the crafting_table title bar with square traffic-light buttons, emerald /> prompt, dark neutral gradient background, and a typing animation with messages indicating the sandbox is asleep and the owner needs to wake it up. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 10f731c commit ce69c91

File tree

1 file changed

+148
-21
lines changed
  • backend/onyx/server/features/build/api

1 file changed

+148
-21
lines changed

backend/onyx/server/features/build/api/api.py

Lines changed: 148 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -393,47 +393,174 @@ def _check_webapp_access(
393393

394394

395395
def _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">/&gt;</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">/&gt; </span><span class="text">' +
539+
t.replace(/&/g,"&amp;").replace(/</g,"&lt;") + "</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

Comments
 (0)