Flask & the project: the system gets a face
420-302-VA · WEEK 11 · FALL 2026

Utility · keep open during the lab

Troubleshoot

The dashboard stacks four layers, server, network, bridge, browser, and each fails with its own accent. The ladder's order matters more than ever this week because a browser cheerfully displays a stale page over a dead server, so prove the bottom before believing the top.

Debug in layers

Four debugging layers in order: A Serve, does Flask run and answer on the Pi itself; B Reach, does another device get the page, bind address and firewall; C Data, does the bridge dict hold live values from MQTT; D Browser, does the page's JavaScript fetch and render, per the console. A · Serve Flask up, answers curl localhost:5000 B · Reach bind + UFW walls the phone test C · Data the bridge holds truth /api/light vs firehose D · Browser fetch, render, cache F12: the console A browser shows the last thing it managed, not the current truth: prove A to C before blaming D, and D before blaming your eyes.
The one-command test per layer: A, curl localhost:5000/api/light in a Pi terminal; B, the same URL from a phone's browser, reading refused vs timeout the Week 9 way; C, compare /api/light's JSON against the mosquitto_sub firehose; D, open the browser console (F12) and read the red lines.

Symptoms and fixes

LayerSymptomLikely causeFix
AAddress already in use at startupAn older app.py still holds port 5000, often in a forgotten SSH terminalFind and Ctrl-C it; as a last resort run on another port and allow it in UFW
ABrowser shows 500; page says Internal Server ErrorYour route's Python raisedThe full traceback is in the Flask terminal: read it bottom-up, Week 4's rule unchanged
ATemplateNotFound: dashboard.htmlThe file is beside app.py instead of inside a folder named exactly templates/Make the folder, move the file; restart and re-test
A404 on a route you wrotePath mismatch: a typo, case, or the stray trailing slash; or the method (POST route hit by GET)Compare the request line in Flask's log against the decorator, character by character
BWorks on the Pi; other devices get connection refused, instantlyThe bind wall: no host="0.0.0.0", so Flask listens on loopback only, Mosquitto's Week 9 wall in PythonAdd the host argument, restart; re-run the phone test
BOther devices hang, then time outDropped silently: UFW has no 5000 rule, or the device is on a different network (phone on mobile data is the classic)sudo ufw allow 5000; confirm the phone is on the bench Wi-Fi, not LTE
C/api/light answers but pct stays null while the firehose shows the node publishingThe bridge never hears: loop_start() missing, or the subscribed topic does not exactly match (STATION constant, s7 vs s07, case)Confirm the loop_start line; print inside on_message; compare topics against the firehose string
CValues freeze; updated ages past a few secondsThe node stopped, the broker restarted and took the thread's session, or the MQTT thread diedFirehose first (is data flowing at all?), then restart app.py; subscribe-inside-on_connect already guards the reconnect case
CNode's connection drops every few seconds; two startup banners in the Flask terminaldebug=True: the auto-reloader imports the module twice, two MQTT clients share one id and kick each other, Week 9's duplicate-id ruleapp.run(..., debug=True, use_reloader=False), or leave debug off
CStatus chip says online while the node sits unpluggedStale retained status, or the chip never falls back; the will may not have fired yet (keep-alive window)Wait out 1.5 × keep-alive; clear ghost retained per Week 9's command; have the page also gray the chip when updated ages
DPage loads; number never changes; no errors anywhere server-sideThe JavaScript never runs its loop: setInterval missing, a typo before it threw, or the browser cached an old pageOpen the console (F12): red lines name the script error; hard-refresh (Ctrl-Shift-R) to defeat the cache
DConsole: Uncaught TypeError: ... is null on the update linegetElementById found nothing: the id in the HTML and the one in the script disagreeMatch them exactly; ids are case-sensitive strings, Week 9's topic-typo lesson in miniature
DSet button does nothing; network tab shows the POST returning 400The form value did not parse: empty input, comma decimal, or the body was not form-encodedSend new URLSearchParams({sp}) as the body; validate the input; watch Flask's log line for the request

Before you ask for help

  1. Name your layer, and show its one-command test failing (the curl, the phone screen, the JSON-vs-firehose pair, the console).
  2. Bring both logs: the Flask terminal (every request is a line; 500s carry tracebacks) and, for layer D, the browser console.
  3. Say the exact URL you tested, scheme, host, port, path; half of this week's mysteries are a wrong quarter of a URL.
  4. State what you changed last and whether the server was restarted since; edited Python serves nothing until it is.