Add dropper location selector: discover available droppers on network, pass through server, and add dropdown in order panel for location-based dispensing
This commit is contained in:
@@ -412,6 +412,51 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dropper location selector */
|
||||||
|
.dropper-select-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropper-label {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--mc-text-gold);
|
||||||
|
text-shadow: 1px 1px 0 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropper-select {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border: 2px solid #444;
|
||||||
|
color: var(--mc-text-white);
|
||||||
|
font-family: 'Silkscreen', 'Courier New', monospace;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 0.375rem 0.5rem;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23aaa'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 0.5rem center;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropper-select:hover {
|
||||||
|
border-color: var(--mc-text-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropper-select:focus {
|
||||||
|
border-color: var(--mc-text-green);
|
||||||
|
box-shadow: inset 0 0 4px rgba(85, 255, 85, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropper-select option {
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: var(--mc-text-white);
|
||||||
|
}
|
||||||
|
|
||||||
.order-btn {
|
.order-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.625rem !important;
|
padding: 0.625rem !important;
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ function InventoryGrid() {
|
|||||||
const [orderAmount, setOrderAmount] = useState(1);
|
const [orderAmount, setOrderAmount] = useState(1);
|
||||||
const [sortBy, setSortBy] = useState('count');
|
const [sortBy, setSortBy] = useState('count');
|
||||||
const [activeCategory, setActiveCategory] = useState('all');
|
const [activeCategory, setActiveCategory] = useState('all');
|
||||||
|
const [selectedDropper, setSelectedDropper] = useState('');
|
||||||
|
|
||||||
|
const droppers = inventory.droppers || [];
|
||||||
|
|
||||||
const items = getFilteredItems();
|
const items = getFilteredItems();
|
||||||
|
|
||||||
@@ -33,8 +36,8 @@ function InventoryGrid() {
|
|||||||
|
|
||||||
const handleOrder = useCallback(async () => {
|
const handleOrder = useCallback(async () => {
|
||||||
if (!selectedItem || orderAmount <= 0) return;
|
if (!selectedItem || orderAmount <= 0) return;
|
||||||
await orderItem(selectedItem.name, orderAmount);
|
await orderItem(selectedItem.name, orderAmount, selectedDropper || undefined);
|
||||||
}, [selectedItem, orderAmount, orderItem]);
|
}, [selectedItem, orderAmount, orderItem, selectedDropper]);
|
||||||
|
|
||||||
const handleItemClick = (item) => {
|
const handleItemClick = (item) => {
|
||||||
setSelectedItem(item);
|
setSelectedItem(item);
|
||||||
@@ -162,6 +165,22 @@ function InventoryGrid() {
|
|||||||
<div className="order-section">
|
<div className="order-section">
|
||||||
<h4>📤 Order Items</h4>
|
<h4>📤 Order Items</h4>
|
||||||
<div className="order-controls">
|
<div className="order-controls">
|
||||||
|
{droppers.length > 1 && (
|
||||||
|
<div className="dropper-select-wrapper">
|
||||||
|
<label className="dropper-label">📍 Dispense to:</label>
|
||||||
|
<select
|
||||||
|
className="dropper-select"
|
||||||
|
value={selectedDropper}
|
||||||
|
onChange={(e) => setSelectedDropper(e.target.value)}
|
||||||
|
>
|
||||||
|
{droppers.map((d) => (
|
||||||
|
<option key={d.name} value={d.name}>
|
||||||
|
{d.name.replace(/^minecraft:/, '')}{d.isDefault ? ' (default)' : ''}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="order-amount-controls">
|
<div className="order-amount-controls">
|
||||||
<button className="mc-btn" onClick={() => setOrderAmount(Math.max(1, orderAmount - 1))}>-</button>
|
<button className="mc-btn" onClick={() => setOrderAmount(Math.max(1, orderAmount - 1))}>-</button>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -5,25 +5,37 @@ import './ItemIcon.css';
|
|||||||
// All textures are proxied & cached through our server
|
// All textures are proxied & cached through our server
|
||||||
const TEXTURE_PROXY_BASE = '/api/texture';
|
const TEXTURE_PROXY_BASE = '/api/texture';
|
||||||
|
|
||||||
// Some items have texture names that differ from their registry name
|
// Items whose texture file name differs from their registry name
|
||||||
const TEXTURE_ALIASES = {
|
const TEXTURE_ALIASES = {
|
||||||
// Crops / seeds
|
// Renamed items in 1.20+
|
||||||
wheat_seeds: 'wheat_seeds',
|
grass: 'short_grass',
|
||||||
melon_seeds: 'melon_seeds',
|
scute: 'turtle_scute',
|
||||||
pumpkin_seeds: 'pumpkin_seeds',
|
};
|
||||||
beetroot_seeds: 'beetroot_seeds',
|
|
||||||
// Potions and such
|
// CC:Tweaked texture paths (registry name → actual file in the CC repo)
|
||||||
experience_bottle: 'experience_bottle',
|
const CC_TEXTURE_MAP = {
|
||||||
// Renamed textures
|
turtle_normal: 'block/turtle_normal_front',
|
||||||
golden_apple: 'golden_apple',
|
turtle_advanced: 'block/turtle_advanced_front',
|
||||||
enchanted_golden_apple: 'enchanted_golden_apple',
|
computer_normal: 'block/computer_normal_front',
|
||||||
// Redstone components
|
computer_advanced: 'block/computer_advanced_front',
|
||||||
redstone: 'redstone_dust',
|
computer_command: 'block/computer_command_front',
|
||||||
repeater: 'repeater',
|
monitor_normal: 'block/monitor_normal_0',
|
||||||
comparator: 'comparator',
|
monitor_advanced: 'block/monitor_advanced_0',
|
||||||
// Misc items with different texture names
|
wired_modem: 'block/wired_modem_face',
|
||||||
map: 'map',
|
wired_modem_full: 'block/wired_modem_face',
|
||||||
filled_map: 'filled_map',
|
wireless_modem_normal: 'block/wireless_modem_normal_face',
|
||||||
|
wireless_modem_advanced: 'block/wireless_modem_advanced_face',
|
||||||
|
speaker: 'block/speaker_front',
|
||||||
|
disk_drive: 'block/disk_drive_front',
|
||||||
|
printer: 'block/printer_front_empty',
|
||||||
|
cable: 'block/cable_core',
|
||||||
|
// CC item textures
|
||||||
|
pocket_computer_normal: 'item/pocket_computer_normal',
|
||||||
|
pocket_computer_advanced: 'item/pocket_computer_advanced',
|
||||||
|
disk: 'item/disk_frame',
|
||||||
|
printed_book: 'item/printed_book',
|
||||||
|
printed_page: 'item/printed_page',
|
||||||
|
printed_pages: 'item/printed_pages',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Items whose texture lives in the block/ folder instead of item/
|
// Items whose texture lives in the block/ folder instead of item/
|
||||||
@@ -39,10 +51,7 @@ const BLOCK_TEXTURES = new Set([
|
|||||||
'cobblestone', 'mossy_cobblestone', 'obsidian', 'crying_obsidian',
|
'cobblestone', 'mossy_cobblestone', 'obsidian', 'crying_obsidian',
|
||||||
'netherrack', 'soul_sand', 'soul_soil', 'basalt', 'polished_basalt', 'smooth_basalt',
|
'netherrack', 'soul_sand', 'soul_soil', 'basalt', 'polished_basalt', 'smooth_basalt',
|
||||||
'glowstone', 'glass', 'tinted_glass',
|
'glowstone', 'glass', 'tinted_glass',
|
||||||
'oak_slab', 'spruce_slab', 'birch_slab', 'jungle_slab', 'acacia_slab', 'dark_oak_slab',
|
|
||||||
'stone_slab', 'cobblestone_slab', 'brick_slab', 'stone_brick_slab',
|
|
||||||
'oak_stairs', 'spruce_stairs', 'birch_stairs', 'jungle_stairs', 'acacia_stairs',
|
|
||||||
'stone_stairs', 'cobblestone_stairs', 'brick_stairs', 'stone_brick_stairs',
|
|
||||||
'bricks', 'stone_bricks', 'mossy_stone_bricks', 'cracked_stone_bricks',
|
'bricks', 'stone_bricks', 'mossy_stone_bricks', 'cracked_stone_bricks',
|
||||||
'chiseled_stone_bricks', 'deepslate_bricks', 'nether_bricks', 'red_nether_bricks',
|
'chiseled_stone_bricks', 'deepslate_bricks', 'nether_bricks', 'red_nether_bricks',
|
||||||
'bookshelf', 'clay', 'pumpkin', 'carved_pumpkin', 'jack_o_lantern', 'melon',
|
'bookshelf', 'clay', 'pumpkin', 'carved_pumpkin', 'jack_o_lantern', 'melon',
|
||||||
@@ -72,14 +81,24 @@ const BLOCK_TEXTURES = new Set([
|
|||||||
'light_gray_terracotta', 'cyan_terracotta', 'purple_terracotta', 'blue_terracotta',
|
'light_gray_terracotta', 'cyan_terracotta', 'purple_terracotta', 'blue_terracotta',
|
||||||
'brown_terracotta', 'green_terracotta', 'red_terracotta', 'black_terracotta',
|
'brown_terracotta', 'green_terracotta', 'red_terracotta', 'black_terracotta',
|
||||||
'white_glazed_terracotta', 'orange_glazed_terracotta', 'magenta_glazed_terracotta',
|
'white_glazed_terracotta', 'orange_glazed_terracotta', 'magenta_glazed_terracotta',
|
||||||
|
'light_blue_glazed_terracotta', 'yellow_glazed_terracotta', 'lime_glazed_terracotta',
|
||||||
|
'pink_glazed_terracotta', 'gray_glazed_terracotta', 'light_gray_glazed_terracotta',
|
||||||
|
'cyan_glazed_terracotta', 'purple_glazed_terracotta', 'blue_glazed_terracotta',
|
||||||
|
'brown_glazed_terracotta', 'green_glazed_terracotta', 'red_glazed_terracotta',
|
||||||
|
'black_glazed_terracotta',
|
||||||
'white_stained_glass', 'orange_stained_glass', 'magenta_stained_glass',
|
'white_stained_glass', 'orange_stained_glass', 'magenta_stained_glass',
|
||||||
|
'light_blue_stained_glass', 'yellow_stained_glass', 'lime_stained_glass',
|
||||||
|
'pink_stained_glass', 'gray_stained_glass', 'light_gray_stained_glass',
|
||||||
|
'cyan_stained_glass', 'purple_stained_glass', 'blue_stained_glass',
|
||||||
|
'brown_stained_glass', 'green_stained_glass', 'red_stained_glass',
|
||||||
|
'black_stained_glass',
|
||||||
'crafting_table', 'furnace', 'blast_furnace', 'smoker', 'smithing_table',
|
'crafting_table', 'furnace', 'blast_furnace', 'smoker', 'smithing_table',
|
||||||
'fletching_table', 'cartography_table', 'loom', 'stonecutter', 'grindstone',
|
'fletching_table', 'cartography_table', 'loom', 'stonecutter', 'grindstone',
|
||||||
'anvil', 'chipped_anvil', 'damaged_anvil', 'enchanting_table',
|
'anvil', 'chipped_anvil', 'damaged_anvil', 'enchanting_table',
|
||||||
'brewing_stand', 'cauldron', 'composter', 'barrel', 'chest', 'trapped_chest',
|
'brewing_stand', 'cauldron', 'composter', 'barrel',
|
||||||
'ender_chest', 'shulker_box', 'dispenser', 'dropper', 'hopper', 'observer',
|
'shulker_box', 'dispenser', 'dropper', 'hopper', 'observer',
|
||||||
'piston', 'sticky_piston', 'redstone_lamp', 'target', 'lever',
|
'piston', 'sticky_piston', 'redstone_lamp', 'target', 'lever',
|
||||||
'beacon', 'conduit', 'bell', 'lodestone', 'respawn_anchor',
|
'beacon', 'conduit', 'lodestone', 'respawn_anchor',
|
||||||
'cactus', 'sugar_cane', 'bamboo',
|
'cactus', 'sugar_cane', 'bamboo',
|
||||||
'mushroom_stem', 'brown_mushroom_block', 'red_mushroom_block',
|
'mushroom_stem', 'brown_mushroom_block', 'red_mushroom_block',
|
||||||
'oak_leaves', 'spruce_leaves', 'birch_leaves', 'jungle_leaves',
|
'oak_leaves', 'spruce_leaves', 'birch_leaves', 'jungle_leaves',
|
||||||
@@ -131,8 +150,8 @@ const BLOCK_TEXTURE_SUFFIXES = {
|
|||||||
dispenser: '_front',
|
dispenser: '_front',
|
||||||
dropper: '_front',
|
dropper: '_front',
|
||||||
observer: '_front',
|
observer: '_front',
|
||||||
piston: '_front',
|
piston: '_top',
|
||||||
sticky_piston: '_front',
|
sticky_piston: '_top',
|
||||||
barrel: '_top',
|
barrel: '_top',
|
||||||
crafting_table: '_top',
|
crafting_table: '_top',
|
||||||
cartography_table: '_top',
|
cartography_table: '_top',
|
||||||
@@ -154,42 +173,99 @@ const BLOCK_TEXTURE_SUFFIXES = {
|
|||||||
polished_basalt: '_side',
|
polished_basalt: '_side',
|
||||||
quartz_pillar: '_side',
|
quartz_pillar: '_side',
|
||||||
purpur_pillar: '_side',
|
purpur_pillar: '_side',
|
||||||
|
tnt: '_side',
|
||||||
|
composter: '_side',
|
||||||
|
enchanting_table: '_top',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Resolve derivative blocks (stairs, slabs, fences, walls, buttons) to parent block texture
|
||||||
|
const WOOD_TYPES = new Set([
|
||||||
|
'oak', 'spruce', 'birch', 'jungle', 'acacia', 'dark_oak',
|
||||||
|
'mangrove', 'cherry', 'bamboo', 'crimson', 'warped',
|
||||||
|
]);
|
||||||
|
const STONE_ALIASES = {
|
||||||
|
brick: 'bricks', stone_brick: 'stone_bricks', mossy_stone_brick: 'mossy_stone_bricks',
|
||||||
|
nether_brick: 'nether_bricks', red_nether_brick: 'red_nether_bricks',
|
||||||
|
end_stone_brick: 'end_stone_bricks', deepslate_brick: 'deepslate_bricks',
|
||||||
|
deepslate_tile: 'deepslate_tiles', polished_blackstone_brick: 'polished_blackstone_bricks',
|
||||||
|
mud_brick: 'mud_bricks', quartz: 'quartz_block', purpur: 'purpur_block',
|
||||||
|
smooth_stone: 'smooth_stone',
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveDerivativeTexture(name) {
|
||||||
|
const suffixes = ['_stairs', '_slab', '_fence_gate', '_fence', '_wall', '_button', '_pressure_plate'];
|
||||||
|
for (const suffix of suffixes) {
|
||||||
|
if (!name.endsWith(suffix)) continue;
|
||||||
|
const base = name.slice(0, -suffix.length);
|
||||||
|
if (BLOCK_TEXTURES.has(base)) return base;
|
||||||
|
if (WOOD_TYPES.has(base)) return `${base}_planks`;
|
||||||
|
if (STONE_ALIASES[base]) return STONE_ALIASES[base];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt multiple texture URLs in order.
|
* Generate texture URLs to try in order.
|
||||||
* For vanilla: item/{name}.png → block/{name}.png
|
* - CC:Tweaked → curated texture map (1 request)
|
||||||
* For mods: mod repo item/ → mod repo block/ → vanilla fallback
|
* - Create → item/ then block/ (2 requests max)
|
||||||
|
* - Unknown mods → empty (instant emoji, no wasted requests)
|
||||||
|
* - Vanilla derivatives → parent block texture (1 request)
|
||||||
|
* - Vanilla → block/ or item/ with fallback
|
||||||
*/
|
*/
|
||||||
function getTextureUrls(fullItemName) {
|
function getTextureUrls(fullItemName) {
|
||||||
// Parse namespace and short name
|
|
||||||
const colonIdx = (fullItemName || '').indexOf(':');
|
const colonIdx = (fullItemName || '').indexOf(':');
|
||||||
const namespace = colonIdx >= 0 ? fullItemName.substring(0, colonIdx) : 'minecraft';
|
const namespace = colonIdx >= 0 ? fullItemName.substring(0, colonIdx) : 'minecraft';
|
||||||
const shortName = colonIdx >= 0 ? fullItemName.substring(colonIdx + 1) : fullItemName;
|
const shortName = colonIdx >= 0 ? fullItemName.substring(colonIdx + 1) : fullItemName;
|
||||||
const alias = TEXTURE_ALIASES[shortName] || shortName;
|
|
||||||
const urls = [];
|
const urls = [];
|
||||||
|
|
||||||
// For non-minecraft mods, try mod-specific URLs first
|
// CC:Tweaked → use curated texture map
|
||||||
if (namespace !== 'minecraft') {
|
if (namespace === 'computercraft') {
|
||||||
const knownMods = ['computercraft', 'create'];
|
const mapped = CC_TEXTURE_MAP[shortName];
|
||||||
if (knownMods.includes(namespace)) {
|
if (mapped) {
|
||||||
urls.push(`${TEXTURE_PROXY_BASE}/${namespace}/item/${shortName}.png`);
|
urls.push(`${TEXTURE_PROXY_BASE}/computercraft/${mapped}.png`);
|
||||||
urls.push(`${TEXTURE_PROXY_BASE}/${namespace}/block/${shortName}.png`);
|
} else {
|
||||||
urls.push(`${TEXTURE_PROXY_BASE}/${namespace}/block/${shortName}_front.png`);
|
urls.push(`${TEXTURE_PROXY_BASE}/computercraft/item/${shortName}.png`);
|
||||||
urls.push(`${TEXTURE_PROXY_BASE}/${namespace}/block/${shortName}_side.png`);
|
urls.push(`${TEXTURE_PROXY_BASE}/computercraft/block/${shortName}.png`);
|
||||||
}
|
}
|
||||||
|
return urls;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vanilla texture URLs (through proxy)
|
// Create mod → item/ then block/
|
||||||
if (BLOCK_TEXTURES.has(shortName)) {
|
if (namespace === 'create') {
|
||||||
const suffix = BLOCK_TEXTURE_SUFFIXES[shortName] || '';
|
urls.push(`${TEXTURE_PROXY_BASE}/create/item/${shortName}.png`);
|
||||||
|
urls.push(`${TEXTURE_PROXY_BASE}/create/block/${shortName}.png`);
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown mod namespace → no textures available, instant emoji fallback
|
||||||
|
if (namespace !== 'minecraft') {
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Vanilla (minecraft) ===
|
||||||
|
const alias = TEXTURE_ALIASES[shortName] || shortName;
|
||||||
|
|
||||||
|
// Derivative blocks (stairs, slabs, fences, walls, buttons) → parent texture
|
||||||
|
const parent = resolveDerivativeTexture(alias);
|
||||||
|
if (parent) {
|
||||||
|
const suffix = BLOCK_TEXTURE_SUFFIXES[parent] || '';
|
||||||
|
urls.push(`${TEXTURE_PROXY_BASE}/minecraft/block/${parent}${suffix}.png`);
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Known block → try block/ first (with suffix if applicable)
|
||||||
|
if (BLOCK_TEXTURES.has(alias)) {
|
||||||
|
const suffix = BLOCK_TEXTURE_SUFFIXES[alias] || '';
|
||||||
urls.push(`${TEXTURE_PROXY_BASE}/minecraft/block/${alias}${suffix}.png`);
|
urls.push(`${TEXTURE_PROXY_BASE}/minecraft/block/${alias}${suffix}.png`);
|
||||||
if (suffix) urls.push(`${TEXTURE_PROXY_BASE}/minecraft/block/${alias}.png`);
|
if (suffix) urls.push(`${TEXTURE_PROXY_BASE}/minecraft/block/${alias}.png`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try item/ texture
|
||||||
urls.push(`${TEXTURE_PROXY_BASE}/minecraft/item/${alias}.png`);
|
urls.push(`${TEXTURE_PROXY_BASE}/minecraft/item/${alias}.png`);
|
||||||
|
|
||||||
if (!BLOCK_TEXTURES.has(shortName)) {
|
// If not a known block, also try block/ as last resort
|
||||||
|
if (!BLOCK_TEXTURES.has(alias)) {
|
||||||
urls.push(`${TEXTURE_PROXY_BASE}/minecraft/block/${alias}.png`);
|
urls.push(`${TEXTURE_PROXY_BASE}/minecraft/block/${alias}.png`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export const useInventoryStore = create((set, get) => ({
|
|||||||
barrelOk: false,
|
barrelOk: false,
|
||||||
furnaceCount: 0,
|
furnaceCount: 0,
|
||||||
furnaceStatus: {},
|
furnaceStatus: {},
|
||||||
|
droppers: [],
|
||||||
},
|
},
|
||||||
activity: {},
|
activity: {},
|
||||||
alerts: [],
|
alerts: [],
|
||||||
|
|||||||
@@ -169,6 +169,23 @@ app.get('/api/texture/:namespace/*', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Clear negative texture cache (.miss files) — call after updating texture mappings
|
||||||
|
app.delete('/api/texture-cache/negative', (req, res) => {
|
||||||
|
let cleared = 0;
|
||||||
|
function walk(dir) {
|
||||||
|
try {
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const full = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) walk(full);
|
||||||
|
else if (entry.name.endsWith('.miss')) { fs.unlinkSync(full); cleared++; }
|
||||||
|
}
|
||||||
|
} catch (_) { /* ignore */ }
|
||||||
|
}
|
||||||
|
walk(TEXTURE_CACHE_DIR);
|
||||||
|
console.log(`[Texture Cache] Cleared ${cleared} negative cache entries`);
|
||||||
|
res.json({ cleared });
|
||||||
|
});
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
app.get('/api/health', (req, res) => {
|
app.get('/api/health', (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
Reference in New Issue
Block a user