Command Palette

Search for a command to run...

Hectal
PHASE 18Advanced ~34 min· topic 5 of 5

Topic 18.5

Capstone: Your Lumen Dashboard, Finished

In one line

Put every part of the Lumen dashboard together, check it against a checklist from Phase 0 to Phase 17, point it at your own cloud bucket by changing one line, build it for production and publish the result. Then see where to go next.

Before you start

You will learn

  • Run the tests and a production build of the whole project, and read what the build prints
  • Move the data to your own Google Cloud Storage or Amazon S3 bucket by changing one line in src/utils/config.js
  • Publish the built dist/ folder to any static host and check the live site against a checklist

You write code in

In your Vite project lumen-dashboard/ (run npm run dev), and in the editor on this page

Files you touch

  • src/utils/config.js

Think of it like this

Opening a shop. The shelves are stocked and the lights work, but you still check the till, the sign on the door, the delivery route and the key. A project is not finished when the code works on your computer. It is finished when a stranger can open it, the data arrives, and you know how to fix it on a bad day.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

production build
The optimised copy of your project that you publish: joined, minified (shortened) files with hashed names.
dist/
The folder vite build writes. It holds everything a static host needs.
static host
A service that only serves files (HTML, CSS, JavaScript, data) and runs no code of yours.
bucket
A named container for files in a cloud storage service (Google Cloud Storage or Amazon S3).
public read
A setting that lets anyone on the internet download an object, without logging in.
CORS
Cross-Origin Resource Sharing: headers from a server that say which other sites' pages may read its files.
cache-control
A header that tells browsers and CDNs how long they may keep a file before asking again.
smoke test
A quick check that the main things work at all: the data loads, the charts draw, the numbers match.

Step by step

011. Walk the checklist, phase by phase

Open the dashboard and tick each line. If one fails, the topic named in brackets shows you the fix.

Phase 0 to 3: the page has the Lumen header, the week bars and the stations-running-low bars, drawn as SVG that scales with the screen (1.4), and you can say why a bar starts at zero (2.5). Phase 4: data loads from DATA_URL, a loading message shows, a failed request shows an error box with Try again, and the dirty rows are cleaned and counted (4.4). Phase 5 and 6: bars update with keys, scales and axes are readable, and the y axis starts at zero for bars. Phase 7: no pie of 25 slices; stacked bars and the heatmap have legends.

Phase 8 and 9: the tooltip works with a mouse, a finger and a keyboard, brushing filters the other charts, and motion stops when prefers-reduced-motion is on. Phase 10: dark mode is readable, colours survive colour-blind simulation, and the charts redraw when the wall screen is resized. Phase 11 and 12: the trips canvas is sharp on a high-density screen and stays fast, and clicking a station works. Phase 13: the map has the six districts and the fleet treemap labels fit.

Phase 14: the React app builds with the same data layer. Phase 15: every chart has a name, a description made from the data and a table twin, and Meera can reach the line with arrow keys. Phase 16: every state is handled, a failed chart does not blank the others, the live feed ignores late data, exports work and npm test passes. Phase 17: the story page reads with no JavaScript and offers a list mode.

022. Install, test and build the whole project

These are the real results for the project assembled from the build files of Phases 4 to 17, in a fresh folder. Your version numbers can differ, since the newest allowed version is installed. The build time is not fixed: yours will say another number.

terminal
$ npm install
npm test
npm run build
── expected output ──
added 129 packages in 13s
 
RUN v5.0.5 lumen-dashboard
 
Test Files 1 passed (1)
Tests 15 passed (15)
 
> lumen-dashboard@0.0.0 build
> vite build
 
vite v8.3.4 building client environment for production...
transforming...
✓ 990 modules transformed.
rendering chunks...
computing gzip size...
dist/story.html 4.21 kB │ gzip: 1.84 kB
dist/index.html 6.57 kB │ gzip: 2.35 kB
dist/assets/dashboard-BUajVF3k.css 6.88 kB │ gzip: 1.96 kB
dist/assets/story-CYfl-KZ5.js 5.83 kB │ gzip: 2.72 kB
dist/assets/load-Uw19VY66.js 87.15 kB │ gzip: 30.12 kB
dist/assets/dashboard-RDKgIuAW.js 625.19 kB │ gzip: 192.08 kB
 
✓ built in 701ms
(!) Some chunks are larger than 500 kB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rolldownOptions.output.codeSplitting to improve chunking: https://rolldown.rs/reference/OutputOptions.codeSplitting
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
Captured on 16 October 2026 with Node 24.11.0 and Vite 8.3.4 (shortened: some blank lines and the colour codes are left out). The dashboard page and the story page are two separate entries, because `vite.config.js` lists both (Topic 17.6). The 25-station data in `public/data/lumen/` was copied into `dist/data/lumen/`: 678 KB, most of it `trips-10k.csv`.

