From eae3e72fd9f1adeec70e97ba91743a61d4e695eb Mon Sep 17 00:00:00 2001 From: funman300 Date: Thu, 25 Jun 2026 17:18:10 -0700 Subject: [PATCH] Phase 13: web dashboard at /_bridge/dashboard Single-page club management UI served by the bridge. The Core URL is injected server-side so client JS calls Core directly without routing through the proxy. Vanilla HTML/CSS/JS, dark theme matching admin.html. Sections: Club (name/manager/coins/stats edit), Collection (filterable card grid with stat bars + chemistry/training pills), Packs (open with modal, history), Objectives (progress bars + one-click claim), Division (W/D/L record, season progress, rivals weekly claim), FUT Champions (current session with in-browser match simulation, claim, history), Notifications (badge count in nav). Co-Authored-By: Claude Sonnet 4.6 --- src/dashboard.html | 750 +++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + src/routes/health.rs | 13 + tests/proxy_test.rs | 54 +++- 4 files changed, 817 insertions(+), 1 deletion(-) create mode 100644 src/dashboard.html diff --git a/src/dashboard.html b/src/dashboard.html new file mode 100644 index 0000000..8be0fe3 --- /dev/null +++ b/src/dashboard.html @@ -0,0 +1,750 @@ + + + + + +OpenFUT — Club Dashboard + + + + +
+

⚽ OpenFUT Dashboard

+ Offline +
+ + + — coins +
+
+ + + +
+ + +
+
+
+

Club Info

+
Loading…
+
+ + + +
+
+
+

Statistics

+
Loading…
+
+
+
+ + +
+
+ + + + + + +
+
+
Loading…
+
+ + +
+
+

Unopened Packs

+ +
+
Loading…
+
+

Pack History

+
Loading…
+
+
+ + +
+
Loading…
+
+ + +
+
+
+

Division Rivals

+
Loading…
+
+ +

Offline mode: claim whenever you're ready to end your rivals week.

+
+
+
+

Season Progress

+
Loading…
+
+
+
+ + +
+
+
+

Current Session

+
Loading…
+
+
+

Past Sessions

+
Loading…
+
+
+
+ + +
+
Loading…
+
+ +
+ + + + + + + diff --git a/src/main.rs b/src/main.rs index f1d5a65..386974d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,6 +56,7 @@ fn build_router(state: ProxyState) -> Router { .route("/_bridge/health", get(routes::health::get_health)) .route("/_bridge/cert.pem", get(routes::health::get_cert)) .route("/_bridge/guide", get(routes::health::get_guide)) + .route("/_bridge/dashboard", get(routes::health::get_dashboard)) .route("/_bridge/admin", get(routes::admin::get_admin_dashboard)) .route("/_bridge/captures", get(routes::admin::get_captures)) .route("/_bridge/captures", delete(routes::admin::delete_captures)) diff --git a/src/routes/health.rs b/src/routes/health.rs index 00c83e8..50aae9a 100644 --- a/src/routes/health.rs +++ b/src/routes/health.rs @@ -46,6 +46,19 @@ pub async fn get_cert(State(state): State) -> Response { } } +/// Club management dashboard served at `/_bridge/dashboard`. +/// +/// Injects the Core URL so client-side JS can call Core directly without going through the proxy. +pub async fn get_dashboard(State(state): State) -> Response { + let template = include_str!("../dashboard.html"); + let html = template.replace("{{CORE_URL}}", &state.config.core_url); + Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "text/html; charset=utf-8") + .body(axum::body::Body::from(html)) + .unwrap() +} + /// HTML setup guide for connecting FIFA 23 to OpenFUT Bridge. pub async fn get_guide(State(state): State) -> Html { let host = state diff --git a/tests/proxy_test.rs b/tests/proxy_test.rs index 1e7b1f1..0125808 100644 --- a/tests/proxy_test.rs +++ b/tests/proxy_test.rs @@ -9,7 +9,7 @@ use openfut_bridge::{ proxy::ProxyState, routes, }; -use axum::routing::{any, delete, get}; +use axum::routing::{any, delete, get, post}; use tower::ServiceExt; // ── Unit tests ──────────────────────────────────────────────────────────────── @@ -93,10 +93,12 @@ fn build_test_app() -> axum::Router { axum::Router::new() .route("/_bridge/health", get(routes::health::get_health)) + .route("/_bridge/dashboard", get(routes::health::get_dashboard)) .route("/_bridge/captures", get(routes::admin::get_captures)) .route("/_bridge/captures", delete(routes::admin::delete_captures)) .route("/_bridge/unknown", get(routes::admin::get_unknown_endpoints)) .route("/_bridge/status", get(routes::admin::get_endpoint_status)) + .route("/_bridge/captures/:id/replay", post(routes::admin::post_replay_capture)) .fallback(any(openfut_bridge::proxy::catch_all_handler)) .with_state(state) } @@ -191,6 +193,56 @@ async fn test_bridge_status_endpoint() { assert!(json["endpoints"].is_array()); } +// ── Phase 13 — Dashboard ────────────────────────────────────────────────────── + +#[tokio::test] +async fn test_dashboard_returns_html() { + let app = build_test_app(); + let resp = app + .oneshot( + Request::builder() + .uri("/_bridge/dashboard") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + let ct = resp.headers().get("content-type").unwrap().to_str().unwrap(); + assert!(ct.contains("text/html"), "expected text/html, got {ct}"); + let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); + let html = String::from_utf8(body.to_vec()).unwrap(); + assert!(html.contains("OpenFUT Dashboard"), "title missing"); + assert!(html.contains("const CORE ="), "core URL injection missing"); + // Placeholder URL injected in test mode + assert!(html.contains("127.0.0.1:9999"), "core URL not injected"); + assert!(!html.contains("{{CORE_URL}}"), "template placeholder was not replaced"); +} + +#[tokio::test] +async fn test_dashboard_contains_key_sections() { + let app = build_test_app(); + let resp = app + .oneshot( + Request::builder() + .uri("/_bridge/dashboard") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); + let html = String::from_utf8(body.to_vec()).unwrap(); + // Verify all major tab sections are present + assert!(html.contains("tab-club"), "club tab missing"); + assert!(html.contains("tab-collection"), "collection tab missing"); + assert!(html.contains("tab-packs"), "packs tab missing"); + assert!(html.contains("tab-objectives"), "objectives tab missing"); + assert!(html.contains("tab-division"), "division tab missing"); + assert!(html.contains("tab-champs"), "fut champs tab missing"); + assert!(html.contains("tab-notifications"), "notifications tab missing"); +} + #[tokio::test] async fn test_tls_cert_generation() { let result = openfut_bridge::tls::generate_self_signed_cert();