← Computer Science
1 / 1
← → move  F fullscreen  Esc read

Raspberry Pi · Lesson 1

Raspberry Pi Web Server

Turn a computer the size of a deck of cards into a live website

Right now the Raspberry Pi in front of you is just a small computer. By the end of today it will be a server: it will have a web address, and anyone on the school network can type that address into another computer and see a page you built. By the end of the week that page will be showing the Pi's own CPU, memory, disk and temperature, updating live, and it will look good.

What you'll learn

What you need

Before you touch the Pi

  1. Power comes out last. Shut the Pi down from the menu before unplugging it. Yanking the power while it is writing to the SD card is the one reliable way to destroy your work.
  2. The SD card stays in. It is the Pi's whole hard drive. Do not pop it out to see what happens.
  3. Nothing goes in the ports except what belongs there. The exposed pins along the edge are live.
  4. Never type a password into a file. Not your Pi password, not the Wi-Fi password, not anything. You will be asked for a password in the terminal on Day 2. That is the only place it goes.
If something goes wrong, stop and tell Mr. Krigger. A Pi that will not boot is a ten-second fix if you say so and a lost week if you do not.

How this lesson works

Every grey box is something you type or paste. The bar on top of each box tells you where it goes.

The bar saysWhat to do
Type in the TerminalA command. Click Copy, click inside the Terminal window, paste, then tap the Enter key.
server.py or static/index.htmlThe inside of a file. Open the file with nano, click Copy, paste into nano, then save.
Gold bar, dashed box, no Copy buttonLook, do not paste. It is an example or what you should see.
Three keys that work differently in the Terminal. Paste is Ctrl+Shift+V, not Ctrl+V. Ctrl+C does not copy, it stops whatever is running. And typing a command does nothing until you tap the Enter key.

If the Copy button says Press Ctrl+C, your browser blocked it. The code is already selected, so press Ctrl+C in the browser and carry on.

Day 1

Start here

Day 1 start
  1. Pick a Pi and keep it. Use the same Pi on Days 2 and 3. Your work is saved on that Pi, so a different Pi will not have it.
  2. Open the Terminal. It is the black rectangle icon on the top bar.
  3. Today: write down your username, your IP address and your folder name, build your Python workspace, then run your first web server.
  4. Finish line: a Chromium tab that says My Pi is a web server.

Step 1: Find out who you are and where you are

Open the Terminal on the Pi. It is the black rectangle icon on the top bar.

What is a Terminal? A way to talk to the computer by typing instead of clicking. Same computer, same files, different door. Think of it as texting your Pi instead of tapping icons.

Type this, then tap the Enter key. The Terminal does nothing until you do:

whoami

That prints your username on this Pi. Write it down. You will need it on Day 2. Now type:

hostname -I

That prints your Pi's IP address, something like 10.0.4.71. Write it on a sticky note and put it on your monitor. Everything on Day 2 depends on it.

Checkpoint: you have two things written down, your username and your Pi's IP address.

What an IP address is

Your network is a neighborhood and every device on it gets a house number. That is the IP address. When someone types it into a browser they are saying "take me to that house."

The computer next to you has one. So does every Chromebook in the school. Your Pi has one, and for the next three days it is the only thing standing between your project and everybody else being able to see it.

It can change. The network sometimes hands your Pi a new house number overnight. That is why Days 2 and 3 start by checking it again.

Claim your own folder

Everyone in both classes shares these Pis. If two people build a folder with the same name, the second person lands inside the first person's project, sees work they did not do, and overwrites it when they save. So before you build anything, you pick a name that is only yours.

Your folder name is your first name plus the first letter of your last name, all lowercase, no spaces. Jordan Baptiste is jordanb. If two of you would get the same name, one of you adds a number: jordanb2.

Type it in the box and every command on this page changes to use it. If the box is not there, type your folder name everywhere you see YOURNAME.

Checkpoint: your folder name is written on your sticky note, under your IP address.

What changed, with an example

This lesson used to have everyone use the same two names, pihealth and pi-dashboard. Same names on a shared Pi means the second person walks into the first person's work. The names stay the same. They just move inside your own folder.

Old wayNew way, for jordanb
Your foldernone~/jordanb
Python toolbox~/pihealth~/jordanb/pihealth
Project~/pi-dashboard~/jordanb/pi-dashboard
Prompt shows(pihealth)(jordanb-pihealth)
Already built ~/pihealth or ~/pi-dashboard the old way? Leave them where they are. You cannot tell whose work is in there. Start fresh in your own folder.

Sharing a Pi without wrecking anyone's work

See whose folders are already on this Pi:

ls ~

You will see Desktop, Documents and the rest, plus one folder per student who has used this Pi. Four rules:

  1. Only work inside your own folder. Every command in this lesson starts with ~/YOURNAME. If a command does not, stop and check.
  2. Never open, edit or delete a folder that is not yours. Treat it like someone's locker.
  3. Your name already there, and it is not your work? Someone else has your name. Add a number and use that instead.
  4. Before you leave, stop your server with Ctrl+C. Only one server can use port 5000 at a time, and the next person needs it.

Step 2: Set up your Python workspace

This project needs two Python add-ons that are not installed by default. They live in a little workspace of their own called pihealth, inside your folder. First, check whether you already built it last time:

ls ~/YOURNAME/pihealth
What you seeWhat it meansWhat to do
A short list: bin, include, lib, pyvenv.cfgAlready builtSkip to Open it
No such file or directoryNot built yetDo Build it first

Build it. Two commands, one at a time. Wait for the first to finish.

mkdir -p ~/YOURNAME && python3 -m venv --prompt YOURNAME-pihealth ~/YOURNAME/pihealth
~/YOURNAME/pihealth/bin/pip install flask psutil

The second one downloads for a minute and prints a lot of text. Wait for your prompt to come back.

Open it. Everyone runs this, whether you just built the workspace or you built it last time:

source ~/YOURNAME/pihealth/bin/activate
Checkpoint: your prompt now starts with (YOURNAME-pihealth), your own name. Someone else's name there means you opened their box. Type deactivate and run your own source line.

What did you just build?

A virtual environment: a box that holds one project's extra Python tools and nothing else. Two tools went in the box. Flask lets Python run a website. psutil lets Python read the computer's own CPU, memory and temperature.

Think of it like a toolbox for one job. The rest of the Pi is not affected, and if you ever wreck it you delete the folder and build a new one in thirty seconds.

The thing that catches everyone. Opening the box only lasts for that one Terminal window. Close it, open a new one, and you are back outside the box. The giveaway is (YOURNAME-pihealth) missing from your prompt, and the symptom is ModuleNotFoundError: No module named 'flask'. The fix is always the same: run the source line again.

Step 3: Make your project folder

mkdir -p ~/YOURNAME/pi-dashboard/static && cd ~/YOURNAME/pi-dashboard

You just made this:

YOURNAME/               ← your folder, nobody else's
├── pihealth/           ← your Python toolbox (Step 2)
└── pi-dashboard/       ← this project
    ├── server.py       ← the Python that runs the server (next step)
    └── static/         ← the files your server hands out
        └── index.html  ← your actual web page (Step 7)

The ~ means the home folder of the account you are logged in as. Everyone on this Pi shares that account, which is exactly why your work lives one level down, in ~/YOURNAME.

Meet nano, the editor you will use all week

nano opens inside the Terminal. There are no menus to click. Mouse clicks will not move the cursor, so use the arrow keys.

The bottom of the screen lists the commands. The little ^ means the Ctrl key, so ^O means hold Ctrl and tap O.

The two in gold are the two you need today.

Save and exit nano, one key at a time

Save (do this often):

  1. Hold the Ctrl key and tap O. A line appears at the bottom: File Name to Write: server.py
  2. Tap the Enter key on your keyboard. That says yes to the name. The bottom now says [ Wrote 9 lines ]

Exit: hold Ctrl and tap X. You are back at the Terminal prompt.