033. Read the build, do not skip it

Three lines matter. The page files are tiny (4 to 7 kB). The main script is 625.19 kB, 192.08 kB gzipped: that is what a phone downloads before the dashboard appears, so it belongs in your budget (Topic 16.5). And the warning is not an error: the build succeeded.

To see the result locally, run npm run preview. It serves dist/ the way a static host would, which catches problems (a wrong base path, a missing file) that the dev server hides.

044. Point the dashboard at your own bucket: one line

Open src/utils/config.js. Keep the local line for development and enable the line for the cloud you use. Replace YOUR-BUCKET (and YOUR-REGION for S3) with your real names. These are placeholders, and no real bucket is used in this course.

src/utils/config.jswhole filejs
// Where the data lives. To read it from cloud storage, change this one line.
export const DATA_URL = "/data/lumen/";

// Google Cloud Storage: replace YOUR-BUCKET with your bucket's name (PLACEHOLDER)
// export const DATA_URL = "https://storage.googleapis.com/YOUR-BUCKET/lumen/";

// Amazon S3: replace YOUR-BUCKET and YOUR-REGION (PLACEHOLDERS)
// export const DATA_URL = "https://YOUR-BUCKET.s3.YOUR-REGION.amazonaws.com/lumen/";

055. Make the bucket public to read, with CORS and a cache time

