Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 86 additions & 43 deletions setup/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
from pathlib import Path

# Fix Windows console encoding to support Unicode
if sys.platform == 'win32':
IS_WINDOWS = sys.platform == 'win32'
if IS_WINDOWS:
try:
# Try to set UTF-8 encoding for stdout/stderr
import io
Expand Down Expand Up @@ -53,21 +54,63 @@ class Colors:
BOLD = '\033[1m'
NC = '\033[0m' # No Color

# Platform-safe emoji/unicode characters
class Icons:
"""Platform-safe icons that work on all terminals."""
if IS_WINDOWS:
ROCKET = ">"
PENCIL = "-"
USER = "*"
CHECK = "OK"
CHECKMARK = "[OK]"
CROSS = "X"
WARNING = "!"
ERROR = "ERROR"
SEARCH = "?"
BUILDING = "+"
STOP = "[]"
RESTART = "@"
CLIPBOARD = "#"
WRENCH = "*"
SEPARATOR = "=" * 44
BOX_TOP = "+" + ("=" * 52) + "+"
BOX_MID = "|" + (" " * 52) + "|"
BOX_BTM = "+" + ("=" * 52) + "+"
else:
ROCKET = "🚀"
PENCIL = "📝"
USER = "👤"
CHECK = "✓"
CHECKMARK = "✅"
CROSS = "✗"
WARNING = "⚠️"
ERROR = "❌"
SEARCH = "🔍"
BUILDING = "🏗️"
STOP = "🛑"
RESTART = "🔄"
CLIPBOARD = "📋"
WRENCH = "🔧"
SEPARATOR = "━" * 44
BOX_TOP = "╔" + ("═" * 52) + "╗"
BOX_MID = "║" + (" " * 52) + "║"
BOX_BTM = "╚" + ("═" * 52) + "╝"

def print_color(color: str, message: str):
"""Print colored message."""
print(f"{color}{message}{Colors.NC}")