Pressed Ctrl+X before saving? nano asks Save modified buffer? Tap the Y key for yes, then the Enter key. Tap N to throw your changes away.
Checkpoint: you saw [ Wrote ... lines ] before you exited. No message means nothing was saved.

Step 4: Write the smallest web server that works

nano server.py

Type or paste exactly this. The easy way: click Copy above the code, click inside the nano window, then press Ctrl+Shift+V.

from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return "<h1>My Pi is a web server.</h1>"

app.run(host="0.0.0.0", port=5000)

Save: hold Ctrl and tap O, then tap the Enter key. Exit: hold Ctrl and tap X.

Pasting into the Terminal is Ctrl+Shift+V, not Ctrl+V. This trips up everyone once.

Step 5: Run it

python3 server.py

The Terminal will print a few lines and then sit there looking frozen. It has not crashed, it is listening.

Open Chromium on the Pi and go to:

http://localhost:5000
Checkpoint: the browser says My Pi is a web server. That is a real server, running on a real computer, serving a real page. It took six lines.

To stop it: click the Terminal window and press Ctrl+C. Remember this. You will need it constantly.

You will also see a yellow warning about a "development server" and not using it in production. Ignore it. It means this server is built for learning, not for handling a million people at once, which is what you are doing.

What the Terminal should print

When your server starts, it prints lines like these. Read them. The last Running on line is your Pi's IP address and port, which is the address Day 2 uses.

 * Serving Flask app 'server'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5000
 * Running on http://10.0.4.71:5000
Press CTRL+C to quit
Saw ModuleNotFoundError instead? You are outside your toolbox. Your prompt is missing (YOURNAME-pihealth). Run the source line from Step 2 again, then run this again.

What a server actually does

A server does one thing, over and over, forever:

  1. It waits.
  2. Something asks it for a thing. This is a request.
  3. It sends that thing back. This is a response.
  4. Go to 1.

Nothing else. When you typed http://localhost:5000 into Chromium, the browser sent a request to your Pi. Your six lines of Python caught it, and the return line was the response.

Netflix does the same thing. So does Instagram, so does your school's grade portal. They have more lines of code and more computers, but the loop is identical: wait, request, response, repeat.

The @app.route("/") line is the part that decides which request. / means the front page. Next you will add a second route, and the server will start answering two different questions.

Step 6: Build the real thing

Stop your server with Ctrl+C. You are replacing the whole file, so throw the old one away and open a blank one. Run these two, one at a time:

rm ~/YOURNAME/pi-dashboard/server.py
nano ~/YOURNAME/pi-dashboard/server.py

The nano screen is empty. Click Copy on the code below, click inside the Terminal, and paste with Ctrl+Shift+V:

from flask import Flask, jsonify, send_from_directory
import psutil
import subprocess
import os

STATIC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")

app = Flask(__name__)


@app.route("/")
def home():
    return send_from_directory(STATIC, "index.html")


@app.route("/<path:filename>")
def other_files(filename):
    return send_from_directory(STATIC, filename)


def temperature_c():
    try:
        for group in psutil.sensors_temperatures().values():
            for reading in group:
                if getattr(reading, "current", None) is not None:
                    return round(float(reading.current), 1)
    except Exception:
        pass
    try:
        output = subprocess.check_output(["vcgencmd", "measure_temp"], text=True)
        return round(float(output.split("=")[1].split("'")[0]), 1)
    except Exception:
        return None


@app.route("/api/stats")
def stats():
    return jsonify({
        "cpu": psutil.cpu_percent(interval=0.2),
        "memory": psutil.virtual_memory().percent,
        "disk": psutil.disk_usage("/").percent,
        "temperature": temperature_c()
    })


app.run(host="0.0.0.0", port=5000)

Save: hold Ctrl and tap O, then tap the Enter key. Exit: hold Ctrl and tap X.

What just happened? Your server now answers three different requests. / hands over the file static/index.html. /api/stats hands over four numbers, freshly measured, as text. Anything else gets looked up in your static folder, which is how images and CSS files will work later.

Step 7: Write the page

nano static/index.html

Same moves as before: Copy, click inside nano, Ctrl+Shift+V.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Pi Health</title>
</head>
<body>

  <h1>Raspberry Pi Health</h1>
  <p>CPU: <span id="cpu">--</span>%</p>
  <p>Memory: <span id="memory">--</span>%</p>
  <p>Disk: <span id="disk">--</span>%</p>
  <p>Temperature: <span id="temperature">--</span> C</p>

  <script>
    async function refresh() {
      const response = await fetch("/api/stats");
      const data = await response.json();
      for (const name of ["cpu", "memory", "disk", "temperature"]) {
        document.getElementById(name).textContent =
          data[name] === null ? "--" : Math.round(data[name]);
      }
    }

    refresh();
    setInterval(refresh, 2000);
  </script>

</body>
</html>

Save and exit the same way (Ctrl+O, Enter, Ctrl+X), then run it:

python3 server.py

Reload http://localhost:5000 in Chromium.

Checkpoint: four numbers, and they change on their own every two seconds. Open a few browser tabs and watch CPU climb. That is your Pi reporting on itself.
Temperature showing --? Some Pi models do not report it. Everything else still works. Not your fault, not a bug.

Meet Thonny, a second editor

nano lives inside the Terminal. Thonny is a Python editor with windows and buttons, and it is already on your Pi. It colors your code, runs your program with one click, and shows errors in red with the line number. Many programmers learn Python in it first.

Today you learn it. On Day 3 you will use it for the Python edit.

  1. Stop your server. Click the Terminal and press Ctrl+C. Thonny will run its own copy, and two servers cannot share port 5000.
  2. Open Thonny: Menu → Programming → Thonny.
  3. Open your server file. Click File, then Open. Click Home, then YOURNAME, then pi-dashboard, then server.py, then Open.
Checkpoint: the code from Step 6 is on screen with colored words. Under it is a window called Shell. If you cannot see the Shell, click View and tick Shell.

Point Thonny at your toolbox

Thonny starts with its own Python, and that Python does not have Flask. It is the same problem as ModuleNotFoundError in the Terminal: you are outside your toolbox. Tell Thonny to use the one you built.

  1. Click Tools, then Options, then the Interpreter tab.
  2. Choose Alternative Python 3 interpreter or virtual environment.
  3. In the box, type this path. Thonny does not understand ~, so write out your username. It is on your sticky note.
/home/USERNAME/YOURNAME/pihealth/bin/python3
  1. Click OK. The Shell restarts.
Checkpoint: the first line in the Shell shows a Python version and a path in brackets, and the path has pihealth in it. If it still shows another path, go back to step 2.

Run your server from Thonny

Click the green Run button on the toolbar. The Shell prints the same Running on lines the Terminal did. Open http://localhost:5000 in Chromium.

To stop it, click the red Stop button. Thonny's Stop does what Ctrl+C did.

Terminal and nanoThonny
Start the serverpython3 server.pyGreen Run button
Stop the serverCtrl+CRed Stop button
SaveCtrl+O, EnterCtrl+S
PasteCtrl+Shift+VCtrl+V
FindCtrl+WCtrl+F
Checkpoint: your dashboard loads at http://localhost:5000 while the server runs from Thonny, and stops loading after you click Stop.
One server at a time. If Run says Address already in use, your server is still running in the Terminal. Stop it there first.

Break it on purpose

Programmers spend a lot of time reading errors, and Thonny makes them easy to read. Break your file once while nothing depends on it.

  1. Scroll to the last line, app.run(host="0.0.0.0", port=5000). Delete the final ).
  2. Click Run.
  3. Read the red text in the Shell. It names the kind of error and the line number. Click the line link to jump to it.
  4. Type the ) back, then click Run again.
Checkpoint: you saw a red SyntaxError, fixed it, and the server ran again. Click Stop when you are done.
Which editor do I use? Either one works for server.py. Thonny can open index.html too, but it will not color it, so the page stays in nano.

Before you leave today

