AISCMS / backup.py
Predator911's picture
Create backup.py
b846ca1 verified
Raw
History Blame Contribute Delete
33.7 kB
import streamlit as st
import pandas as pd
import numpy as np
import requests
import json
from datetime import datetime, timedelta
import plotly.express as px
import plotly.graph_objects as go
from io import StringIO
import time
# API Configuration
API_BASE_URL = "https://aiscms.onrender.com/api" # Update this with your actual API URL
# Set page configuration
st.set_page_config(
page_title="Space Cargo Management",
page_icon="🚀",
layout="wide",
initial_sidebar_state="expanded"
)
# Custom CSS
st.markdown("""
<style>
.main-header {
font-size: 2.5rem;
font-weight: 700;
color: #1E3A8A;
}
.section-header {
font-size: 1.8rem;
font-weight: 600;
color: #2563EB;
margin-top: 1rem;
}
.card {
padding: 1.5rem;
border-radius: 0.5rem;
background-color: #F3F4F6;
margin-bottom: 1rem;
}
.status-card {
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 1rem;
text-align: center;
}
.success-card {
background-color: #D1FAE5;
color: #065F46;
}
.warning-card {
background-color: #FEF3C7;
color: #92400E;
}
.error-card {
background-color: #FEE2E2;
color: #991B1B;
}
.info-text {
font-size: 0.9rem;
color: #6B7280;
}
.step-card {
padding: 0.75rem;
border-radius: 0.25rem;
margin-bottom: 0.5rem;
background-color: #EFF6FF;
}
</style>
""", unsafe_allow_html=True)
# Helper functions
def call_api(endpoint, method="GET", data=None, files=None):
"""Generic API calling function with error handling"""
url = f"{API_BASE_URL}/{endpoint}"
try:
if method == "GET":
response = requests.get(url, params=data)
elif method == "POST":
if files:
response = requests.post(url, data=data, files=files)
else:
response = requests.post(url, json=data)
else:
st.error(f"Unsupported method: {method}")
return None
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
st.error(f"API Error: {str(e)}")
return None
def format_position(position):
"""Format position data for display"""
if not position:
return "Unknown"
try:
start = position.get("start", {})
end = position.get("end", {})
return f"W: {start.get('width', 0):.1f}-{end.get('width', 0):.1f}, " + \
f"D: {start.get('depth', 0):.1f}-{end.get('depth', 0):.1f}, " + \
f"H: {start.get('height', 0):.1f}-{end.get('height', 0):.1f}"
except (KeyError, AttributeError):
return "Invalid format"
# Tabs for different functionalities
def main():
st.markdown("<h1 class='main-header'>Space Cargo Management System</h1>", unsafe_allow_html=True)
tabs = st.tabs([
"Dashboard",
"Add Cargo",
"Search & Retrieve",
"Storage Placement",
"Waste Management",
"Simulation",
"Logs & Reports"
])
# Dashboard tab
with tabs[0]:
display_dashboard()
# Add Cargo tab
with tabs[1]:
add_cargo_form()
# Search & Retrieve tab
with tabs[2]:
search_retrieve_tab()
# Storage Placement tab
with tabs[3]:
storage_placement_tab()
# Waste Management tab
with tabs[4]:
waste_management_tab()
# Simulation tab
with tabs[5]:
simulation_tab()
# Logs & Reports tab
with tabs[6]:
logs_reports_tab()
def display_dashboard():
st.markdown("<h2 class='section-header'>Cargo Status Dashboard</h2>", unsafe_allow_html=True)
# Load dashboard data
col1, col2, col3 = st.columns(3)
with col1:
# Placeholder function to simulate getting item counts
# In production, this would be an API call to a new endpoint that returns counts
st.markdown("<div class='status-card success-card'><h3>Active Items</h3><h2>235</h2></div>", unsafe_allow_html=True)
with col2:
st.markdown("<div class='status-card warning-card'><h3>Items Near Expiry</h3><h2>18</h2></div>", unsafe_allow_html=True)
with col3:
st.markdown("<div class='status-card error-card'><h3>Waste Items</h3><h2>7</h2></div>", unsafe_allow_html=True)
# Display container utilization (mockup)
st.markdown("<h3>Container Utilization</h3>", unsafe_allow_html=True)
# Sample data for visualization
container_data = {
'Container': ['A-101', 'B-202', 'C-303', 'D-404', 'E-505'],
'Capacity': [100, 100, 100, 100, 100],
'Used': [75, 45, 90, 30, 60]
}
df = pd.DataFrame(container_data)
# Calculate utilization percentage
df['Utilization'] = df['Used'] / df['Capacity'] * 100
# Create a horizontal bar chart
fig = px.bar(df, y='Container', x='Utilization',
orientation='h', title='Container Utilization (%)',
labels={'Utilization': 'Space Used (%)'},
color='Utilization',
color_continuous_scale=[(0, 'green'), (0.7, 'yellow'), (1, 'red')])
fig.update_layout(xaxis_range=[0, 100])
st.plotly_chart(fig, use_container_width=True)
# Most retrieved items
st.markdown("<h3>Most Retrieved Items</h3>", unsafe_allow_html=True)
# Sample data
retrieval_data = {
'Item': ['Medical Supplies', 'Food Rations', 'Water', 'Tools', 'Batteries'],
'Retrievals': [42, 35, 28, 22, 15]
}
df_retrievals = pd.DataFrame(retrieval_data)
fig2 = px.bar(df_retrievals, x='Item', y='Retrievals', title='Top Retrieved Items')
st.plotly_chart(fig2, use_container_width=True)
def add_cargo_form():
st.markdown("<h2 class='section-header'>Add New Cargo</h2>", unsafe_allow_html=True)
with st.form("add_cargo_form"):
col1, col2 = st.columns(2)
with col1:
item_id = st.text_input("Item ID", help="Unique identifier for the cargo item")
name = st.text_input("Item Name", help="Name or description of the cargo item")
priority = st.slider("Priority (1-100)", min_value=1, max_value=100, value=50,
help="Higher priority items are placed more accessibly")
preferred_zone = st.selectbox("Preferred Storage Zone",
["Crew Quarters", "Laboratory", "Engineering", "Medical", "Food Storage"])
with col2:
width = st.number_input("Width (cm)", min_value=0.1, max_value=500.0, value=20.0, step=0.1)
depth = st.number_input("Depth (cm)", min_value=0.1, max_value=500.0, value=20.0, step=0.1)
height = st.number_input("Height (cm)", min_value=0.1, max_value=500.0, value=20.0, step=0.1)
mass = st.number_input("Mass (kg)", min_value=0.1, max_value=1000.0, value=5.0, step=0.1)
usage_limit = st.number_input("Usage Limit (Optional)", min_value=0, value=0,
help="Number of times the item can be used before becoming waste. Leave 0 for unlimited.")
submit_button = st.form_submit_button("Add Cargo Item")
if submit_button:
# Prepare payload
payload = {
"itemId": item_id,
"name": name,
"width": width,
"depth": depth,
"height": height,
"mass": mass,
"priority": priority,
"preferredZone": preferred_zone
}
if usage_limit > 0:
payload["usageLimit"] = usage_limit
# Call API
response = call_api("add_cargo", method="POST", data=payload)
if response:
st.success(f"Cargo item added successfully! Item ID: {response.get('item_id')}")
else:
st.error("Failed to add cargo item. Please check the logs.")
# Bulk import section
st.markdown("<h3>Bulk Import Items</h3>", unsafe_allow_html=True)
uploaded_file = st.file_uploader("Upload CSV file with items", type=["csv"])
if uploaded_file is not None:
if st.button("Import Items"):
files = {"file": uploaded_file}
response = call_api("import/items", method="POST", files=files)
if response and response.get("success"):
st.success(f"Successfully imported {response.get('inserted')} items!")
else:
st.error("Failed to import items. Please check the file format.")
# Update the search_retrieve_tab function to correctly format the search payload
def search_retrieve_tab():
st.markdown("<h2 class='section-header'>Search & Retrieve Items</h2>", unsafe_allow_html=True)
# Search form with required fields
col1, col2 = st.columns(2)
with col1:
item_id = st.text_input("Item ID", key="search_item_id")
with col2:
user_id = st.text_input("Your User ID", value="astronaut1", key="search_user_id")
search_button = st.button("Search")
if search_button and item_id:
# Create properly formatted payload with timestamp
search_payload = {
"itemId": item_id,
"userId": user_id,
"timestamp": datetime.now().isoformat()
}
# Call API with POST method and the correct payload
response = call_api("search", method="POST", data=search_payload)
if response and response.get("success") and response.get("found"):
item_details = response.get("itemDetails", {})
st.success(f"Item found: **{item_details.get('name')}**")
col1, col2 = st.columns(2)
with col1:
st.markdown("<div class='card'>", unsafe_allow_html=True)
st.write(f"**Container:** {item_details.get('containerId')}")
st.write(f"**Zone:** {item_details.get('zone')}")
st.write(f"**Position:** {format_position(item_details.get('position'))}")
st.markdown("</div>", unsafe_allow_html=True)
# Display retrieval steps
steps = response.get("retrievalSteps", [])
if steps:
with col2:
st.markdown("<div class='card'>", unsafe_allow_html=True)
st.write(f"**Retrieval Steps:** {len(steps)} steps")
st.write(f"**Obstructing Items:** {len([s for s in steps if s.get('action') == 'remove'])}")
st.markdown("</div>", unsafe_allow_html=True)
st.markdown("<h3>Retrieval Plan</h3>", unsafe_allow_html=True)
for step in steps:
step_class = ""
if step.get("action") == "remove":
step_class = "warning-card"
elif step.get("action") == "retrieve":
step_class = "success-card"
else:
step_class = "info-card"
st.markdown(f"""
<div class='step-card {step_class}'>
<b>Step {step.get('step')}:</b> {step.get('action').title()} -
{step.get('itemName')} (ID: {step.get('itemId')})
</div>
""", unsafe_allow_html=True)
# Add retrieve button
if st.button("Confirm Retrieval"):
# Call retrieve API
retrieve_data = {
"itemId": item_id,
"userId": user_id,
"timestamp": datetime.now().isoformat()
}
retrieve_response = call_api("retrieve", method="POST", data=retrieve_data)
if retrieve_response and retrieve_response.get("success"):
st.success("Item retrieval recorded successfully!")
else:
st.error("Failed to record item retrieval. Please try again.")
elif response and response.get("found") is False:
st.error("Item not found in inventory.")
else:
st.error("Error occurred during search. Please check the API connection.")
st.error(f"Response: {response}")
# Rest of the function remains the same
# Manual placement section
st.markdown("<h3>Manual Placement</h3>", unsafe_allow_html=True)
st.markdown("<p class='info-text'>Use this form after retrieving an item to place it back into storage.</p>", unsafe_allow_html=True)
with st.form("place_item_form"):
col1, col2 = st.columns(2)
with col1:
place_item_id = st.text_input("Item ID", key="place_item_id")
place_user_id = st.text_input("Your User ID", value="astronaut1", key="place_user_id")
place_container_id = st.text_input("Container ID")
with col2:
x_pos = st.number_input("Start Width (cm)", min_value=0.0, step=0.1)
y_pos = st.number_input("Start Depth (cm)", min_value=0.0, step=0.1)
z_pos = st.number_input("Start Height (cm)", min_value=0.0, step=0.1)
width = st.number_input("Width (cm)", min_value=0.1, step=0.1, value=20.0)
depth = st.number_input("Depth (cm)", min_value=0.1, step=0.1, value=20.0)
height = st.number_input("Height (cm)", min_value=0.1, step=0.1, value=20.0)
submit_place = st.form_submit_button("Place Item")
if submit_place:
place_data = {
"itemId": place_item_id,
"userId": place_user_id,
"timestamp": datetime.now().isoformat(),
"containerId": place_container_id,
"position": {
"start": {"width": x_pos, "depth": y_pos, "height": z_pos},
"end": {"width": x_pos + width, "depth": y_pos + depth, "height": z_pos + height}
}
}
place_response = call_api("place", method="POST", data=place_data)
if place_response and place_response.get("success"):
st.success("Item placed successfully!")
else:
st.error("Failed to place item. Please check the details and try again.")
# Update the storage_placement_tab function to include a real 3D visualization
def storage_placement_tab():
st.markdown("<h2 class='section-header'>Storage Placement Optimization</h2>", unsafe_allow_html=True)
# Upload containers
st.markdown("<h3>Step 1: Configure Containers</h3>", unsafe_allow_html=True)
container_upload = st.file_uploader("Upload container configuration CSV", type=["csv"])
if container_upload is not None:
try:
container_df = pd.read_csv(container_upload)
st.dataframe(container_df)
if st.button("Import Containers"):
files = {"file": container_upload}
container_response = call_api("import/containers", method="POST", files=files)
if container_response and container_response.get("success"):
st.success(f"Successfully imported {container_response.get('inserted')} containers!")
else:
st.error("Failed to import containers.")
except Exception as e:
st.error(f"Error reading container file: {str(e)}")
# Manual container entry
st.markdown("<h3>Or Add Container Manually</h3>", unsafe_allow_html=True)
with st.form("add_container_form"):
col1, col2 = st.columns(2)
with col1:
container_id = st.text_input("Container ID")
zone = st.selectbox("Storage Zone",
["Crew Quarters", "Laboratory", "Engineering", "Medical", "Food Storage"])
with col2:
container_width = st.number_input("Width (cm)", min_value=1.0, value=100.0, step=1.0)
container_depth = st.number_input("Depth (cm)", min_value=1.0, value=100.0, step=1.0)
container_height = st.number_input("Height (cm)", min_value=1.0, value=100.0, step=1.0)
submit_container = st.form_submit_button("Add Container")
if submit_container:
container_data = {
"containerId": container_id,
"zone": zone,
"dimensions": {
"width": container_width,
"depth": container_depth,
"height": container_height
}
}
# Call API to add a single container
container_response = call_api("containers/add", method="POST", data=container_data)
if container_response and container_response.get("success"):
st.success(f"Container {container_id} added successfully!")
else:
st.error("Failed to add container. Please check the API connection.")
# Placement optimization
st.markdown("<h3>Step 2: Run Placement Optimization</h3>", unsafe_allow_html=True)
# Run optimization
if st.button("Run Optimal Placement"):
with st.spinner("Optimizing placement..."):
# Get containers and items data for visualization
containers_response = call_api("containers/list", method="GET")
items_response = call_api("items/list", method="GET")
# For demonstration purposes, use sample data if API fails
if not containers_response or not items_response:
st.warning("Using sample data for demonstration - couldn't fetch live data")
# Sample container data
containers = [
{
"containerId": "CONT-A101",
"zone": "Medical",
"dimensions": {"width": 100, "depth": 100, "height": 100}
},
{
"containerId": "CONT-B202",
"zone": "Food Storage",
"dimensions": {"width": 120, "depth": 80, "height": 90}
}
]
# Sample item placement data
placements = [
{
"itemId": "ITEM-001",
"name": "First Aid Kit",
"containerId": "CONT-A101",
"position": {
"start": {"width": 0, "depth": 0, "height": 0},
"end": {"width": 20, "depth": 20, "height": 10}
}
},
{
"itemId": "ITEM-002",
"name": "Medicine Box",
"containerId": "CONT-A101",
"position": {
"start": {"width": 20, "depth": 0, "height": 0},
"end": {"width": 40, "depth": 20, "height": 15}
}
},
{
"itemId": "ITEM-003",
"name": "Food Package",
"containerId": "CONT-B202",
"position": {
"start": {"width": 0, "depth": 0, "height": 0},
"end": {"width": 30, "depth": 25, "height": 20}
}
}
]
else:
containers = containers_response.get("containers", [])
placements = items_response.get("items", [])
# Create 3D visualization using Plotly
st.success("Placement optimization completed!")
# Container selection for visualization
container_ids = [container["containerId"] for container in containers]
selected_container = st.selectbox("Select container to visualize:", container_ids)
# Filter items for selected container
container_items = [item for item in placements if item.get("containerId") == selected_container]
# Find selected container details
selected_container_data = next((c for c in containers if c["containerId"] == selected_container), None)
if selected_container_data:
# Create 3D visualization
fig = go.Figure()
# Add container as a transparent box
cont_width = selected_container_data["dimensions"]["width"]
cont_depth = selected_container_data["dimensions"]["depth"]
cont_height = selected_container_data["dimensions"]["height"]
# Draw container as a wireframe box
fig.add_trace(go.Mesh3d(
x=[0, cont_width, cont_width, 0, 0, cont_width, cont_width, 0],
y=[0, 0, cont_depth, cont_depth, 0, 0, cont_depth, cont_depth],
z=[0, 0, 0, 0, cont_height, cont_height, cont_height, cont_height],
i=[0, 0, 0, 1, 4, 4, 4, 5],
j=[1, 2, 3, 2, 5, 6, 7, 6],
k=[2, 3, 0, 3, 6, 7, 4, 7],
opacity=0.2,
color='lightblue',
name=f"Container {selected_container}"
))
# Add each item as a colored box
colors = px.colors.qualitative.Plotly # Get a list of colors
for i, item in enumerate(container_items):
color_idx = i % len(colors)
# Get item position coordinates
start = item["position"]["start"]
end = item["position"]["end"]
# Create a box for each item
x = [start["width"], end["width"], end["width"], start["width"], start["width"], end["width"], end["width"], start["width"]]
y = [start["depth"], start["depth"], end["depth"], end["depth"], start["depth"], start["depth"], end["depth"], end["depth"]]
z = [start["height"], start["height"], start["height"], start["height"], end["height"], end["height"], end["height"], end["height"]]
i_indices = [0, 0, 0, 1, 4, 4, 4, 5]
j_indices = [1, 2, 3, 2, 5, 6, 7, 6]
k_indices = [2, 3, 0, 3, 6, 7, 4, 7]
fig.add_trace(go.Mesh3d(
x=x, y=y, z=z,
i=i_indices, j=j_indices, k=k_indices,
opacity=0.7,
color=colors[color_idx],
name=f"{item.get('name', 'Item')} ({item.get('itemId', '')})"
))
# Update layout
fig.update_layout(
title=f"3D Visualization of Container {selected_container}",
scene=dict(
xaxis_title="Width (cm)",
yaxis_title="Depth (cm)",
zaxis_title="Height (cm)",
aspectmode='data'
),
height=700,
margin=dict(l=0, r=0, b=0, t=30)
)
st.plotly_chart(fig, use_container_width=True)
# Item listing for the container
st.markdown("<h3>Items in Container</h3>", unsafe_allow_html=True)
if container_items:
# Create a dataframe for items
items_df = pd.DataFrame([
{
"Item ID": item.get("itemId"),
"Name": item.get("name", "Unknown"),
"Position": format_position(item.get("position")),
"Priority": item.get("priority", "N/A")
} for item in container_items
])
st.dataframe(items_df)
else:
st.info("No items in this container")
else:
st.error("Container data not found")
def waste_management_tab():
st.markdown("<h2 class='section-header'>Waste Management</h2>", unsafe_allow_html=True)
# Identify waste
col1, col2 = st.columns(2)
with col1:
if st.button("Identify Waste Items"):
with st.spinner("Scanning for waste items..."):
waste_response = call_api("waste/identify", method="GET")
if waste_response:
if waste_response:
st.success(f"Found {len(waste_response)} waste items")
# Display waste items
if waste_response:
for item in waste_response:
reason_class = "error-card" if item.get("reason") == "Expired" else "warning-card"
st.markdown(f"""
<div class='step-card {reason_class}'>
<b>{item.get('name')} (ID: {item.get('itemId')})</b><br>
Reason: {item.get('reason')}<br>
Container: {item.get('containerId')}
</div>
""", unsafe_allow_html=True)
else:
st.info("No waste items found")
else:
st.error("Failed to identify waste items")
with col2:
st.markdown("<h3>Return Plan</h3>", unsafe_allow_html=True)
max_weight = st.number_input("Maximum Return Weight (kg)", min_value=1.0, value=100.0, step=5.0)
if st.button("Generate Return Plan"):
# Call return plan API
return_plan_response = call_api("waste/return-plan", method="POST", data={"maxWeight": max_weight})
if return_plan_response and return_plan_response.get("success"):
st.success(f"Return plan generated!")
st.write(f"Total Weight: {return_plan_response.get('totalWeight', 0):.2f} kg")
st.write(f"Total Volume: {return_plan_response.get('totalVolume', 0):.2f} m³")
# Display steps
steps = return_plan_response.get("steps", [])
if steps:
for step in steps:
st.markdown(f"""
<div class='step-card'>
<b>Step {step.get('step')}:</b> Return {step.get('name')} (ID: {step.get('itemId')})<br>
Mass: {step.get('mass', 0):.2f} kg, Container: {step.get('containerId')}
</div>
""", unsafe_allow_html=True)
# Complete undocking option
container_id = st.text_input("Container ID for undocking", value=steps[0].get('containerId', ''))
if st.button("Complete Undocking"):
undock_response = call_api("waste/complete-undocking", method="POST", data={"containerId": container_id})
if undock_response and undock_response.get("success"):
st.success(f"Undocking completed! {undock_response.get('itemsRemoved', 0)} items removed.")
else:
st.error("Failed to complete undocking process.")
else:
st.info("No waste items to return")
else:
st.error("Failed to generate return plan")
def simulation_tab():
st.markdown("<h2 class='section-header'>Time Simulation</h2>", unsafe_allow_html=True)
st.markdown("<p class='info-text'>Simulate the passage of time to test expiration and usage depletion.</p>", unsafe_allow_html=True)
col1, col2 = st.columns(2)
with col1:
# Simple time advancement
st.markdown("<h3>Advance Time</h3>", unsafe_allow_html=True)
days = st.number_input("Days to Advance", min_value=1, value=1)
if st.button("Advance Time"):
simulation_data = {
"numOfDays": days,
"itemsToBeUsedPerDay": [] # Empty usage for simple time advancement
}
sim_response = call_api("simulate/day", method="POST", data=simulation_data)
if sim_response and sim_response.get("success"):
st.success(f"Time advanced to {sim_response.get('newDate')}")
expired = sim_response.get("expiredItems", [])
depleted = sim_response.get("depletedItems", [])
if expired:
st.warning(f"{len(expired)} items expired")
st.write("Expired item IDs:", ", ".join(expired))
if depleted:
st.warning(f"{len(depleted)} items depleted")
st.write("Depleted item IDs:", ", ".join(depleted))
if not expired and not depleted:
st.info("No items expired or depleted during this time period")
else:
st.error("Failed to advance time")
with col2:
# Advanced simulation with usage
st.markdown("<h3>Simulate Usage</h3>", unsafe_allow_html=True)
# Sample data for item usage simulation
st.text_area("Item Usage JSON", value="""[
{"day": 1, "usages": [{"itemId": "ITEM-001"}, {"itemId": "ITEM-002"}]},
{"day": 2, "usages": [{"itemId": "ITEM-001"}, {"itemId": "ITEM-003"}]}
]""", height=200)
if st.button("Simulate Usage"):
st.info("This would simulate usage of specific items over time")
# Implementation would parse the JSON and call the simulate API
def logs_reports_tab():
st.markdown("<h2 class='section-header'>Logs & Reports</h2>", unsafe_allow_html=True)
# Date range selection
col1, col2 = st.columns(2)
with col1:
start_date = st.date_input("Start Date", value=datetime.now() - timedelta(days=7))
with col2:
end_date = st.date_input("End Date", value=datetime.now())
# Filters
col1, col2, col3 = st.columns(3)
with col1:
item_id_filter = st.text_input("Item ID (Optional)", key="log_item_id")
with col2:
user_id_filter = st.text_input("User ID (Optional)")
with col3:
action_type = st.selectbox("Action Type", ["", "retrieval", "placement", "add_cargo"])
if st.button("Get Logs"):
# API parameters
log_params = {
"startDate": start_date.isoformat(),
"endDate": end_date.isoformat()
}
if item_id_filter:
log_params["itemId"] = item_id_filter
if user_id_filter:
log_params["userId"] = user_id_filter
if action_type:
log_params["actionType"] = action_type
logs_response = call_api("logs", method="GET", data=log_params)
if logs_response:
# Convert to DataFrame for display
logs_df = pd.DataFrame(logs_response)
if not logs_df.empty:
st.success(f"Found {len(logs_df)} log entries")
st.dataframe(logs_df)
# Download option
csv = logs_df.to_csv(index=False)
st.download_button(
label="Download Logs CSV",
data=csv,
file_name=f"cargo_logs_{start_date}_to_{end_date}.csv",
mime="text/csv"
)
# Simple visualization
if 'actionType' in logs_df.columns:
action_counts = logs_df['actionType'].value_counts().reset_index()
action_counts.columns = ['Action', 'Count']
fig = px.pie(action_counts, values='Count', names='Action',
title='Action Distribution')
st.plotly_chart(fig)
else:
st.info("No logs found for the selected criteria")
else:
st.error("Failed to retrieve logs")
# Export current arrangement
st.markdown("<h3>Export Current Arrangement</h3>", unsafe_allow_html=True)
if st.button("Export Storage Arrangement"):
# Link to export endpoint (direct file download)
st.markdown(f"[Download Current Arrangement CSV]({API_BASE_URL}/export/arrangement)")
st.info("Click the link above to download the current storage arrangement")
if __name__ == "__main__":
main()