diff --git a/backend/app/urls.py b/backend/app/urls.py
index d4ffe8c3..78eadae5 100644
--- a/backend/app/urls.py
+++ b/backend/app/urls.py
@@ -4,4 +4,6 @@
urlpatterns = [
path('build', views.build, name='build'),
path('download', views.download, name='download'),
+ path('install_block', views.install_block, name='install_block'),
+ path('installed_blocks', views.installed_blocks, name='installed_blocks'),
]
diff --git a/backend/app/views.py b/backend/app/views.py
index d6cf79ec..9e6ac6d7 100644
--- a/backend/app/views.py
+++ b/backend/app/views.py
@@ -6,6 +6,8 @@
from django.views.decorators.csrf import csrf_exempt
import json
+import os
+from django.conf import settings
@csrf_exempt
def build(request):
@@ -20,3 +22,45 @@ def build(request):
def download(request):
return JsonResponse({'status': 'request received'})
+
+
+@csrf_exempt
+def install_block(request):
+ try:
+ data = json.loads(request.body)
+ block_name = data.get('package', {}).get('name') or data.get('name', 'Untitled')
+
+ custom_blocks_dir = os.path.join(settings.BASE_DIR, 'custom_blocks')
+ if not os.path.exists(custom_blocks_dir):
+ os.makedirs(custom_blocks_dir)
+
+ safe_filename = "".join([c for c in block_name if c.isalpha() or c.isdigit() or c in (' ')]).rstrip()
+ filepath = os.path.join(custom_blocks_dir, f"{safe_filename}.vc3")
+
+ with open(filepath, 'w') as f:
+ json.dump(data, f, indent=4)
+
+ return JsonResponse({'status': 'success', 'saved_as': os.path.basename(filepath)})
+ except Exception as e:
+ return HttpResponseBadRequest(str(e))
+
+def installed_blocks(request):
+ try:
+ custom_blocks_dir = os.path.join(settings.BASE_DIR, 'custom_blocks')
+ if not os.path.exists(custom_blocks_dir):
+ return JsonResponse({'blocks': []})
+
+ blocks = []
+ for filename in os.listdir(custom_blocks_dir):
+ if filename.endswith('.vc3'):
+ filepath = os.path.join(custom_blocks_dir, filename)
+ try:
+ with open(filepath, 'r') as f:
+ block_data = json.load(f)
+ blocks.append(block_data)
+ except Exception as e:
+ print(f"Failed to load block {filename}: {e}")
+
+ return JsonResponse({'blocks': blocks})
+ except Exception as e:
+ return HttpResponseBadRequest(str(e))
diff --git a/backend/backend/settings.py b/backend/backend/settings.py
index b4d382a6..4d05197a 100644
--- a/backend/backend/settings.py
+++ b/backend/backend/settings.py
@@ -160,6 +160,11 @@
origin.replace(':4000', ':4002')
])
+# if DEBUG:
+# CORS_ALLOWED_ORIGINS = [
+# env.str('VISUAL_CIRCUIT_FRONTEND_HOST'),
+# f"{env.str('VISUAL_CIRCUIT_FRONTEND_HOST')}:{DESIRED_PORT}",
+# ]
+
# Allow frontend to read the Content-Disposition header to get the correct .zip filename
CORS_EXPOSE_HEADERS = ['Content-Disposition']
-
diff --git a/backend/staticfiles/synthesis/main.py b/backend/staticfiles/synthesis/main.py
index 49aed123..6bcca9bf 100644
--- a/backend/staticfiles/synthesis/main.py
+++ b/backend/staticfiles/synthesis/main.py
@@ -50,40 +50,6 @@ def clean_shared_memory(signum, frame, names, processes):
sys.exit(0)
-def process_wrapper(method, inputs, outputs, parameters, sync):
- """
- Wrapper that restores sys.stdin before running a block's main().
-
- On macOS / Windows, multiprocessing uses 'spawn' which starts a fresh
- Python interpreter where sys.stdin is None. Calling input() in that
- state raises EOFError immediately (Issue #359).
-
- Strategy:
- 1. If fd 0 is a real TTY, open /dev/tty so the block gets a proper
- interactive terminal (works even inside Docker with -it).
- 2. Otherwise fall back to os.fdopen(0) for piped / non-TTY contexts.
- 3. If both fail, leave sys.stdin unchanged and let the block handle it.
- """
- import sys
- import os
-
- if os.isatty(0):
- # fd 0 is connected to a real terminal — give the block full TTY access
- try:
- sys.stdin = open('/dev/tty', 'r')
- except OSError:
- # /dev/tty not available (rare); fall back to wrapping fd 0 directly
- try:
- sys.stdin = os.fdopen(0)
- except OSError:
- pass # leave sys.stdin as-is; block may not need input()
- # If fd 0 is NOT a tty (pipe / redirect / Docker without -it),
- # we do NOT restore stdin — input() will raise EOFError, which is the
- # correct, expected behaviour in a non-interactive environment.
-
- method(inputs, outputs, parameters, sync)
-
-
def main():
"""
Main function
@@ -165,8 +131,8 @@ def main():
freq = block_data[block_id].get("frequency", 30)
processes.append(
multiprocessing.Process(
- target=process_wrapper,
- args=(method, inputs, outputs, parameters, Synchronise(1 / (freq if freq != 0 else 30)))
+ target=method,
+ args=(inputs, outputs, parameters, Synchronise(1 / (freq if freq != 0 else 30)))
)
)
diff --git a/frontend/.env.development b/frontend/.env.development
index a1f02554..82d0cf5b 100644
--- a/frontend/.env.development
+++ b/frontend/.env.development
@@ -1,3 +1 @@
-REACT_APP_BACKEND_HOST=http://localhost:8080/api/
-HOST=localhost
-DANGEROUSLY_DISABLE_HOST_CHECK=true
\ No newline at end of file
+REACT_APP_BACKEND_HOST=http://localhost:8080/api/
\ No newline at end of file
diff --git a/frontend/src/components/dialogs/project-info-dialog.tsx b/frontend/src/components/dialogs/project-info-dialog.tsx
index be892b4f..65ebe4f8 100644
--- a/frontend/src/components/dialogs/project-info-dialog.tsx
+++ b/frontend/src/components/dialogs/project-info-dialog.tsx
@@ -36,15 +36,6 @@ const ProjectInfoDialog = ({ isOpen, onResolve, onReject,
// Tags of the package (comma separated)
const [tagsInput, setTags] = useState(tags ? tags.join(', ') : '');
- const ALLOWED_CATEGORIES = [
- "Computer Vision",
- "Control Systems",
- "Locomotion",
- "Machine Learning",
- "Utilities",
- "ROS2"
- ];
-
const fileReader = new FileReader();
fileReader.onload = (event) => {
if (event.target?.result) {
@@ -145,19 +136,14 @@ const ProjectInfoDialog = ({ isOpen, onResolve, onReject,
Category
setCategory(event.target.value)}
fullWidth
- >
- {ALLOWED_CATEGORIES.map((cat) => (
-
- ))}
-
+ />
Tags (comma separated)
diff --git a/frontend/src/components/marketplace/MarketplacePanel.tsx b/frontend/src/components/marketplace/MarketplacePanel.tsx
index 4dbd5969..93d2c3d9 100644
--- a/frontend/src/components/marketplace/MarketplacePanel.tsx
+++ b/frontend/src/components/marketplace/MarketplacePanel.tsx
@@ -96,29 +96,27 @@ const MarketplacePanel: React.FC = ({ open, onClose, edit
const isValid = validateMarketplaceBlock(blockData);
if (isValid) {
- // Save to Local Storage
- let installedBlocks = [];
try {
- const stored = localStorage.getItem('vc_marketplace_blocks');
- if (stored) {
- installedBlocks = JSON.parse(stored);
- }
- } catch (e) {
- console.warn("Could not read local storage", e);
- }
-
- // Avoid duplicates by checking package name
- const exists = installedBlocks.find((b: any) => b.package && b.package.name === blockData.package.name);
- if (!exists) {
- installedBlocks.push(blockData);
- localStorage.setItem('vc_marketplace_blocks', JSON.stringify(installedBlocks));
+ const backendHost = process.env.REACT_APP_BACKEND_HOST || 'http://localhost:8000/api/';
+ const endpoint = backendHost.endsWith('/') ? `${backendHost}install_block` : `${backendHost}/install_block`;
- // Dispatch event so the MenuBar updates
+ const installResponse = await fetch(endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(blockData),
+ });
+
+ if (!installResponse.ok) {
+ throw new Error(`Backend error: ${installResponse.status}`);
+ }
window.dispatchEvent(new Event('vc_marketplace_updated'));
- alert(`Successfully added ${block.name} to your Downloads Menu!`);
- } else {
- alert(`${block.name} is already in your Downloads!`);
+ alert(`Successfully added ${block.name} to your Custom Blocks!`);
+ } catch (e) {
+ console.error("Failed to save to backend", e);
+ alert(`Failed to save ${block.name} to the backend.`);
}
} else {
alert(`Failed to validate block ${block.name}.`);
diff --git a/frontend/src/components/menu/index.tsx b/frontend/src/components/menu/index.tsx
index 9a0e55d5..a932edfc 100644
--- a/frontend/src/components/menu/index.tsx
+++ b/frontend/src/components/menu/index.tsx
@@ -38,14 +38,18 @@ function MenuBar(props: MenuBarProps) {
const [marketplaceOpen, setMarketplaceOpen] = useState(false);
const [downloads, setDownloads] = useState([]);
- const loadDownloads = () => {
+ const loadDownloads = async () => {
try {
- const stored = localStorage.getItem('vc_marketplace_blocks');
- if (stored) {
- setDownloads(JSON.parse(stored));
+ const backendHost = process.env.REACT_APP_BACKEND_HOST || 'http://localhost:8000/api/';
+ const endpoint = backendHost.endsWith('/') ? `${backendHost}installed_blocks` : `${backendHost}/installed_blocks`;
+
+ const response = await fetch(endpoint);
+ if (response.ok) {
+ const data = await response.json();
+ setDownloads(data.blocks || []);
}
} catch (e) {
- console.error("Failed to load downloads", e);
+ console.error("Failed to load downloads from backend", e);
}
};