Day 1 end
  1. Stop your server. Click the Terminal and press Ctrl+C. If you ran it from Thonny, click the red Stop button instead. Only one server can use port 5000, and the next class needs it.
  2. Check your sticky note. It should have three things on it: your username, your Pi's IP address and your folder name. Stick it on the monitor.
  3. Remember where you sat. Day 2 starts at this same Pi. Open the Day 2 tab and begin with Start here.

The other way to do this

Optional

Your page lives in its own file and your Python hands that file over. There is a second way worth seeing once. HTML is just text, and Python is very good at holding text, so you can put the entire page inside the Python file as one long string.

from flask import Flask, render_template_string

app = Flask(__name__)

PAGE = """
<!doctype html>
<html>
  <head><title>Pi Health</title></head>
  <body>
    <h1>My page lives inside the Python file.</h1>
  </body>
</html>
"""


@app.route("/")
def home():
    return render_template_string(PAGE)


app.run(host="0.0.0.0", port=5000)

render_template_string means "here is a page, as text, send it." Compare that to send_from_directory, which means "here is a filename, go find it and send it."

HTML in the stringHTML on its own
Files to track12
Editing 200 lines of HTMLPainfulNormal
Adding a stylesheet or a photoAwkwardAlready works
Your editor coloring the HTMLNoYes

We used two files because the next project is a whole website with images and a stylesheet, and that is the shape it needs. A one-file server is a real and reasonable thing to build when the page is four lines long.

Day 2

Start here: get back to where you stopped

A new Terminal window knows nothing about yesterday, so every day starts the same way.

Day 2 start
  1. Sit at the same Pi as Day 1. Open the Terminal and check your folder is here:
    ls ~
    Your folder name is not in the list? Wrong Pi. Go back to yours. Do not rebuild.
  2. Use a fresh Terminal window. A new window has forgotten your toolbox and does not know which folder you are in. The next step fixes both.
  3. Step back into your toolbox and your project. Click Copy, paste, tap Enter:
    source ~/YOURNAME/pihealth/bin/activate
    cd ~/YOURNAME/pi-dashboard

Check you are really back

Look for these three signs before you do anything else. This takes ten seconds and saves most of the problems from last time.

CheckYou should seeIf not
The start of your prompt(YOURNAME-pihealth)Run the source line again
Type pwd, tap EnterA path ending in YOURNAME/pi-dashboardRun the cd line again. Check the spelling
Type ls, tap Enterserver.py and staticWrong folder or wrong Pi. Run ls ~

Then check your IP address. It can change overnight, and Day 2 depends on it:

hostname -I

Different from your sticky note? Cross the old one out and write the new one.

Now start your server and leave this window alone:

python3 server.py

Step 1: Load your dashboard on a different computer

Your server is running. Now walk to a different computer in the lab that is not a Pi. The one you will use for SSH later works fine. Open its web browser, click the address bar, and type your Pi's IP address and port:

http://YOUR-PI-IP:5000

So if your sticky note says 10.0.4.71, you type http://10.0.4.71:5000 and tap the Enter key.

Checkpoint: your Pi's dashboard on the other computer, updating live. Nothing is on the internet. That computer asked your Pi directly, across the room.
Loading someone else's numbers? You typed their IP. Check your sticky note.
Nothing loads? Check three things, in order. Is the computer on the school network? Is the server still running in the Pi's Terminal? Did you type :5000 after the IP address? Still stuck, tell Mr. Krigger.

Why 0.0.0.0 was the important part

Look at the last line of server.py:

app.run(host="0.0.0.0", port=5000)

0.0.0.0 means answer requests from anywhere on the network, not just from me. If it said 127.0.0.1 instead, the page would work in Chromium on the Pi and every other computer would get nothing.

AddressWhat it meansWorks from
http://localhost:5000this computer, port 5000only the Pi itself
http://10.0.4.71:5000the device at that house number, port 5000any device on the network

Port 5000 is the door number. One computer can run many servers at once as long as each uses a different door.

Step 2: Look at the raw data

Stay on the other computer, in the same browser. You are going to add one thing to the end of the address.

  1. Click the address bar.
  2. Type your Pi's address again, with /api/stats on the end.
  3. Tap the Enter key.
http://YOUR-PI-IP:5000/api/stats

You will see one short line of text and nothing else. No colors, no cards:

{"cpu":12.5,"disk":31.2,"memory":24.8,"temperature":47.1}

Refresh the page. The numbers change. That is your server measuring itself, fresh, every time something asks.

Seeing a table or a box that says Pretty print instead? Some browsers dress the data up. Click Raw or Raw Data so it is one plain line, then take your screenshot.

Step 2b: Take the screenshot now

You need a screenshot of this page for your hand-in. Take it while it is on screen, so you are not hunting for it on Day 3.

  1. Take a whole-screen screenshot. The shortcuts for each kind of computer are in the Turn in tab.
  2. Save it as api-stats.png.
  3. Open it and check: can you read your Pi's IP address in the address bar and the line of data underneath?
10.0.4.71:5000/api/stats
{"cpu":12.5,"disk":31.2,"memory":24.8,"temperature":47.1}
Good. The address bar shows your Pi's IP and /api/stats, and the page is one line of data.
address bar cropped out
{"cpu":12.5,"disk":31.2,"memory":24.8,"temperature":47.1}
Not good. Cropped too tight. Nothing shows where this data came from, so it proves nothing.
Checkpoint: api-stats.png is saved, and you can read the address bar in it.

API and JSON

An API is a door on a server that hands back data instead of a web page.

Think about ordering food. You do not walk into the kitchen. You tell a waiter what you want, the waiter goes to the kitchen, and the waiter brings back a plate. You never see how the kitchen works and you do not need to.

/api/stats is the waiter. Your web page is you. The Python function stats() is the kitchen.

JSON is what is on the plate. It is just text, arranged so a program can read it:

{"cpu": 12.5, "memory": 24.8}

A name, a colon, a value, commas between them, curly braces around the whole thing. The whole format, start to finish. Almost every app on your phone is passing JSON back and forth all day.

Now look at your page again. These three lines are the whole conversation:

const response = await fetch("/api/stats");   // ask the waiter
const data = await response.json();           // take the plate
setInterval(refresh, 2000);                   // do it again in 2 seconds

Step 3: Connect to your Pi from a different computer

SSH lets you type commands on your Pi while sitting at a different machine. No monitor, no keyboard on the Pi at all.

Go to one of the lab computers. Open its terminal. Type this, using your username and your Pi's IP:

ssh YOUR-USERNAME@YOUR-PI-IP

For example:

ssh krigger@10.0.4.71

The first time, it will ask something like Are you sure you want to continue connecting? Type yes and tap the Enter key. Then it asks for your Pi password.

Nothing will appear on screen while you type the password. That is deliberate, so nobody can read it over your shoulder. Type it and tap the Enter key.
Checkpoint: your prompt changed. It now shows your Pi's name, not the lab computer's. Every command you type from here runs on the Pi.

Steps 4 and 5: Prove it, then start the server remotely

Still in the SSH window, run:

hostname
uptime

The first prints your Pi's name. The second prints how long it has been switched on. Neither of those is the computer you are sitting at. To leave:

exit
Stop the server on the Pi first. Your server from Step 1 is still running. Go to the Pi's Terminal and press Ctrl+C. If you skip this, the next command ends in Address already in use, because only one server can use port 5000.

Now SSH back in and start your server without touching the Pi:

source ~/YOURNAME/pihealth/bin/activate
cd ~/YOURNAME/pi-dashboard
python3 server.py

Then open a browser on the lab computer and go to your Pi's address. You just started a server on a computer across the room and loaded its page. Plenty of people do exactly this for a living.

Heads up: when you close the SSH window, the server stops with it. That is normal. There are ways around it, and that is a good thing to ask about if you are curious.

What SSH is

SSH stands for Secure Shell. Two halves:

