Design Localized Virtual Ad Feeds
Build a synchronized ad simulator with regional failover and safe fallbacks.
Introduction
30 Second Summary
A football match can look identical on millions of screens while the advertising around the pitch changes from country to country. The challenge is keeping every regional picture tied to the same moment when one local ad service fails.
In this project, you will build a browser-based control room for four localized virtual advertising feeds over one fictional match. The finished simulation keeps every feed aligned to one authoritative event timeline through a Brazil-only decision failure.
What You'll Build
You will watch four country panels split across different slot numbers before the synchronized control room pulls every localized board back onto one server-owned event timeline.
By the end of this project, you'll have:
- A before-and-after timer drift demo that first shows conflicting local slots before aligning every country to one server-issued slot.
- A Brazil-only failure test where a neutral safety board appears while the other three feeds stay served.
- A production architecture walkthrough that explains why four regional decisions stay small even when viewer delivery reaches 10 million.
- Secret Mission: Write and test a game-day degradation runbook that defines when operators use a stale creative, neutral board, or clean world feed.
Are there any prerequisites?
You need basic JavaScript plus HTTP API experience.
The lab uses Node.js 24.21.0 LTS with Visual Studio Code and Safari on a Mac.
Before We Start
Before the hands-on work begins, this checkpoint defines normal behavior: the United States, United Kingdom, Japan, and Brazil receive synchronized localized board creatives. It also defines the failure boundary that protects the viewer experience: if Brazil's regional ad decision fails, only Brazil receives the neutral safety board while the other three feeds continue normally.
Set Up the Local Broadcast Lab
Your four-country broadcast now has a clear success condition. The timing experiments need a predictable local lab.
A version mismatch can make later timing behavior harder to diagnose. You'll verify Node.js v24.21.0 before writing any code.
You'll also prepare the exact empty file structure in Visual Studio Code. Each later step can then focus on one system-design problem.
In this step, get ready to:
- Open the virtual-board-simulator workspace in Visual Studio Code.
- Confirm the local Node.js runtime matches v24.21.0.
- Create the seven empty simulator files.
Open the local workspace
A named workspace keeps the server file beside the browser files it serves. Placing it on your Desktop also makes the project easy to find throughout the experiment.
- Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Click File in the top menu bar.
- Select Open Folder....
- Navigate to your Desktop in the file picker.
- Use the file picker's folder control to create virtual-board-simulator.
- Select the virtual-board-simulator folder.
- Confirm the selection to open the folder as your workspace.
Your lab now has a fixed home. The Explorer shows virtual-board-simulator as the open workspace.
Verify the Node.js runtime
Node.js runs the local HTTP server later in the project. Matching the pinned runtime removes version uncertainty from the timer experiment.
- Click View in the top menu bar.
- Select Terminal to open the integrated terminal.
- Check the installed Node.js version by running this command:
node -v
What does this command check?
The -v option asks the installed Node.js runtime to print its release number. Matching the project pin keeps the later server behavior on a known runtime.
✔️ I see the required version
That's the runtime locked in. Your terminal prints v24.21.0.
ⓧ I see a different version
Your installed runtime does not match the version used by this project. Install the pinned release before continuing.
macOS may request your password because the installer updates the local runtime. This authorization applies to the installation on your Mac.
- Download the official Node.js 24.21.0 macOS package from the official installer page.
- Open node-v24.21.0.pkg from your Downloads folder.
- Follow the macOS installation prompts to completion.
- Approve the installation with your Mac password when prompted.
- Return to the integrated terminal from earlier.
- Repeat the version check shown above.
The new check should print v24.21.0.
ⓧ The terminal cannot find Node.js
The terminal cannot currently start the Node.js runtime. Installing the pinned macOS package provides the command required by the simulator.
Your Mac may ask for a password before changing system software. The request belongs to the local installation process.
- Download the official Node.js 24.21.0 macOS package from the official installer page.
- Open node-v24.21.0.pkg from your Downloads folder.
- Follow the macOS installation prompts to completion.
- Authorize the installation with your Mac password when prompted.
- Return to the integrated terminal from earlier.
- Repeat the version check shown above.
The terminal should now print v24.21.0.
Still seeing the wrong runtime?
- Close the current integrated terminal after the installer finishes.
- Open a fresh integrated terminal from View followed by Terminal.
- Run the version check again from the fresh terminal.
If the version still does not match, help me diagnose why Visual Studio Code is using the wrong Node.js runtime.
Create the empty simulator files
The simulator separates server logic from browser assets. Empty placeholders establish that boundary before implementation begins.
- Create index.js by selecting New File... in the Explorer.
- Create SYSTEM-DESIGN.md by selecting New File... in the Explorer.
The Explorer now shows both empty files at the workspace level.
- Create public/index.html by selecting New File... in the Explorer.
- Create public/drift.html by selecting New File... in the Explorer.
The public folder now appears with two empty HTML files.
- Create public/styles.css by selecting New File... in the Explorer.
- Create public/app.js by selecting New File... in the Explorer.
The browser asset folder now includes placeholders for shared styling and synchronized control-room behavior.
- Create public/drift.js by selecting New File... in the Explorer.
All seven project files are now visible. Each file remains empty until its implementation step.
✔️ Awesome, I've got everything!
Great, your workspace contains every empty placeholder required for the simulator.
ⓧ I'd like to double check the full code
The complete contents of every file are empty at this stage. This is the exact workspace layout:
- The workspace level contains an empty index.js file.
- The workspace level contains an empty SYSTEM-DESIGN.md file.
- The public folder contains an empty index.html file.
- The public folder contains an empty drift.html file.
- The public folder contains an empty styles.css file.
- The public folder contains an empty app.js file.
- The public folder contains an empty drift.js file.
Every editor tab should contain zero lines of code.
Before the final check, do you expect the runtime result to match the version you confirmed earlier?
- Recheck Node.js from the integrated terminal by running this command:
node -v
What does the final check prove?
This repeat check confirms that the integrated terminal still uses the pinned runtime after the workspace setup. The file layout beside it confirms that the lab is ready for implementation.
Everything is lined up. Your terminal prints v24.21.0.
The Explorer shows index.js plus SYSTEM-DESIGN.md at the workspace level. The public folder shows index.html, drift.html, styles.css, app.js, and drift.js.
Missing a file or version?
- Return to the matching version tab if the terminal does not print the pinned release.
- Select New File... in the Explorer if one of the required paths is missing.
- Remove any text from a placeholder file if it is not empty.
If the workspace still differs, help me compare my Visual Studio Code workspace with the required empty file structure.
Your local broadcast lab is ready. Next up, you'll make four independently timed feeds drift apart and observe the synchronization problem directly.
Make Four Feeds Drift Apart
Your local lab is ready with a verified Node.js runtime plus empty simulator files. The next question is whether four regional feeds can keep one shared match moment while each controls its own clock.
Independent browser timers make each feed responsible for choosing its local slot. This step turns timer drift into a visible experiment across the four country feeds.
In this step, get ready to:
- Start a local server for the drift page.
- Build four stadium feed cards with shared styling.
- Expose slot divergence with four independent browser timers.
Serve the drift page locally
A local HTTP server gives Safari one stable URL for the drift page. The server only exposes the three assets required for this experiment.
- In the Explorer sidebar from earlier, select index.js.
- Replace the empty file with this server foundation:
const http = require('node:http');
const fs = require('node:fs');
const PORT = 3000;
const STATIC_ROUTES = {
'/drift.html': { file: './public/drift.html', type: 'text/html; charset=utf-8' },
'/styles.css': { file: './public/styles.css', type: 'text/css; charset=utf-8' },
'/drift.js': { file: './public/drift.js', type: 'text/javascript; charset=utf-8' },
};
function sendJson(response, statusCode, payload) {
response.writeHead(statusCode, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
response.end(JSON.stringify(payload));
}
function serveStatic(response, route) {
fs.readFile(route.file, (error, data) => {
if (error) {
sendJson(response, 500, { error: 'Unable to read local asset' });
return;
}
response.writeHead(200, { 'Content-Type': route.type });
response.end(data);
});
}
What does this foundation do?
- The node:http module provides the server primitives.
- The node:fs module reads each local asset from disk.
- STATIC_ROUTES limits the server to the drift page plus its stylesheet and script.
- sendJson returns structured JSON errors.
- serveStatic returns a matched file with its registered content type.
- Add the request listener directly below serveStatic by pasting this block:
const server = http.createServer((request, response) => {
const route = STATIC_ROUTES[request.url];
if (request.method === 'GET' && route) {
serveStatic(response, route);
return;
}
sendJson(response, 404, { error: 'Route not found' });
});
server.listen(PORT, () => {
console.log(`Virtual board simulator running at http://127.0.0.1:${PORT}/`);
});
How are requests handled?
- The request listener matches the requested URL against STATIC_ROUTES.
- A matching GET request receives its local file.
- An unknown path receives a 404 JSON response.
- The server listens on port 3000.
- Save index.js.
- Start the local server in the integrated terminal by running:
node index.js
What does this command do?
The node command starts index.js as the server process. The terminal remains busy while the server accepts requests.
You should see Virtual board simulator running at http://127.0.0.1:3000/. Your local server is now ready to deliver the experiment.
Server did not start?
Confirm the integrated terminal is inside the virtual-board-simulator workspace. Check that index.js matches the two blocks above.
If the command still fails, help me diagnose the local server error.
✔️ Awesome, I've got everything!
Your index.js file now matches the local server required for this step.
ⓧ I'd like to double check the full code
const http = require('node:http');
const fs = require('node:fs');
const PORT = 3000;
const STATIC_ROUTES = {
'/drift.html': { file: './public/drift.html', type: 'text/html; charset=utf-8' },
'/styles.css': { file: './public/styles.css', type: 'text/css; charset=utf-8' },
'/drift.js': { file: './public/drift.js', type: 'text/javascript; charset=utf-8' },
};
function sendJson(response, statusCode, payload) {
response.writeHead(statusCode, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
response.end(JSON.stringify(payload));
}
function serveStatic(response, route) {
fs.readFile(route.file, (error, data) => {
if (error) {
sendJson(response, 500, { error: 'Unable to read local asset' });
return;
}
response.writeHead(200, { 'Content-Type': route.type });
response.end(data);
});
}
const server = http.createServer((request, response) => {
const route = STATIC_ROUTES[request.url];
if (request.method === 'GET' && route) {
serveStatic(response, route);
return;
}
sendJson(response, 404, { error: 'Route not found' });
});
server.listen(PORT, () => {
console.log(`Virtual board simulator running at http://127.0.0.1:${PORT}/`);
});
Build the four-feed stadium view
The HTML gives every country the same stadium structure. Country-specific element IDs let the script update one feed without touching another.
- In the Explorer sidebar, select public/drift.html.
- Replace the empty file with this page structure:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Timer Drift Demonstration</title>
<link rel="stylesheet" href="/styles.css">
<script src="/drift.js" defer></script>
</head>
<body>
<main class="page-shell">
<header class="hero warning-hero">
<p class="eyebrow">Designed obstacle</p>
<h1>Independent Timers Lose Alignment</h1>
<p>Every panel starts at local slot zero, but each browser timer advances at a different interval.</p>
<div class="toolbar"><a href="/">Open the synchronized control room</a></div>
</header>
<section class="status-strip single-status" aria-label="Drift status"><div><span class="label">Observed result</span><strong id="drift-status">Initially aligned</strong></div></section>
<section class="feed-grid" aria-label="Drifting broadcast feeds">
<article class="feed-card"><header><h2>United States</h2><span>1400 ms local interval</span></header><div class="stadium"><div class="pitch"></div><div class="board" id="drift-US-board"><span id="drift-US-creative">Starting</span></div></div><p>Local slot: <strong id="drift-US-slot">0</strong></p></article>
<article class="feed-card"><header><h2>United Kingdom</h2><span>1550 ms local interval</span></header><div class="stadium"><div class="pitch"></div><div class="board" id="drift-GB-board"><span id="drift-GB-creative">Starting</span></div></div><p>Local slot: <strong id="drift-GB-slot">0</strong></p></article>
<article class="feed-card"><header><h2>Japan</h2><span>1700 ms local interval</span></header><div class="stadium"><div class="pitch"></div><div class="board" id="drift-JP-board"><span id="drift-JP-creative">Starting</span></div></div><p>Local slot: <strong id="drift-JP-slot">0</strong></p></article>
<article class="feed-card"><header><h2>Brazil</h2><span>1850 ms local interval</span></header><div class="stadium"><div class="pitch"></div><div class="board" id="drift-BR-board"><span id="drift-BR-creative">Starting</span></div></div><p>Local slot: <strong id="drift-BR-slot">0</strong></p></article>
</section>
</main>
</body>
</html>
How is the page organized?
- The header identifies this page as the designed timing obstacle.
- The drift-status element holds the experiment's current verdict.
- Each feed card contains one stadium view plus a country-specific board and slot counter.
- The page loads /styles.css for presentation.
- The page loads /drift.js for timer behavior.
- Save public/drift.html.
- Press Cmd+Space on macOS or the Windows key on Windows to open system search.
- Type Safari and press Enter.
- Enter http://127.0.0.1:3000/drift.html in the address bar.
You should see the page title plus four unstyled country sections. Each section shows Starting and a local slot of 0.
Drift page did not load?
Confirm the server terminal is still running. Check that the address ends with /drift.html.
If the page still fails to load, help me troubleshoot the local drift page.
✔️ Awesome, I've got everything!
Your public/drift.html file now contains all four country feed cards.
ⓧ I'd like to double check the full code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Timer Drift Demonstration</title>
<link rel="stylesheet" href="/styles.css">
<script src="/drift.js" defer></script>
</head>
<body>
<main class="page-shell">
<header class="hero warning-hero">
<p class="eyebrow">Designed obstacle</p>
<h1>Independent Timers Lose Alignment</h1>
<p>Every panel starts at local slot zero, but each browser timer advances at a different interval.</p>
<div class="toolbar"><a href="/">Open the synchronized control room</a></div>
</header>
<section class="status-strip single-status" aria-label="Drift status"><div><span class="label">Observed result</span><strong id="drift-status">Initially aligned</strong></div></section>
<section class="feed-grid" aria-label="Drifting broadcast feeds">
<article class="feed-card"><header><h2>United States</h2><span>1400 ms local interval</span></header><div class="stadium"><div class="pitch"></div><div class="board" id="drift-US-board"><span id="drift-US-creative">Starting</span></div></div><p>Local slot: <strong id="drift-US-slot">0</strong></p></article>
<article class="feed-card"><header><h2>United Kingdom</h2><span>1550 ms local interval</span></header><div class="stadium"><div class="pitch"></div><div class="board" id="drift-GB-board"><span id="drift-GB-creative">Starting</span></div></div><p>Local slot: <strong id="drift-GB-slot">0</strong></p></article>
<article class="feed-card"><header><h2>Japan</h2><span>1700 ms local interval</span></header><div class="stadium"><div class="pitch"></div><div class="board" id="drift-JP-board"><span id="drift-JP-creative">Starting</span></div></div><p>Local slot: <strong id="drift-JP-slot">0</strong></p></article>
<article class="feed-card"><header><h2>Brazil</h2><span>1850 ms local interval</span></header><div class="stadium"><div class="pitch"></div><div class="board" id="drift-BR-board"><span id="drift-BR-creative">Starting</span></div></div><p>Local slot: <strong id="drift-BR-slot">0</strong></p></article>
</section>
</main>
</body>
</html>
The page structure is in place. Shared CSS now turns those sections into a responsive broadcast control room.
- In the Explorer sidebar, select public/styles.css.
- Replace the empty file with these shared styles:
:root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #07111f; color: #eef6ff; }
* { box-sizing: border-box; }
body { margin: 0; min-height: 100vh; background: radial-gradient(circle at top, #17385c 0, #07111f 48%); }
button, a { font: inherit; }
.page-shell { width: min(1180px, calc(100% - 32px)); margin: 0 auto; padding: 32px 0 48px; }
.hero { padding: 24px; border: 1px solid #2f5f8c; border-radius: 18px; background: rgba(9, 27, 45, 0.92); }
.warning-hero { border-color: #a66a17; }
.eyebrow { margin: 0 0 8px; color: #7ed7ff; text-transform: uppercase; letter-spacing: 0.12em; font-size: 0.78rem; }
h1 { margin: 0; font-size: clamp(2rem, 5vw, 3.6rem); }
.hero p { max-width: 780px; line-height: 1.6; }
.toolbar { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 18px; }
.toolbar a, .toolbar button { border: 1px solid #74c9ff; border-radius: 10px; padding: 10px 14px; background: #0e2942; color: #eef6ff; text-decoration: none; cursor: pointer; }
.toolbar button:hover, .toolbar a:hover { background: #163c5f; }
.status-strip { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin: 18px 0; }
.single-status { grid-template-columns: 1fr; }
.status-strip div, .event-panel { padding: 14px; border: 1px solid #294861; border-radius: 12px; background: rgba(7, 21, 34, 0.9); }
.label { display: block; margin-bottom: 6px; color: #93abc0; font-size: 0.8rem; text-transform: uppercase; }
.feed-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
.feed-card { overflow: hidden; border: 1px solid #294861; border-radius: 16px; background: #0a1827; }
.feed-card > header { display: flex; justify-content: space-between; gap: 12px; align-items: baseline; padding: 14px 16px; }
.feed-card h2 { margin: 0; font-size: 1.1rem; }
.feed-card header span { color: #93abc0; font-size: 0.82rem; }
.feed-card > p { padding: 0 16px 16px; }
.stadium { position: relative; min-height: 190px; overflow: hidden; background: linear-gradient(#172e49 0 35%, #16643c 35% 100%); }
.pitch { position: absolute; inset: 42% 8% 18%; border: 2px solid rgba(255, 255, 255, 0.72); background: repeating-linear-gradient(90deg, #187545 0 48px, #1b8250 48px 96px); }
.board { position: absolute; left: 5%; right: 5%; bottom: 10px; min-height: 54px; display: grid; place-items: center; padding: 10px; border: 3px solid #dcecff; background: #155f9d; color: white; text-align: center; font-weight: 800; letter-spacing: 0.02em; transition: background 180ms ease; }
.board[data-status="local"] { background: #8a4e12; }
@media (max-width: 760px) { .status-strip, .feed-grid { grid-template-columns: 1fr; } }
What do these styles create?
- The page shell and hero create the dark control-room frame.
- The feed grid places the country cards in two columns on wider screens.
- The stadium and pitch selectors create one shared camera scene for every country.
- The board selector creates the virtual advertising surface.
- The media query stacks the feeds on narrow screens.
- Save public/styles.css.
- Return to Safari from earlier.
- Refresh http://127.0.0.1:3000/drift.html.
You should see four dark feed cards arranged in a responsive grid. Each card contains the same green pitch plus a virtual board.
Feed cards still look unstyled?
Confirm public/styles.css is saved. Check that public/drift.html still links to /styles.css.
If the styles still do not appear, help me debug the drift page stylesheet.
✔️ Awesome, I've got everything!
Your public/styles.css file now styles the shared stadium view and four country cards.
ⓧ I'd like to double check the full code
:root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #07111f; color: #eef6ff; }
* { box-sizing: border-box; }
body { margin: 0; min-height: 100vh; background: radial-gradient(circle at top, #17385c 0, #07111f 48%); }
button, a { font: inherit; }
.page-shell { width: min(1180px, calc(100% - 32px)); margin: 0 auto; padding: 32px 0 48px; }
.hero { padding: 24px; border: 1px solid #2f5f8c; border-radius: 18px; background: rgba(9, 27, 45, 0.92); }
.warning-hero { border-color: #a66a17; }
.eyebrow { margin: 0 0 8px; color: #7ed7ff; text-transform: uppercase; letter-spacing: 0.12em; font-size: 0.78rem; }
h1 { margin: 0; font-size: clamp(2rem, 5vw, 3.6rem); }
.hero p { max-width: 780px; line-height: 1.6; }
.toolbar { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 18px; }
.toolbar a, .toolbar button { border: 1px solid #74c9ff; border-radius: 10px; padding: 10px 14px; background: #0e2942; color: #eef6ff; text-decoration: none; cursor: pointer; }
.toolbar button:hover, .toolbar a:hover { background: #163c5f; }
.status-strip { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin: 18px 0; }
.single-status { grid-template-columns: 1fr; }
.status-strip div, .event-panel { padding: 14px; border: 1px solid #294861; border-radius: 12px; background: rgba(7, 21, 34, 0.9); }
.label { display: block; margin-bottom: 6px; color: #93abc0; font-size: 0.8rem; text-transform: uppercase; }
.feed-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
.feed-card { overflow: hidden; border: 1px solid #294861; border-radius: 16px; background: #0a1827; }
.feed-card > header { display: flex; justify-content: space-between; gap: 12px; align-items: baseline; padding: 14px 16px; }
.feed-card h2 { margin: 0; font-size: 1.1rem; }
.feed-card header span { color: #93abc0; font-size: 0.82rem; }
.feed-card > p { padding: 0 16px 16px; }
.stadium { position: relative; min-height: 190px; overflow: hidden; background: linear-gradient(#172e49 0 35%, #16643c 35% 100%); }
.pitch { position: absolute; inset: 42% 8% 18%; border: 2px solid rgba(255, 255, 255, 0.72); background: repeating-linear-gradient(90deg, #187545 0 48px, #1b8250 48px 96px); }
.board { position: absolute; left: 5%; right: 5%; bottom: 10px; min-height: 54px; display: grid; place-items: center; padding: 10px; border: 3px solid #dcecff; background: #155f9d; color: white; text-align: center; font-weight: 800; letter-spacing: 0.02em; transition: background 180ms ease; }
.board[data-status="local"] { background: #8a4e12; }
@media (max-width: 760px) { .status-strip, .feed-grid { grid-template-columns: 1fr; } }
Start four independent timers
Each feed stores its own localSlot value. Unequal repeating intervals let you test whether those independent counters stay aligned.
- In the Explorer sidebar, select public/drift.js.
- Replace the empty file with this first JavaScript block:
const DRIFT_FEEDS = [
{ code: 'US', intervalMs: 1400, creatives: ['NorthStar Sports', 'RoadTrip Electric', 'FreshField'] },
{ code: 'GB', intervalMs: 1550, creatives: ['Harbor Mobile', 'Green Railway', 'Corner Market'] },
{ code: 'JP', intervalMs: 1700, creatives: ['Sakura Transit', 'Hikari Mobile', 'Mirai Foods'] },
{ code: 'BR', intervalMs: 1850, creatives: ['Sol Energia', 'Viva Mobile', 'Verde Market'] },
];
const states = DRIFT_FEEDS.map((feed) => ({ ...feed, localSlot: 0 }));
function renderFeed(state) {
const creative = state.creatives[state.localSlot % state.creatives.length];
document.getElementById(`drift-${state.code}-creative`).textContent = creative;
document.getElementById(`drift-${state.code}-slot`).textContent = state.localSlot;
document.getElementById(`drift-${state.code}-board`).setAttribute('data-status', 'local');
}
How is each feed represented?
- DRIFT_FEEDS defines one country code and one interval for every output.
- Each feed carries three fictional advertiser names.
- states creates an independent localSlot counter for every feed.
- renderFeed writes the selected creative and slot into the DOM.
- The creative selection wraps through the three available names.
- Add the status comparison and timer loop below renderFeed by pasting:
function updateDriftStatus() {
const firstSlot = states[0].localSlot;
const allAligned = states.every((state) => state.localSlot === firstSlot);
document.getElementById('drift-status').textContent = allAligned
? `Aligned on local slot ${firstSlot}`
: `DRIFT DETECTED: ${states.map((state) => `${state.code}=${state.localSlot}`).join(' | ')}`;
}
states.forEach((state) => {
renderFeed(state);
setInterval(() => {
state.localSlot += 1;
renderFeed(state);
updateDriftStatus();
}, state.intervalMs);
});
updateDriftStatus();
How does the drift appear?
- updateDriftStatus compares every local slot with the United States slot.
- A matching set produces an aligned message.
- A mismatched set produces DRIFT DETECTED plus the four current slot values.
- Each setInterval callback advances only its own feed.
- The initial calls render all four feeds before the first interval completes.
Before you refresh, do you think the four slot counters will still match after five seconds?
- Save public/drift.js.
- Return to Safari from earlier.
- Refresh http://127.0.0.1:3000/drift.html.
- Wait five seconds.
You'll see the four cards display different local slot numbers. The banner reads DRIFT DETECTED with the current slot for each country.
That's the experiment working: the country creatives render independently while their local timelines split apart. You now have visible evidence that separate feed timers cannot preserve one shared event slot.
Feeds remain on Starting?
- Confirm public/drift.js is saved.
- Check that the page still loads /drift.js.
- Refresh the page after saving the script.
- Wait at least five seconds for the unequal intervals to separate.
If the slots still do not move, help me debug the independent timer script.
✔️ Awesome, I've got everything!
Your public/drift.js file now drives all four independent local timers.
ⓧ I'd like to double check the full code
const DRIFT_FEEDS = [
{ code: 'US', intervalMs: 1400, creatives: ['NorthStar Sports', 'RoadTrip Electric', 'FreshField'] },
{ code: 'GB', intervalMs: 1550, creatives: ['Harbor Mobile', 'Green Railway', 'Corner Market'] },
{ code: 'JP', intervalMs: 1700, creatives: ['Sakura Transit', 'Hikari Mobile', 'Mirai Foods'] },
{ code: 'BR', intervalMs: 1850, creatives: ['Sol Energia', 'Viva Mobile', 'Verde Market'] },
];
const states = DRIFT_FEEDS.map((feed) => ({ ...feed, localSlot: 0 }));
function renderFeed(state) {
const creative = state.creatives[state.localSlot % state.creatives.length];
document.getElementById(`drift-${state.code}-creative`).textContent = creative;
document.getElementById(`drift-${state.code}-slot`).textContent = state.localSlot;
document.getElementById(`drift-${state.code}-board`).setAttribute('data-status', 'local');
}
function updateDriftStatus() {
const firstSlot = states[0].localSlot;
const allAligned = states.every((state) => state.localSlot === firstSlot);
document.getElementById('drift-status').textContent = allAligned
? `Aligned on local slot ${firstSlot}`
: `DRIFT DETECTED: ${states.map((state) => `${state.code}=${state.localSlot}`).join(' | ')}`;
}
states.forEach((state) => {
renderFeed(state);
setInterval(() => {
state.localSlot += 1;
renderFeed(state);
updateDriftStatus();
}, state.intervalMs);
});
updateDriftStatus();
Your four localized feeds now prove the timing problem in the browser. Next, you'll move slot ownership to one shared event clock so every regional output changes together.
Synchronize Every Feed to the Server Clock
Your drift demonstration proved that independent browser timers can assign different slots to the same moment. That timer drift breaks the shared match timeline.
The synchronized control room gives Node.js ownership of the event clock. Each browser refresh receives one slot plus four localized creative decisions.
In this step, get ready to:
- Create an authoritative server clock for five-second advertising slots.
- Build the four-country synchronized control room.
- Poll the server without selecting slots inside the browser.
Give the server one clock
An authoritative event clock gives every regional output the same source of time. The server derives each slot from one recorded event start.
Why use one server-owned slot?
The browser timer now controls when to request fresh state. The server response controls what the browser displays.
This separates refresh scheduling from advertising decisions. A delayed refresh can catch up by reading the current authoritative slot.
The updated server combines routing with the country creative catalog. Its functions depend on each other, so the reference below shows the complete cumulative file.
- Select index.js in the Visual Studio Code Explorer.
- Choose the full-code tab below.
- Replace the current contents of index.js with the complete reference.
✔️ My server code matches
Your server is ready for the authoritative board-state check. Continue below to save it and start the updated process.
ⓧ I'd like to double check the full code
const http = require('node:http');
const fs = require('node:fs');
const PORT = 3000;
const SLOT_MS = 5000;
const eventStartMs = Date.now();
let brazilFailureEnabled = false;
const COUNTRIES = [
{
code: 'US', name: 'United States', latencyMs: 18,
creatives: [
{ id: 'us-northstar', text: 'NorthStar Sports: Own the Moment' },
{ id: 'us-roadtrip', text: 'RoadTrip Electric: Go Further' },
{ id: 'us-freshfield', text: 'FreshField: Game Day Delivered' },
],
},
{
code: 'GB', name: 'United Kingdom', latencyMs: 22,
creatives: [
{ id: 'gb-harbor', text: 'Harbor Mobile: Stay Match Ready' },
{ id: 'gb-railway', text: 'Green Railway: Travel Together' },
{ id: 'gb-market', text: 'Corner Market: Made for Match Day' },
],
},
{
code: 'JP', name: 'Japan', latencyMs: 27,
creatives: [
{ id: 'jp-sakura', text: 'Sakura Transit: Your City in Motion' },
{ id: 'jp-hikari', text: 'Hikari Mobile: Share Every Goal' },
{ id: 'jp-mirai', text: 'Mirai Foods: Energy for the Match' },
],
},
{
code: 'BR', name: 'Brazil', latencyMs: 31,
creatives: [
{ id: 'br-sol', text: 'Sol Energia: Power the Celebration' },
{ id: 'br-viva', text: 'Viva Mobile: Together for Every Goal' },
{ id: 'br-verde', text: 'Verde Market: Fresh for Match Day' },
],
},
];
const STATIC_ROUTES = {
'/': { file: './public/index.html', type: 'text/html; charset=utf-8' },
'/index.html': { file: './public/index.html', type: 'text/html; charset=utf-8' },
'/drift.html': { file: './public/drift.html', type: 'text/html; charset=utf-8' },
'/styles.css': { file: './public/styles.css', type: 'text/css; charset=utf-8' },
'/app.js': { file: './public/app.js', type: 'text/javascript; charset=utf-8' },
'/drift.js': { file: './public/drift.js', type: 'text/javascript; charset=utf-8' },
};
function sendJson(response, statusCode, payload) {
response.writeHead(statusCode, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
response.end(JSON.stringify(payload));
}
function decideAd(country, slotIndex) {
if (country.code === 'BR' && brazilFailureEnabled) {
throw new Error('Brazil ad decision unavailable');
}
const creative = country.creatives[slotIndex % country.creatives.length];
return {
countryCode: country.code,
countryName: country.name,
creativeId: creative.id,
creativeText: creative.text,
status: 'served',
reason: 'regional decision succeeded',
simulatedDecisionLatencyMs: country.latencyMs + (slotIndex % 3),
};
}
function safeDecision(country, slotIndex) {
try {
return decideAd(country, slotIndex);
} catch (error) {
return {
countryCode: country.code,
countryName: country.name,
creativeId: 'neutral-safety-board',
creativeText: 'International Football: Enjoy the Match',
status: 'fallback',
reason: error.message,
simulatedDecisionLatencyMs: country.latencyMs,
};
}
}
function buildBoardState() {
const serverTimestampMs = Date.now();
const eventElapsedMs = serverTimestampMs - eventStartMs;
const slotIndex = Math.floor(eventElapsedMs / SLOT_MS);
return {
serverTimestampMs,
eventStartMs,
eventElapsedMs,
slotIndex,
slotEndsAtMs: eventStartMs + (slotIndex + 1) * SLOT_MS,
slotDurationMs: SLOT_MS,
failureInjection: { countryCode: 'BR', enabled: brazilFailureEnabled },
decisions: COUNTRIES.map((country) => safeDecision(country, slotIndex)),
};
}
function serveStatic(response, route) {
fs.readFile(route.file, (error, data) => {
if (error) {
sendJson(response, 500, { error: 'Unable to read local asset' });
return;
}
response.writeHead(200, { 'Content-Type': route.type });
response.end(data);
});
}
const server = http.createServer((request, response) => {
if (request.method === 'GET' && request.url === '/api/board-state') {
sendJson(response, 200, buildBoardState());
return;
}
if (request.method === 'POST' && request.url === '/api/failures/br/toggle') {
brazilFailureEnabled = !brazilFailureEnabled;
sendJson(response, 200, { countryCode: 'BR', enabled: brazilFailureEnabled });
return;
}
const route = STATIC_ROUTES[request.url];
if (request.method === 'GET' && route) {
serveStatic(response, route);
return;
}
sendJson(response, 404, { error: 'Route not found' });
});
server.listen(PORT, () => {
console.log(`Virtual board simulator running at http://127.0.0.1:${PORT}/`);
});
How does the server synchronize the feeds?
- The eventStartMs value records one starting point when the server begins.
- The SLOT_MS value gives every advertising slot the same five-second duration.
- The buildBoardState() function calculates one slotIndex for the complete response.
- The decideAd() function selects a creative from the relevant country catalog.
- The GET /api/board-state route returns the shared timeline plus all four regional decisions.
- Save index.js.
- Return to the Visual Studio Code terminal from earlier.
- Stop the current server process using the terminal control.
- Start the updated server by running this command:
node index.js
You'll see the local simulator URL in the terminal. The server keeps this terminal occupied while it handles browser requests.
Server does not start?
Check that index.js matches the complete reference. A missing brace can stop Node.js before it begins listening.
A previous server process can also hold port 3000. Stop that process before running the command again.
Help me diagnose the server startup problem.
The new HTTP API exposes the exact timing decision that every panel must follow. Checking it directly separates server synchronization from browser rendering.
- Return to Safari from earlier.
- Enter http://127.0.0.1:3000/api/board-state in the address bar.
- Confirm that the response contains one slotIndex plus four entries in decisions.
The response shows one server-derived slot for the United States, United Kingdom, Japan, and Brazil. Each decision carries its own creative ID.
Build the synchronized control room
The API now supplies synchronized state. The control room needs fixed elements where each regional decision can appear without changing the shared stadium view.
- Select public/index.html in the Visual Studio Code Explorer.
- Choose the full-code tab below.
- Replace the empty file with the complete control-room markup.
✔️ My control room markup matches
Your control room has a fixed destination for every shared timing value and regional decision field.
ⓧ I'd like to double check the full code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Global Virtual Board Control Room</title>
<link rel="stylesheet" href="/styles.css">
<script src="/app.js" defer></script>
</head>
<body>
<main class="page-shell">
<header class="hero">
<p class="eyebrow">Fictional international football broadcast</p>
<h1>Global Virtual Board Control Room</h1>
<p>One stadium view, four localized virtual advertising outputs, one authoritative event clock.</p>
<div class="toolbar">
<a href="/drift.html">Open the timer-drift demonstration</a>
<button id="toggle-failure" type="button">Inject Brazil decision failure</button>
</div>
</header>
<section class="status-strip" aria-label="Synchronization status">
<div><span class="label">Event time</span><strong id="event-time">00:00</strong></div>
<div><span class="label">Authoritative slot</span><strong id="global-slot">0</strong></div>
<div><span class="label">Synchronization</span><strong id="sync-status">Connecting</strong></div>
<div><span class="label">Brazil failure</span><strong id="failure-status">Disabled</strong></div>
</section>
<section class="feed-grid" aria-label="Localized broadcast feeds">
<article class="feed-card"><header><h2>United States</h2><span>US output</span></header><div class="stadium"><div class="pitch"></div><div class="board" id="US-board"><span id="US-creative">Waiting for decision</span></div></div><dl><div><dt>Creative</dt><dd id="US-id">-</dd></div><div><dt>Slot</dt><dd id="US-slot">-</dd></div><div><dt>Status</dt><dd id="US-status">-</dd></div><div><dt>Decision latency</dt><dd id="US-latency">-</dd></div></dl></article>
<article class="feed-card"><header><h2>United Kingdom</h2><span>GB output</span></header><div class="stadium"><div class="pitch"></div><div class="board" id="GB-board"><span id="GB-creative">Waiting for decision</span></div></div><dl><div><dt>Creative</dt><dd id="GB-id">-</dd></div><div><dt>Slot</dt><dd id="GB-slot">-</dd></div><div><dt>Status</dt><dd id="GB-status">-</dd></div><div><dt>Decision latency</dt><dd id="GB-latency">-</dd></div></dl></article>
<article class="feed-card"><header><h2>Japan</h2><span>JP output</span></header><div class="stadium"><div class="pitch"></div><div class="board" id="JP-board"><span id="JP-creative">Waiting for decision</span></div></div><dl><div><dt>Creative</dt><dd id="JP-id">-</dd></div><div><dt>Slot</dt><dd id="JP-slot">-</dd></div><div><dt>Status</dt><dd id="JP-status">-</dd></div><div><dt>Decision latency</dt><dd id="JP-latency">-</dd></div></dl></article>
<article class="feed-card"><header><h2>Brazil</h2><span>BR output</span></header><div class="stadium"><div class="pitch"></div><div class="board" id="BR-board"><span id="BR-creative">Waiting for decision</span></div></div><dl><div><dt>Creative</dt><dd id="BR-id">-</dd></div><div><dt>Slot</dt><dd id="BR-slot">-</dd></div><div><dt>Status</dt><dd id="BR-status">-</dd></div><div><dt>Decision latency</dt><dd id="BR-latency">-</dd></div></dl></article>
</section>
<section class="event-panel"><h2>Latest control-plane event</h2><p id="event-log">Waiting for the first board-state response.</p></section>
</main>
</body>
</html>
How is the control room organized?
- The status strip reserves fields for event time and the global slot.
- Each feed card reserves country-specific creative and latency fields.
- Each country slot field receives the same authoritative value.
- The event panel records the latest combination of slot and regional statuses.
- Save public/index.html.
- Enter http://127.0.0.1:3000/ in the Safari address bar.
- Confirm that the page shows four country cards plus the synchronization status strip.
The cards currently use their fixed placeholder values. This confirms that the server can deliver the synchronized control-room page.
Control room page is blank?
Confirm that index.html sits inside the public folder. The server route points to that exact location.
Check that the page links to /styles.css and /app.js.
Help me find why the synchronized control room is blank.
The shared stylesheet already renders the stadium view. A few additional rules format decision details plus the latest control-plane event.
- Select public/styles.css in the Visual Studio Code Explorer.
- Find the existing .board[data-status="local"] rule.
- Insert these server-decision styles immediately before that rule:
.board[data-status="served"] { background: #155f9d; }
.board[data-status="fallback"] { background: #555f69; border-style: dashed; }
What do these status styles show?
- The served state uses the standard blue advertising board.
- The fallback state uses a grey dashed board that is easy to identify.
- Find the .feed-card > p rule.
- Insert these decision-detail rules immediately after it:
dl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin: 0; padding: 14px 16px 18px; }
dl div { min-width: 0; }
dt { color: #93abc0; font-size: 0.76rem; text-transform: uppercase; }
dd { margin: 4px 0 0; overflow-wrap: anywhere; }
.event-panel { margin-top: 18px; }
.event-panel h2 { margin-top: 0; }
What do these layout rules add?
- The definition list displays creative metadata in a compact two-column grid.
- The overflow rule keeps long creative IDs inside their cards.
- The event panel receives spacing below the feed grid.
✔️ My shared styles match
Your stylesheet now supports both the drift page and the synchronized control room.
ⓧ I'd like to double check the full code
:root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #07111f; color: #eef6ff; }
* { box-sizing: border-box; }
body { margin: 0; min-height: 100vh; background: radial-gradient(circle at top, #17385c 0, #07111f 48%); }
button, a { font: inherit; }
.page-shell { width: min(1180px, calc(100% - 32px)); margin: 0 auto; padding: 32px 0 48px; }
.hero { padding: 24px; border: 1px solid #2f5f8c; border-radius: 18px; background: rgba(9, 27, 45, 0.92); }
.warning-hero { border-color: #a66a17; }
.eyebrow { margin: 0 0 8px; color: #7ed7ff; text-transform: uppercase; letter-spacing: 0.12em; font-size: 0.78rem; }
h1 { margin: 0; font-size: clamp(2rem, 5vw, 3.6rem); }
.hero p { max-width: 780px; line-height: 1.6; }
.toolbar { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 18px; }
.toolbar a, .toolbar button { border: 1px solid #74c9ff; border-radius: 10px; padding: 10px 14px; background: #0e2942; color: #eef6ff; text-decoration: none; cursor: pointer; }
.toolbar button:hover, .toolbar a:hover { background: #163c5f; }
.status-strip { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin: 18px 0; }
.single-status { grid-template-columns: 1fr; }
.status-strip div, .event-panel { padding: 14px; border: 1px solid #294861; border-radius: 12px; background: rgba(7, 21, 34, 0.9); }
.label { display: block; margin-bottom: 6px; color: #93abc0; font-size: 0.8rem; text-transform: uppercase; }
.feed-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
.feed-card { overflow: hidden; border: 1px solid #294861; border-radius: 16px; background: #0a1827; }
.feed-card > header { display: flex; justify-content: space-between; gap: 12px; align-items: baseline; padding: 14px 16px; }
.feed-card h2 { margin: 0; font-size: 1.1rem; }
.feed-card header span { color: #93abc0; font-size: 0.82rem; }
.feed-card > p { padding: 0 16px 16px; }
.stadium { position: relative; min-height: 190px; overflow: hidden; background: linear-gradient(#172e49 0 35%, #16643c 35% 100%); }
.pitch { position: absolute; inset: 42% 8% 18%; border: 2px solid rgba(255, 255, 255, 0.72); background: repeating-linear-gradient(90deg, #187545 0 48px, #1b8250 48px 96px); }
.board { position: absolute; left: 5%; right: 5%; bottom: 10px; min-height: 54px; display: grid; place-items: center; padding: 10px; border: 3px solid #dcecff; background: #155f9d; color: white; text-align: center; font-weight: 800; letter-spacing: 0.02em; transition: background 180ms ease; }
.board[data-status="served"] { background: #155f9d; }
.board[data-status="fallback"] { background: #555f69; border-style: dashed; }
.board[data-status="local"] { background: #8a4e12; }
dl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin: 0; padding: 14px 16px 18px; }
dl div { min-width: 0; }
dt { color: #93abc0; font-size: 0.76rem; text-transform: uppercase; }
dd { margin: 4px 0 0; overflow-wrap: anywhere; }
.event-panel { margin-top: 18px; }
.event-panel h2 { margin-top: 0; }
@media (max-width: 760px) { .status-strip, .feed-grid { grid-template-columns: 1fr; } }
How does the complete stylesheet fit together?
The same visual system supports both demonstrations. Board status selectors distinguish independent local timing from synchronized decisions.
The mobile rule collapses the status strip and feed grid to one column on narrow screens.
- Save public/styles.css.
- Refresh the synchronized control room in Safari.
- Confirm that each country card shows a two-column decision-details area.
- Confirm that the latest control-plane event has its own panel below the cards.
Decision details look unstyled?
Check that the dl rules sit outside every earlier selector block. A missing closing brace can absorb the new rules.
Confirm that Safari loaded /styles.css from the local server.
Help me find why the control-room metadata styles are missing.
Poll without choosing locally
The browser still needs a repeating timer for refreshes. That timer never increments a local slot or selects a creative.
Every update fetches fresh server state. The returned eventElapsedMs and slotIndex remain authoritative.
- Select public/app.js in the Visual Studio Code Explorer.
- Choose the full-code tab below.
- Replace the empty file with the complete polling code.
✔️ My polling code matches
Your browser now renders server decisions without maintaining a competing local slot.
ⓧ I'd like to double check the full code
const COUNTRY_CODES = ['US', 'GB', 'JP', 'BR'];
let lastEventKey = '';
function setText(id, value) { document.getElementById(id).textContent = value; }
function formatEventTime(milliseconds) {
const totalSeconds = Math.floor(milliseconds / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
const paddedSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
return `${minutes}:${paddedSeconds}`;
}
function renderDecision(decision, slotIndex) {
setText(`${decision.countryCode}-creative`, decision.creativeText);
setText(`${decision.countryCode}-id`, decision.creativeId);
setText(`${decision.countryCode}-slot`, slotIndex);
setText(`${decision.countryCode}-status`, decision.status);
setText(`${decision.countryCode}-latency`, `${decision.simulatedDecisionLatencyMs} ms`);
document.getElementById(`${decision.countryCode}-board`).setAttribute('data-status', decision.status);
}
function renderState(state) {
setText('event-time', formatEventTime(state.eventElapsedMs));
setText('global-slot', state.slotIndex);
setText('sync-status', `ALIGNED: ${COUNTRY_CODES.length} feeds on slot ${state.slotIndex}`);
setText('failure-status', state.failureInjection.enabled ? 'Enabled for Brazil' : 'Disabled');
state.decisions.forEach((decision) => renderDecision(decision, state.slotIndex));
const eventKey = `${state.slotIndex}:${state.failureInjection.enabled}`;
if (eventKey !== lastEventKey) {
const statuses = state.decisions.map((decision) => `${decision.countryCode}=${decision.status}/${decision.creativeId}`).join(' | ');
setText('event-log', `Event ${formatEventTime(state.eventElapsedMs)} | slot ${state.slotIndex} | ${statuses}`);
lastEventKey = eventKey;
}
setText('toggle-failure', state.failureInjection.enabled ? 'Recover Brazil decision service' : 'Inject Brazil decision failure');
}
async function refreshBoard() {
try {
const response = await fetch('/api/board-state', { cache: 'no-store' });
if (!response.ok) throw new Error(`Board-state request failed with ${response.status}`);
renderState(await response.json());
} catch (error) {
setText('sync-status', `DISCONNECTED: ${error.message}`);
}
}
async function toggleBrazilFailure() {
const response = await fetch('/api/failures/br/toggle', { method: 'POST' });
if (!response.ok) {
setText('failure-status', `Toggle failed with ${response.status}`);
return;
}
await refreshBoard();
}
document.getElementById('toggle-failure').addEventListener('click', toggleBrazilFailure);
refreshBoard();
setInterval(refreshBoard, 1000);
How does the browser stay synchronized?
- The refreshBoard() function requests the latest server-owned state.
- The renderState() function passes the same response slot to every country card.
- The renderDecision() function combines that shared slot with one localized creative.
- The setInterval() call schedules another request every second.
- The browser contains no local slot counter.
- Save public/app.js.
- Return to the Visual Studio Code terminal.
- Stop the running server process using the terminal control.
Before you restart the server, which value should match across all four cards after two advertising changes?
- Restart the synchronized server by running this command:
node index.js
You'll see the simulator URL again. A fresh server start also records a fresh eventStartMs value.
- Return to Safari from earlier.
- Enter http://127.0.0.1:3000/ in the address bar.
- Watch the control room through two advertising changes.
- Compare the slot number across all four country cards.
You'll see all four cards retain the same slot number during each change. Their creative IDs differ because each country uses its own catalog.
That synchronization gap is closed. One server-owned timestamp now keeps four localized advertising outputs on the same event slot.
Feeds are not updating?
Confirm that the terminal still shows the running server process. The browser cannot refresh its board state after that process stops.
If the status remains disconnected, compare /api/board-state in public/app.js with the route in index.js.
Help me debug the synchronized polling loop.
Your regional feeds now share one timeline while preserving country-specific creative decisions. Next, you'll prove that one regional decision failure can stay inside its own boundary.
Contain a Regional Ad Failure
Your Node.js simulator now keeps four localized feeds on one server-owned slot. That shared timeline gives the control room a stable foundation.
A regional ad-decision timeout could still disrupt every output if all decisions share one unguarded path. In this step, you'll add fault isolation so Brazil can fall back while the other regions keep serving ads.
In this step, get ready to:
- Add a Brazil-only failure switch to the server.
- Protect each regional decision with its own fallback boundary.
- Document the production architecture and its scaling assumptions.
Add the Brazil failure boundary
A bulkhead gives each country decision its own failure boundary. The server can then convert one regional error into a safe result for that region.
- Switch back to index.js in Visual Studio Code.
- Find the line containing const route = STATIC_ROUTES[request.url];.
- Add the Brazil failure route directly above that line by pasting this code:
if (request.method === 'POST' && request.url === '/api/failures/br/toggle') {
brazilFailureEnabled = !brazilFailureEnabled;
sendJson(response, 200, { countryCode: 'BR', enabled: brazilFailureEnabled });
return;
}
What does this route do?
- The route accepts a failure-injection request for Brazil.
- The assignment flips brazilFailureEnabled between its enabled and disabled states.
- The JSON response tells the control room which state is active.
- Save index.js.
- Stop the running server by pressing Control+C in the Visual Studio Code terminal.
- Restart the simulator by running this command:
node index.js
What should you see?
The terminal prints the simulator URL. This confirms the updated route is ready to receive requests.
Server not restarting?
Check that the new route sits inside the server callback. Make sure its closing brace appears before the static route lookup.
Compare the route's braces with the code block above if the terminal reports a syntax error.
Help me fix the new failure route in index.js.
Before you test the route, predict what the Brazil board shows after the failure flag flips.
- Switch back to the synchronized control room in Safari.
- Click Inject Brazil decision failure.
You'll see the Brazil failure status change to Enabled for Brazil. The Brazil decision still shows served because the decision path does not react to the flag yet.
- Click Recover Brazil decision service to restore the disabled state.
The route now records the simulated failure. The next change gives that flag a contained effect.
- Return to index.js in Visual Studio Code.
- Find the complete decideAd function.
- Replace that function with this version:
function decideAd(country, slotIndex) {
if (country.code === 'BR' && brazilFailureEnabled) {
throw new Error('Brazil ad decision unavailable');
}
const creative = country.creatives[slotIndex % country.creatives.length];
return {
countryCode: country.code,
countryName: country.name,
creativeId: creative.id,
creativeText: creative.text,
status: 'served',
reason: 'regional decision succeeded',
simulatedDecisionLatencyMs: country.latencyMs + (slotIndex % 3),
};
}
How does the failure start?
- The guard checks for the Brazil country code.
- The same guard checks whether failure injection is enabled.
- The thrown error represents an unavailable Brazil ad-decision service.
- Find the complete safeDecision function below decideAd.
- Replace that function with this protected version:
function safeDecision(country, slotIndex) {
try {
return decideAd(country, slotIndex);
} catch (error) {
return {
countryCode: country.code,
countryName: country.name,
creativeId: 'neutral-safety-board',
creativeText: 'International Football: Enjoy the Match',
status: 'fallback',
reason: error.message,
simulatedDecisionLatencyMs: country.latencyMs,
};
}
}
How is the failure contained?
- The try block handles one country decision at a time.
- The catch block converts an error into a complete fallback decision.
- The fallback preserves the country metadata and latency field.
- The neutral board prevents a missing regional decision from breaking the full state response.
- Save index.js.
- Stop the running server by pressing Control+C in the terminal.
- Start the protected server by running this command:
node index.js
What changed on the server?
Every country still receives an independent call to safeDecision. A Brazil error now becomes data inside the response instead of interrupting that response.
Seeing a server error?
Check that safeDecision contains both the try block and the catch block.
Confirm that COUNTRIES.map still calls safeDecision for each country.
Help me debug the regional fallback logic.
✔️ Awesome, I've got everything!
Your server now has a Brazil-only failure route and a protected decision boundary. Make sure index.js is saved.
ⓧ I'd like to double check the full code
const http = require('node:http');
const fs = require('node:fs');
const PORT = 3000;
const SLOT_MS = 5000;
const eventStartMs = Date.now();
let brazilFailureEnabled = false;
const COUNTRIES = [
{
code: 'US', name: 'United States', latencyMs: 18,
creatives: [
{ id: 'us-northstar', text: 'NorthStar Sports: Own the Moment' },
{ id: 'us-roadtrip', text: 'RoadTrip Electric: Go Further' },
{ id: 'us-freshfield', text: 'FreshField: Game Day Delivered' },
],
},
{
code: 'GB', name: 'United Kingdom', latencyMs: 22,
creatives: [
{ id: 'gb-harbor', text: 'Harbor Mobile: Stay Match Ready' },
{ id: 'gb-railway', text: 'Green Railway: Travel Together' },
{ id: 'gb-market', text: 'Corner Market: Made for Match Day' },
],
},
{
code: 'JP', name: 'Japan', latencyMs: 27,
creatives: [
{ id: 'jp-sakura', text: 'Sakura Transit: Your City in Motion' },
{ id: 'jp-hikari', text: 'Hikari Mobile: Share Every Goal' },
{ id: 'jp-mirai', text: 'Mirai Foods: Energy for the Match' },
],
},
{
code: 'BR', name: 'Brazil', latencyMs: 31,
creatives: [
{ id: 'br-sol', text: 'Sol Energia: Power the Celebration' },
{ id: 'br-viva', text: 'Viva Mobile: Together for Every Goal' },
{ id: 'br-verde', text: 'Verde Market: Fresh for Match Day' },
],
},
];
const STATIC_ROUTES = {
'/': { file: './public/index.html', type: 'text/html; charset=utf-8' },
'/index.html': { file: './public/index.html', type: 'text/html; charset=utf-8' },
'/drift.html': { file: './public/drift.html', type: 'text/html; charset=utf-8' },
'/styles.css': { file: './public/styles.css', type: 'text/css; charset=utf-8' },
'/app.js': { file: './public/app.js', type: 'text/javascript; charset=utf-8' },
'/drift.js': { file: './public/drift.js', type: 'text/javascript; charset=utf-8' },
};
function sendJson(response, statusCode, payload) {
response.writeHead(statusCode, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
response.end(JSON.stringify(payload));
}
function decideAd(country, slotIndex) {
if (country.code === 'BR' && brazilFailureEnabled) {
throw new Error('Brazil ad decision unavailable');
}
const creative = country.creatives[slotIndex % country.creatives.length];
return {
countryCode: country.code,
countryName: country.name,
creativeId: creative.id,
creativeText: creative.text,
status: 'served',
reason: 'regional decision succeeded',
simulatedDecisionLatencyMs: country.latencyMs + (slotIndex % 3),
};
}
function safeDecision(country, slotIndex) {
try {
return decideAd(country, slotIndex);
} catch (error) {
return {
countryCode: country.code,
countryName: country.name,
creativeId: 'neutral-safety-board',
creativeText: 'International Football: Enjoy the Match',
status: 'fallback',
reason: error.message,
simulatedDecisionLatencyMs: country.latencyMs,
};
}
}
function buildBoardState() {
const serverTimestampMs = Date.now();
const eventElapsedMs = serverTimestampMs - eventStartMs;
const slotIndex = Math.floor(eventElapsedMs / SLOT_MS);
return {
serverTimestampMs,
eventStartMs,
eventElapsedMs,
slotIndex,
slotEndsAtMs: eventStartMs + (slotIndex + 1) * SLOT_MS,
slotDurationMs: SLOT_MS,
failureInjection: { countryCode: 'BR', enabled: brazilFailureEnabled },
decisions: COUNTRIES.map((country) => safeDecision(country, slotIndex)),
};
}
function serveStatic(response, route) {
fs.readFile(route.file, (error, data) => {
if (error) {
sendJson(response, 500, { error: 'Unable to read local asset' });
return;
}
response.writeHead(200, { 'Content-Type': route.type });
response.end(data);
});
}
const server = http.createServer((request, response) => {
if (request.method === 'GET' && request.url === '/api/board-state') {
sendJson(response, 200, buildBoardState());
return;
}
if (request.method === 'POST' && request.url === '/api/failures/br/toggle') {
brazilFailureEnabled = !brazilFailureEnabled;
sendJson(response, 200, { countryCode: 'BR', enabled: brazilFailureEnabled });
return;
}
const route = STATIC_ROUTES[request.url];
if (request.method === 'GET' && route) {
serveStatic(response, route);
return;
}
sendJson(response, 404, { error: 'Route not found' });
});
server.listen(PORT, () => {
console.log(`Virtual board simulator running at http://127.0.0.1:${PORT}/`);
});
Verify the regional bulkhead
The browser code from earlier already renders each returned decision independently. It also records each slot's country statuses and creative IDs in event-log.
Before you inject the failure, predict whether the United States, United Kingdom, and Japan cards keep their current slot.
- Return to the synchronized control room in Safari.
- Click Inject Brazil decision failure.
Brazil changes to International Football: Enjoy the Match with fallback status. The other three feeds retain served status on the same authoritative slot.
- Wait for the next five-second slot.
- Check the Latest control-plane event panel.
You'll see one event entry with the shared slot number. Its country results show Brazil using neutral-safety-board while the other creative IDs remain regional.
Why isolate each decision?
A shared outer error handler could replace every regional result after one exception. Calling safeDecision once per country keeps the failure boundary inside that country's decision.
The timeline remains shared because eventStartMs never changes. Failure isolation protects the output without splitting the event clock.
Brazil still shows a paid creative?
Confirm that the control reads Recover Brazil decision service. That label proves failure injection is enabled.
Check that the Brazil guard in decideAd uses the exact country code BR.
Help me trace why Brazil is not using the fallback.
Document the production design
The simulator proves the behavior locally. A production design must also separate regional decision work from viewer delivery through a CDN.
- Switch to the empty SYSTEM-DESIGN.md file from earlier.
- Add the scope and requirements by pasting this first section:
# Global Virtual Board System Design
## Scope
The product models one physical pitch-side advertising board that is replaced inside four broadcast outputs. Viewers in the United States, United Kingdom, Japan, and Brazil see localized creatives while the event timeline remains common. The prototype simulates decisioning, timing, output status, and failure isolation. It does not implement camera tracking, keying, video encoding, or CDN delivery.
## Requirements
### Functional
1. Assign one country-specific creative to each regional output.
2. Change all outputs on the same authoritative event slot.
3. Return a safe regional fallback when one ad decision fails.
4. Expose creative ID, region, slot, decision status, and simulated latency.
5. Let an operator inject and recover a Brazil-only decision failure.
### Non-functional
- A regional ad failure must not interrupt the other outputs.
- The timeline authority must return the same slot to all regional decisions in one state response.
- A missing paid creative must never expose an empty or broken board.
- Operators must be able to identify the failed region and selected fallback.
- Production delivery must scale separately from regional ad decisioning.
What does this section capture?
The scope draws a boundary around the prototype. The requirements turn its visible behavior into testable system promises.
- Save SYSTEM-DESIGN.md.
- Confirm that the file now shows separate functional and non-functional requirement sections.
Requirements in the wrong section?
Keep observable product capabilities under Functional. Keep resilience and scaling expectations under Non-functional.
Help me check the requirements structure.
- Add the local architecture below the requirements by pasting this section:
## Local architecture
```text
Safari control room
|
| GET /api/board-state
v
Node.js HTTP server
| authoritative eventStartMs and slotIndex
| regional creative catalog
| safeDecision boundary per country
| Brazil failure injector
|
v
Four simulated broadcast panels
```
The browser's one-second timer only asks for a refresh. The server's `eventElapsedMs` and `slotIndex` decide what every panel displays.
What does the local diagram prove?
The browser requests state from one server. The diagram makes the ownership of slot selection explicit.
- Save SYSTEM-DESIGN.md.
- Confirm that the local diagram ends with four simulated broadcast panels.
Diagram formatting looks uneven?
Keep the opening and closing text fences on their own lines. Preserve the spaces inside the diagram.
Help me repair the local architecture diagram.
- Add the production architecture below the local architecture by pasting this section:
## Production architecture
```text
Physical match and perimeter board
|
v
Clean world feed plus tracking or mask data
|
v
Authoritative event timeline and slot coordinator
|
+---------------- Control plane ----------------+
| |
Regional ad cell US Regional ad cell GB Regional ad cell JP Regional ad cell BR
| | | |
v v v v
Virtual compositor US Virtual compositor GB Virtual compositor JP Virtual compositor BR
| | | |
v v v v
Regional packager or distribution output for each market
|
v
CDN and broadcaster delivery to viewers
```
Each regional ad cell is a bulkhead. It owns its ad catalog replica, decision health, fallback state, and output telemetry. The shared timeline distributes a slot identifier, not a paid creative. This keeps one region's campaign failure from changing the other regions' decisions.
How does the production design scale?
One timeline coordinates the outputs. Separate regional cells contain campaign and compositor failures.
Completed regional outputs move into delivery infrastructure. Viewer traffic stays outside the ad-decision path.
- Save SYSTEM-DESIGN.md.
- Confirm that the production diagram shows four regional ad cells and four virtual compositors.
Production paths do not align?
Preserve the spacing between each regional cell and compositor. Keep the entire diagram inside one text fence.
Help me fix the production architecture diagram.
- Add the API contracts and capacity estimate by pasting this section:
## API contract
### GET /api/board-state
Returns `serverTimestampMs`, `eventStartMs`, `eventElapsedMs`, `slotIndex`, `failureInjection`, and one independently protected decision per country.
### POST /api/failures/br/toggle
Toggles the simulated Brazil ad-decision failure. It does not modify the shared event timeline or another country's decision state.
## Capacity estimate
Assumptions: 10,000,000 concurrent viewers, four regional outputs, one creative decision per output every five seconds, and the same creative for every viewer in one output.
```text
4 regional decisions / 5 seconds = 0.8 decisions per second
```
The ad decision path scales with regional outputs and slot frequency, not directly with viewer count. The packaging and CDN layers carry viewer-scale traffic. Per-viewer personalization would have a different request profile and caching strategy.
Why is the decision rate so small?
The design makes one decision for each regional output during a slot. Ten million viewers consume the completed outputs through the delivery layer.
This boundary prevents viewer count from becoming decision request count.
- Save SYSTEM-DESIGN.md.
- Confirm that the capacity section shows 0.8 decisions per second.
Capacity result does not match?
Use four regional decisions as the numerator. Use the five-second slot duration as the denominator.
Help me understand the capacity calculation.
- Add the failure matrix and observability fields by pasting this section:
## Failure matrix
| Failure | Local effect | Safe output | Isolation boundary | Recovery signal |
| --- | --- | --- | --- | --- |
| Brazil ad decision unavailable | Brazil loses its paid creative | Neutral safety board in Brazil | Brazil decision cell | Fresh successful Brazil decision |
| One compositor unhealthy | One regional overlay cannot be trusted | Clean world feed for that region | Regional compositor | Healthy render and operator check |
| Creative asset missing | One campaign cannot render | Approved neutral board | Creative and region | Asset validation succeeds |
| Timeline authority unavailable | Slot progression cannot be trusted | Hold briefly, then neutral policy | Shared timeline | Fresh slot from healthy authority |
| CDN or broadcaster path degraded | Viewers in one path are affected | Existing delivery fallback | Distribution path | Delivery health recovers |
## Observability
Log event ID, slot index, country code, output ID, creative ID, decision status, fallback reason, decision latency, timeouts, compositor health, slot skew, creative age, and operator actions. Alert on fallback rate, timeouts, missing assets, compositor bypass, and slot mismatch.
What does the matrix add?
Each row connects a failure to its blast radius. It also names the safe output and recovery evidence.
The observability fields give operators enough context to identify the affected boundary.
- Save SYSTEM-DESIGN.md.
- Confirm that the failure matrix contains five failure scenarios.
Table columns look broken?
Keep one pipe character between every cell. Keep the separator row directly below the header row.
Help me repair the failure matrix.
- Complete the design document by pasting this final section:
## Key trade-offs
### Four precomposed regional outputs
Best fit when everyone in a market sees the same board. It centralizes rendering, gives broadcasters consistent video, and keeps viewer clients simple. Its cost grows with the number of outputs and compositors.
### Per-viewer client overlay
It supports finer personalization with fewer precomposed feeds, but every player must render and synchronize correctly. Device variation, ad blockers, and client clock behavior increase operational risk.
### Per-viewer server-side ad insertion
It is purpose-built for personalized ad breaks and manifests. It is useful when each viewer receives a different inserted video ad, but it is not the same layer as replacing a board inside the match image. It also creates per-session manifest and targeting concerns that this four-output case does not need.
## Decision
Use four precomposed regional virtual-advertising outputs controlled by one authoritative event timeline. Isolate ad decisioning and compositing by region, keep the clean world feed available, and distribute each completed output through broadcaster and CDN infrastructure.
## Game-day degradation runbook
| Level | Trigger | Maximum age or duration | Output | Owner | Recovery gate |
| --- | --- | --- | --- | --- | --- |
| Last-known-good creative | To define | To define | To define | To define | To define |
| Neutral safety board | To define | To define | To define | To define | To define |
| Clean world feed | To define | To define | To define | To define | To define |
Why choose precomposed outputs?
The project needs one localized board for each market. Four precomposed outputs keep rendering under broadcast control.
Client overlays move synchronization risk onto viewer devices. Per-viewer server-side insertion solves a different problem around personalized video breaks.
- Save SYSTEM-DESIGN.md.
- Confirm that the final section selects four precomposed regional outputs.
- Confirm that the degradation runbook contains three policy rows marked To define.
Missing a trade-off?
Check for one subsection about precomposed outputs. Check for one subsection about client overlays.
Check for one subsection about server-side insertion. The decision must appear after all three.
Help me check the architecture trade-offs.
✔️ Awesome, I've got everything!
Your design now covers production flow, API contracts, capacity, failures, observability, and delivery trade-offs. Make sure SYSTEM-DESIGN.md is saved.
ⓧ I'd like to double check the full code
# Global Virtual Board System Design
## Scope
The product models one physical pitch-side advertising board that is replaced inside four broadcast outputs. Viewers in the United States, United Kingdom, Japan, and Brazil see localized creatives while the event timeline remains common. The prototype simulates decisioning, timing, output status, and failure isolation. It does not implement camera tracking, keying, video encoding, or CDN delivery.
## Requirements
### Functional
1. Assign one country-specific creative to each regional output.
2. Change all outputs on the same authoritative event slot.
3. Return a safe regional fallback when one ad decision fails.
4. Expose creative ID, region, slot, decision status, and simulated latency.
5. Let an operator inject and recover a Brazil-only decision failure.
### Non-functional
- A regional ad failure must not interrupt the other outputs.
- The timeline authority must return the same slot to all regional decisions in one state response.
- A missing paid creative must never expose an empty or broken board.
- Operators must be able to identify the failed region and selected fallback.
- Production delivery must scale separately from regional ad decisioning.
## Local architecture
```text
Safari control room
|
| GET /api/board-state
v
Node.js HTTP server
| authoritative eventStartMs and slotIndex
| regional creative catalog
| safeDecision boundary per country
| Brazil failure injector
|
v
Four simulated broadcast panels
```
The browser's one-second timer only asks for a refresh. The server's `eventElapsedMs` and `slotIndex` decide what every panel displays.
## Production architecture
```text
Physical match and perimeter board
|
v
Clean world feed plus tracking or mask data
|
v
Authoritative event timeline and slot coordinator
|
+---------------- Control plane ----------------+
| |
Regional ad cell US Regional ad cell GB Regional ad cell JP Regional ad cell BR
| | | |
v v v v
Virtual compositor US Virtual compositor GB Virtual compositor JP Virtual compositor BR
| | | |
v v v v
Regional packager or distribution output for each market
|
v
CDN and broadcaster delivery to viewers
```
Each regional ad cell is a bulkhead. It owns its ad catalog replica, decision health, fallback state, and output telemetry. The shared timeline distributes a slot identifier, not a paid creative. This keeps one region's campaign failure from changing the other regions' decisions.
## API contract
### GET /api/board-state
Returns `serverTimestampMs`, `eventStartMs`, `eventElapsedMs`, `slotIndex`, `failureInjection`, and one independently protected decision per country.
### POST /api/failures/br/toggle
Toggles the simulated Brazil ad-decision failure. It does not modify the shared event timeline or another country's decision state.
## Capacity estimate
Assumptions: 10,000,000 concurrent viewers, four regional outputs, one creative decision per output every five seconds, and the same creative for every viewer in one output.
```text
4 regional decisions / 5 seconds = 0.8 decisions per second
```
The ad decision path scales with regional outputs and slot frequency, not directly with viewer count. The packaging and CDN layers carry viewer-scale traffic. Per-viewer personalization would have a different request profile and caching strategy.
## Failure matrix
| Failure | Local effect | Safe output | Isolation boundary | Recovery signal |
| --- | --- | --- | --- | --- |
| Brazil ad decision unavailable | Brazil loses its paid creative | Neutral safety board in Brazil | Brazil decision cell | Fresh successful Brazil decision |
| One compositor unhealthy | One regional overlay cannot be trusted | Clean world feed for that region | Regional compositor | Healthy render and operator check |
| Creative asset missing | One campaign cannot render | Approved neutral board | Creative and region | Asset validation succeeds |
| Timeline authority unavailable | Slot progression cannot be trusted | Hold briefly, then neutral policy | Shared timeline | Fresh slot from healthy authority |
| CDN or broadcaster path degraded | Viewers in one path are affected | Existing delivery fallback | Distribution path | Delivery health recovers |
## Observability
Log event ID, slot index, country code, output ID, creative ID, decision status, fallback reason, decision latency, timeouts, compositor health, slot skew, creative age, and operator actions. Alert on fallback rate, timeouts, missing assets, compositor bypass, and slot mismatch.
## Key trade-offs
### Four precomposed regional outputs
Best fit when everyone in a market sees the same board. It centralizes rendering, gives broadcasters consistent video, and keeps viewer clients simple. Its cost grows with the number of outputs and compositors.
### Per-viewer client overlay
It supports finer personalization with fewer precomposed feeds, but every player must render and synchronize correctly. Device variation, ad blockers, and client clock behavior increase operational risk.
### Per-viewer server-side ad insertion
It is purpose-built for personalized ad breaks and manifests. It is useful when each viewer receives a different inserted video ad, but it is not the same layer as replacing a board inside the match image. It also creates per-session manifest and targeting concerns that this four-output case does not need.
## Decision
Use four precomposed regional virtual-advertising outputs controlled by one authoritative event timeline. Isolate ad decisioning and compositing by region, keep the clean world feed available, and distribute each completed output through broadcaster and CDN infrastructure.
## Game-day degradation runbook
| Level | Trigger | Maximum age or duration | Output | Owner | Recovery gate |
| --- | --- | --- | --- | --- | --- |
| Last-known-good creative | To define | To define | To define | To define | To define |
| Neutral safety board | To define | To define | To define | To define | To define |
| Clean world feed | To define | To define | To define | To define | To define |
Before the final check, predict whether all four cards still advance to one shared slot during the Brazil failure.
- Return to the synchronized control room in Safari.
- Click Recover Brazil decision service if the Brazil failure is currently enabled.
- Click Inject Brazil decision failure.
- Wait for one slot change.
You'll see Brazil display International Football: Enjoy the Match with fallback status. The United States, United Kingdom, and Japan remain served on the same slot number.
That's the containment proof complete. One regional decision can now fail without taking the synchronized broadcast outputs with it.
Secret mission
Write the Game-Day Degradation Runbook
Brazil's neutral board protects the broadcast during a decision failure. Turn that behavior into a game-day policy for stale creatives, neutral output, and clean-feed recovery.
Clean Up Your Resources
Clean Up Your Resources
This simulator runs entirely on your Mac with no ongoing costs. Choose whether to keep it running, pause the server, or delete the workspace.
Resources you used:
- A running Node.js server at http://127.0.0.1:3000.
- The local virtual-board-simulator workspace containing the simulator files.
Keep everything running
No action is needed. Choose this option while you are testing the regional failure behavior or reviewing the completed runbook.
- Leave the local server running while you continue using the control room.
- Keep the virtual-board-simulator workspace as a reference for synchronization logic.
- Use SYSTEM-DESIGN.md when you revisit the game-day degradation policy.
- Return to http://127.0.0.1:3000 in Safari when you want to test another Brazil failure.
Pause - I'll come back to this later
Pausing stops local server activity while preserving every project file for your next session.
- Switch back to the Visual Studio Code terminal where the server is running.
- Press Ctrl+C to stop the server.
- Keep the virtual-board-simulator workspace on your Mac.
That completes the pause. Your simulator files remain ready for another game-day test.
Delete - I don't want to use this again
Remove the server process plus the local workspace when you no longer need the simulator.
Deletion is permanent
Deleting the folder permanently removes the simulator files. The command below targets only the virtual-board-simulator workspace from its parent folder.
Node.js remains installed. Visual Studio Code remains installed.
- Switch back to the Visual Studio Code terminal where the server is running.
- Press Ctrl+C to stop the server.
- Remove the virtual-board-simulator workspace by running these commands:
cd ..
rm -rf virtual-board-simulator
ls
What Do These Commands Do?
- The first line moves the terminal to the folder containing the workspace.
- The second line permanently deletes the simulator folder plus every file inside it.
- The final line lists the remaining folders so you can verify the deletion.
The output should no longer include virtual-board-simulator.
Still See the Workspace?
- Confirm that the terminal moved to the folder containing virtual-board-simulator.
- Compare the visible folder name with virtual-board-simulator before trying the cleanup again.
Help me safely diagnose the remaining workspace.
Your local lab is now removed. Your development tools remain available for future projects.
Nice Work!
Nice Work!
You did it! Your browser-based broadcast control room now shows four localized virtual boards over one shared match timeline. Its Brazil-only failure test proves the remaining feeds can stay synchronized and served.
What you learned:
- Built four localized virtual advertising feeds that display country-specific creative IDs over the same stadium view.
- Demonstrated timer drift by running four independent browser timers. Replaced local slot selection with one authoritative Node.js event clock.
- Applied regional fault isolation so a Brazil ad-decision failure produces a neutral fallback without disrupting the other feeds. Documented the API contracts, capacity assumptions, observability fields, failure boundaries, and delivery trade-offs.
- Secret Mission: Wrote and tested a game-day degradation runbook with a 10-second maximum age for stale creatives. Defined neutral-board and clean-world-feed triggers. Added clear ownership and recovery gates before paid advertising resumes.
Ready to quiz yourself?