Skip to content

Commit a372fdc

Browse files
committed
Add needlectl ui command for serving built React UI
- Add new UI management commands: start, stop, status, build, log - Use Python's built-in HTTP server to serve static files (no Node.js required) - Build UI during installation process - Add UI access information to installation messages - Support custom port configuration for UI server - Include UI build directory in repository for easy deployment
1 parent 5c6f91c commit a372fdc

File tree

10 files changed

+334
-2
lines changed

10 files changed

+334
-2
lines changed

install.sh

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -574,10 +574,37 @@ chmod +x start-needle.sh stop-needle.sh status-needle.sh
574574

575575
print_success "Service management scripts created"
576576

577-
### Step 9: Create logs directory
577+
### Step 9: Build UI for production
578+
print_status "Building UI for production..."
579+
580+
if [ -d "ui" ]; then
581+
cd ui
582+
583+
# Check if node_modules exists
584+
if [ ! -d "node_modules" ]; then
585+
print_status "Installing UI dependencies..."
586+
npm install
587+
fi
588+
589+
# Build the UI
590+
print_status "Building React app..."
591+
npm run build
592+
593+
if [ $? -eq 0 ]; then
594+
print_success "UI built successfully"
595+
else
596+
print_warning "UI build failed, but continuing with installation"
597+
fi
598+
599+
cd ..
600+
else
601+
print_warning "UI directory not found, skipping UI build"
602+
fi
603+
604+
### Step 10: Create logs directory
578605
mkdir -p logs
579606

580-
### Step 10: Final message
607+
### Step 11: Final message
581608
print_success "🎉 Installation complete!"
582609
echo ""
583610
echo "📋 Next steps:"
@@ -590,10 +617,14 @@ echo " - Start services: needlectl service start"
590617
echo " - Stop services: needlectl service stop"
591618
echo " - Check status: needlectl service status"
592619
echo " - View logs: needlectl service log [backend|image-generator-hub|infrastructure]"
620+
echo " - Start UI: needlectl ui start"
621+
echo " - Stop UI: needlectl ui stop"
622+
echo " - UI status: needlectl ui status"
593623
echo ""
594624
echo "🌐 Access Points:"
595625
echo " - Backend API: http://localhost:8000"
596626
echo " - Image Generator: http://localhost:8010"
627+
echo " - Web UI: http://localhost:3000 (when started with 'needlectl ui start')"
597628
echo " - API Documentation: http://localhost:8000/docs"
598629
echo ""
599630
echo "📊 Configuration:"

needlectl/cli/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from cli.generator import generator_app
1212
from cli.query import query_app
1313
from cli.service import service_app
14+
from cli.ui import ui_app
1415
from cli.version import VERSION as NEEDLECTL_VERSION
1516
from docker.docker_compose_manager import DockerComposeManager
1617

@@ -21,6 +22,7 @@
2122
app.add_typer(directory_app, name="directory")
2223
app.add_typer(query_app, name="query")
2324
app.add_typer(generator_app, name="generator")
25+
app.add_typer(ui_app, name="ui")
2426

2527

2628
def get_backend_version() -> str:

