A web-based Yield Monitor dashboard for manufacturing test data, built with FastAPI and SQLite. It shows live testing volume, part-number distribution and per-part yield, lets you enter test results manually, exposes a small natural-language chatbot, and ships with an end-to-end Selenium test.
Yield = (Passed Units / Total Tested Units) × 100 e.g. 3 passed out of 5 tested = 60%
- FastAPI backend with a clean REST API and auto-generated Swagger docs.
- SQLite storage via the standard library (no DB server to install).
- Single-page dashboard with three panels:
- A – Daily Testing Volume — bar chart of units tested over the last 7 days.
- B – Part Number Distribution — doughnut/pie chart; click a slice (or the legend row) to select a part number.
- C – Yield Gauge — circular gauge for the selected part; colour-coded green ≥ 90%, yellow ≥ 80%, red < 80%.
- Manual Testing form (modal) with serial number, part-number dropdown, a live timestamp clock and a Pass/Fail checkbox.
- Header buttons:
Manual Test,View API(opens/docs),View Script(showstest_yield.py). - Yield chatbot — LLM-powered (OpenRouter), answers plain-English questions
grounded in the live data (
POST /chat). - Selenium test that validates the 60% yield scenario end-to-end.
yield_monitor/
├── main.py # FastAPI app entry point (routes, chatbot)
├── database.py # SQLite connection, schema and queries
├── templates/
│ └── index.html # Dashboard frontend (Chart.js)
├── static/
│ └── test_yield.py # Selenium script (served to the "View Script" button)
├── test_yield.py # Selenium script (run this one)
├── .env.example # Template for the chatbot API key
├── requirements.txt
└── README.md
| Method | Path | Description |
|---|---|---|
| POST | /tests |
Insert a new test record |
| GET | /tests |
Return all test records (newest first) |
| GET | /stats |
Per-part-number yield statistics |
| GET | /daily |
Daily unit count for the last 7 days |
| POST | /chat |
Natural-language question → answer |
| GET | /docs |
Swagger UI (built into FastAPI) |
POST /tests body
{ "serial_number": "SN-001", "part_number": "001PN001", "status": true }timestamp is set on the server at insert time; status is true = Pass.
Requires Python 3.10+ and Google Chrome (for the Selenium test).
# 1. create a virtual environment
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# 2. install dependencies
pip install -r requirements.txt
# 3. configure the chatbot LLM (OpenRouter)
cp .env.example .env
# then edit .env and set OPENROUTER_API_KEY=sk-or-v1-...
# 4. start the server
uvicorn main:app --reload
# 5. open the dashboard
# http://localhost:8000
# API docs: http://localhost:8000/docsThe SQLite database file yield_monitor.db is created automatically on first run.
The chatbot needs an OPENROUTER_API_KEY; without one the dashboard and all
other endpoints still work and the chatbot falls back to keyword matching.
With the server running in one terminal:
source .venv/bin/activate
python test_yield.pyThe test opens the dashboard, adds 5 records for 001PN001 (3 pass / 2 fail),
selects that part number and asserts the gauge reads 60%, printing
PASS or FAIL with the actual vs expected value. It runs Chrome headless, so
no visible browser window is needed.
The POST /chat endpoint is LLM-powered with tool calling. The model
(via OpenRouter, default openai/gpt-4o-mini, configurable with
CHAT_MODEL) interprets the question and calls tools; every count and yield
is computed in Python (get_yield_stats, list_daily_breakdown) against the
database, never by the model's own arithmetic — so the figures are always
exact. This lets it answer anything derivable from the data: a specific part, a
specific day, a date range, best/worst day, trends, and so on. Questions
that aren't about the system's testing/yield data get a friendly refusal.
If OPENROUTER_API_KEY is unset or the API call fails, /chat falls back to a
deterministic keyword matcher so the chat keeps working offline during a demo.
Example questions:
- "What is the yield for part 001PN001?"
- "What was the yield from 07.01 to 07.04?"
- "How many units of 001PN001 were tested on 2026-07-02?"
- "Which day had the highest yield?"
- "How many units were tested today?"
The app is a standard FastAPI ASGI application and runs anywhere that supports
Python (Replit, Render, Railway, PythonAnywhere). Set the OPENROUTER_API_KEY
environment variable on the host (and optionally CHAT_MODEL). Generic start
command:
uvicorn main:app --host 0.0.0.0 --port $PORT- Create the Repl from GitHub — on replit.com: Create Repl → Import from
GitHub → this repository. Replit reads the included
.replit/replit.nixand installsrequirements.txtautomatically. - Add the API key as a Secret — open the Secrets (🔒) tab and add
OPENROUTER_API_KEY= your OpenRouter key. (Do not put it in.envon a public Repl; Secrets are the safe place.) Optionally addCHAT_MODEL. AddSEED_DEMO=1to auto-populate a sample dataset on startup so the deployed dashboard and chatbot have data to show (see below). - Run / Deploy — press Run to test, then Deploy (Autoscale) to get a
public
*.replit.appURL the evaluator can open with no local setup.
The Selenium test is not run on the server — it's a local QA script that drives the deployed (or local) dashboard from your machine.
A fresh deployment starts with an empty database. Set the env var SEED_DEMO=1
to have the app populate a deterministic sample dataset on startup (only when
the table is empty): ~155 records across the last 7 days and all three part
numbers, with distinct yields so the gauge shows every colour band
(001PN001 ≈ 93%, 002PN002 = 76%, 003PN003 ≈ 87%).
SEED_DEMO is intentionally left unset locally so the Selenium test runs
against a clean database and produces exactly 60% for the 001PN001 scenario.
- SQLite + stdlib keeps the project dependency-light and instantly runnable;
statusis stored asINTEGER(0/1) since SQLite has no native boolean. /statsand/dailyalways return a complete set of part numbers / 7 days (zero-filled) so the charts have stable categories even with little data.- The Selenium script targets stable element IDs and clicks the legend row
to select a part number, which is far more reliable than clicking a pixel on a
<canvas>slice (the canvas slice is also clickable in the UI). - The chatbot uses grounded generation: live data is injected into the
prompt and the model is constrained to answer only from it. The API key is
read from the environment (
.env, gitignored) and never committed.