On Google Cloud Storage: create the bucket, give the special member allUsers the role Storage Object Viewer (read-only), set a CORS file that allows GET before you upload, and then upload the data under a lumen/ folder with a Cache-Control header. The order matters: Google Cloud Storage may keep serving a file that was fetched before the CORS rule existed without the CORS header until its cached copy expires (up to an hour), so test with a file nobody has fetched yet (Topic 4.6). The CORS file below allows any origin because the data is public; to allow only your own site, list its address (and http://localhost:5173 for development) instead of *. Some organisations block public access by policy: then ask the owner of the organisation, and do not work around it.

On Amazon S3: turn off "Block all public access" for this bucket only (the delete-public-access-block command below does it), add a bucket policy that lets anyone s3:GetObject on lumen/*, add a CORS rule that allows GET, and upload with a Cache-Control header. Use the console if you prefer clicking; the commands below do the same. The flags were written from the cloud providers' documentation and have not been run against a real bucket in this course: if a command differs in your version, trust the current documentation.

cors.json (Google Cloud Storage)whole filejson
[
  {
    "origin": ["*"],
    "method": ["GET"],
    "responseHeader": ["Content-Type"],
    "maxAgeSeconds": 3600
  }
]

066. Publish dist/ and test the live site

Any static host works: a storage bucket set up as a website, GitHub Pages, Netlify, Cloudflare Pages or Vercel's static hosting. Upload the **contents of dist/** and open the address. If the data comes from your bucket, you can delete dist/data/ (678 KB) before uploading.

Then test like a visitor. Open the browser's Network panel and reload: the data requests go to your bucket and return 200. Reload once with the network set to Offline to see the offline state. Open the page on a phone. Run the smoke test in the first example below against your bucket. If a request fails with CORS wording, fix the bucket, not the code (Topic 4.6).

077. Keep it alive

Write down three things in the project's README: how to change the data (upload the new files to the bucket, and bump the file name or wait for the cache time), how to run the tests, and how to publish. When the person Arjun hires opens the project, these three lines save a week.

With Cache-Control: public, max-age=3600, a changed file can take up to an hour to appear for visitors who already have the old one. For data that must update at once, use a shorter time or put a version in the file name.

Try it yourself

  1. 1

    Audit a number that is wrong on purpose

    In the second example, change 933651 in the first check to 933650. The Console prints FAIL total rides is 933,651 for that line, and the page says 6 of 7 checks passed. An audit that can fail is the point: a check that never fails proves nothing.

  2. 2

    Test a path that is wrong

    In the first example, change "/data/lumen/" in BASES to "/data/lumen-old/". Every file on the local base now answers HTTP 404. A 404 means the host answered and the path is wrong, which is different from Failed to fetch.

Code & diagrams

Point the data at a bucket, and smoke-test every base New tab

The preview can only reach this site's own data, so the two cloud bases fail here on purpose (the buckets are placeholders and do not exist). Use the same loop with your real base: each line should say rows or ok. A 404 means a wrong path, `Failed to fetch` a wrong host or missing CORS.

Console

(nothing was printed)

Expected console output

local folder
  stations.csv         25 rows
  daily.csv            365 rows
  station-month.csv    300 rows
  hourly-june.csv      720 rows
  trips-10k.csv        10000 rows
  status.json          ok
  fleet.json           ok
  ardenvale.topojson   ok
Google Cloud Storage (PLACEHOLDER bucket)
  stations.csv         TypeError: Failed to fetch
Amazon S3 (PLACEHOLDER bucket)
  stations.csv         TypeError: Failed to fetch
wrong path on the local base: HTTP 404
The finished dashboard in miniature, with a numbers audit New tab

This is a small version of the dashboard, in one script, because the preview cannot run the Vite project (Topic 4.1): it keeps the KPIs, the rides line and the top stations, and adds an audit. The whole project, with React, live status, the map and the story, is in the files of Phases 4 to 17. Run the same kind of audit against your real dashboard before you publish.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

PASS total rides is 933,651
PASS quietest day is 2025-08-14 with 439
PASS busiest day is 2025-07-18 with 4,380
PASS station-month adds up to daily.csv
PASS 25 stations
PASS top station is Quay with 58,584
PASS no NaN in any attribute
Bucket setup commands (Google Cloud Storage and Amazon S3)bash

Not run in this course: they need your cloud account. YOUR-BUCKET and YOUR-REGION are placeholders. Run these from the folder that holds public/data/lumen/.

# --- Google Cloud Storage ---
gcloud storage buckets create gs://YOUR-BUCKET --location=EU --uniform-bucket-level-access
gcloud storage buckets add-iam-policy-binding gs://YOUR-BUCKET \
  --member=allUsers --role=roles/storage.objectViewer
gcloud storage buckets update gs://YOUR-BUCKET --cors-file=cors.json   # before the upload: see the cache note
gcloud storage cp public/data/lumen/* gs://YOUR-BUCKET/lumen/ \
  --cache-control="public, max-age=3600"

# --- Amazon S3 (the second command turns off Block Public Access for this one bucket) ---
aws s3 mb s3://YOUR-BUCKET --region YOUR-REGION
aws s3api delete-public-access-block --bucket YOUR-BUCKET
aws s3 cp public/data/lumen/ s3://YOUR-BUCKET/lumen/ --recursive \
  --cache-control "public, max-age=3600"
aws s3api put-bucket-policy --bucket YOUR-BUCKET --policy file://policy.json
aws s3api put-bucket-cors --bucket YOUR-BUCKET --cors-configuration file://cors-s3.json
policy.json (S3: anyone may read lumen/)json

PLACEHOLDER bucket name. This makes everything under lumen/ public: keep private files elsewhere.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadLumenData",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::YOUR-BUCKET/lumen/*"
    }
  ]
}
cors-s3.json (S3: GET from any site)json
{
  "CORSRules": [
    {
      "AllowedOrigins": ["*"],
      "AllowedMethods": ["GET"],
      "AllowedHeaders": ["*"],
      "MaxAgeSeconds": 3600
    }
  ]
}

Your project files

Your lumen-dashboard project after this topic. Click a file to see it in full.

src/utils/config.jsjs

The whole file after this topic. If yours differs, you can copy this one and carry on.

// Where the data lives. To read it from cloud storage, change this one line.
export const DATA_URL = "/data/lumen/";

// Google Cloud Storage: replace YOUR-BUCKET with your bucket's name (PLACEHOLDER)
// export const DATA_URL = "https://storage.googleapis.com/YOUR-BUCKET/lumen/";

// Amazon S3: replace YOUR-BUCKET and YOUR-REGION (PLACEHOLDERS)
// export const DATA_URL = "https://YOUR-BUCKET.s3.YOUR-REGION.amazonaws.com/lumen/";

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget to publish both pages

In vite.config.js, remove the story line from rollupOptions.input and run npm run build again.

terminal
$ npm run build
── what you'll see ──
dist/index.html 6.50 kB │ gzip: 2.33 kB
dist/assets/dashboard-BUajVF3k.css 6.88 kB │ gzip: 1.96 kB
dist/assets/dashboard-CFjEi9zD.js 711.64 kB │ gzip: 220.65 kB
✓ built in 878ms

Myth vs fact

Myth

If it works on my laptop, it is finished.

Fact

The laptop has your cache, your fast network, your cookies and a dev server. A production build on a clean host, opened on a phone, finds the missing file, the wrong path and the missing CORS header.

Myth

Putting an API key in the frontend is fine if the repository is private.

Fact

Everything in the built JavaScript is public: anyone can read it in the browser. Keys belong on a server. Public data needs no key, and private data needs a signed link or a login.

Myth

The large-chunk warning means the build failed.

Fact

It is a warning about size, not an error. It tells you to look at your budget (Topic 16.5): import fewer modules, split the code, or accept the size on purpose.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Hashed file names make dist/assets/ safe to cache for a year, but index.html must not be cached for long, since it names the current hashes. Set a short cache time for the HTML and a long one for assets/.

  • ▸

    allUsers read access (the Storage Object Viewer role) on Google Cloud Storage exposes every object in the bucket to the internet and lets anyone list the file names, and S3's public policy does the same for the prefix it names. Keep a bucket for public data only, and never mix private files into it.

  • ▸

    A published dashboard needs a way to know it is broken. Add one external check (a simple uptime monitor on the page and on status.json) so that you learn about a failure before the board does.

Remember this

  1. 1

    The problem this solves: Arjun's dashboard runs on his laptop. The company needs it on the wall screen, on phones and on the city's site, with data that Priya's team can update without touching the code. This topic closes that chapter: the dashboard is finished, built, published, and ready to hand to the person Arjun is about to hire.

  2. 2

    What the dashboard can do now that it could not at the start. It loads the real year of rides (Phase 4) and cleans it (4.4), draws a line, bars, a stacked chart, a heatmap, a trips canvas, a station map and a fleet treemap (Phases 5 to 13), moves smoothly, respects colour, dark mode and reduced motion (9, 10), reads well on a phone and a wall screen (10.5), works in React (14), speaks to a screen reader with table twins and keyboard access (15), shows live status with every state handled, exports and has tests (16), and has a story page for the city (17). In Phase 0 it was seven hand-drawn bars.

  3. 3

    One line moves the data. Every data file is read through DATA_URL in src/utils/config.js (Topic 4.6). Today it is /data/lumen/, the folder next to the page. To serve the files from your own bucket, you comment that line out and enable another one: https://storage.googleapis.com/YOUR-BUCKET/lumen/ for Google Cloud Storage, or https://YOUR-BUCKET.s3.YOUR-REGION.amazonaws.com/lumen/ for Amazon S3. The names YOUR-BUCKET and YOUR-REGION are placeholders: you replace them with your own. Nothing else in the code changes.

  4. 4

    The bucket needs three settings, and no secret. The files must be readable by anyone (the dashboard runs in a stranger's browser), so the bucket allows public read of its objects. It must send a CORS header, so the browser lets a page on another address read the files (GET from any origin is enough for public data). And it should send Cache-Control, so browsers and CDNs keep the files for a while. Keys and passwords never go in frontend code: everything in src/ is downloaded by every visitor. If data must be private, a small server hands out short-lived signed links instead (Topic 4.6).

  5. 5

    **npm run build makes the files you publish.** vite build joins the modules, removes unused code, adds a hash to each file name (so a new version is never confused with a cached old one) and writes everything to dist/. Upload dist/ to any static host, because it is only HTML, CSS and JavaScript. The next section shows the real output for this project: two pages (the dashboard and the story), about 192 kB of gzipped JavaScript in the dashboard's main file, and a warning that the file is large. The warning is honest: React, Recharts and D3 together are heavy. Importing only the D3 modules you use (Topic 16.5) and splitting the code are the next savings.

  6. 6

    What is still missing, honestly. Nothing in this course's scope, but real products go further: 3D and WebGL charts, richer animation, a full testing course (visual tests, end-to-end tests), real-time pipelines and a back end. Hectal plans courses for 3D Web, Animation and Testing; they are coming and have no pages yet. What exists today: the React course (/react), the TypeScript course (/typescript) and the System Design course (/system-design), which go deeper on the parts you used here.

Explain it without notes

01

How do you move the dashboard's data to your own bucket, and what must be set on the bucket?

02

What does npm run build produce, and how do you check it before publishing?

03

Why must keys never be in frontend code, and what do you do for private data?

04

What would you build next, and why?

Practice

01

Write the three lines you would add to the README so that a new hire can change the data, run the tests and publish.

02

Add a post-build script to package.json named check that runs the tests and the build, and explain what it protects against.

03

Write a cors.json that allows GET only from https://dashboard.example.com, and explain when you would prefer it to *.

Trade-offs

  • ↔

    Serving the data from a bucket frees the dashboard from redeploys when data changes, but adds a place that can fail and a cache delay; bundling the data into dist/ is simpler and always consistent with the code.

  • ↔

    A larger single JavaScript file is simpler to host and cache; splitting it loads the first screen faster but adds requests and complexity.

Done when you can

  • I can walk the dashboard through the phase checklist and fix what fails.

  • I can run the tests and a production build, and read the file sizes it prints.

  • I can point the dashboard at my own bucket by changing the one line in config.js.

  • I can set a bucket to public read with CORS and a cache time, and explain why no key goes in the frontend.

  • I can publish dist/ to a static host and test the live site like a visitor.

  • I can name what I would learn or build next.