From 4129686b0723f86798ad8dcadd283c823a7f328b Mon Sep 17 00:00:00 2001 From: Tanner Date: Wed, 4 Nov 2026 16:50:40 +0000 Subject: [PATCH] Overhaul the web UI --- .gitignore | 1 + README.md | 8 + mosfet.py | 90 +- web_interface/.gitignore | 1 + web_interface/build.js | 22 + web_interface/build/asset-manifest.json | 17 - web_interface/build/index.html | 108 +- .../build/static/js/2.2b306052.chunk.js | 3 - .../static/js/2.2b306052.chunk.js.LICENSE.txt | 41 - .../build/static/js/2.2b306052.chunk.js.map | 1 - .../build/static/js/main.27336089.chunk.js | 2 - .../static/js/main.27336089.chunk.js.map | 1 - .../build/static/js/runtime-main.97b64705.js | 2 - .../static/js/runtime-main.97b64705.js.map | 1 - web_interface/package.json | 38 +- web_interface/public/index.html | 123 +- web_interface/src/App.js | 53 - web_interface/src/dashboard.css | 147 + web_interface/src/dashboard.js | 207 + web_interface/src/index.html | 107 + web_interface/src/index.js | 10 - web_interface/yarn.lock | 11418 ---------------- 22 files changed, 787 insertions(+), 11614 deletions(-) create mode 100644 web_interface/build.js delete mode 100644 web_interface/build/asset-manifest.json delete mode 100644 web_interface/build/static/js/2.2b306052.chunk.js delete mode 100644 web_interface/build/static/js/2.2b306052.chunk.js.LICENSE.txt delete mode 100644 web_interface/build/static/js/2.2b306052.chunk.js.map delete mode 100644 web_interface/build/static/js/main.27336089.chunk.js delete mode 100644 web_interface/build/static/js/main.27336089.chunk.js.map delete mode 100644 web_interface/build/static/js/runtime-main.97b64705.js delete mode 100644 web_interface/build/static/js/runtime-main.97b64705.js.map delete mode 100644 web_interface/src/App.js create mode 100644 web_interface/src/dashboard.css create mode 100644 web_interface/src/dashboard.js create mode 100644 web_interface/src/index.html delete mode 100644 web_interface/src/index.js delete mode 100644 web_interface/yarn.lock diff --git a/.gitignore b/.gitignore index 9ae1158..4839229 100644 --- a/.gitignore +++ b/.gitignore @@ -103,3 +103,4 @@ ENV/ minecraft_data/ mcdata/ +build/ diff --git a/README.md b/README.md index 0a9f33b..046a593 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,14 @@ $ SERVER=127.0.0.1 python mosfet.py `mosfet.py` defaults to that server, player name `mosfet1`, and the current AuthMe password. Override `SERVER`, `PORT`, `PLAYER_NAME`, or `GAME_PASSWORD` through environment variables as needed. Microsoft authentication is not used. +The dashboard is served at `/` (normally `http://localhost:3300`) and can be +rebuilt with `npm --prefix web_interface run build`. It uses bundled Minecraft +item icons where available, then fetches and caches missing textures from the +26.3 asset CDN. If no texture is available, it displays a two-letter +placeholder. + +Inventory item icons are from https://mcitemgallery.com/ + Python edits under `mosfet/` are hot-reloaded in place; the player connection is kept alive. Edits to connection/bootstrap files are reported as deferred until the next manual start, rather than triggering an automatic reconnect. diff --git a/mosfet.py b/mosfet.py index 9f85dc2..e200fe6 100644 --- a/mosfet.py +++ b/mosfet.py @@ -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/') +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') diff --git a/web_interface/.gitignore b/web_interface/.gitignore index 8996736..13e62fe 100644 --- a/web_interface/.gitignore +++ b/web_interface/.gitignore @@ -10,6 +10,7 @@ # misc .DS_Store +/item_cache/ .env.local .env.development.local .env.test.local diff --git a/web_interface/build.js b/web_interface/build.js new file mode 100644 index 0000000..968383d --- /dev/null +++ b/web_interface/build.js @@ -0,0 +1,22 @@ +const fs = require('fs'); +const path = require('path'); + +const root = __dirname; +const source = path.join(root, 'src'); +const build = path.join(root, 'build'); + +fs.rmSync(build, {recursive: true, force: true}); +fs.mkdirSync(path.join(build, 'static', 'css'), {recursive: true}); +fs.mkdirSync(path.join(build, 'static', 'js'), {recursive: true}); +fs.copyFileSync(path.join(source, 'index.html'), path.join(build, 'index.html')); +fs.copyFileSync(path.join(source, 'dashboard.css'), + path.join(build, 'static', 'css', 'dashboard.css')); +fs.copyFileSync(path.join(source, 'dashboard.js'), + path.join(build, 'static', 'js', 'dashboard.js')); +const bundledIcons = path.join(root, 'item-icons'); +fs.cpSync(bundledIcons, path.join(build, 'static', 'item-icons'), + {recursive: true}); +// Keep the standalone public entry synchronized for tools that serve it directly. +fs.copyFileSync(path.join(source, 'index.html'), + path.join(root, 'public', 'index.html')); +console.log('Built static Mosfet dashboard into web_interface/build'); diff --git a/web_interface/build/asset-manifest.json b/web_interface/build/asset-manifest.json deleted file mode 100644 index 5533ef8..0000000 --- a/web_interface/build/asset-manifest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "files": { - "main.js": "/static/js/main.27336089.chunk.js", - "main.js.map": "/static/js/main.27336089.chunk.js.map", - "runtime-main.js": "/static/js/runtime-main.97b64705.js", - "runtime-main.js.map": "/static/js/runtime-main.97b64705.js.map", - "static/js/2.2b306052.chunk.js": "/static/js/2.2b306052.chunk.js", - "static/js/2.2b306052.chunk.js.map": "/static/js/2.2b306052.chunk.js.map", - "index.html": "/index.html", - "static/js/2.2b306052.chunk.js.LICENSE.txt": "/static/js/2.2b306052.chunk.js.LICENSE.txt" - }, - "entrypoints": [ - "static/js/runtime-main.97b64705.js", - "static/js/2.2b306052.chunk.js", - "static/js/main.27336089.chunk.js" - ] -} \ No newline at end of file diff --git a/web_interface/build/index.html b/web_interface/build/index.html index 2155cd8..3e31d07 100644 --- a/web_interface/build/index.html +++ b/web_interface/build/index.html @@ -1 +1,107 @@ -React App
\ No newline at end of file + + + + + + + + Mosfet · Minecraft Bot + + + + +
+
+
+
+
LIVE SESSION
+

