martian7777
feat: implement backend core with ORM models, authentication, and AI-driven telemetry diagnostics
e5be436 | """Gradio web frontend for the Predictive Maintenance system. | |
| Four tabs: Dashboard/Auth, Telemetry Upload, Analytics & Visuals, AI Assistant. | |
| Each browser session gets its own authenticated APIClient held in gr.State. | |
| Run with: python -m frontend.app | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import time | |
| import gradio as gr | |
| import plotly.graph_objects as go | |
| from frontend.client import APIClient, APIError | |
| STATUS_EMOJI = {"OK": "🟢", "WARNING": "🟡", "CRITICAL": "🔴"} | |
| # --------------------------------------------------------------------------- auth | |
| def do_register(client: APIClient, email, password, full_name): | |
| client = client or APIClient() | |
| try: | |
| client.register(email, password, full_name or None) | |
| client.login(email, password) | |
| return ( | |
| client, | |
| _status_md(f"✅ Registered and logged in as **{email}**."), | |
| *_post_login(client), | |
| ) | |
| except APIError as e: | |
| return client, _status_md(f"❌ {e.detail}"), gr.update(), gr.update(), "" | |
| def do_login(client: APIClient, email, password): | |
| client = client or APIClient() | |
| try: | |
| client.login(email, password) | |
| return client, _status_md(f"✅ Logged in as **{email}**."), *_post_login(client) | |
| except APIError as e: | |
| return client, _status_md(f"❌ {e.detail}"), gr.update(), gr.update(), "" | |
| def do_logout(client: APIClient): | |
| if client: | |
| client.logout() | |
| return ( | |
| APIClient(), | |
| _status_md("Logged out."), | |
| gr.update(choices=[], value=None), | |
| gr.update(choices=[], value=None), | |
| "", | |
| ) | |
| def _post_login(client: APIClient): | |
| """Return updates for the two machine dropdowns + dashboard markdown.""" | |
| choices = _machine_choices(client) | |
| dash = _render_dashboard(client) | |
| return ( | |
| gr.update(choices=choices, value=choices[0][1] if choices else None), | |
| gr.update(choices=choices, value=choices[0][1] if choices else None), | |
| dash, | |
| ) | |
| # ----------------------------------------------------------------------- machines | |
| def _machine_choices(client: APIClient) -> list[tuple[str, str]]: | |
| if not client or not client.is_authenticated: | |
| return [] | |
| try: | |
| machines = client.list_machines() | |
| except APIError: | |
| return [] | |
| return [ | |
| (f"{STATUS_EMOJI.get(m['status'], '')} {m['name']} ({m['type']})", m["id"]) | |
| for m in machines | |
| ] | |
| def _render_dashboard(client: APIClient) -> str: | |
| if not client or not client.is_authenticated: | |
| return "_Not logged in._" | |
| try: | |
| machines = client.list_machines() | |
| except APIError as e: | |
| return f"❌ {e.detail}" | |
| if not machines: | |
| return "No machines registered yet. Create one below. 👇" | |
| lines = ["| Status | Name | Type | Location |", "|---|---|---|---|"] | |
| for m in machines: | |
| emoji = STATUS_EMOJI.get(m["status"], "") | |
| lines.append( | |
| f"| {emoji} {m['status']} | {m['name']} | {m['type']} | {m.get('location') or '—'} |" | |
| ) | |
| return "\n".join(lines) | |
| def create_machine(client: APIClient, name, type_, location): | |
| if not client or not client.is_authenticated: | |
| return client, _status_md("❌ Please log in first."), gr.update(), gr.update(), "" | |
| if not name or not type_: | |
| msg = _status_md("❌ Name and type are required.") | |
| return client, msg, gr.update(), gr.update(), _render_dashboard(client) | |
| try: | |
| client.create_machine(name, type_, location or None) | |
| msg = f"✅ Machine **{name}** created." | |
| except APIError as e: | |
| msg = f"❌ {e.detail}" | |
| choices = _machine_choices(client) | |
| return ( | |
| client, | |
| _status_md(msg), | |
| gr.update(choices=choices, value=choices[0][1] if choices else None), | |
| gr.update(choices=choices, value=choices[0][1] if choices else None), | |
| _render_dashboard(client), | |
| ) | |
| def refresh_dashboard(client: APIClient): | |
| choices = _machine_choices(client) | |
| return ( | |
| gr.update(choices=choices, value=choices[0][1] if choices else None), | |
| gr.update(choices=choices, value=choices[0][1] if choices else None), | |
| _render_dashboard(client), | |
| ) | |
| # ----------------------------------------------------------------------- upload | |
| def upload_and_track(client: APIClient, machine_id, file_obj, progress=gr.Progress()): | |
| if not client or not client.is_authenticated: | |
| return "❌ Please log in first." | |
| if not machine_id: | |
| return "❌ Select a machine first." | |
| if file_obj is None: | |
| return "❌ Choose a CSV file to upload." | |
| try: | |
| progress(0.05, desc="Uploading file...") | |
| resp = client.upload_csv(machine_id, file_obj.name) | |
| task_id = resp["task_id"] | |
| except APIError as e: | |
| return f"❌ Upload failed: {e.detail}" | |
| # Poll the background task until it completes. | |
| deadline = time.time() + 600 | |
| last = {} | |
| while time.time() < deadline: | |
| try: | |
| last = client.get_task(task_id) | |
| except APIError as e: | |
| return f"❌ {e.detail}" | |
| status = last["status"] | |
| rows = last.get("rows_processed", 0) | |
| anomalies = last.get("anomalies_detected", 0) | |
| if status in ("COMPLETED", "FAILED"): | |
| break | |
| progress(0.5, desc=f"{status}: {rows:,} rows, {anomalies:,} anomalies") | |
| time.sleep(1.0) | |
| if last.get("status") == "COMPLETED": | |
| return ( | |
| f"✅ **Completed.** Processed {last['rows_processed']:,} rows, " | |
| f"detected {last['anomalies_detected']:,} anomalies.\n\n" | |
| "Switch to the **Analytics** tab to visualise the results." | |
| ) | |
| if last.get("status") == "FAILED": | |
| return f"❌ **Failed:** {last.get('error_message', 'unknown error')}" | |
| return "⏳ Still processing — check back shortly." | |
| # ----------------------------------------------------------------------- analytics | |
| def _empty_fig(message: str) -> go.Figure: | |
| fig = go.Figure() | |
| fig.add_annotation(text=message, showarrow=False, font={"size": 16}) | |
| fig.update_layout(template="plotly_white", height=300) | |
| return fig | |
| def render_charts(client: APIClient, machine_id): | |
| if not client or not client.is_authenticated: | |
| return _empty_fig("Please log in."), _empty_fig("Please log in."), "—" | |
| if not machine_id: | |
| return _empty_fig("Select a machine."), _empty_fig("Select a machine."), "—" | |
| try: | |
| series = client.get_series(machine_id, limit=10000) | |
| summary = client.machine_summary(machine_id) | |
| except APIError as e: | |
| return _empty_fig(e.detail), _empty_fig(e.detail), f"❌ {e.detail}" | |
| ts = series["timestamps"] | |
| if not ts: | |
| return ( | |
| _empty_fig("No telemetry yet. Upload a CSV first."), | |
| _empty_fig("No telemetry yet."), | |
| "No data.", | |
| ) | |
| is_anom = series["is_anomaly"] | |
| anom_idx = [i for i, a in enumerate(is_anom) if a] | |
| temp_fig = _metric_figure(ts, series["temperature"], anom_idx, "Temperature", "#e74c3c") | |
| vib_fig = _metric_figure(ts, series["vibration"], anom_idx, "Vibration", "#2980b9") | |
| md = ( | |
| f"### {summary['name']} — {STATUS_EMOJI.get(summary['status'], '')} {summary['status']}\n" | |
| f"- **Total readings:** {summary['telemetry_count']:,}\n" | |
| f"- **Anomalies:** {summary['anomaly_count']:,}\n" | |
| f"- **Last reading:** {summary.get('last_reading_at') or '—'}" | |
| ) | |
| return temp_fig, vib_fig, md | |
| def _metric_figure(ts, values, anom_idx, title, color) -> go.Figure: | |
| fig = go.Figure() | |
| fig.add_trace( | |
| go.Scatter(x=ts, y=values, mode="lines", name=title, line={"color": color, "width": 1.5}) | |
| ) | |
| if anom_idx: | |
| fig.add_trace( | |
| go.Scatter( | |
| x=[ts[i] for i in anom_idx], | |
| y=[values[i] for i in anom_idx], | |
| mode="markers", | |
| name="Anomaly", | |
| marker={"color": "red", "size": 7, "symbol": "x"}, | |
| ) | |
| ) | |
| fig.update_layout( | |
| title=title, | |
| template="plotly_white", | |
| height=350, | |
| margin={"l": 40, "r": 20, "t": 40, "b": 30}, | |
| legend={"orientation": "h", "yanchor": "bottom", "y": 1.02}, | |
| ) | |
| return fig | |
| # ----------------------------------------------------------------------- ai | |
| def run_ai(client: APIClient, machine_id, window): | |
| if not client or not client.is_authenticated: | |
| return "❌ Please log in first." | |
| if not machine_id: | |
| return "❌ Select a machine first." | |
| try: | |
| r = client.explain(machine_id, int(window)) | |
| except APIError as e: | |
| return f"❌ {e.detail}" | |
| badge = "🤖 *mock explainer*" if r["is_mock"] else f"🧠 *{r['model_used']}*" | |
| recs = "\n".join(f"- {rec}" for rec in r["recommendations"]) | |
| return ( | |
| f"## Maintenance Report — {r['machine_name']} {badge}\n\n" | |
| f"**Status:** {STATUS_EMOJI.get(r['machine_status'], '')} {r['machine_status']} | " | |
| f"**Window:** {r['window_analyzed']} readings | " | |
| f"**Anomalies:** {r['anomalies_found']}\n\n" | |
| f"### Summary\n{r['summary']}\n\n" | |
| f"### Explanation\n{r['explanation']}\n\n" | |
| f"### Recommendations\n{recs}" | |
| ) | |
| def _status_md(text: str) -> str: | |
| return text | |
| # --------------------------------------------------------------------------- UI | |
| def build_ui() -> gr.Blocks: | |
| with gr.Blocks(title="Predictive Maintenance", theme=gr.themes.Soft()) as demo: | |
| client_state = gr.State(value=None) | |
| gr.Markdown("# 🛠️ Predictive Maintenance Platform") | |
| with gr.Tabs(): # noqa: SIM117 - Gradio layout contexts nest idiomatically | |
| # ---- Tab 1: Dashboard & Auth ---- | |
| with gr.Tab("Dashboard & Auth"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Account") | |
| email_in = gr.Textbox(label="Email", placeholder="you@example.com") | |
| pw_in = gr.Textbox(label="Password", type="password") | |
| name_in = gr.Textbox(label="Full name (register only)") | |
| with gr.Row(): | |
| login_btn = gr.Button("Log in", variant="primary") | |
| register_btn = gr.Button("Register") | |
| logout_btn = gr.Button("Log out", size="sm") | |
| auth_status = gr.Markdown("_Not logged in._") | |
| with gr.Column(scale=2): | |
| gr.Markdown("### Registered Machines") | |
| dashboard_md = gr.Markdown("_Log in to view machines._") | |
| refresh_btn = gr.Button("🔄 Refresh", size="sm") | |
| gr.Markdown("### Register a New Machine") | |
| with gr.Row(): | |
| m_name = gr.Textbox(label="Name", scale=2) | |
| m_type = gr.Textbox( | |
| label="Type", placeholder="pump / motor / turbine", scale=2 | |
| ) | |
| m_loc = gr.Textbox(label="Location", scale=2) | |
| create_btn = gr.Button("➕ Create Machine", variant="primary") | |
| # ---- Tab 2: Telemetry Upload ---- | |
| with gr.Tab("Telemetry Upload"): | |
| gr.Markdown( | |
| "Upload a sensor CSV (columns: `timestamp, temperature, vibration, " | |
| "pressure, rotational_speed`). Large files are streamed and processed " | |
| "in chunks." | |
| ) | |
| upload_machine = gr.Dropdown(label="Machine", choices=[], interactive=True) | |
| csv_file = gr.File(label="Sensor CSV", file_types=[".csv"]) | |
| upload_btn = gr.Button("⬆️ Upload & Process", variant="primary") | |
| upload_status = gr.Markdown() | |
| # ---- Tab 3: Analytics & Visuals ---- | |
| with gr.Tab("Analytics & Visuals"): | |
| with gr.Row(): | |
| analytics_machine = gr.Dropdown(label="Machine", choices=[], interactive=True) | |
| load_charts_btn = gr.Button("📊 Load Charts", variant="primary") | |
| analytics_summary = gr.Markdown("—") | |
| temp_plot = gr.Plot(label="Temperature") | |
| vib_plot = gr.Plot(label="Vibration") | |
| # ---- Tab 4: AI Assistant ---- | |
| with gr.Tab("AI Assistant"): | |
| gr.Markdown( | |
| "Generate an AI-powered maintenance report. Uses OpenRouter " | |
| "(Gemini) when configured, otherwise a built-in rule-based explainer." | |
| ) | |
| with gr.Row(): | |
| ai_machine = gr.Dropdown(label="Machine", choices=[], interactive=True) | |
| ai_window = gr.Slider( | |
| label="Readings to analyse", minimum=10, maximum=5000, value=500, step=10 | |
| ) | |
| ai_btn = gr.Button("🧠 Generate Report", variant="primary") | |
| ai_output = gr.Markdown() | |
| # The upload/analytics/ai machine dropdowns are kept in sync after every | |
| # auth / create / refresh action via the .then(...) callbacks below. | |
| # --- wiring: auth --- | |
| login_btn.click( | |
| do_login, | |
| [client_state, email_in, pw_in], | |
| [client_state, auth_status, upload_machine, analytics_machine, dashboard_md], | |
| ).then(_sync_third_dropdown, [client_state], [ai_machine]) | |
| register_btn.click( | |
| do_register, | |
| [client_state, email_in, pw_in, name_in], | |
| [client_state, auth_status, upload_machine, analytics_machine, dashboard_md], | |
| ).then(_sync_third_dropdown, [client_state], [ai_machine]) | |
| logout_btn.click( | |
| do_logout, | |
| [client_state], | |
| [client_state, auth_status, upload_machine, analytics_machine, dashboard_md], | |
| ).then(_sync_third_dropdown, [client_state], [ai_machine]) | |
| create_btn.click( | |
| create_machine, | |
| [client_state, m_name, m_type, m_loc], | |
| [client_state, auth_status, upload_machine, analytics_machine, dashboard_md], | |
| ).then(_sync_third_dropdown, [client_state], [ai_machine]) | |
| refresh_btn.click( | |
| refresh_dashboard, | |
| [client_state], | |
| [upload_machine, analytics_machine, dashboard_md], | |
| ).then(_sync_third_dropdown, [client_state], [ai_machine]) | |
| # --- wiring: features --- | |
| upload_btn.click( | |
| upload_and_track, | |
| [client_state, upload_machine, csv_file], | |
| [upload_status], | |
| ) | |
| load_charts_btn.click( | |
| render_charts, | |
| [client_state, analytics_machine], | |
| [temp_plot, vib_plot, analytics_summary], | |
| ) | |
| ai_btn.click(run_ai, [client_state, ai_machine, ai_window], [ai_output]) | |
| return demo | |
| def _sync_third_dropdown(client: APIClient): | |
| choices = _machine_choices(client) | |
| return gr.update(choices=choices, value=choices[0][1] if choices else None) | |
| def main() -> None: | |
| host = os.getenv("FRONTEND_HOST", "0.0.0.0") | |
| port = int(os.getenv("FRONTEND_PORT", "7860")) | |
| build_ui().queue().launch(server_name=host, server_port=port) | |
| if __name__ == "__main__": | |
| main() | |