Local Network Oil Price Dashboard

Build a live oil price dashboard accessible from any device on your network.

Introduction

30 Second Summary

Oil prices shift throughout the day, but most people only notice when they fill up at the pump or pay a heating bill. What if you had a personal price tracker running on your home network that any device in your house could check?

In this project, you will use Python and Flask to build a live dashboard that pulls real oil prices from an API and highlights price movements. Any device on your Wi-Fi can pull up the dashboard and see the latest prices.

What You'll Build

Imagine pulling out your phone at the kitchen table, tapping a bookmark, and instantly seeing which oil prices moved since your last check, with green and red glows showing the direction.

By the end of this project, you'll have:

  • A live oil price dashboard showing real-time prices for seven major commodities like Brent Crude, WTI, and Natural Gas, with timestamps on every update.
  • Smart change detection that highlights which prices moved with green (up) and red (down) indicators, so you can spot shifts at a glance.
  • Network-wide access from any phone, tablet, or PC on your Wi-Fi, plus a Progressive Web App you can install to your home screen on localhost.
  • Secret Mission: Set up HTTPS with mkcert so the PWA is installable from any device on your network, not just localhost.

Are there any prerequisites?

No prior experience is required. This project walks you through every tool and account you need, including Python, Flask, and a free OilPriceAPI account.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters to you.

Set Up Your Project and Install Dependencies

Your oil price dashboard needs two things to come alive. A web framework to serve the dashboard page, and a data source that provides live oil prices.

In this step, you'll create your project folder, install Flask and the OilPriceAPI SDK, and grab a free API key to access real-time oil price data.

In this step, get ready to:
  • Create a project folder with a Python virtual environment.
  • Install Flask, the OilPriceAPI SDK, and python-dotenv.
  • Sign up for a free OilPriceAPI account and store your API key.
Create your project folder and virtual environment
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Open a terminal inside VS Code by pressing Ctrl+` (the backtick key, above Tab on your keyboard).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder by running this command:
mkdir oil-dashboard
  • Open the oil-dashboard folder in VS Code so the Explorer sidebar and terminal are rooted in your project.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the oil-dashboard folder, and click Open (macOS) or Select Folder (Windows).

Seeing a trust dialog?

VS Code may ask whether you trust the authors of the files in this folder. Click Yes, I trust the authors to continue. This is a one-time security prompt for new folders.

VS Code reloads with oil-dashboard as your workspace. You should see the folder name in the Explorer sidebar on the left.

  • Open a new terminal by pressing Ctrl+` (backtick). The prompt should show oil-dashboard as your current directory.

A virtual environment is an isolated copy of Python for your project. Packages you install inside it stay separate from your system Python, so different projects never clash.

  • Create and activate your virtual environment by running these commands:

macOS

python3 -m venv venv
source venv/bin/activate

Windows

python -m venv venv
venv\Scripts\activate

You should see (venv) appear at the start of your terminal prompt. This confirms you are working inside the virtual environment.

Don't see (venv) in your prompt?

  • Double-check you ran the activation command for your operating system. macOS uses source venv/bin/activate and Windows uses venv\Scripts\activate.
  • On Windows, if you see a permissions error about running scripts, open a new PowerShell terminal and run:
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
  • Then try the activation command again.
  • Why isn't my virtual environment activating?
Install your project dependencies
  • Install Flask, the OilPriceAPI SDK, and python-dotenv by running:
pip install Flask==3.1.3 oilpriceapi==1.12.8 python-dotenv==1.2.3

What are these packages?

  • Flask is a lightweight Python web framework. It turns your Python script into a web server that serves pages to a browser.
  • oilpriceapi is the official Python SDK for the OilPriceAPI. It handles authentication and response parsing so you can fetch oil prices with a single method call.
  • python-dotenv loads environment variables from a .env file into your Python process. This keeps sensitive values like API keys out of your code.

Before you verify the install, what do you expect to see if all three packages installed correctly?

  • Verify the installation by running:
python -c "import flask; import oilpriceapi; print('Flask', flask.__version__)"

You should see Flask 3.1.3 printed with no errors. Both imports succeeded, which means Flask and the OilPriceAPI SDK are ready to use.

Seeing an import error?

  • Check that (venv) is still visible in your prompt. If it disappeared, re-run the activation command from the previous substep.
  • If you see a message about packages not being found, re-run the pip install command above and check for any error output during the install.
  • Help me fix my import error.
Sign up for a free OilPriceAPI account

Your code needs an API key to authenticate with the OilPriceAPI. The free tier gives you 1,000 requests per month, which is plenty for building and testing your dashboard.

  • Open the OilPriceAPI signup page in your browser.
  • Sign up with your email. No credit card is required.
  • After signing in, copy your API key from the OilPriceAPI dashboard.

Keep your API key handy

You will paste this key into a file in the next step. Keep the OilPriceAPI dashboard tab open so you can copy it easily.

Now you will store the API key in a .env file so your Python code can read it without the key being hardcoded in your source code.

  • In VS Code's Explorer sidebar, hover over the oil-dashboard folder name.
  • Click the New File icon (a page with a plus sign).
  • Type .env as the filename and press Enter.
  • Paste the following line into the file, replacing your-api-key-here with the API key you copied from OilPriceAPI:
OILPRICEAPI_KEY=your-api-key-here

What does this line do?

OILPRICEAPI_KEY is the environment variable name that the OilPriceAPI SDK looks for when authenticating. The python-dotenv package loads this value from the .env file at runtime, so your key never appears in your Python code.

  • Save the file by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Confirm the file exists by running:
ls

You should see .env and venv listed in the output.

✔️ Awesome, I've got everything!

Great. Double-check that you have saved your .env file with your real API key in place.

ⓧ I'd like to double check the full code

Your .env file should contain exactly one line (with your own API key substituted):

OILPRICEAPI_KEY=your-api-key-here