Mosfet.

+

Waiting for the bot connection

+
+
+
CURRENT POSITION
+
—
+
NO DIMENSION
+
+ + +
+ +
+
+
HEALTH♥
+
—/ 20
+
+
+
+
HUNGER◆
+
—/ 20
+
+
+
+
MOVEMENT
+
IDLE
+
No active path
+
+
+
WORLD CACHE▦
+
— chunks
+
Waiting for world data
+
+
+ +
+
+
+
+
PLAYER STORAGE
+

Inventory

+
+
+ 0 items + + 0 / 46 slots +
+
+ +
+
Empty slot Hover an item for its name and stack size
+
+ + +
+ +
+ ■ MOSFET / 26.3 + OFFLINE + Waiting for API data +
+
+
+
+ + diff --git a/web_interface/build/static/js/2.2b306052.chunk.js b/web_interface/build/static/js/2.2b306052.chunk.js deleted file mode 100644 index 0c44ebc..0000000 --- a/web_interface/build/static/js/2.2b306052.chunk.js +++ /dev/null @@ -1,3 +0,0 @@ -/*! For license information please see 2.2b306052.chunk.js.LICENSE.txt */ -(this.webpackJsonpweb_interface=this.webpackJsonpweb_interface||[]).push([[2],[function(e,t,n){"use strict";e.exports=n(40)},function(e,t,n){"use strict";var r=n(6),a=Object.prototype.toString;function o(e){return"[object Array]"===a.call(e)}function l(e){return"undefined"===typeof e}function i(e){return null!==e&&"object"===typeof e}function u(e){if("[object Object]"!==a.call(e))return!1;var t=Object.getPrototypeOf(e);return null===t||t===Object.prototype}function s(e){return"[object Function]"===a.call(e)}function c(e,t){if(null!==e&&"undefined"!==typeof e)if("object"!==typeof e&&(e=[e]),o(e))for(var n=0,r=e.length;n=200&&e<300},headers:{common:{Accept:"application/json, text/plain, */*"}}};r.forEach(["delete","get","head"],(function(e){i.headers[e]={}})),r.forEach(["post","put","patch"],(function(e){i.headers[e]=r.merge(o)})),e.exports=i}).call(this,n(27))},function(e,t,n){"use strict";var r=n(1),a=n(29),o=n(31),l=n(7),i=n(32),u=n(35),s=n(36),c=n(11);e.exports=function(e){return new Promise((function(t,n){var f=e.data,d=e.headers;r.isFormData(f)&&delete d["Content-Type"];var p=new XMLHttpRequest;if(e.auth){var h=e.auth.username||"",m=e.auth.password?unescape(encodeURIComponent(e.auth.password)):"";d.Authorization="Basic "+btoa(h+":"+m)}var v=i(e.baseURL,e.url);if(p.open(e.method.toUpperCase(),l(v,e.params,e.paramsSerializer),!0),p.timeout=e.timeout,p.onreadystatechange=function(){if(p&&4===p.readyState&&(0!==p.status||p.responseURL&&0===p.responseURL.indexOf("file:"))){var r="getAllResponseHeaders"in p?u(p.getAllResponseHeaders()):null,o={data:e.responseType&&"text"!==e.responseType?p.response:p.responseText,status:p.status,statusText:p.statusText,headers:r,config:e,request:p};a(t,n,o),p=null}},p.onabort=function(){p&&(n(c("Request aborted",e,"ECONNABORTED",p)),p=null)},p.onerror=function(){n(c("Network Error",e,null,p)),p=null},p.ontimeout=function(){var t="timeout of "+e.timeout+"ms exceeded";e.timeoutErrorMessage&&(t=e.timeoutErrorMessage),n(c(t,e,"ECONNABORTED",p)),p=null},r.isStandardBrowserEnv()){var y=(e.withCredentials||s(v))&&e.xsrfCookieName?o.read(e.xsrfCookieName):void 0;y&&(d[e.xsrfHeaderName]=y)}if("setRequestHeader"in p&&r.forEach(d,(function(e,t){"undefined"===typeof f&&"content-type"===t.toLowerCase()?delete d[t]:p.setRequestHeader(t,e)})),r.isUndefined(e.withCredentials)||(p.withCredentials=!!e.withCredentials),e.responseType)try{p.responseType=e.responseType}catch(g){if("json"!==e.responseType)throw g}"function"===typeof e.onDownloadProgress&&p.addEventListener("progress",e.onDownloadProgress),"function"===typeof e.onUploadProgress&&p.upload&&p.upload.addEventListener("progress",e.onUploadProgress),e.cancelToken&&e.cancelToken.promise.then((function(e){p&&(p.abort(),n(e),p=null)})),f||(f=null),p.send(f)}))}},function(e,t,n){"use strict";var r=n(30);e.exports=function(e,t,n,a,o){var l=new Error(e);return r(l,t,n,a,o)}},function(e,t,n){"use strict";var r=n(1);e.exports=function(e,t){t=t||{};var n={},a=["url","method","data"],o=["headers","auth","proxy","params"],l=["baseURL","transformRequest","transformResponse","paramsSerializer","timeout","timeoutMessage","withCredentials","adapter","responseType","xsrfCookieName","xsrfHeaderName","onUploadProgress","onDownloadProgress","decompress","maxContentLength","maxBodyLength","maxRedirects","transport","httpAgent","httpsAgent","cancelToken","socketPath","responseEncoding"],i=["validateStatus"];function u(e,t){return r.isPlainObject(e)&&r.isPlainObject(t)?r.merge(e,t):r.isPlainObject(t)?r.merge({},t):r.isArray(t)?t.slice():t}function s(a){r.isUndefined(t[a])?r.isUndefined(e[a])||(n[a]=u(void 0,e[a])):n[a]=u(e[a],t[a])}r.forEach(a,(function(e){r.isUndefined(t[e])||(n[e]=u(void 0,t[e]))})),r.forEach(o,s),r.forEach(l,(function(a){r.isUndefined(t[a])?r.isUndefined(e[a])||(n[a]=u(void 0,e[a])):n[a]=u(void 0,t[a])})),r.forEach(i,(function(r){r in t?n[r]=u(e[r],t[r]):r in e&&(n[r]=u(void 0,e[r]))}));var c=a.concat(o).concat(l).concat(i),f=Object.keys(e).concat(Object.keys(t)).filter((function(e){return-1===c.indexOf(e)}));return r.forEach(f,s),n}},function(e,t,n){"use strict";function r(e){this.message=e}r.prototype.toString=function(){return"Cancel"+(this.message?": "+this.message:"")},r.prototype.__CANCEL__=!0,e.exports=r},function(e,t,n){"use strict";!function e(){if("undefined"!==typeof __REACT_DEVTOOLS_GLOBAL_HOOK__&&"function"===typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE)try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(t){console.error(t)}}(),e.exports=n(18)},function(e,t,n){"use strict";function r(e,t,n,r,a,o,l){try{var i=e[o](l),u=i.value}catch(s){return void n(s)}i.done?t(u):Promise.resolve(u).then(r,a)}function a(e){return function(){var t=this,n=arguments;return new Promise((function(a,o){var l=e.apply(t,n);function i(e){r(l,a,o,i,u,"next",e)}function u(e){r(l,a,o,i,u,"throw",e)}i(void 0)}))}}n.d(t,"a",(function(){return a}))},function(e,t,n){"use strict";function r(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n