import asyncio
import json
import logging
import threading
import time
from pathlib import Path

from flask import Flask, jsonify, render_template_string

from analysis import CrashAnalysis
from crash import CrashProcessor
from engine import Engine
from protocol import CrashProtocol


# ============================================================
# APPLICATION SETUP
# ============================================================

BASE_DIR = Path(__file__).resolve().parent

with open(
    BASE_DIR / "config.json",
    "r",
    encoding="utf-8"
) as file:
    CONFIG = json.load(file)


HOST = CONFIG.get(
    "dashboard",
    {}
).get("host", "127.0.0.1")

PORT = int(
    CONFIG.get(
        "dashboard",
        {}
    ).get("port", 8000)
)


app = Flask(__name__)

logging.basicConfig(
    level=logging.INFO,
    format=(
        "%(asctime)s | "
        "%(levelname)s | "
        "%(name)s | "
        "%(message)s"
    )
)

logger = logging.getLogger("Dashboard")


# ============================================================
# CORE SYSTEM COMPONENTS
# ============================================================

protocol = CrashProtocol()
crash = CrashProcessor()
analysis = CrashAnalysis()
engine = Engine()


# ============================================================
# ENGINE STATE
# ============================================================

engine_state = {
    "running": False,
    "connected": False,
    "messages": 0,
    "last_message": None,
    "last_message_time": None,
    "error": None,
    "started_at": None
}

engine_thread = None


# ============================================================
# ENGINE MESSAGE PIPELINE
# ============================================================

async def handle_engine_message(message):
    """
    Complete message pipeline:

        WebSocket
            ↓
        Protocol parser
            ↓
        Crash processor
            ↓
        Stored analysis data
            ↓
        Dashboard
    """

    engine_state["messages"] += 1
    engine_state["last_message_time"] = time.time()

    try:
        packet = protocol.parse(message)

        engine_state["last_message"] = {
            "type": packet.get("type"),
            "format": packet.get("format"),
            "length": packet.get("length")
        }

        crash.process(packet)

    except Exception as error:
        engine_state["error"] = str(error)

        logger.exception(
            "Message processing error"
        )


async def engine_worker():
    engine_state["running"] = True
    engine_state["started_at"] = time.time()
    engine_state["error"] = None

    try:
        await engine.run(
            message_handler=handle_engine_message
        )

    except asyncio.CancelledError:
        raise

    except Exception as error:
        engine_state["error"] = str(error)

        logger.exception(
            "Engine worker stopped"
        )

    finally:
        engine_state["running"] = False
        engine_state["connected"] = False


def start_engine():
    global engine_thread

    if (
        engine_thread
        and engine_thread.is_alive()
    ):
        return False

    def runner():
        asyncio.run(
            engine_worker()
        )

    engine_thread = threading.Thread(
        target=runner,
        daemon=True
    )

    engine_thread.start()

    return True


# ============================================================
# DATA HELPERS
# ============================================================

def get_history():
    return crash.get_history(500)


def get_prediction():
    history = crash.get_history(
        max(
            analysis.minimum_samples,
            500
        )
    )

    return analysis.predict_range(
        history
    )


def get_analysis_snapshot():
    history = crash.get_history(1000)

    return analysis.analyze(
        history
    )


def get_prediction_performance():
    return analysis.performance()


def safe_round(value, digits=4):
    if value is None:
        return None

    try:
        return round(
            float(value),
            digits
        )
    except (
        TypeError,
        ValueError
    ):
        return None


# ============================================================
# API ROUTES
# ============================================================

@app.route("/api/status")
def api_status():

    history = get_history()

    latest = (
        history[-1]
        if history
        else None
    )

    prediction = get_prediction()

    return jsonify({
        "system": "BC Crash Research System",
        "running": engine_state["running"],
        "connected": engine_state["connected"],
        "messages": engine_state["messages"],
        "last_message_time":
            engine_state["last_message_time"],
        "error": engine_state["error"],
        "samples": len(history),
        "latest": latest,
        "prediction": prediction
    })


@app.route("/api/history")
def api_history():

    history = get_history()

    return jsonify({
        "count": len(history),
        "history": history
    })


@app.route("/api/analysis")
def api_analysis():

    return jsonify(
        get_analysis_snapshot()
    )


@app.route("/api/prediction")
def api_prediction():

    prediction = get_prediction()

    return jsonify(
        prediction
    )