Don't see .env in the output?

  • Make sure you created the file inside the oil-dashboard folder, not a parent folder. Check that your terminal prompt shows oil-dashboard as the current directory.
  • On macOS, files starting with a dot are hidden by default in Finder, but they appear normally in VS Code's Explorer sidebar and in terminal ls output.
  • Help me find my .env file.

Your project folder is ready, your packages are installed, and your API key is in place. Next up, you'll build a Flask app that fetches live oil prices and displays them in your browser.

Build a Basic Flask App That Fetches Oil Prices

Your tools are installed and your API key is ready. Now it's time to see real oil price data flowing through your app.

Before building the full dashboard, you need to confirm the API actually works and returns real price data. In this step, you'll create a bare-bones Flask app that fetches live prices and displays them on a plain web page.

In this step, get ready to:
  • Create a Flask app that fetches oil prices from the OilPriceAPI.
  • Build an HTML template to display the price data.
  • Run the app and verify live prices appear in your browser.
Create the Flask app file

Your app needs to do three things: load your API key from the .env file, call the OilPriceAPI to fetch prices for seven oil commodities, and serve the results as a web page.

  • Create a new file called app.py in your oil-dashboard folder by clicking the new file icon in the VS Code Explorer sidebar.
  • Paste the following code into app.py to set up the app and define which commodities to track:
import os
from flask import Flask, render_template, jsonify, send_from_directory, make_response
from oilpriceapi import OilPriceAPI
from dotenv import load_dotenv

# Load the API key from the .env file
load_dotenv()

app = Flask(__name__)

# The seven oil commodity codes the API recognizes
COMMODITIES = [
    "BRENT_CRUDE_USD",
    "WTI_USD",
    "NATURAL_GAS_USD",
    "DIESEL_USD",
    "GASOLINE_USD",
    "HEATING_OIL_USD",
    "JET_FUEL_USD",
]

# Human-readable names for each commodity
DISPLAY_NAMES = {
    "BRENT_CRUDE_USD": "Brent Crude",
    "WTI_USD": "WTI Crude",
    "NATURAL_GAS_USD": "Natural Gas",
    "DIESEL_USD": "Diesel",
    "GASOLINE_USD": "Gasoline",
    "HEATING_OIL_USD": "Heating Oil",
    "JET_FUEL_USD": "Jet Fuel",
}

What does this code do?

load_dotenv() reads your .env file and makes your API key available as an environment variable. Flask(__name__) creates the web application that will serve your dashboard.

COMMODITIES lists the seven oil codes the API accepts. DISPLAY_NAMES maps each code to a friendly name so your dashboard shows "Brent Crude" instead of "BRENT_CRUDE_USD".

The extra imports like jsonify and send_from_directory are Flask utilities you'll use in later steps.

  • Add the following code below the DISPLAY_NAMES dictionary in app.py:
# Fetch live prices from the OilPriceAPI
def fetch_prices():
    prices = []
    try:
        with OilPriceAPI(api_key=os.environ["OILPRICEAPI_KEY"]) as client:
            results = client.prices.get_multiple(COMMODITIES)
            for price in results:
                code = price.commodity
                current_value = price.value
                # Build a dictionary for each commodity
                prices.append({
                    "code": code,
                    "name": DISPLAY_NAMES.get(code, code),
                    "price": current_value,
                    "currency": price.currency,
                    "unit": price.unit,
                    "timestamp": price.timestamp.isoformat(),
                })
    except Exception as e:
        print(f"Error fetching prices: {e}")
    return prices

# Serve the dashboard page
@app.route("/")
def index():
    prices = fetch_prices()
    return render_template("index.html", prices=prices)

if __name__ == "__main__":
    app.run(debug=True)

What does this code do?

fetch_prices() opens a connection to the OilPriceAPI using your API key. It calls client.prices.get_multiple() to request prices for all seven commodities in a single API call, then builds a list of dictionaries with each commodity's name, price, currency, unit, and timestamp.

The @app.route("/") decorator tells Flask to call index() when someone visits the root URL. render_template("index.html", prices=prices) passes the price data into an HTML template that you'll create next.

  • Save app.py.

Seeing a syntax error when you save?

Check that the second code block starts directly after the closing } of DISPLAY_NAMES with a blank line between them. Make sure the try and except blocks are indented at the same level inside fetch_prices().

help me fix a syntax error in my Flask app.py file

✔️ Awesome, I've got everything!

Great. Make sure you've saved app.py before moving on.

ⓧ I'd like to double check the full code

Scroll up and confirm your app.py contains these sections in order:

  • The four import lines, followed by load_dotenv() and app = Flask(__name__).
  • The COMMODITIES list with seven oil codes.
  • The DISPLAY_NAMES dictionary mapping each code to a friendly name.
  • The fetch_prices() function with the try/except block.
  • The @app.route("/") route and index() function.
  • The if __name__ == "__main__" block at the bottom.
  • Start the Flask development server by running this command in your terminal:
python app.py

You should see Flask print a message confirming the server is running, including a line like Running on http://127.0.0.1:5000.

  • Open your browser and go to http://localhost:5000.

You'll see an error page with a traceback mentioning the missing template. This is expected. Flask is running and responding to your request, but the HTML template doesn't exist yet.

Why am I seeing an error?

Your index() route calls render_template("index.html"), but you haven't created that file yet. Flask looks for templates in a folder called templates, and since neither the folder nor the file exists, it raises an error. You'll fix this in the next substep by creating the template.

Server won't start?

If you see an error about a missing module, make sure your virtual environment is activated. You should see (venv) at the beginning of your terminal prompt. If it's missing, run venv\Scripts\activate and try again.

If you see an error about OILPRICEAPI_KEY, double-check that your .env file is in the oil-dashboard folder and contains your actual API key.

help me troubleshoot why my Flask server won't start

Create the HTML template