needlectl/cli/ui.py

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# cli/ui.py
2+
3+
import os
4+
import subprocess
5+
import typer
6+
from pathlib import Path
7+
from typing import Optional
8+
9+
from cli.utils import print_result
10+
11+
ui_app = typer.Typer(help="Manage Needle UI (Web Interface).")
12+
13+
14+
class UIManager:
15+
"""Manages the Needle web UI."""
16+
17+
def __init__(self, needle_home: str):
18+
self.needle_home = Path(needle_home)
19+
self.ui_dir = self.needle_home / "ui"
20+
self.build_dir = self.ui_dir / "build"
21+
self.ui_pid_file = self.needle_home / "logs" / "ui.pid"
22+
23+
def _is_ui_running(self) -> bool:
24+
"""Check if the UI server is running."""
25+
if not self.ui_pid_file.exists():
26+
return False
27+
28+
try:
29+
with open(self.ui_pid_file, 'r') as f:
30+
pid = int(f.read().strip())
31+
32+
# Check if process is still running
33+
os.kill(pid, 0)
34+
return True
35+
except (OSError, ValueError, FileNotFoundError):
36+
return False
37+
38+
def _get_ui_pid(self) -> Optional[int]:
39+
"""Get the PID of the UI server if it's running."""
40+
if not self._is_ui_running():
41+
return None
42+
43+
try:
44+
with open(self.ui_pid_file, 'r') as f:
45+
return int(f.read().strip())
46+
except (OSError, ValueError, FileNotFoundError):
47+
return None
48+
49+
def start_ui(self, port: int = 3000):
50+
"""Start the UI server."""
51+
if self._is_ui_running():
52+
pid = self._get_ui_pid()
53+
typer.echo(f"UI is already running (PID: {pid})")
54+
typer.echo(f"🌐 Access the UI at: http://localhost:{port}")
55+
return True
56+
57+
# Check if build directory exists
58+
if not self.build_dir.exists():
59+
typer.echo("❌ UI build directory not found. Please build the UI first:")
60+
typer.echo(" cd ui && npm install && npm run build")
61+
return False
62+
63+
# Ensure logs directory exists
64+
self.ui_pid_file.parent.mkdir(parents=True, exist_ok=True)
65+
66+
# Start UI server using Python's built-in HTTP server
67+
log_file = self.needle_home / "logs" / "ui.log"
68+
69+
# Use Python's built-in HTTP server to serve the static files
70+
command = [
71+
"python3", "-m", "http.server", str(port), "--directory", str(self.build_dir)
72+
]
73+
74+
# Start server in background
75+
with open(log_file, 'w') as log_f:
76+
process = subprocess.Popen(
77+
command,
78+
stdout=log_f,
79+
stderr=subprocess.STDOUT,
80+
cwd=self.build_dir
81+
)
82+
83+
# Save PID
84+
with open(self.ui_pid_file, 'w') as f:
85+
f.write(str(process.pid))
86+
87+
typer.echo(f"✅ UI started (PID: {process.pid})")
88+
typer.echo(f"🌐 Access the UI at: http://localhost:{port}")
89+
typer.echo(f"📝 Logs: {log_file}")
90+
return True
91+
92+
def stop_ui(self):
93+
"""Stop the UI server."""
94+
if not self._is_ui_running():
95+
typer.echo("UI is not running")
96+
return True
97+
98+
pid = self._get_ui_pid()
99+
if pid:
100+
try:
101+
os.kill(pid, 15) # SIGTERM
102+
import time
103+
time.sleep(2)
104+
105+
# Check if still running
106+
if self._is_ui_running():
107+
os.kill(pid, 9) # SIGKILL
108+
time.sleep(1)
109+
110+
typer.echo("✅ UI stopped")
111+
except OSError:
112+
typer.echo("❌ Error stopping UI")
113+
return False
114+
finally:
115+
# Remove PID file
116+
if self.ui_pid_file.exists():
117+
self.ui_pid_file.unlink()
118+
119+
return True
120+
121+
def get_status(self):
122+
"""Get UI status."""
123+
is_running = self._is_ui_running()
124+
pid = self._get_ui_pid() if is_running else None
125+
126+
return {
127+
"running": is_running,
128+
"pid": pid,
129+
"build_exists": self.build_dir.exists(),
130+
"ui_directory": str(self.ui_dir),
131+
"build_directory": str(self.build_dir)
132+
}
133+
134+
135+
@ui_app.command("start")
136+
def ui_start(ctx: typer.Context, port: int = typer.Option(3000, "--port", "-p", help="Port to run the UI on")):
137+
"""Start the Needle web UI."""
138+
needle_home = ctx.obj.get("needle_home", ".")
139+
manager = UIManager(needle_home)
140+
manager.start_ui(port)
141+
142+
143+
@ui_app.command("stop")
144+
def ui_stop(ctx: typer.Context):
145+
"""Stop the Needle web UI."""
146+
needle_home = ctx.obj.get("needle_home", ".")
147+
manager = UIManager(needle_home)
148+
manager.stop_ui()
149+
150+
151+
@ui_app.command("status")
152+
def ui_status_cmd(ctx: typer.Context):
153+
"""Show UI status."""
154+
needle_home = ctx.obj.get("needle_home", ".")
155+
manager = UIManager(needle_home)
156+
status = manager.get_status()
157+
print_result(status, ctx.obj["output"])
158+
159+
160+
@ui_app.command("build")
161+
def ui_build(ctx: typer.Context):
162+
"""Build the UI for production."""
163+
needle_home = ctx.obj.get("needle_home", ".")
164+
ui_dir = Path(needle_home) / "ui"
165+
166+
if not ui_dir.exists():
167+
typer.echo("❌ UI directory not found. Please run this from the Needle project root.")
168+
return
169+
170+
typer.echo("🔨 Building UI for production...")
171+
172+
# Check if node_modules exists
173+
node_modules = ui_dir / "node_modules"
174+
if not node_modules.exists():
175+
typer.echo("📦 Installing dependencies...")
176+
result = subprocess.run(["npm", "install"], cwd=ui_dir, capture_output=True, text=True)
177+
if result.returncode != 0:
178+
typer.echo(f"❌ Failed to install dependencies: {result.stderr}")
179+
return
180+
181+
# Build the UI
182+
typer.echo("🏗️ Building React app...")
183+
result = subprocess.run(["npm", "run", "build"], cwd=ui_dir, capture_output=True, text=True)
184+
if result.returncode != 0:
185+
typer.echo(f"❌ Build failed: {result.stderr}")
186+
return
187+
188+
typer.echo("✅ UI built successfully!")
189+
typer.echo(f"📁 Build directory: {ui_dir / 'build'}")
190+
typer.echo("🚀 You can now start the UI with: needlectl ui start")
191+
192+
193+
@ui_app.command("log")
194+
def ui_log_cmd(ctx: typer.Context):
195+
"""Show UI logs."""
196+
needle_home = ctx.obj.get("needle_home", ".")
197+
log_file = Path(needle_home) / "logs" / "ui.log"
198+
199+
if log_file.exists():
200+
typer.echo("📝 UI Logs:")
201+
with open(log_file, 'r') as f:
202+
typer.echo(f.read())
203+
else:
204+
typer.echo("📝 No UI logs found")

ui/build/asset-manifest.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"files": {
3+
"main.css": "/static/css/main.2f1214e0.css",
4+
"main.js": "/static/js/main.dfab9501.js",
5+
"index.html": "/index.html",
6+
"main.2f1214e0.css.map": "/static/css/main.2f1214e0.css.map",
7+
"main.dfab9501.js.map": "/static/js/main.dfab9501.js.map"
8+
},
9+
"entrypoints": [
10+
"static/css/main.2f1214e0.css",
11+
"static/js/main.dfab9501.js"
12+
]
13+
}

ui/build/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Needle UI - Image Search and Management Interface"/><title>Needle UI</title><script defer="defer" src="/static/js/main.dfab9501.js"></script><link href="/static/css/main.2f1214e0.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

ui/build/static/css/main.2f1214e0.css

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/build/static/css/main.2f1214e0.css.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/build/static/js/main.dfab9501.js

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)