def print_header():
"""Print startup header."""
print()
print_color(Colors.BOLD, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
print_color(Colors.BOLD, f"🚀 {APP_DISPLAY_NAME} Quick Start")
print_color(Colors.BOLD, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
print_color(Colors.BOLD, Icons.SEPARATOR)
print_color(Colors.BOLD, f"{Icons.ROCKET} {APP_DISPLAY_NAME} Quick Start")
print_color(Colors.BOLD, Icons.SEPARATOR)
print()

def prompt_for_config() -> dict:
"""Prompt user for configuration in interactive mode."""
print_color(Colors.BLUE, "📝 Configuration")
print_color(Colors.BLUE, f"{Icons.PENCIL} Configuration")
print()

# Environment name
Expand All @@ -94,7 +137,7 @@ def prompt_for_config() -> dict:

def prompt_for_admin() -> dict:
"""Prompt user for admin credentials."""
print_color(Colors.BLUE, "👤 Admin Account")
print_color(Colors.BLUE, f"{Icons.USER} Admin Account")
print()

# Email
Expand Down Expand Up @@ -124,12 +167,12 @@ def check_docker():
text=True
)
if result.returncode == 0:
print_color(Colors.GREEN, f" Docker found: {result.stdout.strip()}")
print_color(Colors.GREEN, f"{Icons.CHECKMARK} Docker found: {result.stdout.strip()}")
return True
except FileNotFoundError:
pass

print_color(Colors.RED, "❌ Docker not found. Please install Docker Desktop.")
print_color(Colors.RED, f"{Icons.ERROR} Docker not found. Please install Docker Desktop.")
return False

def generate_env_file(env_name: str, port_offset: int, env_file: Path, secrets_file: Path, dev_mode: bool = False, quick_mode: bool = False):
Expand All @@ -142,8 +185,8 @@ def generate_env_file(env_name: str, port_offset: int, env_file: Path, secrets_f
if conflicts:
if quick_mode:
# In quick mode, automatically find available ports
print_color(Colors.YELLOW, f"⚠️ Port conflict detected: {conflicts}")
print_color(Colors.BLUE, "🔍 Auto-finding available ports...")
print_color(Colors.YELLOW, f"{Icons.WARNING} Port conflict detected: {conflicts}")
print_color(Colors.BLUE, f"{Icons.SEARCH} Auto-finding available ports...")

# Try incrementing port offset until we find available ports (max 100 attempts)
for _ in range(100):
Expand All @@ -153,13 +196,13 @@ def generate_env_file(env_name: str, port_offset: int, env_file: Path, secrets_f

all_available, conflicts = validate_ports([backend_port, webui_port])
if all_available:
print_color(Colors.GREEN, f" Found available ports (offset: {port_offset})")
print_color(Colors.GREEN, f"{Icons.CHECKMARK} Found available ports (offset: {port_offset})")
break
else:
print_color(Colors.RED, "❌ Could not find available ports after 100 attempts")
print_color(Colors.RED, f"{Icons.ERROR} Could not find available ports after 100 attempts")
return None
else:
print_color(Colors.RED, f" Port conflict: {conflicts}")
print_color(Colors.RED, f"{Icons.ERROR} Port conflict: {conflicts}")
return None

# Find available Redis database
Expand Down Expand Up @@ -214,7 +257,7 @@ def generate_env_file(env_name: str, port_offset: int, env_file: Path, secrets_f
env_file.write_text(env_content)
os.chmod(env_file, 0o600)

print_color(Colors.GREEN, "✅ Environment configured")
print_color(Colors.GREEN, f"{Icons.CHECKMARK} Environment configured")
print(f" Name: {env_name}")
print(f" Project: {compose_project_name}")
print(f" Backend: {backend_port}")
Expand All @@ -225,9 +268,9 @@ def generate_env_file(env_name: str, port_offset: int, env_file: Path, secrets_f
# Ensure secrets.yaml exists
created_new, _ = ensure_secrets_yaml(str(secrets_file))
if created_new:
print_color(Colors.GREEN, f"Secrets file written: {secrets_file}")
print_color(Colors.GREEN, f"Secrets file written: {Icons.CHECKMARK} {secrets_file}")
else:
print(f"Secrets already configured: {secrets_file}")
print(f"Secrets already configured: {Icons.CHECKMARK} {secrets_file}")

return {
"backend_port": backend_port,
Expand Down Expand Up @@ -256,22 +299,22 @@ def compose_up(dev_mode: bool, build: bool = False) -> bool:
"""Start containers (optionally with rebuild)."""
# Ensure Docker networks exist
if not ensure_networks():
print_color(Colors.RED, "❌ Failed to create required Docker networks (ushadow-network, infra-network)")
print_color(Colors.RED, f"{Icons.ERROR} Failed to create required Docker networks (ushadow-network, infra-network)")
print_color(Colors.YELLOW, " Make sure Docker is running and you have permissions to create networks")
return False

# Check/start infrastructure
infra_running = check_infrastructure_running()
if not infra_running:
print_color(Colors.YELLOW, "🏗️ Starting infrastructure...")
print_color(Colors.YELLOW, f"{Icons.BUILDING} Starting infrastructure...")
success, message = start_infrastructure(INFRA_COMPOSE_FILE, INFRA_PROJECT_NAME)
if not success:
print_color(Colors.RED, f" {message}")
print_color(Colors.RED, f"{Icons.ERROR} {message}")
return False

mode_label = "dev" if dev_mode else "prod"
action = "Building and starting" if build else "Starting"
print_color(Colors.BLUE, f"🚀 {action} {APP_DISPLAY_NAME} ({mode_label} mode)...")
print_color(Colors.BLUE, f"{Icons.ROCKET} {action} {APP_DISPLAY_NAME} ({mode_label} mode)...")

cmd = get_compose_cmd(dev_mode) + ["up", "-d"]
if build:
Expand All @@ -280,42 +323,42 @@ def compose_up(dev_mode: bool, build: bool = False) -> bool:
# Use native path format for cwd (subprocess handles it correctly)
result = subprocess.run(cmd, cwd=str(PROJECT_ROOT))
if result.returncode != 0:
print_color(Colors.RED, "❌ Failed to start application")
print_color(Colors.RED, f"{Icons.ERROR} Failed to start application")
return False

print_color(Colors.GREEN, "✅ Done")
print_color(Colors.GREEN, f"{Icons.CHECKMARK} Done")
return True


def compose_down(dev_mode: bool) -> bool:
"""Stop containers."""
mode_label = "dev" if dev_mode else "prod"
print_color(Colors.BLUE, f"🛑 Stopping {APP_DISPLAY_NAME} ({mode_label} mode)...")
print_color(Colors.BLUE, f"{Icons.STOP} Stopping {APP_DISPLAY_NAME} ({mode_label} mode)...")

cmd = get_compose_cmd(dev_mode) + ["down"]
result = subprocess.run(cmd, cwd=str(PROJECT_ROOT))

if result.returncode != 0:
print_color(Colors.RED, "❌ Failed to stop application")
print_color(Colors.RED, f"{Icons.ERROR} Failed to stop application")
return False

print_color(Colors.GREEN, "✅ Stopped")
print_color(Colors.GREEN, f"{Icons.CHECKMARK} Stopped")
return True


def compose_restart(dev_mode: bool) -> bool:
"""Restart containers."""
mode_label = "dev" if dev_mode else "prod"
print_color(Colors.BLUE, f"🔄 Restarting {APP_DISPLAY_NAME} ({mode_label} mode)...")
print_color(Colors.BLUE, f"{Icons.RESTART} Restarting {APP_DISPLAY_NAME} ({mode_label} mode)...")

cmd = get_compose_cmd(dev_mode) + ["restart"]
result = subprocess.run(cmd, cwd=str(PROJECT_ROOT))

if result.returncode != 0:
print_color(Colors.RED, "❌ Failed to restart application")
print_color(Colors.RED, f"{Icons.ERROR} Failed to restart application")
return False

print_color(Colors.GREEN, "✅ Restarted")
print_color(Colors.GREEN, f"{Icons.CHECKMARK} Restarted")
return True


Expand All @@ -333,23 +376,23 @@ def wait_and_open(backend_port: int, webui_port: int, open_browser: bool):

print()
if healthy:
print_color(Colors.GREEN + Colors.BOLD, f" {APP_DISPLAY_NAME} is ready!")
print_color(Colors.GREEN + Colors.BOLD, f"{Icons.CHECKMARK} {APP_DISPLAY_NAME} is ready!")
else:
print_color(Colors.YELLOW, "⚠️ Backend is starting... (may take a moment)")
print_color(Colors.YELLOW, f"{Icons.WARNING} Backend is starting... (may take a moment)")

# Print success box
print()
print_color(Colors.BOLD, "╔════════════════════════════════════════════════════╗")
print_color(Colors.BOLD, "║ ║")
print_color(Colors.BOLD, f"║ 🚀 {APP_DISPLAY_NAME} is ready! ║")
print_color(Colors.BOLD, "║ ║")
print_color(Colors.BOLD, Icons.BOX_TOP)
print_color(Colors.BOLD, Icons.BOX_MID)
print_color(Colors.BOLD, f"║ {Icons.ROCKET} {APP_DISPLAY_NAME} is ready! ║")
print_color(Colors.BOLD, Icons.BOX_MID)
print_color(Colors.BOLD, f"║ http://localhost:{webui_port} ║")
print_color(Colors.BOLD, "║ ║")
print_color(Colors.BOLD, "╚════════════════════════════════════════════════════╝")
print_color(Colors.BOLD, Icons.BOX_MID)
print_color(Colors.BOLD, Icons.BOX_BTM)
print()

# First-time setup instructions
print_color(Colors.BOLD, "📋 First-Time Setup:")
print_color(Colors.BOLD, f"{Icons.CLIPBOARD} First-Time Setup:")
print()
print(" 1. Open the web interface (link above)")
print(" 2. Complete the setup wizard:")
Expand Down Expand Up @@ -441,9 +484,9 @@ def main():
# Always ensure secrets.yaml exists with auth keys
created_new, secrets_data = ensure_secrets_yaml(str(secrets_file))
if created_new:
print_color(Colors.GREEN, f"Secrets file written: {secrets_file}")
print_color(Colors.GREEN, f"Secrets file written: {Icons.CHECKMARK} {secrets_file}")
else:
print(f"Secrets already configured: {secrets_file}")
print(f"Secrets already configured: {Icons.CHECKMARK} {secrets_file}")

# Check for existing config
use_existing = False
Expand All @@ -470,7 +513,7 @@ def main():
print()

if use_existing:
print_color(Colors.GREEN, "✅ Using existing configuration")
print_color(Colors.GREEN, f"{Icons.CHECKMARK} Using existing configuration")
env_content = env_file.read_text()
backend_port = DEFAULT_BACKEND_PORT
webui_port = DEFAULT_WEBUI_PORT
Expand Down Expand Up @@ -505,11 +548,11 @@ def main():
f.write("# Ushadow Secrets\n")
f.write("# DO NOT COMMIT - Contains sensitive credentials\n\n")
yaml.dump(secrets_data, f, default_flow_style=False, sort_keys=False)
print_color(Colors.GREEN, "✅ Admin credentials saved to secrets.yaml")
print_color(Colors.GREEN, f"{Icons.CHECKMARK} Admin credentials saved to secrets.yaml")
except Exception as e:
print_color(Colors.YELLOW, f"⚠️ Could not save admin credentials: {e}")
print_color(Colors.YELLOW, f"{Icons.WARNING} Could not save admin credentials: {e}")

print_color(Colors.BLUE, "🔧 Generating configuration...")
print_color(Colors.BLUE, f"{Icons.WRENCH} Generating configuration...")
print()
config = generate_env_file(
env_name=env_name,
Expand Down
8 changes: 8 additions & 0 deletions ushadow/frontend/src/components/BugReportButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ function buildBugReportUrl(): string {
export default function BugReportButton() {
const { isDark } = useTheme()

// Check if we're running inside the launcher (via query parameter)
const isInLauncher = new URLSearchParams(window.location.search).has('launcher')

// Don't render button when in launcher
if (isInLauncher) {
return null
}

return (
<a
href={buildBugReportUrl()}
Expand Down
8 changes: 8 additions & 0 deletions ushadow/frontend/src/components/layout/EnvironmentFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ export default function EnvironmentFooter() {
// Only show environment indicator in development mode
const showEnvIndicator = nodeEnv === 'development' && envName

// Check if we're running inside the launcher (via query parameter)
const isInLauncher = new URLSearchParams(window.location.search).has('launcher')

// Don't render footer when in launcher
if (isInLauncher) {
return null
}

return (
<footer
className={`fixed bottom-0 left-0 right-0 z-50 ${showEnvIndicator ? `${bg} border-t-2 ${border}` : ''}`}
Expand Down
Loading
Loading