@app.route("/api/performance")
def api_performance():

    return jsonify(
        get_prediction_performance()
    )


@app.route("/api/sequence")
def api_sequence():

    history = crash.get_history(1000)

    return jsonify(
        analysis.sequence_analysis(
            history
        )
    )


@app.route("/api/runs")
def api_runs():

    history = crash.get_history(1000)

    return jsonify(
        analysis.run_analysis(
            history
        )
    )


@app.route("/api/rolling")
def api_rolling():

    history = crash.get_history(1000)

    return jsonify({
        "50": analysis.rolling_statistics(
            history,
            50
        ),
        "100": analysis.rolling_statistics(
            history,
            100
        ),
        "250": analysis.rolling_statistics(
            history,
            250
        )
    })


# ============================================================
# DASHBOARD PAGE
# ============================================================

HTML = r"""
<!DOCTYPE html>
<html lang="en">

<head>

<meta charset="UTF-8">

<meta
    name="viewport"
    content="width=device-width, initial-scale=1.0"
>

<title>
    BC Crash Research System
</title>

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

<style>

* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

:root {
    --bg: #07090d;
    --panel: #0d1118;
    --panel2: #111722;
    --border: rgba(255,255,255,.08);
    --text: #f5f7fa;
    --muted: #8993a4;
    --accent: #5eead4;
    --accent2: #60a5fa;
    --warning: #fbbf24;
    --danger: #fb7185;
    --success: #34d399;
    --shadow:
        0 20px 60px rgba(0,0,0,.35);
}

body {
    min-height: 100vh;
    background:
        radial-gradient(
            circle at 20% 0%,
            rgba(96,165,250,.12),
            transparent 32%
        ),
        radial-gradient(
            circle at 90% 10%,
            rgba(94,234,212,.08),
            transparent 30%
        ),
        var(--bg);
    color: var(--text);
    font-family:
        Inter,
        -apple-system,
        BlinkMacSystemFont,
        "Segoe UI",
        sans-serif;
}

button {
    font: inherit;
}

.app {
    display: flex;
    min-height: 100vh;
}


/* ============================================================
   SIDEBAR
   ============================================================ */

.sidebar {
    width: 250px;
    min-height: 100vh;
    position: fixed;
    left: 0;
    top: 0;
    bottom: 0;
    padding: 24px 16px;
    background:
        rgba(8,11,16,.92);
    border-right: 1px solid var(--border);
    backdrop-filter: blur(20px);
    z-index: 20;
}

.logo {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 4px 10px 30px;
}

.logo-mark {
    width: 40px;
    height: 40px;
    border-radius: 12px;
    display: grid;
    place-items: center;
    background:
        linear-gradient(
            135deg,
            var(--accent2),
            var(--accent)
        );
    color: #061016;
    font-weight: 900;
    box-shadow:
        0 8px 25px rgba(94,234,212,.18);
}

.logo-title {
    font-weight: 800;
    font-size: 15px;
}

.logo-subtitle {
    font-size: 11px;
    color: var(--muted);
    margin-top: 3px;
}

.nav-title {
    color: #5f6b7c;
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 1.5px;
    padding: 0 12px 10px;
}

.nav-item {
    width: 100%;
    border: 0;
    background: transparent;
    color: #9aa5b5;
    padding: 12px;
    margin-bottom: 4px;
    border-radius: 10px;
    text-align: left;
    cursor: pointer;
    transition: .2s ease;
}

.nav-item:hover,
.nav-item.active {
    color: white;
    background:
        rgba(255,255,255,.06);
}

.nav-item.active {
    box-shadow:
        inset 3px 0 var(--accent);
}

.system-card {
    position: absolute;
    left: 16px;
    right: 16px;
    bottom: 20px;
    padding: 14px;
    border: 1px solid var(--border);
    border-radius: 14px;
    background:
        linear-gradient(
            145deg,
            rgba(255,255,255,.045),
            rgba(255,255,255,.015)
        );
}

.system-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-size: 12px;
}

.dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    display: inline-block;
    margin-right: 7px;
    background: var(--danger);
}

.dot.online {
    background: var(--success);
    box-shadow:
        0 0 12px rgba(52,211,153,.65);
}


/* ============================================================
   MAIN
   ============================================================ */

.main {
    margin-left: 250px;
    width: calc(100% - 250px);
    padding: 28px;
}

.topbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 26px;
}

.page-title {
    font-size: 27px;
    font-weight: 800;
    letter-spacing: -.7px;
}

.page-description {
    color: var(--muted);
    font-size: 13px;
    margin-top: 6px;
}

.top-actions {
    display: flex;
    gap: 10px;
}

.btn {
    border: 1px solid var(--border);
    background: rgba(255,255,255,.04);
    color: white;
    padding: 10px 14px;
    border-radius: 10px;
    cursor: pointer;
    transition: .2s ease;
}

.btn:hover {
    background: rgba(255,255,255,.08);
    transform: translateY(-1px);
}

.btn.primary {
    border: 0;
    color: #061016;
    font-weight: 800;
    background:
        linear-gradient(
            135deg,
            var(--accent2),
            var(--accent)
        );
}


/* ============================================================
   STATUS BANNER
   ============================================================ */

.status-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 15px;
    padding: 13px 16px;
    border-radius: 13px;
    margin-bottom: 18px;
    border: 1px solid var(--border);
    background: rgba(255,255,255,.025);
}

.status-left {
    display: flex;
    align-items: center;
    gap: 9px;
    font-size: 13px;
}

.status-text {
    color: var(--muted);
}


/* ============================================================
   STAT CARDS
   ============================================================ */

.grid {
    display: grid;
    gap: 16px;
}

.stats-grid {
    grid-template-columns:
        repeat(4, minmax(0, 1fr));
}

.card {
    border: 1px solid var(--border);
    background:
        linear-gradient(
            145deg,
            rgba(255,255,255,.045),
            rgba(255,255,255,.018)
        );
    border-radius: 16px;
    box-shadow: var(--shadow);
    overflow: hidden;
}

.stat-card {
    padding: 18px;
    min-height: 125px;
}

.stat-label {
    color: var(--muted);
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 1px;
}

.stat-value {
    margin-top: 13px;
    font-size: 28px;
    font-weight: 800;
    letter-spacing: -.8px;
}

.stat-meta {
    color: #697587;
    font-size: 11px;
    margin-top: 7px;
}


/* ============================================================
   MAIN GRID
   ============================================================ */

.main-grid {
    grid-template-columns:
        minmax(0, 1.7fr)
        minmax(300px, 1fr);
    margin-top: 16px;
}

.panel {
    padding: 20px;
}

.panel-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 20px;
}

.panel-title {
    font-weight: 750;
    font-size: 15px;
}

.panel-subtitle {
    color: var(--muted);
    font-size: 11px;
    margin-top: 5px;
}

.chart-wrap {
    height: 330px;
    position: relative;
}


/* ============================================================
   PREDICTION
   ============================================================ */

.prediction-card {
    padding: 22px;
    position: relative;
    overflow: hidden;
}

.prediction-card::before {
    content: "";
    position: absolute;
    width: 170px;
    height: 170px;
    right: -60px;
    top: -70px;
    border-radius: 50%;
    background:
        rgba(94,234,212,.07);
    filter: blur(4px);
}

.prediction-label {
    color: var(--muted);
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 1px;
}

.range-value {
    font-size: 38px;
    font-weight: 900;
    margin-top: 12px;
    letter-spacing: -1.5px;
}

.range-value span {
    color: var(--accent);
}

.confidence {
    margin-top: 14px;
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.confidence-label {
    color: var(--muted);
    font-size: 12px;
}

.confidence-value {
    font-weight: 800;
    color: var(--accent);
}

.progress {
    height: 6px;
    border-radius: 20px;
    background: rgba(255,255,255,.06);
    margin-top: 9px;
    overflow: hidden;
}

.progress-bar {
    height: 100%;
    width: 0%;
    border-radius: inherit;
    background:
        linear-gradient(
            90deg,
            var(--accent2),
            var(--accent)
        );
    transition: width .5s ease;
}

.prediction-note {
    margin-top: 17px;
    padding-top: 15px;
    border-top: 1px solid var(--border);
    color: #727e8e;
    font-size: 11px;
    line-height: 1.6;
}


/* ============================================================
   TABLE
   ============================================================ */

.table-wrap {
    overflow-x: auto;
}

table {
    width: 100%;
    border-collapse: collapse;
}

th {
    color: #687487;
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: .9px;
    text-align: left;
    padding: 11px;
    border-bottom: 1px solid var(--border);
}

td {
    padding: 12px 11px;
    font-size: 12px;
    border-bottom: 1px solid rgba(255,255,255,.045);
}

.multiplier {
    font-weight: 800;
}

.pill {
    display: inline-flex;
    padding: 4px 8px;
    border-radius: 100px;
    font-size: 10px;
    font-weight: 700;
    background: rgba(255,255,255,.06);
}

.pill.good {
    color: var(--success);
    background:
        rgba(52,211,153,.08);
}

.pill.bad {
    color: var(--danger);
    background:
        rgba(251,113,133,.08);
}


/* ============================================================
   DIAGNOSTICS
   ============================================================ */

.diagnostics {
    display: grid;
    grid-template-columns:
        repeat(3, 1fr);
    gap: 10px;
}

.diag {
    padding: 14px;
    border-radius: 12px;
    background: rgba(255,255,255,.025);
    border: 1px solid var(--border);
}

.diag-label {
    font-size: 10px;
    color: var(--muted);
}

.diag-value {
    margin-top: 8px;
    font-size: 18px;
    font-weight: 800;
}


/* ============================================================
   MOBILE
   ============================================================ */

.mobile-menu {
    display: none;
}

@media (max-width: 1100px) {

    .stats-grid {
        grid-template-columns:
            repeat(2, 1fr);
    }

    .main-grid {
        grid-template-columns: 1fr;
    }

}

@media (max-width: 760px) {

    .sidebar {
        transform: translateX(-100%);
        transition: .25s ease;
    }

    .sidebar.open {
        transform: translateX(0);
    }

    .main {
        margin-left: 0;
        width: 100%;
        padding: 17px;
    }

    .mobile-menu {
        display: block;
    }

    .topbar {
        align-items: flex-start;
    }

    .page-title {
        font-size: 22px;
    }

    .stats-grid {
        grid-template-columns: 1fr 1fr;
        gap: 10px;
    }

    .stat-card {
        min-height: 110px;
        padding: 14px;
    }

    .stat-value {
        font-size: 22px;
    }

    .top-actions .btn:not(.primary) {
        display: none;
    }

    .chart-wrap {
        height: 260px;
    }

    .diagnostics {
        grid-template-columns: 1fr;
    }

}

</style>

</head>


<body>

<div class="app">


<!-- ========================================================
     SIDEBAR
========================================================= -->

<aside class="sidebar" id="sidebar">

    <div class="logo">

        <div class="logo-mark">
            BC
        </div>

        <div>
            <div class="logo-title">
                Crash Research
            </div>

            <div class="logo-subtitle">
                Forensic Analysis Engine
            </div>
        </div>

    </div>


    <div class="nav-title">
        Research
    </div>

    <button
        class="nav-item active"
        onclick="scrollToSection('overview')"
    >
        ◉ &nbsp; Overview
    </button>

    <button
        class="nav-item"
        onclick="scrollToSection('prediction')"
    >
        ◇ &nbsp; Prediction
    </button>

    <button
        class="nav-item"
        onclick="scrollToSection('history')"
    >
        ≋ &nbsp; History
    </button>

    <button
        class="nav-item"
        onclick="scrollToSection('diagnostics')"
    >
        ◌ &nbsp; Diagnostics
    </button>


    <div class="system-card">

        <div class="system-row">

            <span>
                <span
                    class="dot"
                    id="sidebarDot"
                ></span>

                Engine
            </span>

            <span
                id="sidebarStatus"
                style="color:#8993a4"
            >
                Offline
            </span>

        </div>

    </div>

</aside>


<!-- ========================================================
     MAIN CONTENT
========================================================= -->

<main class="main">


    <div class="topbar">

        <div>

            <button
                class="btn mobile-menu"
                onclick="toggleSidebar()"
            >
                ☰
            </button>

            <div
                class="page-title"
                id="overview"
            >
                Research Dashboard
            </div>

            <div class="page-description">
                Historical sequence analysis,
                range estimation and model diagnostics.
            </div>

        </div>


        <div class="top-actions">

            <button
                class="btn"
                onclick="refreshAll()"
            >
                ↻ Refresh
            </button>

            <button
                class="btn primary"
                onclick="startEngine()"
            >
                Start Capture
            </button>

        </div>

    </div>


    <!-- STATUS -->

    <div class="status-banner">

        <div class="status-left">

            <span
                class="dot"
                id="statusDot"
            ></span>

            <strong id="statusTitle">
                System offline
            </strong>

            <span
                class="status-text"
                id="statusDescription"
            >
                Waiting for capture engine.
            </span>

        </div>

        <div
            style="
                color:#687487;
                font-size:11px;
            "
            id="lastUpdate"
        >
            —
        </div>

    </div>


    <!-- STATISTICS -->

    <section class="grid stats-grid">

        <div class="card stat-card">

            <div class="stat-label">
                Samples
            </div>

            <div
                class="stat-value"
                id="sampleCount"
            >
                0
            </div>

            <div class="stat-meta">
                Recorded observations
            </div>

        </div>


        <div class="card stat-card">

            <div class="stat-label">
                Latest
            </div>

            <div
                class="stat-value"
                id="latestValue"
            >
                —
            </div>

            <div class="stat-meta">
                Most recent result
            </div>

        </div>


        <div class="card stat-card">

            <div class="stat-label">
                Median
            </div>

            <div
                class="stat-value"
                id="medianValue"
            >
                —
            </div>

            <div class="stat-meta">
                Historical distribution
            </div>

        </div>


        <div class="card stat-card">

            <div class="stat-label">
                Coverage
            </div>

            <div
                class="stat-value"
                id="coverageValue"
            >
                —
            </div>

            <div class="stat-meta">
                Evaluated range predictions
            </div>

        </div>

    </section>


    <!-- MAIN PANELS -->

    <section class="grid main-grid">


        <!-- CHART -->

        <div class="card panel">

            <div class="panel-header">

                <div>

                    <div class="panel-title">
                        Crash History
                    </div>

                    <div class="panel-subtitle">
                        Most recent recorded observations
                    </div>

                </div>

                <div
                    class="pill"
                    id="chartCount"
                >
                    0 observations
                </div>

            </div>

            <div class="chart-wrap">

                <canvas id="historyChart"></canvas>

            </div>

        </div>


        <!-- PREDICTION -->

        <div
            class="card prediction-card"
            id="prediction"
        >

            <div class="prediction-label">
                Next-Round Range
            </div>

            <div
                class="range-value"
                id="rangeValue"
            >
                —
            </div>

            <div class="confidence">

                <span class="confidence-label">
                    Target confidence
                </span>

                <span
                    class="confidence-value"
                    id="confidenceValue"
                >
                    —
                </span>

            </div>

            <div class="progress">

                <div
                    class="progress-bar"
                    id="confidenceBar"
                ></div>

            </div>

            <div
                style="
                    margin-top:18px;
                    color:#778394;
                    font-size:11px;
                "
            >
                Model:
                <span
                    id="modelName"
                    style="color:#b8c1ce"
                >
                    —
                </span>
            </div>

            <div class="prediction-note">
                The displayed interval is a statistical
                research estimate generated before the next
                recorded observation. It is not an exact
                outcome prediction.
            </div>

        </div>

    </section>


    <!-- DIAGNOSTICS -->

    <section
        class="card panel"
        id="diagnostics"
        style="margin-top:16px"
    >

        <div class="panel-header">

            <div>

                <div class="panel-title">
                    Model Diagnostics
                </div>

                <div class="panel-subtitle">
                    Distribution and rolling-window measurements
                </div>

            </div>

        </div>


        <div class="diagnostics">

            <div class="diag">

                <div class="diag-label">
                    Mean
                </div>

                <div
                    class="diag-value"
                    id="meanValue"
                >
                    —
                </div>

            </div>


            <div class="diag">

                <div class="diag-label">
                    Standard Deviation
                </div>

                <div
                    class="diag-value"
                    id="stdevValue"
                >
                    —
                </div>

            </div>


            <div class="diag">

                <div class="diag-label">
                    90th Percentile
                </div>

                <div
                    class="diag-value"
                    id="q90Value"
                >
                    —
                </div>

            </div>


            <div class="diag">

                <div class="diag-label">
                    Recent Mean
                </div>

                <div
                    class="diag-value"
                    id="recentMean"
                >
                    —
                </div>

            </div>


            <div class="diag">

                <div class="diag-label">
                    Tested Predictions
                </div>

                <div
                    class="diag-value"
                    id="testedPredictions"
                >
                    0
                </div>

            </div>


            <div class="diag">

                <div class="diag-label">
                    Correct Ranges
                </div>

                <div
                    class="diag-value"
                    id="correctPredictions"
                >
                    0
                </div>

            </div>

        </div>

    </section>


    <!-- HISTORY TABLE -->

    <section
        class="card panel"
        id="history"
        style="margin-top:16px"
    >

        <div class="panel-header">

            <div>

                <div class="panel-title">
                    Recent Observations
                </div>

                <div class="panel-subtitle">
                    Recorded multiplier history
                </div>

            </div>

        </div>


        <div class="table-wrap">

            <table>

                <thead>

                    <tr>

                        <th>
                            #
                        </th>

                        <th>
                            Multiplier
                        </th>

                        <th>
                            Timestamp
                        </th>

                    </tr>

                </thead>

                <tbody
                    id="historyTable"
                >

                </tbody>

            </table>

        </div>

    </section>


    <!-- FOOTER -->

    <div
        style="
            padding:24px 5px;
            color:#4e5969;
            font-size:10px;
            text-align:center;
        "
    >
        BC Crash Research System ·
        Statistical analysis interface ·
        Historical data only
    </div>


</main>

</div>


<script>

let chart = null;


/* ============================================================
   HELPERS
============================================================ */

function formatMultiplier(value) {

    if (
        value === null ||
        value === undefined ||
        isNaN(value)
    ) {
        return "—";
    }

    return Number(value).toFixed(2) + "×";
}


function formatPercent(value) {

    if (
        value === null ||
        value === undefined ||
        isNaN(value)
    ) {
        return "—";
    }

    return (
        Number(value).toFixed(1)
        + "%"
    );
}


function formatNumber(value) {

    if (
        value === null ||
        value === undefined ||
        isNaN(value)
    ) {
        return "—";
    }

    return Number(value).toFixed(2);
}


/* ============================================================
   STATUS
============================================================ */

async function loadStatus() {

    try {

        const response =
            await fetch("/api/status");

        const data =
            await response.json();

        const connected =
            data.connected === true;

        const running =
            data.running === true;


        document.getElementById(
            "sampleCount"
        ).textContent =
            data.samples || 0;


        document.getElementById(
            "latestValue"
        ).textContent =
            data.latest
                ? formatMultiplier(
                    data.latest.multiplier
                )
                : "—";


        const dot =
            document.getElementById(
                "statusDot"
            );

        const sidebarDot =
            document.getElementById(
                "sidebarDot"
            );


        dot.classList.toggle(
            "online",
            connected
        );

        sidebarDot.classList.toggle(
            "online",
            connected
        );


        document.getElementById(
            "statusTitle"
        ).textContent =
            connected
                ? "Capture engine connected"
                : running
                    ? "Capture engine running"
                    : "System offline";


        document.getElementById(
            "statusDescription"
        ).textContent =
            connected
                ? "Receiving live socket traffic."
                : running
                    ? "Attempting connection..."
                    : "Waiting for capture engine.";


        document.getElementById(
            "sidebarStatus"
        ).textContent =
            connected
                ? "Connected"
                : running
                    ? "Running"
                    : "Offline";


        document.getElementById(
            "lastUpdate"
        ).textContent =
            data.last_message_time
                ? new Date(
                    data.last_message_time * 1000
                ).toLocaleTimeString()
                : "—";

    }

    catch (error) {

        console.error(
            "Status error:",
            error
        );

    }
}


/* ============================================================
   PREDICTION
============================================================ */

async function loadPrediction() {

    try {

        const response =
            await fetch(
                "/api/prediction"
            );

        const data =
            await response.json();


        if (!data.ready) {

            document.getElementById(
                "rangeValue"
            ).textContent =
                "Not ready";

            document.getElementById(
                "confidenceValue"
            ).textContent =
                data.samples +
                " / " +
                data.required_samples;

            document.getElementById(
                "confidenceBar"
            ).style.width = "0%";

            document.getElementById(
                "modelName"
            ).textContent =
                data.reason || "Insufficient data";

            return;
        }


        document.getElementById(
            "rangeValue"
        ).innerHTML =
            formatMultiplier(
                data.lower_bound
            )
            +
            " <span>—</span> "
            +
            formatMultiplier(
                data.upper_bound
           