That second half is the whole reason it exists. Before SSH, people used a tool called Telnet that sent passwords across the network in plain readable text. If you were on the same network, you could just read them.

This is how IT staff fix servers they will never physically see, how your school's tech department reaches every machine in the building from one desk, and how anyone who runs a game server does anything at all.

Before you leave today

Day 2 end
  1. Check you have api-stats.png. If you do not, load /api/stats once more and take it now.
  2. Stop your server with Ctrl+C, then close the SSH window by typing exit and tapping Enter.
  3. Update your sticky note if your IP address changed today.
  4. Tomorrow: same Pi. Open the Day 3 tab and begin with Start here.

Day 3

Start here: get back to where you stopped

Same start as Day 2. Only the page changes today.

Day 3 start
  1. Same Pi, fresh Terminal. Your folder should be on it.
  2. Step back in. Click Copy, paste, tap Enter:
    source ~/YOURNAME/pihealth/bin/activate
    cd ~/YOURNAME/pi-dashboard
  3. Check the three signs. Prompt starts with (YOURNAME-pihealth). pwd ends in YOURNAME/pi-dashboard. ls shows server.py and static.
  4. Prove yesterday's work runs. Start it, open http://localhost:5000 in Chromium, see four numbers, then Ctrl+C:
    python3 server.py

Step 1: Start the page over

Nothing about the Python changes today. The server already works. You are only replacing the page it hands out. Real web work usually goes this way, with the thing underneath staying put while the thing people see gets redesigned.

Make sure your server is stopped (Ctrl+C). First keep a copy of yesterday's page, just in case. Then throw the old one away and open a blank one:

cp ~/YOURNAME/pi-dashboard/static/index.html ~/YOURNAME/pi-dashboard/static/index-day2.html
rm ~/YOURNAME/pi-dashboard/static/index.html
nano ~/YOURNAME/pi-dashboard/static/index.html

You now have an empty file. Nothing to select, nothing to accidentally delete half of.

Step 2: Paste the new page

Click Copy to grab the whole page, click inside the empty nano window, and press Ctrl+Shift+V. Then save and exit (Ctrl+O, tap Enter, Ctrl+X).

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pi Health</title>
<style>
  /* =====================================================================
     THEME BLOCK. Everything you need to restyle this whole page lives
     between these two lines. Change a value, save, refresh.
     ===================================================================== */
  :root {
    --bg-top:    #0f2027;
    --bg-bottom: #2c5364;
    --text:      #ffffff;
    --muted:     rgba(255, 255, 255, 0.70);
    --card:      rgba(255, 255, 255, 0.12);
    --card-edge: rgba(255, 255, 255, 0.18);
    --track:     rgba(255, 255, 255, 0.15);
    --ok:        #4ade80;
    --warn:      #facc15;
    --hot:       #f87171;
    --round:     16px;
    --font:      system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  }
  /* ================================================================== */

  * { box-sizing: border-box; }

  body {
    margin: 0;
    min-height: 100vh;
    padding: 32px 20px;
    color: var(--text);
    font-family: var(--font);
    background: linear-gradient(135deg, var(--bg-top), var(--bg-bottom));
  }

  h1 { margin: 0 0 4px; font-size: 28px; }
  .sub { margin: 0 0 28px; color: var(--muted); font-size: 14px; }

  .grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 18px;
    max-width: 960px;
  }

  .card {
    padding: 20px;
    border-radius: var(--round);
    border: 1px solid var(--card-edge);
    background: var(--card);
    backdrop-filter: blur(10px);
    transition: transform 0.15s ease;
  }
  .card:hover { transform: translateY(-4px); }

  .card h2 { margin: 0 0 12px; font-size: 15px; font-weight: 600; color: var(--muted); }

  .value { font-size: 42px; font-weight: 700; line-height: 1; }
  .unit  { font-size: 16px; font-weight: 400; color: var(--muted); margin-left: 2px; }

  .bar {
    margin-top: 14px;
    height: 8px;
    border-radius: 999px;
    background: var(--track);
    overflow: hidden;
  }
  .fill {
    width: 0%;
    height: 100%;
    border-radius: 999px;
    background: var(--ok);
    transition: width 0.4s ease, background 0.4s ease;
  }

  .warn .fill  { background: var(--warn); }
  .warn .value { color: var(--warn); }
  .hot  .fill  { background: var(--hot); }
  .hot  .value { color: var(--hot); }
</style>
</head>
<body>

<h1>🖥️ Raspberry Pi Health</h1>
<p class="sub">Live from my Pi. Updates every 2 seconds.</p>

<div class="grid">

  <div class="card" id="card-cpu">
    <h2>⚡ CPU Usage</h2>
    <div><span class="value" id="cpu">--</span><span class="unit">%</span></div>
    <div class="bar"><div class="fill" id="bar-cpu"></div></div>
  </div>

  <div class="card" id="card-memory">
    <h2>🧠 Memory Used</h2>
    <div><span class="value" id="memory">--</span><span class="unit">%</span></div>
    <div class="bar"><div class="fill" id="bar-memory"></div></div>
  </div>

  <div class="card" id="card-disk">
    <h2>💽 Disk Used</h2>
    <div><span class="value" id="disk">--</span><span class="unit">%</span></div>
    <div class="bar"><div class="fill" id="bar-disk"></div></div>
  </div>

  <div class="card" id="card-temperature">
    <h2>🔥 Temperature</h2>
    <div><span class="value" id="temperature">--</span><span class="unit">&deg;C</span></div>
    <div class="bar"><div class="fill" id="bar-temperature"></div></div>
  </div>

</div>

<script>
  const CARDS = [
    { key: "cpu",         max: 100, warn: 50, hot: 80 },
    { key: "memory",      max: 100, warn: 60, hot: 85 },
    { key: "disk",        max: 100, warn: 70, hot: 90 },
    { key: "temperature", max: 90,  warn: 60, hot: 75 }
  ];

  async function refresh() {
    const response = await fetch("/api/stats");
    const data = await response.json();

    for (const card of CARDS) {
      const reading = data[card.key];
      const box = document.getElementById("card-" + card.key);
      const value = document.getElementById(card.key);
      const fill = document.getElementById("bar-" + card.key);

      if (reading === null || reading === undefined) {
        value.textContent = "--";
        fill.style.width = "0%";
        continue;
      }

      value.textContent = Math.round(reading);
      fill.style.width = Math.min(100, (reading / card.max) * 100) + "%";

      box.classList.remove("warn", "hot");
      if (reading >= card.hot) {
        box.classList.add("hot");
      } else if (reading >= card.warn) {
        box.classList.add("warn");
      }
    }
  }

  refresh();
  setInterval(refresh, 2000);
</script>

</body>
</html>

Save and exit (Ctrl+O, tap Enter, Ctrl+X), then run it:

source ~/YOURNAME/pihealth/bin/activate
cd ~/YOURNAME/pi-dashboard
python3 server.py
Checkpoint: a dark blue gradient, four frosted cards, big numbers, and a bar under each one that fills up and turns yellow then red as the number climbs. Hover a card and it lifts.
Same page as before? Hard refresh with Ctrl+Shift+R. Browsers keep old copies.

The three new things you just used

Gradient. Instead of one flat color, a smooth blend between two:

background: linear-gradient(135deg, #0f2027, #2c5364);

135deg is the angle. The two # codes are hex colors, six characters that mix red, green and blue.

Grid. Four cards that rearrange themselves depending on screen width:

grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));

This says "make as many columns as fit, each at least 220px wide." Drag your browser window narrower and watch four columns become two and then one. Nobody wrote a rule for phones. That one line did it. This is called responsive design.

Classes as switches. The color change is not JavaScript drawing anything. The CSS already knows what .hot looks like. JavaScript just adds and removes the label:

box.classList.add("hot");

That split matters. CSS handles how things look, JavaScript handles when, and keeping the two apart is what lets a big site stay workable once there are thousands of lines of it.

Step 3: Make it yours

