diff --git a/.eslintrc.js b/.eslintrc.js
index 1d957a0..bc4b827 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -106,7 +106,7 @@ module.exports = {
"comma-style": ["error", "last"],
"complexity": "error",
"computed-property-spacing": ["error", "never"],
- "consistent-return": "error",
+ "consistent-return": "off",
"consistent-this": "error",
"curly": "error",
"default-case": "error",
@@ -124,7 +124,7 @@ module.exports = {
"function-paren-newline": "off",
"generator-star-spacing": ["error", { "before": false, "after": true }],
"grouped-accessor-pairs": "error",
- "guard-for-in": "error",
+ "guard-for-in": "off",
"id-blacklist": "error",
"id-length": "off",
"id-match": "error",
diff --git a/src/routes/tileserver.js b/src/routes/tileserver.js
index b069d93..58ed8f5 100644
--- a/src/routes/tileserver.js
+++ b/src/routes/tileserver.js
@@ -1,15 +1,29 @@
"use strict";
const fs = require("fs-extra");
const path = require("path");
+const sharp = require("sharp");
+
+module.exports = async function registerTileServer(app, tilesPath) {
+ // Create single color PNG for missing tiles using Sharp
+ const blackImage = await sharp({
+ create: {
+ width: 256,
+ height: 256,
+ channels: 4,
+ background: { r: 0, g: 0, b: 0, alpha: 1 },
+ // background: { r: 65, g: 57, b: 18, alpha: 1 }, // Forest green
+ },
+ })
+ .png()
+ .toBuffer();
-module.exports = function registerTileServer(app, tilesPath) {
// Serve tiles
app.get("/api/gridworld/tiles/:z/:x/:y.png", async (req, res) => {
try {
let file = await fs.readFile(path.resolve(tilesPath, `z${req.params.z}x${req.params.x}y${req.params.y}.png`));
res.send(file);
} catch (e) {
- res.status(404).send("Tile not found");
+ res.send(blackImage);
}
});
};
diff --git a/web/components/GridVisualizer.jsx b/web/components/GridVisualizer.jsx
index 9a3fdf7..9dbbb42 100644
--- a/web/components/GridVisualizer.jsx
+++ b/web/components/GridVisualizer.jsx
@@ -1,6 +1,7 @@
-import React, { useContext, useState } from "react";
+import React, { useContext, useState, useEffect } from "react";
import { Row, Col } from "antd";
-import { MapContainer, Polyline, Rectangle, Tooltip, TileLayer, SVGOverlay, Marker, Popup, Circle } from "react-leaflet";
+import { MapContainer, Polyline, Rectangle, Tooltip, SVGOverlay, Popup, Circle } from "react-leaflet";
+import { TileLayer as TileLayerCustom } from "./leaflet/TileLayerCustomReact";
import { ControlContext, useInstance, statusColors } from "@clusterio/web_ui";
import { useMapData } from "../model/mapData";
@@ -23,6 +24,14 @@ export default function GridVisualizer(props) {
const playerPositions = usePlayerPosition(control);
const [mapData] = useMapData();
const [activeInstance, setActiveInstance] = useState();
+ const [refreshTiles, setRefreshTiles] = useState("1");
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setRefreshTiles(Math.floor(Math.random() * 10000).toString());
+ }, 2500);
+ return () => clearInterval(interval);
+ });
return <>
@@ -34,7 +43,6 @@ export default function GridVisualizer(props) {
// eslint-disable-next-line max-len
integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
crossOrigin="">
-
{mapData.map_data?.length ?
-
@@ -110,8 +118,10 @@ function InstanceRender(props) {
-1 * position[1] / scaleFactor,
position[0] / scaleFactor,
]))}
- onclick={() => {
- props.setActiveInstance(props.instance.instance_id);
+ eventHandlers={{
+ click: () => {
+ props.setActiveInstance(props.instance.instance_id);
+ },
}}
color={props.instance.instance_id === props.activeInstance ? "#ffff00" : "#3388ff"}
opacity={0.5}
diff --git a/web/components/InstanceModal.jsx b/web/components/InstanceModal.jsx
index 106d946..b08417c 100644
--- a/web/components/InstanceModal.jsx
+++ b/web/components/InstanceModal.jsx
@@ -22,14 +22,13 @@ import MigrateInstanceButton from "./MigrateInstanceButton";
function InstanceModal(props) {
let control = useContext(ControlContext);
let [instance] = useInstance(props.instance_id);
- let [host] = useHost(instance?.["assigned_host"]);
+ let [host] = useHost(instance?.assignedHost);
let account = useAccount();
let navigate = useNavigate();
return <>
{props.instance_id && <>
}
>
- {!instance.assigned_host
+ {!instance.assignedHost
? Unassigned
- : host["name"] || instance["assigned_host"]
+ : host.name || instance.assignedHost
}
{instance["status"] &&
}
-
- {
- account.hasAllPermission("core.instance.save.list", "core.instance.save.list_subscribe")
- &&
-
-
- }
- {
- account.hasAnyPermission("core.log.follow", "core.instance.send_rcon")
- &&
- Console
- {account.hasPermission("core.log.follow") && }
- {account.hasPermission("core.instance.send_rcon")
- && }
-
- }
- {
- account.hasPermission("core.instance.get_config")
- &&
-
-
- }
-
+ ,
+ }, {
+ disabled: !account.hasAnyPermission("core.log.follow", "core.instance.send_rcon"),
+ label: "Console",
+ key: "console",
+ children:
+ Console
+ {account.hasPermission("core.log.follow") && }
+ {account.hasPermission("core.instance.send_rcon")
+ && }
+
,
+ }, {
+ disabled: !account.hasPermission("core.instance.get_config"),
+ label: "Config",
+ key: "config",
+ children: ,
+ },
+ ]}
+ />
>}
>;
}
diff --git a/web/components/MigrateInstanceModal.jsx b/web/components/MigrateInstanceModal.jsx
index 96898db..4e5490e 100644
--- a/web/components/MigrateInstanceModal.jsx
+++ b/web/components/MigrateInstanceModal.jsx
@@ -53,10 +53,10 @@ export default function MigrateInstanceModal(props) {
-