summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkboz <kutayboz@gmail.com>2026-09-27 22:36:36 +0200
committerkboz <kutayboz@gmail.com>2026-09-27 22:36:36 +0200
commit526c8ec005d03b17950e9f5e7fe74208b491dd68 (patch)
treeb6a5ea59040bf48ffd0953a5682a43648a03ca51
parent1b59961ce2385252e3346ea6107b15a2c1feafe6 (diff)
downloadhaul-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.css54
-rw-r--r--frontend/src/App.js38
-rw-r--r--frontend/src/Setup.js142
-rw-r--r--scripts/setup_auth.py131
-rw-r--r--scripts/setup_eve_data.py55
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