diff --git a/doc/pics/stats_webfragment.png b/doc/pics/stats_webfragment.png
new file mode 100644
index 0000000..5a9eba6
Binary files /dev/null and b/doc/pics/stats_webfragment.png differ
diff --git a/doc/stats_webfragment.md b/doc/stats_webfragment.md
new file mode 100644
index 0000000..d6f015e
--- /dev/null
+++ b/doc/stats_webfragment.md
@@ -0,0 +1,11 @@
+
+# Stats webfragment
+
+
+
+The "world stats" info from the bottom right corner of the mapserver
+can be embedded as an iframe into any existing web-page:
+
+```html
+
+```
diff --git a/readme.md b/readme.md
index 35c0625..b841f9c 100644
--- a/readme.md
+++ b/readme.md
@@ -23,6 +23,7 @@ Demo: [Pandorabox Server map](https://pandorabox.io/map/#-1782.25/493.5/10)
* [Search](doc/search.md)
* [Configuration](doc/config.md)
* [Recommended specs](doc/recommended_specs.md)
+* [Stats webfragment](doc/stats_webfragment.md)
* [Contribution](doc/contrib.md)
* [Development](doc/dev.md)
* [License](doc/license.md)
diff --git a/static/js/bundle-stats.js b/static/js/bundle-stats.js
new file mode 100644
index 0000000..dbc7ce2
--- /dev/null
+++ b/static/js/bundle-stats.js
@@ -0,0 +1,104 @@
+(function(f){typeof define==='function'&&define.amd?define(f):f();}((function(){'use strict';function WorldStats(info){
+
+ var timeIcon = m("span", { class: "fa fa-sun", style: "color: orange;" });
+
+ if (info.time < 5500 || info.time > 19000) //0 - 24'000
+ timeIcon = m("span", { class: "fa fa-moon", style: "color: blue;" });
+
+ function getHour(){
+ return Math.floor(info.time/1000);
+ }
+
+ function getMinute(){
+ var min = Math.floor((info.time % 1000) / 1000 * 60);
+ return min > 10 ? min : "0" + min;
+ }
+
+ function getLag(){
+ var color = "green";
+ if (info.max_lag > 0.8)
+ color = "orange";
+ else if (info.max_lag > 1.2)
+ color = "red";
+
+ return [
+ m("span", { class: "fa fa-wifi", style: "color: " + color }),
+ parseInt(info.max_lag*1000),
+ " ms"
+ ];
+ }
+
+ function getPlayers(){
+ return [
+ m("span", { class: "fa fa-users" }),
+ info.players ? info.players.length : "0"
+ ];
+ }
+
+ return m("div", [
+ getPlayers(),
+ " ",
+ getLag(),
+ " ",
+ m("span", { class: "fa fa-clock" }),
+ timeIcon,
+ getHour(), ":", getMinute()
+ ]);
+
+}class WebSocketChannel {
+ constructor(){
+ this.wsUrl = window.location.protocol.replace("http", "ws") +
+ "//" + window.location.host +
+ window.location.pathname.substring(0, window.location.pathname.lastIndexOf("/")) +
+ "/api/ws";
+
+ this.listenerMap = {/* type -> [listeners] */};
+ }
+
+ addListener(type, listener){
+ var list = this.listenerMap[type];
+ if (!list){
+ list = [];
+ this.listenerMap[type] = list;
+ }
+
+ list.push(listener);
+ }
+
+ removeListener(type, listener){
+ var list = this.listenerMap[type];
+ if (!list){
+ return;
+ }
+
+ this.listenerMap[type] = list.filter(l => l != listener);
+ }
+
+ connect(){
+ var ws = new WebSocket(this.wsUrl);
+ var self = this;
+
+ ws.onmessage = function(e){
+ var event = JSON.parse(e.data);
+ //rendered-tile, mapobject-created, mapobjects-cleared
+
+ var listeners = self.listenerMap[event.type];
+ if (listeners){
+ listeners.forEach(function(listener){
+ listener(event.data);
+ });
+ }
+ };
+
+ ws.onerror = function(){
+ //reconnect after some time
+ setTimeout(self.connect.bind(self), 1000);
+ };
+ }
+}
+
+var wsChannel = new WebSocketChannel();wsChannel.connect();
+
+wsChannel.addListener("minetest-info", function(info){
+ m.render(document.getElementById("app"), WorldStats(info));
+});})));//# sourceMappingURL=bundle-stats.js.map
diff --git a/static/js/bundle-stats.js.map b/static/js/bundle-stats.js.map
new file mode 100644
index 0000000..d98b2d8
--- /dev/null
+++ b/static/js/bundle-stats.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"bundle-stats.js","sources":["components/WorldStats.js","WebSocketChannel.js","stats.js"],"sourcesContent":["\nexport default function(info){\n\n var timeIcon = m(\"span\", { class: \"fa fa-sun\", style: \"color: orange;\" });\n\n if (info.time < 5500 || info.time > 19000) //0 - 24'000\n timeIcon = m(\"span\", { class: \"fa fa-moon\", style: \"color: blue;\" });\n\n function getHour(){\n return Math.floor(info.time/1000);\n }\n\n function getMinute(){\n var min = Math.floor((info.time % 1000) / 1000 * 60);\n return min > 10 ? min : \"0\" + min;\n }\n\n function getLag(){\n var color = \"green\";\n if (info.max_lag > 0.8)\n color = \"orange\";\n else if (info.max_lag > 1.2)\n color = \"red\";\n\n return [\n m(\"span\", { class: \"fa fa-wifi\", style: \"color: \" + color }),\n parseInt(info.max_lag*1000),\n \" ms\"\n ];\n }\n\n function getPlayers(){\n return [\n m(\"span\", { class: \"fa fa-users\" }),\n info.players ? info.players.length : \"0\"\n ];\n }\n\n return m(\"div\", [\n getPlayers(),\n \" \",\n getLag(),\n \" \",\n m(\"span\", { class: \"fa fa-clock\" }),\n timeIcon,\n getHour(), \":\", getMinute()\n ]);\n\n};\n","\nclass WebSocketChannel {\n constructor(){\n this.wsUrl = window.location.protocol.replace(\"http\", \"ws\") +\n \"//\" + window.location.host +\n window.location.pathname.substring(0, window.location.pathname.lastIndexOf(\"/\")) +\n \"/api/ws\";\n\n this.listenerMap = {/* type -> [listeners] */};\n }\n\n addListener(type, listener){\n var list = this.listenerMap[type];\n if (!list){\n list = [];\n this.listenerMap[type] = list;\n }\n\n list.push(listener);\n }\n\n removeListener(type, listener){\n var list = this.listenerMap[type];\n if (!list){\n return;\n }\n\n this.listenerMap[type] = list.filter(l => l != listener);\n }\n\n connect(){\n var ws = new WebSocket(this.wsUrl);\n var self = this;\n\n ws.onmessage = function(e){\n var event = JSON.parse(e.data);\n //rendered-tile, mapobject-created, mapobjects-cleared\n\n var listeners = self.listenerMap[event.type];\n if (listeners){\n listeners.forEach(function(listener){\n listener(event.data);\n });\n }\n };\n\n ws.onerror = function(){\n //reconnect after some time\n setTimeout(self.connect.bind(self), 1000);\n };\n }\n}\n\nexport default new WebSocketChannel();\n","\nimport WorldStats from './components/WorldStats.js';\nimport wsChannel from './WebSocketChannel.js';\n\nwsChannel.connect();\n\nwsChannel.addListener(\"minetest-info\", function(info){\n\tm.render(document.getElementById(\"app\"), WorldStats(info));\n});\n"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/static/js/components/WorldStats.js b/static/js/components/WorldStats.js
index 303ea79..cb139f9 100644
--- a/static/js/components/WorldStats.js
+++ b/static/js/components/WorldStats.js
@@ -12,7 +12,7 @@ export default function(info){
function getMinute(){
var min = Math.floor((info.time % 1000) / 1000 * 60);
- return min > 10 ? min : "0" + min;
+ return min >= 10 ? min : "0" + min;
}
function getLag(){
diff --git a/static/js/rollup.config.js b/static/js/rollup.config.js
index bc5416a..47ad1ea 100644
--- a/static/js/rollup.config.js
+++ b/static/js/rollup.config.js
@@ -1,5 +1,5 @@
-export default {
+export default [{
input: 'main.js',
output: {
file :'bundle.js',
@@ -7,4 +7,12 @@ export default {
sourcemap: true,
compact: true
}
-};
+},{
+ input: 'stats.js',
+ output: {
+ file :'bundle-stats.js',
+ format: 'umd',
+ sourcemap: true,
+ compact: true
+ }
+}];
diff --git a/static/js/stats.js b/static/js/stats.js
index e69de29..c9643d5 100644
--- a/static/js/stats.js
+++ b/static/js/stats.js
@@ -0,0 +1,9 @@
+
+import WorldStats from './components/WorldStats.js';
+import wsChannel from './WebSocketChannel.js';
+
+wsChannel.connect();
+
+wsChannel.addListener("minetest-info", function(info){
+ m.render(document.getElementById("app"), WorldStats(info));
+});
diff --git a/static/stats.html b/static/stats.html
index a761b20..3842994 100644
--- a/static/stats.html
+++ b/static/stats.html
@@ -14,9 +14,9 @@
-
+
-
+