Flask looks for HTML templates in a folder called templates by default. You need to create this folder and add a simple HTML file that displays the price data.

  • In the VS Code Explorer sidebar, right-click inside the file list and select New Folder.
  • Name the folder templates and press Enter.
  • Right-click the new templates folder and select New File.
  • Name the file index.html and press Enter.
  • Paste the following code into index.html:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Oil Price Dashboard</title>
</head>
<body>
    <h1>Oil Price Dashboard</h1>
    <!-- Loop through each price object passed from Flask -->
    {% for p in prices %}
    <p>{{ p.name }}: ${{ "%.2f"|format(p.price) }} {{ p.currency }} / {{ p.unit }}</p>
    {% endfor %}
</body>
</html>

What does this template do?

The {% for p in prices %} block is Jinja2 template syntax. It loops through the list of price dictionaries that fetch_prices() built, creating one paragraph for each commodity.

The "%.2f"|format(p.price) filter rounds each price to two decimal places so you see clean dollar amounts like $72.45 instead of long floating-point numbers.

Before you refresh the browser, what do you expect to see on the page?

  • Save index.html and refresh http://localhost:5000 in your browser.

You should see a plain page titled "Oil Price Dashboard" with seven oil commodities and their current prices listed.

Still seeing the error page?

Make sure the templates folder is directly inside oil-dashboard, not nested inside another folder. The file structure should be oil-dashboard/templates/index.html.

Check that the file is named exactly index.html with no extra spaces or typos. Flask is case-sensitive when looking up template names.

help me fix a TemplateNotFound error in my Flask Oil Price Dashboard

Seeing prices as 0.00 or getting an API error?

Open your .env file and verify your API key is pasted correctly with no extra spaces or quotation marks around the value. The line should look like OILPRICEAPI_KEY=abc123... with no quotes.

If you see "Error fetching prices" in your terminal, your API key may be invalid. Log back into the OilPriceAPI dashboard at oilpriceapi.com and copy a fresh key.

help me debug why my OilPriceAPI call is failing

✔️ Awesome, I've got everything!

Great. Make sure both app.py and index.html are saved before moving on.

ⓧ I'd like to double check the full code

Here is the full index.html for comparison:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Oil Price Dashboard</title>
</head>
<body>
    <h1>Oil Price Dashboard</h1>
    {% for p in prices %}
    <p>{{ p.name }}: ${{ "%.2f"|format(p.price) }} {{ p.currency }} / {{ p.unit }}</p>
    {% endfor %}
</body>
</html>

For app.py, scroll up and confirm it contains these sections in order:

  • Four import lines, then load_dotenv() and app = Flask(__name__).
  • The COMMODITIES list with seven commodity codes.
  • The DISPLAY_NAMES dictionary with seven entries.
  • The fetch_prices() function containing the OilPriceAPI client call.
  • The @app.route("/") decorator and index() function.
  • The if __name__ == "__main__" block with app.run(debug=True).

You have live oil prices flowing through your app. Next up, you'll add change detection so the dashboard highlights which prices went up and which went down.

Add Change Detection and Price Movement Indicators

Your dashboard shows live oil prices, but right now there is no way to tell whether a price went up, went down, or stayed the same since the last time you checked.

In this step, you will add a server-side price cache that remembers the last set of prices. When new prices arrive, the server compares them to the cache and tags each commodity as up, down, or unchanged.

In this step, get ready to:
  • Add a price cache and change detection logic to your Flask server.
  • Create a JSON API endpoint that the frontend can poll for updated prices.
  • Restyle the dashboard with cards, color-coded arrows, and automatic 60-second refresh.
Add a server-side price cache and change detection

To know whether a price moved, you need to remember what it was last time. A Python dictionary stored at the module level acts as a simple cache that persists between requests while the server is running.

  • In app.py, find the DISPLAY_NAMES dictionary. Directly below its closing }, add this line:
price_cache = {}
  • Now find the for price in results: loop inside fetch_prices(). It currently looks like this:
            for price in results:
                code = price.commodity
                current_value = price.value
                prices.append({
                    "code": code,
                    "name": DISPLAY_NAMES.get(code, code),
                    "price": current_value,
                    "currency": price.currency,
                    "unit": price.unit,
                    "timestamp": price.timestamp.isoformat(),
                })
  • Replace that entire for loop with this updated version that compares prices to the cache:
            for price in results:
                code = price.commodity
                current_value = price.value
                # Look up the previous price from the cache
                previous_value = price_cache.get(code)

                # Determine the direction of price change
                if previous_value is None:
                    direction = "unchanged"
                elif current_value > previous_value:
                    direction = "up"
                elif current_value < previous_value:
                    direction = "down"
                else:
                    direction = "unchanged"

                # Store the new price in the cache for next comparison
                price_cache[code] = current_value

                prices.append({
                    "code": code,
                    "name": DISPLAY_NAMES.get(code, code),
                    "price": current_value,
                    "currency": price.currency,
                    "unit": price.unit,
                    "timestamp": price.timestamp.isoformat(),
                    "direction": direction,
                })

What does this code do?

Each time the server fetches prices, the loop now compares each commodity's new value to what was stored in price_cache.

  • If there is no cached value (first load), the direction is unchanged.
  • If the new price is higher, the direction is up. If lower, down.
  • After comparing, the cache is updated so the next fetch has a fresh baseline.

The existing except block only prints an error and returns an empty list. If the API is temporarily down, the dashboard would go blank.

  • Find the except block in fetch_prices(). It currently looks like this:
    except Exception as e:
        print(f"Error fetching prices: {e}")
    return prices
  • Replace that except block and return with this version that serves cached prices as a fallback:
    except Exception as e:
        print(f"Error fetching prices: {e}")
        # Serve cached prices if the API call fails
        for code, cached_price in price_cache.items():
            prices.append({
                "code": code,
                "name": DISPLAY_NAMES.get(code, code),
                "price": cached_price,
                "currency": "USD",
                "unit": "per barrel",
                "timestamp": "cached",
                "direction": "unchanged",
            })
    return prices

