Overhaul the web UI

This commit is contained in:
Tanner
2026-11-04 16:50:40 +00:00
parent 67c0ed5af4
commit 4129686b07
22 changed files with 787 additions and 11614 deletions
+86 -4
View File
@@ -1,6 +1,9 @@
import importlib
import json
import queue
import re
import urllib.error
import urllib.request
import threading
import time
import traceback
@@ -11,7 +14,7 @@ import logging
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
from flask import Flask
from flask import Flask, abort, send_file
from flask_cors import CORS
build_folder = 'web_interface/build'
app = Flask(__name__, template_folder=build_folder, static_folder=build_folder, static_url_path='')
@@ -49,6 +52,9 @@ g.health = 20
g.food = 20
g.sand_origin = None
g.reload_lock = threading.RLock()
_item_icon_cache = Path(__file__).resolve().parent / 'web_interface' / 'item_cache'
_item_icon_locks = {}
_item_icon_locks_guard = threading.Lock()
def _api_json_default(value):
"""Serialize live protocol objects without invoking fragile __repr__ code."""
@@ -60,6 +66,13 @@ def _api_json_default(value):
return {'__type__': type(value).__name__,
'packet_name': getattr(value, 'packet_name', None),
'fields': fields}
if (type(value).__name__ == 'Slot' and
all(hasattr(value, key) for key in
('present', 'item_id', 'item_count'))):
return {'present': value.present, 'item_id': value.item_id,
'item_count': value.item_count}
if all(hasattr(value, key) for key in ('x', 'y', 'z')):
return {'x': value.x, 'y': value.y, 'z': value.z}
if isinstance(value, (bytes, bytearray)):
return value.hex()
try:
@@ -70,15 +83,84 @@ def _api_json_default(value):
@app.route('/api/global')
def api_global():
data = json.dumps(g, default=_api_json_default, indent=4)
snapshot = dict(g)
job = g.get('job')
job_state = getattr(job, 'state', None) if job else None
snapshot['job_state'] = getattr(job_state, '__name__', 'idle')
chunks = g.get('chunks')
if chunks is not None and hasattr(chunks, 'chunks'):
snapshot['chunk_state'] = {
'loaded': len(chunks.chunks),
'center': chunks.center,
'radius': chunks.radius,
'dimension': chunks.dimension,
}
response = app.response_class(
response=data,
response=json.dumps(snapshot, default=_api_json_default),
status=200,
mimetype='application/json'
)
return response
@app.route('/api/items')
def api_items():
from mosfet.info.items import ITEM_NAMES
data = {str(item_id): name for item_id, name in ITEM_NAMES.items()}
return app.response_class(
response=json.dumps(data), status=200, mimetype='application/json')
def _item_texture_urls(name):
root = 'https://assets.mcasset.cloud/26.3/assets/minecraft/textures'
yield root + '/item/{}.png'.format(name)
yield root + '/block/{}.png'.format(name)
simplified = re.sub(
r'_(slab|stairs|wall|fence_gate|fence|door|trapdoor|button|'
r'pressure_plate|sign|hanging_sign|carpet|banner|bed|candle)$',
'', name)
if simplified != name:
yield root + '/block/{}.png'.format(simplified)
yield root + '/item/{}.png'.format(simplified)
def _fetch_item_texture(name):
for url in _item_texture_urls(name):
request = urllib.request.Request(
url, headers={'User-Agent': 'curl/8.0 Mosfet/26.3'})
try:
with urllib.request.urlopen(request, timeout=4) as response:
image = response.read(2 * 1024 * 1024 + 1)
if len(image) <= 2 * 1024 * 1024 and image.startswith(b'\x89PNG\r\n\x1a\n'):
return image
except (urllib.error.URLError, TimeoutError, OSError):
continue
return None
@app.route('/api/item-icon/<name>')
def api_item_icon(name):
from mosfet.info.items import ITEM_NAMES
if not re.fullmatch(r'[a-z0-9_]+', name):
abort(404)
if name not in set(ITEM_NAMES.values()):
abort(404)
with _item_icon_locks_guard:
lock = _item_icon_locks.setdefault(name, threading.Lock())
_item_icon_cache.mkdir(parents=True, exist_ok=True)
cached = _item_icon_cache / (name + '.png')
with lock:
if not cached.is_file():
image = _fetch_item_texture(name)
if image is None:
abort(404)
temporary = cached.with_suffix('.tmp')
temporary.write_bytes(image)
temporary.replace(cached)
return send_file(cached, mimetype='image/png', max_age=86400)
@app.route('/')
def root():
return app.send_static_file('index.html')