diff options
| author | kboz <kutayboz@gmail.com> | 2026-09-27 22:36:36 +0200 |
|---|---|---|
| committer | kboz <kutayboz@gmail.com> | 2026-09-27 22:36:36 +0200 |
| commit | 526c8ec005d03b17950e9f5e7fe74208b491dd68 (patch) | |
| tree | b6a5ea59040bf48ffd0953a5682a43648a03ca51 | |
| parent | 1b59961ce2385252e3346ea6107b15a2c1feafe6 (diff) | |
| download | haul-go-backend.tar.gz | |
feat(frontend): add guided setup flow for backend bootstrapgo-backend
Poll the backend /api/setup/status and show a Setup screen until data,
auth, and backend readiness are confirmed. Remove the manual
setup_auth.py and setup_eve_data.py scripts superseded by the automated
setup endpoints.
| -rw-r--r-- | frontend/src/App.css | 54 | ||||
| -rw-r--r-- | frontend/src/App.js | 38 | ||||
| -rw-r--r-- | frontend/src/Setup.js | 142 | ||||
| -rw-r--r-- | scripts/setup_auth.py | 131 | ||||
| -rw-r--r-- | scripts/setup_eve_data.py | 55 |
5 files changed, 231 insertions, 189 deletions
diff --git a/frontend/src/App.css b/frontend/src/App.css index 19cf08b..c67769b 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -20,4 +20,56 @@ .hide-scrollbar::-webkit-scrollbar { display: none; -}
\ No newline at end of file +} +.flex-center { + display: flex; + align-items: center; + justify-content: center; +} + +.setup-container { + background: var(--bg-primary, #1e1e1e); + padding: 3rem; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0,0,0,0.3); + text-align: center; + max-width: 500px; +} + +.setup-step { + margin: 2rem 0; + padding: 1rem; + border: 1px solid #444; + border-radius: 4px; +} + +.setup-step h2 { + margin-top: 0; + font-size: 1.2rem; + color: #fff; +} + +.setup-container button { + background: #3b82f6; + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 4px; + cursor: pointer; + font-weight: bold; +} + +.setup-container button:hover { + background: #2563eb; +} + +.setup-container button:disabled { + background: #555; + cursor: not-allowed; +} + +.setup-message { + margin-top: 1rem; + font-size: 0.9rem; + color: #aaa; +} diff --git a/frontend/src/App.js b/frontend/src/App.js index 569ff74..af95c63 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,18 +1,52 @@ +import React, { useState, useEffect } from 'react'; import './App.css'; import Path from './Path'; import Log from './Log'; import Options from './Options'; import Info from './Info'; +import Setup from './Setup'; function App() { + const [setupStatus, setSetupStatus] = useState(null); + + useEffect(() => { + const checkStatus = async () => { + try { + const resp = await fetch('http://localhost:8000/api/setup/status'); + const rootResp = await fetch('http://localhost:8000/'); + if (resp.ok && rootResp.ok) { + const data = await resp.json(); + const rootData = await rootResp.json(); + data.backend_ready = rootData.ready; + setSetupStatus(data); + if (!rootData.ready) { + setTimeout(checkStatus, 2000); + } + } else { + setTimeout(checkStatus, 2000); + } + } catch (e) { + console.error("Could not reach backend API, retrying in 2s..."); + setTimeout(checkStatus, 2000); + } + }; + checkStatus(); + }, []); + + if (!setupStatus) { + return <div className="loading">Initializing Backend Connection...</div>; + } + + if (!setupStatus.data_ready || !setupStatus.auth_ready || !setupStatus.backend_ready) { + return <Setup status={setupStatus} onComplete={() => window.location.reload()} />; + } + return ( <div className="App flex flex-wrap h-screen"> - {/* Main component for Path */} <div className="w-2/3 h-full overflow-auto hide-scrollbar"> <Path /> </div> - {/* Sidebar for route info, controls, and log output */} <div className="w-1/3 h-full flex flex-col"> <div className="h-1/4 overflow-auto"> <Info /> diff --git a/frontend/src/Setup.js b/frontend/src/Setup.js new file mode 100644 index 0000000..537a36c --- /dev/null +++ b/frontend/src/Setup.js @@ -0,0 +1,142 @@ +import React, { useState, useEffect } from 'react'; +import './App.css'; + +function Setup({ status, onComplete }) { + const [loadingData, setLoadingData] = useState(false); + const [loginUrl, setLoginUrl] = useState(''); + const [message, setMessage] = useState(''); + const [manualCode, setManualCode] = useState(''); + const [showManualCode, setShowManualCode] = useState(false); + + useEffect(() => { + // Automatically handle the OAuth callback if the URL has ?code= + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get('code'); + if (code) { + handleAuthCallback(code); + } + }, []); + + const handleAuthCallback = async (code) => { + setMessage('Completing login...'); + try { + const resp = await fetch('http://localhost:8000/api/setup/auth/callback', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code }) + }); + if (resp.ok) { + setMessage('Login successful. Please ensure both steps are complete.'); + } else { + setMessage('Login failed. Please try again.'); + } + } catch (e) { + setMessage('Error during login callback.'); + } + }; + + const downloadData = async () => { + setLoadingData(true); + setMessage('Downloading EVE Online static data (this may take a few minutes)...'); + try { + const resp = await fetch('http://localhost:8000/api/setup/data', { method: 'POST' }); + if (resp.ok) { + setMessage('Data downloaded successfully!'); + } else { + setMessage('Failed to download data.'); + } + } catch (e) { + setMessage('Error downloading data.'); + } + setLoadingData(false); + }; + + const fetchLoginUrl = async () => { + try { + const resp = await fetch('http://localhost:8000/api/setup/auth/url'); + if (resp.ok) { + const data = await resp.json(); + window.open(data.url, '_blank'); + setShowManualCode(true); + } + } catch (e) { + setMessage('Failed to fetch login URL.'); + } + }; + + return ( + <div className="App dark-mode flex-center"> + <div className="setup-container"> + <h1>Welcome to EVE Haul</h1> + <p>Let's get your environment set up.</p> + + <div className="setup-step"> + <h2>1. EVE Static Data</h2> + {status.data_ready ? ( + <p style={{ color: 'lightgreen' }}>Data is ready!</p> + ) : ( + <button onClick={downloadData} disabled={loadingData}> + {loadingData ? 'Downloading...' : 'Download Game Data'} + </button> + )} + </div> + + <div className="setup-step"> + <h2>2. EVE Single Sign-On</h2> + {status.auth_ready ? ( + <p style={{ color: 'lightgreen' }}>Authenticated!</p> + ) : ( + <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}> + <button onClick={() => { + setShowManualCode(true); + fetchLoginUrl(); + }}> + Login to EVE Online + </button> + {showManualCode && ( + <div style={{ fontSize: '0.9em', color: '#ccc', textAlign: 'left', marginTop: '10px' }}> + <p>After granting access, you might be redirected to a broken page (like `https://localhost/callback/...`).</p> + <p>Copy the `code=` part from the URL address bar and paste it below:</p> + <div style={{ display: 'flex', gap: '5px', marginTop: '5px' }}> + <input + type="text" + placeholder="Paste the code here" + value={manualCode} + onChange={(e) => setManualCode(e.target.value)} + style={{ flex: 1, padding: '5px', borderRadius: '3px', background: '#222', color: '#fff', border: '1px solid #444' }} + /> + <button onClick={() => { + if (manualCode.includes('?code=')) { + const params = new URLSearchParams(manualCode.split('?')[1]); + handleAuthCallback(params.get('code') || manualCode); + } else if (manualCode.startsWith('http') && manualCode.includes('code=')) { + const url = new URL(manualCode); + handleAuthCallback(url.searchParams.get('code') || manualCode); + } else { + handleAuthCallback(manualCode); + } + }}>Submit + </button> + </div> + </div> + )} + </div> + )} + </div> + + <p className="setup-message">{message}</p> + + {status.data_ready && status.auth_ready && !status.backend_ready && ( + <div style={{ marginTop: '20px', padding: '15px', background: '#333', border: '1px solid #ffaa00', borderRadius: '5px' }}> + <h3 style={{ color: '#ffaa00', margin: '0 0 10px 0' }}>Restart Required</h3> + <p>Setup is complete, but the EVE Haul backend process needs to be restarted to load the downloaded data.</p> + <p>Please go to your terminal, press `Ctrl+C` to stop the backend, and start it again.</p> + <p>This page will automatically launch EVE Haul when the backend is ready.</p> + </div> + )} + </div> + </div> + ); +} + +export default Setup; diff --git a/scripts/setup_auth.py b/scripts/setup_auth.py deleted file mode 100644 index c4f60c5..0000000 --- a/scripts/setup_auth.py +++ /dev/null @@ -1,131 +0,0 @@ -from __future__ import annotations - -import base64 -import json -from datetime import datetime, UTC -from getpass import getpass -from pathlib import Path -import shlex -from urllib.parse import urlencode, quote - -import requests -from dotenv import load_dotenv -from jose import jwt -import os - - -ROOT_DIR = Path(__file__).resolve().parent.parent -AUTH_FILE = ROOT_DIR / "backend" / "src" / "cache" / "auth.json" -ENV_FILE = ROOT_DIR / ".env" -REDIRECT_URI = "https://localhost/callback/" -TOKEN_URL = "https://login.eveonline.com/v2/oauth/token" -AUTH_URL = "https://login.eveonline.com/v2/oauth/authorize/" -SCOPES = [ - "esi-assets.read_assets.v1", - "esi-location.read_location.v1", - "esi-location.read_online.v1", - "esi-wallet.read_character_wallet.v1", - "esi-ui.open_window.v1", - "esi-ui.write_waypoint.v1", -] - - -def build_auth_url(client_id: str) -> str: - params = { - "response_type": "code", - "redirect_uri": REDIRECT_URI, - "client_id": client_id, - "state": "haul-local", - "scope": " ".join(SCOPES), - } - return f"{AUTH_URL}?{urlencode(params, quote_via=quote)}" - - -def prompt_for_value(name: str, secret: bool = False) -> str: - while True: - value = getpass(f"{name}: ") if secret else input(f"{name}: ").strip() - if value: - return value - print(f"{name} is required.") - - -def append_missing_env_values(values: dict[str, str]) -> None: - existing_text = ENV_FILE.read_text(encoding="utf-8") if ENV_FILE.exists() else "" - existing_keys = set() - - for raw_line in existing_text.splitlines(): - line = raw_line.strip() - if not line or line.startswith("#") or "=" not in line: - continue - key, _ = line.split("=", 1) - existing_keys.add(key.strip()) - - lines_to_add = [] - for key, value in values.items(): - if key not in existing_keys: - lines_to_add.append(f"{key}={shlex.quote(value)}") - - if not lines_to_add: - return - - if existing_text and not existing_text.endswith("\n"): - existing_text += "\n" - - ENV_FILE.write_text(existing_text + "\n".join(lines_to_add) + "\n", encoding="utf-8") - - -def main() -> int: - load_dotenv(ENV_FILE) - - client_id = os.getenv("CLIENT_ID") or prompt_for_value("CLIENT_ID") - secret_key = os.getenv("SECRET_KEY") or prompt_for_value("SECRET_KEY", secret=True) - - print("Open this URL in your browser and complete the EVE SSO flow:\n") - print("If you authenticated earlier without wallet access, reauthorization is required for transaction-backed route confirmation.\n") - print(build_auth_url(client_id)) - print("\nAfter the redirect, copy the 'code' query parameter and paste it below.") - code = input("Authorization code: ").strip() - - if not code: - print("No authorization code provided.") - return 1 - - user_pass = f"{client_id}:{secret_key}".encode("utf-8") - headers = { - "Authorization": f"Basic {base64.urlsafe_b64encode(user_pass).decode()}", - "Content-Type": "application/x-www-form-urlencoded", - "Host": "login.eveonline.com", - } - form_values = { - "grant_type": "authorization_code", - "code": code, - } - - response = requests.post(TOKEN_URL, data=form_values, headers=headers, timeout=30) - response.raise_for_status() - auth_data = response.json() - auth_data["expires_at"] = int(datetime.now(UTC).timestamp()) + int(auth_data.get("expires_in", 0)) - - AUTH_FILE.parent.mkdir(parents=True, exist_ok=True) - AUTH_FILE.write_text(json.dumps(auth_data, indent=2), encoding="utf-8") - - claims = jwt.get_unverified_claims(auth_data["access_token"]) - character_id = claims.get("sub", "").split(":")[-1] if claims.get("sub") else "unknown" - character_name = claims.get("name", "unknown") - - append_missing_env_values( - { - "CLIENT_ID": client_id, - "SECRET_KEY": secret_key, - "character_id": character_id, - } - ) - - print(f"Saved auth cache to {AUTH_FILE}") - print(f"Detected character: {character_name} ({character_id})") - print(f"Ensured runtime env values exist in {ENV_FILE}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main())
\ No newline at end of file diff --git a/scripts/setup_eve_data.py b/scripts/setup_eve_data.py deleted file mode 100644 index a7f7efd..0000000 --- a/scripts/setup_eve_data.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import annotations - -import bz2 -import shutil -import sys -import urllib.request -from pathlib import Path - - -ROOT_DIR = Path(__file__).resolve().parent.parent -DATA_DIR = ROOT_DIR / "backend" / "data" -BASE_URL = "https://www.fuzzwork.co.uk/dump/latest" - -REQUIRED_FILES = { - "mapDenormalize.csv": f"{BASE_URL}/mapDenormalize.csv.bz2", - "mapJumps.csv": f"{BASE_URL}/mapJumps.csv.bz2", - "invTypes.csv": f"{BASE_URL}/invTypes.csv.bz2", - "mapRegions.csv": f"{BASE_URL}/mapRegions.csv.bz2", -} - - -def download_and_extract(file_name: str, url: str) -> None: - destination = DATA_DIR / file_name - archive_path = destination.with_suffix(destination.suffix + ".bz2") - - if destination.exists(): - print(f"Skipping {file_name}; already exists.") - return - - print(f"Downloading {file_name} from {url}") - - try: - with urllib.request.urlopen(url, timeout=60) as response, archive_path.open("wb") as output_file: - shutil.copyfileobj(response, output_file) - - with bz2.open(archive_path, "rb") as compressed_file, destination.open("wb") as output_file: - shutil.copyfileobj(compressed_file, output_file) - finally: - archive_path.unlink(missing_ok=True) - - print(f"Wrote {destination}") - - -def main() -> int: - DATA_DIR.mkdir(parents=True, exist_ok=True) - - for file_name, url in REQUIRED_FILES.items(): - download_and_extract(file_name, url) - - print("EVE static data is ready.") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main())
\ No newline at end of file |
