Compare commits
5 Commits
feat/aw-po
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ccfa25388 | |||
| 5acce0f015 | |||
| 64d711e256 | |||
|
|
d9b7b232ad | ||
| cbcad273ef |
55
404.html
Normal file
55
404.html
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Lost in the Tower</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #0a0a0f;
|
||||||
|
color: #e0d8c8;
|
||||||
|
font-family: Georgia, 'Times New Roman', serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: normal;
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #8a7f6a;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #6a6050;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #8a7f6a;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0.8rem 2rem;
|
||||||
|
border: 1px solid #2a2520;
|
||||||
|
transition: border-color 0.3s, color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
border-color: #8a7f6a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Lost in the Tower</h1>
|
||||||
|
<p>This room doesn't exist.</p>
|
||||||
|
<a href="/">Return to the Entry Hall</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
10
api/health/index.json
Normal file
10
api/health/index.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"services": {
|
||||||
|
"api": true,
|
||||||
|
"agent_loop": false,
|
||||||
|
"websocket": false
|
||||||
|
},
|
||||||
|
"uptime": null,
|
||||||
|
"version": "20260322.230710"
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<link href="https://alexanderwhitestone.com/blog/"/>
|
<link href="https://alexanderwhitestone.com/blog/"/>
|
||||||
<link rel="self" href="https://alexanderwhitestone.com/blog/feed.xml"/>
|
<link rel="self" href="https://alexanderwhitestone.com/blog/feed.xml"/>
|
||||||
<id>https://alexanderwhitestone.com/blog/</id>
|
<id>https://alexanderwhitestone.com/blog/</id>
|
||||||
<updated>2026-03-19T01:42:22Z</updated>
|
<updated>2026-03-19T01:45:33Z</updated>
|
||||||
<author>
|
<author>
|
||||||
<name>Alexander Whitestone</name>
|
<name>Alexander Whitestone</name>
|
||||||
</author>
|
</author>
|
||||||
|
|||||||
115
scripts/aw-post
115
scripts/aw-post
@@ -2,51 +2,104 @@
|
|||||||
# aw-post — Quick-post a scroll from the command line
|
# aw-post — Quick-post a scroll from the command line
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# aw-post "Title of the Post"
|
# aw-post "Title" # Create empty post, open for editing
|
||||||
# aw-post "Title" < body.md
|
# aw-post "Title" < body.md # Pipe body from stdin
|
||||||
# echo "Post body here" | aw-post "Title"
|
# echo "Post body" | aw-post "Title" # Pipe body from stdin
|
||||||
|
# aw-post --file thoughts.md # Post from a file (title from first line)
|
||||||
|
# aw-post --from-x "Tweet text here" # Port an X/Twitter post
|
||||||
#
|
#
|
||||||
# Creates a new markdown file in blog/posts/ with frontmatter,
|
# Creates a markdown file in blog/posts/, rebuilds the blog.
|
||||||
# then rebuilds the blog index, post pages, and RSS feed.
|
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
BLOG_DIR="${REPO_DIR}/blog/posts"
|
BLOG_DIR="${REPO_DIR}/blog/posts"
|
||||||
|
|
||||||
if [ $# -lt 1 ]; then
|
|
||||||
echo "Usage: aw-post \"Title of the Post\""
|
|
||||||
echo " Pipe or redirect body content via stdin."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
TITLE="$1"
|
|
||||||
DATE=$(date +%Y-%m-%d)
|
DATE=$(date +%Y-%m-%d)
|
||||||
SLUG=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd 'a-z0-9-')
|
|
||||||
FILENAME="${DATE}-${SLUG}.md"
|
|
||||||
FILEPATH="${BLOG_DIR}/${FILENAME}"
|
|
||||||
|
|
||||||
if [ -f "$FILEPATH" ]; then
|
slugify() {
|
||||||
echo "Error: $FILEPATH already exists"
|
echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd 'a-z0-9-' | head -c 60
|
||||||
exit 1
|
}
|
||||||
fi
|
|
||||||
|
|
||||||
# Read body from stdin if available
|
create_post() {
|
||||||
BODY=""
|
local title="$1"
|
||||||
if [ ! -t 0 ]; then
|
local body="$2"
|
||||||
BODY=$(cat)
|
local slug
|
||||||
fi
|
slug=$(slugify "$title")
|
||||||
|
local filename="${DATE}-${slug}.md"
|
||||||
|
local filepath="${BLOG_DIR}/${filename}"
|
||||||
|
|
||||||
cat > "$FILEPATH" << EOF
|
if [ -f "$filepath" ]; then
|
||||||
|
echo "Error: $filepath already exists"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$filepath" << EOF
|
||||||
---
|
---
|
||||||
title: "${TITLE}"
|
title: "${title}"
|
||||||
date: ${DATE}
|
date: ${DATE}
|
||||||
---
|
---
|
||||||
|
|
||||||
${BODY}
|
${body}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "Created: ${FILEPATH}"
|
echo "Created: ${filepath}"
|
||||||
|
python3 "${REPO_DIR}/scripts/build.py"
|
||||||
|
}
|
||||||
|
|
||||||
# Rebuild the blog
|
# Parse arguments
|
||||||
python3 "${REPO_DIR}/scripts/build.py"
|
case "${1:-}" in
|
||||||
|
--file)
|
||||||
|
# Post from a file — title from first non-empty line
|
||||||
|
if [ -z "${2:-}" ] || [ ! -f "${2:-}" ]; then
|
||||||
|
echo "Usage: aw-post --file <path-to-markdown>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
BODY=$(cat "$2")
|
||||||
|
# Extract title: first line starting with # or first non-empty line
|
||||||
|
TITLE=$(echo "$BODY" | grep -m1 '^#' | sed 's/^#* *//' || echo "$BODY" | grep -m1 '.' || echo "Untitled")
|
||||||
|
if [ -z "$TITLE" ]; then
|
||||||
|
TITLE="Untitled"
|
||||||
|
fi
|
||||||
|
# Strip the title line from body if it was a markdown header
|
||||||
|
BODY=$(echo "$BODY" | sed '1{/^#/d;}')
|
||||||
|
create_post "$TITLE" "$BODY"
|
||||||
|
;;
|
||||||
|
|
||||||
|
--from-x)
|
||||||
|
# Port an X/Twitter post — the text becomes both title and body
|
||||||
|
if [ -z "${2:-}" ]; then
|
||||||
|
echo "Usage: aw-post --from-x \"Tweet text here\""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
TEXT="$2"
|
||||||
|
# Title: first 60 chars, cleaned up
|
||||||
|
TITLE=$(echo "$TEXT" | head -1 | cut -c1-60 | sed 's/[[:space:]]*$//')
|
||||||
|
create_post "$TITLE" "$TEXT"
|
||||||
|
;;
|
||||||
|
|
||||||
|
--help|-h)
|
||||||
|
echo "aw-post — Quick-post a scroll to The Scrolls"
|
||||||
|
echo ""
|
||||||
|
echo "Usage:"
|
||||||
|
echo " aw-post \"Title\" Create a post (body from stdin)"
|
||||||
|
echo " aw-post --file thoughts.md Post from a markdown file"
|
||||||
|
echo " aw-post --from-x \"Tweet text\" Port an X/Twitter post"
|
||||||
|
echo " aw-post --help Show this help"
|
||||||
|
;;
|
||||||
|
|
||||||
|
"")
|
||||||
|
echo "Usage: aw-post \"Title of the Post\""
|
||||||
|
echo " See: aw-post --help"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
# Standard mode: title as arg, body from stdin
|
||||||
|
TITLE="$1"
|
||||||
|
BODY=""
|
||||||
|
if [ ! -t 0 ]; then
|
||||||
|
BODY=$(cat)
|
||||||
|
fi
|
||||||
|
create_post "$TITLE" "$BODY"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|||||||
@@ -17,14 +17,17 @@ Generates:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import html
|
import html
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
SITE_URL = "https://alexanderwhitestone.com"
|
SITE_URL = "https://alexanderwhitestone.com"
|
||||||
BLOG_DIR = Path(__file__).parent.parent / "blog"
|
ROOT_DIR = Path(__file__).parent.parent
|
||||||
|
BLOG_DIR = ROOT_DIR / "blog"
|
||||||
POSTS_DIR = BLOG_DIR / "posts"
|
POSTS_DIR = BLOG_DIR / "posts"
|
||||||
|
HEALTH_DIR = ROOT_DIR / "api" / "health"
|
||||||
|
|
||||||
PAGE_STYLE = """\
|
PAGE_STYLE = """\
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
@@ -291,6 +294,23 @@ def generate_feed(posts):
|
|||||||
print(f" Generated feed with {len(entries)} entry/entries.")
|
print(f" Generated feed with {len(entries)} entry/entries.")
|
||||||
|
|
||||||
|
|
||||||
|
def generate_health():
|
||||||
|
"""Generate api/health/index.json with build-time metadata."""
|
||||||
|
HEALTH_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
health = {
|
||||||
|
"status": "ok",
|
||||||
|
"services": {
|
||||||
|
"api": True,
|
||||||
|
"agent_loop": False,
|
||||||
|
"websocket": False,
|
||||||
|
},
|
||||||
|
"uptime": None,
|
||||||
|
"version": datetime.now(timezone.utc).strftime("%Y%m%d.%H%M%S"),
|
||||||
|
}
|
||||||
|
(HEALTH_DIR / "index.json").write_text(json.dumps(health, indent=2) + "\n")
|
||||||
|
print(" Generated api/health endpoint.")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print("Building The Scrolls...")
|
print("Building The Scrolls...")
|
||||||
posts = load_posts()
|
posts = load_posts()
|
||||||
@@ -299,6 +319,7 @@ def main():
|
|||||||
print(f" Built: {out.relative_to(BLOG_DIR.parent)}")
|
print(f" Built: {out.relative_to(BLOG_DIR.parent)}")
|
||||||
generate_index(posts)
|
generate_index(posts)
|
||||||
generate_feed(posts)
|
generate_feed(posts)
|
||||||
|
generate_health()
|
||||||
print("Build complete.")
|
print("Build complete.")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,57 @@
|
|||||||
color: #6a6050;
|
color: #6a6050;
|
||||||
}
|
}
|
||||||
.placeholder h1 { font-size: 1.2rem; font-weight: normal; margin-bottom: 1rem; }
|
.placeholder h1 { font-size: 1.2rem; font-weight: normal; margin-bottom: 1rem; }
|
||||||
.placeholder p { font-size: 0.85rem; }
|
.placeholder p { font-size: 0.85rem; margin-bottom: 0.5rem; }
|
||||||
.placeholder a { color: #8a7f6a; }
|
.placeholder a { color: #8a7f6a; }
|
||||||
|
|
||||||
|
/* Connection status HUD */
|
||||||
|
#status-hud {
|
||||||
|
position: fixed;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
background: rgba(10, 10, 15, 0.85);
|
||||||
|
border: 1px solid #2a2520;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6a6050;
|
||||||
|
z-index: 100;
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
#status-hud .status-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
#status-hud .status-line:last-child { margin-bottom: 0; }
|
||||||
|
#status-hud .dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
#status-hud .dot.connecting { background: #b8860b; animation: pulse 1.2s ease-in-out infinite; }
|
||||||
|
#status-hud .dot.online { background: #4a9; }
|
||||||
|
#status-hud .dot.offline { background: #a44; }
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.3; }
|
||||||
|
}
|
||||||
|
#retry-btn {
|
||||||
|
display: none;
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
background: #2a2520;
|
||||||
|
border: 1px solid #4a4030;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #8a7f6a;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#retry-btn:hover { background: #3a3530; color: #c0b8a8; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -31,7 +80,45 @@
|
|||||||
<p><a href="/">← Back to the Tower</a></p>
|
<p><a href="/">← Back to the Tower</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="status-hud">
|
||||||
|
<div class="status-line">
|
||||||
|
<span class="dot connecting" id="status-dot"></span>
|
||||||
|
<span id="status-text">INITIALIZING</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-line">AGENTS: <span id="agent-count">0</span></div>
|
||||||
|
<button id="retry-btn">Retry connection</button>
|
||||||
|
<a id="fallback-link" href="/api/ui" style="display:none; margin-top:6px; color:#8a7f6a; font-size:0.7rem;">Open API dashboard →</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reject unknown sub-paths: only /world/ is valid -->
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var path = window.location.pathname.replace(/\/+$/, '') || '/';
|
||||||
|
if (path !== '/world') {
|
||||||
|
window.location.replace('/404.html');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<!-- Fallback: if main.js fails to load, don't leave user on INITIALIZING forever -->
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var INIT_TIMEOUT_MS = 8000;
|
||||||
|
window.__workshopBooted = false;
|
||||||
|
setTimeout(function() {
|
||||||
|
if (window.__workshopBooted) return;
|
||||||
|
var dot = document.getElementById('status-dot');
|
||||||
|
var text = document.getElementById('status-text');
|
||||||
|
var btn = document.getElementById('retry-btn');
|
||||||
|
var fallback = document.getElementById('fallback-link');
|
||||||
|
if (dot) { dot.className = 'dot offline'; }
|
||||||
|
if (text) { text.textContent = 'WORKSHOP UNREACHABLE'; }
|
||||||
|
if (btn) { btn.style.display = 'block'; }
|
||||||
|
if (fallback) { fallback.style.display = 'block'; }
|
||||||
|
}, INIT_TIMEOUT_MS);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
<!-- Three.js scene will mount to #scene -->
|
<!-- Three.js scene will mount to #scene -->
|
||||||
<!-- <script type="module" src="main.js"></script> -->
|
<script type="module" src="main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
130
world/main.js
130
world/main.js
@@ -1,20 +1,130 @@
|
|||||||
/**
|
/**
|
||||||
* The Workshop — Three.js scene bootstrap
|
* The Workshop — Three.js scene bootstrap
|
||||||
*
|
*
|
||||||
* This file will initialize the 3D world where Timmy lives.
|
* Initializes the 3D world where Timmy lives.
|
||||||
* Currently a placeholder until tech decisions are made:
|
* Handles WebSocket connection to tower-hermes backend with
|
||||||
* - 3D engine confirmed (Three.js vs Babylon.js)
|
* timeout, retry, and clear status display.
|
||||||
* - Character design direction chosen
|
|
||||||
* - WebSocket bridge to Timmy's soul designed (#243)
|
|
||||||
*
|
*
|
||||||
* See: #242 (3D world), #243 (WebSocket bridge), #265 (presence schema)
|
* See: #242 (3D world), #243 (WebSocket bridge), #265 (presence schema)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Future: import * as THREE from 'three';
|
// Future: import * as THREE from 'three';
|
||||||
|
|
||||||
export function initWorkshop(container) {
|
const HERMES_WS_URL = (location.protocol === 'https:' ? 'wss://' : 'ws://') +
|
||||||
// TODO: Initialize 3D scene
|
location.host + '/ws/tower';
|
||||||
// TODO: Load wizard character model
|
const CONNECT_TIMEOUT_MS = 5000;
|
||||||
// TODO: Connect to Timmy presence WebSocket
|
const RETRY_DELAY_MS = 3000;
|
||||||
console.log('[Workshop] Scene container ready:', container.id);
|
const MAX_AUTO_RETRIES = 3;
|
||||||
|
|
||||||
|
const Status = { CONNECTING: 'connecting', ONLINE: 'online', OFFLINE: 'offline' };
|
||||||
|
|
||||||
|
const dom = {
|
||||||
|
dot: document.getElementById('status-dot'),
|
||||||
|
text: document.getElementById('status-text'),
|
||||||
|
agents: document.getElementById('agent-count'),
|
||||||
|
retryBtn: document.getElementById('retry-btn'),
|
||||||
|
fallback: document.getElementById('fallback-link'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Signal to inline fallback script that main.js loaded successfully
|
||||||
|
window.__workshopBooted = true;
|
||||||
|
|
||||||
|
let ws = null;
|
||||||
|
let autoRetries = 0;
|
||||||
|
let connectTimer = null;
|
||||||
|
|
||||||
|
function setStatus(state, message) {
|
||||||
|
dom.dot.className = 'dot ' + state;
|
||||||
|
dom.text.textContent = message;
|
||||||
|
var isOffline = state === Status.OFFLINE;
|
||||||
|
dom.retryBtn.style.display = isOffline ? 'block' : 'none';
|
||||||
|
if (dom.fallback) dom.fallback.style.display = isOffline ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setAgentCount(n) {
|
||||||
|
dom.agents.textContent = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
clearTimeout(connectTimer);
|
||||||
|
if (ws) {
|
||||||
|
ws.onopen = null;
|
||||||
|
ws.onclose = null;
|
||||||
|
ws.onerror = null;
|
||||||
|
ws.onmessage = null;
|
||||||
|
if (ws.readyState <= WebSocket.OPEN) ws.close();
|
||||||
|
ws = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
cleanup();
|
||||||
|
setStatus(Status.CONNECTING, 'CONNECTING\u2026');
|
||||||
|
setAgentCount(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ws = new WebSocket(HERMES_WS_URL);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Workshop] WebSocket creation failed:', err);
|
||||||
|
onFail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectTimer = setTimeout(function () {
|
||||||
|
console.warn('[Workshop] Connection timeout after ' + CONNECT_TIMEOUT_MS + 'ms');
|
||||||
|
cleanup();
|
||||||
|
onFail();
|
||||||
|
}, CONNECT_TIMEOUT_MS);
|
||||||
|
|
||||||
|
ws.onopen = function () {
|
||||||
|
clearTimeout(connectTimer);
|
||||||
|
autoRetries = 0;
|
||||||
|
setStatus(Status.ONLINE, 'ONLINE');
|
||||||
|
console.log('[Workshop] Connected to tower-hermes');
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = function (evt) {
|
||||||
|
try {
|
||||||
|
var msg = JSON.parse(evt.data);
|
||||||
|
if (typeof msg.agents === 'number') setAgentCount(msg.agents);
|
||||||
|
} catch (_) {
|
||||||
|
// non-JSON messages are ignored
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = function () {
|
||||||
|
clearTimeout(connectTimer);
|
||||||
|
console.log('[Workshop] Connection closed');
|
||||||
|
onFail();
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = function () {
|
||||||
|
clearTimeout(connectTimer);
|
||||||
|
console.error('[Workshop] WebSocket error');
|
||||||
|
// onclose will fire after this, which calls onFail
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFail() {
|
||||||
|
if (autoRetries < MAX_AUTO_RETRIES) {
|
||||||
|
autoRetries++;
|
||||||
|
setStatus(Status.CONNECTING, 'RETRYING (' + autoRetries + '/' + MAX_AUTO_RETRIES + ')\u2026');
|
||||||
|
setTimeout(connect, RETRY_DELAY_MS);
|
||||||
|
} else {
|
||||||
|
setStatus(Status.OFFLINE, 'WORKSHOP OFFLINE');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual retry resets the counter
|
||||||
|
dom.retryBtn.addEventListener('click', function () {
|
||||||
|
autoRetries = 0;
|
||||||
|
connect();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Boot
|
||||||
|
export function initWorkshop(container) {
|
||||||
|
console.log('[Workshop] Scene container ready:', container.id);
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
initWorkshop(document.getElementById('scene'));
|
||||||
|
|||||||
Reference in New Issue
Block a user