diff --git a/tests/test_web_inventory_api.py b/tests/test_web_inventory_api.py
index 70a2be7..6d420b3 100644
--- a/tests/test_web_inventory_api.py
+++ b/tests/test_web_inventory_api.py
@@ -32,7 +32,7 @@ class TestWebInventoryAPI(unittest.TestCase):
self.g = mosfet_web.g
self.original = {
key: self.g.get(key) for key in
- ('connected', 'game', 'inv', 'chest_viewers')
+ ('connected', 'game', 'inv', 'chest_viewers', 'time', 'time_base')
}
self.game = FakeGame()
self.g.connected = True
@@ -54,6 +54,16 @@ class TestWebInventoryAPI(unittest.TestCase):
item = response.get_json()['inv']['9']
self.assertEqual(item['components']['custom_name']['text'], 'Test Paper')
+ def test_global_api_exposes_world_time_ticks(self):
+ self.g.time = 12345.5
+ self.g.time_base = 12345.5
+
+ response = self.client.get('/api/global')
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.get_json()['time'], 12345.5)
+ self.assertEqual(response.get_json()['time_base'], 12345.5)
+
def test_global_api_stringifies_tuple_keys(self):
self.g.chest_viewers = {(12, 64, -7): 1}
diff --git a/web_interface/build/index.html b/web_interface/build/index.html
index 6fb265d..c3e9ef4 100644
--- a/web_interface/build/index.html
+++ b/web_interface/build/index.html
@@ -86,6 +86,7 @@
- Game mode
- —
+ - Time
- —
- Active job
- IDLE
- Path nodes
- 0
- Entities
- 0 mobs · 0 drops
diff --git a/web_interface/public/index.html b/web_interface/public/index.html
index 6fb265d..c3e9ef4 100644
--- a/web_interface/public/index.html
+++ b/web_interface/public/index.html
@@ -86,6 +86,7 @@
- Game mode
- —
+ - Time
- —
- Active job
- IDLE
- Path nodes
- 0
- Entities
- 0 mobs · 0 drops
diff --git a/web_interface/src/dashboard.js b/web_interface/src/dashboard.js
index ebf8d69..fe6d362 100644
--- a/web_interface/src/dashboard.js
+++ b/web_interface/src/dashboard.js
@@ -57,6 +57,19 @@
return modes[mode] || (mode == null ? '—' : String(mode));
}
+ function formatWorldClock(ticks) {
+ if (ticks == null) return '—';
+ const time = Number(ticks);
+ if (!Number.isFinite(time)) return '—';
+ const dayTicks = ((Math.floor(time) % 24000) + 24000) % 24000;
+ const minutes = Math.floor((((dayTicks + 6000) % 24000) / 1000) * 60);
+ const hour24 = Math.floor(minutes / 60);
+ const hour12 = hour24 % 12 || 12;
+ const minute = minutes % 60;
+ const period = hour24 < 12 ? 'AM' : 'PM';
+ return `${String(hour12).padStart(2, '0')}:${String(minute).padStart(2, '0')} ${period}`;
+ }
+
function textureCandidates(name) {
const id = String(name || '').replace(/^minecraft:/, '').toLowerCase();
if (!/^[a-z0-9_]+$/.test(id) || id === 'air') return [];
@@ -451,6 +464,11 @@
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);
+ const worldTime = online && state.time_base != null && state.time != null
+ ? Number(state.time) : NaN;
+ $('world-time').textContent = Number.isFinite(worldTime)
+ ? `${Math.floor(worldTime).toLocaleString()} t · ${formatWorldClock(worldTime)}`
+ : '—';
$('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`;
diff --git a/web_interface/src/index.html b/web_interface/src/index.html
index 6fb265d..c3e9ef4 100644
--- a/web_interface/src/index.html
+++ b/web_interface/src/index.html
@@ -86,6 +86,7 @@
- Game mode
- —
+ - Time
- —
- Active job
- IDLE
- Path nodes
- 0
- Entities
- 0 mobs · 0 drops