NDT Inspection Tracker Dashboard
Build a full-stack NDT inspection dashboard and deploy it to Vercel.
Introduction
30 Second Summary
Every pipeline, pressure vessel, and weld in the energy sector needs regular inspection to stay safe. The teams running these inspections track hundreds of jobs across different testing methods, technicians, and assets.
In this project, you will build a full-stack NDT Inspection Tracker Dashboard that serves realistic well services data through a Python API and displays it with interactive charts on a live, deployed website.
What You'll Build
A web-based NDT Inspection Tracker for a well services company, displaying real-time inspection KPIs, NDT method distribution, technician certifications, and pass/fail trends across pipeline and pressure vessel assets.
By the end of this project, you'll have:
- A FastAPI backend with REST endpoints serving inspection job data, technician records, and KPI summaries.
- An interactive dashboard with Chart.js visualizations showing a doughnut chart for method distribution, a bar chart for monthly trends, and color-coded KPI cards.
- A live deployment on Vercel accessible via a public URL, proving your ability to ship full-stack applications.
- Secret Mission: Add a dynamic filtering system that lets users filter inspections by NDT method, asset type, and date range with live chart updates.
Are there any prerequisites?
This project assumes intermediate Python knowledge and basic familiarity with HTML, CSS, and JavaScript. You will also need a free Vercel account, which you can create during the setup step.
Before We Start
Before you can build your NDT Inspection Tracker, you need to set up your development environment.
In this step, you will create a project folder, install the tools your app depends on, and scaffold the file structure that your backend and frontend will live in.
In this step, get ready to:
- Create the project folder and a Python virtual environment.
- Install FastAPI and the Vercel CLI.
- Set up the full file structure for your backend and frontend.
Create the project directory and virtual environment
A virtual environment isolates your project's Python packages from the rest of your system. This prevents version conflicts between projects.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Open the integrated terminal by pressing Ctrl+` (both macOS and Windows).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir ndt-tracker
cd ndt-tracker
- Open this folder in VS Code by clicking File in the top menu bar.
- Click Open Folder.
- Select the ndt-tracker folder on your Desktop, then click Open.
Why open the folder in VS Code?
Opening the project folder directly tells VS Code to treat ndt-tracker as your workspace root. This means the file explorer, terminal, and any extensions all work relative to your project.
- Open a fresh terminal inside the new workspace by pressing Ctrl+`.
- Create a Python virtual environment by running this command:
python -m venv .venv
What does this command do?
python -m venv .venv creates a folder called .venv inside your project. This folder contains a fresh copy of the Python interpreter and a place for packages to be installed in isolation.
- Activate the virtual environment by running the command for your operating system:
macOS
source .venv/bin/activate
Windows
.venv\Scripts\activate
You should see (.venv) appear at the start of your terminal prompt, confirming the virtual environment is active.
- Install FastAPI and its dependencies by running:
pip install "fastapi[standard]"
What is FastAPI?
FastAPI is a modern Python web framework for building APIs. It automatically generates interactive documentation for your endpoints and handles request validation using Python type hints.
After installation completes, you should see a success message listing all installed packages including fastapi and uvicorn.
Seeing a permissions error?
Make sure (.venv) appears in your terminal prompt. If it does not, your virtual environment is not active. Re-run the activation command above before trying the install again.
help me fix a permissions error when installing FastAPI
Install the Vercel CLI
Vercel is the platform you will deploy your dashboard to. Its CLI lets you deploy and test directly from your terminal.
- Install the Vercel CLI globally by running:
npm install -g vercel
- Verify the installation by running:
vercel --version
You should see a version number printed in your terminal, confirming the CLI is installed.
Command not found?
If npm is not recognized, you need Node.js installed. Download it from nodejs.org and restart your terminal after installation.
help me install Node.js so I can use npm to install the Vercel CLI
- Log in to your Vercel account by running:
vercel login
What happens when you log in?
This command opens your default browser and asks you to authorize the CLI. Once you approve in the browser, the terminal confirms you are logged in.
If the browser does not open automatically, the CLI provides a URL you can visit manually.
You should see a confirmation message in your terminal saying you are logged in.
Create the project file structure
Your project needs two directories: api/ for the Python backend and public/ for the HTML dashboard.
- Create the directories and files by running these commands:
macOS
mkdir api public
touch api/__init__.py api/data.py api/index.py
touch public/index.html public/styles.css
touch pyproject.toml vercel.json
Windows
mkdir api, public
New-Item api/__init__.py, api/data.py, api/index.py -Force
New-Item public/index.html, public/styles.css -Force
New-Item pyproject.toml, vercel.json -Force
What are these files for?
- api/__init__.py makes the api folder a Python package so modules can import from each other.
- api/data.py will hold your sample NDT inspection records.
- api/index.py will hold your FastAPI application and API endpoints.
- public/index.html will be your dashboard frontend.
- public/styles.css will hold your dashboard styling.
- pyproject.toml defines your Python dependencies for Vercel.
- vercel.json configures how Vercel routes requests between your API and static files.
- Confirm the files exist by running:
ls
You should see api, public, pyproject.toml, and vercel.json listed in the output.
- Verify the contents of the api folder by running:
ls api
You should see __init__.py, data.py, and index.py.
Missing files?
Double-check that your terminal is inside the ndt-tracker folder. Run pwd (macOS) or cd (Windows) to see your current directory. If you are not inside ndt-tracker, navigate there and re-run the file creation commands.
help me fix my project file structure
Your development environment is ready. Next up, you will build the FastAPI backend that serves your NDT inspection data through REST endpoints.
Build the FastAPI Backend
Your project structure is in place. Now you need the backend to serve inspection data to the dashboard.
In this step, you will create realistic NDT inspection records and build a FastAPI application that serves this data through four REST endpoints. By the end, you will have a working API with auto-generated documentation you can test in your browser.
In this step, get ready to:
- Create sample NDT inspection and technician data.
- Build four API endpoints for inspections, technicians, KPIs, and trends.
- Test the API locally using FastAPI's interactive Swagger documentation.
Create the inspection data
Every dashboard needs data behind it. You will populate api/data.py with realistic NDT inspection records covering pipelines, pressure vessels, and welds across five standard inspection methods.
- In VS Code, open api/data.py from the Explorer sidebar.
- Add the inspection records list by pasting the following code:
inspections = [
{
"job_id": "NDT-2026-001",
"asset_name": "Pipeline Section A-12",
"asset_type": "pipeline",
"ndt_method": "UT",
"inspector_name": "Ahmed Al-Rashid",
"inspection_date": "2026-06-15",
"result": "pass",
"defect_type": None,
"wall_thickness_mm": 12.4,
"location": "Well Pad 3 - North Field"
},
{
"job_id": "NDT-2026-002",
"asset_name": "Pressure Vessel PV-08",
"asset_type": "pressure_vessel",
"ndt_method": "RT",
"inspector_name": "James Okonkwo",
"inspection_date": "2026-06-16",
"result": "fail",
"defect_type": "porosity",
"wall_thickness_mm": 18.2,
"location": "Processing Plant B"
},
{
"job_id": "NDT-2026-003",
"asset_name": "Weld Joint WJ-44",
"asset_type": "weld",
"ndt_method": "MT",
"inspector_name": "Ahmed Al-Rashid",
"inspection_date": "2026-06-17",
"result": "pass",
"defect_type": None,
"wall_thickness_mm": None,
"location": "Pipeline Tie-In Station 7"
},
{
"job_id": "NDT-2026-004",
"asset_name": "Pipeline Section C-03",
"asset_type": "pipeline",
"ndt_method": "UT",
"inspector_name": "Sarah Mitchell",
"inspection_date": "2026-06-18",
"result": "requires_follow_up",
"defect_type": "wall_thinning",
"wall_thickness_mm": 8.1,
"location": "Subsea Crossing 2"
},
{
"job_id": "NDT-2026-005",
"asset_name": "Weld Joint WJ-51",
"asset_type": "weld",
"ndt_method": "PT",
"inspector_name": "James Okonkwo",
"inspection_date": "2026-06-19",
"result": "pass",
"defect_type": None,
"wall_thickness_mm": None,
"location": "Manifold Station 4"
},
{
"job_id": "NDT-2026-006",
"asset_name": "Storage Tank TK-02",
"asset_type": "pressure_vessel",
"ndt_method": "VT",
"inspector_name": "Sarah Mitchell",
"inspection_date": "2026-06-20",
"result": "pass",
"defect_type": None,
"wall_thickness_mm": 22.0,
"location": "Tank Farm East"
},
{
"job_id": "NDT-2026-007",
"asset_name": "Pipeline Section B-07",
"asset_type": "pipeline",
"ndt_method": "UT",
"inspector_name": "Ahmed Al-Rashid",
"inspection_date": "2026-06-22",
"result": "fail",
"defect_type": "lamination",
"wall_thickness_mm": 9.8,
"location": "Well Pad 5 - South Field"
},
{
"job_id": "NDT-2026-008",
"asset_name": "Pressure Vessel PV-11",
"asset_type": "pressure_vessel",
"ndt_method": "RT",
"inspector_name": "James Okonkwo",
"inspection_date": "2026-06-23",
"result": "pass",
"defect_type": None,
"wall_thickness_mm": 15.6,
"location": "Processing Plant A"
},
{
"job_id": "NDT-2026-009",
"asset_name": "Weld Joint WJ-67",
"asset_type": "weld",
"ndt_method": "MT",
"inspector_name": "Sarah Mitchell",
"inspection_date": "2026-06-25",
"result": "requires_follow_up",
"defect_type": "surface_crack",
"wall_thickness_mm": None,
"location": "Pipeline Tie-In Station 12"
},
{
"job_id": "NDT-2026-010",
"asset_name": "Pipeline Section D-01",
"asset_type": "pipeline",
"ndt_method": "UT",
"inspector_name": "Ahmed Al-Rashid",
"inspection_date": "2026-07-01",
"result": "pass",
"defect_type": None,
"wall_thickness_mm": 14.2,
"location": "Export Line Terminal"
},
{
"job_id": "NDT-2026-011",
"asset_name": "Weld Joint WJ-72",
"asset_type": "weld",
"ndt_method": "PT",
"inspector_name": "James Okonkwo",
"inspection_date": "2026-07-02",
"result": "pass",
"defect_type": None,
"wall_thickness_mm": None,
"location": "Manifold Station 6"
},
{
"job_id": "NDT-2026-012",
"asset_name": "Pressure Vessel PV-03",
"asset_type": "pressure_vessel",
"ndt_method": "VT",
"inspector_name": "Sarah Mitchell",
"inspection_date": "2026-07-03",
"result": "fail",
"defect_type": "external_corrosion",
"wall_thickness_mm": 19.5,
"location": "Tank Farm West"
}
]
What does this data represent?
Each inspection record models a real NDT job in the oil and gas industry. The five ndt_method values (UT, RT, MT, PT, VT) are the standard non-destructive testing methods used in pipeline integrity and pressure vessel inspections.
The result field uses three states: pass, fail, and requires_follow_up. These match how inspection outcomes are tracked in practice.
- Below the inspections list, add the technician records:
technicians = [
{
"id": "TECH-001",
"name": "Ahmed Al-Rashid",
"certification_level": "ASNT Level III",
"certifications": ["UT", "MT", "VT"],
"expiry_date": "2027-03-15",
"status": "active"
},
{
"id": "TECH-002",
"name": "James Okonkwo",
"certification_level": "ASNT Level II",
"certifications": ["RT", "PT", "VT"],
"expiry_date": "2026-08-20",
"status": "active"
},
{
"id": "TECH-003",
"name": "Sarah Mitchell",
"certification_level": "ASNT Level II",
"certifications": ["UT", "MT", "VT", "PT"],
"expiry_date": "2026-07-30",
"status": "expiring_soon"
}
]
What are ASNT certification levels?
ASNT Level II technicians can perform examinations and interpret results. ASNT Level III technicians handle procedure development, technique qualification, and program administration. The status field tracks whether a certification is active or approaching its expiry date.
- Save api/data.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved api/data.py before moving on.
ⓧ I'd like to double check the full code
inspections = [
{
"job_id": "NDT-2026-001",
"asset_name": "Pipeline Section A-12",
"asset_type": "pipeline",
"ndt_method": "UT",
"inspector_name": "Ahmed Al-Rashid",
"inspection_date": "2026-06-15",
"result": "pass",
"defect_type": None,
"wall_thickness_mm": 12.4,
"location": "Well Pad 3 - North Field"
},
{
"job_id": "NDT-2026-002",
"asset_name": "Pressure Vessel PV-08",
"asset_type": "pressure_vessel",
"ndt_method": "RT",
"inspector_name": "James Okonkwo",
"inspection_date": "2026-06-16",
"result": "fail",
"defect_type": "porosity",
"wall_thickness_mm": 18.2,
"location": "Processing Plant B"
},
{
"job_id": "NDT-2026-003",
"asset_name": "Weld Joint WJ-44",
"asset_type": "weld",
"ndt_method": "MT",
"inspector_name": "Ahmed Al-Rashid",
"inspection_date": "2026-06-17",
"result": "pass",
"defect_type": None,
"wall_thickness_mm": None,
"location": "Pipeline Tie-In Station 7"
},
{
"job_id": "NDT-2026-004",
"asset_name": "Pipeline Section C-03",
"asset_type": "pipeline",
"ndt_method": "UT",
"inspector_name": "Sarah Mitchell",
"inspection_date": "2026-06-18",
"result": "requires_follow_up",
"defect_type": "wall_thinning",
"wall_thickness_mm": 8.1,
"location": "Subsea Crossing 2"
},
{
"job_id": "NDT-2026-005",
"asset_name": "Weld Joint WJ-51",
"asset_type": "weld",
"ndt_method": "PT",
"inspector_name": "James Okonkwo",
"inspection_date": "2026-06-19",
"result": "pass",
"defect_type": None,
"wall_thickness_mm": None,
"location": "Manifold Station 4"
},
{
"job_id": "NDT-2026-006",
"asset_name": "Storage Tank TK-02",
"asset_type": "pressure_vessel",
"ndt_method": "VT",
"inspector_name": "Sarah Mitchell",
"inspection_date": "2026-06-20",
"result": "pass",
"defect_type": None,
"wall_thickness_mm": 22.0,
"location": "Tank Farm East"
},
{
"job_id": "NDT-2026-007",
"asset_name": "Pipeline Section B-07",
"asset_type": "pipeline",
"ndt_method": "UT",
"inspector_name": "Ahmed Al-Rashid",
"inspection_date": "2026-06-22",
"result": "fail",
"defect_type": "lamination",
"wall_thickness_mm": 9.8,
"location": "Well Pad 5 - South Field"
},
{
"job_id": "NDT-2026-008",
"asset_name": "Pressure Vessel PV-11",
"asset_type": "pressure_vessel",
"ndt_method": "RT",
"inspector_name": "James Okonkwo",
"inspection_date": "2026-06-23",
"result": "pass",
"defect_type": None,
"wall_thickness_mm": 15.6,
"location": "Processing Plant A"
},
{
"job_id": "NDT-2026-009",
"asset_name": "Weld Joint WJ-67",
"asset_type": "weld",
"ndt_method": "MT",
"inspector_name": "Sarah Mitchell",
"inspection_date": "2026-06-25",
"result": "requires_follow_up",
"defect_type": "surface_crack",
"wall_thickness_mm": None,
"location": "Pipeline Tie-In Station 12"
},
{
"job_id": "NDT-2026-010",
"asset_name": "Pipeline Section D-01",
"asset_type": "pipeline",
"ndt_method": "UT",
"inspector_name": "Ahmed Al-Rashid",
"inspection_date": "2026-07-01",
"result": "pass",
"defect_type": None,
"wall_thickness_mm": 14.2,
"location": "Export Line Terminal"
},
{
"job_id": "NDT-2026-011",
"asset_name": "Weld Joint WJ-72",
"asset_type": "weld",
"ndt_method": "PT",
"inspector_name": "James Okonkwo",
"inspection_date": "2026-07-02",
"result": "pass",
"defect_type": None,
"wall_thickness_mm": None,
"location": "Manifold Station 6"
},
{
"job_id": "NDT-2026-012",
"asset_name": "Pressure Vessel PV-03",
"asset_type": "pressure_vessel",
"ndt_method": "VT",
"inspector_name": "Sarah Mitchell",
"inspection_date": "2026-07-03",
"result": "fail",
"defect_type": "external_corrosion",
"wall_thickness_mm": 19.5,
"location": "Tank Farm West"
}
]
technicians = [
{
"id": "TECH-001",
"name": "Ahmed Al-Rashid",
"certification_level": "ASNT Level III",
"certifications": ["UT", "MT", "VT"],
"expiry_date": "2027-03-15",
"status": "active"
},
{
"id": "TECH-002",
"name": "James Okonkwo",
"certification_level": "ASNT Level II",
"certifications": ["RT", "PT", "VT"],
"expiry_date": "2026-08-20",
"status": "active"
},
{
"id": "TECH-003",
"name": "Sarah Mitchell",
"certification_level": "ASNT Level II",
"certifications": ["UT", "MT", "VT", "PT"],
"expiry_date": "2026-07-30",
"status": "expiring_soon"
}
]
Build the FastAPI application
With the data ready, you will now create the API layer that exposes it through four endpoints. The frontend dashboard will call these endpoints to load inspection data, technician records, computed KPIs, and trend summaries.
- Open api/index.py in VS Code.
- Add the imports, app initialization, and the two simple data endpoints by pasting this code:
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pathlib import Path
from api.data import inspections, technicians
app = FastAPI(title="NDT Inspection Tracker API")
@app.get("/api/inspections")
def get_inspections():
# Return all inspection records
return inspections
@app.get("/api/technicians")
def get_technicians():
# Return all technician certification data
return technicians
What does this code do?
The FastAPI() constructor creates your application instance. The title parameter sets the name shown in the auto-generated API documentation.
The @app.get() decorator registers each function as a GET endpoint at the specified path. These first two endpoints simply return the raw data lists as JSON.
- Below the get_technicians function, add the KPI computation endpoint:
@app.get("/api/kpis")
def get_kpis():
# Calculate summary statistics from inspection data
total = len(inspections)
passed = sum(1 for i in inspections if i["result"] == "pass")
failed = sum(1 for i in inspections if i["result"] == "fail")
follow_up = sum(1 for i in inspections if i["result"] == "requires_follow_up")
pass_rate = round((passed / total) * 100, 1) if total > 0 else 0
# Count inspections per NDT method
methods = {}
for i in inspections:
method = i["ndt_method"]
methods[method] = methods.get(method, 0) + 1
# Technician certification status
active_techs = sum(1 for t in technicians if t["status"] == "active")
expiring_certs = sum(1 for t in technicians if t["status"] == "expiring_soon")
return {
"total_inspections": total,
"passed": passed,
"failed": failed,
"requires_follow_up": follow_up,
"pass_rate": pass_rate,
"active_technicians": active_techs,
"expiring_certifications": expiring_certs,
"inspections_by_method": methods
}
What does the KPI endpoint compute?
This endpoint processes the raw inspection list into dashboard-ready summaries. It counts pass, fail, and follow-up results, calculates the overall pass rate as a percentage, tallies inspections by NDT method, and checks technician certification statuses.
The dashboard will use this single endpoint to populate all four KPI cards and the method distribution chart.
- Below the get_kpis function, add the trends endpoint:
@app.get("/api/trends")
def get_trends():
# Group inspections by month and result
monthly = {}
for i in inspections:
month = i["inspection_date"][:7]
if month not in monthly:
monthly[month] = {"pass": 0, "fail": 0, "requires_follow_up": 0}
monthly[month][i["result"]] += 1
return {
"months": sorted(monthly.keys()),
"data": {month: monthly[month] for month in sorted(monthly.keys())}
}
What does the trends endpoint do?
This endpoint slices each inspection date to its year-month ([:7] gives 2026-06 from 2026-06-15), then counts pass, fail, and follow-up results per month. The bar chart on the dashboard will use this to show monthly inspection trends.
- Finally, add the static file serving section at the bottom of the file:
public_path = Path(__file__).parent.parent / "public"
@app.get("/")
def serve_dashboard():
# Serve the HTML dashboard at the root URL
return FileResponse(public_path / "index.html")
app.mount("/", StaticFiles(directory=str(public_path)), name="static")
Why serve static files from FastAPI?
The public_path variable points to the public/ folder relative to this file. The root route (/) returns index.html directly, and app.mount() makes all other files in public/ (like CSS) accessible to the browser.
When deployed to Vercel, static files are served from Vercel's CDN instead. This setup lets you test the full app locally.
- Save api/index.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved api/index.py before moving on.
ⓧ I'd like to double check the full code
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pathlib import Path
from api.data import inspections, technicians
app = FastAPI(title="NDT Inspection Tracker API")
@app.get("/api/inspections")
def get_inspections():
return inspections
@app.get("/api/technicians")
def get_technicians():
return technicians
@app.get("/api/kpis")
def get_kpis():
total = len(inspections)
passed = sum(1 for i in inspections if i["result"] == "pass")
failed = sum(1 for i in inspections if i["result"] == "fail")
follow_up = sum(1 for i in inspections if i["result"] == "requires_follow_up")
pass_rate = round((passed / total) * 100, 1) if total > 0 else 0
methods = {}
for i in inspections:
method = i["ndt_method"]
methods[method] = methods.get(method, 0) + 1
active_techs = sum(1 for t in technicians if t["status"] == "active")
expiring_certs = sum(1 for t in technicians if t["status"] == "expiring_soon")
return {
"total_inspections": total,
"passed": passed,
"failed": failed,
"requires_follow_up": follow_up,
"pass_rate": pass_rate,
"active_technicians": active_techs,
"expiring_certifications": expiring_certs,
"inspections_by_method": methods
}
@app.get("/api/trends")
def get_trends():
monthly = {}
for i in inspections:
month = i["inspection_date"][:7]
if month not in monthly:
monthly[month] = {"pass": 0, "fail": 0, "requires_follow_up": 0}
monthly[month][i["result"]] += 1
return {
"months": sorted(monthly.keys()),
"data": {month: monthly[month] for month in sorted(monthly.keys())}
}
public_path = Path(__file__).parent.parent / "public"
@app.get("/")
def serve_dashboard():
return FileResponse(public_path / "index.html")
app.mount("/", StaticFiles(directory=str(public_path)), name="static")
Test the API locally
FastAPI auto-generates interactive documentation for your API using Swagger UI. You can use this to verify all four endpoints are working correctly before building the frontend.
- Start the development server by running this command in your terminal:
uvicorn api.index:app --reload
What does this command do?
The uvicorn server runs your FastAPI app. api.index:app tells it to look for the app object inside api/index.py. The --reload flag automatically restarts the server when you save code changes.
- Open your browser and navigate to http://127.0.0.1:8000/docs.
You should see the Swagger UI page titled "NDT Inspection Tracker API" with all four endpoints listed: /api/inspections, /api/technicians, /api/kpis, and /api/trends.
- Click on the /api/kpis endpoint to expand it.
- Click Try it out, then click Execute.
You should see a JSON response containing total_inspections, pass_rate, inspections_by_method, and other computed values.
Seeing a ModuleNotFoundError?
If you see an error about api.data not being found, make sure the api/__init__.py file exists. This empty file tells Python that api/ is a package. Also confirm your terminal is in the ndt-tracker root folder, not inside the api/ subfolder.
help me fix ModuleNotFoundError for api.data in my FastAPI app
Your backend is serving real NDT inspection data through a clean REST API. Next up, you will build the frontend dashboard that visualizes this data with interactive charts and KPI cards.
Build the Dashboard Frontend
Your API is serving inspection data, but raw JSON responses are not useful for operations managers who need to spot trends at a glance.
In this step, you will build an interactive HTML dashboard that pulls live data from your API endpoints and renders it as KPI cards, charts, and a sortable table.
In this step, get ready to:
- Create the HTML layout with KPI cards, chart canvases, and an inspections table.
- Load Chart.js and write JavaScript to fetch API data and render interactive charts.
- Style the dashboard with a professional dark theme using CSS Grid.
Create the HTML dashboard layout
The dashboard needs a clear visual hierarchy. KPI summary cards go at the top for instant status, charts in the middle for trend analysis, and a detailed table at the bottom for individual records.
- In VS Code, open public/index.html and paste the following HTML structure:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NDT Inspection Tracker | Well Services</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<header>
<h1>NDT Inspection Tracker</h1>
<p>Well Services & Non-Destructive Testing Operations</p>
</header>
<main>
<section class="kpi-grid">
<div class="kpi-card kpi-total">
<span class="kpi-value" id="total-inspections">--</span>
<span class="kpi-label">Total Inspections</span>
</div>
<div class="kpi-card kpi-pass">
<span class="kpi-value" id="pass-rate">--</span>
<span class="kpi-label">Pass Rate</span>
</div>
<div class="kpi-card kpi-techs">
<span class="kpi-value" id="active-techs">--</span>
<span class="kpi-label">Active Technicians</span>
</div>
<div class="kpi-card kpi-warning">
<span class="kpi-value" id="expiring-certs">--</span>
<span class="kpi-label">Expiring Certifications</span>
</div>
</section>
<section class="charts-grid">
<div class="chart-container">
<h2>Inspections by NDT Method</h2>
<canvas id="methodChart"></canvas>
</div>
<div class="chart-container">
<h2>Monthly Inspection Trends</h2>
<canvas id="trendsChart"></canvas>
</div>
</section>
<section class="table-section">
<h2>Recent Inspections</h2>
<table id="inspections-table">
<thead>
<tr>
<th>Job ID</th>
<th>Asset</th>
<th>Method</th>
<th>Inspector</th>
<th>Date</th>
<th>Result</th>
</tr>
</thead>
<tbody id="inspections-body">
</tbody>
</table>
</section>
</main>
</body>
</html>
What does this HTML do?
The page has three visual sections stacked vertically inside <main>.
- The kpi-grid section holds four cards. Each card has a kpi-value span (placeholder --) and a label. JavaScript will fill these with live numbers later.
- The charts-grid section contains two <canvas> elements. Chart.js draws directly onto canvas elements to render the doughnut and bar charts.
- The table-section provides a table with headers for each inspection field. The <tbody> is empty because JavaScript will populate rows dynamically.
- Save public/index.html.
You won't see a styled result yet. The next two chunks add the JavaScript logic and CSS that bring the page to life.
Add Chart.js and the data-loading script
Chart.js is a lightweight charting library that renders responsive visualizations on HTML canvas elements. Loading it from a CDN means zero build tooling.
What is Chart.js?
Chart.js is an open-source JavaScript library that creates interactive, responsive charts using the HTML5 <canvas> element. It supports dozens of chart types including doughnut, bar, line, and scatter plots.
You load it via a CDN link, which means no package installation or build step is required.
- In public/index.html, find the closing </main> tag. Add the following script block between </main> and </body>:
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
async function loadDashboard() {
const [kpisRes, trendsRes, inspectionsRes] = await Promise.all([
fetch('/api/kpis'),
fetch('/api/trends'),
fetch('/api/inspections')
]);
const kpis = await kpisRes.json();
const trends = await trendsRes.json();
const inspections = await inspectionsRes.json();
document.getElementById('total-inspections').textContent = kpis.total_inspections;
document.getElementById('pass-rate').textContent = kpis.pass_rate + '%';
document.getElementById('active-techs').textContent = kpis.active_technicians;
document.getElementById('expiring-certs').textContent = kpis.expiring_certifications;
const methodCtx = document.getElementById('methodChart').getContext('2d');
new Chart(methodCtx, {
type: 'doughnut',
data: {
labels: Object.keys(kpis.inspections_by_method),
datasets: [{
data: Object.values(kpis.inspections_by_method),
backgroundColor: ['#3498db', '#e74c3c', '#f39c12', '#9b59b6', '#2ecc71']
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom',
labels: { color: '#ecf0f1' }
}
}
}
});
const trendsCtx = document.getElementById('trendsChart').getContext('2d');
new Chart(trendsCtx, {
type: 'bar',
data: {
labels: trends.months,
datasets: [
{
label: 'Pass',
data: trends.months.map(m => trends.data[m].pass),
backgroundColor: '#2ecc71'
},
{
label: 'Fail',
data: trends.months.map(m => trends.data[m].fail),
backgroundColor: '#e74c3c'
},
{
label: 'Follow-up',
data: trends.months.map(m => trends.data[m].requires_follow_up),
backgroundColor: '#f39c12'
}
]
},
options: {
responsive: true,
scales: {
x: { ticks: { color: '#ecf0f1' }, grid: { color: '#34495e' } },
y: { beginAtZero: true, ticks: { color: '#ecf0f1' }, grid: { color: '#34495e' } }
},
plugins: {
legend: { labels: { color: '#ecf0f1' } }
}
}
});
const tbody = document.getElementById('inspections-body');
inspections.forEach(insp => {
const row = document.createElement('tr');
const resultClass = insp.result === 'pass' ? 'result-pass' :
insp.result === 'fail' ? 'result-fail' : 'result-followup';
row.innerHTML = `
<td>${insp.job_id}</td>
<td>${insp.asset_name}</td>
<td>${insp.ndt_method}</td>
<td>${insp.inspector_name}</td>
<td>${insp.inspection_date}</td>
<td class="${resultClass}">${insp.result.replace('_', ' ')}</td>
`;
tbody.appendChild(row);
});
}
loadDashboard();
</script>
What does this script do?
The loadDashboard() function runs as soon as the page loads. It fetches data from all three API endpoints in parallel using Promise.all().
- KPI values from /api/kpis are inserted into the four card spans by their element IDs.
- The doughnut chart uses inspections_by_method to show how many inspections used each NDT method (UT, RT, MT, PT, VT).
- The bar chart uses /api/trends data to display monthly pass, fail, and follow-up counts as stacked bars.
- The table loop creates a row for each inspection, applying color classes (result-pass, result-fail, result-followup) to the result cell.
- Save public/index.html.
You won't see a change yet because the CSS file is still empty. The next chunk adds the styling.
Seeing a blank page later?
- Make sure the script block is placed between </main> and </body>. If it appears inside <main>, the page structure breaks.
- Check your browser console for JavaScript errors. A typo in a fetch() URL (like missing the /api/ prefix) will silently fail.
- Confirm the CDN script tag is above your inline <script> block. Chart.js must load first.
Still stuck? Help me debug why my dashboard shows a blank page.
✔️ Awesome, I've got everything!
Great. Double check you have saved public/index.html before moving on.
ⓧ I'd like to double check the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NDT Inspection Tracker | Well Services</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<header>
<h1>NDT Inspection Tracker</h1>
<p>Well Services & Non-Destructive Testing Operations</p>
</header>
<main>
<section class="kpi-grid">
<div class="kpi-card kpi-total">
<span class="kpi-value" id="total-inspections">--</span>
<span class="kpi-label">Total Inspections</span>
</div>
<div class="kpi-card kpi-pass">
<span class="kpi-value" id="pass-rate">--</span>
<span class="kpi-label">Pass Rate</span>
</div>
<div class="kpi-card kpi-techs">
<span class="kpi-value" id="active-techs">--</span>
<span class="kpi-label">Active Technicians</span>
</div>
<div class="kpi-card kpi-warning">
<span class="kpi-value" id="expiring-certs">--</span>
<span class="kpi-label">Expiring Certifications</span>
</div>
</section>
<section class="charts-grid">
<div class="chart-container">
<h2>Inspections by NDT Method</h2>
<canvas id="methodChart"></canvas>
</div>
<div class="chart-container">
<h2>Monthly Inspection Trends</h2>
<canvas id="trendsChart"></canvas>
</div>
</section>
<section class="table-section">
<h2>Recent Inspections</h2>
<table id="inspections-table">
<thead>
<tr>
<th>Job ID</th>
<th>Asset</th>
<th>Method</th>
<th>Inspector</th>
<th>Date</th>
<th>Result</th>
</tr>
</thead>
<tbody id="inspections-body">
</tbody>
</table>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
async function loadDashboard() {
const [kpisRes, trendsRes, inspectionsRes] = await Promise.all([
fetch('/api/kpis'),
fetch('/api/trends'),
fetch('/api/inspections')
]);
const kpis = await kpisRes.json();
const trends = await trendsRes.json();
const inspections = await inspectionsRes.json();
document.getElementById('total-inspections').textContent = kpis.total_inspections;
document.getElementById('pass-rate').textContent = kpis.pass_rate + '%';
document.getElementById('active-techs').textContent = kpis.active_technicians;
document.getElementById('expiring-certs').textContent = kpis.expiring_certifications;
const methodCtx = document.getElementById('methodChart').getContext('2d');
new Chart(methodCtx, {
type: 'doughnut',
data: {
labels: Object.keys(kpis.inspections_by_method),
datasets: [{
data: Object.values(kpis.inspections_by_method),
backgroundColor: ['#3498db', '#e74c3c', '#f39c12', '#9b59b6', '#2ecc71']
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom',
labels: { color: '#ecf0f1' }
}
}
}
});
const trendsCtx = document.getElementById('trendsChart').getContext('2d');
new Chart(trendsCtx, {
type: 'bar',
data: {
labels: trends.months,
datasets: [
{
label: 'Pass',
data: trends.months.map(m => trends.data[m].pass),
backgroundColor: '#2ecc71'
},
{
label: 'Fail',
data: trends.months.map(m => trends.data[m].fail),
backgroundColor: '#e74c3c'
},
{
label: 'Follow-up',
data: trends.months.map(m => trends.data[m].requires_follow_up),
backgroundColor: '#f39c12'
}
]
},
options: {
responsive: true,
scales: {
x: { ticks: { color: '#ecf0f1' }, grid: { color: '#34495e' } },
y: { beginAtZero: true, ticks: { color: '#ecf0f1' }, grid: { color: '#34495e' } }
},
plugins: {
legend: { labels: { color: '#ecf0f1' } }
}
}
});
const tbody = document.getElementById('inspections-body');
inspections.forEach(insp => {
const row = document.createElement('tr');
const resultClass = insp.result === 'pass' ? 'result-pass' :
insp.result === 'fail' ? 'result-fail' : 'result-followup';
row.innerHTML = `
<td>${insp.job_id}</td>
<td>${insp.asset_name}</td>
<td>${insp.ndt_method}</td>
<td>${insp.inspector_name}</td>
<td>${insp.inspection_date}</td>
<td class="${resultClass}">${insp.result.replace('_', ' ')}</td>
`;
tbody.appendChild(row);
});
}
loadDashboard();
</script>
</body>
</html>
Style the dashboard with a dark theme
The dashboard needs a dark color scheme that matches the operational feel of an industrial monitoring tool. CSS Grid handles the responsive card and chart layouts.
- Open public/styles.css and paste the following stylesheet:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #1a1a2e;
color: #ecf0f1;
min-height: 100vh;
}
header {
background-color: #16213e;
padding: 1.5rem 2rem;
border-bottom: 3px solid #0f3460;
}
header h1 {
font-size: 1.8rem;
color: #e94560;
}
header p {
color: #a0a0b0;
margin-top: 0.25rem;
}
main {
padding: 2rem;
max-width: 1400px;
margin: 0 auto;
}
.kpi-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.kpi-card {
background-color: #16213e;
border-radius: 8px;
padding: 1.5rem;
text-align: center;
border-left: 4px solid #0f3460;
}
.kpi-card.kpi-total {
border-left-color: #3498db;
}
.kpi-card.kpi-pass {
border-left-color: #2ecc71;
}
.kpi-card.kpi-techs {
border-left-color: #9b59b6;
}
.kpi-card.kpi-warning {
border-left-color: #f39c12;
}
.kpi-value {
display: block;
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.kpi-label {
font-size: 0.9rem;
color: #a0a0b0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.charts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
margin-bottom: 2rem;
}
.chart-container {
background-color: #16213e;
border-radius: 8px;
padding: 1.5rem;
}
.chart-container h2 {
font-size: 1.1rem;
margin-bottom: 1rem;
color: #ecf0f1;
}
.table-section {
background-color: #16213e;
border-radius: 8px;
padding: 1.5rem;
}
.table-section h2 {
font-size: 1.1rem;
margin-bottom: 1rem;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid #2c3e50;
}
th {
background-color: #0f3460;
font-weight: 600;
text-transform: uppercase;
font-size: 0.8rem;
letter-spacing: 0.5px;
}
tr:hover {
background-color: #1e3a5f;
}
.result-pass {
color: #2ecc71;
font-weight: 600;
}
.result-fail {
color: #e74c3c;
font-weight: 600;
}
.result-followup {
color: #f39c12;
font-weight: 600;
}
@media (max-width: 768px) {
.charts-grid {
grid-template-columns: 1fr;
}
.kpi-grid {
grid-template-columns: repeat(2, 1fr);
}
}
What do these styles do?
- The dark background (#1a1a2e) with light text (#ecf0f1) creates an industrial monitoring aesthetic.
- Each KPI card gets a color-coded left border. Blue for total count, green for pass rate, purple for technicians, and amber for warnings.
- CSS Grid with repeat(auto-fit, minmax(200px, 1fr)) makes the KPI cards wrap responsively on smaller screens.
- The media query at 768px switches the charts from a two-column grid to a single column on mobile devices.
- Save public/styles.css.
- Start your dev server by running this command in the terminal:
uvicorn api.index:app --reload
- Open http://127.0.0.1:8000 in your browser. You should see a dark-themed dashboard with four KPI cards showing numbers (12 total inspections, 66.7% pass rate, 2 active technicians, 1 expiring certification), a doughnut chart on the left, a bar chart on the right, and a table of recent inspections below.
Charts not rendering?
- Open your browser's developer console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows). Look for 404 errors on the /api/ endpoints.
- If you see Chart is not defined, the CDN script tag may be missing or placed after your inline script.
- If the KPI cards show -- but no numbers, your API server might not be running. Check the terminal for the uvicorn process.
Need more help? Help me figure out why my Chart.js charts are not rendering.
✔️ Awesome, I've got everything!
Great. Double check you have saved public/styles.css before moving on.
ⓧ I'd like to double check the full code
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #1a1a2e;
color: #ecf0f1;
min-height: 100vh;
}
header {
background-color: #16213e;
padding: 1.5rem 2rem;
border-bottom: 3px solid #0f3460;
}
header h1 {
font-size: 1.8rem;
color: #e94560;
}
header p {
color: #a0a0b0;
margin-top: 0.25rem;
}
main {
padding: 2rem;
max-width: 1400px;
margin: 0 auto;
}
.kpi-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.kpi-card {
background-color: #16213e;
border-radius: 8px;
padding: 1.5rem;
text-align: center;
border-left: 4px solid #0f3460;
}
.kpi-card.kpi-total {
border-left-color: #3498db;
}
.kpi-card.kpi-pass {
border-left-color: #2ecc71;
}
.kpi-card.kpi-techs {
border-left-color: #9b59b6;
}
.kpi-card.kpi-warning {
border-left-color: #f39c12;
}
.kpi-value {
display: block;
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.kpi-label {
font-size: 0.9rem;
color: #a0a0b0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.charts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
margin-bottom: 2rem;
}
.chart-container {
background-color: #16213e;
border-radius: 8px;
padding: 1.5rem;
}
.chart-container h2 {
font-size: 1.1rem;
margin-bottom: 1rem;
color: #ecf0f1;
}
.table-section {
background-color: #16213e;
border-radius: 8px;
padding: 1.5rem;
}
.table-section h2 {
font-size: 1.1rem;
margin-bottom: 1rem;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid #2c3e50;
}
th {
background-color: #0f3460;
font-weight: 600;
text-transform: uppercase;
font-size: 0.8rem;
letter-spacing: 0.5px;
}
tr:hover {
background-color: #1e3a5f;
}
.result-pass {
color: #2ecc71;
font-weight: 600;
}
.result-fail {
color: #e74c3c;
font-weight: 600;
}
.result-followup {
color: #f39c12;
font-weight: 600;
}
@media (max-width: 768px) {
.charts-grid {
grid-template-columns: 1fr;
}
.kpi-grid {
grid-template-columns: repeat(2, 1fr);
}
}
Your frontend is pulling live data from the API and rendering it as a polished dashboard. Next up, you will wire together the deployment configuration and push the whole application live to Vercel.
Connect and Configure for Vercel
Your backend API and frontend dashboard are both working. The missing piece is the deployment configuration that tells Vercel how to run your Python app and serve your static files.
Vercel needs two configuration files to understand your project. One declares your Python dependencies, and the other sets up URL routing so API requests reach FastAPI while everything else loads from the public/ folder.
In this step, get ready to:
- Create pyproject.toml and vercel.json configuration files.
- Confirm the FastAPI app serves the dashboard at the root route.
- Test the full integration locally with vercel dev.
Create the deployment configuration files
Vercel reads pyproject.toml to know which Python packages to install and where your app's entrypoint lives.
- Open pyproject.toml in the ndt-tracker folder and paste the following configuration:
[project]
name = "ndt-tracker"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"fastapi==0.139.0",
"uvicorn>=0.30.0",
]
[tool.vercel]
entrypoint = "api.index:app"
What does this configuration do?
The [project] section declares your app's name, version, minimum Python version, and the packages Vercel needs to install on deploy.
The [tool.vercel] section tells Vercel exactly which Python object to run. api.index:app means "find the app variable inside api/index.py."
- Save pyproject.toml.
Next, Vercel needs a routing file to direct incoming requests. API calls go to your Python function, and all other requests serve static files from public/.
- Open vercel.json in the ndt-tracker folder and paste this routing configuration:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"rewrites": [
{ "source": "/api/(.*)", "destination": "/api/index.py" }
]
}
What does this routing do?
The rewrites array maps URL patterns to handlers. Any request matching /api/(.*) gets forwarded to your FastAPI function at /api/index.py.
Everything else (your HTML, CSS, and JavaScript) is automatically served from the public/ directory by Vercel's CDN.
- Save vercel.json.
Seeing a JSON syntax error?
Make sure you have the opening and closing curly braces, and that the source and destination values are wrapped in double quotes. JSON does not allow trailing commas.
Help me fix a JSON syntax error in vercel.json
Confirm the FastAPI app serves static files
For the dashboard to load at the root URL, your FastAPI app needs to serve index.html when someone visits / and make all static assets available.
- Open api/index.py and scroll to the bottom of the file.
- Confirm you see these lines at the end of the file:
public_path = Path(__file__).parent.parent / "public"
@app.get("/")
def serve_dashboard():
return FileResponse(public_path / "index.html")
app.mount("/", StaticFiles(directory=str(public_path)), name="static")
What do these lines do?
The public_path variable builds the absolute path to the public/ folder relative to where index.py lives.
The serve_dashboard() function returns index.html when someone visits the root URL. The app.mount() line makes all other files in public/ (like styles.css) accessible as static assets.
- Also confirm the imports at the top of api/index.py include StaticFiles, FileResponse, and Path.
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pathlib import Path
Missing an import?
If any of these imports are missing, add them now. Without FileResponse, the root route cannot serve HTML. Without StaticFiles, your CSS file will not load.
Help me fix missing imports in my FastAPI app
Test the full integration locally
The Vercel CLI can simulate the full production environment on your machine. This lets you verify that routing, static files, and API endpoints all work together before deploying.
- Run the local development server by entering this command in the terminal:
vercel dev
What does vercel dev do?
This command replicates the Vercel production environment on your local machine. It reads your vercel.json routing rules, starts your FastAPI function, and serves static files from public/ exactly as it would in production.
- If prompted to link the project, follow the prompts to set up a new Vercel project.
- Open your browser and navigate to http://localhost:3000.
- Verify the KPI cards show real numbers (12 total inspections, 66.7% pass rate, 2 active technicians, 1 expiring certification).
- Verify the doughnut chart shows the NDT method distribution with colored segments.
- Verify the bar chart shows monthly pass/fail/follow-up counts.
- Verify the inspections table lists all 12 records with color-coded results.
Dashboard not loading at localhost:3000?
Make sure vercel dev is running without errors in your terminal. Check that vercel.json is saved in the root of your ndt-tracker folder (not inside api/ or public/).
If charts appear blank but KPI cards load, check your browser console for JavaScript errors. A network error on /api/kpis means the rewrite rule is not routing correctly.
Help me debug my vercel dev local server
✔️ Awesome, I've got everything!
Great. Double check you have saved both pyproject.toml and vercel.json.
ⓧ I'd like to double check the full code
Here is the complete pyproject.toml:
[project]
name = "ndt-tracker"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"fastapi==0.139.0",
"uvicorn>=0.30.0",
]
[tool.vercel]
entrypoint = "api.index:app"
Here is the complete vercel.json:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"rewrites": [
{ "source": "/api/(.*)", "destination": "/api/index.py" }
]
}
Here is the complete api/index.py:
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pathlib import Path
from api.data import inspections, technicians
app = FastAPI(title="NDT Inspection Tracker API")
@app.get("/api/inspections")
def get_inspections():
return inspections
@app.get("/api/technicians")
def get_technicians():
return technicians
@app.get("/api/kpis")
def get_kpis():
total = len(inspections)
passed = sum(1 for i in inspections if i["result"] == "pass")
failed = sum(1 for i in inspections if i["result"] == "fail")
follow_up = sum(1 for i in inspections if i["result"] == "requires_follow_up")
pass_rate = round((passed / total) * 100, 1) if total > 0 else 0
methods = {}
for i in inspections:
method = i["ndt_method"]
methods[method] = methods.get(method, 0) + 1
active_techs = sum(1 for t in technicians if t["status"] == "active")
expiring_certs = sum(1 for t in technicians if t["status"] == "expiring_soon")
return {
"total_inspections": total,
"passed": passed,
"failed": failed,
"requires_follow_up": follow_up,
"pass_rate": pass_rate,
"active_technicians": active_techs,
"expiring_certifications": expiring_certs,
"inspections_by_method": methods
}
@app.get("/api/trends")
def get_trends():
monthly = {}
for i in inspections:
month = i["inspection_date"][:7]
if month not in monthly:
monthly[month] = {"pass": 0, "fail": 0, "requires_follow_up": 0}
monthly[month][i["result"]] += 1
return {
"months": sorted(monthly.keys()),
"data": {month: monthly[month] for month in sorted(monthly.keys())}
}
public_path = Path(__file__).parent.parent / "public"
@app.get("/")
def serve_dashboard():
return FileResponse(public_path / "index.html")
app.mount("/", StaticFiles(directory=str(public_path)), name="static")
Your project is fully wired up and running locally. Next up, you'll deploy it to Vercel so the world can see your NDT dashboard.
Deploy to Vercel
Your NDT Inspection Tracker is fully built and tested locally. The final step is to put it on the internet so anyone with the URL can access it.
Vercel turns your FastAPI app into a serverless function and serves your static files from a global CDN. You will deploy a preview first to verify everything works, then promote it to production.
In this step, get ready to:
- Deploy a preview build and verify the dashboard loads correctly.
- Promote to production and confirm the live URL works.
Deploy a preview build
Vercel uses a two-stage deployment model. A preview deployment lets you test on a real URL before pushing to production.
- Return to the terminal in VS Code.
- Deploy your project by running this command:
vercel deploy
What prompts will Vercel ask?
Because this is the first time you are deploying from this folder, the Vercel CLI needs to link it to a project on your account. It will ask a series of setup questions.
- When asked to set up and deploy, confirm with Y.
- Select your personal account (or team) when prompted.
- When asked to link to an existing project, enter N to create a new one.
- For the project name, press Enter to accept the default ndt-tracker (your folder name).
- When asked to customize settings, enter N to use the defaults.
After the build completes, Vercel prints a preview URL in the terminal. It looks something like ndt-tracker-xxxxxxxx.vercel.app.
- Copy the preview URL from the terminal output and open it in your browser.
- Verify the dashboard loads with KPI cards showing data (12 total inspections, 66.7% pass rate), the doughnut chart displaying NDT method distribution, and the bar chart showing monthly trends.
Dashboard not loading on the preview URL?
- Make sure you are in the ndt-tracker folder when you run the deploy command. The CLI uses the current directory.
- Check that your vercel.json and pyproject.toml are saved at the project root. Vercel reads these to route requests correctly.
- If you see a 404 or blank page, check the Vercel build logs in your terminal output for Python import errors.
help me troubleshoot my Vercel preview deployment for a FastAPI Python project
Deploy to production
A preview deployment is only visible at a temporary URL. Production deployments get assigned to your project's permanent domain.
- Deploy to production by running this command:
vercel --prod
What is the difference between preview and production?
Preview deployments are for testing. They get a unique, randomized URL and use preview environment variables.
Production deployments are your live site. They get assigned to your permanent *.vercel.app domain and use production environment variables.
Vercel prints your production URL in the terminal once the build finishes.
- Copy the production URL from the terminal and open it in your browser.
- Confirm the dashboard loads, KPI cards show data, and both charts render with the correct inspection data.
Production deployment failing?
- If the preview worked but production fails, the issue is likely an environment variable mismatch. This project has no environment variables, so this is unlikely.
- Make sure your api/__init__.py file exists (even if empty). Python needs it to recognize the api folder as a package.
help me debug my Vercel production deployment
Your NDT Inspection Tracker is live on the internet. Anyone with the URL can see your dashboard, complete with real-time KPI cards, method distribution charts, and monthly trend visualizations.
Secret mission
Add Inspection Filtering
Your dashboard shows all inspections at once, but a real operations manager needs to drill down. What if they want to see only Ultrasonic Testing results for pipelines in June? Add a dynamic filtering system that lets users slice data by NDT method, asset type, and date range with live chart updates.
Clean Up Your Resources
Clean Up Your Resources
This project runs on Vercel's Hobby tier, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Vercel project (Hobby tier, free).
- Local ndt-tracker project folder on your Desktop.
- Python virtual environment (.venv inside the project folder).
Keep everything running
No action needed. Choose this if you're still actively building or want to keep the dashboard as a live portfolio piece.
- Your Vercel deployment stays live at no cost on the Hobby tier.
- Your local project files remain on your Desktop for future edits.
- The Vercel CLI stays installed globally via npm.
Pause - I'll come back to this later
Vercel's Hobby tier has no idle costs, so there is nothing to pause on the cloud side. You can free up local resources if needed.
- Deactivate your virtual environment by running:
- Your Vercel deployment continues to run for free. No action needed there.
deactivate
When you return, reactivate the environment and pick up where you left off.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- Remove the Vercel project by running this command from your terminal:
vercel rm ndt-tracker
What does this command do?
This deletes the entire Vercel project including all deployments, domains, and settings associated with it. The CLI will ask you to confirm before proceeding.
- Delete the local project folder. On macOS or Linux, run:
cd ~/Desktop
rm -rf ndt-tracker
- On Windows, run:
cd ~\Desktop
Remove-Item -Recurse -Force ndt-tracker
- Confirm the folder is gone by running:
ls
You should no longer see ndt-tracker in the output.
Nice Work!
Nice Work!
You just built and deployed a full-stack NDT Inspection Tracker Dashboard for a well services energy company. This is exactly the kind of domain-specific IT tool that hiring managers in oil and gas look for.
You learned how to:
- Built a FastAPI backend with RESTful endpoints serving realistic NDT inspection data, technician records, and computed KPIs for well services operations.
- Created an interactive dashboard with Chart.js visualizations showing inspection method distribution, monthly pass/fail trends, and color-coded KPI cards.
- Modeled real-world NDT domain concepts including five inspection methods (UT, RT, MT, PT, VT), ASNT certification tracking, defect types, and asset integrity data for pipelines and pressure vessels.
- Deployed the full application to Vercel as a single serverless project accessible via a public URL.
- Secret Mission: Added a dynamic filtering system with live chart updates based on NDT method, asset type, and date range.
Ready to quiz yourself?