Overhaul the web UI
This commit is contained in:
@@ -103,3 +103,4 @@ ENV/
|
||||
|
||||
minecraft_data/
|
||||
mcdata/
|
||||
build/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
/item_cache/
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
|
||||
@@ -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');
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -1 +1,107 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><title>React App</title></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function r(r){for(var n,f,i=r[0],l=r[1],a=r[2],p=0,s=[];p<i.length;p++)f=i[p],Object.prototype.hasOwnProperty.call(o,f)&&o[f]&&s.push(o[f][0]),o[f]=0;for(n in l)Object.prototype.hasOwnProperty.call(l,n)&&(e[n]=l[n]);for(c&&c(r);s.length;)s.shift()();return u.push.apply(u,a||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++){var l=t[i];0!==o[l]&&(n=!1)}n&&(u.splice(r--,1),e=f(f.s=t[0]))}return e}var n={},o={1:0},u=[];function f(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,f),t.l=!0,t.exports}f.m=e,f.c=n,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(e,r){if(1&r&&(e=f(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)f.d(t,n,function(r){return e[r]}.bind(null,n));return t},f.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return f.d(r,"a",r),r},f.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},f.p="/";var i=this.webpackJsonpweb_interface=this.webpackJsonpweb_interface||[],l=i.push.bind(i);i.push=r,i=i.slice();for(var a=0;a<i.length;a++)r(i[a]);var c=l;t()}([])</script><script src="/static/js/2.2b306052.chunk.js"></script><script src="/static/js/main.27336089.chunk.js"></script></body></html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#10130f" />
|
||||
<meta name="description" content="Live Minecraft bot dashboard" />
|
||||
<title>Mosfet · Minecraft Bot</title>
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css?v=4" />
|
||||
<script defer src="/static/js/dashboard.js?v=7"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<main>
|
||||
<section class="hero panel">
|
||||
<div class="hero-main">
|
||||
<div class="eyebrow">LIVE SESSION <span class="eyebrow-line"></span></div>
|
||||
<h1 id="bot-name">Mosfet<span class="name-period">.</span></h1>
|
||||
<p id="session-subtitle" class="session-subtitle">Waiting for the bot connection</p>
|
||||
</div>
|
||||
<div class="hero-coordinates">
|
||||
<div class="coordinate-label">CURRENT POSITION</div>
|
||||
<div id="position" class="coordinate-value">—</div>
|
||||
<div id="dimension" class="dimension-value">NO DIMENSION</div>
|
||||
</div>
|
||||
<div class="hero-grid" aria-hidden="true"></div>
|
||||
<div class="hero-glow" aria-hidden="true"></div>
|
||||
</section>
|
||||
|
||||
<section class="stats-grid" aria-label="Bot status">
|
||||
<article class="stat-card panel">
|
||||
<div class="stat-top"><span class="stat-label">HEALTH</span><span class="stat-icon heart">♥</span></div>
|
||||
<div class="stat-value"><span id="health-value">—</span><small>/ 20</small></div>
|
||||
<div class="meter"><span id="health-meter" class="meter-fill health-fill"></span></div>
|
||||
</article>
|
||||
<article class="stat-card panel">
|
||||
<div class="stat-top"><span class="stat-label">HUNGER</span><span class="stat-icon food">◆</span></div>
|
||||
<div class="stat-value"><span id="food-value">—</span><small>/ 20</small></div>
|
||||
<div class="meter"><span id="food-meter" class="meter-fill food-fill"></span></div>
|
||||
</article>
|
||||
<article class="stat-card panel">
|
||||
<div class="stat-top"><span class="stat-label">MOVEMENT</span><span class="live-dot"></span></div>
|
||||
<div id="movement-value" class="stat-value stat-word">IDLE</div>
|
||||
<div id="movement-detail" class="stat-caption">No active path</div>
|
||||
</article>
|
||||
<article class="stat-card panel">
|
||||
<div class="stat-top"><span class="stat-label">WORLD CACHE</span><span class="stat-icon cache-icon">▦</span></div>
|
||||
<div class="stat-value"><span id="chunk-count">—</span><small> chunks</small></div>
|
||||
<div id="chunk-detail" class="stat-caption">Waiting for world data</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="content-grid">
|
||||
<section class="inventory-panel panel">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<div class="eyebrow">PLAYER STORAGE</div>
|
||||
<h2>Inventory</h2>
|
||||
</div>
|
||||
<div class="inventory-summary">
|
||||
<span id="inventory-count">0 items</span>
|
||||
<span class="heading-divider"></span>
|
||||
<span id="inventory-slots">0 / 46 slots</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="inventory-warning" class="inventory-warning" hidden>
|
||||
Some item components are not decoded yet; displayed contents may be partial.
|
||||
</div>
|
||||
<div id="inventory-grid" class="inventory-grid" aria-label="Minecraft inventory"></div>
|
||||
<div class="inventory-legend"><span class="legend-swatch"></span> Empty slot <span class="legend-note">Hover an item for its name and stack size</span></div>
|
||||
</section>
|
||||
|
||||
<aside class="side-column">
|
||||
<section class="side-panel panel">
|
||||
<div class="section-heading compact-heading">
|
||||
<div><div class="eyebrow">MULTIPLAYER</div><h2>Players</h2></div>
|
||||
<span id="player-count" class="count-badge">0</span>
|
||||
</div>
|
||||
<div id="player-list" class="player-list"><div class="empty-state">No player data yet</div></div>
|
||||
</section>
|
||||
|
||||
<section class="side-panel panel">
|
||||
<div class="section-heading compact-heading">
|
||||
<div><div class="eyebrow">AUTOMATION</div><h2>Runtime</h2></div>
|
||||
</div>
|
||||
<dl class="runtime-list">
|
||||
<div><dt>Game mode</dt><dd id="game-mode">—</dd></div>
|
||||
<div><dt>Active job</dt><dd id="job-state">IDLE</dd></div>
|
||||
<div><dt>Path nodes</dt><dd id="path-length">0</dd></div>
|
||||
<div><dt>Entities</dt><dd id="entity-count">0 mobs · 0 drops</dd></div>
|
||||
<div><dt>AFK</dt><dd id="afk-state">NO</dd></div>
|
||||
</dl>
|
||||
<div id="movement-error" class="error-box" hidden></div>
|
||||
</section>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<footer class="footer">
|
||||
<span class="footer-brand"><span class="footer-mark">■</span> MOSFET / 26.3</span>
|
||||
<span id="connection-pill" class="connection-pill is-offline"><i></i><span>OFFLINE</span></span>
|
||||
<span id="last-update">Waiting for API data</span>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
object-assign
|
||||
(c) Sindre Sorhus
|
||||
@license MIT
|
||||
*/
|
||||
|
||||
/** @license React v0.20.2
|
||||
* scheduler.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v17.0.2
|
||||
* react-dom.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v17.0.2
|
||||
* react-jsx-runtime.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v17.0.2
|
||||
* react.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
||||
(this.webpackJsonpweb_interface=this.webpackJsonpweb_interface||[]).push([[0],{41:function(e,t,n){"use strict";n.r(t);var c=n(2),r=n.n(c),s=n(14),a=n.n(s),i=n(4),j=n.n(i),l=n(15),d=n(16),o=n(5),p=n.n(o),b=n(0);p.a.defaults.baseURL=Object({NODE_ENV:"production",PUBLIC_URL:"",WDS_SOCKET_HOST:void 0,WDS_SOCKET_PATH:void 0,WDS_SOCKET_PORT:void 0,FAST_REFRESH:!0}).REACT_APP_SERVER;var u=function(){var e=Object(c.useState)(!1),t=Object(d.a)(e,2),n=t[0],r=t[1];return Object(c.useEffect)((function(){var e=setInterval(function(){var e=Object(l.a)(j.a.mark((function e(){var t;return j.a.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,p.a.get("/api/global");case 3:t=e.sent,r(t.data),e.next=10;break;case 7:e.prev=7,e.t0=e.catch(0),r(!1);case 10:case"end":return e.stop()}}),e,null,[[0,7]])})));return function(){return e.apply(this,arguments)}}(),500);return function(){return clearInterval(e)}}),[]),Object(b.jsx)("div",{className:"app",children:n&&Object(b.jsxs)(b.Fragment,{children:[Object(b.jsxs)("p",{children:["Name: ",n.name]}),Object(b.jsxs)("p",{children:["Pos: ",n.pos]}),Object(b.jsxs)("p",{children:["Yaw: ",n.yaw]}),Object(b.jsxs)("p",{children:["Pitch: ",n.pitch]}),Object(b.jsxs)("p",{children:["Dimention: ",n.dimension]}),Object(b.jsxs)("p",{children:["Players:",Object.values(n.players).map((function(e){return Object(b.jsx)("div",{children:n.player_names[e.player_uuid]})}))]}),Object(b.jsxs)("p",{children:["Holding: ",n.holding]}),Object(b.jsxs)("p",{children:["AFK: ","".concat(n.afk)]}),Object(b.jsxs)("p",{children:["Health: ",n.health]}),Object(b.jsxs)("p",{children:["Food: ",n.food]}),Object(b.jsxs)("p",{children:["Time: ",n.time]})]})})};a.a.render(Object(b.jsx)(r.a.StrictMode,{children:Object(b.jsx)(u,{})}),document.getElementById("root"))}},[[41,1,2]]]);
|
||||
//# sourceMappingURL=main.27336089.chunk.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"sources":["App.js","index.js"],"names":["axios","defaults","baseURL","process","REACT_APP_SERVER","App","useState","global","setGlobal","useEffect","interval","setInterval","a","get","res","data","clearInterval","className","name","pos","yaw","pitch","dimension","Object","values","players","map","x","player_names","player_uuid","holding","afk","health","food","time","ReactDOM","render","StrictMode","document","getElementById"],"mappings":"kNAGAA,IAAMC,SAASC,QAAUC,mIAAYC,iBAiDtBC,MA/Cf,WAAgB,IAAD,EACcC,oBAAS,GADvB,mBACPC,EADO,KACCC,EADD,KAiBd,OAdAC,qBAAU,WACT,IASMC,EAAWC,YATR,uCAAG,4BAAAC,EAAA,+EAEQZ,IAAMa,IAAI,eAFlB,OAEJC,EAFI,OAGVN,EAAUM,EAAIC,MAHJ,gDAKVP,GAAU,GALA,yDAAH,qDASyB,KAClC,OAAO,kBAAMQ,cAAcN,MACzB,IAGF,qBAAKO,UAAU,MAAf,SACEV,GACA,qCACC,uCAAUA,EAAOW,QACjB,sCAASX,EAAOY,OAChB,sCAASZ,EAAOa,OAChB,wCAAWb,EAAOc,SAElB,4CAAed,EAAOe,aAEtB,yCACEC,OAAOC,OAAOjB,EAAOkB,SAASC,KAAI,SAAAC,GAAC,OACnC,8BACEpB,EAAOqB,aAAaD,EAAEE,qBAK1B,0CAAatB,EAAOuB,WACpB,gDAAYvB,EAAOwB,QACnB,yCAAYxB,EAAOyB,UACnB,uCAAUzB,EAAO0B,QACjB,uCAAU1B,EAAO2B,cCzCtBC,IAASC,OACR,cAAC,IAAMC,WAAP,UACC,cAAC,EAAD,MAEDC,SAASC,eAAe,W","file":"static/js/main.27336089.chunk.js","sourcesContent":["import React, { useState, useEffect } from 'react';\nimport axios from 'axios';\n\naxios.defaults.baseURL = process.env.REACT_APP_SERVER;\n\nfunction App() {\n\tconst [global, setGlobal] = useState(false);\n\n\tuseEffect(() => {\n\t\tconst get = async() => {\n\t\t\ttry {\n\t\t\t\tconst res = await axios.get('/api/global');\n\t\t\t\tsetGlobal(res.data);\n\t\t\t} catch (error) {\n\t\t\t\tsetGlobal(false);\n\t\t\t}\n\t\t};\n\n\t\tconst interval = setInterval(get, 500);\n\t\treturn () => clearInterval(interval);\n\t}, []);\n\n\treturn (\n\t\t<div className=\"app\">\n\t\t\t{global &&\n\t\t\t\t<>\n\t\t\t\t\t<p>Name: {global.name}</p>\n\t\t\t\t\t<p>Pos: {global.pos}</p>\n\t\t\t\t\t<p>Yaw: {global.yaw}</p>\n\t\t\t\t\t<p>Pitch: {global.pitch}</p>\n\n\t\t\t\t\t<p>Dimention: {global.dimension}</p>\n\n\t\t\t\t\t<p>Players:\n\t\t\t\t\t\t{Object.values(global.players).map(x =>\n\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t{global.player_names[x.player_uuid]}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</p>\n\n\t\t\t\t\t<p>Holding: {global.holding}</p>\n\t\t\t\t\t<p>AFK: {`${global.afk}`}</p>\n\t\t\t\t\t<p>Health: {global.health}</p>\n\t\t\t\t\t<p>Food: {global.food}</p>\n\t\t\t\t\t<p>Time: {global.time}</p>\n\t\t\t\t</>\n\t\t\t}\n\t\t</div>\n\t);\n}\n\nexport default App;\n","import React from 'react';\nimport ReactDOM from 'react-dom';\nimport App from './App';\n\nReactDOM.render(\n\t<React.StrictMode>\n\t\t<App />\n\t</React.StrictMode>,\n\tdocument.getElementById('root')\n);\n"],"sourceRoot":""}
|
||||
@@ -1,2 +0,0 @@
|
||||
!function(e){function r(r){for(var n,f,i=r[0],l=r[1],a=r[2],p=0,s=[];p<i.length;p++)f=i[p],Object.prototype.hasOwnProperty.call(o,f)&&o[f]&&s.push(o[f][0]),o[f]=0;for(n in l)Object.prototype.hasOwnProperty.call(l,n)&&(e[n]=l[n]);for(c&&c(r);s.length;)s.shift()();return u.push.apply(u,a||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++){var l=t[i];0!==o[l]&&(n=!1)}n&&(u.splice(r--,1),e=f(f.s=t[0]))}return e}var n={},o={1:0},u=[];function f(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,f),t.l=!0,t.exports}f.m=e,f.c=n,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(e,r){if(1&r&&(e=f(e)),8&r)return e;if(4&r&&"object"===typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)f.d(t,n,function(r){return e[r]}.bind(null,n));return t},f.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return f.d(r,"a",r),r},f.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},f.p="/";var i=this.webpackJsonpweb_interface=this.webpackJsonpweb_interface||[],l=i.push.bind(i);i.push=r,i=i.slice();for(var a=0;a<i.length;a++)r(i[a]);var c=l;t()}([]);
|
||||
//# sourceMappingURL=runtime-main.97b64705.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -1,39 +1,9 @@
|
||||
{
|
||||
"name": "web_interface",
|
||||
"version": "0.1.0",
|
||||
"name": "mosfet-dashboard",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"axios": "^0.21.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "4.0.3",
|
||||
"web-vitals": "^1.0.1"
|
||||
},
|
||||
"description": "Static dashboard served by the Mosfet Flask API",
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
"build": "node build.js"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,107 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<meta name="theme-color" content="#10130f" />
|
||||
<meta name="description" content="Live Minecraft bot dashboard" />
|
||||
<title>Mosfet · Minecraft Bot</title>
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css?v=4" />
|
||||
<script defer src="/static/js/dashboard.js?v=7"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
<div class="shell">
|
||||
<main>
|
||||
<section class="hero panel">
|
||||
<div class="hero-main">
|
||||
<div class="eyebrow">LIVE SESSION <span class="eyebrow-line"></span></div>
|
||||
<h1 id="bot-name">Mosfet<span class="name-period">.</span></h1>
|
||||
<p id="session-subtitle" class="session-subtitle">Waiting for the bot connection</p>
|
||||
</div>
|
||||
<div class="hero-coordinates">
|
||||
<div class="coordinate-label">CURRENT POSITION</div>
|
||||
<div id="position" class="coordinate-value">—</div>
|
||||
<div id="dimension" class="dimension-value">NO DIMENSION</div>
|
||||
</div>
|
||||
<div class="hero-grid" aria-hidden="true"></div>
|
||||
<div class="hero-glow" aria-hidden="true"></div>
|
||||
</section>
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
<section class="stats-grid" aria-label="Bot status">
|
||||
<article class="stat-card panel">
|
||||
<div class="stat-top"><span class="stat-label">HEALTH</span><span class="stat-icon heart">♥</span></div>
|
||||
<div class="stat-value"><span id="health-value">—</span><small>/ 20</small></div>
|
||||
<div class="meter"><span id="health-meter" class="meter-fill health-fill"></span></div>
|
||||
</article>
|
||||
<article class="stat-card panel">
|
||||
<div class="stat-top"><span class="stat-label">HUNGER</span><span class="stat-icon food">◆</span></div>
|
||||
<div class="stat-value"><span id="food-value">—</span><small>/ 20</small></div>
|
||||
<div class="meter"><span id="food-meter" class="meter-fill food-fill"></span></div>
|
||||
</article>
|
||||
<article class="stat-card panel">
|
||||
<div class="stat-top"><span class="stat-label">MOVEMENT</span><span class="live-dot"></span></div>
|
||||
<div id="movement-value" class="stat-value stat-word">IDLE</div>
|
||||
<div id="movement-detail" class="stat-caption">No active path</div>
|
||||
</article>
|
||||
<article class="stat-card panel">
|
||||
<div class="stat-top"><span class="stat-label">WORLD CACHE</span><span class="stat-icon cache-icon">▦</span></div>
|
||||
<div class="stat-value"><span id="chunk-count">—</span><small> chunks</small></div>
|
||||
<div id="chunk-detail" class="stat-caption">Waiting for world data</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
<section class="content-grid">
|
||||
<section class="inventory-panel panel">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<div class="eyebrow">PLAYER STORAGE</div>
|
||||
<h2>Inventory</h2>
|
||||
</div>
|
||||
<div class="inventory-summary">
|
||||
<span id="inventory-count">0 items</span>
|
||||
<span class="heading-divider"></span>
|
||||
<span id="inventory-slots">0 / 46 slots</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="inventory-warning" class="inventory-warning" hidden>
|
||||
Some item components are not decoded yet; displayed contents may be partial.
|
||||
</div>
|
||||
<div id="inventory-grid" class="inventory-grid" aria-label="Minecraft inventory"></div>
|
||||
<div class="inventory-legend"><span class="legend-swatch"></span> Empty slot <span class="legend-note">Hover an item for its name and stack size</span></div>
|
||||
</section>
|
||||
|
||||
<aside class="side-column">
|
||||
<section class="side-panel panel">
|
||||
<div class="section-heading compact-heading">
|
||||
<div><div class="eyebrow">MULTIPLAYER</div><h2>Players</h2></div>
|
||||
<span id="player-count" class="count-badge">0</span>
|
||||
</div>
|
||||
<div id="player-list" class="player-list"><div class="empty-state">No player data yet</div></div>
|
||||
</section>
|
||||
|
||||
<section class="side-panel panel">
|
||||
<div class="section-heading compact-heading">
|
||||
<div><div class="eyebrow">AUTOMATION</div><h2>Runtime</h2></div>
|
||||
</div>
|
||||
<dl class="runtime-list">
|
||||
<div><dt>Game mode</dt><dd id="game-mode">—</dd></div>
|
||||
<div><dt>Active job</dt><dd id="job-state">IDLE</dd></div>
|
||||
<div><dt>Path nodes</dt><dd id="path-length">0</dd></div>
|
||||
<div><dt>Entities</dt><dd id="entity-count">0 mobs · 0 drops</dd></div>
|
||||
<div><dt>AFK</dt><dd id="afk-state">NO</dd></div>
|
||||
</dl>
|
||||
<div id="movement-error" class="error-box" hidden></div>
|
||||
</section>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<footer class="footer">
|
||||
<span class="footer-brand"><span class="footer-mark">■</span> MOSFET / 26.3</span>
|
||||
<span id="connection-pill" class="connection-pill is-offline"><i></i><span>OFFLINE</span></span>
|
||||
<span id="last-update">Waiting for API data</span>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
|
||||
axios.defaults.baseURL = process.env.REACT_APP_SERVER;
|
||||
|
||||
function App() {
|
||||
const [global, setGlobal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const get = async() => {
|
||||
try {
|
||||
const res = await axios.get('/api/global');
|
||||
setGlobal(res.data);
|
||||
} catch (error) {
|
||||
setGlobal(false);
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(get, 500);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
{global &&
|
||||
<>
|
||||
<p>Name: {global.name}</p>
|
||||
<p>Pos: {global.pos}</p>
|
||||
<p>Yaw: {global.yaw}</p>
|
||||
<p>Pitch: {global.pitch}</p>
|
||||
|
||||
<p>Dimention: {global.dimension}</p>
|
||||
|
||||
<p>Players:
|
||||
{Object.values(global.players).map(x =>
|
||||
<div>
|
||||
{global.player_names[x.player_uuid]}
|
||||
</div>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<p>Holding: {global.holding}</p>
|
||||
<p>AFK: {`${global.afk}`}</p>
|
||||
<p>Health: {global.health}</p>
|
||||
<p>Food: {global.food}</p>
|
||||
<p>Time: {global.time}</p>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,147 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #101210;
|
||||
--panel: #1a1d1a;
|
||||
--panel-2: #212521;
|
||||
--slot: #292e29;
|
||||
--slot-empty: #181b18;
|
||||
--border: #434943;
|
||||
--border-soft: #303630;
|
||||
--text: #f1f3ef;
|
||||
--muted: #b4bcb3;
|
||||
--subtle: #8b938a;
|
||||
--accent: #9bd76d;
|
||||
--red: #ff7777;
|
||||
--amber: #f0c36c;
|
||||
--sans: Arial, Helvetica, sans-serif;
|
||||
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-family: var(--sans);
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html { min-width: 320px; background: var(--bg); }
|
||||
body { margin: 0; min-height: 100vh; background: var(--bg); color: var(--text); }
|
||||
.shell { width: min(1320px, calc(100% - 32px)); margin: 0 auto; }
|
||||
|
||||
.topbar {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.brand { display: inline-flex; align-items: center; gap: 9px; color: var(--text); text-decoration: none; }
|
||||
.brand-mark { display: block; width: 19px; height: 19px; position: relative; background: var(--accent); }
|
||||
.brand-mark i { display: none; }
|
||||
.brand strong { display: block; font-size: 15px; letter-spacing: .08em; line-height: 1; }
|
||||
.brand small { display: block; margin-top: 4px; color: var(--muted); font: 10px var(--mono); letter-spacing: .08em; }
|
||||
.topbar-right { display: flex; align-items: center; gap: 9px; }
|
||||
.version-tag, .connection-pill { padding: 5px 8px; border: 1px solid var(--border); border-radius: 3px; font: 11px var(--mono); }
|
||||
.version-tag { color: var(--muted); }
|
||||
.version-tag b { color: var(--text); font-weight: 600; margin-left: 3px; }
|
||||
.connection-pill { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); }
|
||||
.connection-pill i, .live-dot { display: block; width: 7px; height: 7px; border-radius: 50%; background: var(--subtle); }
|
||||
.connection-pill.is-online { color: var(--accent); border-color: #68894e; }
|
||||
.connection-pill.is-online i, .live-dot { background: var(--accent); }
|
||||
.connection-pill.is-offline { color: var(--muted); }
|
||||
|
||||
main { padding: 18px 0 30px; }
|
||||
.panel { background: var(--panel); border: 1px solid var(--border); border-radius: 4px; }
|
||||
.hero { min-height: 118px; display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 18px 22px; }
|
||||
.hero-main, .hero-coordinates { min-width: 0; }
|
||||
.eyebrow { color: var(--muted); font: 10px var(--mono); letter-spacing: .08em; text-transform: uppercase; }
|
||||
.eyebrow-line, .hero-grid, .hero-glow { display: none; }
|
||||
h1 { margin: 5px 0 3px; font-size: 34px; line-height: 1.05; letter-spacing: -.025em; font-weight: 600; }
|
||||
.session-subtitle { margin: 0; color: var(--muted); font-size: 13px; }
|
||||
.hero-coordinates { min-width: 220px; padding-left: 20px; border-left: 1px solid var(--border); }
|
||||
.coordinate-label { color: var(--muted); font: 10px var(--mono); letter-spacing: .06em; }
|
||||
.coordinate-value { margin-top: 5px; font: 20px var(--mono); letter-spacing: -.04em; color: var(--text); }
|
||||
.dimension-value { margin-top: 4px; color: var(--accent); font: 10px var(--mono); letter-spacing: .08em; }
|
||||
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; margin-top: 8px; }
|
||||
.stat-card { min-height: 92px; padding: 11px 13px; }
|
||||
.stat-top { display: flex; justify-content: space-between; align-items: center; }
|
||||
.stat-label { color: var(--muted); font: 10px var(--mono); letter-spacing: .06em; }
|
||||
.stat-icon { font-size: 12px; color: var(--muted); }
|
||||
.heart { color: var(--red); }
|
||||
.food { color: var(--amber); }
|
||||
.cache-icon { color: var(--accent); }
|
||||
.stat-value { margin-top: 8px; font: 24px var(--mono); letter-spacing: -.03em; }
|
||||
.stat-value small { margin-left: 4px; color: var(--muted); font-size: 10px; letter-spacing: 0; }
|
||||
.stat-word { font-size: 15px; letter-spacing: .01em; }
|
||||
.stat-caption { margin-top: 4px; color: var(--muted); font-size: 12px; }
|
||||
.meter { height: 4px; margin-top: 9px; border-radius: 1px; overflow: hidden; background: #383d38; }
|
||||
.meter-fill { display: block; width: 0; height: 100%; transition: width .2s ease; }
|
||||
.health-fill { background: var(--red); }
|
||||
.food-fill { background: var(--amber); }
|
||||
|
||||
.content-grid { display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(275px, .8fr); gap: 8px; margin-top: 8px; align-items: start; }
|
||||
.inventory-panel { padding: 15px; }
|
||||
.section-heading { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 11px; }
|
||||
h2 { margin: 3px 0 0; font-size: 20px; letter-spacing: -.01em; font-weight: 600; }
|
||||
.inventory-summary { display: flex; gap: 8px; align-items: center; color: var(--muted); font: 11px var(--mono); }
|
||||
.heading-divider { width: 1px; height: 12px; background: var(--border); }
|
||||
.inventory-grid { display: grid; grid-template-columns: repeat(9, minmax(0, 1fr)); gap: 4px; padding: 8px; background: #111411; border: 1px solid var(--border-soft); border-radius: 3px; }
|
||||
.inventory-slot { aspect-ratio: 1; min-width: 0; position: relative; display: grid; place-items: center; border: 1px solid #3d433d; border-radius: 2px; background: var(--slot-empty); }
|
||||
.inventory-slot.occupied { border-color: #596259; background: var(--slot); }
|
||||
.inventory-slot.hotbar-slot { border-bottom: 2px solid #66834e; }
|
||||
.slot-number { position: absolute; top: 2px; left: 3px; z-index: 1; color: #c3cbc1; font: 10px var(--mono); }
|
||||
.item-art { width: 67%; height: 67%; display: grid; place-items: center; }
|
||||
.item-art img { display: block; width: 100%; height: 100%; object-fit: contain; image-rendering: pixelated; filter: drop-shadow(1px 1px 1px #000); }
|
||||
.icon-fallback { width: 25px; height: 25px; place-items: center; border: 1px solid #667260; border-radius: 2px; color: var(--text); background: #30382d; font: 10px var(--mono); }
|
||||
.icon-fallback:not([hidden]) { display: grid; }
|
||||
.stack-count { position: absolute; z-index: 2; right: 2px; bottom: 0; color: #fff; line-height: .9; font: 700 24px var(--mono); -webkit-text-stroke: 1px #000; text-shadow: 2px 2px 2px #000, -2px -2px 2px #000; }
|
||||
.inventory-legend { display: flex; align-items: center; gap: 6px; margin-top: 8px; color: var(--muted); font-size: 11px; }
|
||||
.legend-swatch { width: 8px; height: 8px; border: 1px solid var(--border); border-radius: 1px; }
|
||||
.legend-note { margin-left: auto; color: var(--subtle); }
|
||||
.inventory-warning { margin: 0 0 9px; padding: 7px 9px; border: 1px solid #927542; border-radius: 3px; background: #332b1c; color: #f0c36c; font-size: 12px; }
|
||||
|
||||
.side-column { display: grid; gap: 8px; }
|
||||
.side-panel { padding: 14px; }
|
||||
.compact-heading { margin-bottom: 9px; }
|
||||
.compact-heading h2 { font-size: 18px; }
|
||||
.count-badge { min-width: 21px; height: 21px; display: grid; place-items: center; border: 1px solid var(--border); border-radius: 3px; color: var(--accent); font: 10px var(--mono); }
|
||||
.player-list { display: grid; gap: 4px; }
|
||||
.player-row { min-width: 0; display: flex; align-items: center; gap: 7px; padding: 6px; border: 1px solid var(--border-soft); border-radius: 3px; background: #171a17; }
|
||||
.player-avatar { width: 22px; height: 22px; flex: 0 0 22px; display: grid; place-items: center; border: 1px solid #53604b; border-radius: 2px; background: #293126; color: var(--accent); font: 10px var(--mono); }
|
||||
.player-name { overflow: hidden; color: var(--text); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.player-pos { margin-left: auto; color: var(--muted); font: 10px var(--mono); white-space: nowrap; }
|
||||
.empty-state { padding: 12px 4px; color: var(--muted); text-align: center; font-size: 12px; }
|
||||
.runtime-list { display: grid; gap: 0; margin: 0; }
|
||||
.runtime-list div { display: flex; justify-content: space-between; gap: 10px; padding: 7px 0; border-bottom: 1px solid var(--border-soft); }
|
||||
.runtime-list div:last-child { border-bottom: 0; }
|
||||
.runtime-list dt { color: var(--muted); font-size: 12px; }
|
||||
.runtime-list dd { max-width: 62%; margin: 0; overflow: hidden; color: var(--text); font: 11px var(--mono); text-overflow: ellipsis; white-space: nowrap; }
|
||||
.error-box { margin-top: 8px; max-height: 90px; overflow: auto; padding: 7px; border: 1px solid #8f4848; border-radius: 3px; color: #ff9999; background: #2e1b1b; font: 10px var(--mono); white-space: pre-wrap; }
|
||||
.footer { display: flex; justify-content: space-between; margin-top: 10px; color: var(--muted); font: 10px var(--mono); letter-spacing: .04em; }
|
||||
.footer-mark { color: var(--accent); font-size: 7px; }
|
||||
.toast { position: fixed; right: 12px; bottom: 12px; max-width: 340px; padding: 9px 12px; border: 1px solid var(--border); border-radius: 3px; color: var(--text); background: var(--panel); opacity: 0; transform: translateY(5px); transition: .15s; pointer-events: none; }
|
||||
.toast.visible { opacity: 1; transform: translateY(0); }
|
||||
|
||||
@media (max-width: 850px) {
|
||||
.shell { width: min(100% - 24px, 700px); }
|
||||
.content-grid { grid-template-columns: 1fr; }
|
||||
.side-column { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.shell { width: calc(100% - 16px); }
|
||||
.topbar { height: 50px; }
|
||||
.version-tag { display: none; }
|
||||
main { padding-top: 9px; }
|
||||
.hero { min-height: auto; display: block; padding: 14px; }
|
||||
.hero-coordinates { min-width: 0; margin-top: 12px; padding: 10px 0 0; border: 0; border-top: 1px solid var(--border); }
|
||||
.coordinate-value { font-size: 17px; }
|
||||
.stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 5px; margin-top: 5px; }
|
||||
.stat-card { min-height: 78px; padding: 8px; }
|
||||
.stat-value { margin-top: 6px; font-size: 20px; }
|
||||
.content-grid { gap: 5px; margin-top: 5px; }
|
||||
.inventory-panel { padding: 10px 7px; }
|
||||
.inventory-summary { font-size: 10px; gap: 5px; }
|
||||
.inventory-grid { gap: 3px; padding: 5px; }
|
||||
.slot-number { top: 2px; left: 2px; font-size: 9px; }
|
||||
.stack-count { right: 1px; bottom: 0; font-size: 20px; }
|
||||
.side-column { grid-template-columns: 1fr; gap: 5px; }
|
||||
.footer { margin: 9px 2px; }
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const POLL_MS = 1000;
|
||||
const $ = (id) => document.getElementById(id);
|
||||
let itemNames = {};
|
||||
let lastInventorySignature = '';
|
||||
let lastState = null;
|
||||
|
||||
const escapeHTML = (value) => String(value ?? '').replace(/[&<>"']/g, (ch) => ({
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||||
})[ch]);
|
||||
|
||||
function prettyName(name) {
|
||||
return String(name || 'unknown item').replace(/^minecraft:/, '')
|
||||
.replaceAll('_', ' ').replace(/\b\w/g, (letter) => letter.toUpperCase());
|
||||
}
|
||||
|
||||
function vectorText(pos) {
|
||||
if (!pos) return '—';
|
||||
if (typeof pos === 'object' && ['x', 'y', 'z'].every((key) => key in pos)) {
|
||||
return `${Number(pos.x).toFixed(1)}, ${Number(pos.y).toFixed(1)}, ${Number(pos.z).toFixed(1)}`;
|
||||
}
|
||||
const match = String(pos).match(/-?\d+(?:\.\d+)?/g);
|
||||
return match && match.length >= 3 ? match.slice(0, 3).join(', ') : String(pos);
|
||||
}
|
||||
|
||||
function dimensionName(name) {
|
||||
return String(name || 'unknown').replace(/^minecraft:/, '').replaceAll('_', ' ').toUpperCase();
|
||||
}
|
||||
|
||||
function formatMode(mode) {
|
||||
const modes = {0: 'Survival', 1: 'Creative', 2: 'Adventure', 3: 'Spectator'};
|
||||
return modes[mode] || (mode == null ? '—' : String(mode));
|
||||
}
|
||||
|
||||
function textureCandidates(name) {
|
||||
const id = String(name || '').replace(/^minecraft:/, '').toLowerCase();
|
||||
if (!/^[a-z0-9_]+$/.test(id) || id === 'air') return [];
|
||||
// Prefer bundled icons, then the same-origin asset proxy. The external
|
||||
// sources remain a last resort for uncached items on older bot processes.
|
||||
const local = `/static/item-icons/${encodeURIComponent(id)}.png`;
|
||||
const proxy = `/api/item-icon/${encodeURIComponent(id)}`;
|
||||
const root = 'https://assets.mcasset.cloud/26.3/assets/minecraft/textures';
|
||||
const paths = [`item/${id}.png`, `block/${id}.png`];
|
||||
const simplified = id.replace(/_(slab|stairs|wall|fence_gate|fence|door|trapdoor|button|pressure_plate|sign|hanging_sign|carpet|banner|bed|candle)$/, '');
|
||||
if (simplified !== id) paths.push(`block/${simplified}.png`, `item/${simplified}.png`);
|
||||
return [local, proxy, ...[...new Set(paths)].map((path) => `${root}/${path}`)];
|
||||
}
|
||||
|
||||
function renderItemIcon(img, fallback, itemName) {
|
||||
const candidates = textureCandidates(itemName);
|
||||
let index = 0;
|
||||
const tryNext = () => {
|
||||
if (index >= candidates.length) {
|
||||
img.remove();
|
||||
fallback.hidden = false;
|
||||
fallback.textContent = prettyName(itemName).slice(0, 2).toUpperCase();
|
||||
return;
|
||||
}
|
||||
img.src = candidates[index++];
|
||||
};
|
||||
img.onerror = tryNext;
|
||||
img.onload = () => { fallback.hidden = true; };
|
||||
tryNext();
|
||||
}
|
||||
|
||||
function inventorySignature(inv, partial) {
|
||||
return JSON.stringify([Boolean(partial), Object.entries(inv || {}).map(([slot, item]) => [
|
||||
slot, item && item.present, item && item.item_id, item && item.item_count
|
||||
])]);
|
||||
}
|
||||
|
||||
function renderInventory(state) {
|
||||
const inv = state.inv || {};
|
||||
const signature = inventorySignature(inv, state.inventory_partial);
|
||||
if (signature === lastInventorySignature) return;
|
||||
lastInventorySignature = signature;
|
||||
|
||||
const grid = $('inventory-grid');
|
||||
const items = [];
|
||||
let stackCount = 0;
|
||||
let totalCount = 0;
|
||||
for (let slot = 0; slot < 46; slot++) {
|
||||
const item = inv[String(slot)] || inv[slot];
|
||||
if (item && item.present) {
|
||||
stackCount++;
|
||||
totalCount += Number(item.item_count || 0);
|
||||
}
|
||||
items.push({slot, item});
|
||||
}
|
||||
|
||||
grid.innerHTML = items.map(({slot, item}) => {
|
||||
const present = Boolean(item && item.present);
|
||||
const itemId = present ? item.item_id : null;
|
||||
const name = present ? (itemNames[String(itemId)] || `item_${itemId}`) : '';
|
||||
const count = present ? Number(item.item_count || 0) : 0;
|
||||
const title = present ? `${prettyName(name)} × ${count} (id ${itemId}, slot ${slot})` : `Empty slot ${slot}`;
|
||||
return `<div class="inventory-slot${present ? ' occupied' : ''}${slot >= 36 && slot <= 44 ? ' hotbar-slot' : ''}"
|
||||
title="${escapeHTML(title)}" aria-label="${escapeHTML(title)}">
|
||||
<span class="slot-number">${slot}</span>
|
||||
${present ? `<span class="item-art"><img alt="" loading="lazy"><span class="icon-fallback" hidden></span></span>
|
||||
<span class="stack-count">${count > 1 ? count : ''}</span>` : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
items.forEach(({slot, item}) => {
|
||||
if (!item || !item.present) return;
|
||||
const icon = grid.children[slot]?.querySelector('img');
|
||||
const fallback = grid.children[slot]?.querySelector('.icon-fallback');
|
||||
if (icon && fallback) renderItemIcon(icon, fallback, itemNames[String(item.item_id)] || `item_${item.item_id}`);
|
||||
});
|
||||
|
||||
$('inventory-count').textContent = `${totalCount} items`;
|
||||
$('inventory-slots').textContent = `${stackCount} / 46 slots`;
|
||||
$('inventory-warning').hidden = !state.inventory_partial;
|
||||
}
|
||||
|
||||
function renderPlayers(state) {
|
||||
const players = Object.values(state.players || {});
|
||||
$('player-count').textContent = String(players.length);
|
||||
if (!players.length) {
|
||||
$('player-list').innerHTML = '<div class="empty-state">No player data in range</div>';
|
||||
return;
|
||||
}
|
||||
$('player-list').innerHTML = players.map((player) => {
|
||||
const uuid = String(player.player_uuid || '');
|
||||
const name = state.player_names?.[uuid] || state.player_names?.[uuid.toLowerCase()] || `Player ${player.entity_id}`;
|
||||
const pos = [player.x, player.y, player.z].map((n) => Number(n).toFixed(0)).join(', ');
|
||||
return `<div class="player-row"><span class="player-avatar">${escapeHTML(name.slice(0, 1).toUpperCase())}</span>
|
||||
<span class="player-name">${escapeHTML(name)}</span><span class="player-pos">${escapeHTML(pos)}</span></div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function render(state) {
|
||||
lastState = state;
|
||||
const online = Boolean(state.connected) && state.connection_status === 'in_game';
|
||||
const pill = $('connection-pill');
|
||||
pill.classList.toggle('is-online', online);
|
||||
pill.classList.toggle('is-offline', !online);
|
||||
pill.querySelector('span').textContent = online ? 'ONLINE' : (state.connection_status || 'OFFLINE').toUpperCase();
|
||||
|
||||
$('bot-name').textContent = state.name || 'Mosfet';
|
||||
$('session-subtitle').textContent = online
|
||||
? `${formatMode(state.game_mode)} mode · protocol 777 · AuthMe session active`
|
||||
: `Connection state: ${state.connection_status || 'disconnected'}`;
|
||||
$('position').textContent = vectorText(state.pos || state.player_position);
|
||||
$('dimension').textContent = dimensionName(state.dimension);
|
||||
|
||||
const health = Number(state.health);
|
||||
const food = Number(state.food);
|
||||
$('health-value').textContent = Number.isFinite(health) ? String(health) : '—';
|
||||
$('food-value').textContent = Number.isFinite(food) ? String(food) : '—';
|
||||
$('health-meter').style.width = `${Math.max(0, Math.min(100, health * 5 || 0))}%`;
|
||||
$('food-meter').style.width = `${Math.max(0, Math.min(100, food * 5 || 0))}%`;
|
||||
|
||||
const movement = state.movement_status || (state.path?.length ? 'following_path' : 'idle');
|
||||
$('movement-value').textContent = String(movement).replaceAll('_', ' ').toUpperCase();
|
||||
$('movement-detail').textContent = state.path?.length ? `${state.path.length} nodes remaining` : 'No active path';
|
||||
$('chunk-count').textContent = state.chunk_state?.loaded ?? '—';
|
||||
const center = state.chunk_state?.center;
|
||||
$('chunk-detail').textContent = center ? `Center ${center[0]}, ${center[1]} · radius ${state.chunk_state.radius ?? '—'}` : 'Waiting for world data';
|
||||
$('game-mode').textContent = formatMode(state.game_mode);
|
||||
$('job-state').textContent = prettyName(state.job_state || 'idle').toUpperCase();
|
||||
$('path-length').textContent = String(state.path?.length || 0);
|
||||
$('entity-count').textContent = `${Object.keys(state.mobs || {}).length} mobs · ${Object.keys(state.objects || {}).length} drops`;
|
||||
$('afk-state').textContent = state.afk ? 'YES' : 'NO';
|
||||
$('movement-error').hidden = !state.movement_error;
|
||||
$('movement-error').textContent = state.movement_error ? `Movement error: ${state.movement_error}` : '';
|
||||
|
||||
renderInventory(state);
|
||||
renderPlayers(state);
|
||||
$('last-update').textContent = `Updated ${new Date().toLocaleTimeString()}`;
|
||||
}
|
||||
|
||||
async function loadItems() {
|
||||
try {
|
||||
const response = await fetch('/api/items', {cache: 'force-cache'});
|
||||
if (response.ok) itemNames = await response.json();
|
||||
if (lastState) {
|
||||
lastInventorySignature = '';
|
||||
renderInventory(lastState);
|
||||
}
|
||||
} catch (_) {
|
||||
// Inventory still renders with numeric IDs if the name endpoint is absent.
|
||||
}
|
||||
}
|
||||
|
||||
async function poll() {
|
||||
try {
|
||||
const response = await fetch('/api/global', {cache: 'no-store'});
|
||||
if (!response.ok) throw new Error(`API ${response.status}`);
|
||||
render(await response.json());
|
||||
} catch (error) {
|
||||
const pill = $('connection-pill');
|
||||
pill.classList.add('is-offline');
|
||||
pill.classList.remove('is-online');
|
||||
pill.querySelector('span').textContent = 'API OFFLINE';
|
||||
$('session-subtitle').textContent = 'Cannot reach the bot web API';
|
||||
$('last-update').textContent = `Last update failed · ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
loadItems();
|
||||
poll();
|
||||
window.setInterval(poll, POLL_MS);
|
||||
})();
|
||||
@@ -0,0 +1,107 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#10130f" />
|
||||
<meta name="description" content="Live Minecraft bot dashboard" />
|
||||
<title>Mosfet · Minecraft Bot</title>
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css?v=4" />
|
||||
<script defer src="/static/js/dashboard.js?v=7"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<main>
|
||||
<section class="hero panel">
|
||||
<div class="hero-main">
|
||||
<div class="eyebrow">LIVE SESSION <span class="eyebrow-line"></span></div>
|
||||
<h1 id="bot-name">Mosfet<span class="name-period">.</span></h1>
|
||||
<p id="session-subtitle" class="session-subtitle">Waiting for the bot connection</p>
|
||||
</div>
|
||||
<div class="hero-coordinates">
|
||||
<div class="coordinate-label">CURRENT POSITION</div>
|
||||
<div id="position" class="coordinate-value">—</div>
|
||||
<div id="dimension" class="dimension-value">NO DIMENSION</div>
|
||||
</div>
|
||||
<div class="hero-grid" aria-hidden="true"></div>
|
||||
<div class="hero-glow" aria-hidden="true"></div>
|
||||
</section>
|
||||
|
||||
<section class="stats-grid" aria-label="Bot status">
|
||||
<article class="stat-card panel">
|
||||
<div class="stat-top"><span class="stat-label">HEALTH</span><span class="stat-icon heart">♥</span></div>
|
||||
<div class="stat-value"><span id="health-value">—</span><small>/ 20</small></div>
|
||||
<div class="meter"><span id="health-meter" class="meter-fill health-fill"></span></div>
|
||||
</article>
|
||||
<article class="stat-card panel">
|
||||
<div class="stat-top"><span class="stat-label">HUNGER</span><span class="stat-icon food">◆</span></div>
|
||||
<div class="stat-value"><span id="food-value">—</span><small>/ 20</small></div>
|
||||
<div class="meter"><span id="food-meter" class="meter-fill food-fill"></span></div>
|
||||
</article>
|
||||
<article class="stat-card panel">
|
||||
<div class="stat-top"><span class="stat-label">MOVEMENT</span><span class="live-dot"></span></div>
|
||||
<div id="movement-value" class="stat-value stat-word">IDLE</div>
|
||||
<div id="movement-detail" class="stat-caption">No active path</div>
|
||||
</article>
|
||||
<article class="stat-card panel">
|
||||
<div class="stat-top"><span class="stat-label">WORLD CACHE</span><span class="stat-icon cache-icon">▦</span></div>
|
||||
<div class="stat-value"><span id="chunk-count">—</span><small> chunks</small></div>
|
||||
<div id="chunk-detail" class="stat-caption">Waiting for world data</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="content-grid">
|
||||
<section class="inventory-panel panel">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<div class="eyebrow">PLAYER STORAGE</div>
|
||||
<h2>Inventory</h2>
|
||||
</div>
|
||||
<div class="inventory-summary">
|
||||
<span id="inventory-count">0 items</span>
|
||||
<span class="heading-divider"></span>
|
||||
<span id="inventory-slots">0 / 46 slots</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="inventory-warning" class="inventory-warning" hidden>
|
||||
Some item components are not decoded yet; displayed contents may be partial.
|
||||
</div>
|
||||
<div id="inventory-grid" class="inventory-grid" aria-label="Minecraft inventory"></div>
|
||||
<div class="inventory-legend"><span class="legend-swatch"></span> Empty slot <span class="legend-note">Hover an item for its name and stack size</span></div>
|
||||
</section>
|
||||
|
||||
<aside class="side-column">
|
||||
<section class="side-panel panel">
|
||||
<div class="section-heading compact-heading">
|
||||
<div><div class="eyebrow">MULTIPLAYER</div><h2>Players</h2></div>
|
||||
<span id="player-count" class="count-badge">0</span>
|
||||
</div>
|
||||
<div id="player-list" class="player-list"><div class="empty-state">No player data yet</div></div>
|
||||
</section>
|
||||
|
||||
<section class="side-panel panel">
|
||||
<div class="section-heading compact-heading">
|
||||
<div><div class="eyebrow">AUTOMATION</div><h2>Runtime</h2></div>
|
||||
</div>
|
||||
<dl class="runtime-list">
|
||||
<div><dt>Game mode</dt><dd id="game-mode">—</dd></div>
|
||||
<div><dt>Active job</dt><dd id="job-state">IDLE</dd></div>
|
||||
<div><dt>Path nodes</dt><dd id="path-length">0</dd></div>
|
||||
<div><dt>Entities</dt><dd id="entity-count">0 mobs · 0 drops</dd></div>
|
||||
<div><dt>AFK</dt><dd id="afk-state">NO</dd></div>
|
||||
</dl>
|
||||
<div id="movement-error" class="error-box" hidden></div>
|
||||
</section>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<footer class="footer">
|
||||
<span class="footer-brand"><span class="footer-mark">■</span> MOSFET / 26.3</span>
|
||||
<span id="connection-pill" class="connection-pill is-offline"><i></i><span>OFFLINE</span></span>
|
||||
<span id="last-update">Waiting for API data</span>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
-11418
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user