Now the page is yours to change. A good way to work, so you never lose a dashboard that works:

Your plan for the period. Level 1 takes five minutes. Then pick one Level 2 theme pack. Then choose any two Level 3 upgrades. You do not need to do them all.

Use two Terminal windows

WindowIts job
Terminal 1Runs your server. Start it, then leave it alone. Do not close it and do not type in it.
Terminal 2Opens nano so you can edit the page. Click the black rectangle icon again to get a second window. It does not need your toolbox.

Start Terminal 1 now with python3 server.py if it is not already running.

The loop: change, save, refresh

  1. Terminal 2: open the page. The -l shows line numbers down the side.
  2. nano -l ~/YOURNAME/pi-dashboard/static/index.html
  3. Change one thing. Arrow keys move the cursor. One change at a time, so you know what did what.
  4. Save and exit: Ctrl+O, tap Enter, Ctrl+X
  5. Browser: press Ctrl+Shift+R to reload without the old copy.
You do not restart the server for page changes. Your server hands out the file fresh every time something asks. The only time you restart is when you change server.py: click Terminal 1, press Ctrl+C, run python3 server.py again.

Your undo button

Before each big change, save a copy that works. If it goes wrong, put the copy back. You cannot lose your dashboard this way.

cp ~/YOURNAME/pi-dashboard/static/index.html ~/YOURNAME/pi-dashboard/static/index-works.html
cp ~/YOURNAME/pi-dashboard/static/index-works.html ~/YOURNAME/pi-dashboard/static/index.html

The map of your page

Your page is one file with four zones. Every change in Step 3 happens in one of them, so this is the map. The line numbers are about right. They drift as you add things, which is why the next slide teaches you to search instead of counting.

About lineWhat is thereYou edit it for
12 to 25Zone 1: the theme block. The :root { ... } lines with all the colorsLevel 1, Level 2, and the --font line
26 to 82Zone 2: the rest of the CSS. How cards, bars and text lookMostly leave alone
83</style>, the end of the CSSNew Level 3 CSS goes on the line above this one
87 to 116Zone 3: the HTML. The title, then the four cardsClock, the big card, a fifth card
118 to 156Zone 4: the JavaScript. const CARDS first, then refresh()Warning levels, the last-updated stamp, the clock
Why new CSS goes at the bottom. When two rules style the same thing, the one written later wins. Put your new rules last and they beat the old ones. Nothing has to be deleted.

Find things fast in nano

Scrolling for a line is slow. nano can jump straight to it.

  1. Hold Ctrl and tap W. The bottom says Search:
  2. Type the word from the table, then tap the Enter key. The cursor jumps there.
  3. Wrong one? Ctrl+W then Enter jumps to the next match.
To get toSearch for
The colors and the corner roundness--bg-top or --round
The font--font
The end of the CSS</style>
The title at the top of the page<h1>
The start of the JavaScript<script>
The warning levelsCARDS
The end of the JavaScript</script>
To put a new line above one you found: tap the Home key (goes to the start of the line), paste, then tap Enter so the old line drops down. To put one below: tap End, tap Enter, paste. The cursor is where the text lands, so check where it is before you paste.

Level 1: The theme block

Where this goes: Zone 1, about lines 12 to 25. In nano press Ctrl+W, type --bg-top, tap Enter. You land on the first line of the theme block.

Look at the top of your CSS. Everything between the two long comment lines is the theme. Change a value there and it changes everywhere on the page at once, because every rule below reads from it instead of having its own copy of the color.

VariableWhat it controls
--bg-top and --bg-bottomThe two colors the background fades between
--textEvery piece of normal text
--mutedLabels and units. Dimmer than --text on purpose
--cardThe card's own fill. rgba because it is see-through
--card-edgeThe thin line around each card
--trackThe empty part of each bar
--ok, --warn, --hotGreen, yellow and red
--roundHow rounded the corners are. 0px is sharp, 28px is a pill
--fontThe typeface for the whole page

Try this first, so you can see how far one variable reaches:

--round: 0px;

Save, then refresh the browser. Every card went square. You changed one number.

Level 2: How to swap in a theme pack

Each pack replaces your whole :root block. Copy one in, refresh, look at it. Then take the one you like best and start changing its numbers.

