Compare commits

...

4 Commits

Author SHA1 Message Date
MayaTheShy
026d0c8d6b Add Cross-Project Integration API for RemoteTurtle system 2026-03-22 04:10:11 -04:00
MayaTheShy
33845c70d7 Add cross-link to Turtle Dashboard in App component 2026-03-22 04:10:06 -04:00
MayaTheShy
fa18c72cf7 Add styles for cross-link button in App.css 2026-03-22 04:10:02 -04:00
MayaTheShy
c5aa4b5332 Add TURTLE_SERVER_URL environment variable to server configuration 2026-03-22 04:09:56 -04:00
4 changed files with 113 additions and 4 deletions

View File

@@ -4,6 +4,31 @@
@import url('https://fonts.googleapis.com/css2?family=Silkscreen:wght@400;700&display=swap');
/* === Cross-link Button === */
.cross-link-btn {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.375rem 0.75rem;
border: 2px solid var(--mc-dark);
background: #2e4a8b;
color: var(--mc-text-aqua);
font-size: 0.7rem;
font-weight: 700;
font-family: 'Silkscreen', 'Courier New', monospace;
text-shadow: 1px 1px 0 var(--mc-dark);
text-decoration: none;
cursor: pointer;
transition: all 0.1s;
box-shadow: inset 0 1px 0 #4466bb, inset 0 -1px 0 #1a2a66;
white-space: nowrap;
}
.cross-link-btn:hover {
background: #3e5a9b;
color: white;
}
:root {
--mc-dark: #1a1a1a;
--mc-darker: #0e0e0e;

View File

@@ -22,6 +22,8 @@ function App() {
const [showSettings, setShowSettings] = useState(false);
const [, forceRender] = useState(0);
const turtleDashboardUrl = import.meta.env.VITE_TURTLE_DASHBOARD_URL || `${window.location.protocol}//${window.location.hostname}:4444`;
useEffect(() => {
connect();
}, [connect]);
@@ -34,10 +36,6 @@ function App() {
const staleSecs = lastUpdate ? Math.floor((Date.now() - lastUpdate) / 1000) : null;
useEffect(() => {
connect();
}, [connect]);
const renderPanelContent = () => {
switch (panelTab) {
case 'inventory':
@@ -60,6 +58,16 @@ function App() {
<div className="app-header">
<h1> Inventory Manager</h1>
<div className="header-right">
{/* Cross-link to Turtle Dashboard */}
<a
href={turtleDashboardUrl}
className="cross-link-btn"
title="Open Turtle Control Dashboard"
target="_blank"
rel="noopener noreferrer"
>
🐢 Turtles
</a>
{/* Settings gear button */}
<button
className="settings-gear"

View File

@@ -7,6 +7,7 @@ services:
- server-data:/data
environment:
- API_KEY=${API_KEY:-}
- TURTLE_SERVER_URL=${TURTLE_SERVER_URL:-}
restart: unless-stopped
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3001/api/health',r=>{process.exit(r.statusCode===200?0:1)}).on('error',()=>process.exit(1))"]

View File

@@ -966,6 +966,78 @@ wss.on('close', () => {
clearInterval(WS_PING_INTERVAL);
});
// ========== Cross-Project Integration API ==========
// These endpoints allow the RemoteTurtle system to query inventory state
const TURTLE_SERVER_URL = process.env.TURTLE_SERVER_URL || ''; // e.g. http://turtle-server:3001
// Find where a specific item is stored (for turtles to pick up items)
app.get('/api/integration/locate-item', (req, res) => {
const { name, minCount } = req.query;
if (!name) return res.status(400).json({ error: 'Item name required (?name=minecraft:diamond)' });
const items = inventoryState.itemList || [];
const match = items.find(i => i.name === name);
if (!match || match.count < (parseInt(minCount) || 1)) {
return res.json({ found: false, available: match ? match.count : 0 });
}
res.json({ found: true, name: match.name, count: match.count, displayName: match.displayName });
});
// Search items by partial name (for turtle autocomplete/fuzzy matching)
app.get('/api/integration/search-items', (req, res) => {
const { q, limit } = req.query;
if (!q) return res.status(400).json({ error: 'Search query required (?q=diamond)' });
const items = inventoryState.itemList || [];
const query = q.toLowerCase();
const results = items
.filter(i => i.name.toLowerCase().includes(query) || (i.displayName || '').toLowerCase().includes(query))
.sort((a, b) => b.count - a.count)
.slice(0, parseInt(limit) || 20);
res.json({ results });
});
// Get storage summary (available space, total items) for turtle decision-making
app.get('/api/integration/storage-status', (req, res) => {
res.json({
grandTotal: inventoryState.grandTotal || 0,
chestCount: inventoryState.chestCount || 0,
totalSlots: inventoryState.totalSlots || 0,
usedSlots: inventoryState.usedSlots || 0,
freeSlots: (inventoryState.totalSlots || 0) - (inventoryState.usedSlots || 0),
lastUpdate,
bridgeConnected: bridgeClients.size > 0,
});
});
// Get full item list (for turtle dump target selection)
app.get('/api/integration/items', (req, res) => {
const items = inventoryState.itemList || [];
res.json({ items: items.map(i => ({ name: i.name, count: i.count })) });
});
// Get alerts (so turtles know what items are running low)
app.get('/api/integration/low-stock', (req, res) => {
const triggered = (alertsState || []).filter(a => a.triggered !== false);
res.json({ alerts: triggered });
});
// Proxy to turtle server for combined dashboard info
app.get('/api/integration/turtle-status', async (req, res) => {
if (!TURTLE_SERVER_URL) {
return res.json({ configured: false, message: 'TURTLE_SERVER_URL not configured' });
}
try {
const resp = await fetch(`${TURTLE_SERVER_URL}/api/turtles`);
const data = await resp.json();
res.json({ configured: true, ...data });
} catch (err) {
res.status(502).json({ configured: true, error: `Cannot reach turtle server: ${err.message}` });
}
});
// ========== Start Server ==========
server.listen(PORT, HOST, () => {
@@ -973,6 +1045,9 @@ server.listen(PORT, HOST, () => {
console.log(`\nBridge HTTP endpoint: http://localhost:${PORT}/api/bridge/state`);
console.log(`Bridge WebSocket: ws://localhost:${PORT}/ws/bridge`);
console.log(`Web client WebSocket: ws://localhost:${PORT}/ws`);
if (TURTLE_SERVER_URL) {
console.log(`🐢 Turtle server integration: ${TURTLE_SERVER_URL}`);
}
});
// Graceful shutdown