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…
+
+
+
+
+
+
+
+
+
+
+
Unopened Packs
+
+
+
Loading…
+
+
Pack History
+
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…
+
+
+
+
+
+
+
+
+
+
+
+
+
Pack Opened!
+
+
+
+
+
+
+
+
+
+
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();