How to swap one in. Press Ctrl+W, type :root, tap Enter. The cursor is now on the :root { line. Tap Ctrl+K 14 times, once per line, until the whole block is gone and the two comment lines are still above and below the gap. Then click Copy on a pack and press Ctrl+Shift+V.

Theme packs: Terminal and Midnight

Terminal. Sharp corners, monospace, the look of a machine that does not care about your feelings.

  :root {
    --bg-top:    #000000;
    --bg-bottom: #0a1a0a;
    --text:      #d1fae5;
    --muted:     rgba(209, 250, 229, 0.55);
    --card:      rgba(34, 197, 94, 0.07);
    --card-edge: rgba(34, 197, 94, 0.30);
    --track:     rgba(34, 197, 94, 0.15);
    --ok:        #22c55e;
    --warn:      #eab308;
    --hot:       #ef4444;
    --round:     4px;
    --font:      "Courier New", monospace;
  }

Midnight. Purple, soft, easy on the eyes in a dark room.

  :root {
    --bg-top:    #16162e;
    --bg-bottom: #4a1f6b;
    --text:      #f3e8ff;
    --muted:     rgba(243, 232, 255, 0.65);
    --card:      rgba(255, 255, 255, 0.10);
    --card-edge: rgba(216, 180, 254, 0.28);
    --track:     rgba(255, 255, 255, 0.14);
    --ok:        #86efac;
    --warn:      #fde047;
    --hot:       #fb7185;
    --round:     20px;
    --font:      system-ui, sans-serif;
  }

Theme packs, continued

Volcano. For a Pi that runs hot and wants you to know it.

  :root {
    --bg-top:    #1c0a08;
    --bg-bottom: #57180f;
    --text:      #fff7ed;
    --muted:     rgba(255, 247, 237, 0.62);
    --card:      rgba(255, 255, 255, 0.09);
    --card-edge: rgba(255, 170, 120, 0.25);
    --track:     rgba(255, 255, 255, 0.13);
    --ok:        #7ee787;
    --warn:      #fbbf24;
    --hot:       #ff5c4d;
    --round:     14px;
    --font:      system-ui, sans-serif;
  }

Sea Glass. Teal and calm. This one looks the most like something a company would ship.

  :root {
    --bg-top:    #042f2e;
    --bg-bottom: #115e59;
    --text:      #ecfeff;
    --muted:     rgba(236, 254, 255, 0.66);
    --card:      rgba(255, 255, 255, 0.11);
    --card-edge: rgba(153, 246, 228, 0.26);
    --track:     rgba(255, 255, 255, 0.14);
    --ok:        #5eead4;
    --warn:      #fcd34d;
    --hot:       #fb7185;
    --round:     18px;
    --font:      system-ui, sans-serif;
  }

Theme packs, last two

Slate. Grey, quiet, professional. The boring one, and boring is sometimes correct.

  :root {
    --bg-top:    #1e293b;
    --bg-bottom: #334155;
    --text:      #f1f5f9;
    --muted:     rgba(241, 245, 249, 0.60);
    --card:      rgba(255, 255, 255, 0.07);
    --card-edge: rgba(255, 255, 255, 0.14);
    --track:     rgba(255, 255, 255, 0.12);
    --ok:        #4ade80;
    --warn:      #facc15;
    --hot:       #f87171;
    --round:     10px;
    --font:      system-ui, sans-serif;
  }

Paper. A light theme. Look closely at this one: the text went dark, the cards went white, and the transparent values all flipped. Nothing outside the theme block changed.

  :root {
    --bg-top:    #f8fafc;
    --bg-bottom: #dbe3ec;
    --text:      #0f172a;
    --muted:     rgba(15, 23, 42, 0.60);
    --card:      rgba(255, 255, 255, 0.75);
    --card-edge: rgba(15, 23, 42, 0.12);
    --track:     rgba(15, 23, 42, 0.10);
    --ok:        #16a34a;
    --warn:      #ca8a04;
    --hot:       #dc2626;
    --round:     14px;
    --font:      system-ui, sans-serif;
  }

Why nearly every one of these is dark

Your page has to show green, yellow and red and have all three read clearly. Those three colors have nowhere to go on a bright orange or hot pink background, they just turn to mud. Real dashboards, the ones air traffic controllers and network engineers stare at all day, are almost always dark for this reason.

If you build a bright theme anyway, check that a red card still shouts at you from across the room. If it does not, the theme is pretty and useless.

Build your own palette at coolors.co. Pick two close colors for the background and keep your green, yellow and red bright.

Level 3: A real typeface

Where this goes, two edits: (1) the font link goes in the head, on the line above <style>. Search <style>, tap Home, paste, tap Enter. (2) The --font line is already in your theme block. Search --font and change it.

Free fonts from Google Fonts. Pick one, then add one line in your <head>, above <style>:

<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&display=swap">

And point the theme at it:

--font: "Space Grotesk", system-ui, sans-serif;

Swap the font name for any from the site, with + instead of spaces. Keep system-ui, sans-serif on the end. It is the backup, covering the two seconds before the font arrives, or forever if the Wi-Fi is down.

Level 3: A background that moves

Where this goes: CSS, at the very bottom. Search </style>, tap Home, paste, tap Enter.

Now a background that moves:

@keyframes drift {
  0%   { background-position: 0% 50%; }
  50%  { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}

body {
  background: linear-gradient(135deg, var(--bg-top), var(--bg-bottom), var(--bg-top));
  background-size: 400% 400%;
  animation: drift 18s ease infinite;
}

The gradient is stretched to four times the screen and then slid slowly back and forth. Change 18s to 4s to see it clearly, then put it back to something calm. A background that races is a background nobody can read in front of.

Level 3: Glowing numbers

Where this goes: CSS, at the very bottom, above </style>. Search </style>, tap Home, paste, tap Enter. Your page already has a .value rule. Leave it alone. A second one at the bottom adds to it.

Make the numbers glow:

.value {
  text-shadow: 0 0 18px currentColor;
}

currentColor means "whatever color this text already is." So the green numbers glow green, and the moment a card goes red the glow goes red too. One line, and it follows your thresholds for free.

Level 3: A pulsing alarm

Where this goes: CSS again, at the very bottom, above </style>.

Make a hot card pulse:

@keyframes alarm {
  0%, 100% { box-shadow: 0 0 0 0 rgba(248, 113, 113, 0.55); }
  50%      { box-shadow: 0 0 0 14px rgba(248, 113, 113, 0); }
}

.card.hot {
  animation: alarm 1.4s ease-out infinite;
}

A ring pushes outward from the card and fades. It only runs on cards your JavaScript has marked hot, so most of the time you will never see it. Good alarms are quiet. Open six browser tabs to drive the CPU up and watch it fire.

Level 3: A clock

Two edits, two zones. One line of HTML under the title, and one function in the JavaScript.

Edit 1, HTML. Search <h1>. Tap End, tap Enter, paste:

<p class="sub">Live from my Pi &middot; <span id="clock">--:--:--</span></p>

Edit 2, JavaScript. Search </script>. Tap Home, paste, tap Enter:

function tick() {
  document.getElementById("clock").textContent =
    new Date().toLocaleTimeString();
}
tick();
setInterval(tick, 1000);

This clock is the browser's time, not the Pi's. It never asks the server anything.

Level 3: When the data last arrived

Two edits. This slide is edit 1, a line of HTML under the cards. Edit 2 is on the next slide.

Edit 1, HTML. Search <script>. Tap Home, paste, tap Enter. It lands just under the cards:

<p class="sub" id="stamp">Waiting for the first reading.</p>

Level 3: The stamp, edit 2

Where this goes: inside refresh(), in the JavaScript.

Edit 2, JavaScript. This one goes inside refresh(), after the for loop closes. Here is the end of refresh() so you can find the spot:

      } else if (reading >= card.warn) {
        box.classList.add("warn");
      }
    }                          <- this closes the for loop
    <- paste the two stamp lines here
  }                            <- this closes refresh()

Click at the end of the } that closes the for loop, tap Enter, paste:

document.getElementById("stamp").textContent =
  "Last updated " + new Date().toLocaleTimeString();

Now you can tell "everything is fine" from "the server died three minutes ago." Every monitoring tool has this.

Level 3: One big card

Where this goes: CSS, at the very bottom. Search </style>, tap Home, paste, tap Enter. Your .grid rule is already in the page, so you only add these two.

Give the page a main character instead of four things shouting equally:

#card-cpu {
  grid-column: span 2;
}

#card-cpu .value {
  font-size: 72px;
}

CPU now takes two columns and a much bigger number. Want a different card to be the big one? Change cpu to memory, disk or temperature in both places.

Level 3: A photo behind everything

Two parts: get a photo into your static folder, then add CSS. Part 2 is on the next slide.

Part 1, get the photo. Find one at unsplash.com. In Chromium, right-click the photo and choose Save image as. Browse to Home, then your folder, then pi-dashboard, then static. Name it exactly pi-background.jpg and save. Check it landed:

ls ~/YOURNAME/pi-dashboard/static

Level 3: The photo, part 2

Where this goes: CSS, at the very bottom, above </style>.

Part 2, the CSS. Search </style>, tap Home, paste, tap Enter:

body {
  background-image:
    linear-gradient(135deg, rgba(15, 32, 39, 0.85), rgba(44, 83, 100, 0.85)),
    url("/pi-background.jpg");
  background-size: cover;
  background-position: center;
  background-attachment: fixed;
}

Two backgrounds stacked: your gradient on top, at 85% opacity, and the photo under it. Without that gradient layer the photo eats your text.

Filename rule. /pi-background.jpg has to match the real file exactly, lowercase and all. Your server hands out anything in static, so the path starts with a single /.

Level 3: Your own warning levels

Where this goes: Zone 4, the JavaScript. Press Ctrl+W, type CARDS, tap Enter.

In the CARDS list at the top of your JavaScript:

{ key: "cpu", max: 100, warn: 50, hot: 80 },

Set warn to 5, save, and refresh. The card sits in yellow permanently, forever, no matter what the Pi is doing.

An alarm that is always on is worse than no alarm, because people stop looking at it. Real engineers argue about these two numbers for hours. Pick yours on purpose and be ready to say why in your reflection.

Level 3: Everything else

Before you screenshot it. Step back from the monitor. Can you tell in one second which number is the worst one? If every card looks equally loud, that is a design problem, not a color problem. Make one thing bigger, or make the calm cards quieter.

Base and Stretch

Base · everyone

A working server on your Pi, serving your own dashboard, loading on a computer that is not the Pi, with at least two visible changes that are yours. You can explain what /api/stats sends back and who asks for it.

Stretch · required for high school

Add a fifth stat, wired all the way through. Not a new color. A new number that does not exist yet, travelling from the Pi's hardware to the screen. Three edits, one in each layer: Python, HTML, JavaScript.

Uptime is the worked example. Open the psutil documentation and pick something else if you want: how many processes are running, how many bytes have crossed the network, how many CPU cores are working. The three edits are the same for any stat.

Stretch edit 1: Python

Where this goes: server.py, not the page. Open it in Terminal 2 with nano -l ~/YOURNAME/pi-dashboard/server.py, or in Thonny (below).
Thonny works well for this edit. Open it, click File, Open, and pick server.py in your pi-dashboard folder. Search with Ctrl+F instead of Ctrl+W, and paste with Ctrl+V. A missing comma shows up as a red SyntaxError with a line number when you click Run. Not set up yet? The Interpreter step is on Day 1.

1. At the top, add one line under import os. Search import os, tap End, tap Enter, paste:

import time