Why serve cached prices on failure?

If the API is temporarily unreachable, the dashboard now shows the last known prices instead of going blank. The timestamp field is set to cached so the frontend can tell the data is stale.

  • Save app.py.
  • Stop the Flask server if it is still running by pressing Ctrl+C in the terminal.
  • Start the server again by running:
python app.py
  • Open http://localhost:5000 in your browser.

You should still see the same plain list of oil prices. This is the first load, so every commodity is tagged as unchanged.

Seeing an error when the page loads?

  • Check that price_cache = {} is placed outside of any function, at the same indentation level as DISPLAY_NAMES.
  • Make sure the for loop is indented inside the with block, not outside of it.
  • Confirm that "direction": direction, appears inside the prices.append() dictionary.
  • Help me debug my price cache code.
Create a JSON API endpoint for the frontend to poll

The frontend needs a way to fetch updated prices without reloading the entire page. A dedicated JSON endpoint returns price data that JavaScript can consume directly.

  • In app.py, find the index() route. Add this new route directly below it:
@app.route("/api/prices")
def api_prices():
    # Return current prices with change direction as JSON
    prices = fetch_prices()
    return jsonify(prices)

What does this endpoint do?

The /api/prices route calls fetch_prices() and wraps the result in a JSON response using Flask's jsonify().

The frontend will call this endpoint on a timer to get fresh prices without a full page reload.

  • Save app.py and restart the server (Ctrl+C, then python app.py).
  • Open http://localhost:5000/api/prices in your browser.

You should see raw JSON data with each commodity's name, price, and a direction field.

Seeing a 404 or no JSON?

  • Make sure the @app.route("/api/prices") decorator is placed above the def api_prices(): line, at the same indentation level as the other routes.
  • Confirm you restarted the server after saving.
  • Help me fix my /api/prices endpoint.

✔️ Awesome, I've got everything!

Make sure you have saved your file before moving on.

ⓧ I'd like to double check the full code

Your app.py should now contain these sections, in order:

  • The imports and load_dotenv() call (unchanged from before).
  • The COMMODITIES list and DISPLAY_NAMES dictionary (unchanged).
  • The new price_cache = {} line.
  • The updated fetch_prices() function with comparison logic and the fallback except block.
  • The / route (unchanged).
  • The new /api/prices route.
  • The if __name__ == "__main__": block (unchanged).

Scroll up and confirm each section is present and in the right order.

Update the template with styled cards and auto-refresh