Stretch edit 1, continued: the jsonify block

Where this goes: server.py, still open in Terminal 2.

2. In the jsonify block, the old last line needs a comma added, and your new line goes under it with no comma:

        "disk": psutil.disk_usage("/").percent,
        "temperature": temperature_c(),
        "uptime": round((time.time() - psutil.boot_time()) / 60)
    })
"uptime": round((time.time() - psutil.boot_time()) / 60)

3. server.py changed, so this one does need a restart. In Terminal 1 press Ctrl+C, then run python3 server.py. In Thonny, click the red Stop button, then the green Run button. Reload /api/stats on the other computer.

If your new name is not in that JSON, stop and fix it before touching the HTML. A SyntaxError in Terminal 1 almost always means a missing comma after temperature_c().

Stretch edit 2: HTML

2. HTML. Add a fifth card. Search bar-temperature, tap the Down arrow once (that is the line closing the temperature card), tap End, tap Enter, paste:

  <div class="card" id="card-uptime">
    <h2>⏱️ Uptime</h2>
    <div><span class="value" id="uptime">--</span><span class="unit">min</span></div>
    <div class="bar"><div class="fill" id="bar-uptime"></div></div>
  </div>

Making a different stat? Change uptime to your stat's name in all three ids, and change the min unit.

Stretch edit 3: JavaScript

Where this goes: the CARDS list in your page, in the JavaScript. Search CARDS.

3. JavaScript. Search CARDS. The old last line needs a comma, and the new line goes under it with no comma:

    { key: "disk",        max: 100, warn: 70, hot: 90 },
    { key: "temperature", max: 90,  warn: 60, hot: 75 },
    { key: "uptime", max: 1440, warn: 720, hot: 1200 }
  ];

max is where the bar is full. Uptime is in minutes, so 1440 is a full day. Your new key has to match the name you used in server.py exactly.

Checkpoint: a fifth card on the other computer's screen, with a real number in it. Save the file, refresh, and it appears.

What you turn in

The reflection has to answer: what does /api/stats send back and what asks for it, how often does the page ask and which line decides that, and what did you change and what was the hardest part.

High school, and middle school going for bonus: name the stat you added and say which two files you had to edit to make it appear.

Everything is submitted in Schoology, along with a separate exit ticket.

Taking the two screenshots

Both screenshots are taken on the other computer, not on the Pi, because the address bar has to show your Pi's IP.

File nameAddress bar showsPage showsTake it
api-stats.pngYOUR-PI-IP:5000/api/statsOne line of dataDay 2, as soon as it works
dashboard.pngYOUR-PI-IP:5000Your finished dashboardEnd of Day 3, after your changes

Capture the whole screen, not a piece of it. A whole-screen capture always has the address bar in it. Cropping is how screenshots end up proving nothing.

ComputerWhole-screen shortcutIt saves to
WindowsWindows + Print ScreenPictures, then Screenshots
MacCmd + Shift + 3The Desktop
ChromebookCtrl + the Show windows key (a rectangle with two lines, top row)Downloads
Raspberry PiPrint Screen. If nothing happens: Menu → Accessories → ScreenshotYour home folder, or a small capture window opens
Rename it to dashboard.png or api-stats.png, then open it and check you can read the address bar.

Getting your files to Schoology: pick a route

Your two files are where you made them, on the Pi:

~/YOURNAME/pi-dashboard/server.py
~/YOURNAME/pi-dashboard/static/index.html

Your screenshots are on the other computer. The files have to get from the Pi to Schoology somehow. There are three ways. Try Route 1 first.

RouteWhat happensLogins
1. Schoology on the PiLog in to Schoology on the Pi and upload the two files from there.Two: the Pi, then the other computer for the screenshots
2. Email yourselfEmail the two files to yourself from the Pi, then open that email on another computer.Email on the Pi, then Schoology on the other computer
3. Download from the PiYour Pi shares your folder for a minute. You save the files on the other computer.One: Schoology on the other computer
Upload, never Create. In Schoology, choose Submit Assignment, then Upload. Create only takes typed text.

Last resort: copy the pi-dashboard folder to a jump drive and submit from a lab computer.

Route 1: Schoology on the Pi

  1. Open Chromium on the Pi and press Ctrl+Shift+N. That opens a Private window (Chromium calls it Incognito). It forgets your login when you close it.
  2. Log in to Schoology. If Chromium offers to save the password, click Never. Do not tick any box that says remember me.
  3. Open the assignment, click Submit Assignment, then Upload.
  4. Pick server.py from Home → YOURNAME → pi-dashboard. Upload index.html from the static folder inside it.
  5. Check both names are in the list, then submit.
Before you walk away: click your name at the top right of Schoology and choose Log out. Then close the Private window with the X. The next student uses this Pi.

Then log in on the other computer and upload the two screenshots there.

Route 2: Email yourself

  1. On the Pi, press Ctrl+Shift+N in Chromium for a Private window.
  2. Log in to your email. If Chromium offers to save the password, click Never.
  3. Write a new message to yourself. Click the paperclip and attach server.py from pi-dashboard, then attach index.html from the static folder inside it.
  4. Send it. Wait until your email says the message was sent.
  5. Log out of your email, then close the Private window. Do this before you do anything else.
  6. On the other computer, open the email, download both files, and upload them to Schoology along with your screenshots.
Never leave your email open on a shared Pi. Anyone who sits down next could read it. If a file is refused, use Route 1 or Route 3.

Route 3: Download from the Pi

Your Pi can hand out your files the same way it hands out your dashboard. Nothing to log in to on the Pi.

On the Pi, in the Terminal:

cd ~/YOURNAME/pi-dashboard && python3 -m http.server 8001

It prints a line like this, then sits there listening:

Serving HTTP on 0.0.0.0 port 8001 (http://0.0.0.0:8001/) ...

On the other computer, go to this address with your Pi's IP:

http://YOUR-PI-IP:8001
  1. You see a list with server.py and static/.
  2. Right-click server.py and choose Save link as.
  3. Click static/, then right-click index.html and choose Save link as.
Now stop it. Click the Terminal on the Pi and press Ctrl+C. While it runs, anyone on the school network can read that folder. We use port 8001 because your dashboard owns 5000.

Upload the files and your screenshots to Schoology from the other computer. One login covers everything.

Words worth knowing

TermWhat it means
TerminalA window where you type commands instead of clicking. Same computer, different door
PromptThe text at the start of a Terminal line, waiting for you. Yours should start with (YOURNAME-pihealth)
nanoA text editor that runs inside the Terminal. Save with Ctrl+O, exit with Ctrl+X
ServerA computer whose job is to wait for requests and send back responses
RequestA device asking a server for something
ResponseWhat the server sends back
FlaskThe Python tool that turns a Python file into a web server
RouteA line like @app.route("/api/stats") saying which request a function answers
APIA door on a server that hands back data instead of a web page
JSONA text format for data that programs can read, like {"cpu": 45}
IP AddressA device's house number on a network, like 10.0.4.71
PortThe door number on that house. Ours is 5000
localhostA name that always means "this computer, right here"
SSHSecure Shell. Typing commands on one computer that run on another
HTMLThe structure of a page. What is on it
CSSThe style of a page. What it looks like
JavaScriptThe behavior of a page. What it does and when
DOMThe browser's live copy of your HTML, which JavaScript can change
Virtual EnvironmentA clean workspace holding one project's Python tools
psutilThe Python library that reads the computer's own CPU, memory and temperature
Full-StackWorking on both the server and the page. You did both

When it goes wrong

ModuleNotFoundError: No module named 'flask'
Look at your prompt. Does it start with (YOURNAME-pihealth)? If not, run source ~/YOURNAME/pihealth/bin/activate. You need this in every new Terminal window.
Address already in use
Your server is already running in another Terminal window. Find it and press Ctrl+C. If you cannot find it, run pkill -f YOURNAME/pi-dashboard/server.py and start again. Never run plain pkill -f server.py. It stops every student’s server on this Pi, not just yours.
The page says Not Found.
Run ls ~/YOURNAME/pi-dashboard/static. If index.html is not in that list, you saved it in the wrong folder.
The other computer cannot load my dashboard.
Three things, in order. Is that computer on the school network? Is the server still running in the Pi's Terminal? Did you type :5000 after the IP address? Check hostname -I on the Pi too. The IP can change overnight.
It loads someone else's dashboard.
You typed their IP. Run hostname -I on your Pi again and fix your sticky note.
Temperature shows two dashes.
Some Pi models do not report it. Everything else works. Leave it.
I changed the page and nothing happened.
Did you save? nano shows [ Wrote ... lines ] when it saves. Then hard refresh with Ctrl+Shift+R. If you changed server.py, that one needs a restart: Ctrl+C in the server's Terminal, then python3 server.py.
Nothing appears when I type my password.
That is deliberate. The terminal hides password characters so nobody can read them over your shoulder. Type it and tap the Enter key.
I pasted into nano and it came out mangled.
Paste in the Terminal is Ctrl+Shift+V. If the file is a mess, delete it and start the file over. It is faster than repairing it.

When it goes wrong, continued

My prompt does not start with (YOURNAME-pihealth).
You are outside your toolbox. Run source ~/YOURNAME/pihealth/bin/activate. A new Terminal window always starts outside it.
No such file or directory.
Either a spelling slip in the folder name, or you are at a different Pi than yesterday. Run ls ~ and look for your folder name. Not there? Find your Pi. Do not rebuild.
nano says File Name to Write and I do not know what to do.
Tap the Enter key. That is it. The name is already filled in, and Enter says yes to it.
I pressed Ctrl+X and nano asks Save modified buffer?
That means you had unsaved changes. Tap the Y key to save them, then the Enter key. Tap N if you want to throw them away.
Ctrl+C did not copy my text.
In the Terminal, Ctrl+C stops a running program. It does not copy. Use the Copy button above the code box, and paste with Ctrl+Shift+V.
The Terminal looks frozen after python3 server.py.
It is not frozen, it is listening. That window is now your server. Open a second Terminal window for anything else.
My dashboard stopped updating.
The server stopped. Look at the Terminal where you started it. If you see your prompt again, run python3 server.py again.
Thonny says No module named 'flask'.
Thonny is using its own Python, not your toolbox. Click Tools, Options, Interpreter, choose Alternative Python 3 interpreter or virtual environment, and enter /home/USERNAME/YOURNAME/pihealth/bin/python3 with your own username and folder name. Click OK.
Thonny says Address already in use.
Another copy of your server is running, either in a Terminal window or in another Thonny window. Stop that one with Ctrl+C or the red Stop button, then click Run again.
I cannot see the Shell in Thonny.
Click View in the menu bar and tick Shell. The Shell is where Thonny prints Running on and your errors.
My email will not take a file, or says it was blocked.
Some email systems refuse certain file types. Use Route 1 or Route 3 in the Turn in tab instead.
I broke it so badly I want to start over.
rm ~/YOURNAME/pi-dashboard/server.py and redo Step 6. This is a normal thing that normal programmers do.

Appendix

Setting up a Pi from a blank card

Skip this unless Mr. Krigger tells you to do it. The Pis in the lab are already set up, and Day 1 assumes yours boots to a desktop. This appendix is here for the times it does not: a brand new Pi, a card that got wiped, or a class doing the whole build from scratch.

There are two walkthroughs below because the lab runs two versions of Raspberry Pi OS. Find out which one you need before you start. If the Pi still boots, open a Terminal and run:

cat /etc/os-release

Look at the VERSION_CODENAME line. It says either bookworm or trixie. If the card is blank and there is nothing to boot, use the Bookworm walkthrough unless you were told otherwise.

Version A: Bookworm

This is the version most of the lab is running.

A1. Get the Imager. On any Mac or Windows computer, download and install Raspberry Pi Imager. Put the microSD card into that computer.

A2. Choose the three things. Imager asks three questions. Choose Device, pick the Pi model you are holding, printed on the board. Choose OS, Bookworm is no longer the default so it is a few clicks in: Raspberry Pi OS (other), then the 64-bit Bookworm entry. Choose Storage, pick your SD card.

Look hard at the storage list. Imager erases whatever you choose, completely, with no undo. If you see an external drive in that list, make very sure you are not about to pick it.

A3: Fill in OS customisation

Imager asks whether you want to customise before writing. Say yes. This is the whole reason setup is easy now.

FieldWhat to put
HostnameSomething you will recognize, like krigger-pi
UsernameYour choice. Write it down. There is no default anymore
PasswordYour choice. Write it down
Wireless LANThe network name and password Mr. Krigger gives you
Wireless LAN countryUS
Locale and time zoneWhatever the school is on

Then open the Services tab and turn on Enable SSH, with password authentication.

This screen replaces three old setup steps at once. It creates your account, joins the Wi-Fi, and turns on remote access, all before the card has ever been in a Pi. Older instructions on the internet tell you to edit a file called wpa_supplicant.conf to join Wi-Fi. That has not worked since 2023.
Your password does not go in any file, ever. You typed it here and you wrote it on paper. That is where it lives.

A4 to A7: Write, boot, update, confirm

A4. Click Write and wait. This takes several minutes, then verifies what it wrote, which takes several more. Leave it alone.

A5. Put the card in the Pi, connect the monitor, keyboard and mouse, then plug in the power last. The first boot takes longer than normal and the Pi may restart itself once. Let it.

Checkpoint: a desktop, and the Wi-Fi icon in the top right shows it is connected.

A6 and A7: Update it, then confirm

A6. Open the Terminal and run:

sudo apt update && sudo apt full-upgrade -y
sudo reboot

The first command can take fifteen minutes on a fresh card, longer if a whole class is doing it at once. sudo means run as administrator, apt update checks what new versions exist, full-upgrade -y installs them without asking you to confirm each one, and reboot restarts so the updates take effect.

A7. Confirm it is ready for Day 1:

whoami
hostname -I

The first prints the username you chose in Imager. The second prints an IP address. If either comes back empty or wrong, fix it now rather than in the middle of Day 1.

Version B: Trixie

Trixie is the newer Raspberry Pi OS, built on Debian 13. Some of the lab machines run it.

B1. Everything in A1 through A4 is the same. Same Imager, same three questions, same customisation screen, same Write button. One difference: at Choose OS, Trixie is the default now, so it is the plain Raspberry Pi OS (64-bit) entry at the top. You do not need to go into "other".

B2. First boot is the same as A5. The desktop looks different from Bookworm, redrawn, so the icons and wallpaper will not match what the student next to you is looking at. Nothing about this project changes because of it.

B3. Update, identical to A6:

sudo apt update && sudo apt full-upgrade -y
sudo reboot

B4. Confirm, identical to A7. Run whoami and hostname -I and write both down.

Where things are on each desktop

WhatBookwormTrixie
TerminalThe black rectangle icon on the top bar
Thonny (Python editor)Menu → Programming → Thonny
SSH on or offMenu → Preferences → Raspberry Pi Configuration → Interfaces
ScreenshotPrint ScreenPrint Screen, or Menu → Accessories → Screenshot
If a menu is not where this table says it is, the desktop got rearranged in an update. Say so and Mr. Krigger will fix this table. Everything you do at the Terminal works identically on both versions, and that is a large part of why this project lives at the Terminal.

What is actually different between the two

Short answer, for the curious: almost nothing you will touch.

BookwormTrixie
Debian version underneath1213
Which one Imager offers firstUnder "other"The default
How the desktop looksOlder lookRedrawn
Terminal, Python, Flask, SSHIdentical
Wi-Fi setupNetworkManager
Default pi / raspberry loginNone, on either

Every command in Days 1 through 3 runs the same on both, and that was deliberate. Commands stay stable for decades while menus get redesigned every couple of years, so the commands are the part worth learning.