The plain list of prices served its purpose, but now you need cards with color-coded arrows and automatic polling. You will replace the entire index.html template.

  • Open templates/index.html in VS Code.
  • Select all content with Cmd+A (macOS) or Ctrl+A (Windows), then delete it.
  • Paste the new <head> section with all the dashboard styles:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Oil Price Dashboard</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #eee; min-height: 100vh; padding: 20px; }
        h1 { text-align: center; margin-bottom: 8px; font-size: 1.6rem; color: #e0e0e0; }
        .last-updated { text-align: center; color: #888; font-size: 0.85rem; margin-bottom: 24px; }
        .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; max-width: 960px; margin: 0 auto; }
        .card { background: #16213e; border-radius: 12px; padding: 20px; transition: box-shadow 0.3s ease; }
        .card.up { box-shadow: 0 0 12px rgba(0, 200, 83, 0.4); }
        .card.down { box-shadow: 0 0 12px rgba(255, 82, 82, 0.4); }
        .commodity-name { font-size: 0.9rem; color: #aaa; margin-bottom: 8px; }
        .price-row { display: flex; align-items: center; gap: 10px; }
        .price { font-size: 1.8rem; font-weight: 700; }
        .arrow { font-size: 1.4rem; }
        .arrow.up { color: #00c853; }
        .arrow.down { color: #ff5252; }
        .meta { margin-top: 8px; font-size: 0.75rem; color: #666; }
        .status { text-align: center; margin-top: 16px; font-size: 0.8rem; color: #555; }
    </style>
</head>

What do these styles do?

  • The dark background (#1a1a2e) and card style (#16213e) give the dashboard a clean monitoring-station look.
  • The .card.up and .card.down classes add a green or red glow around the entire card when a price moves.
  • The .grid uses CSS Grid to arrange cards responsively across different screen sizes.
  • Directly below the </head> you just pasted, add the body structure with the Jinja template for rendering price cards:
<body>
    <h1>Oil Price Dashboard</h1>
    <p class="last-updated" id="lastUpdated">Loading...</p>
    <div class="grid" id="priceGrid">
        {% for p in prices %}
        <div class="card {{ p.direction }}" data-code="{{ p.code }}">
            <div class="commodity-name">{{ p.name }}</div>
            <div class="price-row">
                <span class="price">${{ "%.2f"|format(p.price) }}</span>
                {% if p.direction == "up" %}<span class="arrow up">&#9650;</span>{% elif p.direction == "down" %}<span class="arrow down">&#9660;</span>{% endif %}
            </div>
            <div class="meta">{{ p.currency }} / {{ p.unit }}</div>
        </div>
        {% endfor %}
    </div>
    <p class="status">Auto-refreshes every 60 seconds</p>
</body>
</html>

What does this template do?

  • The {% for p in prices %} loop creates one card per commodity.
  • Each card's CSS class includes the direction value (up, down, or empty) which triggers the green or red glow.
  • The arrow symbols (&#9650; for up, &#9660; for down) only render when the price actually moved.
  • Save index.html and restart the server (Ctrl+C, then python app.py).
  • Open http://localhost:5000 in your browser.

You should see a dark-themed dashboard with seven commodity cards. Each card shows the commodity name, its price in large text, and a currency/unit label below.

Dashboard looks broken or shows raw Jinja tags?

  • Check that the </head> tag appears on its own line between the CSS block and the <body> tag.
  • Make sure the Jinja template uses double curly braces ({{ p.name }}), not single.
  • Help me fix my dashboard template.

The cards display prices but cannot update themselves yet. The final piece is a JavaScript function that polls your /api/prices endpoint and rebuilds the cards when new data arrives.

  • In index.html, find the closing </body> tag. Paste this script block just above it:
    <script>
        function updatePrices() {
            fetch('/api/prices')
                .then(response => response.json())
                .then(data => {
                    const grid = document.getElementById('priceGrid');
                    grid.innerHTML = '';
                    data.forEach(p => {
                        const card = document.createElement('div');
                        card.className = 'card ' + p.direction;
                        card.setAttribute('data-code', p.code);
                        let arrowHtml = '';
                        if (p.direction === 'up') { arrowHtml = '<span class="arrow up">&#9650;</span>'; }
                        else if (p.direction === 'down') { arrowHtml = '<span class="arrow down">&#9660;</span>'; }
                        card.innerHTML = '<div class="commodity-name">' + p.name + '</div>' +
                            '<div class="price-row"><span class="price">$' + p.price.toFixed(2) + '</span>' + arrowHtml + '</div>' +
                            '<div class="meta">' + p.currency + ' / ' + p.unit + '</div>';
                        grid.appendChild(card);
                    });
                    document.getElementById('lastUpdated').textContent = 'Last updated: ' + new Date().toLocaleTimeString();
                })
                .catch(err => { console.error('Failed to fetch prices:', err); });
        }
        setInterval(updatePrices, 60000);
        document.getElementById('lastUpdated').textContent = 'Last updated: ' + new Date().toLocaleTimeString();
    </script>

What does this script do?

  • The updatePrices() function calls your /api/prices endpoint using fetch(), then rebuilds every card in the grid from the JSON response.
  • setInterval(updatePrices, 60000) runs this function every 60 seconds, so the dashboard updates automatically.
  • The last line sets the initial timestamp so the learner sees when the page first loaded.
  • Save index.html and restart the server (Ctrl+C, then python app.py).

Before you refresh, think about this: on the first page load, all prices were stored in the cache. What do you expect to see when you load the page again?

  • Open http://localhost:5000 in your browser.

If any commodity's price moved since your last page load, you see a green up-arrow or red down-arrow next to it, and the card glows with a matching color. Prices that stayed the same show no arrow.

Not seeing any arrows?

Oil prices are updated roughly every 5 minutes by the API. If you reload quickly, prices may not have changed yet, so no arrows appear. Wait a few minutes and refresh again, or leave the page open and let the 60-second auto-refresh catch a movement.

The "Last updated" timestamp at the top of the page updates each time the auto-refresh runs.

Cards not updating after 60 seconds?

  • Open your browser's developer console (F12 or Cmd+Option+J on macOS). Check for red error messages.
  • Make sure the <script> block is inside the <body> tag, just above </body>.
  • Verify that visiting http://localhost:5000/api/prices still returns JSON. If not, the /api/prices route may be missing from app.py.
  • Help me debug my auto-refresh script.

✔️ Awesome, I've got everything!

Make sure you have saved both files before moving on.

ⓧ I'd like to double check the full code

Your templates/index.html should contain these three sections, in order:

  • The <head> with all CSS styles (17 rules from * through .status).
  • The <body> with the Jinja {% for p in prices %} loop that creates card divs.
  • The <script> block with updatePrices() and the setInterval call, just above </body>.

Scroll up and confirm each section is present and in the right order.

Your dashboard now shows live oil prices with change detection and auto-refresh. Next up, you will make it accessible from any device on your network so you can check prices from your phone.

Make the Dashboard Accessible from Any Device on Your Network

Your oil price dashboard works great on your PC. But what if you want to check prices from your phone while you're on the couch, or from a tablet in another room?

Right now, Flask only accepts connections from your own machine. In this step, you'll open it up so any device on your Wi-Fi can reach the dashboard by visiting your PC's IP address.

In this step, get ready to:
  • Bind Flask to all network interfaces so other devices can connect.
  • Find your PC's local IP address and test the dashboard from another device.
Bind Flask to all network interfaces

By default, Flask listens on 127.0.0.1, which means "this machine only." To let other devices on your network reach the server, you need to tell Flask to listen on 0.0.0.0, which means "all network interfaces."

  • In app.py, scroll down to the bottom of the file.
  • Find this line:
    app.run(debug=True)
  • Replace it with the following so Flask listens on all network interfaces at port 5000:
    app.run(host="0.0.0.0", port=5000, debug=True)

What does 0.0.0.0 mean?

When you set host="0.0.0.0", you're telling Flask to accept connections on every network interface your PC has. That includes localhost (your own machine) and your Wi-Fi adapter (other devices on your network).

Setting port=5000 makes the port explicit. Flask defaults to 5000 anyway, but spelling it out keeps things clear when you share the address with other devices.

  • Save app.py.
  • If your Flask server is still running from earlier, stop it by pressing Ctrl+C in the terminal.
  • Restart the server by running:
python app.py

You should see a line that includes Running on http://0.0.0.0:5000 in the terminal output. That confirms Flask is now listening on all interfaces.

Still seeing 127.0.0.1 in the output?

  • Make sure you saved app.py after making the change.
  • Double check that you replaced app.run(debug=True) with app.run(host="0.0.0.0", port=5000, debug=True) inside the if __name__ == "__main__": block.
  • Help me troubleshoot my Flask host binding
Test from another device on your network

Flask is now accepting connections from other devices, but you need your PC's local IP address so those devices know where to point their browser.

  • Open a second terminal in VS Code by clicking Terminal in the top menu bar, then New Terminal. Leave the Flask server running in the first terminal.
  • Find your PC's local IP address by running:
ipconfig
  • Look for the section labelled Wireless LAN adapter Wi-Fi (or Ethernet adapter if you're using a wired connection).
  • Find the line that says IPv4 Address. It looks something like 192.168.1.42. Record your IP address: your PC's IPv4 address

What is an IPv4 address?

Your IPv4 address is the unique number your router assigns to your PC on the local network. Other devices use it to send data directly to your machine. It typically starts with 192.168. or 10. followed by more numbers.

Before you try from another device, what do you think will happen when you open your dashboard using this IP address from your phone?

  • On your phone, tablet, or another computer connected to the same Wi-Fi network, open a browser.
  • Navigate to your dashboard by entering this URL in the address bar:
http://[[PC_IP="your PC's IPv4 address"]]:5000

✔️ I see the oil price dashboard

Your dashboard is live on the network. Any device on your Wi-Fi can now check oil prices by visiting this address.

ⓧ The page won't load

Windows Firewall is likely blocking incoming connections on port 5000. You need to create a firewall rule to allow traffic through.

  • Press the Windows key, type cmd, right-click Command Prompt, and select Run as administrator.
  • Create a firewall rule to allow inbound traffic on port 5000 by running:
netsh advfirewall firewall add rule name="Open Port 5000" dir=in action=allow protocol=TCP localport=5000

What does this command do?

  • netsh advfirewall firewall add rule tells Windows Firewall to create a new rule.
  • name="Open Port 5000" gives the rule a human-readable name so you can find it later.
  • dir=in means the rule applies to incoming connections (devices reaching your PC).
  • protocol=TCP localport=5000 limits the rule to TCP traffic on port 5000, which is where Flask is listening.
  • Try loading the dashboard again from your phone or other device.

Still can't connect?

  • Make sure both devices are on the same Wi-Fi network.
  • Confirm the Flask server is still running in your first terminal (you should see the Running on message).
  • Double check you typed the correct IP address, including the :5000 port at the end.
  • Help me troubleshoot my network connection

✔️ Awesome, I've got everything!

Great. Double check you've saved app.py with the updated app.run() call.

ⓧ I'd like to double check the full code

The only change from the previous step is the last line of app.py. Scroll to the bottom of your file and confirm it matches this:

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=True)

Everything above this block stays the same as the previous step. If anything looks off, scroll up through your earlier chunks and compare them to verify nothing was accidentally changed.

Your dashboard is now a local network app. Anyone on your Wi-Fi can pull up oil prices from their own device. Next up, you'll turn this into a Progressive Web App so you can install it to your home screen like a native app.

Turn the Dashboard into a PWA

Your dashboard is live on your local network. Any device on your Wi-Fi can pull up oil prices by visiting your PC's IP address.

Right now, checking prices means opening a browser and typing a URL every time. A Progressive Web App lets you install the dashboard to your home screen so it launches like a native app.

In this step, get ready to:
  • Create a PWA manifest file with your app's metadata and placeholder icons.
  • Add a service worker that caches your dashboard for fast loading.
  • Wire the manifest and service worker into your HTML so Chrome can install the app.
Create the PWA manifest and add Flask routes

A PWA manifest is a JSON file that tells the browser your app's name, theme colors, and icons. Without it, Chrome doesn't know your site can be installed.

  • Create the static/ folder by running this command in your terminal:
mkdir static

Chrome requires icon image files to show the PWA install prompt. The manifest you'll create references two icons: a 192x192 and a 512x512 pixel PNG.

  • Create a temporary helper script to generate placeholder icons. Create a new file called create_icons.py in your oil-dashboard folder with this code:
# Create placeholder PWA icon files
import struct
import zlib

def make_png(size, filename):
    # Build raw pixel data as a solid dark blue (#1a1a2e)
    raw = b''.join(b'\x00' + b'\x1a\x1a\x2e' * size for _ in range(size))

    def chunk(chunk_type, data):
        combined = chunk_type + data
        crc = struct.pack('>I', zlib.crc32(combined) & 0xffffffff)
        return struct.pack('>I', len(data)) + combined + crc

    header = struct.pack('>IIBBBBB', size, size, 8, 2, 0, 0, 0)

    with open(filename, 'wb') as f:
        f.write(b'\x89PNG\r\n\x1a\n')
        f.write(chunk(b'IHDR', header))
        f.write(chunk(b'IDAT', zlib.compress(raw)))
        f.write(chunk(b'IEND', b''))

make_png(192, 'static/icon-192.png')
make_png(512, 'static/icon-512.png')
print('Created icon-192.png and icon-512.png in static/')

What does this script do?

It generates two solid dark-blue PNG images that match your dashboard's background color. These are just placeholders so Chrome recognises valid icons in the manifest.

You can delete create_icons.py after running it. It's a one-time utility.

  • Generate the icons by running this command:
python create_icons.py

You should see Created icon-192.png and icon-512.png in static/ printed in your terminal.

Seeing a FileNotFoundError?

Make sure you ran mkdir static first. The script writes files into the static/ folder, which must already exist.

help me fix a FileNotFoundError when running create_icons.py

  • Now create the manifest itself. Create a file called manifest.json inside your static/ folder with this code:
{
    "name": "Oil Price Dashboard",
    "short_name": "Oil Prices",
    "start_url": "/",
    "display": "standalone",
    "background_color": "#1a1a2e",
    "theme_color": "#1a1a2e",
    "description": "Live oil price tracker for your local network",
    "icons": [
        {
            "src": "/static/icon-192.png",
            "sizes": "192x192",
            "type": "image/png"
        },
        {
            "src": "/static/icon-512.png",
            "sizes": "512x512",
            "type": "image/png"
        }
    ]
}

What does this file do?

  • name and short_name set the app's display name on the home screen and in the app launcher.
  • display: "standalone" tells the browser to open the app in its own window without browser chrome (no address bar).
  • icons points Chrome to the two placeholder PNGs you just generated. Chrome requires a 192x192 and a 512x512 icon for installability.
  • Save static/manifest.json.

The manifest file is ready, but Flask doesn't serve it at the right URL yet. You need Flask routes that serve both the manifest and the service worker (which you'll create next) from the correct paths.

  • In app.py, find the api_prices route. Add these two new routes below it, above the if __name__ == "__main__": block:
# Serve the PWA manifest from the static folder
@app.route("/manifest.json")
def manifest():
    return send_from_directory("static", "manifest.json")


# Serve the service worker from root scope with correct headers
@app.route("/sw.js")
def service_worker():
    response = make_response(send_from_directory("static", "sw.js"))
    response.headers["Content-Type"] = "application/javascript"
    response.headers["Cache-Control"] = "no-cache"
    return response

What do these routes do?

  • The /manifest.json route serves your manifest from the static/ folder. Browsers expect the manifest at the site root, not under /static/.
  • The /sw.js route serves the service worker from the root scope. Service workers can only control pages at or below their scope. Serving from / gives it control over the entire site.
  • Cache-Control: no-cache ensures browsers always check for updated service worker code instead of using a stale cached copy.
  • Save app.py. Flask's debug mode detects the change and auto-reloads the server.
  • Open http://localhost:5000/manifest.json in your browser. You should see the manifest JSON displayed as raw text.

Not seeing JSON in the browser?

  • Check that manifest.json is saved inside the static/ folder, not in the project root.
  • Make sure you saved app.py after adding the routes. Check your terminal for auto-reload messages.
  • If the server crashed after the reload, stop it with Ctrl+C and restart with python app.py.

help me fix my Flask manifest route not serving JSON

✔️ Awesome, I've got everything!

Great. Double check you've saved static/manifest.json.

ⓧ I'd like to double check the full code

{
    "name": "Oil Price Dashboard",
    "short_name": "Oil Prices",
    "start_url": "/",
    "display": "standalone",
    "background_color": "#1a1a2e",
    "theme_color": "#1a1a2e",
    "description": "Live oil price tracker for your local network",
    "icons": [
        {
            "src": "/static/icon-192.png",
            "sizes": "192x192",
            "type": "image/png"
        },
        {
            "src": "/static/icon-512.png",
            "sizes": "512x512",
            "type": "image/png"
        }
    ]
}
Add a service worker

A service worker is a script that runs in the background of the browser. It sits between your app and the network, intercepting requests and serving cached responses. This is what makes the dashboard load instantly after the first visit.

  • Create a file called sw.js inside your static/ folder. Start with the cache setup and lifecycle event listeners:
// Name for this cache version
var CACHE_NAME = 'oil-dashboard-v1';
// URLs to cache when the service worker installs
var SHELL_URLS = [
    '/',
    '/manifest.json'
];

// On install, open the cache and store the app shell
self.addEventListener('install', function(event) {
    event.waitUntil(
        caches.open(CACHE_NAME).then(function(cache) {
            return cache.addAll(SHELL_URLS);
        })
    );
});

// On activate, delete any old caches from previous versions
self.addEventListener('activate', function(event) {
    event.waitUntil(
        caches.keys().then(function(cacheNames) {
            return Promise.all(
                cacheNames.filter(function(name) {
                    return name !== CACHE_NAME;
                }).map(function(name) {
                    return caches.delete(name);
                })
            );
        })
    );
});

What does this code do?

  • CACHE_NAME labels this version of the cache. If you ever update the service worker, you change this name so the old cache gets cleaned up.
  • SHELL_URLS lists the core pages to cache on install. The dashboard page (/) and manifest are cached so they load instantly on repeat visits.
  • The install listener pre-caches these URLs the first time the service worker activates.
  • The activate listener cleans up any outdated caches from older versions of the service worker.
  • Add the fetch handler below the existing code in static/sw.js:
// Intercept all network requests
self.addEventListener('fetch', function(event) {
    if (event.request.url.includes('/api/')) {
        // API calls: try network first, return empty array if offline
        event.respondWith(
            fetch(event.request).catch(function() {
                return new Response(
                    JSON.stringify([]),
                    { headers: { 'Content-Type': 'application/json' } }
                );
            })
        );
    } else {
        // Static assets: serve from cache first, fall back to network
        event.respondWith(
            caches.match(event.request).then(function(response) {
                return response || fetch(event.request);
            })
        );
    }
});

What does this code do?

The fetch handler uses two different strategies depending on the request:

  • For /api/ requests (price data), it tries the network first. If the network fails, it returns an empty JSON array so the page doesn't crash.
  • For everything else (the HTML page, manifest, icons), it serves from the cache first for speed. If the resource isn't cached, it falls back to the network.
  • Save static/sw.js.
  • Open http://localhost:5000/sw.js in your browser. You should see the JavaScript code displayed as plain text.

Seeing a 500 error or blank page?

  • Make sure sw.js is inside the static/ folder, not in the project root.
  • Check that you saved app.py after adding both routes in the previous substep.
  • If the server crashed, restart it with python app.py.

help me fix my Flask service worker route returning a 500 error

✔️ Awesome, I've got everything!

Great. Double check you've saved static/sw.js.

ⓧ I'd like to double check the full code

Scroll up and confirm these sections are present in static/sw.js, in this order:

  • The CACHE_NAME and SHELL_URLS variables at the top.
  • The install event listener that caches the app shell.
  • The activate event listener that cleans up old caches.
  • The fetch event listener with network-first for API calls and cache-first for everything else.
Register the service worker in your HTML

The manifest and service worker files exist and Flask is serving them. The last step is telling index.html about them so the browser actually loads and registers them.

  • In templates/index.html, find the <title> tag in the <head> section:
    <title>Oil Price Dashboard</title>
  • Add these two lines directly above the <title> tag, so the <head> now looks like this:
    <meta name="theme-color" content="#1a1a2e">
    <link rel="manifest" href="/manifest.json">
    <title>Oil Price Dashboard</title>

What do these tags do?

  • <meta name="theme-color"> sets the browser toolbar color when the app is installed. It matches the dashboard's dark background.
  • <link rel="manifest"> points the browser to your manifest file so it can read your app's PWA metadata.
  • Scroll to the bottom of the <script> block. Find this line:
        document.getElementById('lastUpdated').textContent =
            'Last updated: ' + new Date().toLocaleTimeString();
  • Add this code directly below it, before the closing </script> tag:
        // Register the service worker for PWA functionality
        if ('serviceWorker' in navigator) {
            window.addEventListener('load', function() {
                navigator.serviceWorker.register('/sw.js', { scope: '/' })
                    .then(function(registration) { console.log('Service Worker registered'); })
                    .catch(function(err) { console.log('Service Worker registration failed:', err); });
            });
        }

What does this do?

This checks whether the browser supports service workers, then registers /sw.js after the page loads. Once registered, the service worker runs in the background and intercepts network requests.

On localhost, browsers treat the connection as secure, so the service worker registers without HTTPS. On other devices on your network (via your IP address), the service worker will not register because the connection isn't secure.

  • Save templates/index.html.

✔️ Awesome, I've got everything!

Great. Confirm you've added the <meta name="theme-color"> and <link rel="manifest"> tags in the <head>, and the service worker registration script at the end of the <script> block.

ⓧ I'd like to double check the full code

Scroll up and confirm these additions are in templates/index.html:

  • In the <head>: <meta name="theme-color" content="#1a1a2e"> and <link rel="manifest" href="/manifest.json"> appear above the <title> tag.
  • At the end of the <script> block: the if ('serviceWorker' in navigator) block that calls navigator.serviceWorker.register('/sw.js').

Before you refresh the page, what do you think will be different about Chrome's address bar this time?

  • Open http://localhost:5000 in Chrome, or refresh if it's already open.
  • Look for a small install icon in the right side of the address bar. It looks like a monitor with a down arrow.
  • Click the install icon.

The Oil Price Dashboard opens in its own standalone window, looking and feeling like a native app. No address bar, no browser tabs.

Don't see the install icon?

  • Make sure you are on http://localhost:5000, not your IP address. PWA install only works on localhost without HTTPS.
  • Chrome requires you to interact with the page first (click or tap somewhere) and spend at least 30 seconds viewing it before showing the install prompt.
  • Open Chrome DevTools (F12), go to the Application tab, then Manifest. Check for any errors listed under Installability.
  • Confirm the icon files (icon-192.png and icon-512.png) exist in your static/ folder. Missing icons prevent installability.

help me debug why Chrome isn't showing the PWA install icon on localhost

Secret mission

Enable PWA Install from Any Device with HTTPS

Right now, your PWA install prompt only appears on localhost. Other devices on your network can see the dashboard, but the browser blocks the service worker and install prompt because the connection is not secure. In this Secret Mission, you will set up HTTPS with a locally trusted certificate so the PWA is installable from any device on your Wi-Fi.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local machine with no cloud resources or ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Flask development server running on your PC.
  • oil-dashboard project folder with all source files and virtual environment.
  • OilPriceAPI free account and API key.
  • mkcert local certificate authority on your PC (Secret Mission only).
  • mkcert root CA certificate installed on your phone or tablet (Secret Mission only).

Keep everything running

No action needed. Choose this if you are still actively monitoring oil prices on your network.

Watch your API usage

The OilPriceAPI free tier allows 1,000 requests per month. At the default 60-second polling interval with the browser open continuously, you would send roughly 43,200 requests per month and exceed the free tier.

To stay within the limit, increase the polling interval in index.html from 60000 milliseconds to 900000 (every 15 minutes). That keeps you under roughly 2,880 requests per month.

Pause - I'll come back to this later

Stop the server to free up your terminal. Your project files stay on disk, ready to restart anytime.

  • Press Ctrl+C in the terminal to stop the Flask server.
  • To restart later, open a terminal in the oil-dashboard folder, activate the virtual environment, and start the server by running:
python app.py

Delete - I don't want to use this again

Remove all project resources and start fresh.

  • Press Ctrl+C in the terminal to stop the Flask server.
  • Deactivate the virtual environment and navigate out of the project folder by running:
deactivate
cd ..
  • Delete the oil-dashboard folder.

macOS

rm -rf oil-dashboard

Windows

Remove-Item -Recurse -Force oil-dashboard
  • If you installed the PWA in Chrome, open the installed app, click the three-dot menu in the title bar, and choose the option to uninstall it.
  • Optionally revoke your API key by signing in at oilpriceapi.com and deleting the key from your account dashboard.

Completed the Secret Mission?

  • Remove the local certificate authority from your Windows trust store by running:
mkcert -uninstall
  • On your phone or tablet, remove the mkcert root CA certificate from your device's security or certificate settings.

Nice Work!

Nice Work!

You just built a personal oil price monitoring station that runs on your home network. Any device on your Wi-Fi can pull up live prices for seven major commodities, spot which ones moved, and even install the dashboard as an app.

You've learned how to:

  • Build a Flask web server that fetches live oil prices from the OilPriceAPI and uses change detection to highlight price movements with green and red indicators.
  • Make the dashboard accessible from any device on your local network by binding Flask to 0.0.0.0 and serving a JSON API endpoint for auto-refreshing prices without a full page reload.
  • Turn the dashboard into an installable Progressive Web App with a service worker for caching and a manifest so the dashboard opens in its own window like a native app.
  • Secret Mission: Set up HTTPS with mkcert so the PWA is installable from any device on your network, with a locally trusted certificate authority.

Ready to quiz yourself?